Skip to content

Commit

Permalink
Implemented card permissions as SwitchEntities
Browse files Browse the repository at this point in the history
  • Loading branch information
twystd committed Jan 12, 2024
1 parent c1b3fd3 commit 44d8af2
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 24 deletions.
12 changes: 5 additions & 7 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ KeyboardInterrupt

- [ ] Config flow
- [ ] Cards
- [x] generate unique ID
- [x] defer start/end dates and permissions to UI
- [ ] configure in pages of 4 or 5 cards
- [ ] Ad hoc controllers
- [ ] From configuration.yaml
Expand All @@ -33,12 +31,14 @@ KeyboardInterrupt


- [ ] Controller
- [ ] generate unique id in config-/option-flow
- [ ] Rework as Device
- [ ] device_info
- [ ] entity_category
- [ ] Set default timezone in configuration.yaml

- [ ] Doors
- [ ] generate unique id in config-/option-flow
- [x] Rework opened as EventEntity
- [ ] Rework button as EventEntity
- [ ] Rework unlocked as EventEntiy
Expand All @@ -55,15 +55,13 @@ KeyboardInterrupt

- [ ] Cards
- [x] unique id should allow sharing across uhppoteds
- [ ] set max cards in _configuration.yaml_
- [ ] set preferred cards in _configuration.yaml_
- [x] sensor:CardInfo
- [x] Card 'state'- ok, not valid, expired, no access, inconsistent
- [x] Update state from controllers
- [x] sensor:CardHolder
- [x] datetime:StartDate
- [x] datetime:EndDate
- [ ] permissions
- [x] permissions
- [ ] set max cards in _configuration.yaml_
- [ ] set preferred cards in _configuration.yaml_
- [ ] PIN
- [x] Only set cardholder in config-flow
- [ ] Only set cardholder and (maybe) PIN in options-flow
Expand Down
2 changes: 2 additions & 0 deletions custom_components/uhppoted/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, "button"))
hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, "event"))
hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, "date"))
hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, "switch"))

entry.async_on_unload(entry.add_update_listener(update_listener))

Expand All @@ -62,6 +63,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Platform.BUTTON,
Platform.EVENT,
Platform.DATE,
Platform.SWITCH,
]

# TODO pre-unload cleanup (if any)
Expand Down
175 changes: 169 additions & 6 deletions custom_components/uhppoted/card.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,21 @@

from homeassistant.components.sensor import SensorEntity
from homeassistant.components.date import DateEntity
from homeassistant.components.switch import SwitchEntity

from .const import ATTR_CONTROLLER_SERIAL_NUMBER
from .const import ATTR_DOOR_CONTROLLER
from .const import ATTR_DOOR_NUMBER
from .const import ATTR_CARD_HOLDER
from .const import ATTR_CARD_STARTDATE
from .const import ATTR_CARD_ENDDATE
from .const import ATTR_CARD_PERMISSIONS

from .const import CONF_DOOR_ID
from .const import CONF_DOOR_NUMBER
from .const import CONF_DOOR_CONTROLLER
from .const import CONF_CONTROLLER_SERIAL_NUMBER

from .config import default_card_start_date
from .config import default_card_end_date

Expand Down Expand Up @@ -154,7 +163,7 @@ async def async_update(self):


class CardStartDate(DateEntity):
_attr_icon = 'mdi:calendar-clock-outline'
_attr_icon = 'mdi:card-account-details'
_attr_has_entity_name: True

def __init__(self, u, card, name, unique_id):
Expand Down Expand Up @@ -210,8 +219,9 @@ async def async_set_value(self, v: datetime.date) -> None:
PIN = response.pin

response = self.driver['api'].put_card(controller, card, start, end, door1, door2, door3, door4, PIN)

if not response.stored:
if response.stored:
_LOGGER.info(f'controller {controller}: card {self.card} start date updated')
else:
_LOGGER.warning(f'controller {controller}: card {self.card} start date not updated')
self._available = False

Expand Down Expand Up @@ -240,7 +250,7 @@ async def async_update(self):


class CardEndDate(DateEntity):
_attr_icon = 'mdi:calendar-clock-outline'
_attr_icon = 'mdi:card-account-details'
_attr_has_entity_name: True

def __init__(self, u, card, name, unique_id):
Expand Down Expand Up @@ -296,8 +306,9 @@ async def async_set_value(self, v: datetime.date) -> None:
PIN = response.pin

response = self.driver['api'].put_card(controller, card, start, end, door1, door2, door3, door4, PIN)

if not response.stored:
if response.stored:
_LOGGER.info(f'controller {controller}: card {self.card} end date updated')
else:
_LOGGER.warning(f'controller {controller}: card {self.card} end date not updated')
self._available = False

Expand All @@ -323,3 +334,155 @@ async def async_update(self):
except (Exception):
self._available = False
_LOGGER.exception(f'error retrieving card {self.cared} end date')


class CardPermission(SwitchEntity):
_attr_icon = 'mdi:card-account-details'
_attr_has_entity_name: True

def __init__(self, u, card, name, door, unique_id):
super().__init__()

_LOGGER.debug(f'card {card} permission for door {door[CONF_DOOR_ID]}')

self.driver = u
self.card = int(f'{card}')
self.door = door

self._unique_id = unique_id
self._name = f'uhppoted.card.{card}.{door[CONF_DOOR_ID]}'.lower()
self._allowed = None
self._available = False
self._attributes: Dict[str, Any] = {}

@property
def unique_id(self) -> str:
return f'uhppoted.card.{self._unique_id}.{self.door[CONF_DOOR_ID]}'.lower()

@property
def name(self) -> str:
return self._name

@property
def available(self) -> bool:
return self._available

@property
def is_on(self):
if self._available:
return self._allowed

return None

@property
def extra_state_attributes(self) -> Dict[str, Any]:
return {
ATTR_DOOR_CONTROLLER: self.door[CONF_DOOR_CONTROLLER],
ATTR_CONTROLLER_SERIAL_NUMBER: self.door[CONF_CONTROLLER_SERIAL_NUMBER],
ATTR_DOOR_NUMBER: self.door[CONF_DOOR_NUMBER],
}

async def async_turn_on(self, **kwargs):
_LOGGER.debug(f'card:{self.card} enable access for door {self.door[CONF_DOOR_ID]}')
try:
controller = int(f'{self.door[CONF_CONTROLLER_SERIAL_NUMBER]}')
door = int(f'{self.door[CONF_DOOR_NUMBER]}')
card = self.card

start = default_card_start_date()
end = default_card_end_date()
door1 = 1 if door == 1 else 0
door2 = 1 if door == 2 else 0
door3 = 1 if door == 3 else 0
door4 = 1 if door == 4 else 0
PIN = 0

response = self.driver['api'].get_card(controller, self.card)
if response.controller == controller and response.card_number == self.card:
start = response.start_date
end = response.start_date
door1 = 1 if door == 1 else response.door_1
door2 = 1 if door == 2 else response.door_2
door3 = 1 if door == 3 else response.door_3
door4 = 1 if door == 4 else response.door_4
PIN = response.pin

response = self.driver['api'].put_card(controller, card, start, end, door1, door2, door3, door4, PIN)
if response.stored:
_LOGGER.info(
f'controller {self.door[CONF_DOOR_CONTROLLER]}, card {self.card} door {self.door[CONF_DOOR_ID]} permission updated'
)
else:
_LOGGER.warning(
f'controller {self.door[CONF_DOOR_CONTROLLER]}, card {self.card} door {self.door[CONF_DOOR_ID]} permission not updated'
)
self._available = False

except (Exception):
self._available = False
_LOGGER.exception(f'error updating card {self.card} access for door {self.door[CONF_DOOR_ID]}')

async def async_turn_off(self, **kwargs):
_LOGGER.debug(f'card:{self.card} remove access for door {self.door[CONF_DOOR_ID]}')
try:
controller = int(f'{self.door[CONF_CONTROLLER_SERIAL_NUMBER]}')
door = int(f'{self.door[CONF_DOOR_NUMBER]}')
card = self.card

start = default_card_start_date()
end = default_card_end_date()
door1 = 1 if door == 1 else 0
door2 = 1 if door == 2 else 0
door3 = 1 if door == 3 else 0
door4 = 1 if door == 4 else 0
PIN = 0

response = self.driver['api'].get_card(controller, self.card)
if response.controller == controller and response.card_number == self.card:
start = response.start_date
end = response.start_date
door1 = 0 if door == 1 else response.door_1
door2 = 0 if door == 2 else response.door_2
door3 = 0 if door == 3 else response.door_3
door4 = 0 if door == 4 else response.door_4
PIN = response.pin

response = self.driver['api'].put_card(controller, card, start, end, door1, door2, door3, door4, PIN)
if response.stored:
_LOGGER.info(
f'controller {self.door[CONF_DOOR_CONTROLLER]}, card {self.card} door {self.door[CONF_DOOR_ID]} permission updated'
)
else:
_LOGGER.warning(
f'controller {self.door[CONF_DOOR_CONTROLLER]}, card {self.card} door {self.door[CONF_DOOR_ID]} permission not updated'
)
self._available = False

except (Exception):
self._available = False
_LOGGER.exception(f'error updating card {self.card} access for door {self.door[CONF_DOOR_ID]}')

async def async_update(self):
_LOGGER.debug(f'card:{self.card} update door {self.door[CONF_DOOR_ID]} access')
try:
controller = int(f'{self.door[CONF_CONTROLLER_SERIAL_NUMBER]}')
door = int(f'{self.door[CONF_DOOR_NUMBER]}')
response = self.driver['api'].get_card(controller, self.card)

if response.controller == controller and response.card_number == self.card:
if door == 1:
self._allowed = response.door_1 != 0
elif door == 2:
self._allowed = response.door_2 != 0
elif door == 3:
self._allowed = response.door_3 != 0
elif door == 4:
self._allowed = response.door_4 != 0
else:
self._allowed = False

self._available = True

except (Exception):
self._available = False
_LOGGER.exception(f'error updating card {self.card} access for door {self.door}')
5 changes: 3 additions & 2 deletions custom_components/uhppoted/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,14 @@ def get_all_cards(options):
# ... add cards from options
if options and CONF_CARDS in options:
for v in options[CONF_CARDS]:
k = v[CONF_CARD_NUMBER]
k = int(f'{v[CONF_CARD_NUMBER]}')
cards[k] = v

# ... convert cards list to records

return [cards[k] for k in sorted(cards.keys())]


def get_card(card_number, options):
if options and CONF_CARDS in options:
for v in options[CONF_CARDS]:
Expand All @@ -263,6 +264,7 @@ def get_card(card_number, options):
CONF_CARD_NAME: f'{card_number}',
}


def configure_controllers(options, f):
if CONF_CONTROLLERS in options:
controllers = options[CONF_CONTROLLERS]
Expand Down Expand Up @@ -297,7 +299,6 @@ def configure_doors(options, g):
def configure_cards(options, f):
if CONF_CARDS in options:
cards = options[CONF_CARDS]

for c in cards:
card = f'{c[CONF_CARD_NUMBER]}'.strip()
name = f'{c[CONF_CARD_NAME]}'.strip()
Expand Down
3 changes: 2 additions & 1 deletion custom_components/uhppoted/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,8 @@ def f(v):
errors[CONF_CARD_NAME] = f'{err}'

if not errors:
v = []
v = self.options[CONF_CARDS] if CONF_CARDS in self.options else []

v.append({
CONF_CARD_NUMBER: card,
CONF_CARD_NAME: user_input[CONF_CARD_NAME],
Expand Down
2 changes: 2 additions & 0 deletions custom_components/uhppoted/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
ATTR_GATEWAY = 'gateway'
ATTR_FIRMWARE = 'firmware'

ATTR_CONTROLLER_SERIAL_NUMBER = 'serial_no'

ATTR_DOOR_CONTROLLER = 'controller'
ATTR_DOOR_NUMBER = 'door'

Expand Down
22 changes: 14 additions & 8 deletions custom_components/uhppoted/options_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ def g(c):
return await self.async_step_card()

cards = get_all_cards(self.options)
defaults = [f'{v[CONF_CARD_NUMBER]}' for v in self.options[CONF_CARDS]]
defaults = [f'{v[CONF_CARD_NUMBER]}' for v in self.options[CONF_CARDS]] if CONF_CARDS in self.options else []

# if len(cards) < 2:
# self.configuration['cards'] = [{
Expand All @@ -452,6 +452,7 @@ def g(c):
return self.async_show_form(step_id="cards", data_schema=schema, errors=errors)

async def async_step_card(self, user_input: Optional[Dict[str, Any]] = None):

def f(v):
return not v['configured']

Expand All @@ -477,12 +478,18 @@ def f(v):
errors[CONF_CARD_NAME] = f'{err}'

if not errors:
v = []
v.append({
CONF_CARD_NUMBER: card,
CONF_CARD_NAME: user_input[CONF_CARD_NAME],
CONF_CARD_UNIQUE_ID: unique_id,
})
v = self.options[CONF_CARDS] if CONF_CARDS in self.options else []

for c in v:
if int(f'{c[CONF_CARD_NUMBER]}') == int(f'{card}'):
c[CONF_CARD_NAME] = user_input[CONF_CARD_NAME]
break
else:
v.append({
CONF_CARD_NUMBER: card,
CONF_CARD_NAME: user_input[CONF_CARD_NAME],
CONF_CARD_UNIQUE_ID: unique_id,
})

self.options.update({CONF_CARDS: v})
it['configured'] = True
Expand Down Expand Up @@ -511,4 +518,3 @@ def f(v):
data_schema=schema,
errors=errors,
description_placeholders=placeholders)

Loading

0 comments on commit 44d8af2

Please sign in to comment.