diff --git a/eventhandler.py b/eventhandler.py index 375553c..8afaf3b 100644 --- a/eventhandler.py +++ b/eventhandler.py @@ -1,4 +1,6 @@ import appdaemon.plugins.hass.hassapi as hass +import urllib.request +import json # ============================================================================= # EventDispatcher / EventHandler — HA event subscription helpers @@ -13,6 +15,11 @@ import appdaemon.plugins.hass.hassapi as hass # event_data : dict of key/value pairs that must all match the event # payload (deep partial match — nested dicts are matched # recursively). None means "match any payload". +# Special key: ``device_name`` — for zha_event payloads you +# may supply the friendly ZHA device name instead of the raw +# IEEE address. It is resolved once at construction time via +# the ``zha/get_devices`` service and stored as +# ``device_ieee`` in the effective filter dict. # reset_data : optional dict. When set, the dispatcher becomes a # one-shot latch: the callback fires once on event_data # match, then waits for a reset_data match before it can @@ -51,13 +58,15 @@ import appdaemon.plugins.hass.hassapi as hass class EventDispatcher: def __init__(self,ad_api,event_name,callback,event_data,reset_data,event_context): + self.ad_api = ad_api + event_data = self._resolve_zha_device_name(event_data) + reset_data = self._resolve_zha_device_name(reset_data) self.event_name = event_name self.callback = callback self.event_data = event_data self.reset_data = reset_data self.waiting_for_reset = False self.event_context = event_context - self.ad_api = ad_api if event_data == None: self.ad_api.listen_event(self.on_event,event_name) else: @@ -78,6 +87,59 @@ class EventDispatcher: def on_event(self, event_name, data, kwargs): self.process_event(data) + def _lookup_zha_ieee(self, device_name): + # Resolve a ZHA device friendly name to its IEEE address by rendering + # a Jinja2 template via the HA REST API (/api/template). + # Retrieves the HA URL and token from the HASS plugin config object. + try: + plugin = self.ad_api.AD.plugins.get_plugin_object('default') + ha_url = str(plugin.config.ha_url).rstrip('/') + token = plugin.config.token.get_secret_value() + template = ( + "{% set ns = namespace(ieee='') %}" + "{% for eid in integration_entities('zha') %}" + " {% set did = device_id(eid) %}" + " {% if did and not ns.ieee %}" + " {% if device_attr(did, 'name_by_user') == '" + device_name + "'" + " or device_attr(did, 'name') == '" + device_name + "' %}" + " {% for conn in device_attr(did, 'connections') %}" + " {% if conn[0] == 'zigbee' %}{% set ns.ieee = conn[1] %}{% endif %}" + " {% endfor %}" + " {% endif %}" + " {% endif %}" + "{% endfor %}" + "{{ ns.ieee }}" + ) + payload = json.dumps({'template': template}).encode('utf-8') + req = urllib.request.Request( + f"{ha_url}/api/template", + data=payload, + headers={'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}, + method='POST', + ) + with urllib.request.urlopen(req, timeout=10) as resp: + result = resp.read().decode('utf-8').strip() + return result if result else None + except Exception as e: + self.ad_api.log_error(f"[EventDispatcher] Failed to resolve ZHA device name '{device_name}': {e}") + return None + + def _resolve_zha_device_name(self, data): + # Replace a device_name key in data with device_ieee looked up from + # the ZHA device registry. Returns data unchanged when the key is + # absent or data is None. + if data is None or 'device_name' not in data: + return data + name = data['device_name'] + ieee = self._lookup_zha_ieee(name) + resolved = {k: v for k, v in data.items() if k != 'device_name'} + if ieee is not None: + self.ad_api.log_info(f"[EventDispatcher] Resolved ZHA device name '{name}' to IEEE '{ieee}'") + resolved['device_ieee'] = ieee + else: + self.ad_api.log_error(f"[EventDispatcher] ZHA device '{name}' not found — 'device_name' filter ignored") + return resolved + def process_event(self,data): def are_data_matching(ref_data, data): if ref_data != None: @@ -119,10 +181,7 @@ class EventHandler: for event_block in events_block.values(): register_event_with_params(event_block,callback,event_context) - def log(self,message,**kwargs): - self.__ad_api.log(message,**kwargs) - def add_dispatcher(self,event_name,callback,event_data = None,reset_data = None ,event_context = None): - self.log(f'Registering dispatcher {callback.__name__} for event "{event_name}" ({event_data})') + self.__ad_api.log_info(f'Registering dispatcher {callback.__name__} for event "{event_name}" ({event_data})') dispatcher = EventDispatcher(self.__ad_api,event_name,callback,event_data,reset_data,event_context) self.event_dispatchers.append(dispatcher) \ No newline at end of file