Skip to content

Commit

Permalink
- fix the ask for update service to accept lists of duofern devices
Browse files Browse the repository at this point in the history
- finally found a way to avoid having layers upon layers of code to thread the configuration through - add setter for the automatic poll interval.
  • Loading branch information
gluap committed Mar 31, 2023
1 parent 979f4cf commit 3447347
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 28 deletions.
63 changes: 48 additions & 15 deletions custom_components/duofern/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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)


Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -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)
24 changes: 13 additions & 11 deletions custom_components/duofern/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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):
Expand All @@ -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)


Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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}")
17 changes: 15 additions & 2 deletions custom_components/duofern/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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

0 comments on commit 3447347

Please sign in to comment.