From 3447347d46ec1e2e69f3220ae4a4dc4e0bb64ba8 Mon Sep 17 00:00:00 2001 From: gluap Date: Fri, 31 Mar 2023 16:40:51 +0200 Subject: [PATCH] - fix the ask for update service to accept lists of duofern devices - finally found a way to avoid having layers upon layers of code to thread the configuration through - add setter for the automatic poll interval. --- custom_components/duofern/__init__.py | 63 +++++++++++++++++++------ custom_components/duofern/cover.py | 24 +++++----- custom_components/duofern/services.yaml | 17 ++++++- 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/custom_components/duofern/__init__.py b/custom_components/duofern/__init__.py index 344c25b..213b71d 100644 --- a/custom_components/duofern/__init__.py +++ b/custom_components/duofern/__init__.py @@ -9,6 +9,7 @@ import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers import entity_registry from pyduofern.duofern_stick import DuofernStickThreaded @@ -52,7 +53,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: configfile = configEntries[0].data['config_file'] stick = DuofernStickThreaded(serial_port=serial_port, system_code=code, config_file_json=configfile, - ephemeral=False) + ephemeral=False) _registerServices(hass, stick, configEntries[0]) _registerUpdateHassFromStickCallback(hass, stick) @@ -62,6 +63,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: return True + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Setup the Duofern Config entries (entities, devices, etc...)""" for component in DUOFERN_COMPONENTS: @@ -72,11 +74,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True - def _registerStartStickHook(hass: HomeAssistant, stick: DuofernStickThreaded) -> None: def started_callback(event: Any) -> None: - stick.start() # Start the stick when ha is ready - + stick.start() # Start the stick when ha is ready + hass.bus.listen("homeassistant_started", started_callback) @@ -85,17 +86,18 @@ def update_callback(id: str | None, key: Any, value: Any) -> None: if id is not None: try: _LOGGER.info(f"Updatecallback for {id}") - device = hass.data[DOMAIN]['devices'][id] # Get device by id + device = hass.data[DOMAIN]['devices'][id] # Get device by id if device.enabled: try: - device.schedule_update_ha_state(True) # Trigger update on the updated entity + device.schedule_update_ha_state(True) # Trigger update on the updated entity except AssertionError: - _LOGGER.info("Update callback called before HA is ready") # Trying to update before HA is ready + _LOGGER.info("Update callback called before HA is ready") # Trying to update before HA is ready except KeyError: - _LOGGER.info("Update callback called on unknown device id") # Ignore invalid device ids + _LOGGER.info("Update callback called on unknown device id") # Ignore invalid device ids stick.add_updates_callback(update_callback) + def _registerServices(hass: HomeAssistant, stick: DuofernStickThreaded, entry: ConfigEntry) -> None: def start_pairing(call: ServiceCall) -> None: _LOGGER.warning("start pairing") @@ -118,26 +120,56 @@ def clean_config(call: ServiceCall) -> None: stick.sync_devices() def ask_for_update(call: ServiceCall) -> None: + device_id = None + + def get_device_id(hass_entity_id): + for ent in hass.data[DOMAIN]['devices'].values(): + if ent.entity_id == hass_entity_id: + return ent._duofernId + return None + try: hass_device_id = call.data.get('device_id', None) - device_id = re.sub(r"[^\.]*.([0-9a-fA-F]+)", "\\1", hass_device_id) if hass_device_id is not None else None + if not isinstance(hass_device_id, list): + device_ids = [get_device_id(hass_device_id)] + else: + device_ids = [get_device_id(i) for i in hass_device_id] except Exception: - _LOGGER.exception(f"exception while getting device id {call}, {call.data}") + _LOGGER.exception(f"exception while getting device id {call}, {call.data}, i konw {hass.data[DOMAIN]['deviceByHassId']}, fyi deviceByID is {hass.data[DOMAIN]['devices']}") + for id,dev in hass.data[DOMAIN]['deviceByHassId'].items(): + _LOGGER.warning(f"{id}, {dev.__dict__}") raise - if device_id is None: + if device_ids is None: _LOGGER.warning(f"device_id missing from call {call.data}") return - if device_id not in hass.data[DOMAIN]['stick'].duofern_parser.modules['by_code']: - _LOGGER.warning(f"{device_id} is not a valid duofern device, I only know {hass.data[DOMAIN]['stick'].duofern_parser.modules['by_code'].keys()}") + for device_id in device_ids: + if device_id not in hass.data[DOMAIN]['stick'].duofern_parser.modules['by_code']: + _LOGGER.warning(f"{device_id} is not a valid duofern device, I only know {hass.data[DOMAIN]['stick'].duofern_parser.modules['by_code'].keys()}. Gonna handle the other devices in {device_ids} though.") + continue + _LOGGER.info(f"scheduling update for {device_id}") + getDuofernStick(hass).command(device_id, 'getStatus') + + def set_update_interval(call: ServiceCall) -> None: + try: + period_minutes = call.data.get('period_minutes', None) + if period_minutes == int(0): + period_minutes = None + _LOGGER.warning("set period_minutes to 0 - no updates will be triggered automatically") + except Exception: + _LOGGER.warning("something went wrong while reading period from parameters") return - getDuofernStick(hass).command(device_id, 'getStatus') + getDuofernStick(hass).updating_interval = period_minutes PAIRING_SCHEMA = vol.Schema({ vol.Optional('timeout', default=30): cv.positive_int, }) UPDATE_SCHEMA = vol.Schema({ - vol.Required('device_id', default=None): cv.string, + vol.Required('device_id', default=list): list, + }) + + UPDATE_INTERVAL_SCHEMA = vol.Schema({ + vol.Optional('period_minutes', default=5): cv.positive_int, }) hass.services.register(DOMAIN, 'start_pairing', start_pairing, PAIRING_SCHEMA) @@ -146,3 +178,4 @@ def ask_for_update(call: ServiceCall) -> None: hass.services.register(DOMAIN, 'clean_config', clean_config) hass.services.register(DOMAIN, 'dump_device_state', dump_device_state) hass.services.register(DOMAIN, 'ask_for_update', ask_for_update, UPDATE_SCHEMA) + hass.services.register(DOMAIN, 'set_update_interval', set_update_interval, UPDATE_INTERVAL_SCHEMA) diff --git a/custom_components/duofern/cover.py b/custom_components/duofern/cover.py index c9276fe..77a87d8 100644 --- a/custom_components/duofern/cover.py +++ b/custom_components/duofern/cover.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity import DeviceInfo +from random import randint from pyduofern.duofern_stick import DuofernStickThreaded @@ -21,7 +22,7 @@ CoverEntityFeature ) -from custom_components.duofern.domain_data import getDuofernStick, isDeviceSetUp, saveDeviceAsSetUp +from custom_components.duofern.domain_data import getDuofernStick, isDeviceSetUp, saveDeviceAsSetUp from .const import DOMAIN @@ -32,20 +33,20 @@ SHUTTER_IDS = {"40", "41", "42", "47", "49", "4b", "4c", "4e", "70", "61"} -def is_shutter(id: str)-> bool: +def is_shutter(id: str) -> bool: return any([id.startswith(i) for i in SHUTTER_IDS]) async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Setup the Awesome Light platform.""" stick = getDuofernStick(hass) - to_add:List[DuofernShutter] = [] + to_add: List[DuofernShutter] = [] for duofernDevice in stick.config['devices']: duofernId: str = duofernDevice['id'] if not is_shutter(duofernId): @@ -59,7 +60,7 @@ async def async_setup_entry( entity = DuofernShutter(duofernId, duofernDevice['name'], stick) to_add.append(entity) saveDeviceAsSetUp(hass, entity, duofernId) - + async_add_entities(to_add) @@ -74,7 +75,7 @@ def __init__(self, duofernId: str, name: str, stick: DuofernStickThreaded): self._stick = stick self._openclose: Literal["up", "down", "stop"] = 'stop' self._last_update_time = datetime.datetime.now() - self._updating_interval = 5 + self._stick.updating_interval = 5 @property def name(self) -> str: @@ -95,7 +96,7 @@ def device_info(self) -> DeviceInfo: "name": self.name, "default_manufacturer": "Rademacher", "default_name": "Unkown Duofern Device", - } #type: ignore #(We only care about a subset and DeviceInfo doesn't mark the rest as optional) + } # type: ignore #(We only care about a subset and DeviceInfo doesn't mark the rest as optional) @property def current_cover_position(self) -> int | None: @@ -167,7 +168,8 @@ def update(self) -> None: self._openclose = self._stick.duofern_parser.modules['by_code'][self._duofernId]['moving'] except KeyError: self._state = None - if datetime.datetime.now() - self._last_update_time > datetime.timedelta(minutes=self._updating_interval): + if self._stick.updating_interval is not None and \ + datetime.datetime.now() - self._last_update_time > datetime.timedelta(minutes=self._stick.updating_interval): self._stick.command(self._duofernId, 'getStatus') - self._last_update_time = datetime.datetime.now() + self._last_update_time = datetime.datetime.now() + datetime.timedelta(seconds=randint(0, 60)) _LOGGER.info(f"{self._duofernId} state is now {self._state}") diff --git a/custom_components/duofern/services.yaml b/custom_components/duofern/services.yaml index db7cd39..8084293 100644 --- a/custom_components/duofern/services.yaml +++ b/custom_components/duofern/services.yaml @@ -20,7 +20,20 @@ ask_for_update: fields: device_id: description: id of device to ask for an update + required: true selector: entity: - multiple: false - integration: duofern \ No newline at end of file + multiple: true + integration: duofern + +set_update_interval: + description: set the automatically broadcasting a "please send an update" interval + fields: + period_minutes: + description: approximate forced refresh interval in minutes - defaults to 5, 0 means never + example: 5 + required: true + selector: + number: + min: 0 + max: 60