Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
gluap committed Jan 9, 2024
2 parents 55f760b + 9308026 commit 06a7812
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 46 deletions.
76 changes: 71 additions & 5 deletions custom_components/duofern/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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({
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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')
Expand Down Expand Up @@ -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)
4 changes: 3 additions & 1 deletion custom_components/duofern/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions custom_components/duofern/domain_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from custom_components.duofern.const import DOMAIN


class DuofernDomainData(TypedDict):
stick: DuofernStickThreaded
devices: dict[str, Entity]
Expand All @@ -15,19 +16,33 @@ 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,
'devices': {},
'deviceByHassId': {}
})


def _getData(hass: HomeAssistant) -> DuofernDomainData:
return cast(DuofernDomainData, hass.data[DOMAIN])
6 changes: 1 addition & 5 deletions custom_components/duofern/services.yaml
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
38 changes: 3 additions & 35 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,48 +43,16 @@ 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.

``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.
Binary file removed sync_devices.png
Binary file not shown.

0 comments on commit 06a7812

Please sign in to comment.