Skip to content

Commit 06a7812

Browse files
committed
Merge remote-tracking branch 'origin/master'
2 parents 55f760b + 9308026 commit 06a7812

File tree

6 files changed

+93
-46
lines changed

6 files changed

+93
-46
lines changed

custom_components/duofern/__init__.py

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import asyncio
12
import logging
23
import os
34
import re
45
from typing import Any
56

67
# from homeassistant.const import 'serial_port', 'config_file', 'code'
7-
from homeassistant.core import HomeAssistant, ServiceCall
8+
from homeassistant.core import HomeAssistant, ServiceCall, callback
89
from homeassistant.config_entries import ConfigEntry
910
import homeassistant.helpers.config_validation as cv
1011
import voluptuous as vol
@@ -25,6 +26,13 @@
2526
_LOGGER = logging.getLogger(__name__)
2627

2728
from .const import DOMAIN, DUOFERN_COMPONENTS
29+
from .domain_data import _getData
30+
from custom_components.duofern.domain_data import getDuofernStick, isDeviceSetUp, saveDeviceAsSetUp, unsetupDevice
31+
32+
from homeassistant.helpers.device_registry import DeviceEntry
33+
34+
SERVICES = ['start_pairing', 'start_unpairing', 'clean_config', 'dump_device_state', 'ask_for_update',
35+
'set_update_interval']
2836

2937
# Validation of the user's configuration
3038
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
@@ -37,6 +45,62 @@
3745
}, extra=vol.ALLOW_EXTRA)
3846

3947

48+
async def async_remove_config_entry_device(
49+
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
50+
) -> bool:
51+
"""Remove a config entry from a device."""
52+
stick = getDuofernStick(hass)
53+
if device_entry.name in stick.duofern_parser.modules["by_code"]:
54+
del stick.duofern_parser.modules["by_code"][device_entry.name]
55+
stick.config['devices'] = [dev for dev in stick.config['devices'] if dev['id'] != device_entry.name]
56+
return True
57+
58+
59+
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
60+
"""Unload deCONZ config entry."""
61+
62+
stick = getDuofernStick(hass)
63+
stick.sync_devices()
64+
stick.stop()
65+
try:
66+
stick.serial_connection.close()
67+
except:
68+
_LOGGER.exception("closing serial connection failed")
69+
70+
await asyncio.sleep(0.5)
71+
72+
73+
74+
for duofernDevice in stick.config['devices']:
75+
_LOGGER.info(f"unsetting up device {duofernDevice}")
76+
duofernId: str = duofernDevice['id']
77+
if not isDeviceSetUp(hass, duofernId):
78+
continue
79+
_LOGGER.info(f"unsetting up device {duofernDevice}")
80+
unsetupDevice(hass, duofernId)
81+
82+
for component in DUOFERN_COMPONENTS:
83+
hass.async_create_task(
84+
hass.config_entries.async_forward_entry_unload(config_entry, component)
85+
)
86+
87+
88+
newstick = DuofernStickThreaded(serial_port=stick.port, system_code=stick.system_code,
89+
config_file_json=stick.config_file,
90+
ephemeral=False)
91+
newstick.start()
92+
hass.data[DOMAIN]['stick'] = newstick
93+
del stick
94+
95+
return True
96+
97+
98+
@callback
99+
def async_unload_services(hass: HomeAssistant) -> None:
100+
for service in SERVICES:
101+
hass.services.async_remove(DOMAIN, service)
102+
103+
40104
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
41105
"""Setup the duofern stick for communicating with the duofern devices via entities"""
42106
configEntries = hass.config_entries.async_entries(DOMAIN)
@@ -137,8 +201,9 @@ def get_device_id(hass_entity_id):
137201
_LOGGER.info("Asking specific devices for update")
138202
device_ids = [get_device_id(i) for i in hass_device_id]
139203
except Exception:
140-
_LOGGER.exception(f"Exception while getting device id {call}, {call.data}, i know {hass.data[DOMAIN]['deviceByHassId']}, fyi deviceByID is {hass.data[DOMAIN]['devices']}")
141-
for id,dev in hass.data[DOMAIN]['deviceByHassId'].items():
204+
_LOGGER.exception(
205+
f"Exception while getting device id {call}, {call.data}, i know {hass.data[DOMAIN]['deviceByHassId']}, fyi deviceByID is {hass.data[DOMAIN]['devices']}")
206+
for id, dev in hass.data[DOMAIN]['deviceByHassId'].items():
142207
_LOGGER.warning(f"{id}, {dev.__dict__}")
143208
raise
144209
if device_ids is None:
@@ -150,7 +215,8 @@ def get_device_id(hass_entity_id):
150215
for device_id in device_ids:
151216
if device_id is not None:
152217
if device_id not in hass.data[DOMAIN]['stick'].duofern_parser.modules['by_code']:
153-
_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.")
218+
_LOGGER.warning(
219+
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.")
154220
continue
155221
_LOGGER.info(f"asking {device_id} for update")
156222
getDuofernStick(hass).command(device_id, 'getStatus')
@@ -182,7 +248,7 @@ def set_update_interval(call: ServiceCall) -> None:
182248
hass.services.register(DOMAIN, 'start_pairing', start_pairing, PAIRING_SCHEMA)
183249
hass.services.register(DOMAIN, 'start_unpairing', start_unpairing, PAIRING_SCHEMA)
184250
hass.services.register(DOMAIN, 'sync_devices', sync_devices)
185-
hass.services.register(DOMAIN, 'clean_config', clean_config)
251+
#hass.services.register(DOMAIN, 'clean_config', clean_config)
186252
hass.services.register(DOMAIN, 'dump_device_state', dump_device_state)
187253
hass.services.register(DOMAIN, 'ask_for_update', ask_for_update, UPDATE_SCHEMA)
188254
hass.services.register(DOMAIN, 'set_update_interval', set_update_interval, UPDATE_INTERVAL_SCHEMA)

custom_components/duofern/cover.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,10 @@ def current_cover_position(self) -> int | None:
102102
return self._state
103103

104104
@property
105-
def is_closed(self) -> bool:
105+
def is_closed(self) -> bool | None:
106106
"""Return true if cover is close."""
107+
if self._state is None:
108+
return None
107109
return self._state == 0
108110

109111
@property

custom_components/duofern/domain_data.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from custom_components.duofern.const import DOMAIN
88

9+
910
class DuofernDomainData(TypedDict):
1011
stick: DuofernStickThreaded
1112
devices: dict[str, Entity]
@@ -15,19 +16,33 @@ class DuofernDomainData(TypedDict):
1516
def getDuofernStick(hass: HomeAssistant) -> DuofernStickThreaded:
1617
return _getData(hass)['stick']
1718

19+
1820
def isDeviceSetUp(hass: HomeAssistant, duofernId: str, subIdWithinHassDevice: str = "") -> bool:
1921
return (duofernId + subIdWithinHassDevice) in _getData(hass)['devices']
2022

23+
2124
def saveDeviceAsSetUp(hass: HomeAssistant, device: Entity, duofernId: str, subIdWithinHassDevice: str = "") -> None:
2225
_getData(hass)['devices'][duofernId + subIdWithinHassDevice] = device
2326
_getData(hass)['deviceByHassId'][device.unique_id] = device
2427

28+
29+
def unsetupDevice(hass: HomeAssistant, duofernId: str) -> None:
30+
device_ids = [d for d in _getData(hass)['devices'] if d.startswith(duofernId)]
31+
unique_ids = [_getData(hass)['devices'][d].unique_id for d in device_ids]
32+
for did in device_ids:
33+
if did in _getData(hass)['devices']:
34+
del _getData(hass)['devices'][did]
35+
for uid in unique_ids:
36+
del _getData(hass)['deviceByHassId'][uid]
37+
38+
2539
def setupDomainData(hass: HomeAssistant, stick: DuofernStickThreaded) -> None:
2640
hass.data[DOMAIN] = DuofernDomainData({
2741
'stick': stick,
2842
'devices': {},
2943
'deviceByHassId': {}
3044
})
3145

46+
3247
def _getData(hass: HomeAssistant) -> DuofernDomainData:
3348
return cast(DuofernDomainData, hass.data[DOMAIN])

custom_components/duofern/services.yaml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
# Describes the format for available Wink services
22

33
start_pairing:
4-
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.
4+
description: Pair duofern devices. Remember - There is no pairing UI. To pick up the newly paired devices later, you need to restart HA.
55
fields:
66
timeout:
77
description: timeout in seconds
88
example: 60
99

10-
sync_devices:
11-
description: Re-sync Devices (trigger after pairing, if it does not work, a restart of homeassistant may help). Also writes duofern config file.
12-
May raise warnings for already-created devices. Do not worry.
13-
1410
clean_config:
1511
description: Clean the duofern config. More info in the readme.
1612

readme.md

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -43,48 +43,16 @@ To use ``pyduofern`` within [Homeassistant](https://home-assistant.io/), add the
4343

4444
There are some services you can call via the service interface. A few of these to get you started:
4545

46-
``duofern.start_pairing`` starts the pairing mode for a given number of seconds.
46+
``duofern.start_pairing`` starts the pairing mode for a given number of seconds. After pairing reload the integration to make the new devices visible.
4747

4848
![Pairing](./pairing.png)
4949

50-
``duofern.sync_devices`` will force-sync any newly discovered devices.
51-
52-
![sync](./sync_devices.png)
53-
54-
Please use the renaming feature in the homeassistant GUI to arrive at human readable
55-
names for your deices.
56-
5750
``duofern.ask_for_update``
5851

5952
Ask duofern devices to re-send their state in case. Can be used in setups where RF is finnicky.
6053

6154
``duofern.dump_device_state``
6255
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.
6356

64-
``duofern.clean_config``
65-
> **Warning**
66-
> 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.
67-
68-
**Use when:**
69-
- you have "ghost" devices that do not correspond to a physical device
70-
71-
**Use like this:**
72-
- If you want to be sure you can go back: backup ``duofern.json``.
73-
- Call ``duofern.clean_config``.
74-
- Restart homeassistant.
75-
- Observe that all your duofern devices are now disabled/unavailable.
76-
- Toggle/move all your duofern devices at the device to make sure that they send messages for homeassistant to pick up.
77-
- You can diagnose what devices were picked up again using ``duofern.dump_device_state``.
78-
- Once all devices are there: call ``duofern.sync_devices``.
79-
- Restart homeassistant for good measure.
80-
- Observe that the devices are now back.
81-
- If some are still missing: toggle them at the device and diagnose using ``dump_device_state`` until they are found again.
82-
- Once they are: ``duofern.sync_devices``, final restart.
83-
- Everything works.
84-
- If not: maybe you want to return to your backed-up ``duofern.json``.
85-
86-
The duofern python module keeps a list of devices that are paired. ``clean_config`` throws that list away.
87-
88-
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.
89-
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.
90-
57+
``duofern.sync_devices``
58+
Write the duofern config file with the known devices. normally not required from the user.

sync_devices.png

-28.5 KB
Binary file not shown.

0 commit comments

Comments
 (0)