forked from platypush/platypush
More refactors and fixes for zigbee.mqtt
This commit is contained in:
parent
38438230d7
commit
22a566a88b
3 changed files with 100 additions and 113 deletions
|
@ -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}">
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue