More refactors and fixes for `zigbee.mqtt`

This commit is contained in:
Fabio Manganiello 2023-01-13 02:58:47 +01:00
parent 38438230d7
commit 22a566a88b
Signed by: blacklight
GPG Key ID: D90FBA7F76362774
3 changed files with 100 additions and 113 deletions

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="entity-container-wrapper" <div class="entity-container-wrapper"
:class="{'with-children': hasChildren, collapsed: isCollapsed}"> :class="{'with-children': hasChildren, collapsed: isCollapsed, hidden: !value?.name?.length}">
<div class="row item entity-container" <div class="row item entity-container"
:class="{blink: justUpdated, 'with-children': hasChildren, collapsed: isCollapsed}"> :class="{blink: justUpdated, 'with-children': hasChildren, collapsed: isCollapsed}">
<div class="adjuster" :class="{'col-12': !hasChildren, 'col-11': hasChildren}"> <div class="adjuster" :class="{'col-12': !hasChildren, 'col-11': hasChildren}">

View File

@ -246,7 +246,7 @@ class ZigbeeMqttBackend(MqttBackend):
self.bus.post(ZigbeeMqttDeviceConnectedEvent(device=name, **event_args)) self.bus.post(ZigbeeMqttDeviceConnectedEvent(device=name, **event_args))
exposes = (device.get('definition', {}) or {}).get('exposes', []) exposes = (device.get('definition', {}) or {}).get('exposes', [])
payload = self._plugin.build_device_get_request(exposes) payload = self._plugin._build_device_get_request(exposes)
if payload: if payload:
client.publish( client.publish(
self.base_topic + '/' + name + '/get', self.base_topic + '/' + name + '/get',

View File

@ -27,7 +27,6 @@ from platypush.entities.sensors import (
) )
from platypush.entities.switches import Switch, EnumSwitch from platypush.entities.switches import Switch, EnumSwitch
from platypush.entities.temperature import TemperatureSensor from platypush.entities.temperature import TemperatureSensor
from platypush.message import Mapping
from platypush.message.response import Response from platypush.message.response import Response
from platypush.plugins.mqtt import MqttPlugin, action from platypush.plugins.mqtt import MqttPlugin, action
@ -175,9 +174,9 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
self.base_topic = base_topic self.base_topic = base_topic
self.timeout = timeout self.timeout = timeout
self._info = { self._info = {
'devices': {},
'groups': {},
'devices_by_addr': {}, 'devices_by_addr': {},
'devices_by_name': {},
'groups': {},
} }
@staticmethod @staticmethod
@ -350,8 +349,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
) )
# Cache the new results # Cache the new results
self._info['devices'] = { self._info['devices_by_name'] = {
device.get('friendly_name', device['ieee_address']): device self._preferred_name(device): device
for device in info.get('devices', []) for device in info.get('devices', [])
} }
@ -751,7 +750,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
) )
@staticmethod @staticmethod
def build_device_get_request(values: List[Dict[str, Any]]) -> dict: def _build_device_get_request(values: List[Dict[str, Any]]) -> dict:
def extract_value(value: dict, root: dict, depth: int = 0): def extract_value(value: dict, root: dict, depth: int = 0):
for feature in value.get('features', []): for feature in value.get('features', []):
new_root = root new_root = root
@ -779,11 +778,37 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
return ret return ret
def _get_device_info(self, device: str) -> Mapping: def _get_device_info(self, device: str, **kwargs) -> dict:
return self._info['devices'].get( device_info = self._info['devices_by_name'].get(
device, self._info['devices_by_addr'].get(device, {}) # First: check by friendly name
device,
# Second: check by address
self._info['devices_by_addr'].get(device, {}),
) )
if not device_info:
# Third: try and get the device from upstream
network_info = self._get_network_info(**kwargs)
next(
iter(
d
for d in network_info.get('devices', [])
if self._device_name_matches(device, d)
),
{},
)
return device_info
@staticmethod
def _preferred_name(device: dict) -> str:
return device.get('friendly_name') or device.get('ieee_address') or ''
@classmethod
def _device_name_matches(cls, name: str, device: dict) -> bool:
name = str(cls._ieee_address(name))
return name == device.get('friendly_name') or name == device.get('ieee_address')
@action @action
def device_get( def device_get(
self, device: str, property: Optional[str] = None, **kwargs self, device: str, property: Optional[str] = None, **kwargs
@ -800,9 +825,9 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
:return: Key->value map of the device properties. :return: Key->value map of the device properties.
""" """
kwargs = self._mqtt_args(**kwargs) kwargs = self._mqtt_args(**kwargs)
device_info = self._get_device_info(device) device_info = self._get_device_info(device, **kwargs)
if device_info: assert device_info, f'No such device: {device}'
device = device_info.get('friendly_name') or self._ieee_address(device_info) # type: ignore device = self._preferred_name(device_info)
if property: if property:
properties = self.publish( properties = self.publish(
@ -815,40 +840,24 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
assert property in properties, f'No such property: {property}' assert property in properties, f'No such property: {property}'
return {property: properties[property]} return {property: properties[property]}
if device not in self._info.get('devices', {}): exposes = (device_info.get('definition', {}) or {}).get('exposes', [])
# Refresh devices info
self._get_network_info(**kwargs)
dev = self._info.get('devices', {}).get(
device, self._info.get('devices_by_addr', {}).get(device)
)
assert dev, f'No such device: {device}'
exposes = (
self._info.get('devices', {}).get(device, {}).get('definition', {}) or {}
).get('exposes', [])
if not exposes: if not exposes:
return {} return {}
device_state = self.publish( # If the device has no queriable properties, don't specify a reply
# topic to listen on
req = self._build_device_get_request(exposes)
reply_topic = self._topic(device)
if not req:
reply_topic = None
return self.publish(
topic=self._topic(device) + '/get', topic=self._topic(device) + '/get',
reply_topic=self._topic(device), reply_topic=reply_topic,
msg=self.build_device_get_request(exposes), msg=req,
**kwargs, **kwargs,
).output # type: ignore[reportGeneralTypeIssues] ).output # type: ignore[reportGeneralTypeIssues]
if device_info:
self.publish_entities( # type: ignore
[
{
**device_info,
'state': device_state,
}
]
)
return device_state
@action @action
def devices_get( def devices_get(
self, devices: Optional[List[str]] = None, **kwargs self, devices: Optional[List[str]] = None, **kwargs
@ -879,8 +888,9 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
if not devices: if not devices:
devices = list( devices = list(
{ {
device['friendly_name'] or device['ieee_address'] self._preferred_name(device)
for device in self.devices(**kwargs).output # type: ignore[reportGeneralTypeIssues] for device in self.devices(**kwargs).output # type: ignore[reportGeneralTypeIssues]
if self._preferred_name(device)
} }
) )
@ -942,40 +952,27 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
(default: query the default configured device). (default: query the default configured device).
""" """
msg = (values or {}).copy() msg = (values or {}).copy()
reply_topic = self._topic(device) reply_topic = None
device_info = self._get_device_info(device, **kwargs)
assert device_info, f'No such device: {device}'
device = self._preferred_name(device_info)
if property: if property:
dev_def = ( # Check if we're trying to set an option
self._info.get('devices_by_addr', {}).get(device, {}).get('definition') stored_option = self._get_options(device_info).get(property)
or {} if stored_option:
) return self.device_set_option(device, property, value, **kwargs)
stored_property = next( # Check if it's a property
iter( reply_topic = self._topic(device)
exposed stored_property = self._get_properties(device_info).get(property)
for exposed in dev_def.get('exposes', {}) assert stored_property, f'No such property: {property}'
if exposed.get('property') == property
),
None,
)
if stored_property: # Set the new value on the message
msg[property] = value msg[property] = value
else:
stored_property = next(
iter(
option
for option in dev_def.get('options', {})
if option.get('property') == property
),
None,
)
if stored_property: # Don't wait for an update from a value that is not readable
return self.device_set_option(device, property, value, **kwargs) if self._is_write_only(stored_property):
if stored_property and self._is_write_only(stored_property):
# Don't wait for an update from a value that is not readable
reply_topic = None reply_topic = None
properties = self.publish( properties = self.publish(
@ -986,7 +983,9 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
).output # type: ignore[reportGeneralTypeIssues] ).output # type: ignore[reportGeneralTypeIssues]
if property and reply_topic: if property and reply_topic:
assert property in properties, 'No such property: ' + property assert (
property in properties
), f'Could not retrieve the new state for {property}'
return {property: properties[property]} return {property: properties[property]}
return properties return properties
@ -1448,10 +1447,9 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
""" """
Turn on/set to true a switch, a binary property or an option. Turn on/set to true a switch, a binary property or an option.
""" """
switch = self._get_switch(device) device, prop_info = self._get_switch_info(device)
address, prop = self._ieee_address(str(switch.id), with_property=True)
self.device_set( self.device_set(
address, prop, switch.data.get('value_on', 'ON') device, prop_info['property'], prop_info.get('value_on', 'ON')
).output # type: ignore[reportGeneralTypeIssues] ).output # type: ignore[reportGeneralTypeIssues]
@action @action
@ -1459,10 +1457,9 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
""" """
Turn off/set to false a switch, a binary property or an option. Turn off/set to false a switch, a binary property or an option.
""" """
switch = self._get_switch(device) device, prop_info = self._get_switch_info(device)
address, prop = self._ieee_address(str(switch.id), with_property=True)
self.device_set( self.device_set(
address, prop, switch.data.get('value_on', 'OFF') device, prop_info['property'], prop_info.get('value_off', 'OFF')
).output # type: ignore[reportGeneralTypeIssues] ).output # type: ignore[reportGeneralTypeIssues]
@action @action
@ -1470,40 +1467,36 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
""" """
Toggles the state of a switch, a binary property or an option. Toggles the state of a switch, a binary property or an option.
""" """
switch = self._get_switch(device) device, prop_info = self._get_switch_info(device)
address, prop = self._ieee_address(str(switch.id), with_property=True) prop = prop_info['property']
device_state = self.device_get(device).output # type: ignore
self.device_set( self.device_set(
address, device,
prop, prop,
switch.data.get( prop_info.get(
'value_toggle', 'value_toggle',
'OFF' if switch.state == switch.data.get('value_on', 'ON') else 'ON', 'OFF'
if device_state.get(prop) == prop_info.get('value_on', 'ON')
else 'ON',
), ),
).output # type: ignore[reportGeneralTypeIssues] ).output # type: ignore[reportGeneralTypeIssues]
def _get_switch(self, name: str) -> Switch: def _get_switch_info(self, name: str) -> Tuple[str, dict]:
address, prop = self._ieee_address(name, with_property=True) name, prop = self._ieee_address(name, with_property=True)
all_switches = self._get_all_switches() if not prop or prop == 'light':
entity_id = f'{address}:state' prop = 'state'
if prop:
entity_id = f'{address}:{prop}'
switch = all_switches.get(entity_id) device_info = self._get_device_info(name)
assert switch, f'No such entity ID: {entity_id}' assert device_info, f'No such device: {name}'
return switch name = self._preferred_name(device_info)
def _get_all_switches(self) -> Dict[str, Switch]: property = self._get_properties(device_info).get(prop)
devices = self.devices().output # type: ignore[reportGeneralTypeIssues] option = self._get_options(device_info).get(prop)
all_switches = {} if option:
return name, option
for device in devices: assert property, f'No such property on device {name}: {prop}'
exposed = self._get_properties(device) return name, property
options = self._get_options(device)
switches = self._get_switches(device, exposed, options)
for switch in switches:
all_switches[switch.id] = switch
return all_switches
@staticmethod @staticmethod
def _is_read_only(feature: dict) -> bool: def _is_read_only(feature: dict) -> bool:
@ -1821,14 +1814,10 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
Set the state for one or more Zigbee lights. Set the state for one or more Zigbee lights.
""" """
lights = [lights] if isinstance(lights, str) else lights lights = [lights] if isinstance(lights, str) else lights
lights = [self._ieee_address(t) for t in lights] devices = [self._get_device_info(light) for light in lights]
devices = [
dev
for dev in self._get_network_info().get('devices', [])
if self._ieee_address(dev) in lights or dev.get('friendly_name') in lights
]
for dev in devices: for i, dev in enumerate(devices):
assert dev, f'No such device: {lights[i]}'
light_meta = self._get_light_meta(dev) light_meta = self._get_light_meta(dev)
assert light_meta, f'{dev["name"]} is not a light' assert light_meta, f'{dev["name"]} is not a light'
data = {} data = {}
@ -1869,9 +1858,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
else: else:
data[attr] = value data[attr] = value
self.device_set( self.device_set(self._preferred_name(dev), values=data)
dev.get('friendly_name', dev.get('ieee_address')), values=data
)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et: