From 394f1788f4b32caf7e4e6fb2afac627389d11b0c Mon Sep 17 00:00:00 2001 From: Snuffy2 Date: Fri, 27 Dec 2024 21:03:09 -0500 Subject: [PATCH 1/4] Fix config_flow --- custom_components/keymaster/config_flow.py | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/custom_components/keymaster/config_flow.py b/custom_components/keymaster/config_flow.py index e2b080e7..f54a68aa 100644 --- a/custom_components/keymaster/config_flow.py +++ b/custom_components/keymaster/config_flow.py @@ -9,12 +9,16 @@ import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.binary_sensor import DOMAIN as BINARY_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import ConfigFlowResult +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify @@ -45,11 +49,9 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) -class KeymasterFlowHandler(config_entries.ConfigFlow): +class KeymasterConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for keymaster.""" - domain: str = DOMAIN - VERSION: int = 3 DEFAULTS: MutableMapping[str, Any] = { CONF_SLOTS: DEFAULT_CODE_SLOTS, @@ -85,16 +87,16 @@ async def async_step_user( @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> KeymasterOptionsFlow: """Get the options flow for this handler.""" return KeymasterOptionsFlow(config_entry) -class KeymasterOptionsFlow(config_entries.OptionsFlow): +class KeymasterOptionsFlow(OptionsFlow): """Options flow for keymaster.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry @@ -310,7 +312,7 @@ def _get_default(key: str, fallback_default: Any = None) -> Any: async def _start_config_flow( - cls: KeymasterFlowHandler | KeymasterOptionsFlow, + cls: KeymasterConfigFlow | KeymasterOptionsFlow, step_id: str, title: str, user_input: MutableMapping[str, Any] | None, @@ -345,9 +347,8 @@ async def _start_config_flow( step_id, user_input, ) - if step_id == "user" or not entry_id: + if isinstance(cls, KeymasterConfigFlow) or step_id == "user" or not entry_id: return cls.async_create_entry(title=title, data=user_input) - assert isinstance(cls, KeymasterOptionsFlow) cls.hass.config_entries.async_update_entry( cls.config_entry, data=user_input ) From ab493a6ee3c861805de0080bd0d0682889f49178 Mon Sep 17 00:00:00 2001 From: Snuffy2 Date: Fri, 27 Dec 2024 21:30:48 -0500 Subject: [PATCH 2/4] Remove assertions --- custom_components/keymaster/coordinator.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/custom_components/keymaster/coordinator.py b/custom_components/keymaster/coordinator.py index 43fb7e1b..ef5af38b 100644 --- a/custom_components/keymaster/coordinator.py +++ b/custom_components/keymaster/coordinator.py @@ -230,8 +230,9 @@ def _dict_to_kmlocks(self, data: dict, cls: type) -> Any: for field in fields(cls): field_name: str = field.name - assert isinstance(field.type, type) - field_type: type = keymasterlock_type_lookup.get(field_name, field.type) + field_type: type | None = keymasterlock_type_lookup.get(field_name) + if not field_type and isinstance(field.type, type): + field_type = field.type field_value: Any = data.get(field_name) @@ -1618,7 +1619,8 @@ async def _connect_and_update_lock(self, kmlock: KeymasterLock) -> bool: return False kmlock.lock_config_entry_id = lock_ent_reg_entry.config_entry_id - assert kmlock.lock_config_entry_id is not None + if kmlock.lock_config_entry_id is None: + return False try: zwave_entry = self.hass.config_entries.async_get_entry( kmlock.lock_config_entry_id From 7fbaae5258390c9947cced2abd375d3c479bc1b2 Mon Sep 17 00:00:00 2001 From: Snuffy2 Date: Fri, 27 Dec 2024 21:44:06 -0500 Subject: [PATCH 3/4] Initialize code_slot and dow to None --- custom_components/keymaster/entity.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/keymaster/entity.py b/custom_components/keymaster/entity.py index d98331c2..eefb89fc 100644 --- a/custom_components/keymaster/entity.py +++ b/custom_components/keymaster/entity.py @@ -49,10 +49,12 @@ def __init__(self, entity_description: KeymasterEntityDescription) -> None: f"{self._config_entry.entry_id}_{slugify(self._property)}" ) # _LOGGER.debug(f"[Entity init] self._property: {self._property}, unique_id: {self.unique_id}") + self._code_slot: None | int = None if ".code_slots" in self._property: - self._code_slot: None | int = self._get_code_slots_num() + self._code_slot = self._get_code_slots_num() + self._day_of_week_num: None | int = None if "accesslimit_day_of_week" in self._property: - self._day_of_week_num: None | int = self._get_day_of_week_num() + self._day_of_week_num = self._get_day_of_week_num() self._attr_extra_state_attributes: dict[str, Any] = {} self._attr_device_info: DeviceInfo = { "identifiers": {(DOMAIN, self._config_entry.entry_id)}, From 655a47b7271de7d364be3ecab422d0a930b99bc4 Mon Sep 17 00:00:00 2001 From: Snuffy2 Date: Sat, 28 Dec 2024 13:47:49 -0500 Subject: [PATCH 4/4] Update ruff for Py3.13 and Fix/Format --- custom_components/keymaster/__init__.py | 33 +-- custom_components/keymaster/binary_sensor.py | 4 +- custom_components/keymaster/button.py | 7 +- custom_components/keymaster/config_flow.py | 41 ++- custom_components/keymaster/coordinator.py | 261 +++++++------------ custom_components/keymaster/datetime.py | 78 +++--- custom_components/keymaster/entity.py | 13 +- custom_components/keymaster/exceptions.py | 5 +- custom_components/keymaster/helpers.py | 60 +---- custom_components/keymaster/lovelace.py | 51 ++-- custom_components/keymaster/migrate.py | 30 +-- custom_components/keymaster/number.py | 34 ++- custom_components/keymaster/sensor.py | 17 +- custom_components/keymaster/switch.py | 61 ++--- custom_components/keymaster/text.py | 74 +++--- custom_components/keymaster/time.py | 58 +++-- pyproject.toml | 15 ++ tests/common.py | 4 +- tests/conftest.py | 17 +- tests/test_config_flow.py | 8 +- 20 files changed, 372 insertions(+), 499 deletions(-) diff --git a/custom_components/keymaster/__init__.py b/custom_components/keymaster/__init__.py index 9612d2f3..72cfa03d 100644 --- a/custom_components/keymaster/__init__.py +++ b/custom_components/keymaster/__init__.py @@ -76,24 +76,18 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b updated_config[CONF_NOTIFY_SCRIPT_NAME] = ( f"keymaster_{updated_config.get(CONF_LOCK_NAME)}_manual_notify" ) - elif isinstance( - updated_config.get(CONF_NOTIFY_SCRIPT_NAME), str - ) and updated_config[CONF_NOTIFY_SCRIPT_NAME].startswith("script."): - updated_config[CONF_NOTIFY_SCRIPT_NAME] = updated_config[ - CONF_NOTIFY_SCRIPT_NAME - ].split(".", maxsplit=1)[1] + elif isinstance(updated_config.get(CONF_NOTIFY_SCRIPT_NAME), str) and updated_config[ + CONF_NOTIFY_SCRIPT_NAME + ].startswith("script."): + updated_config[CONF_NOTIFY_SCRIPT_NAME] = updated_config[CONF_NOTIFY_SCRIPT_NAME].split( + ".", maxsplit=1 + )[1] if updated_config.get(CONF_DOOR_SENSOR_ENTITY_ID) == DEFAULT_DOOR_SENSOR: updated_config[CONF_DOOR_SENSOR_ENTITY_ID] = None - if ( - updated_config.get(CONF_ALARM_LEVEL_OR_USER_CODE_ENTITY_ID) - == DEFAULT_ALARM_LEVEL_SENSOR - ): + if updated_config.get(CONF_ALARM_LEVEL_OR_USER_CODE_ENTITY_ID) == DEFAULT_ALARM_LEVEL_SENSOR: updated_config[CONF_ALARM_LEVEL_OR_USER_CODE_ENTITY_ID] = None - if ( - updated_config.get(CONF_ALARM_TYPE_OR_ACCESS_CONTROL_ENTITY_ID) - == DEFAULT_ALARM_TYPE_SENSOR - ): + if updated_config.get(CONF_ALARM_TYPE_OR_ACCESS_CONTROL_ENTITY_ID) == DEFAULT_ALARM_TYPE_SENSOR: updated_config[CONF_ALARM_TYPE_OR_ACCESS_CONTROL_ENTITY_ID] = None if updated_config != config_entry.data: @@ -153,9 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b "Sunday", ] ): - dow_slots[i] = KeymasterCodeSlotDayOfWeek( - day_of_week_num=i, day_of_week_name=dow - ) + dow_slots[i] = KeymasterCodeSlotDayOfWeek(day_of_week_num=i, day_of_week_name=dow) code_slots[x] = KeymasterCodeSlot(number=x, accesslimit_day_of_week=dow_slots) kmlock = KeymasterLock( @@ -225,8 +217,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> if coordinator.count_locks_not_pending_delete == 0: _LOGGER.debug( - "[async_unload_entry] Possibly empty coordinator. " - "Will evaluate for removal at %s", + "[async_unload_entry] Possibly empty coordinator. Will evaluate for removal at %s", datetime.now().astimezone() + timedelta(seconds=20), ) async_call_later( @@ -265,9 +256,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> data = config_entry.data.copy() data[CONF_ALARM_LEVEL_OR_USER_CODE_ENTITY_ID] = data.pop(CONF_ALARM_LEVEL, None) - data[CONF_ALARM_TYPE_OR_ACCESS_CONTROL_ENTITY_ID] = data.pop( - CONF_ALARM_TYPE, None - ) + data[CONF_ALARM_TYPE_OR_ACCESS_CONTROL_ENTITY_ID] = data.pop(CONF_ALARM_TYPE, None) data[CONF_LOCK_ENTITY_ID] = data.pop(CONF_ENTITY_ID) if CONF_HIDE_PINS not in data: data[CONF_HIDE_PINS] = DEFAULT_HIDE_PINS diff --git a/custom_components/keymaster/binary_sensor.py b/custom_components/keymaster/binary_sensor.py index 08ebb479..c956736a 100644 --- a/custom_components/keymaster/binary_sensor.py +++ b/custom_components/keymaster/binary_sensor.py @@ -28,9 +28,7 @@ async def async_setup_entry( ): """Create the keymaster Binary Sensors.""" coordinator: KeymasterCoordinator = hass.data[DOMAIN][COORDINATOR] - kmlock = await coordinator.get_lock_by_config_entry_id( - config_entry.entry_id - ) + kmlock = await coordinator.get_lock_by_config_entry_id(config_entry.entry_id) entities: list = [] if async_using_zwave_js(hass=hass, kmlock=kmlock): entities.append( diff --git a/custom_components/keymaster/button.py b/custom_components/keymaster/button.py index 50f6bdb7..d14ac8bc 100644 --- a/custom_components/keymaster/button.py +++ b/custom_components/keymaster/button.py @@ -60,9 +60,7 @@ async def async_setup_entry( @dataclass(frozen=True, kw_only=True) -class KeymasterButtonEntityDescription( - KeymasterEntityDescription, ButtonEntityDescription -): +class KeymasterButtonEntityDescription(KeymasterEntityDescription, ButtonEntityDescription): """Entity Description for Keymaster Buttons.""" @@ -89,7 +87,8 @@ def _handle_coordinator_update(self) -> None: return if ( - ".code_slots" in self._property and isinstance(self._kmlock.code_slots, MutableMapping) + ".code_slots" in self._property + and isinstance(self._kmlock.code_slots, MutableMapping) and self._code_slot not in self._kmlock.code_slots ): self._attr_available = False diff --git a/custom_components/keymaster/config_flow.py b/custom_components/keymaster/config_flow.py index f54a68aa..07faa29b 100644 --- a/custom_components/keymaster/config_flow.py +++ b/custom_components/keymaster/config_flow.py @@ -13,12 +13,7 @@ from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import ( - ConfigEntry, - ConfigFlow, - ConfigFlowResult, - OptionsFlow, -) +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify @@ -212,15 +207,11 @@ def _get_default(key: str, fallback_default: Any = None) -> Any: script_default: str | None = _get_default(CONF_NOTIFY_SCRIPT_NAME) if isinstance(script_default, str) and not script_default.startswith("script."): script_default = f"script.{script_default}" - _LOGGER.debug( - "[get_schema] script_default: %s (%s)", script_default, type(script_default) - ) + _LOGGER.debug("[get_schema] script_default: %s (%s)", script_default, type(script_default)) schema = vol.Schema( { vol.Required(CONF_LOCK_NAME, default=_get_default(CONF_LOCK_NAME)): str, - vol.Required( - CONF_LOCK_ENTITY_ID, default=_get_default(CONF_LOCK_ENTITY_ID) - ): vol.In( + vol.Required(CONF_LOCK_ENTITY_ID, default=_get_default(CONF_LOCK_ENTITY_ID)): vol.In( _get_entities( hass=hass, domain=LOCK_DOMAIN, @@ -229,15 +220,15 @@ def _get_default(key: str, fallback_default: Any = None) -> Any: ), ) ), - vol.Optional( - CONF_PARENT, default=_get_default(CONF_PARENT, "(none)") - ): vol.In(_available_parent_locks(hass, entry_id)), - vol.Required( - CONF_SLOTS, default=_get_default(CONF_SLOTS, DEFAULT_CODE_SLOTS) - ): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Required( - CONF_START, default=_get_default(CONF_START, DEFAULT_START) - ): vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional(CONF_PARENT, default=_get_default(CONF_PARENT, "(none)")): vol.In( + _available_parent_locks(hass, entry_id) + ), + vol.Required(CONF_SLOTS, default=_get_default(CONF_SLOTS, DEFAULT_CODE_SLOTS)): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Required(CONF_START, default=_get_default(CONF_START, DEFAULT_START)): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), vol.Optional( CONF_DOOR_SENSOR_ENTITY_ID, default=_get_default(CONF_DOOR_SENSOR_ENTITY_ID, DEFAULT_DOOR_SENSOR), @@ -349,15 +340,15 @@ async def _start_config_flow( ) if isinstance(cls, KeymasterConfigFlow) or step_id == "user" or not entry_id: return cls.async_create_entry(title=title, data=user_input) - cls.hass.config_entries.async_update_entry( - cls.config_entry, data=user_input - ) + cls.hass.config_entries.async_update_entry(cls.config_entry, data=user_input) await cls.hass.config_entries.async_reload(entry_id) return cls.async_create_entry(title="", data={}) return cls.async_show_form( step_id=step_id, - data_schema=_get_schema(hass=cls.hass, user_input=user_input, default_dict=defaults, entry_id=entry_id), + data_schema=_get_schema( + hass=cls.hass, user_input=user_input, default_dict=defaults, entry_id=entry_id + ), errors=errors, description_placeholders=description_placeholders, ) diff --git a/custom_components/keymaster/coordinator.py b/custom_components/keymaster/coordinator.py index ef5af38b..fd581ae0 100644 --- a/custom_components/keymaster/coordinator.py +++ b/custom_components/keymaster/coordinator.py @@ -118,9 +118,7 @@ def __init__(self, hass: HomeAssistant) -> None: update_interval=timedelta(seconds=60), config_entry=None, ) - self._json_folder: str = self.hass.config.path( - "custom_components", DOMAIN, "json_kmlocks" - ) + self._json_folder: str = self.hass.config.path("custom_components", DOMAIN, "json_kmlocks") self._json_filename: str = f"{DOMAIN}_kmlocks.json" async def initial_setup(self) -> None: @@ -135,9 +133,7 @@ async def _async_setup(self) -> None: ) await self.hass.async_add_executor_job(self._create_json_folder) - imported_config = await self.hass.async_add_executor_job( - self._get_dict_from_json_file - ) + imported_config = await self.hass.async_add_executor_job(self._get_dict_from_json_file) _LOGGER.debug("[Coordinator] Imported %s keymaster locks", len(imported_config)) self.kmlocks = imported_config @@ -203,8 +199,7 @@ def _get_dict_from_json_file(self) -> MutableMapping: # _LOGGER.debug(f"[get_dict_from_json_file] Imported JSON: {config}") kmlocks: MutableMapping = { - key: self._dict_to_kmlocks(value, KeymasterLock) - for key, value in config.items() + key: self._dict_to_kmlocks(value, KeymasterLock) for key, value in config.items() } _LOGGER.debug("[get_dict_from_json_file] Imported kmlocks: %s", kmlocks) @@ -294,9 +289,7 @@ def _dict_to_kmlocks(self, data: dict, cls: type) -> Any: field_value = { ( int(k) - if key_type is int - and isinstance(k, str) - and k.isdigit() + if key_type is int and isinstance(k, str) and k.isdigit() else k ): self._dict_to_kmlocks(v, value_type) for k, v in field_value.items() @@ -306,16 +299,18 @@ def _dict_to_kmlocks(self, data: dict, cls: type) -> Any: field_value = { ( int(k) - if key_type is int - and isinstance(k, str) - and k.isdigit() + if key_type is int and isinstance(k, str) and k.isdigit() else k ): v for k, v in field_value.items() } # Handle nested dataclasses - elif isinstance(field_value, dict) and is_dataclass(field_type) and isinstance(field_type, type): + elif ( + isinstance(field_value, dict) + and is_dataclass(field_type) + and isinstance(field_type, type) + ): # _LOGGER.debug(f"[dict_to_kmlocks] Recursively converting nested dataclass: {field_name}") field_value = self._dict_to_kmlocks(field_value, field_type) @@ -367,11 +362,7 @@ def _kmlocks_to_dict(self, instance: object) -> object: ] elif isinstance(field_value, dict): result[field_name] = { - k: ( - self._kmlocks_to_dict(v) - if hasattr(v, "__dataclass_fields__") - else v - ) + k: (self._kmlocks_to_dict(v) if hasattr(v, "__dataclass_fields__") else v) for k, v in field_value.items() } else: @@ -453,13 +444,8 @@ async def _rebuild_lock_relationships(self) -> None: if kmlock.parent_name == parent_lock.lock_name: if kmlock.parent_config_entry_id is None: kmlock.parent_config_entry_id = parent_config_entry_id - if ( - keymaster_config_entry_id - not in parent_lock.child_config_entry_ids - ): - parent_lock.child_config_entry_ids.append( - keymaster_config_entry_id - ) + if keymaster_config_entry_id not in parent_lock.child_config_entry_ids: + parent_lock.child_config_entry_ids.append(keymaster_config_entry_id) break for child_config_entry_id in kmlock.child_config_entry_ids: if ( @@ -468,13 +454,11 @@ async def _rebuild_lock_relationships(self) -> None: != keymaster_config_entry_id ): with contextlib.suppress(ValueError): - self.kmlocks[ + self.kmlocks[child_config_entry_id].child_config_entry_ids.remove( child_config_entry_id - ].child_config_entry_ids.remove(child_config_entry_id) + ) - async def _handle_zwave_js_lock_event( - self, kmlock: KeymasterLock, event: Event - ) -> None: + async def _handle_zwave_js_lock_event(self, kmlock: KeymasterLock, event: Event) -> None: """Handle Z-Wave JS event.""" if ( @@ -529,9 +513,7 @@ async def _handle_lock_state_change( event: Event[EventStateChangedData], ) -> None: """Track state changes to lock entities.""" - _LOGGER.debug( - "[handle_lock_state_change] %s: event: %s", kmlock.lock_name, event - ) + _LOGGER.debug("[handle_lock_state_change] %s: event: %s", kmlock.lock_name, event) if not event: return @@ -552,8 +534,7 @@ async def _handle_lock_state_change( action_type: str = "" if kmlock.alarm_type_or_access_control_entity_id and ( ALARM_TYPE in kmlock.alarm_type_or_access_control_entity_id - or ALARM_TYPE.replace("_", "") - in kmlock.alarm_type_or_access_control_entity_id + or ALARM_TYPE.replace("_", "") in kmlock.alarm_type_or_access_control_entity_id ): action_type = ALARM_TYPE if ( @@ -565,9 +546,7 @@ async def _handle_lock_state_change( # Get alarm_level/usercode and alarm_type/access_control states alarm_level_state = None if kmlock.alarm_level_or_user_code_entity_id: - alarm_level_state = self.hass.states.get( - kmlock.alarm_level_or_user_code_entity_id - ) + alarm_level_state = self.hass.states.get(kmlock.alarm_level_or_user_code_entity_id) alarm_level_value: int | None = ( int(alarm_level_state.state) if alarm_level_state @@ -576,13 +555,10 @@ async def _handle_lock_state_change( ) alarm_type_state = None if kmlock.alarm_type_or_access_control_entity_id: - alarm_type_state = self.hass.states.get( - kmlock.alarm_type_or_access_control_entity_id - ) + alarm_type_state = self.hass.states.get(kmlock.alarm_type_or_access_control_entity_id) alarm_type_value: int | None = ( int(alarm_type_state.state) - if alarm_type_state - and alarm_type_state.state not in {STATE_UNKNOWN, STATE_UNAVAILABLE} + if alarm_type_state and alarm_type_state.state not in {STATE_UNKNOWN, STATE_UNAVAILABLE} else None ) @@ -612,9 +588,7 @@ async def _handle_lock_state_change( # Lookup action text based on alarm type value action_text: str | None = ( - ACTION_MAP.get(action_type, {}).get( - alarm_type_value, "Unknown Alarm Type Value" - ) + ACTION_MAP.get(action_type, {}).get(alarm_type_value, "Unknown Alarm Type Value") if alarm_type_value is not None else None ) @@ -625,9 +599,7 @@ async def _handle_lock_state_change( new_state, ) if old_state not in {LockState.LOCKED, LockState.UNLOCKED}: - _LOGGER.debug( - "[handle_lock_state_change] %s: Ignoring state change", kmlock.lock_name - ) + _LOGGER.debug("[handle_lock_state_change] %s: Ignoring state change", kmlock.lock_name) elif new_state == LockState.UNLOCKED: await self._lock_unlocked( kmlock=kmlock, @@ -656,9 +628,7 @@ async def _handle_door_state_change( event: Event[EventStateChangedData], ) -> None: """Track state changes to door entities.""" - _LOGGER.debug( - "[handle_door_state_change] %s: event: %s", kmlock.lock_name, event - ) + _LOGGER.debug("[handle_door_state_change] %s: event: %s", kmlock.lock_name, event) if not event: return @@ -681,9 +651,7 @@ async def _handle_door_state_change( new_state, ) if old_state not in {STATE_ON, STATE_OFF}: - _LOGGER.debug( - "[handle_door_state_change] %s: Ignoring state change", kmlock.lock_name - ) + _LOGGER.debug("[handle_door_state_change] %s: Ignoring state change", kmlock.lock_name) elif new_state == STATE_ON: await self._door_opened(kmlock) elif new_state == STATE_OFF: @@ -715,7 +683,10 @@ async def _create_listeners( ) ) - if kmlock.door_sensor_entity_id is not None and kmlock.door_sensor_entity_id != DEFAULT_DOOR_SENSOR: + if ( + kmlock.door_sensor_entity_id is not None + and kmlock.door_sensor_entity_id != DEFAULT_DOOR_SENSOR + ): _LOGGER.debug( "[create_listeners] %s: Creating handle_door_state_change listener", kmlock.lock_name, @@ -753,9 +724,7 @@ async def _create_listeners( @staticmethod async def _unsubscribe_listeners(kmlock: KeymasterLock) -> None: # Unsubscribe to any listeners - _LOGGER.debug( - "[unsubscribe_listeners] %s: Removing all listeners", kmlock.lock_name - ) + _LOGGER.debug("[unsubscribe_listeners] %s: Removing all listeners", kmlock.lock_name) if not hasattr(kmlock, "listeners") or kmlock.listeners is None: kmlock.listeners = [] return @@ -773,8 +742,7 @@ async def _update_listeners(self, kmlock: KeymasterLock) -> None: await self._create_listeners(kmlock=kmlock) else: _LOGGER.debug( - "[update_listeners] %s: " - "Setting create_listeners to run when HA starts", + "[update_listeners] %s: " "Setting create_listeners to run when HA starts", kmlock.lock_name, ) self.hass.bus.async_listen_once( @@ -793,9 +761,7 @@ async def _lock_unlocked( if not self._throttle.is_allowed( "lock_unlocked", kmlock.keymaster_config_entry_id, THROTTLE_SECONDS ): - _LOGGER.debug( - "[lock_unlocked] %s: Throttled. source: %s", kmlock.lock_name, source - ) + _LOGGER.debug("[lock_unlocked] %s: Throttled. source: %s", kmlock.lock_name, source) return kmlock.lock_state = LockState.UNLOCKED @@ -826,14 +792,9 @@ async def _lock_unlocked( ) if code_slot > 0 and code_slot in kmlock.code_slots: - if ( - kmlock.parent_name is not None - and not kmlock.code_slots[code_slot].override_parent - ): - parent_kmlock: KeymasterLock | None = ( - await self.get_lock_by_config_entry_id( - kmlock.parent_config_entry_id - ) + if kmlock.parent_name is not None and not kmlock.code_slots[code_slot].override_parent: + parent_kmlock: KeymasterLock | None = await self.get_lock_by_config_entry_id( + kmlock.parent_config_entry_id ) if ( isinstance(parent_kmlock, KeymasterLock) @@ -842,9 +803,13 @@ async def _lock_unlocked( and code_slot in parent_kmlock.code_slots and parent_kmlock.code_slots[code_slot].accesslimit_count_enabled ): - accesslimit_count: int | None = parent_kmlock.code_slots[code_slot].accesslimit_count + accesslimit_count: int | None = parent_kmlock.code_slots[ + code_slot + ].accesslimit_count if accesslimit_count is not None and accesslimit_count > 0: - parent_kmlock.code_slots[code_slot].accesslimit_count = int(accesslimit_count) - 1 + parent_kmlock.code_slots[code_slot].accesslimit_count = ( + int(accesslimit_count) - 1 + ) elif ( kmlock.code_slots[code_slot].accesslimit_count_enabled and isinstance(kmlock.code_slots[code_slot].accesslimit_count, int) @@ -852,10 +817,7 @@ async def _lock_unlocked( ): kmlock.code_slots[code_slot].accesslimit_count -= 1 - if ( - kmlock.code_slots[code_slot].notifications - and not kmlock.lock_notifications - ): + if kmlock.code_slots[code_slot].notifications and not kmlock.lock_notifications: await send_manual_notification( hass=self.hass, script_name=kmlock.notify_script_name, @@ -874,21 +836,15 @@ async def _lock_unlocked( ATTR_ACTION_CODE: action_code, ATTR_ACTION_TEXT: event_label, ATTR_CODE_SLOT: code_slot, - ATTR_CODE_SLOT_NAME: ( - kmlock.code_slots[code_slot].name if code_slot != 0 else "" - ), + ATTR_CODE_SLOT_NAME: (kmlock.code_slots[code_slot].name if code_slot != 0 else ""), }, ) - async def _lock_locked( - self, kmlock, source=None, event_label=None, action_code=None - ) -> None: + async def _lock_locked(self, kmlock, source=None, event_label=None, action_code=None) -> None: if not self._throttle.is_allowed( "lock_locked", kmlock.keymaster_config_entry_id, THROTTLE_SECONDS ): - _LOGGER.debug( - "[lock_locked] %s: Throttled. source: %s", kmlock.lock_name, source - ) + _LOGGER.debug("[lock_locked] %s: Throttled. source: %s", kmlock.lock_name, source) return kmlock.lock_state = LockState.LOCKED _LOGGER.debug( @@ -1013,9 +969,7 @@ async def _timer_triggered(self, kmlock: KeymasterLock, _: datetime) -> None: else: await self._lock_lock(kmlock=kmlock) - async def _update_door_and_lock_state( - self, trigger_actions_if_changed=False - ) -> None: + async def _update_door_and_lock_state(self, trigger_actions_if_changed=False) -> None: _LOGGER.debug("[update_door_and_lock_state] Running") for kmlock in self.kmlocks.values(): if isinstance(kmlock.lock_entity_id, str) and kmlock.lock_entity_id: @@ -1108,9 +1062,7 @@ async def add_lock(self, kmlock: KeymasterLock, update: bool = False) -> None: self.kmlocks[kmlock.keymaster_config_entry_id].pending_delete = False await self._update_lock(kmlock) return - _LOGGER.debug( - "[add_lock] %s: Lock already exists, not adding", kmlock.lock_name - ) + _LOGGER.debug("[add_lock] %s: Lock already exists, not adding", kmlock.lock_name) return _LOGGER.debug("[add_lock] %s", kmlock.lock_name) self.kmlocks[kmlock.keymaster_config_entry_id] = kmlock @@ -1125,12 +1077,17 @@ async def _update_lock(self, new: KeymasterLock) -> bool: await self._initial_setup_done_event.wait() _LOGGER.debug("[update_lock] %s", new.lock_name) if new.keymaster_config_entry_id not in self.kmlocks: - _LOGGER.debug( - "[update_lock] %s: Can't update, lock doesn't exist", new.lock_name - ) + _LOGGER.debug("[update_lock] %s: Can't update, lock doesn't exist", new.lock_name) return False old: KeymasterLock = self.kmlocks[new.keymaster_config_entry_id] - if not old.starting_code_slot or not old.number_of_code_slots or not new.number_of_code_slots or not new.starting_code_slot or not new.code_slots or not old.code_slots: + if ( + not old.starting_code_slot + or not old.number_of_code_slots + or not new.number_of_code_slots + or not new.starting_code_slot + or not new.code_slots + or not old.code_slots + ): return False await KeymasterCoordinator._unsubscribe_listeners(old) # _LOGGER.debug(f"[update_lock] {new.lock_name}: old: {old}") @@ -1161,25 +1118,15 @@ async def _update_lock(self, new: KeymasterLock) -> bool: new_slot.notifications = old_slot.notifications new_slot.accesslimit_count_enabled = old_slot.accesslimit_count_enabled new_slot.accesslimit_count = old_slot.accesslimit_count - new_slot.accesslimit_date_range_enabled = ( - old_slot.accesslimit_date_range_enabled - ) - new_slot.accesslimit_date_range_start = ( - old_slot.accesslimit_date_range_start - ) - new_slot.accesslimit_date_range_end = ( - old_slot.accesslimit_date_range_end - ) - new_slot.accesslimit_day_of_week_enabled = ( - old_slot.accesslimit_day_of_week_enabled - ) + new_slot.accesslimit_date_range_enabled = old_slot.accesslimit_date_range_enabled + new_slot.accesslimit_date_range_start = old_slot.accesslimit_date_range_start + new_slot.accesslimit_date_range_end = old_slot.accesslimit_date_range_end + new_slot.accesslimit_day_of_week_enabled = old_slot.accesslimit_day_of_week_enabled if not new_slot.accesslimit_day_of_week: continue for dow_num, new_dow in new_slot.accesslimit_day_of_week.items(): if old_slot.accesslimit_day_of_week: - old_dow: KeymasterCodeSlotDayOfWeek = ( - old_slot.accesslimit_day_of_week[dow_num] - ) + old_dow: KeymasterCodeSlotDayOfWeek = old_slot.accesslimit_day_of_week[dow_num] new_dow.dow_enabled = old_dow.dow_enabled new_dow.limit_by_time = old_dow.limit_by_time new_dow.include_exclude = old_dow.include_exclude @@ -1212,9 +1159,7 @@ async def _delete_lock(self, kmlock: KeymasterLock, _: datetime) -> None: ) return _LOGGER.debug("[delete_lock] %s: Deleting", kmlock.lock_name) - await self.hass.async_add_executor_job( - delete_lovelace, self.hass, kmlock.lock_name - ) + await self.hass.async_add_executor_job(delete_lovelace, self.hass, kmlock.lock_name) if kmlock.autolock_timer: await kmlock.autolock_timer.cancel() await KeymasterCoordinator._unsubscribe_listeners( @@ -1257,17 +1202,13 @@ def count_locks_not_pending_delete(self) -> int: count += 1 return count - async def get_lock_by_config_entry_id( - self, config_entry_id: str - ) -> KeymasterLock | None: + async def get_lock_by_config_entry_id(self, config_entry_id: str) -> KeymasterLock | None: """Get a keymaster lock by entry_id.""" await self._initial_setup_done_event.wait() # _LOGGER.debug(f"[get_lock_by_config_entry_id] config_entry_id: {config_entry_id}") return self.kmlocks.get(config_entry_id, None) - def sync_get_lock_by_config_entry_id( - self, config_entry_id: str - ) -> KeymasterLock | None: + def sync_get_lock_by_config_entry_id(self, config_entry_id: str) -> KeymasterLock | None: """Get a keymaster lock by entry_id.""" # _LOGGER.debug(f"[sync_get_lock_by_config_entry_id] config_entry_id: {config_entry_id}") return self.kmlocks.get(config_entry_id, None) @@ -1284,9 +1225,7 @@ async def set_pin_on_lock( await self._initial_setup_done_event.wait() # _LOGGER.debug(f"[set_pin_on_lock] config_entry_id: {config_entry_id}, code_slot: {code_slot}, pin: {pin}, update_after: {update_after}") - kmlock: KeymasterLock | None = await self.get_lock_by_config_entry_id( - config_entry_id - ) + kmlock: KeymasterLock | None = await self.get_lock_by_config_entry_id(config_entry_id) if not isinstance(kmlock, KeymasterLock): _LOGGER.error( "[Coordinator] Can't find lock with config_entry_id: %s", @@ -1345,8 +1284,10 @@ async def set_pin_on_lock( kmlock.code_slots[code_slot].synced = Synced.ADDING self._quick_refresh = True - if async_using_zwave_js(hass=self.hass, entity_id=kmlock.lock_entity_id) and kmlock.zwave_js_lock_node: - + if ( + async_using_zwave_js(hass=self.hass, entity_id=kmlock.lock_entity_id) + and kmlock.zwave_js_lock_node + ): try: await set_usercode(kmlock.zwave_js_lock_node, code_slot, pin) except BaseZwaveJSServerError as e: @@ -1376,9 +1317,7 @@ async def clear_pin_from_lock( ) -> bool: """Clear the usercode from a code slot.""" await self._initial_setup_done_event.wait() - kmlock: KeymasterLock | None = await self.get_lock_by_config_entry_id( - config_entry_id - ) + kmlock: KeymasterLock | None = await self.get_lock_by_config_entry_id(config_entry_id) if not isinstance(kmlock, KeymasterLock): _LOGGER.error( "[Coordinator] Can't find lock with config_entry_id: %s", @@ -1418,7 +1357,10 @@ async def clear_pin_from_lock( kmlock.code_slots[code_slot].synced = Synced.DELETING self._quick_refresh = True - if async_using_zwave_js(hass=self.hass, entity_id=kmlock.lock_entity_id) and kmlock.zwave_js_lock_node: + if ( + async_using_zwave_js(hass=self.hass, entity_id=kmlock.lock_entity_id) + and kmlock.zwave_js_lock_node + ): try: await clear_usercode(kmlock.zwave_js_lock_node, code_slot) except BaseZwaveJSServerError as e: @@ -1501,9 +1443,7 @@ async def reset_code_slot(self, config_entry_id: str, code_slot: int) -> None: "Sunday", ] ): - dow_slots[i] = KeymasterCodeSlotDayOfWeek( - day_of_week_num=i, day_of_week_name=dow - ) + dow_slots[i] = KeymasterCodeSlotDayOfWeek(day_of_week_num=i, day_of_week_name=dow) new_code_slot = KeymasterCodeSlot( number=code_slot, enabled=False, accesslimit_day_of_week=dow_slots ) @@ -1534,12 +1474,8 @@ async def _is_slot_active(slot: KeymasterCodeSlot) -> bool: if slot.accesslimit_day_of_week_enabled and slot.accesslimit_day_of_week: today_index: int = datetime.now().astimezone().weekday() - today: KeymasterCodeSlotDayOfWeek = slot.accesslimit_day_of_week[ - today_index - ] - _LOGGER.debug( - "[is_slot_active] today_index: %s, today: %s", today_index, today - ) + today: KeymasterCodeSlotDayOfWeek = slot.accesslimit_day_of_week[today_index] + _LOGGER.debug("[is_slot_active] today_index: %s, today: %s", today_index, today) if not today.dow_enabled: return False @@ -1574,14 +1510,10 @@ async def _is_slot_active(slot: KeymasterCodeSlot) -> bool: async def _trigger_quick_refresh(self, _: datetime): await self.async_request_refresh() - async def update_slot_active_state( - self, config_entry_id: str, code_slot: int - ) -> bool: + async def update_slot_active_state(self, config_entry_id: str, code_slot: int) -> bool: """Update the active state for a code slot.""" await self._initial_setup_done_event.wait() - kmlock: KeymasterLock | None = await self.get_lock_by_config_entry_id( - config_entry_id - ) + kmlock: KeymasterLock | None = await self.get_lock_by_config_entry_id(config_entry_id) if not isinstance(kmlock, KeymasterLock): _LOGGER.error( "[Coordinator] Can't find lock with config_entry_id: %s", @@ -1591,15 +1523,14 @@ async def update_slot_active_state( if not kmlock.code_slots or code_slot not in kmlock.code_slots: _LOGGER.debug( - "[update_slot_active_state] %s: " - "Keymaster code slot %s doesn't exist.", + "[update_slot_active_state] %s: " "Keymaster code slot %s doesn't exist.", kmlock.lock_name, code_slot, ) return False - kmlock.code_slots[code_slot].active = ( - await KeymasterCoordinator._is_slot_active(kmlock.code_slots[code_slot]) + kmlock.code_slots[code_slot].active = await KeymasterCoordinator._is_slot_active( + kmlock.code_slots[code_slot] ) return True @@ -1622,9 +1553,7 @@ async def _connect_and_update_lock(self, kmlock: KeymasterLock) -> bool: if kmlock.lock_config_entry_id is None: return False try: - zwave_entry = self.hass.config_entries.async_get_entry( - kmlock.lock_config_entry_id - ) + zwave_entry = self.hass.config_entries.async_get_entry(kmlock.lock_config_entry_id) if zwave_entry: client = zwave_entry.runtime_data[ZWAVE_JS_DATA_CLIENT] else: @@ -1644,9 +1573,7 @@ async def _connect_and_update_lock(self, kmlock: KeymasterLock) -> bool: kmlock.connected = False return False - kmlock.connected = bool( - client.connected and client.driver and client.driver.controller - ) + kmlock.connected = bool(client.connected and client.driver and client.driver.controller) if not kmlock.connected: return False @@ -1662,8 +1589,7 @@ async def _connect_and_update_lock(self, kmlock: KeymasterLock) -> bool: return True _LOGGER.debug( - "[connect_and_update_lock] %s: " - "Lock connected, updating Device and Nodes", + "[connect_and_update_lock] %s: " "Lock connected, updating Device and Nodes", kmlock.lock_name, ) @@ -1679,9 +1605,7 @@ async def _connect_and_update_lock(self, kmlock: KeymasterLock) -> bool: lock_dev_reg_entry = None if lock_ent_reg_entry and lock_ent_reg_entry.device_id: - lock_dev_reg_entry = self._device_registry.async_get( - lock_ent_reg_entry.device_id - ) + lock_dev_reg_entry = self._device_registry.async_get(lock_ent_reg_entry.device_id) if not lock_dev_reg_entry: _LOGGER.error( "[Coordinator] %s: Can't find the lock in the Device Registry", @@ -1844,10 +1768,7 @@ async def _sync_pin(self, kmlock: KeymasterLock, code_slot: int, usercode: str): pin=pin, override=True, ) - elif ( - not kmlock.code_slots[code_slot].enabled - or not kmlock.code_slots[code_slot].active - ): + elif not kmlock.code_slots[code_slot].enabled or not kmlock.code_slots[code_slot].active: await self.clear_pin_from_lock( config_entry_id=kmlock.keymaster_config_entry_id, code_slot=code_slot, @@ -1894,9 +1815,7 @@ async def _sync_child_lock(self, kmlock: KeymasterLock, child_entry_id: str) -> return if not async_using_zwave_js(hass=self.hass, kmlock=child_kmlock): - _LOGGER.error( - "[Coordinator] %s: Not using Z-Wave JS", child_kmlock.lock_name - ) + _LOGGER.error("[Coordinator] %s: Not using Z-Wave JS", child_kmlock.lock_name) return if kmlock.code_slots == child_kmlock.code_slots: @@ -1909,7 +1828,9 @@ async def _sync_child_lock(self, kmlock: KeymasterLock, child_entry_id: str) -> await self._update_child_code_slots(kmlock, child_kmlock) - async def _update_child_code_slots(self, kmlock: KeymasterLock, child_kmlock: KeymasterLock) -> None: + async def _update_child_code_slots( + self, kmlock: KeymasterLock, child_kmlock: KeymasterLock + ) -> None: """Update code slots on a child lock based on parent settings.""" if not kmlock.code_slots: return diff --git a/custom_components/keymaster/datetime.py b/custom_components/keymaster/datetime.py index 33fe3463..75b39a2a 100644 --- a/custom_components/keymaster/datetime.py +++ b/custom_components/keymaster/datetime.py @@ -29,38 +29,38 @@ async def async_setup_entry( config_entry.data[CONF_START], config_entry.data[CONF_START] + config_entry.data[CONF_SLOTS], ): - entities.extend([ - KeymasterDateTime( - entity_description=KeymasterDateTimeEntityDescription( - key=f"datetime.code_slots:{x}.accesslimit_date_range_start", - name=f"Code Slot {x}: Date Range Start", - icon="mdi:calendar-start", - entity_registry_enabled_default=True, - hass=hass, - config_entry=config_entry, - coordinator=coordinator, + entities.extend( + [ + KeymasterDateTime( + entity_description=KeymasterDateTimeEntityDescription( + key=f"datetime.code_slots:{x}.accesslimit_date_range_start", + name=f"Code Slot {x}: Date Range Start", + icon="mdi:calendar-start", + entity_registry_enabled_default=True, + hass=hass, + config_entry=config_entry, + coordinator=coordinator, + ), ), - ), - KeymasterDateTime( - entity_description=KeymasterDateTimeEntityDescription( - key=f"datetime.code_slots:{x}.accesslimit_date_range_end", - name=f"Code Slot {x}: Date Range End", - icon="mdi:calendar-end", - entity_registry_enabled_default=True, - hass=hass, - config_entry=config_entry, - coordinator=coordinator, + KeymasterDateTime( + entity_description=KeymasterDateTimeEntityDescription( + key=f"datetime.code_slots:{x}.accesslimit_date_range_end", + name=f"Code Slot {x}: Date Range End", + icon="mdi:calendar-end", + entity_registry_enabled_default=True, + hass=hass, + config_entry=config_entry, + coordinator=coordinator, + ), ), - ) - ]) + ] + ) async_add_entities(entities, True) @dataclass(frozen=True, kw_only=True) -class KeymasterDateTimeEntityDescription( - KeymasterEntityDescription, DateTimeEntityDescription -): +class KeymasterDateTimeEntityDescription(KeymasterEntityDescription, DateTimeEntityDescription): """Entity Description for keymaster DateTime.""" @@ -90,15 +90,18 @@ def _handle_coordinator_update(self) -> None: if ( ".code_slots" in self._property and self._kmlock.parent_name is not None - and (not self._kmlock.code_slots or not self._code_slot or not self._kmlock.code_slots[self._code_slot].override_parent) + and ( + not self._kmlock.code_slots + or not self._code_slot + or not self._kmlock.code_slots[self._code_slot].override_parent + ) ): self._attr_available = False self.async_write_ha_state() return - if ( - ".code_slots" in self._property - and (not self._kmlock.code_slots or self._code_slot not in self._kmlock.code_slots) + if ".code_slots" in self._property and ( + not self._kmlock.code_slots or self._code_slot not in self._kmlock.code_slots ): self._attr_available = False self.async_write_ha_state() @@ -107,9 +110,11 @@ def _handle_coordinator_update(self) -> None: if ( self._property.endswith(".accesslimit_date_range_start") or self._property.endswith(".accesslimit_date_range_end") - ) and (not self._kmlock.code_slots or not self._code_slot or not self._kmlock.code_slots[ - self._code_slot - ].accesslimit_date_range_enabled): + ) and ( + not self._kmlock.code_slots + or not self._code_slot + or not self._kmlock.code_slots[self._code_slot].accesslimit_date_range_enabled + ): self._attr_available = False self.async_write_ha_state() return @@ -129,8 +134,13 @@ async def async_set_value(self, value: datetime) -> None: if ( ".code_slots" in self._property - and self._kmlock and self._kmlock.parent_name - and (not self._kmlock.code_slots or not self._code_slot or not self._kmlock.code_slots[self._code_slot].override_parent) + and self._kmlock + and self._kmlock.parent_name + and ( + not self._kmlock.code_slots + or not self._code_slot + or not self._kmlock.code_slots[self._code_slot].override_parent + ) ): _LOGGER.debug( "[DateTime async_set_value] %s: " diff --git a/custom_components/keymaster/entity.py b/custom_components/keymaster/entity.py index eefb89fc..8e3df76b 100644 --- a/custom_components/keymaster/entity.py +++ b/custom_components/keymaster/entity.py @@ -1,4 +1,5 @@ """Base entity for keymaster.""" + from __future__ import annotations from dataclasses import dataclass @@ -33,21 +34,15 @@ def __init__(self, entity_description: KeymasterEntityDescription) -> None: self._config_entry: ConfigEntry = entity_description.config_entry self.entity_description: KeymasterEntityDescription = entity_description self._attr_available = False - self._property: str = ( - entity_description.key - ) # ..:.: *Only if needed + self._property: str = entity_description.key # ..:.: *Only if needed self._kmlock = self.coordinator.sync_get_lock_by_config_entry_id( self._config_entry.entry_id ) self._attr_name: str | None = None if self._attr_name: - self._attr_name = ( - f"{self._kmlock.lock_name} {self.entity_description.name}" - ) + self._attr_name = f"{self._kmlock.lock_name} {self.entity_description.name}" # _LOGGER.debug(f"[Entity init] entity_description.name: {self.entity_description.name}, name: {self.name}") - self._attr_unique_id: str = ( - f"{self._config_entry.entry_id}_{slugify(self._property)}" - ) + self._attr_unique_id: str = f"{self._config_entry.entry_id}_{slugify(self._property)}" # _LOGGER.debug(f"[Entity init] self._property: {self._property}, unique_id: {self.unique_id}") self._code_slot: None | int = None if ".code_slots" in self._property: diff --git a/custom_components/keymaster/exceptions.py b/custom_components/keymaster/exceptions.py index 78e927f3..01951f9a 100644 --- a/custom_components/keymaster/exceptions.py +++ b/custom_components/keymaster/exceptions.py @@ -8,10 +8,7 @@ class ZWaveIntegrationNotConfiguredError(HomeAssistantError): def __str__(self) -> str: """Error string to show when zwave integration is not configured.""" - return ( - "A Z-Wave integration has not been configured for this " - "Home Assistant instance" - ) + return "A Z-Wave integration has not been configured for this " "Home Assistant instance" class NoNodeSpecifiedError(HomeAssistantError): diff --git a/custom_components/keymaster/helpers.py b/custom_components/keymaster/helpers.py index a87706ca..345ebc36 100644 --- a/custom_components/keymaster/helpers.py +++ b/custom_components/keymaster/helpers.py @@ -31,9 +31,7 @@ class Throttle: def __init__(self) -> None: """Initialize Throttle class.""" - self._cooldowns: MutableMapping = ( - {} - ) # Nested dictionary: {function_name: {key: last_called_time}} + self._cooldowns: MutableMapping = {} # Nested dictionary: {function_name: {key: last_called_time}} def is_allowed(self, func_name, key, cooldown_seconds) -> bool: """Check if function is allowed to run or not.""" @@ -73,18 +71,14 @@ async def start(self) -> bool: _LOGGER.error("[KeymasterTimer] Cannot start timer as timer not setup") return False - if isinstance(self._end_time, datetime) and isinstance( - self._unsub_events, list - ): + if isinstance(self._end_time, datetime) and isinstance(self._unsub_events, list): # Already running so reset and restart timer for unsub in self._unsub_events: unsub() self._unsub_events = [] if sun.is_up(self.hass): - delay: int = ( - self._kmlock.autolock_min_day or DEFAULT_AUTOLOCK_MIN_DAY - ) * 60 + delay: int = (self._kmlock.autolock_min_day or DEFAULT_AUTOLOCK_MIN_DAY) * 60 else: delay = (self._kmlock.autolock_min_night or DEFAULT_AUTOLOCK_MIN_NIGHT) * 60 self._end_time = datetime.now().astimezone() + timedelta(seconds=delay) @@ -96,9 +90,7 @@ async def start(self) -> bool: self._unsub_events.append( async_call_later(hass=self.hass, delay=delay, action=self._call_action) ) - self._unsub_events.append( - async_call_later(hass=self.hass, delay=delay, action=self.cancel) - ) + self._unsub_events.append(async_call_later(hass=self.hass, delay=delay, action=self.cancel)) return True async def cancel(self, timer_elapsed: datetime | None = None) -> None: @@ -118,10 +110,7 @@ def is_running(self) -> bool: """Return if the timer is running.""" if not self._end_time: return False - if ( - isinstance(self._end_time, datetime) - and self._end_time >= datetime.now().astimezone() - ): + if isinstance(self._end_time, datetime) and self._end_time >= datetime.now().astimezone(): if isinstance(self._unsub_events, list): for unsub in self._unsub_events: unsub() @@ -133,10 +122,7 @@ def is_running(self) -> bool: @property def is_setup(self) -> bool: """Return if the timer has been initially setup.""" - if ( - isinstance(self._end_time, datetime) - and self._end_time >= datetime.now().astimezone() - ): + if isinstance(self._end_time, datetime) and self._end_time >= datetime.now().astimezone(): if isinstance(self._unsub_events, list): for unsub in self._unsub_events: unsub() @@ -149,10 +135,7 @@ def end_time(self) -> datetime | None: """Returns when the timer will end.""" if not self._end_time: return None - if ( - isinstance(self._end_time, datetime) - and self._end_time >= datetime.now().astimezone() - ): + if isinstance(self._end_time, datetime) and self._end_time >= datetime.now().astimezone(): if isinstance(self._unsub_events, list): for unsub in self._unsub_events: unsub() @@ -166,10 +149,7 @@ def remaining_seconds(self) -> int | None: """Return the seconds until the timer ends.""" if not self._end_time: return None - if ( - isinstance(self._end_time, datetime) - and self._end_time >= datetime.now().astimezone() - ): + if isinstance(self._end_time, datetime) and self._end_time >= datetime.now().astimezone(): if isinstance(self._unsub_events, list): for unsub in self._unsub_events: unsub() @@ -253,9 +233,7 @@ async def delete_code_slot_entities( if entity_id: try: entity_registry.async_remove(entity_id) - _LOGGER.debug( - "[delete_code_slot_entities] Removed entity: %s", entity_id - ) + _LOGGER.debug("[delete_code_slot_entities] Removed entity: %s", entity_id) except (KeyError, ValueError) as e: _LOGGER.warning( "Error removing entity: %s. %s: %s", @@ -283,9 +261,7 @@ async def delete_code_slot_entities( if entity_id: try: entity_registry.async_remove(entity_id) - _LOGGER.debug( - "[delete_code_slot_entities] Removed entity: %s", entity_id - ) + _LOGGER.debug("[delete_code_slot_entities] Removed entity: %s", entity_id) except (KeyError, ValueError) as e: _LOGGER.warning( "Error removing entity: %s. %s: %s", @@ -294,9 +270,7 @@ async def delete_code_slot_entities( e, ) else: - _LOGGER.debug( - "[delete_code_slot_entities] No entity_id found for %s", prop - ) + _LOGGER.debug("[delete_code_slot_entities] No entity_id found for %s", prop) async def call_hass_service( @@ -316,9 +290,7 @@ async def call_hass_service( ) try: - await hass.services.async_call( - domain, service, service_data=service_data, target=target - ) + await hass.services.async_call(domain, service, service_data=service_data, target=target) except ServiceNotFound: _LOGGER.warning("Action Not Found: %s.%s", domain, service) # except Exception as e: @@ -371,11 +343,7 @@ async def send_persistent_notification( ) -async def dismiss_persistent_notification( - hass: HomeAssistant, notification_id: str -) -> None: +async def dismiss_persistent_notification(hass: HomeAssistant, notification_id: str) -> None: """Clear or dismisss a persistent notification.""" - _LOGGER.debug( - "[dismiss_persistent_notification] notification_id: %s", notification_id - ) + _LOGGER.debug("[dismiss_persistent_notification] notification_id: %s", notification_id) persistent_notification.async_dismiss(hass=hass, notification_id=notification_id) diff --git a/custom_components/keymaster/lovelace.py b/custom_components/keymaster/lovelace.py index ece51242..b7b66535 100644 --- a/custom_components/keymaster/lovelace.py +++ b/custom_components/keymaster/lovelace.py @@ -38,7 +38,9 @@ async def generate_lovelace( child=bool(parent_config_entry_id), door=bool(door_sensor not in {None, DEFAULT_DOOR_SENSOR}), ) - mapped_badges_list: MutableMapping[str, Any] | list[MutableMapping[str, Any]] = await _map_property_to_entity_id( + mapped_badges_list: ( + MutableMapping[str, Any] | list[MutableMapping[str, Any]] + ) = await _map_property_to_entity_id( hass=hass, lovelace_entities=badges_list, keymaster_config_entry_id=keymaster_config_entry_id, @@ -60,11 +62,11 @@ async def generate_lovelace( code_slot=x ) else: - code_slot_dict = await _generate_code_slot_dict( - code_slot=x - ) + code_slot_dict = await _generate_code_slot_dict(code_slot=x) code_slot_list.append(code_slot_dict) - lovelace_list: MutableMapping[str, Any] | list[MutableMapping[str, Any]] = await _map_property_to_entity_id( + lovelace_list: ( + MutableMapping[str, Any] | list[MutableMapping[str, Any]] + ) = await _map_property_to_entity_id( hass=hass, lovelace_entities=code_slot_list, keymaster_config_entry_id=keymaster_config_entry_id, @@ -133,14 +135,11 @@ def _create_lovelace_folder(folder) -> None: def _dump_with_indent(data: Any, indent: int = 2) -> str: """Convert dict to YAML and indent each line by a given number of spaces.""" yaml_string: str = yaml.dump(data, default_flow_style=False, sort_keys=False) - indented_yaml: str = "\n".join( - " " * indent + line for line in yaml_string.splitlines() - ) + indented_yaml: str = "\n".join(" " * indent + line for line in yaml_string.splitlines()) return indented_yaml def _write_lovelace_yaml(folder: str, filename: str, lovelace: Any) -> None: - # Indent YAML to make copy/paste easier indented_yaml: str = _dump_with_indent(lovelace, indent=2) @@ -180,17 +179,17 @@ async def _map_property_to_entity_id( # f"parent_config_entry_id: {parent_config_entry_id}" # ) entity_registry: er.EntityRegistry = er.async_get(hass) - lovelace_list: list[MutableMapping[str, Any]] | MutableMapping[str, Any] = ( - await _process_entities( - lovelace_entities, - "entity", - functools.partial( - _get_entity_id, - entity_registry, - keymaster_config_entry_id, - parent_config_entry_id, - ), - ) + lovelace_list: ( + list[MutableMapping[str, Any]] | MutableMapping[str, Any] + ) = await _process_entities( + lovelace_entities, + "entity", + functools.partial( + _get_entity_id, + entity_registry, + keymaster_config_entry_id, + parent_config_entry_id, + ), ) return lovelace_list @@ -205,15 +204,11 @@ async def _process_entities(data: Any, key_to_find: str, process_func: Callable) updated_dict[key] = await process_func(value) else: # Recursively process the value - updated_dict[key] = await _process_entities( - value, key_to_find, process_func - ) + updated_dict[key] = await _process_entities(value, key_to_find, process_func) return updated_dict if isinstance(data, list): # Recursively process each item in the list - return [ - await _process_entities(item, key_to_find, process_func) for item in data - ] + return [await _process_entities(item, key_to_find, process_func) for item in data] # If not a dict or list, return the data as-is return data @@ -416,9 +411,7 @@ async def _generate_code_slot_dict(code_slot, child=False) -> MutableMapping[str ] ) - dow_list: list[MutableMapping[str, Any]] = await _generate_dow_entities( - code_slot=code_slot - ) + dow_list: list[MutableMapping[str, Any]] = await _generate_dow_entities(code_slot=code_slot) code_slot_dict["cards"][1]["card"]["entities"].extend(dow_list) return code_slot_dict diff --git a/custom_components/keymaster/migrate.py b/custom_components/keymaster/migrate.py index e819eb83..1e1af195 100644 --- a/custom_components/keymaster/migrate.py +++ b/custom_components/keymaster/migrate.py @@ -68,9 +68,7 @@ async def migrate_2to3(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: ent = hass.states.get(ent_id) if not ent: continue - await _migrate_2to3_set_property_value( - kmlock=kmlock, prop=prop, value=ent.state - ) + await _migrate_2to3_set_property_value(kmlock=kmlock, prop=prop, value=ent.state) _LOGGER.debug("[migrate_2to3] kmlock: %s", kmlock) hass.data.setdefault(DOMAIN, {}) if COORDINATOR not in hass.data[DOMAIN]: @@ -90,9 +88,7 @@ async def migrate_2to3(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: # Delete Package files _LOGGER.info("[migrate_2to3] Deleting Package files") - await hass.async_add_executor_job( - _migrate_2to3_delete_lock_and_base_folder, hass, config_entry - ) + await hass.async_add_executor_job(_migrate_2to3_delete_lock_and_base_folder, hass, config_entry) await _migrate_2to3_reload_package_platforms(hass=hass) # Delete existing integration entities @@ -155,9 +151,7 @@ async def _migrate_2to3_create_kmlock(config_entry: ConfigEntry) -> KeymasterLoc "Sunday", ] ): - dow_slots[i] = KeymasterCodeSlotDayOfWeek( - day_of_week_num=i, day_of_week_name=dow - ) + dow_slots[i] = KeymasterCodeSlotDayOfWeek(day_of_week_num=i, day_of_week_name=dow) code_slots[x] = KeymasterCodeSlot(number=x, accesslimit_day_of_week=dow_slots) return KeymasterLock( @@ -179,9 +173,7 @@ async def _migrate_2to3_create_kmlock(config_entry: ConfigEntry) -> KeymasterLoc ) -async def _migrate_2to3_set_property_value( - kmlock: KeymasterLock, prop: str, value: Any -) -> bool: +async def _migrate_2to3_set_property_value(kmlock: KeymasterLock, prop: str, value: Any) -> bool: if "." not in prop: return False @@ -199,10 +191,8 @@ async def _migrate_2to3_set_property_value( final_prop: str = prop_list[-1] if ":" in final_prop: attr, num = final_prop.split(":") - getattr(obj, attr)[int(num)] = ( - await _migrate_2to3_validate_and_convert_property( - prop=prop, attr=attr, value=value - ) + getattr(obj, attr)[int(num)] = await _migrate_2to3_validate_and_convert_property( + prop=prop, attr=attr, value=value ) else: setattr( @@ -217,7 +207,9 @@ async def _migrate_2to3_set_property_value( async def _migrate_2to3_validate_and_convert_property(prop: str, attr: str, value) -> Any: - if keymasterlock_type_lookup.get(attr) is not None and isinstance(value, keymasterlock_type_lookup.get(attr, object)): + if keymasterlock_type_lookup.get(attr) is not None and isinstance( + value, keymasterlock_type_lookup.get(attr, object) + ): # return value pass elif keymasterlock_type_lookup.get(attr) is bool and isinstance(value, str): @@ -228,9 +220,7 @@ async def _migrate_2to3_validate_and_convert_property(prop: str, attr: str, valu except ValueError: try: time_obj: datetime = datetime.strptime(value, "%H:%M:%S") - value = round( - time_obj.hour * 60 + time_obj.minute + round(time_obj.second) - ) + value = round(time_obj.hour * 60 + time_obj.minute + round(time_obj.second)) except ValueError: _LOGGER.debug( "[migrate_2to3_set_property_value] Value Type Mismatch, cannot convert str to int. Property: %s, final_prop: %s, value: %s. Type: %s, Expected Type: %s", diff --git a/custom_components/keymaster/number.py b/custom_components/keymaster/number.py index a79fafd2..49d061a0 100644 --- a/custom_components/keymaster/number.py +++ b/custom_components/keymaster/number.py @@ -95,9 +95,7 @@ async def async_setup_entry( @dataclass(frozen=True, kw_only=True) -class KeymasterNumberEntityDescription( - KeymasterEntityDescription, NumberEntityDescription -): +class KeymasterNumberEntityDescription(KeymasterEntityDescription, NumberEntityDescription): """Entity Description for keymaster Number entities.""" @@ -126,24 +124,29 @@ def _handle_coordinator_update(self) -> None: if ( ".code_slots" in self._property - and self._kmlock and self._kmlock.parent_name - and (not self._kmlock.code_slots or not self._code_slot or not self._kmlock.code_slots[self._code_slot].override_parent) + and self._kmlock + and self._kmlock.parent_name + and ( + not self._kmlock.code_slots + or not self._code_slot + or not self._kmlock.code_slots[self._code_slot].override_parent + ) ): self._attr_available = False self.async_write_ha_state() return - if ( - ".code_slots" in self._property - and (not self._kmlock.code_slots or self._code_slot not in self._kmlock.code_slots) + if ".code_slots" in self._property and ( + not self._kmlock.code_slots or self._code_slot not in self._kmlock.code_slots ): self._attr_available = False self.async_write_ha_state() return - if ( - self._property.endswith(".accesslimit_count") - and (not self._kmlock.code_slots or not self._code_slot or not self._kmlock.code_slots[self._code_slot].accesslimit_count_enabled) + if self._property.endswith(".accesslimit_count") and ( + not self._kmlock.code_slots + or not self._code_slot + or not self._kmlock.code_slots[self._code_slot].accesslimit_count_enabled ): self._attr_available = False self.async_write_ha_state() @@ -170,8 +173,13 @@ async def async_set_native_value(self, value: float) -> None: ) if ( self._property.endswith(".accesslimit_count") - and self._kmlock and self._kmlock.parent_name - and (not self._kmlock.code_slots or not self._code_slot or not self._kmlock.code_slots[self._code_slot].override_parent) + and self._kmlock + and self._kmlock.parent_name + and ( + not self._kmlock.code_slots + or not self._code_slot + or not self._kmlock.code_slots[self._code_slot].override_parent + ) ): _LOGGER.debug( "[Number async_set_value] %s: Child lock and code slot %s not set to override parent. Ignoring change", diff --git a/custom_components/keymaster/sensor.py b/custom_components/keymaster/sensor.py index d51c91a8..422eec1c 100644 --- a/custom_components/keymaster/sensor.py +++ b/custom_components/keymaster/sensor.py @@ -15,15 +15,11 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) -async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities -): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities): """Create keymaster Sensor entities.""" coordinator: KeymasterCoordinator = hass.data[DOMAIN][COORDINATOR] - kmlock = await coordinator.get_lock_by_config_entry_id( - config_entry.entry_id - ) + kmlock = await coordinator.get_lock_by_config_entry_id(config_entry.entry_id) entities: list = [] entities.append( @@ -80,9 +76,7 @@ async def async_setup_entry( @dataclass(frozen=True, kw_only=True) -class KeymasterSensorEntityDescription( - KeymasterEntityDescription, SensorEntityDescription -): +class KeymasterSensorEntityDescription(KeymasterEntityDescription, SensorEntityDescription): """Entity Description for keymaster Sensors.""" @@ -109,9 +103,8 @@ def _handle_coordinator_update(self) -> None: self.async_write_ha_state() return - if ( - ".code_slots" in self._property - and (not self._kmlock.code_slots or self._code_slot not in self._kmlock.code_slots) + if ".code_slots" in self._property and ( + not self._kmlock.code_slots or self._code_slot not in self._kmlock.code_slots ): self._attr_available = False self.async_write_ha_state() diff --git a/custom_components/keymaster/switch.py b/custom_components/keymaster/switch.py index 4bf2cde1..75c662c7 100644 --- a/custom_components/keymaster/switch.py +++ b/custom_components/keymaster/switch.py @@ -195,9 +195,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @dataclass(frozen=True, kw_only=True) -class KeymasterSwitchEntityDescription( - KeymasterEntityDescription, SwitchEntityDescription -): +class KeymasterSwitchEntityDescription(KeymasterEntityDescription, SwitchEntityDescription): """Entitiy Description for keymaster Switches.""" @@ -231,7 +229,11 @@ def _handle_coordinator_update(self) -> None: or self._property.endswith(".notifications") ) and self._kmlock.parent_name is not None - and (not self._kmlock.code_slots or not self._code_slot or not self._kmlock.code_slots[self._code_slot].override_parent) + and ( + not self._kmlock.code_slots + or not self._code_slot + or not self._kmlock.code_slots[self._code_slot].override_parent + ) ): self._attr_available = False self.async_write_ha_state() @@ -249,9 +251,11 @@ def _handle_coordinator_update(self) -> None: if ( ".accesslimit_day_of_week" in self._property and not self._property.endswith(".accesslimit_day_of_week_enabled") - and (not self._kmlock.code_slots or not self._code_slot or not self._kmlock.code_slots[ - self._code_slot - ].accesslimit_day_of_week_enabled) + and ( + not self._kmlock.code_slots + or not self._code_slot + or not self._kmlock.code_slots[self._code_slot].accesslimit_day_of_week_enabled + ) ): self._attr_available = False self.async_write_ha_state() @@ -262,16 +266,13 @@ def _handle_coordinator_update(self) -> None: if self._code_slot is not None and code_slots and self._code_slot in code_slots: accesslimit_dow = code_slots[self._code_slot].accesslimit_day_of_week - if ( - self._property.endswith(".limit_by_time") - and ( - not code_slots - or self._code_slot is None - or not accesslimit_dow - or self._day_of_week_num is None - or self._day_of_week_num not in accesslimit_dow - or not accesslimit_dow[self._day_of_week_num].dow_enabled - ) + if self._property.endswith(".limit_by_time") and ( + not code_slots + or self._code_slot is None + or not accesslimit_dow + or self._day_of_week_num is None + or self._day_of_week_num not in accesslimit_dow + or not accesslimit_dow[self._day_of_week_num].dow_enabled ): self._attr_available = False self.async_write_ha_state() @@ -289,17 +290,14 @@ def _handle_coordinator_update(self) -> None: # self.async_write_ha_state() # return - if ( - self._property.endswith(".include_exclude") - and ( - not code_slots - or self._code_slot is None - or not accesslimit_dow - or self._day_of_week_num is None - or self._day_of_week_num not in accesslimit_dow - or not accesslimit_dow[self._day_of_week_num].dow_enabled - or not accesslimit_dow[self._day_of_week_num].limit_by_time - ) + if self._property.endswith(".include_exclude") and ( + not code_slots + or self._code_slot is None + or not accesslimit_dow + or self._day_of_week_num is None + or self._day_of_week_num not in accesslimit_dow + or not accesslimit_dow[self._day_of_week_num].dow_enabled + or not accesslimit_dow[self._day_of_week_num].limit_by_time ): self._attr_available = False self.async_write_ha_state() @@ -322,7 +320,12 @@ async def async_turn_on(self, **_) -> None: if self._set_property_value(True): self._attr_is_on = True - if self._property.endswith(".enabled") and self._kmlock and self._code_slot and self._kmlock.code_slots: + if ( + self._property.endswith(".enabled") + and self._kmlock + and self._code_slot + and self._kmlock.code_slots + ): await self.coordinator.update_slot_active_state( config_entry_id=self._config_entry.entry_id, code_slot=self._code_slot, diff --git a/custom_components/keymaster/text.py b/custom_components/keymaster/text.py index b6fb6d65..76e0e8cb 100644 --- a/custom_components/keymaster/text.py +++ b/custom_components/keymaster/text.py @@ -29,35 +29,37 @@ async def async_setup_entry( config_entry.data[CONF_START], config_entry.data[CONF_START] + config_entry.data[CONF_SLOTS], ): - entities.extend([ - KeymasterText( - entity_description=KeymasterTextEntityDescription( - key=f"text.code_slots:{x}.name", - name=f"Code Slot {x}: Name", - icon="mdi:form-textbox-lock", - entity_registry_enabled_default=True, - hass=hass, - config_entry=config_entry, - coordinator=coordinator, + entities.extend( + [ + KeymasterText( + entity_description=KeymasterTextEntityDescription( + key=f"text.code_slots:{x}.name", + name=f"Code Slot {x}: Name", + icon="mdi:form-textbox-lock", + entity_registry_enabled_default=True, + hass=hass, + config_entry=config_entry, + coordinator=coordinator, + ), ), - ), - KeymasterText( - entity_description=KeymasterTextEntityDescription( - key=f"text.code_slots:{x}.pin", - name=f"Code Slot {x}: PIN", - icon="mdi:lock-smart", - mode=( - TextMode.PASSWORD - if config_entry.data.get(CONF_HIDE_PINS) - else TextMode.TEXT + KeymasterText( + entity_description=KeymasterTextEntityDescription( + key=f"text.code_slots:{x}.pin", + name=f"Code Slot {x}: PIN", + icon="mdi:lock-smart", + mode=( + TextMode.PASSWORD + if config_entry.data.get(CONF_HIDE_PINS) + else TextMode.TEXT + ), + entity_registry_enabled_default=True, + hass=hass, + config_entry=config_entry, + coordinator=coordinator, ), - entity_registry_enabled_default=True, - hass=hass, - config_entry=config_entry, - coordinator=coordinator, ), - ) - ]) + ] + ) async_add_entities(entities, True) @@ -93,15 +95,18 @@ def _handle_coordinator_update(self) -> None: if ( ".code_slots" in self._property and self._kmlock.parent_name is not None - and (not self._kmlock.code_slots or not self._code_slot or not self._kmlock.code_slots[self._code_slot].override_parent) + and ( + not self._kmlock.code_slots + or not self._code_slot + or not self._kmlock.code_slots[self._code_slot].override_parent + ) ): self._attr_available = False self.async_write_ha_state() return - if ( - ".code_slots" in self._property - and (not self._kmlock.code_slots or self._code_slot not in self._kmlock.code_slots) + if ".code_slots" in self._property and ( + not self._kmlock.code_slots or self._code_slot not in self._kmlock.code_slots ): self._attr_available = False self.async_write_ha_state() @@ -134,8 +139,13 @@ async def async_set_value(self, value: str) -> None: return elif ( self._property.endswith(".name") - and self._kmlock and self._kmlock.parent_name - and (not self._kmlock.code_slots or not self._code_slot or not self._kmlock.code_slots[self._code_slot].override_parent) + and self._kmlock + and self._kmlock.parent_name + and ( + not self._kmlock.code_slots + or not self._code_slot + or not self._kmlock.code_slots[self._code_slot].override_parent + ) ): _LOGGER.debug( "[Text async_set_value] %s: " diff --git a/custom_components/keymaster/time.py b/custom_components/keymaster/time.py index 04dc7b3a..ca70605d 100644 --- a/custom_components/keymaster/time.py +++ b/custom_components/keymaster/time.py @@ -105,51 +105,55 @@ def _handle_coordinator_update(self) -> None: if ( ".code_slots" in self._property and self._kmlock.parent_name is not None - and (not self._kmlock.code_slots or not self._code_slot or not self._kmlock.code_slots[self._code_slot].override_parent) + and ( + not self._kmlock.code_slots + or not self._code_slot + or not self._kmlock.code_slots[self._code_slot].override_parent + ) ): self._attr_available = False self.async_write_ha_state() return - if ( - ".code_slots" in self._property - and (not self._kmlock.code_slots or self._code_slot not in self._kmlock.code_slots) + if ".code_slots" in self._property and ( + not self._kmlock.code_slots or self._code_slot not in self._kmlock.code_slots ): self._attr_available = False self.async_write_ha_state() return - if ( - ".accesslimit_day_of_week" in self._property - and (not self._kmlock.code_slots or not self._code_slot or not self._kmlock.code_slots[ - self._code_slot - ].accesslimit_day_of_week_enabled - )): + if ".accesslimit_day_of_week" in self._property and ( + not self._kmlock.code_slots + or not self._code_slot + or not self._kmlock.code_slots[self._code_slot].accesslimit_day_of_week_enabled + ): self._attr_available = False self.async_write_ha_state() return - if ( - self._property.endswith(".time_start") - or self._property.endswith(".time_end") - ): + if self._property.endswith(".time_start") or self._property.endswith(".time_end"): code_slots: MutableMapping[int, KeymasterCodeSlot] | None = self._kmlock.code_slots if self._code_slot is None or code_slots is None or self._code_slot not in code_slots: self._attr_available = False self.async_write_ha_state() return - accesslimit_day_of_week: MutableMapping[int, KeymasterCodeSlotDayOfWeek] | None = code_slots[self._code_slot].accesslimit_day_of_week - if self._day_of_week_num is None or accesslimit_day_of_week is None or self._day_of_week_num not in accesslimit_day_of_week: + accesslimit_day_of_week: MutableMapping[int, KeymasterCodeSlotDayOfWeek] | None = ( + code_slots[self._code_slot].accesslimit_day_of_week + ) + if ( + self._day_of_week_num is None + or accesslimit_day_of_week is None + or self._day_of_week_num not in accesslimit_day_of_week + ): self._attr_available = False self.async_write_ha_state() return - day_of_week: KeymasterCodeSlotDayOfWeek | None = accesslimit_day_of_week[self._day_of_week_num] - if ( - day_of_week is None or not day_of_week.dow_enabled - or not day_of_week.limit_by_time - ): + day_of_week: KeymasterCodeSlotDayOfWeek | None = accesslimit_day_of_week[ + self._day_of_week_num + ] + if day_of_week is None or not day_of_week.dow_enabled or not day_of_week.limit_by_time: self._attr_available = False self.async_write_ha_state() return @@ -181,12 +185,14 @@ async def async_set_value(self, value: dt_time) -> None: value, ) if ( - ( - self._property.endswith(".time_start") - or self._property.endswith(".time_end") + (self._property.endswith(".time_start") or self._property.endswith(".time_end")) + and self._kmlock + and self._kmlock.parent_name + and ( + not self._kmlock.code_slots + or not self._code_slot + or not self._kmlock.code_slots[self._code_slot].override_parent ) - and self._kmlock and self._kmlock.parent_name - and (not self._kmlock.code_slots or not self._code_slot or not self._kmlock.code_slots[self._code_slot].override_parent) ): _LOGGER.debug( "[Time async_set_value] %s: Child lock and code slot %s not set to override parent. Ignoring change", diff --git a/pyproject.toml b/pyproject.toml index e9cb6547..928cd117 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -362,8 +362,21 @@ timeout = 30 addopts = "-vv --cov=custom_components/keymaster --cov-report=xml" [tool.ruff] +line-length = 100 +indent-width = 4 +fix = true +force-exclude = true +target-version = "py313" required-version = ">=0.8.0" +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "lf" +docstring-code-format = true +docstring-code-line-length = "dynamic" + [tool.ruff.lint] select = [ "A001", # Variable {name} is shadowing a Python builtin @@ -608,6 +621,8 @@ ignore_errors = true description = "Lint code using ruff under {base_python}" ignore_errors = true commands = [ + ["ruff", "format", "custom_components{/}"], + ["ruff", "format", "tests{/}"], ["ruff", "check", "custom_components{/}"], ["ruff", "check", "tests{/}"], ] diff --git a/tests/common.py b/tests/common.py index a0df6f4a..1ebdbfd9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -43,9 +43,7 @@ def threadsafe_callback_factory(func): def threadsafe(*args, **kwargs): """Call func threadsafe.""" hass = args[0] - return run_callback_threadsafe( - hass.loop, ft.partial(func, *args, **kwargs) - ).result() + return run_callback_threadsafe(hass.loop, ft.partial(func, *args, **kwargs)).result() return threadsafe diff --git a/tests/conftest.py b/tests/conftest.py index 93344d94..32bcd54a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,7 +38,6 @@ def mock_get_entities(): with patch( "custom_components.keymaster.config_flow._get_entities", autospec=True ) as mock_get_entities: - mock_get_entities.return_value = [ "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", @@ -133,9 +132,7 @@ def lock_kwikset_910_fixture(client, lock_kwikset_910_state): def mock_client_fixture(controller_state, version_state, log_config_state): """Mock a client.""" - with patch( - "homeassistant.components.zwave_js.ZwaveClient", autospec=True - ) as client_class: + with patch("homeassistant.components.zwave_js.ZwaveClient", autospec=True) as client_class: client = client_class.return_value async def connect(): @@ -211,27 +208,21 @@ async def mock_zwavejs_get_usercodes(): {"code_slot": 13, "usercode": "", "in_use": False}, {"code_slot": 14, "usercode": "", "in_use": False}, ] - with patch( - "zwave_js_server.util.lock.get_usercodes", return_value=slot_data - ) as mock_usercodes: + with patch("zwave_js_server.util.lock.get_usercodes", return_value=slot_data) as mock_usercodes: yield mock_usercodes @pytest.fixture async def mock_zwavejs_clear_usercode(): """Fixture to mock clear_usercode.""" - with patch( - "zwave_js_server.util.lock.clear_usercode", return_value=None - ) as mock_usercodes: + with patch("zwave_js_server.util.lock.clear_usercode", return_value=None) as mock_usercodes: yield mock_usercodes @pytest.fixture async def mock_zwavejs_set_usercode(): """Fixture to mock set_usercode.""" - with patch( - "zwave_js_server.util.lock.set_usercode", return_value=None - ) as mock_usercodes: + with patch("zwave_js_server.util.lock.set_usercode", return_value=None) as mock_usercodes: yield mock_usercodes diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index a36b1fb8..124ec2a8 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -43,9 +43,9 @@ "start_from": 1, "hide_pins": False, "parent": None, - } + }, ) - ] + ], ) async def test_form(test_user_input, title, final_config_flow_data, hass, mock_get_entities): """Test we get the form.""" @@ -63,9 +63,7 @@ async def test_form(test_user_input, title, final_config_flow_data, hass, mock_g "custom_components.keymaster.async_setup_entry", return_value=True ) as mock_setup_entry: _LOGGER.warning("[test_form] result2 Starting") - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], test_user_input - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], test_user_input) _LOGGER.warning("[test_form] result2: %s", result2) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == title