2021-02-06 02:19:15 +01:00
|
|
|
import json
|
2022-11-02 22:49:19 +01:00
|
|
|
import re
|
2020-01-22 18:34:28 +01:00
|
|
|
import threading
|
|
|
|
|
2021-02-19 02:54:12 +01:00
|
|
|
from queue import Queue
|
2023-01-09 01:02:49 +01:00
|
|
|
from typing import Optional, List, Any, Dict, Type, Union, Tuple
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-01-09 01:02:49 +01:00
|
|
|
from platypush.entities import Entity, manages
|
2022-10-29 14:09:44 +02:00
|
|
|
from platypush.entities.batteries import Battery
|
2022-11-30 00:55:04 +01:00
|
|
|
from platypush.entities.devices import Device
|
2022-11-14 21:30:43 +01:00
|
|
|
from platypush.entities.dimmers import Dimmer
|
2022-11-02 16:38:17 +01:00
|
|
|
from platypush.entities.electricity import (
|
|
|
|
CurrentSensor,
|
|
|
|
EnergySensor,
|
|
|
|
PowerSensor,
|
|
|
|
VoltageSensor,
|
|
|
|
)
|
|
|
|
from platypush.entities.humidity import HumiditySensor
|
2022-11-30 02:16:56 +01:00
|
|
|
from platypush.entities.illuminance import IlluminanceSensor
|
2022-05-01 21:10:54 +02:00
|
|
|
from platypush.entities.lights import Light
|
2022-10-30 11:03:22 +01:00
|
|
|
from platypush.entities.linkquality import LinkQuality
|
2022-11-21 00:04:07 +01:00
|
|
|
from platypush.entities.sensors import (
|
|
|
|
BinarySensor,
|
|
|
|
EnumSensor,
|
|
|
|
NumericSensor,
|
|
|
|
Sensor,
|
|
|
|
)
|
2022-11-11 01:46:38 +01:00
|
|
|
from platypush.entities.switches import Switch, EnumSwitch
|
2022-11-02 16:38:17 +01:00
|
|
|
from platypush.entities.temperature import TemperatureSensor
|
2022-05-01 21:10:54 +02:00
|
|
|
from platypush.message import Mapping
|
2021-02-09 02:33:43 +01:00
|
|
|
from platypush.message.response import Response
|
2020-01-22 18:34:28 +01:00
|
|
|
from platypush.plugins.mqtt import MqttPlugin, action
|
|
|
|
|
|
|
|
|
2022-11-30 00:55:04 +01:00
|
|
|
@manages(Battery, Device, Dimmer, Light, LinkQuality, Sensor, Switch)
|
2022-05-01 21:10:54 +02:00
|
|
|
class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
|
|
|
This plugin allows you to interact with Zigbee devices over MQTT through any Zigbee sniffer and
|
|
|
|
`zigbee2mqtt <https://www.zigbee2mqtt.io/>`_.
|
|
|
|
|
|
|
|
In order to get started you'll need:
|
|
|
|
|
2021-02-06 02:19:15 +01:00
|
|
|
- A Zigbee USB adapter/sniffer (in this example I'll use the
|
|
|
|
`CC2531 <https://hackaday.io/project/163487-zigbee-cc2531-smart-home-usb-adapter>`_.
|
2020-01-22 18:34:28 +01:00
|
|
|
- A Zigbee debugger/emulator + downloader cable (only to flash the firmware).
|
|
|
|
|
|
|
|
Instructions:
|
|
|
|
|
|
|
|
- Install `cc-tool <https://github.com/dashesy/cc-tool>`_ either from sources or from a package manager.
|
2021-03-14 01:09:01 +01:00
|
|
|
- Connect the Zigbee to your PC/RaspberryPi in this way: ::
|
|
|
|
|
2021-02-06 02:19:15 +01:00
|
|
|
USB -> CC debugger -> downloader cable -> CC2531 -> USB
|
2021-03-14 01:09:01 +01:00
|
|
|
|
|
|
|
- The debugger and the adapter should be connected *at the same time*. If the later ``cc-tool`` command throws
|
2021-02-06 02:19:15 +01:00
|
|
|
up an error, put the device in sync while connected by pressing the _Reset_ button on the debugger.
|
2020-01-22 18:34:28 +01:00
|
|
|
- Check where the device is mapped. On Linux it will usually be ``/dev/ttyACM0``.
|
2021-02-06 02:19:15 +01:00
|
|
|
- Download the latest `Z-Stack firmware <https://github.com/Koenkk/Z-Stack-firmware/tree/master/coordinator>`_
|
|
|
|
to your device. Instructions for a CC2531 device:
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
.. code-block:: shell
|
|
|
|
|
2022-04-05 00:07:55 +02:00
|
|
|
wget https://github.com/Koenkk/Z-Stack-firmware/raw/master\
|
|
|
|
/coordinator/Z-Stack_Home_1.2/bin/default/CC2531_DEFAULT_20201127.zip
|
2021-02-22 01:20:01 +01:00
|
|
|
unzip CC2531_DEFAULT_20201127.zip
|
2020-01-22 18:34:28 +01:00
|
|
|
[sudo] cc-tool -e -w CC2531ZNP-Prod.hex
|
|
|
|
|
|
|
|
- You can disconnect your debugger and downloader cable once the firmware is flashed.
|
|
|
|
|
2021-02-06 02:19:15 +01:00
|
|
|
- Install ``zigbee2mqtt``. First install a node/npm environment, then either install ``zigbee2mqtt`` manually or
|
2021-02-11 23:50:28 +01:00
|
|
|
through your package manager. **NOTE**: many API breaking changes have occurred on Zigbee2MQTT 1.17.0,
|
|
|
|
therefore this integration will only be compatible with the version 1.17.0 of the service or higher versions.
|
|
|
|
Manual instructions:
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
.. code-block:: shell
|
|
|
|
|
|
|
|
# Clone zigbee2mqtt repository
|
|
|
|
[sudo] git clone https://github.com/Koenkk/zigbee2mqtt.git /opt/zigbee2mqtt
|
|
|
|
[sudo] chown -R pi:pi /opt/zigbee2mqtt # Or whichever is your user
|
|
|
|
|
|
|
|
# Install dependencies (as user "pi")
|
|
|
|
cd /opt/zigbee2mqtt
|
|
|
|
npm install
|
|
|
|
|
|
|
|
- You need to have an MQTT broker running somewhere. If not, you can install
|
|
|
|
`Mosquitto <https://mosquitto.org/>`_ through your package manager on any device in your network.
|
|
|
|
|
|
|
|
- Edit the ``/opt/zigbee2mqtt/data/configuration.yaml`` file to match the configuration of your MQTT broker:
|
|
|
|
|
|
|
|
.. code-block:: yaml
|
|
|
|
|
|
|
|
# MQTT settings
|
|
|
|
mqtt:
|
|
|
|
# MQTT base topic for zigbee2mqtt MQTT messages
|
|
|
|
base_topic: zigbee2mqtt
|
|
|
|
# MQTT server URL
|
|
|
|
server: 'mqtt://localhost'
|
|
|
|
# MQTT server authentication, uncomment if required:
|
|
|
|
# user: my_user
|
|
|
|
# password: my_password
|
|
|
|
|
|
|
|
- Also make sure that ``permit_join`` is set to ``True``, in order to allow Zigbee devices to join the network
|
|
|
|
while you're configuring it. It's equally important to set ``permit_join`` to ``False`` once you have
|
|
|
|
configured your network, to prevent accidental/malignant joins from outer Zigbee devices.
|
|
|
|
|
|
|
|
- Start the ``zigbee2mqtt`` daemon on your device (the
|
2022-04-05 00:07:55 +02:00
|
|
|
`official documentation <https://www.zigbee2mqtt.io/getting_started
|
|
|
|
/running_zigbee2mqtt.html#5-optional-running-as-a-daemon-with-systemctl>`_
|
2020-01-22 18:34:28 +01:00
|
|
|
also contains instructions on how to configure it as a ``systemd`` service:
|
|
|
|
|
|
|
|
.. code-block:: shell
|
|
|
|
|
|
|
|
cd /opt/zigbee2mqtt
|
|
|
|
npm start
|
|
|
|
|
|
|
|
- If you have Zigbee devices that are paired to other bridges, unlink them or do a factory reset to pair them
|
|
|
|
to your new bridge.
|
|
|
|
|
|
|
|
- If it all goes fine, once the daemon is running and a new device is found you should see traces like this in
|
|
|
|
the output of ``zigbee2mqtt``::
|
|
|
|
|
2021-02-06 02:19:15 +01:00
|
|
|
zigbee2mqtt:info 2019-11-09T12:19:56: Successfully interviewed '0x00158d0001dc126a', device has
|
|
|
|
successfully been paired
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
- You are now ready to use this integration.
|
|
|
|
|
|
|
|
Requires:
|
|
|
|
|
|
|
|
* **paho-mqtt** (``pip install paho-mqtt``)
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2022-04-05 00:07:55 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
host: str = 'localhost',
|
|
|
|
port: int = 1883,
|
|
|
|
base_topic: str = 'zigbee2mqtt',
|
|
|
|
timeout: int = 10,
|
|
|
|
tls_certfile: Optional[str] = None,
|
|
|
|
tls_keyfile: Optional[str] = None,
|
|
|
|
tls_version: Optional[str] = None,
|
|
|
|
tls_ciphers: Optional[str] = None,
|
|
|
|
username: Optional[str] = None,
|
|
|
|
password: Optional[str] = None,
|
|
|
|
**kwargs,
|
|
|
|
):
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2020-02-23 22:54:50 +01:00
|
|
|
:param host: Default MQTT broker where ``zigbee2mqtt`` publishes its messages (default: ``localhost``).
|
2020-01-22 18:34:28 +01:00
|
|
|
:param port: Broker listen port (default: 1883).
|
|
|
|
:param base_topic: Topic prefix, as specified in ``/opt/zigbee2mqtt/data/configuration.yaml``
|
|
|
|
(default: '``base_topic``').
|
|
|
|
:param timeout: If the command expects from a response, then this timeout value will be used
|
|
|
|
(default: 60 seconds).
|
|
|
|
:param tls_cafile: If the connection requires TLS/SSL, specify the certificate authority file
|
|
|
|
(default: None)
|
|
|
|
:param tls_certfile: If the connection requires TLS/SSL, specify the certificate file (default: None)
|
|
|
|
:param tls_keyfile: If the connection requires TLS/SSL, specify the key file (default: None)
|
|
|
|
:param tls_version: If the connection requires TLS/SSL, specify the minimum TLS supported version
|
|
|
|
(default: None)
|
|
|
|
:param tls_ciphers: If the connection requires TLS/SSL, specify the supported ciphers (default: None)
|
|
|
|
:param username: If the connection requires user authentication, specify the username (default: None)
|
|
|
|
:param password: If the connection requires user authentication, specify the password (default: None)
|
|
|
|
"""
|
2022-04-05 00:07:55 +02:00
|
|
|
super().__init__(
|
|
|
|
host=host,
|
|
|
|
port=port,
|
|
|
|
tls_certfile=tls_certfile,
|
|
|
|
tls_keyfile=tls_keyfile,
|
|
|
|
tls_version=tls_version,
|
|
|
|
tls_ciphers=tls_ciphers,
|
|
|
|
username=username,
|
|
|
|
password=password,
|
|
|
|
**kwargs,
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
self.base_topic = base_topic
|
|
|
|
self.timeout = timeout
|
2021-02-08 01:45:21 +01:00
|
|
|
self._info = {
|
|
|
|
'devices': {},
|
|
|
|
'groups': {},
|
2022-04-11 21:16:45 +02:00
|
|
|
'devices_by_addr': {},
|
2021-02-08 01:45:21 +01:00
|
|
|
}
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-01-09 01:02:49 +01:00
|
|
|
@staticmethod
|
|
|
|
def _get_properties(device: dict) -> dict:
|
|
|
|
exposes = (device.get('definition') or {}).get('exposes', []).copy()
|
|
|
|
properties = {}
|
|
|
|
|
|
|
|
while exposes:
|
|
|
|
exposed = exposes.pop(0)
|
|
|
|
exposes += exposed.get('features', [])
|
|
|
|
if exposed.get('property'):
|
|
|
|
properties[exposed['property']] = exposed
|
|
|
|
|
|
|
|
return properties
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _get_options(device: dict) -> dict:
|
|
|
|
return {
|
|
|
|
option['property']: option
|
|
|
|
for option in (device.get('definition') or {}).get('options', [])
|
|
|
|
if option.get('property')
|
|
|
|
}
|
|
|
|
|
2022-04-05 00:07:55 +02:00
|
|
|
def transform_entities(self, devices):
|
|
|
|
compatible_entities = []
|
|
|
|
for dev in devices:
|
2022-04-05 00:31:04 +02:00
|
|
|
if not dev:
|
|
|
|
continue
|
|
|
|
|
2022-11-30 00:55:04 +01:00
|
|
|
dev_def = dev.get('definition') or {}
|
2022-04-05 00:07:55 +02:00
|
|
|
dev_info = {
|
2022-11-30 00:55:04 +01:00
|
|
|
attr: dev.get(attr)
|
|
|
|
for attr in (
|
|
|
|
'type',
|
|
|
|
'date_code',
|
|
|
|
'ieee_address',
|
|
|
|
'network_address',
|
|
|
|
'power_source',
|
|
|
|
'software_build_id',
|
|
|
|
'model_id',
|
|
|
|
'model',
|
|
|
|
'vendor',
|
|
|
|
'supported',
|
|
|
|
)
|
2022-04-05 00:07:55 +02:00
|
|
|
}
|
|
|
|
|
2023-01-09 01:02:49 +01:00
|
|
|
exposed = self._get_properties(dev)
|
|
|
|
options = self._get_options(dev)
|
2022-11-30 00:55:04 +01:00
|
|
|
reachable = dev.get('supported', False)
|
2023-01-09 01:02:49 +01:00
|
|
|
|
2022-05-01 21:10:54 +02:00
|
|
|
light_info = self._get_light_meta(dev)
|
2022-11-30 00:55:04 +01:00
|
|
|
dev_entities = [
|
2023-01-09 01:02:49 +01:00
|
|
|
*self._get_sensors(dev, exposed, options),
|
|
|
|
*self._get_dimmers(dev, exposed, options),
|
|
|
|
*self._get_switches(dev, exposed, options),
|
|
|
|
*self._get_enum_switches(dev, exposed, options),
|
2022-11-30 00:55:04 +01:00
|
|
|
]
|
2022-05-01 21:10:54 +02:00
|
|
|
|
|
|
|
if light_info:
|
2022-11-30 00:55:04 +01:00
|
|
|
dev_entities.append(
|
2022-10-29 18:15:45 +02:00
|
|
|
Light(
|
2022-11-02 23:07:12 +01:00
|
|
|
id=f'{dev["ieee_address"]}:light',
|
2022-11-30 01:02:25 +01:00
|
|
|
name='Light',
|
2022-10-29 18:15:45 +02:00
|
|
|
on=dev.get('state', {}).get('state')
|
2023-01-09 01:02:49 +01:00
|
|
|
== light_info.get('value_on'),
|
2022-10-29 18:15:45 +02:00
|
|
|
brightness_min=light_info.get('brightness_min'),
|
|
|
|
brightness_max=light_info.get('brightness_max'),
|
|
|
|
temperature_min=light_info.get('temperature_min'),
|
|
|
|
temperature_max=light_info.get('temperature_max'),
|
|
|
|
hue_min=light_info.get('hue_min'),
|
|
|
|
hue_max=light_info.get('hue_max'),
|
|
|
|
saturation_min=light_info.get('saturation_min'),
|
|
|
|
saturation_max=light_info.get('saturation_max'),
|
|
|
|
brightness=(
|
2022-11-02 23:07:12 +01:00
|
|
|
dev.get('state', {}).get(
|
|
|
|
light_info.get('brightness_name', 'brightness')
|
|
|
|
)
|
2022-10-29 18:15:45 +02:00
|
|
|
),
|
|
|
|
temperature=(
|
2022-11-02 23:07:12 +01:00
|
|
|
dev.get('state', {}).get(
|
|
|
|
light_info.get('temperature_name', 'temperature')
|
|
|
|
)
|
2022-10-29 18:15:45 +02:00
|
|
|
),
|
|
|
|
hue=(
|
|
|
|
dev.get('state', {})
|
|
|
|
.get('color', {})
|
|
|
|
.get(light_info.get('hue_name', 'hue'))
|
|
|
|
),
|
|
|
|
saturation=(
|
|
|
|
dev.get('state', {})
|
|
|
|
.get('color', {})
|
|
|
|
.get(light_info.get('saturation_name', 'saturation'))
|
|
|
|
),
|
|
|
|
x=(
|
|
|
|
dev.get('state', {})
|
|
|
|
.get('color', {})
|
|
|
|
.get(light_info.get('x_name', 'x'))
|
|
|
|
),
|
|
|
|
y=(
|
|
|
|
dev.get('state', {})
|
|
|
|
.get('color', {})
|
|
|
|
.get(light_info.get('y_name', 'y'))
|
|
|
|
),
|
|
|
|
description=dev_def.get('description'),
|
|
|
|
data=dev_info,
|
|
|
|
)
|
2022-05-01 21:10:54 +02:00
|
|
|
)
|
2022-10-29 18:15:45 +02:00
|
|
|
|
2022-11-30 00:55:04 +01:00
|
|
|
if dev_entities:
|
|
|
|
parent = Device(
|
|
|
|
id=dev['ieee_address'],
|
|
|
|
name=dev.get('friendly_name'),
|
|
|
|
description=dev_def.get('description'),
|
|
|
|
reachable=reachable,
|
|
|
|
)
|
|
|
|
|
|
|
|
for entity in dev_entities:
|
|
|
|
entity.parent = parent
|
|
|
|
dev_entities.append(parent)
|
|
|
|
|
|
|
|
compatible_entities += dev_entities
|
|
|
|
|
2022-04-05 00:31:04 +02:00
|
|
|
return super().transform_entities(compatible_entities) # type: ignore
|
2022-04-05 00:07:55 +02:00
|
|
|
|
2022-10-29 14:09:44 +02:00
|
|
|
def _get_network_info(self, **kwargs) -> dict:
|
2021-02-06 02:19:15 +01:00
|
|
|
self.logger.info('Fetching Zigbee network information')
|
|
|
|
client = None
|
|
|
|
mqtt_args = self._mqtt_args(**kwargs)
|
|
|
|
timeout = 30
|
|
|
|
if 'timeout' in mqtt_args:
|
|
|
|
timeout = mqtt_args.pop('timeout')
|
|
|
|
|
|
|
|
info = {
|
|
|
|
'state': None,
|
|
|
|
'info': {},
|
|
|
|
'config': {},
|
|
|
|
'devices': [],
|
|
|
|
'groups': [],
|
|
|
|
}
|
|
|
|
|
|
|
|
info_ready_events = {topic: threading.Event() for topic in info.keys()}
|
|
|
|
|
|
|
|
def _on_message():
|
|
|
|
def callback(_, __, msg):
|
|
|
|
topic = msg.topic.split('/')[-1]
|
|
|
|
if topic in info:
|
2022-04-05 00:07:55 +02:00
|
|
|
info[topic] = (
|
|
|
|
msg.payload.decode()
|
|
|
|
if topic == 'state'
|
|
|
|
else json.loads(msg.payload.decode())
|
|
|
|
)
|
2021-02-06 02:19:15 +01:00
|
|
|
info_ready_events[topic].set()
|
|
|
|
|
|
|
|
return callback
|
|
|
|
|
|
|
|
try:
|
|
|
|
host = mqtt_args.pop('host')
|
|
|
|
port = mqtt_args.pop('port')
|
|
|
|
client = self._get_client(**mqtt_args)
|
|
|
|
client.on_message = _on_message()
|
|
|
|
client.connect(host, port, keepalive=timeout)
|
|
|
|
client.subscribe(self.base_topic + '/bridge/#')
|
|
|
|
client.loop_start()
|
|
|
|
|
|
|
|
for event in info_ready_events.values():
|
|
|
|
info_ready = event.wait(timeout=timeout)
|
|
|
|
if not info_ready:
|
2022-04-05 00:07:55 +02:00
|
|
|
raise TimeoutError(
|
|
|
|
'A timeout occurred while fetching the Zigbee network information'
|
|
|
|
)
|
2021-02-06 02:19:15 +01:00
|
|
|
|
2021-02-08 01:45:21 +01:00
|
|
|
# Cache the new results
|
|
|
|
self._info['devices'] = {
|
|
|
|
device.get('friendly_name', device['ieee_address']): device
|
|
|
|
for device in info.get('devices', [])
|
|
|
|
}
|
|
|
|
|
2022-04-11 21:16:45 +02:00
|
|
|
self._info['devices_by_addr'] = {
|
|
|
|
device['ieee_address']: device for device in info.get('devices', [])
|
|
|
|
}
|
|
|
|
|
2021-02-08 01:45:21 +01:00
|
|
|
self._info['groups'] = {
|
2022-04-05 00:07:55 +02:00
|
|
|
group.get('name'): group for group in info.get('groups', [])
|
2021-02-08 01:45:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
self.logger.info('Zigbee network configuration updated')
|
2021-02-06 02:19:15 +01:00
|
|
|
finally:
|
|
|
|
try:
|
2022-10-29 14:09:44 +02:00
|
|
|
if client:
|
|
|
|
client.loop_stop()
|
|
|
|
client.disconnect()
|
2021-02-06 02:19:15 +01:00
|
|
|
except Exception as e:
|
2022-04-05 00:07:55 +02:00
|
|
|
self.logger.warning(
|
|
|
|
'Error on MQTT client disconnection: {}'.format(str(e))
|
|
|
|
)
|
2021-02-06 02:19:15 +01:00
|
|
|
|
2022-10-29 14:09:44 +02:00
|
|
|
return info
|
|
|
|
|
2020-01-22 18:34:28 +01:00
|
|
|
def _topic(self, topic):
|
|
|
|
return self.base_topic + '/' + topic
|
|
|
|
|
2021-02-09 02:33:43 +01:00
|
|
|
@staticmethod
|
|
|
|
def _parse_response(response: Union[dict, Response]) -> dict:
|
|
|
|
if isinstance(response, Response):
|
2022-10-29 14:09:44 +02:00
|
|
|
response = response.output # type: ignore[reportGeneralTypeIssues]
|
2021-02-09 02:33:43 +01:00
|
|
|
|
2022-10-29 14:09:44 +02:00
|
|
|
assert response.get('status') != 'error', response.get( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
'error', 'zigbee2mqtt error'
|
|
|
|
)
|
2022-10-29 14:09:44 +02:00
|
|
|
return response # type: ignore[reportGeneralTypeIssues]
|
2021-02-09 02:33:43 +01:00
|
|
|
|
2020-01-22 18:34:28 +01:00
|
|
|
@action
|
|
|
|
def devices(self, **kwargs) -> List[Dict[str, Any]]:
|
|
|
|
"""
|
|
|
|
Get the list of devices registered to the service.
|
|
|
|
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
|
|
|
|
:return: List of paired devices. Example output:
|
|
|
|
|
|
|
|
.. code-block:: json
|
|
|
|
|
|
|
|
[
|
|
|
|
{
|
2021-02-06 02:19:15 +01:00
|
|
|
"date_code": "20190608",
|
2020-01-22 18:34:28 +01:00
|
|
|
"friendly_name": "Coordinator",
|
2021-02-06 02:19:15 +01:00
|
|
|
"ieee_address": "0x00123456789abcde",
|
|
|
|
"network_address": 0,
|
|
|
|
"supported": false,
|
|
|
|
"type": "Coordinator",
|
|
|
|
"interviewing": false,
|
|
|
|
"interviewing_completed": true,
|
|
|
|
"definition": null,
|
|
|
|
"endpoints": {
|
|
|
|
"13": {
|
|
|
|
"bindings": [],
|
|
|
|
"clusters": {
|
|
|
|
"input": ["genOta"],
|
|
|
|
"output": []
|
|
|
|
},
|
|
|
|
"output": []
|
|
|
|
}
|
|
|
|
}
|
2020-01-22 18:34:28 +01:00
|
|
|
},
|
2021-02-06 02:19:15 +01:00
|
|
|
|
2020-01-22 18:34:28 +01:00
|
|
|
{
|
2021-02-06 02:19:15 +01:00
|
|
|
"date_code": "20180906",
|
2020-01-22 18:34:28 +01:00
|
|
|
"friendly_name": "My Lightbulb",
|
2021-02-06 02:19:15 +01:00
|
|
|
"ieee_address": "0x00123456789abcdf",
|
|
|
|
"network_address": 52715,
|
|
|
|
"power_source": "Mains (single phase)",
|
|
|
|
"software_build_id": "5.127.1.26581",
|
|
|
|
"model_id": "LCT001",
|
|
|
|
"supported": true,
|
|
|
|
"interviewing": false,
|
|
|
|
"interviewing_completed": true,
|
|
|
|
"type": "Router",
|
|
|
|
"definition": {
|
|
|
|
"description": "Hue white and color ambiance E26/E27/E14",
|
|
|
|
"model": "9290012573A",
|
|
|
|
"vendor": "Philips",
|
|
|
|
"exposes": [
|
|
|
|
{
|
|
|
|
"features": [
|
|
|
|
{
|
|
|
|
"access": 7,
|
|
|
|
"description": "On/off state of this light",
|
|
|
|
"name": "state",
|
|
|
|
"property": "state",
|
|
|
|
"type": "binary",
|
|
|
|
"value_off": "OFF",
|
|
|
|
"value_on": "ON",
|
|
|
|
"value_toggle": "TOGGLE"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"access": 7,
|
|
|
|
"description": "Brightness of this light",
|
|
|
|
"name": "brightness",
|
|
|
|
"property": "brightness",
|
|
|
|
"type": "numeric",
|
|
|
|
"value_max": 254,
|
|
|
|
"value_min": 0
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"access": 7,
|
|
|
|
"description": "Color temperature of this light",
|
|
|
|
"name": "color_temp",
|
|
|
|
"property": "color_temp",
|
|
|
|
"type": "numeric",
|
|
|
|
"unit": "mired",
|
|
|
|
"value_max": 500,
|
|
|
|
"value_min": 150
|
|
|
|
},
|
|
|
|
{
|
2022-04-05 00:07:55 +02:00
|
|
|
"description": "Color of this light in the XY space",
|
2021-02-06 02:19:15 +01:00
|
|
|
"features": [
|
|
|
|
{
|
|
|
|
"access": 7,
|
|
|
|
"name": "x",
|
|
|
|
"property": "x",
|
|
|
|
"type": "numeric"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"access": 7,
|
|
|
|
"name": "y",
|
|
|
|
"property": "y",
|
|
|
|
"type": "numeric"
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"name": "color_xy",
|
|
|
|
"property": "color",
|
|
|
|
"type": "composite"
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"type": "light"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"access": 2,
|
2022-04-05 00:07:55 +02:00
|
|
|
"description": "Triggers an effect on the light",
|
2021-02-06 02:19:15 +01:00
|
|
|
"name": "effect",
|
|
|
|
"property": "effect",
|
|
|
|
"type": "enum",
|
|
|
|
"values": [
|
|
|
|
"blink",
|
|
|
|
"breathe",
|
|
|
|
"okay",
|
|
|
|
"channel_change",
|
|
|
|
"finish_effect",
|
|
|
|
"stop_effect"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"access": 1,
|
|
|
|
"description": "Link quality (signal strength)",
|
|
|
|
"name": "linkquality",
|
|
|
|
"property": "linkquality",
|
|
|
|
"type": "numeric",
|
|
|
|
"unit": "lqi",
|
|
|
|
"value_max": 255,
|
|
|
|
"value_min": 0
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
|
|
|
|
"endpoints": {
|
|
|
|
"11": {
|
|
|
|
"bindings": [],
|
|
|
|
"clusters": {
|
|
|
|
"input": [
|
|
|
|
"genBasic",
|
|
|
|
"genIdentify",
|
|
|
|
"genGroups",
|
|
|
|
"genScenes",
|
|
|
|
"genOnOff",
|
|
|
|
"genLevelCtrl",
|
|
|
|
"touchlink",
|
|
|
|
"lightingColorCtrl",
|
|
|
|
"manuSpecificUbisysDimmerSetup"
|
|
|
|
],
|
|
|
|
"output": [
|
|
|
|
"genOta"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"configured_reportings": []
|
|
|
|
},
|
|
|
|
"242": {
|
|
|
|
"bindings": [],
|
|
|
|
"clusters": {
|
|
|
|
"input": [
|
|
|
|
"greenPower"
|
|
|
|
],
|
|
|
|
"output": [
|
|
|
|
"greenPower"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"configured_reportings": []
|
|
|
|
}
|
|
|
|
}
|
2020-01-22 18:34:28 +01:00
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
"""
|
2022-10-29 14:09:44 +02:00
|
|
|
return self._get_network_info(**kwargs).get('devices', {})
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
2022-04-05 00:07:55 +02:00
|
|
|
def permit_join(
|
|
|
|
self, permit: bool = True, timeout: Optional[float] = None, **kwargs
|
|
|
|
):
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
|
|
|
Enable/disable devices from joining the network. This is not persistent (will not be saved to
|
|
|
|
``configuration.yaml``).
|
|
|
|
|
|
|
|
:param permit: Set to True to allow joins, False otherwise.
|
|
|
|
:param timeout: Allow/disallow joins only for this amount of time.
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
|
|
|
if timeout:
|
2021-02-09 02:33:43 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/permit_join'),
|
|
|
|
msg={'value': permit, 'time': timeout},
|
|
|
|
reply_topic=self._topic('bridge/response/permit_join'),
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
2022-10-29 14:09:44 +02:00
|
|
|
or {}
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
return self.publish(
|
|
|
|
topic=self._topic('bridge/request/permit_join'),
|
|
|
|
msg={'value': permit},
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def factory_reset(self, **kwargs):
|
|
|
|
"""
|
2021-02-10 02:00:52 +01:00
|
|
|
Perform a factory reset of a device connected to the network, following the procedure required by the particular
|
|
|
|
device (for instance, Hue bulbs require the Zigbee adapter to be close to the device while a button on the back
|
|
|
|
of the bulb is pressed).
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2022-04-05 00:07:55 +02:00
|
|
|
self.publish(
|
|
|
|
topic=self._topic('bridge/request/touchlink/factory_reset'),
|
|
|
|
msg='',
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def log_level(self, level: str, **kwargs):
|
|
|
|
"""
|
|
|
|
Change the log level at runtime. This change will not be persistent.
|
|
|
|
|
|
|
|
:param level: Possible values: 'debug', 'info', 'warn', 'error'.
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2021-02-09 02:33:43 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/config/log_level'),
|
|
|
|
msg={'value': level},
|
|
|
|
reply_topic=self._topic('bridge/response/config/log_level'),
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def device_set_option(self, device: str, option: str, value: Any, **kwargs):
|
|
|
|
"""
|
|
|
|
Change the options of a device. Options can only be changed, not added or deleted.
|
|
|
|
|
2022-11-02 16:38:17 +01:00
|
|
|
:param device: Display name or IEEE address of the device.
|
2020-01-22 18:34:28 +01:00
|
|
|
:param option: Option name.
|
|
|
|
:param value: New value.
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2021-02-09 02:33:43 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/device/options'),
|
|
|
|
reply_topic=self._topic('bridge/response/device/options'),
|
|
|
|
msg={
|
|
|
|
'id': device,
|
|
|
|
'options': {
|
|
|
|
option: value,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def device_remove(self, device: str, force: bool = False, **kwargs):
|
|
|
|
"""
|
|
|
|
Remove a device from the network.
|
|
|
|
|
|
|
|
:param device: Display name of the device.
|
|
|
|
:param force: Force the remove also if the removal wasn't acknowledged by the device. Note: a forced remove
|
|
|
|
only removes the entry from the internal database, but the device is likely to connect again when
|
|
|
|
restarted unless it's factory reset (default: False).
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2021-02-09 02:33:43 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/device/remove'),
|
|
|
|
msg={'id': device, 'force': force},
|
|
|
|
reply_topic=self._topic('bridge/response/device/remove'),
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def device_ban(self, device: str, **kwargs):
|
|
|
|
"""
|
|
|
|
Ban a device from the network.
|
|
|
|
|
|
|
|
:param device: Display name of the device.
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2021-02-09 02:33:43 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/device/ban'),
|
|
|
|
reply_topic=self._topic('bridge/response/device/ban'),
|
|
|
|
msg={'id': device},
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def device_whitelist(self, device: str, **kwargs):
|
|
|
|
"""
|
|
|
|
Whitelist a device on the network. Note: once at least a device is whitelisted, all the other non-whitelisted
|
|
|
|
devices will be removed from the network.
|
|
|
|
|
|
|
|
:param device: Display name of the device.
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2021-02-09 02:33:43 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/device/whitelist'),
|
|
|
|
reply_topic=self._topic('bridge/response/device/whitelist'),
|
|
|
|
msg={'id': device},
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def device_rename(self, name: str, device: Optional[str] = None, **kwargs):
|
|
|
|
"""
|
|
|
|
Rename a device on the network.
|
|
|
|
|
|
|
|
:param name: New name.
|
|
|
|
:param device: Current name of the device to rename. If no name is specified then the rename will
|
|
|
|
affect the last device that joined the network.
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2020-02-23 22:54:50 +01:00
|
|
|
if name == device:
|
|
|
|
self.logger.info('Old and new name are the same: nothing to do')
|
|
|
|
return
|
|
|
|
|
2022-10-29 14:09:44 +02:00
|
|
|
devices = self.devices().output # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
assert not [
|
|
|
|
dev for dev in devices if dev.get('friendly_name') == name
|
|
|
|
], 'A device named {} already exists on the network'.format(name)
|
2020-02-23 22:54:50 +01:00
|
|
|
|
2021-02-08 01:45:21 +01:00
|
|
|
if device:
|
|
|
|
req = {
|
|
|
|
'from': device,
|
|
|
|
'to': name,
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
req = {
|
|
|
|
'last': True,
|
|
|
|
'to': name,
|
|
|
|
}
|
|
|
|
|
2021-02-09 02:33:43 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/device/rename'),
|
|
|
|
msg=req,
|
|
|
|
reply_topic=self._topic('bridge/response/device/rename'),
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2021-02-08 01:45:21 +01:00
|
|
|
|
|
|
|
@staticmethod
|
2021-02-11 23:50:28 +01:00
|
|
|
def build_device_get_request(values: List[Dict[str, Any]]) -> dict:
|
2022-11-04 22:51:40 +01:00
|
|
|
def extract_value(value: dict, root: dict, depth: int = 0):
|
2022-11-02 21:54:47 +01:00
|
|
|
for feature in value.get('features', []):
|
2022-11-04 22:51:40 +01:00
|
|
|
new_root = root
|
|
|
|
if depth > 0:
|
|
|
|
new_root = root[value['property']] = root.get(value['property'], {})
|
|
|
|
|
|
|
|
extract_value(feature, new_root, depth=depth + 1)
|
2022-11-02 21:54:47 +01:00
|
|
|
|
2022-10-31 00:51:26 +01:00
|
|
|
if not value.get('access', 1) & 0x4:
|
|
|
|
# Property not readable/query-able
|
2021-02-08 01:45:21 +01:00
|
|
|
return
|
|
|
|
|
|
|
|
if 'features' not in value:
|
|
|
|
if 'property' in value:
|
2021-02-11 23:50:28 +01:00
|
|
|
root[value['property']] = 0 if value['type'] == 'numeric' else ''
|
2021-02-08 01:45:21 +01:00
|
|
|
return
|
|
|
|
|
|
|
|
if 'property' in value:
|
|
|
|
root[value['property']] = root.get(value['property'], {})
|
|
|
|
root = root[value['property']]
|
|
|
|
|
|
|
|
ret = {}
|
|
|
|
for value in values:
|
|
|
|
extract_value(value, root=ret)
|
|
|
|
|
|
|
|
return ret
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2022-04-11 21:16:45 +02:00
|
|
|
def _get_device_info(self, device: str) -> Mapping:
|
|
|
|
return self._info['devices'].get(
|
|
|
|
device, self._info['devices_by_addr'].get(device, {})
|
|
|
|
)
|
|
|
|
|
2020-01-22 18:34:28 +01:00
|
|
|
@action
|
2022-04-05 00:07:55 +02:00
|
|
|
def device_get(
|
|
|
|
self, device: str, property: Optional[str] = None, **kwargs
|
|
|
|
) -> Dict[str, Any]:
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
|
|
|
Get the properties of a device. The returned keys vary depending on the device. For example, a light bulb
|
|
|
|
may have the "``state``" and "``brightness``" properties, while an environment sensor may have the
|
|
|
|
"``temperature``" and "``humidity``" properties, and so on.
|
|
|
|
|
|
|
|
:param device: Display name of the device.
|
|
|
|
:param property: Name of the property that should be retrieved (default: all).
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
:return: Key->value map of the device properties.
|
|
|
|
"""
|
2021-02-08 01:45:21 +01:00
|
|
|
kwargs = self._mqtt_args(**kwargs)
|
2022-04-11 21:16:45 +02:00
|
|
|
device_info = self._get_device_info(device)
|
|
|
|
if device_info:
|
2023-01-09 01:02:49 +01:00
|
|
|
device = device_info.get('friendly_name') or self._ieee_address(device_info) # type: ignore
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
if property:
|
2022-04-05 00:07:55 +02:00
|
|
|
properties = self.publish(
|
|
|
|
topic=self._topic(device) + f'/get/{property}',
|
|
|
|
reply_topic=self._topic(device),
|
|
|
|
msg={property: ''},
|
|
|
|
**kwargs,
|
2022-10-29 14:09:44 +02:00
|
|
|
).output # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
|
|
|
|
assert property in properties, f'No such property: {property}'
|
2020-01-22 18:34:28 +01:00
|
|
|
return {property: properties[property]}
|
|
|
|
|
2021-02-08 01:45:21 +01:00
|
|
|
if device not in self._info.get('devices', {}):
|
|
|
|
# Refresh devices info
|
|
|
|
self._get_network_info(**kwargs)
|
|
|
|
|
2022-05-01 21:10:54 +02:00
|
|
|
dev = self._info.get('devices', {}).get(
|
|
|
|
device, self._info.get('devices_by_addr', {}).get(device)
|
|
|
|
)
|
|
|
|
|
|
|
|
assert dev, f'No such device: {device}'
|
2022-04-05 00:07:55 +02:00
|
|
|
exposes = (
|
|
|
|
self._info.get('devices', {}).get(device, {}).get('definition', {}) or {}
|
|
|
|
).get('exposes', [])
|
2021-02-08 01:45:21 +01:00
|
|
|
if not exposes:
|
|
|
|
return {}
|
|
|
|
|
2022-04-11 21:16:45 +02:00
|
|
|
device_state = self.publish(
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic(device) + '/get',
|
|
|
|
reply_topic=self._topic(device),
|
|
|
|
msg=self.build_device_get_request(exposes),
|
|
|
|
**kwargs,
|
2022-10-29 14:09:44 +02:00
|
|
|
).output # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
|
2022-04-11 21:16:45 +02:00
|
|
|
if device_info:
|
2022-04-12 14:41:21 +02:00
|
|
|
self.publish_entities( # type: ignore
|
2022-04-11 21:16:45 +02:00
|
|
|
[
|
2022-04-12 14:41:21 +02:00
|
|
|
{
|
2022-04-11 21:16:45 +02:00
|
|
|
**device_info,
|
|
|
|
'state': device_state,
|
|
|
|
}
|
|
|
|
]
|
|
|
|
)
|
2022-04-05 00:07:55 +02:00
|
|
|
|
2022-04-11 21:16:45 +02:00
|
|
|
return device_state
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2021-02-19 02:54:12 +01:00
|
|
|
@action
|
2022-04-05 00:07:55 +02:00
|
|
|
def devices_get(
|
|
|
|
self, devices: Optional[List[str]] = None, **kwargs
|
|
|
|
) -> Dict[str, dict]:
|
2021-02-19 02:54:12 +01:00
|
|
|
"""
|
2021-03-05 02:23:28 +01:00
|
|
|
Get the properties of the devices connected to the network.
|
2021-02-19 02:54:12 +01:00
|
|
|
|
|
|
|
:param devices: If set, then only the status of these devices (by friendly name) will be retrieved (default:
|
|
|
|
retrieve all).
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
:return: Key->value map of the device properties:
|
|
|
|
|
|
|
|
.. code-block:: json
|
|
|
|
|
|
|
|
{
|
|
|
|
"Bulb": {
|
|
|
|
"state": "ON",
|
|
|
|
"brightness": 254
|
|
|
|
},
|
|
|
|
"Sensor": {
|
|
|
|
"temperature": 22.5
|
|
|
|
}
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
kwargs = self._mqtt_args(**kwargs)
|
|
|
|
|
|
|
|
if not devices:
|
2022-10-29 14:09:44 +02:00
|
|
|
devices = list(
|
|
|
|
{
|
|
|
|
device['friendly_name'] or device['ieee_address']
|
|
|
|
for device in self.devices(**kwargs).output # type: ignore[reportGeneralTypeIssues]
|
|
|
|
}
|
|
|
|
)
|
2021-02-19 02:54:12 +01:00
|
|
|
|
|
|
|
def worker(device: str, q: Queue):
|
2022-10-29 14:09:44 +02:00
|
|
|
q.put(self.device_get(device, **kwargs).output) # type: ignore[reportGeneralTypeIssues]
|
2021-02-19 02:54:12 +01:00
|
|
|
|
|
|
|
queues = {}
|
|
|
|
workers = {}
|
|
|
|
response = {}
|
|
|
|
|
|
|
|
for device in devices:
|
|
|
|
queues[device] = Queue()
|
2022-04-05 00:07:55 +02:00
|
|
|
workers[device] = threading.Thread(
|
|
|
|
target=worker, args=(device, queues[device])
|
|
|
|
)
|
2021-02-19 02:54:12 +01:00
|
|
|
workers[device].start()
|
|
|
|
|
|
|
|
for device in devices:
|
|
|
|
try:
|
|
|
|
response[device] = queues[device].get(timeout=kwargs.get('timeout'))
|
|
|
|
workers[device].join(timeout=kwargs.get('timeout'))
|
|
|
|
except Exception as e:
|
2022-04-05 00:07:55 +02:00
|
|
|
self.logger.warning(
|
2023-01-11 01:22:56 +01:00
|
|
|
'An error occurred while getting the status of the device {}: {}'.format(
|
2022-04-05 00:07:55 +02:00
|
|
|
device, str(e)
|
|
|
|
)
|
|
|
|
)
|
2021-02-19 02:54:12 +01:00
|
|
|
|
|
|
|
return response
|
|
|
|
|
2021-03-05 02:23:28 +01:00
|
|
|
@action
|
|
|
|
def status(self, device: Optional[str] = None, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Get the status of a device (by friendly name) or of all the connected devices (it wraps :meth:`.devices_get`).
|
|
|
|
|
|
|
|
:param device: Device friendly name (default: get all devices).
|
|
|
|
"""
|
2022-04-05 00:07:55 +02:00
|
|
|
return self.devices_get([device] if device else None, *args, **kwargs)
|
2021-03-05 02:23:28 +01:00
|
|
|
|
2020-01-22 18:34:28 +01:00
|
|
|
@action
|
2022-10-12 03:00:42 +02:00
|
|
|
def device_set(
|
|
|
|
self,
|
|
|
|
device: str,
|
|
|
|
property: Optional[str] = None,
|
|
|
|
value: Optional[Any] = None,
|
|
|
|
values: Optional[Dict[str, Any]] = None,
|
|
|
|
**kwargs,
|
|
|
|
):
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
|
|
|
Set a properties on a device. The compatible properties vary depending on the device. For example, a light bulb
|
|
|
|
may have the "``state``" and "``brightness``" properties, while an environment sensor may have the
|
|
|
|
"``temperature``" and "``humidity``" properties, and so on.
|
|
|
|
|
|
|
|
:param device: Display name of the device.
|
|
|
|
:param property: Name of the property that should be set.
|
|
|
|
:param value: New value of the property.
|
2022-10-12 03:00:42 +02:00
|
|
|
:param values: If you want to set multiple values, then pass this mapping instead of ``property``+``value``.
|
2020-01-22 18:34:28 +01:00
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2022-10-12 03:00:42 +02:00
|
|
|
msg = (values or {}).copy()
|
2022-11-11 01:46:38 +01:00
|
|
|
reply_topic = self._topic(device)
|
|
|
|
|
2022-10-12 03:00:42 +02:00
|
|
|
if property:
|
2023-01-09 01:02:49 +01:00
|
|
|
dev_def = (
|
|
|
|
self._info.get('devices_by_addr', {}).get(device, {}).get('definition')
|
|
|
|
or {}
|
|
|
|
)
|
|
|
|
|
2022-11-11 01:46:38 +01:00
|
|
|
stored_property = next(
|
|
|
|
iter(
|
|
|
|
exposed
|
2023-01-09 01:02:49 +01:00
|
|
|
for exposed in dev_def.get('exposes', {})
|
2022-11-11 01:46:38 +01:00
|
|
|
if exposed.get('property') == property
|
|
|
|
),
|
|
|
|
None,
|
|
|
|
)
|
|
|
|
|
2023-01-09 01:02:49 +01:00
|
|
|
if stored_property:
|
|
|
|
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:
|
|
|
|
return self.device_set_option(device, property, value, **kwargs)
|
|
|
|
|
2022-11-11 01:46:38 +01:00
|
|
|
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
|
2022-10-12 03:00:42 +02:00
|
|
|
|
2022-04-05 00:07:55 +02:00
|
|
|
properties = self.publish(
|
|
|
|
topic=self._topic(device + '/set'),
|
2022-11-11 01:46:38 +01:00
|
|
|
reply_topic=reply_topic,
|
2022-10-12 03:00:42 +02:00
|
|
|
msg=msg,
|
2022-04-05 00:07:55 +02:00
|
|
|
**self._mqtt_args(**kwargs),
|
2022-10-29 14:09:44 +02:00
|
|
|
).output # type: ignore[reportGeneralTypeIssues]
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2022-11-11 01:46:38 +01:00
|
|
|
if property and reply_topic:
|
2020-01-22 18:34:28 +01:00
|
|
|
assert property in properties, 'No such property: ' + property
|
|
|
|
return {property: properties[property]}
|
|
|
|
|
|
|
|
return properties
|
|
|
|
|
2022-11-11 01:46:38 +01:00
|
|
|
@action
|
|
|
|
def set_value(
|
|
|
|
self, device: str, property: Optional[str] = None, data=None, **kwargs
|
|
|
|
):
|
|
|
|
"""
|
|
|
|
Entity-compatible way of setting a value on a node.
|
|
|
|
|
|
|
|
:param device: Device friendly name, IEEE address or internal entity ID
|
|
|
|
in ``<address>:<property>`` format.
|
|
|
|
:param property: Name of the property to set. If not specified here, it
|
|
|
|
should be specified on ``device`` in ``<address>:<property>``
|
|
|
|
format.
|
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
|
|
|
"""
|
|
|
|
dev, prop = self._ieee_address(device, with_property=True)
|
|
|
|
if not property:
|
|
|
|
property = prop
|
|
|
|
|
|
|
|
self.device_set(dev, property, data, **kwargs)
|
|
|
|
|
2020-01-22 18:34:28 +01:00
|
|
|
@action
|
2021-02-10 02:00:52 +01:00
|
|
|
def device_check_ota_updates(self, device: str, **kwargs) -> dict:
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2021-02-10 02:00:52 +01:00
|
|
|
Check if the specified device has any OTA updates available to install.
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2021-02-10 02:00:52 +01:00
|
|
|
:param device: Address or friendly name of the device.
|
2020-01-22 18:34:28 +01:00
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
2021-02-10 02:00:52 +01:00
|
|
|
|
|
|
|
:return:
|
|
|
|
|
|
|
|
.. code-block:: json
|
|
|
|
|
|
|
|
{
|
|
|
|
"id": "<device ID>",
|
|
|
|
"update_available": true,
|
|
|
|
"status": "ok"
|
|
|
|
}
|
|
|
|
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2021-02-10 02:00:52 +01:00
|
|
|
ret = self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/device/ota_update/check'),
|
|
|
|
reply_topic=self._topic('bridge/response/device/ota_update/check'),
|
|
|
|
msg={'id': device},
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2021-02-10 02:00:52 +01:00
|
|
|
return {
|
|
|
|
'status': ret['status'],
|
|
|
|
'id': ret.get('data', {}).get('id'),
|
|
|
|
'update_available': ret.get('data', {}).get('update_available', False),
|
|
|
|
}
|
|
|
|
|
|
|
|
@action
|
|
|
|
def device_install_ota_updates(self, device: str, **kwargs):
|
|
|
|
"""
|
|
|
|
Install OTA updates for a device if available.
|
|
|
|
|
|
|
|
:param device: Address or friendly name of the device.
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/device/ota_update/update'),
|
|
|
|
reply_topic=self._topic('bridge/response/device/ota_update/update'),
|
|
|
|
msg={'id': device},
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2020-02-23 22:54:50 +01:00
|
|
|
@action
|
2021-02-06 02:19:15 +01:00
|
|
|
def groups(self, **kwargs) -> List[dict]:
|
2020-02-23 22:54:50 +01:00
|
|
|
"""
|
|
|
|
Get the groups registered on the device.
|
|
|
|
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2022-10-29 14:09:44 +02:00
|
|
|
return self._get_network_info(**kwargs).get('groups', [])
|
2020-02-23 22:54:50 +01:00
|
|
|
|
2021-02-06 02:19:15 +01:00
|
|
|
@action
|
|
|
|
def info(self, **kwargs) -> dict:
|
|
|
|
"""
|
|
|
|
Get the information, configuration and state of the network.
|
2020-02-23 22:54:50 +01:00
|
|
|
|
2021-02-06 02:19:15 +01:00
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
|
|
|
|
:return: Example:
|
|
|
|
|
|
|
|
.. code-block:: json
|
|
|
|
|
|
|
|
{
|
|
|
|
"state": "online",
|
|
|
|
"commit": "07cdc9d",
|
|
|
|
"config": {
|
|
|
|
"advanced": {
|
|
|
|
"adapter_concurrent": null,
|
|
|
|
"adapter_delay": null,
|
|
|
|
"availability_blacklist": [],
|
|
|
|
"availability_blocklist": [],
|
|
|
|
"availability_passlist": [],
|
|
|
|
"availability_timeout": 0,
|
|
|
|
"availability_whitelist": [],
|
|
|
|
"cache_state": true,
|
|
|
|
"cache_state_persistent": true,
|
|
|
|
"cache_state_send_on_startup": true,
|
|
|
|
"channel": 11,
|
|
|
|
"elapsed": false,
|
|
|
|
"ext_pan_id": [
|
|
|
|
221,
|
|
|
|
221,
|
|
|
|
221,
|
|
|
|
221,
|
|
|
|
221,
|
|
|
|
221,
|
|
|
|
221,
|
|
|
|
221
|
|
|
|
],
|
|
|
|
"homeassistant_discovery_topic": "homeassistant",
|
|
|
|
"homeassistant_legacy_triggers": true,
|
|
|
|
"homeassistant_status_topic": "hass/status",
|
|
|
|
"last_seen": "disable",
|
|
|
|
"legacy_api": true,
|
|
|
|
"log_directory": "/opt/zigbee2mqtt/data/log/%TIMESTAMP%",
|
|
|
|
"log_file": "log.txt",
|
|
|
|
"log_level": "debug",
|
|
|
|
"log_output": [
|
|
|
|
"console",
|
|
|
|
"file"
|
|
|
|
],
|
|
|
|
"log_rotation": true,
|
|
|
|
"log_syslog": {},
|
|
|
|
"pan_id": 6754,
|
|
|
|
"report": false,
|
|
|
|
"soft_reset_timeout": 0,
|
|
|
|
"timestamp_format": "YYYY-MM-DD HH:mm:ss"
|
|
|
|
},
|
|
|
|
"ban": [],
|
|
|
|
"blocklist": [],
|
|
|
|
"device_options": {},
|
|
|
|
"devices": {
|
|
|
|
"0x00123456789abcdf": {
|
|
|
|
"friendly_name": "My Lightbulb"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"experimental": {
|
|
|
|
"output": "json"
|
|
|
|
},
|
|
|
|
"external_converters": [],
|
|
|
|
"groups": {},
|
|
|
|
"homeassistant": false,
|
|
|
|
"map_options": {
|
|
|
|
"graphviz": {
|
|
|
|
"colors": {
|
|
|
|
"fill": {
|
|
|
|
"coordinator": "#e04e5d",
|
|
|
|
"enddevice": "#fff8ce",
|
|
|
|
"router": "#4ea3e0"
|
|
|
|
},
|
|
|
|
"font": {
|
|
|
|
"coordinator": "#ffffff",
|
|
|
|
"enddevice": "#000000",
|
|
|
|
"router": "#ffffff"
|
|
|
|
},
|
|
|
|
"line": {
|
|
|
|
"active": "#009900",
|
|
|
|
"inactive": "#994444"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"mqtt": {
|
|
|
|
"base_topic": "zigbee2mqtt",
|
|
|
|
"force_disable_retain": false,
|
|
|
|
"include_device_information": false,
|
|
|
|
"server": "mqtt://localhost"
|
|
|
|
},
|
|
|
|
"passlist": [],
|
|
|
|
"permit_join": true,
|
|
|
|
"serial": {
|
|
|
|
"disable_led": false,
|
|
|
|
"port": "/dev/ttyUSB0"
|
|
|
|
},
|
|
|
|
"whitelist": []
|
|
|
|
},
|
|
|
|
"coordinator": {
|
|
|
|
"meta": {
|
|
|
|
"maintrel": 3,
|
|
|
|
"majorrel": 2,
|
|
|
|
"minorrel": 6,
|
|
|
|
"product": 0,
|
|
|
|
"revision": 20190608,
|
|
|
|
"transportrev": 2
|
|
|
|
},
|
|
|
|
"type": "zStack12"
|
|
|
|
},
|
|
|
|
"log_level": "debug",
|
|
|
|
"network": {
|
|
|
|
"channel": 11,
|
|
|
|
"extended_pan_id": "0xdddddddddddddddd",
|
|
|
|
"pan_id": 6754
|
|
|
|
},
|
|
|
|
"permit_join": true,
|
|
|
|
"version": "1.17.0"
|
|
|
|
}
|
|
|
|
|
|
|
|
"""
|
|
|
|
info = self._get_network_info(**kwargs)
|
|
|
|
return {
|
|
|
|
'state': info.get('state'),
|
|
|
|
'info': info.get('info'),
|
|
|
|
}
|
2020-02-23 22:54:50 +01:00
|
|
|
|
2020-01-22 18:34:28 +01:00
|
|
|
@action
|
|
|
|
def group_add(self, name: str, id: Optional[int] = None, **kwargs):
|
|
|
|
"""
|
|
|
|
Add a new group.
|
|
|
|
|
|
|
|
:param name: Display name of the group.
|
|
|
|
:param id: Optional numeric ID (default: auto-generated).
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2022-04-05 00:07:55 +02:00
|
|
|
payload = (
|
|
|
|
name
|
|
|
|
if id is None
|
|
|
|
else {
|
|
|
|
'id': id,
|
|
|
|
'friendly_name': name,
|
|
|
|
}
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2021-02-09 02:33:43 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/group/add'),
|
|
|
|
reply_topic=self._topic('bridge/response/group/add'),
|
|
|
|
msg=payload,
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
2021-02-09 02:33:43 +01:00
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2021-02-10 02:00:52 +01:00
|
|
|
@action
|
|
|
|
def group_get(self, group: str, property: Optional[str] = None, **kwargs) -> dict:
|
|
|
|
"""
|
|
|
|
Get one or more properties of a group. The compatible properties vary depending on the devices on the group.
|
|
|
|
For example, a light bulb may have the "``state``" (with values ``"ON"`` and ``"OFF"``) and "``brightness``"
|
|
|
|
properties, while an environment sensor may have the "``temperature``" and "``humidity``" properties, and so on.
|
|
|
|
|
|
|
|
:param group: Display name of the group.
|
|
|
|
:param property: Name of the property to retrieve (default: all available properties)
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
|
|
|
msg = {}
|
|
|
|
if property:
|
|
|
|
msg = {property: ''}
|
|
|
|
|
2022-04-05 00:07:55 +02:00
|
|
|
properties = self.publish(
|
|
|
|
topic=self._topic(group + '/get'),
|
|
|
|
reply_topic=self._topic(group),
|
|
|
|
msg=msg,
|
|
|
|
**self._mqtt_args(**kwargs),
|
2022-10-29 14:09:44 +02:00
|
|
|
).output # type: ignore[reportGeneralTypeIssues]
|
2021-02-10 02:00:52 +01:00
|
|
|
|
|
|
|
if property:
|
|
|
|
assert property in properties, 'No such property: ' + property
|
|
|
|
return {property: properties[property]}
|
|
|
|
|
|
|
|
return properties
|
|
|
|
|
2020-02-23 22:54:50 +01:00
|
|
|
# noinspection PyShadowingBuiltins,DuplicatedCode
|
|
|
|
@action
|
|
|
|
def group_set(self, group: str, property: str, value: Any, **kwargs):
|
|
|
|
"""
|
|
|
|
Set a properties on a group. The compatible properties vary depending on the devices on the group.
|
|
|
|
For example, a light bulb may have the "``state``" (with values ``"ON"`` and ``"OFF"``) and "``brightness``"
|
|
|
|
properties, while an environment sensor may have the "``temperature``" and "``humidity``" properties, and so on.
|
|
|
|
|
|
|
|
:param group: Display name of the group.
|
|
|
|
:param property: Name of the property that should be set.
|
|
|
|
:param value: New value of the property.
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2022-04-05 00:07:55 +02:00
|
|
|
properties = self.publish(
|
|
|
|
topic=self._topic(group + '/set'),
|
|
|
|
reply_topic=self._topic(group),
|
|
|
|
msg={property: value},
|
|
|
|
**self._mqtt_args(**kwargs),
|
2022-10-29 14:09:44 +02:00
|
|
|
).output # type: ignore[reportGeneralTypeIssues]
|
2020-02-23 22:54:50 +01:00
|
|
|
|
|
|
|
if property:
|
|
|
|
assert property in properties, 'No such property: ' + property
|
|
|
|
return {property: properties[property]}
|
|
|
|
|
|
|
|
return properties
|
|
|
|
|
|
|
|
@action
|
|
|
|
def group_rename(self, name: str, group: str, **kwargs):
|
|
|
|
"""
|
|
|
|
Rename a group.
|
|
|
|
|
|
|
|
:param name: New name.
|
|
|
|
:param group: Current name of the group to rename.
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
|
|
|
if name == group:
|
|
|
|
self.logger.info('Old and new name are the same: nothing to do')
|
|
|
|
return
|
|
|
|
|
2022-10-29 14:09:44 +02:00
|
|
|
groups = {
|
|
|
|
group.get('friendly_name'): group
|
|
|
|
for group in self.groups().output # type: ignore[reportGeneralTypeIssues]
|
|
|
|
}
|
|
|
|
|
2022-04-05 00:07:55 +02:00
|
|
|
assert (
|
|
|
|
name not in groups
|
|
|
|
), 'A group named {} already exists on the network'.format(name)
|
2020-02-23 22:54:50 +01:00
|
|
|
|
2021-02-09 02:33:43 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/group/rename'),
|
|
|
|
reply_topic=self._topic('bridge/response/group/rename'),
|
|
|
|
msg={'from': group, 'to': name} if group else name,
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2020-02-23 22:54:50 +01:00
|
|
|
|
2020-01-22 18:34:28 +01:00
|
|
|
@action
|
|
|
|
def group_remove(self, name: str, **kwargs):
|
|
|
|
"""
|
|
|
|
Remove a group.
|
|
|
|
|
|
|
|
:param name: Display name of the group.
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2021-02-09 02:33:43 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/group/remove'),
|
|
|
|
reply_topic=self._topic('bridge/response/group/remove'),
|
|
|
|
msg=name,
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def group_add_device(self, group: str, device: str, **kwargs):
|
|
|
|
"""
|
|
|
|
Add a device to a group.
|
|
|
|
|
|
|
|
:param group: Display name of the group.
|
|
|
|
:param device: Display name of the device to be added.
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2021-02-10 02:00:52 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/group/members/add'),
|
|
|
|
reply_topic=self._topic('bridge/response/group/members/add'),
|
|
|
|
msg={
|
|
|
|
'group': group,
|
|
|
|
'device': device,
|
|
|
|
},
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def group_remove_device(self, group: str, device: Optional[str] = None, **kwargs):
|
|
|
|
"""
|
|
|
|
Remove a device from a group.
|
|
|
|
|
|
|
|
:param group: Display name of the group.
|
|
|
|
:param device: Display name of the device to be removed. If none is specified then all the devices registered
|
|
|
|
to the specified group will be removed.
|
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2021-02-10 02:00:52 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic(
|
|
|
|
'bridge/request/group/members/remove{}'.format(
|
|
|
|
'_all' if device is None else ''
|
|
|
|
)
|
|
|
|
),
|
2021-02-19 02:54:12 +01:00
|
|
|
reply_topic=self._topic(
|
2022-04-05 00:07:55 +02:00
|
|
|
'bridge/response/group/members/remove{}'.format(
|
|
|
|
'_all' if device is None else ''
|
|
|
|
)
|
|
|
|
),
|
2021-02-19 02:54:12 +01:00
|
|
|
msg={
|
|
|
|
'group': group,
|
|
|
|
'device': device,
|
2022-04-05 00:07:55 +02:00
|
|
|
},
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
2021-02-10 02:00:52 +01:00
|
|
|
def bind_devices(self, source: str, target: str, **kwargs):
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
|
|
|
Bind two devices. Binding makes it possible that devices can directly control each other without the
|
|
|
|
intervention of zigbee2mqtt or any home automation software. You may want to use this feature to bind
|
|
|
|
for example an IKEA/Philips Hue dimmer switch to a light bulb, or a Zigbee remote to a thermostat.
|
|
|
|
Read more on the `zigbee2mqtt binding page <https://www.zigbee2mqtt.io/information/binding.html>`_.
|
|
|
|
|
|
|
|
:param source: Name of the source device. It can also be a group name, although the support is
|
|
|
|
`still experimental <https://www.zigbee2mqtt.io/information/binding.html#binding-a-group>`_.
|
2021-02-10 02:00:52 +01:00
|
|
|
You can also bind a specific device endpoint - for example ``MySensor/temperature``.
|
2020-01-22 18:34:28 +01:00
|
|
|
:param target: Name of the target device.
|
2021-02-10 02:00:52 +01:00
|
|
|
You can also bind a specific device endpoint - for example ``MyLight/state``.
|
2020-01-22 18:34:28 +01:00
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2021-02-10 02:00:52 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/device/bind'),
|
|
|
|
reply_topic=self._topic('bridge/response/device/bind'),
|
|
|
|
msg={'from': source, 'to': target},
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def unbind_devices(self, source: str, target: str, **kwargs):
|
|
|
|
"""
|
|
|
|
Un-bind two devices.
|
|
|
|
|
|
|
|
:param source: Name of the source device.
|
2021-02-10 02:00:52 +01:00
|
|
|
You can also bind a specific device endpoint - for example ``MySensor/temperature``.
|
2020-01-22 18:34:28 +01:00
|
|
|
:param target: Name of the target device.
|
2021-02-10 02:00:52 +01:00
|
|
|
You can also bind a specific device endpoint - for example ``MyLight/state``.
|
2020-01-22 18:34:28 +01:00
|
|
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
|
|
|
(default: query the default configured device).
|
|
|
|
"""
|
2021-02-10 02:00:52 +01:00
|
|
|
return self._parse_response(
|
2022-10-29 14:09:44 +02:00
|
|
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic('bridge/request/device/unbind'),
|
|
|
|
reply_topic=self._topic('bridge/response/device/unbind'),
|
|
|
|
msg={'from': source, 'to': target},
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
)
|
2021-02-19 02:54:12 +01:00
|
|
|
|
|
|
|
@action
|
2023-01-09 01:02:49 +01:00
|
|
|
def on(self, device, *_, **__):
|
2021-02-19 02:54:12 +01:00
|
|
|
"""
|
2023-01-09 01:02:49 +01:00
|
|
|
Turn on/set to true a switch, a binary property or an option.
|
2021-02-19 02:54:12 +01:00
|
|
|
"""
|
2023-01-09 01:02:49 +01:00
|
|
|
switch = self._get_switch(device)
|
|
|
|
address, prop = self._ieee_address(str(switch.id), with_property=True)
|
|
|
|
self.device_set(
|
|
|
|
address, prop, switch.data.get('value_on', 'ON')
|
2022-10-29 14:09:44 +02:00
|
|
|
).output # type: ignore[reportGeneralTypeIssues]
|
2021-02-19 02:54:12 +01:00
|
|
|
|
|
|
|
@action
|
2023-01-09 01:02:49 +01:00
|
|
|
def off(self, device, *_, **__):
|
2021-02-19 02:54:12 +01:00
|
|
|
"""
|
2023-01-09 01:02:49 +01:00
|
|
|
Turn off/set to false a switch, a binary property or an option.
|
2021-02-19 02:54:12 +01:00
|
|
|
"""
|
2023-01-09 01:02:49 +01:00
|
|
|
switch = self._get_switch(device)
|
|
|
|
address, prop = self._ieee_address(str(switch.id), with_property=True)
|
|
|
|
self.device_set(
|
|
|
|
address, prop, switch.data.get('value_on', 'OFF')
|
2022-10-29 14:09:44 +02:00
|
|
|
).output # type: ignore[reportGeneralTypeIssues]
|
2021-02-19 02:54:12 +01:00
|
|
|
|
|
|
|
@action
|
2023-01-09 01:02:49 +01:00
|
|
|
def toggle(self, device, *_, **__):
|
|
|
|
"""
|
|
|
|
Toggles the state of a switch, a binary property or an option.
|
|
|
|
"""
|
|
|
|
switch = self._get_switch(device)
|
|
|
|
address, prop = self._ieee_address(str(switch.id), with_property=True)
|
|
|
|
self.device_set(
|
|
|
|
address,
|
|
|
|
prop,
|
|
|
|
switch.data.get(
|
|
|
|
'value_toggle',
|
|
|
|
'OFF' if switch.state == switch.data.get('value_on', 'ON') else 'ON',
|
|
|
|
),
|
2022-10-29 14:09:44 +02:00
|
|
|
).output # type: ignore[reportGeneralTypeIssues]
|
2021-02-19 02:54:12 +01:00
|
|
|
|
2023-01-09 01:02:49 +01:00
|
|
|
def _get_switch(self, name: str) -> Switch:
|
|
|
|
address, prop = self._ieee_address(name, with_property=True)
|
|
|
|
all_switches = self._get_all_switches()
|
|
|
|
entity_id = f'{address}:state'
|
|
|
|
if prop:
|
|
|
|
entity_id = f'{address}:{prop}'
|
2022-04-11 21:16:45 +02:00
|
|
|
|
2023-01-09 01:02:49 +01:00
|
|
|
switch = all_switches.get(entity_id)
|
|
|
|
assert switch, f'No such entity ID: {entity_id}'
|
|
|
|
return switch
|
2022-04-11 21:16:45 +02:00
|
|
|
|
2023-01-09 01:02:49 +01:00
|
|
|
def _get_all_switches(self) -> Dict[str, Switch]:
|
|
|
|
devices = self.devices().output # type: ignore[reportGeneralTypeIssues]
|
|
|
|
all_switches = {}
|
|
|
|
|
|
|
|
for device in devices:
|
|
|
|
exposed = self._get_properties(device)
|
|
|
|
options = self._get_options(device)
|
|
|
|
switches = self._get_switches(device, exposed, options)
|
|
|
|
for switch in switches:
|
|
|
|
all_switches[switch.id] = switch
|
|
|
|
|
|
|
|
return all_switches
|
2021-02-19 02:54:12 +01:00
|
|
|
|
2022-04-05 00:07:55 +02:00
|
|
|
@staticmethod
|
2022-11-02 16:38:17 +01:00
|
|
|
def _is_read_only(feature: dict) -> bool:
|
|
|
|
return bool(feature.get('access', 0) & 2) == 0 and (
|
|
|
|
bool(feature.get('access', 0) & 1) == 1
|
|
|
|
or bool(feature.get('access', 0) & 4) == 1
|
|
|
|
)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _is_write_only(feature: dict) -> bool:
|
|
|
|
return bool(feature.get('access', 0) & 2) == 1 and (
|
|
|
|
bool(feature.get('access', 0) & 1) == 0
|
|
|
|
or bool(feature.get('access', 0) & 4) == 0
|
|
|
|
)
|
|
|
|
|
2022-11-11 20:40:36 +01:00
|
|
|
@staticmethod
|
|
|
|
def _is_query_disabled(feature: dict) -> bool:
|
|
|
|
return bool(feature.get('access', 0) & 4) == 0
|
|
|
|
|
2022-11-02 16:38:17 +01:00
|
|
|
@staticmethod
|
2022-11-11 01:46:38 +01:00
|
|
|
def _ieee_address(
|
|
|
|
device: Union[dict, str], with_property=False
|
|
|
|
) -> Union[str, Tuple[str, Optional[str]]]:
|
2022-11-02 16:38:17 +01:00
|
|
|
# Entity value IDs are stored in the `<address>:<property>`
|
|
|
|
# format. Therefore, we need to split by `:` if we want to
|
|
|
|
# retrieve the original address.
|
2022-11-02 22:49:19 +01:00
|
|
|
if isinstance(device, dict):
|
|
|
|
dev = device['ieee_address']
|
|
|
|
else:
|
|
|
|
dev = device
|
|
|
|
|
|
|
|
# IEEE address + property format
|
|
|
|
if re.search(r'^0x[0-9a-fA-F]{16}:', dev):
|
2022-11-11 01:46:38 +01:00
|
|
|
parts = dev.split(':')
|
|
|
|
return (
|
|
|
|
(parts[0], parts[1] if len(parts) > 1 else None)
|
|
|
|
if with_property
|
|
|
|
else parts[0]
|
|
|
|
)
|
|
|
|
|
|
|
|
return (dev, None) if with_property else dev
|
2022-11-02 16:38:17 +01:00
|
|
|
|
|
|
|
@classmethod
|
2023-01-09 01:02:49 +01:00
|
|
|
def _get_switches(
|
|
|
|
cls, device_info: dict, props: dict, options: dict
|
|
|
|
) -> List[Switch]:
|
|
|
|
return [
|
|
|
|
cls._to_entity(
|
|
|
|
Switch,
|
|
|
|
device_info,
|
|
|
|
prop,
|
|
|
|
options=options,
|
|
|
|
state=device_info.get('state', {}).get(prop['property'])
|
|
|
|
== prop['value_on'],
|
|
|
|
data={
|
|
|
|
'value_on': prop['value_on'],
|
|
|
|
'value_off': prop['value_off'],
|
|
|
|
'value_toggle': prop.get('value_toggle'),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
for prop in [*props.values(), *options.values()]
|
|
|
|
if (
|
|
|
|
prop.get('type') == 'binary'
|
|
|
|
and 'value_on' in prop
|
|
|
|
and 'value_off' in prop
|
|
|
|
and not cls._is_read_only(prop)
|
|
|
|
)
|
|
|
|
]
|
2021-02-19 02:54:12 +01:00
|
|
|
|
2022-11-02 16:38:17 +01:00
|
|
|
@classmethod
|
2023-01-09 01:02:49 +01:00
|
|
|
def _get_sensors(
|
|
|
|
cls, device_info: dict, props: dict, options: dict
|
|
|
|
) -> List[Sensor]:
|
2022-11-02 16:38:17 +01:00
|
|
|
sensors = []
|
2023-01-09 01:02:49 +01:00
|
|
|
properties = [
|
|
|
|
prop
|
|
|
|
for prop in [*props.values(), *options.values()]
|
|
|
|
if cls._is_read_only(prop)
|
2022-11-02 16:38:17 +01:00
|
|
|
]
|
2022-10-29 18:15:45 +02:00
|
|
|
|
2023-01-09 01:02:49 +01:00
|
|
|
for prop in properties:
|
2022-11-02 16:38:17 +01:00
|
|
|
entity_type = None
|
|
|
|
sensor_args = {
|
2023-01-09 01:02:49 +01:00
|
|
|
'value': device_info.get('state', {}).get(prop['property']),
|
2022-11-02 16:38:17 +01:00
|
|
|
}
|
2022-10-30 11:03:22 +01:00
|
|
|
|
2023-01-09 01:02:49 +01:00
|
|
|
if prop.get('type') == 'numeric':
|
2022-11-05 01:47:50 +01:00
|
|
|
sensor_args.update(
|
|
|
|
{
|
2023-01-09 01:02:49 +01:00
|
|
|
'min': prop.get('value_min'),
|
|
|
|
'max': prop.get('value_max'),
|
|
|
|
'unit': prop.get('unit'),
|
2022-11-05 01:47:50 +01:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2023-01-09 01:02:49 +01:00
|
|
|
if prop.get('property') == 'battery':
|
2022-11-02 16:38:17 +01:00
|
|
|
entity_type = Battery
|
2023-01-09 01:02:49 +01:00
|
|
|
elif prop.get('property') == 'linkquality':
|
2022-11-02 16:38:17 +01:00
|
|
|
entity_type = LinkQuality
|
2023-01-09 01:02:49 +01:00
|
|
|
elif prop.get('property') == 'current':
|
2022-11-02 16:38:17 +01:00
|
|
|
entity_type = CurrentSensor
|
2023-01-09 01:02:49 +01:00
|
|
|
elif prop.get('property') == 'energy':
|
2022-11-02 16:38:17 +01:00
|
|
|
entity_type = EnergySensor
|
2023-01-09 01:02:49 +01:00
|
|
|
elif prop.get('property') == 'power':
|
2022-11-02 16:38:17 +01:00
|
|
|
entity_type = PowerSensor
|
2023-01-09 01:02:49 +01:00
|
|
|
elif prop.get('property') == 'voltage':
|
2022-11-02 16:38:17 +01:00
|
|
|
entity_type = VoltageSensor
|
2023-01-09 01:02:49 +01:00
|
|
|
elif prop.get('property', '').endswith('temperature'):
|
2022-11-02 16:38:17 +01:00
|
|
|
entity_type = TemperatureSensor
|
2023-01-09 01:02:49 +01:00
|
|
|
elif re.search(r'(humidity|moisture)$', prop.get('property' '')):
|
2022-11-02 16:38:17 +01:00
|
|
|
entity_type = HumiditySensor
|
2023-01-09 01:02:49 +01:00
|
|
|
elif re.search(r'(illuminance|luminosity)$', prop.get('property' '')):
|
2022-11-30 02:16:56 +01:00
|
|
|
entity_type = IlluminanceSensor
|
2023-01-09 01:02:49 +01:00
|
|
|
elif prop.get('type') == 'binary':
|
2022-11-05 01:47:50 +01:00
|
|
|
entity_type = BinarySensor
|
2023-01-09 01:02:49 +01:00
|
|
|
sensor_args['value'] = sensor_args['value'] == prop.get(
|
2022-11-05 01:47:50 +01:00
|
|
|
'value_on', True
|
|
|
|
)
|
2023-01-09 01:02:49 +01:00
|
|
|
elif prop.get('type') == 'enum':
|
2022-11-21 00:04:07 +01:00
|
|
|
entity_type = EnumSensor
|
2023-01-09 01:02:49 +01:00
|
|
|
sensor_args['values'] = prop.get('values', [])
|
|
|
|
elif prop.get('type') == 'numeric':
|
2022-11-02 16:38:17 +01:00
|
|
|
entity_type = NumericSensor
|
|
|
|
|
|
|
|
if entity_type:
|
2023-01-09 01:02:49 +01:00
|
|
|
sensors.append(
|
|
|
|
cls._to_entity(
|
|
|
|
entity_type, device_info, prop, options=options, **sensor_args
|
|
|
|
)
|
|
|
|
)
|
2022-11-02 16:38:17 +01:00
|
|
|
|
|
|
|
return sensors
|
|
|
|
|
2022-11-11 01:46:38 +01:00
|
|
|
@classmethod
|
2023-01-09 01:02:49 +01:00
|
|
|
def _get_dimmers(
|
|
|
|
cls, device_info: dict, props: dict, options: dict
|
|
|
|
) -> List[Dimmer]:
|
2022-11-13 18:48:36 +01:00
|
|
|
return [
|
2023-01-09 01:02:49 +01:00
|
|
|
cls._to_entity(
|
|
|
|
Dimmer,
|
|
|
|
device_info,
|
|
|
|
prop,
|
|
|
|
options=options,
|
|
|
|
value=device_info.get('state', {}).get(prop['property']),
|
|
|
|
min=prop.get('value_min'),
|
|
|
|
max=prop.get('value_max'),
|
|
|
|
unit=prop.get('unit'),
|
2022-11-13 18:48:36 +01:00
|
|
|
)
|
2023-01-09 01:02:49 +01:00
|
|
|
for prop in [*props.values(), *options.values()]
|
2022-11-13 18:48:36 +01:00
|
|
|
if (
|
2023-01-09 01:02:49 +01:00
|
|
|
prop.get('property')
|
|
|
|
and prop.get('type') == 'numeric'
|
|
|
|
and not cls._is_read_only(prop)
|
2022-11-13 18:48:36 +01:00
|
|
|
)
|
|
|
|
]
|
|
|
|
|
|
|
|
@classmethod
|
2023-01-09 01:02:49 +01:00
|
|
|
def _get_enum_switches(
|
|
|
|
cls, device_info: dict, props: dict, options: dict
|
|
|
|
) -> List[EnumSwitch]:
|
2022-11-13 18:48:36 +01:00
|
|
|
return [
|
2023-01-09 01:02:49 +01:00
|
|
|
cls._to_entity(
|
|
|
|
EnumSwitch,
|
|
|
|
device_info,
|
|
|
|
prop,
|
|
|
|
options=options,
|
|
|
|
value=device_info.get('state', {}).get(prop['property']),
|
|
|
|
values=prop.get('values', []),
|
2022-11-13 18:48:36 +01:00
|
|
|
)
|
2023-01-09 01:02:49 +01:00
|
|
|
for prop in [*props.values(), *options.values()]
|
2022-11-11 01:46:38 +01:00
|
|
|
if (
|
2023-01-09 01:02:49 +01:00
|
|
|
prop.get('access', 0) & 2
|
|
|
|
and prop.get('type') == 'enum'
|
|
|
|
and prop.get('values')
|
2022-11-11 01:46:38 +01:00
|
|
|
)
|
|
|
|
]
|
|
|
|
|
2023-01-09 01:02:49 +01:00
|
|
|
@classmethod
|
|
|
|
def _to_entity(
|
|
|
|
cls,
|
|
|
|
entity_type: Type[Entity],
|
|
|
|
device_info: dict,
|
|
|
|
property: dict,
|
|
|
|
options: dict,
|
|
|
|
**kwargs,
|
|
|
|
) -> Entity:
|
|
|
|
return entity_type(
|
|
|
|
id=f'{device_info["ieee_address"]}:{property["property"]}',
|
|
|
|
name=property.get('description', ''),
|
|
|
|
is_read_only=cls._is_read_only(property),
|
|
|
|
is_write_only=cls._is_write_only(property),
|
|
|
|
is_query_disabled=cls._is_query_disabled(property),
|
|
|
|
is_configuration=property['property'] in options,
|
|
|
|
**kwargs,
|
|
|
|
)
|
|
|
|
|
2022-11-02 16:38:17 +01:00
|
|
|
@classmethod
|
|
|
|
def _get_light_meta(cls, device_info: dict) -> dict:
|
2022-05-01 21:10:54 +02:00
|
|
|
exposes = (device_info.get('definition', {}) or {}).get('exposes', [])
|
|
|
|
for exposed in exposes:
|
|
|
|
if exposed.get('type') == 'light':
|
|
|
|
features = exposed.get('features', [])
|
|
|
|
switch = {}
|
|
|
|
brightness = {}
|
|
|
|
temperature = {}
|
2022-10-12 03:00:42 +02:00
|
|
|
color = {}
|
2022-05-01 21:10:54 +02:00
|
|
|
|
|
|
|
for feature in features:
|
2022-11-11 20:40:36 +01:00
|
|
|
data = {
|
|
|
|
'is_read_only': cls._is_read_only(feature),
|
|
|
|
'is_write_only': cls._is_write_only(feature),
|
|
|
|
'is_query_disabled': cls._is_query_disabled(feature),
|
|
|
|
}
|
|
|
|
|
2022-05-01 21:10:54 +02:00
|
|
|
if (
|
|
|
|
feature.get('property') == 'state'
|
|
|
|
and feature.get('type') == 'binary'
|
|
|
|
and 'value_on' in feature
|
|
|
|
and 'value_off' in feature
|
|
|
|
):
|
|
|
|
switch = {
|
|
|
|
'value_on': feature['value_on'],
|
|
|
|
'value_off': feature['value_off'],
|
|
|
|
'state_name': feature['name'],
|
|
|
|
'value_toggle': feature.get('value_toggle', None),
|
2022-11-11 20:40:36 +01:00
|
|
|
**data,
|
2022-05-01 21:10:54 +02:00
|
|
|
}
|
|
|
|
elif (
|
|
|
|
feature.get('property') == 'brightness'
|
|
|
|
and feature.get('type') == 'numeric'
|
|
|
|
and 'value_min' in feature
|
|
|
|
and 'value_max' in feature
|
|
|
|
):
|
|
|
|
brightness = {
|
|
|
|
'brightness_name': feature['name'],
|
|
|
|
'brightness_min': feature['value_min'],
|
|
|
|
'brightness_max': feature['value_max'],
|
2022-11-11 20:40:36 +01:00
|
|
|
**data,
|
2022-05-01 21:10:54 +02:00
|
|
|
}
|
|
|
|
elif (
|
|
|
|
feature.get('property') == 'color_temp'
|
|
|
|
and feature.get('type') == 'numeric'
|
|
|
|
and 'value_min' in feature
|
|
|
|
and 'value_max' in feature
|
|
|
|
):
|
|
|
|
temperature = {
|
|
|
|
'temperature_name': feature['name'],
|
|
|
|
'temperature_min': feature['value_min'],
|
|
|
|
'temperature_max': feature['value_max'],
|
2022-11-11 20:40:36 +01:00
|
|
|
**data,
|
2022-05-01 21:10:54 +02:00
|
|
|
}
|
2022-10-12 03:00:42 +02:00
|
|
|
elif (
|
|
|
|
feature.get('property') == 'color'
|
|
|
|
and feature.get('type') == 'composite'
|
|
|
|
):
|
|
|
|
color_features = feature.get('features', [])
|
|
|
|
for color_feature in color_features:
|
|
|
|
if color_feature.get('property') == 'hue':
|
|
|
|
color.update(
|
|
|
|
{
|
|
|
|
'hue_name': color_feature['name'],
|
|
|
|
'hue_min': color_feature.get('value_min', 0),
|
|
|
|
'hue_max': color_feature.get(
|
|
|
|
'value_max', 65535
|
|
|
|
),
|
2022-11-11 20:40:36 +01:00
|
|
|
**data,
|
2022-10-12 03:00:42 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
elif color_feature.get('property') == 'saturation':
|
|
|
|
color.update(
|
|
|
|
{
|
|
|
|
'saturation_name': color_feature['name'],
|
|
|
|
'saturation_min': color_feature.get(
|
|
|
|
'value_min', 0
|
|
|
|
),
|
|
|
|
'saturation_max': color_feature.get(
|
|
|
|
'value_max', 255
|
|
|
|
),
|
2022-11-11 20:40:36 +01:00
|
|
|
**data,
|
2022-10-12 03:00:42 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
elif color_feature.get('property') == 'x':
|
|
|
|
color.update(
|
|
|
|
{
|
|
|
|
'x_name': color_feature['name'],
|
|
|
|
'x_min': color_feature.get('value_min', 0.0),
|
|
|
|
'x_max': color_feature.get('value_max', 1.0),
|
2022-11-11 20:40:36 +01:00
|
|
|
**data,
|
2022-10-12 03:00:42 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
elif color_feature.get('property') == 'y':
|
|
|
|
color.update(
|
|
|
|
{
|
|
|
|
'y_name': color_feature['name'],
|
|
|
|
'y_min': color_feature.get('value_min', 0),
|
|
|
|
'y_max': color_feature.get('value_max', 255),
|
2022-11-11 20:40:36 +01:00
|
|
|
**data,
|
2022-10-12 03:00:42 +02:00
|
|
|
}
|
|
|
|
)
|
2022-05-01 21:10:54 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
'friendly_name': device_info.get('friendly_name'),
|
|
|
|
'ieee_address': device_info.get('friendly_name'),
|
|
|
|
**switch,
|
|
|
|
**brightness,
|
|
|
|
**temperature,
|
2022-10-12 03:00:42 +02:00
|
|
|
**color,
|
2022-05-01 21:10:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
@action
|
|
|
|
def set_lights(self, lights, **kwargs):
|
2022-10-12 03:00:42 +02:00
|
|
|
"""
|
|
|
|
Set the state for one or more Zigbee lights.
|
|
|
|
"""
|
|
|
|
lights = [lights] if isinstance(lights, str) else lights
|
2022-11-02 23:32:21 +01:00
|
|
|
lights = [self._ieee_address(t) for t in lights]
|
2022-05-01 21:10:54 +02:00
|
|
|
devices = [
|
|
|
|
dev
|
|
|
|
for dev in self._get_network_info().get('devices', [])
|
2022-11-02 16:38:17 +01:00
|
|
|
if self._ieee_address(dev) in lights or dev.get('friendly_name') in lights
|
2022-05-01 21:10:54 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
for dev in devices:
|
|
|
|
light_meta = self._get_light_meta(dev)
|
|
|
|
assert light_meta, f'{dev["name"]} is not a light'
|
2022-10-12 03:00:42 +02:00
|
|
|
data = {}
|
2022-05-01 21:10:54 +02:00
|
|
|
|
|
|
|
for attr, value in kwargs.items():
|
|
|
|
if attr == 'on':
|
2022-10-12 03:00:42 +02:00
|
|
|
data[light_meta['state_name']] = value
|
2022-05-01 21:10:54 +02:00
|
|
|
elif attr in {'brightness', 'bri'}:
|
2022-10-12 03:00:42 +02:00
|
|
|
data[light_meta['brightness_name']] = int(value)
|
2022-05-01 21:10:54 +02:00
|
|
|
elif attr in {'temperature', 'ct'}:
|
2022-10-12 03:00:42 +02:00
|
|
|
data[light_meta['temperature_name']] = int(value)
|
|
|
|
elif attr in {'saturation', 'sat'}:
|
|
|
|
data['color'] = {
|
|
|
|
**data.get('color', {}),
|
|
|
|
light_meta['saturation_name']: int(value),
|
|
|
|
}
|
|
|
|
elif attr == 'hue':
|
|
|
|
data['color'] = {
|
|
|
|
**data.get('color', {}),
|
|
|
|
light_meta['hue_name']: int(value),
|
|
|
|
}
|
|
|
|
elif attr == 'xy':
|
|
|
|
data['color'] = {
|
|
|
|
**data.get('color', {}),
|
|
|
|
light_meta['x_name']: float(value[0]),
|
|
|
|
light_meta['y_name']: float(value[1]),
|
|
|
|
}
|
|
|
|
elif attr == 'x':
|
|
|
|
data['color'] = {
|
|
|
|
**data.get('color', {}),
|
|
|
|
light_meta['x_name']: float(value),
|
|
|
|
}
|
|
|
|
elif attr == 'y':
|
|
|
|
data['color'] = {
|
|
|
|
**data.get('color', {}),
|
|
|
|
light_meta['y_name']: float(value),
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
data[attr] = value
|
2022-05-01 21:10:54 +02:00
|
|
|
|
2022-10-12 03:00:42 +02:00
|
|
|
self.device_set(
|
|
|
|
dev.get('friendly_name', dev.get('ieee_address')), values=data
|
|
|
|
)
|
2022-05-01 21:10:54 +02:00
|
|
|
|
2021-02-06 14:45:50 +01:00
|
|
|
|
2021-02-19 02:54:12 +01:00
|
|
|
# vim:sw=4:ts=4:et:
|