diff --git a/custom_components/duofern/__init__.py b/custom_components/duofern/__init__.py index 3c0d910..5d70664 100644 --- a/custom_components/duofern/__init__.py +++ b/custom_components/duofern/__init__.py @@ -1,10 +1,11 @@ +import asyncio import logging import os import re from typing import Any # from homeassistant.const import 'serial_port', 'config_file', 'code' -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.config_entries import ConfigEntry import homeassistant.helpers.config_validation as cv import voluptuous as vol @@ -25,6 +26,13 @@ _LOGGER = logging.getLogger(__name__) from .const import DOMAIN, DUOFERN_COMPONENTS +from .domain_data import _getData +from custom_components.duofern.domain_data import getDuofernStick, isDeviceSetUp, saveDeviceAsSetUp, unsetupDevice + +from homeassistant.helpers.device_registry import DeviceEntry + +SERVICES = ['start_pairing', 'start_unpairing', 'clean_config', 'dump_device_state', 'ask_for_update', + 'set_update_interval'] # Validation of the user's configuration CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({ @@ -37,6 +45,62 @@ }, extra=vol.ALLOW_EXTRA) +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + stick = getDuofernStick(hass) + if device_entry.name in stick.duofern_parser.modules["by_code"]: + del stick.duofern_parser.modules["by_code"][device_entry.name] + stick.config['devices'] = [dev for dev in stick.config['devices'] if dev['id'] != device_entry.name] + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload deCONZ config entry.""" + + stick = getDuofernStick(hass) + stick.sync_devices() + stick.stop() + try: + stick.serial_connection.close() + except: + _LOGGER.exception("closing serial connection failed") + + await asyncio.sleep(0.5) + + + + for duofernDevice in stick.config['devices']: + _LOGGER.info(f"unsetting up device {duofernDevice}") + duofernId: str = duofernDevice['id'] + if not isDeviceSetUp(hass, duofernId): + continue + _LOGGER.info(f"unsetting up device {duofernDevice}") + unsetupDevice(hass, duofernId) + + for component in DUOFERN_COMPONENTS: + hass.async_create_task( + hass.config_entries.async_forward_entry_unload(config_entry, component) + ) + + + newstick = DuofernStickThreaded(serial_port=stick.port, system_code=stick.system_code, + config_file_json=stick.config_file, + ephemeral=False) + newstick.start() + hass.data[DOMAIN]['stick'] = newstick + del stick + + return True + + +@callback +def async_unload_services(hass: HomeAssistant) -> None: + for service in SERVICES: + hass.services.async_remove(DOMAIN, service) + + def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Setup the duofern stick for communicating with the duofern devices via entities""" configEntries = hass.config_entries.async_entries(DOMAIN) @@ -137,8 +201,9 @@ def get_device_id(hass_entity_id): _LOGGER.info("Asking specific devices for update") 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}, i know {hass.data[DOMAIN]['deviceByHassId']}, fyi deviceByID is {hass.data[DOMAIN]['devices']}") - for id,dev in hass.data[DOMAIN]['deviceByHassId'].items(): + _LOGGER.exception( + f"Exception while getting device id {call}, {call.data}, i know {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_ids is None: @@ -150,7 +215,8 @@ def get_device_id(hass_entity_id): for device_id in device_ids: if device_id is not None: 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.") + _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"asking {device_id} for update") getDuofernStick(hass).command(device_id, 'getStatus') @@ -182,7 +248,7 @@ def set_update_interval(call: ServiceCall) -> None: hass.services.register(DOMAIN, 'start_pairing', start_pairing, PAIRING_SCHEMA) hass.services.register(DOMAIN, 'start_unpairing', start_unpairing, PAIRING_SCHEMA) hass.services.register(DOMAIN, 'sync_devices', sync_devices) - hass.services.register(DOMAIN, 'clean_config', clean_config) + #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 5292030..f5c6d9c 100644 --- a/custom_components/duofern/cover.py +++ b/custom_components/duofern/cover.py @@ -102,8 +102,10 @@ def current_cover_position(self) -> int | None: return self._state @property - def is_closed(self) -> bool: + def is_closed(self) -> bool | None: """Return true if cover is close.""" + if self._state is None: + return None return self._state == 0 @property diff --git a/custom_components/duofern/domain_data.py b/custom_components/duofern/domain_data.py index 804dd35..a79d151 100644 --- a/custom_components/duofern/domain_data.py +++ b/custom_components/duofern/domain_data.py @@ -6,6 +6,7 @@ from custom_components.duofern.const import DOMAIN + class DuofernDomainData(TypedDict): stick: DuofernStickThreaded devices: dict[str, Entity] @@ -15,13 +16,26 @@ class DuofernDomainData(TypedDict): def getDuofernStick(hass: HomeAssistant) -> DuofernStickThreaded: return _getData(hass)['stick'] + def isDeviceSetUp(hass: HomeAssistant, duofernId: str, subIdWithinHassDevice: str = "") -> bool: return (duofernId + subIdWithinHassDevice) in _getData(hass)['devices'] + def saveDeviceAsSetUp(hass: HomeAssistant, device: Entity, duofernId: str, subIdWithinHassDevice: str = "") -> None: _getData(hass)['devices'][duofernId + subIdWithinHassDevice] = device _getData(hass)['deviceByHassId'][device.unique_id] = device + +def unsetupDevice(hass: HomeAssistant, duofernId: str) -> None: + device_ids = [d for d in _getData(hass)['devices'] if d.startswith(duofernId)] + unique_ids = [_getData(hass)['devices'][d].unique_id for d in device_ids] + for did in device_ids: + if did in _getData(hass)['devices']: + del _getData(hass)['devices'][did] + for uid in unique_ids: + del _getData(hass)['deviceByHassId'][uid] + + def setupDomainData(hass: HomeAssistant, stick: DuofernStickThreaded) -> None: hass.data[DOMAIN] = DuofernDomainData({ 'stick': stick, @@ -29,5 +43,6 @@ def setupDomainData(hass: HomeAssistant, stick: DuofernStickThreaded) -> None: 'deviceByHassId': {} }) + def _getData(hass: HomeAssistant) -> DuofernDomainData: return cast(DuofernDomainData, hass.data[DOMAIN]) diff --git a/custom_components/duofern/services.yaml b/custom_components/duofern/services.yaml index 4560c62..bb17c17 100644 --- a/custom_components/duofern/services.yaml +++ b/custom_components/duofern/services.yaml @@ -1,16 +1,12 @@ # Describes the format for available Wink services start_pairing: - description: Pair duofern devices. Remember - There is no pairing UI. To pick up the newly paired devices later, you need to call sync_devices and possibly restart HA. + description: Pair duofern devices. Remember - There is no pairing UI. To pick up the newly paired devices later, you need to restart HA. fields: timeout: description: timeout in seconds example: 60 -sync_devices: - description: Re-sync Devices (trigger after pairing, if it does not work, a restart of homeassistant may help). Also writes duofern config file. - May raise warnings for already-created devices. Do not worry. - clean_config: description: Clean the duofern config. More info in the readme. diff --git a/readme.md b/readme.md index 292c2e7..3de7be9 100644 --- a/readme.md +++ b/readme.md @@ -43,17 +43,10 @@ To use ``pyduofern`` within [Homeassistant](https://home-assistant.io/), add the There are some services you can call via the service interface. A few of these to get you started: -``duofern.start_pairing`` starts the pairing mode for a given number of seconds. +``duofern.start_pairing`` starts the pairing mode for a given number of seconds. After pairing reload the integration to make the new devices visible. ![Pairing](./pairing.png) -``duofern.sync_devices`` will force-sync any newly discovered devices. - -![sync](./sync_devices.png) - -Please use the renaming feature in the homeassistant GUI to arrive at human readable -names for your deices. - ``duofern.ask_for_update`` Ask duofern devices to re-send their state in case. Can be used in setups where RF is finnicky. @@ -61,30 +54,5 @@ Ask duofern devices to re-send their state in case. Can be used in setups where ``duofern.dump_device_state`` Dump the current last received state for all duofern modules as a warning level message to the log. This reflects the current state of all RF messages received from devices - What's not here wasn't received by the stick or came in garbled. -``duofern.clean_config`` -> **Warning** -> You should absolutely NOT use it if you have been running duofern for a long time and your covers have "human" names in the .duofern.json file. That option hasn't been used for a long time though - it is still from the time when homeassistant had no UI way of renaming entities/devices. - -**Use when:** -- you have "ghost" devices that do not correspond to a physical device - -**Use like this:** -- If you want to be sure you can go back: backup ``duofern.json``. -- Call ``duofern.clean_config``. -- Restart homeassistant. -- Observe that all your duofern devices are now disabled/unavailable. -- Toggle/move all your duofern devices at the device to make sure that they send messages for homeassistant to pick up. -- You can diagnose what devices were picked up again using ``duofern.dump_device_state``. -- Once all devices are there: call ``duofern.sync_devices``. -- Restart homeassistant for good measure. -- Observe that the devices are now back. -- If some are still missing: toggle them at the device and diagnose using ``dump_device_state`` until they are found again. -- Once they are: ``duofern.sync_devices``, final restart. -- Everything works. -- If not: maybe you want to return to your backed-up ``duofern.json``. - -The duofern python module keeps a list of devices that are paired. ``clean_config`` throws that list away. - -In normal operation, the list should rebuild itself - whenever a message is received from a device that was previously paired it should appear in the list. -It's not very well tested because it's not a common situation. I ran it, restarted homeassistant, and my devices became available again after a few seconds. - +``duofern.sync_devices`` +Write the duofern config file with the known devices. normally not required from the user. \ No newline at end of file diff --git a/sync_devices.png b/sync_devices.png deleted file mode 100644 index c75b02d..0000000 Binary files a/sync_devices.png and /dev/null differ