2023-09-06 02:44:56 +02:00
|
|
|
import contextlib
|
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
|
|
|
|
|
2023-09-14 23:05:27 +02:00
|
|
|
from queue import Empty, Queue
|
2023-01-29 02:34:48 +01:00
|
|
|
from typing import (
|
|
|
|
Any,
|
2023-02-02 23:21:12 +01:00
|
|
|
Collection,
|
2023-01-29 02:34:48 +01:00
|
|
|
Dict,
|
|
|
|
List,
|
2023-09-06 02:44:56 +02:00
|
|
|
Mapping,
|
2023-01-29 02:34:48 +01:00
|
|
|
Optional,
|
|
|
|
Tuple,
|
|
|
|
Type,
|
|
|
|
Union,
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
import paho.mqtt.client as mqtt
|
|
|
|
|
|
|
|
from platypush.bus import Bus
|
|
|
|
from platypush.context import get_bus
|
2023-02-02 23:21:12 +01:00
|
|
|
from platypush.entities import (
|
|
|
|
DimmerEntityManager,
|
|
|
|
Entity,
|
|
|
|
EnumSwitchEntityManager,
|
|
|
|
LightEntityManager,
|
|
|
|
SensorEntityManager,
|
|
|
|
SwitchEntityManager,
|
|
|
|
)
|
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
|
2023-09-06 02:44:56 +02:00
|
|
|
from platypush.message.event.zigbee.mqtt import (
|
|
|
|
ZigbeeMqttOnlineEvent,
|
|
|
|
ZigbeeMqttOfflineEvent,
|
|
|
|
ZigbeeMqttDevicePairingEvent,
|
|
|
|
ZigbeeMqttDeviceConnectedEvent,
|
|
|
|
ZigbeeMqttDeviceBannedEvent,
|
|
|
|
ZigbeeMqttDeviceRemovedEvent,
|
|
|
|
ZigbeeMqttDeviceRemovedFailedEvent,
|
|
|
|
ZigbeeMqttDeviceWhitelistedEvent,
|
|
|
|
ZigbeeMqttDeviceRenamedEvent,
|
|
|
|
ZigbeeMqttDeviceBindEvent,
|
|
|
|
ZigbeeMqttDevicePropertySetEvent,
|
|
|
|
ZigbeeMqttDeviceUnbindEvent,
|
|
|
|
ZigbeeMqttGroupAddedEvent,
|
|
|
|
ZigbeeMqttGroupAddedFailedEvent,
|
|
|
|
ZigbeeMqttGroupRemovedEvent,
|
|
|
|
ZigbeeMqttGroupRemovedFailedEvent,
|
|
|
|
ZigbeeMqttGroupRemoveAllEvent,
|
|
|
|
ZigbeeMqttGroupRemoveAllFailedEvent,
|
|
|
|
ZigbeeMqttErrorEvent,
|
|
|
|
)
|
2021-02-09 02:33:43 +01:00
|
|
|
from platypush.message.response import Response
|
2023-09-06 02:44:56 +02:00
|
|
|
from platypush.plugins.mqtt import DEFAULT_TIMEOUT, MqttClient, MqttPlugin, action
|
2023-09-14 23:05:27 +02:00
|
|
|
from ._state import BridgeState, ZigbeeState
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
|
2023-02-11 04:04:21 +01:00
|
|
|
# pylint: disable=too-many-ancestors
|
2023-02-02 23:21:12 +01:00
|
|
|
class ZigbeeMqttPlugin(
|
|
|
|
MqttPlugin,
|
|
|
|
DimmerEntityManager,
|
|
|
|
EnumSwitchEntityManager,
|
|
|
|
LightEntityManager,
|
|
|
|
SensorEntityManager,
|
|
|
|
SwitchEntityManager,
|
2023-02-11 04:04:21 +01:00
|
|
|
):
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
Support for Zigbee devices using any Zigbee adapter compatible with
|
2020-01-22 18:34:28 +01:00
|
|
|
`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:
|
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
- 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
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
- The debugger and the adapter should be connected *at the same time*.
|
|
|
|
If the later ``cc-tool`` command throws 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``.
|
2023-09-06 02:44:56 +02:00
|
|
|
- Download the latest `Z-Stack firmware
|
|
|
|
<https://github.com/Koenkk/Z-Stack-firmware/tree/master/coordinator>`_
|
2021-02-06 02:19:15 +01:00
|
|
|
to your device. Instructions for a CC2531 device:
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
.. code-block:: shell
|
|
|
|
|
2023-04-23 13:03:10 +02:00
|
|
|
wget https://github.com/Koenkk/Z-Stack-firmware/raw/master\
|
|
|
|
/coordinator/Z-Stack_Home_1.2/bin/default/CC2531_DEFAULT_20201127.zip
|
|
|
|
unzip CC2531_DEFAULT_20201127.zip
|
|
|
|
[sudo] cc-tool -e -w CC2531ZNP-Prod.hex
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
- You can disconnect your debugger and downloader cable once the firmware is flashed.
|
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
- Install ``zigbee2mqtt``. First install a node/npm environment, then
|
|
|
|
either install ``zigbee2mqtt`` manually or 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
|
|
|
|
|
2023-04-23 13:03:10 +02:00
|
|
|
# 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
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-04-23 13:03:10 +02:00
|
|
|
# Install dependencies (as user "pi")
|
|
|
|
cd /opt/zigbee2mqtt
|
|
|
|
npm install
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
- 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.
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
- Edit the ``/opt/zigbee2mqtt/data/configuration.yaml`` file to match
|
|
|
|
the configuration of your MQTT broker:
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
.. code-block:: yaml
|
|
|
|
|
2023-04-23 13:03:10 +02:00
|
|
|
# MQTT settings
|
|
|
|
mqtt:
|
|
|
|
# MQTT base topic for zigbee2mqtt MQTT messages
|
2023-09-07 21:32:56 +02:00
|
|
|
topic_prefix: zigbee2mqtt
|
2023-04-23 13:03:10 +02:00
|
|
|
# MQTT server URL
|
|
|
|
server: 'mqtt://localhost'
|
|
|
|
# MQTT server authentication, uncomment if required:
|
|
|
|
# user: my_user
|
|
|
|
# password: my_password
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
- 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.
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
- Start the ``zigbee2mqtt`` daemon on your device (the `official
|
|
|
|
documentation
|
|
|
|
<https://www.zigbee2mqtt.io/getting_started/running_zigbee2mqtt.html#5-optional-running-as-a-daemon-with-systemctl>`_
|
|
|
|
also contains instructions on how to configure it as a ``systemd``
|
|
|
|
service:
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
.. code-block:: shell
|
|
|
|
|
|
|
|
cd /opt/zigbee2mqtt
|
|
|
|
npm start
|
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
- 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.
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
- 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``::
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-09-06 02:44:56 +02: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``)
|
|
|
|
|
2023-01-27 01:59:57 +01:00
|
|
|
Triggers:
|
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttOnlineEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttOfflineEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePropertySetEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePairingEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceConnectedEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceBannedEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceRemovedEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceRemovedFailedEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceWhitelistedEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceRenamedEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceBindEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceUnbindEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupAddedEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupAddedFailedEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemovedEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemovedFailedEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemoveAllEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemoveAllFailedEvent`
|
|
|
|
* :class:`platypush.message.event.zigbee.mqtt.ZigbeeMqttErrorEvent`
|
2023-01-27 01:59:57 +01:00
|
|
|
|
2023-04-23 13:03:10 +02:00
|
|
|
""" # noqa: E501
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2022-04-05 00:07:55 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
2023-09-06 02:44:56 +02:00
|
|
|
host: str,
|
2022-04-05 00:07:55 +02:00
|
|
|
port: int = 1883,
|
2023-09-07 21:32:56 +02:00
|
|
|
topic_prefix: str = 'zigbee2mqtt',
|
|
|
|
base_topic: Optional[str] = None,
|
2022-04-05 00:07:55 +02:00
|
|
|
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
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
:param host: Default MQTT broker where ``zigbee2mqtt`` publishes its messages.
|
2020-01-22 18:34:28 +01:00
|
|
|
:param port: Broker listen port (default: 1883).
|
2023-09-07 21:32:56 +02:00
|
|
|
:param topic_prefix: Prefix for the published topics, as specified in
|
2023-09-06 02:44:56 +02:00
|
|
|
``/opt/zigbee2mqtt/data/configuration.yaml`` (default: '``zigbee2mqtt``').
|
2023-09-07 21:32:56 +02:00
|
|
|
:param base_topic: Legacy alias for ``topic_prefix`` (default:
|
|
|
|
'``zigbee2mqtt``').
|
2023-09-06 02:44:56 +02:00
|
|
|
: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)
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2023-09-07 21:32:56 +02:00
|
|
|
if base_topic:
|
|
|
|
self.logger.warning(
|
2023-09-14 23:05:27 +02:00
|
|
|
'base_topic is deprecated, please use topic_prefix instead'
|
2023-09-07 21:32:56 +02:00
|
|
|
)
|
|
|
|
topic_prefix = base_topic
|
|
|
|
|
2022-04-05 00:07:55 +02:00
|
|
|
super().__init__(
|
|
|
|
host=host,
|
|
|
|
port=port,
|
2023-09-06 02:44:56 +02:00
|
|
|
topics=[
|
2023-09-07 21:32:56 +02:00
|
|
|
f'{topic_prefix}/{topic}'
|
2023-09-06 02:44:56 +02:00
|
|
|
for topic in [
|
|
|
|
'bridge/state',
|
|
|
|
'bridge/log',
|
|
|
|
'bridge/logging',
|
|
|
|
'bridge/devices',
|
|
|
|
'bridge/groups',
|
|
|
|
]
|
|
|
|
],
|
2022-04-05 00:07:55 +02:00
|
|
|
tls_certfile=tls_certfile,
|
|
|
|
tls_keyfile=tls_keyfile,
|
|
|
|
tls_version=tls_version,
|
|
|
|
tls_ciphers=tls_ciphers,
|
|
|
|
username=username,
|
|
|
|
password=password,
|
2023-09-14 23:05:27 +02:00
|
|
|
timeout=timeout,
|
2022-04-05 00:07:55 +02:00
|
|
|
**kwargs,
|
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-09-07 21:32:56 +02:00
|
|
|
self.topic_prefix = topic_prefix
|
2023-09-14 23:05:27 +02:00
|
|
|
self._info = ZigbeeState()
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-01-09 01:02:49 +01:00
|
|
|
@staticmethod
|
|
|
|
def _get_properties(device: dict) -> dict:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Static method that parses the properties of a device from its received
|
|
|
|
definition.
|
|
|
|
"""
|
2023-01-09 01:02:49 +01:00
|
|
|
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:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Static method that parses the options of a device from its received
|
|
|
|
definition.
|
|
|
|
"""
|
2023-01-09 01:02:49 +01:00
|
|
|
return {
|
|
|
|
option['property']: option
|
|
|
|
for option in (device.get('definition') or {}).get('options', [])
|
|
|
|
if option.get('property')
|
|
|
|
}
|
|
|
|
|
2023-02-02 23:21:12 +01:00
|
|
|
def transform_entities(self, entities: Collection[dict]) -> List[Entity]:
|
2022-04-05 00:07:55 +02:00
|
|
|
compatible_entities = []
|
2023-02-02 23:21:12 +01:00
|
|
|
for dev in entities:
|
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)
|
2023-01-14 22:35:17 +01:00
|
|
|
reachable = dev.get('supported', False) and not dev.get(
|
|
|
|
'interviewing', False
|
|
|
|
)
|
2023-01-09 01:02:49 +01:00
|
|
|
|
2022-05-01 21:10:54 +02:00
|
|
|
light_info = self._get_light_meta(dev)
|
2023-02-02 23:21:12 +01:00
|
|
|
dev_entities: List[Entity] = [
|
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'),
|
2023-01-15 20:01:47 +01:00
|
|
|
external_url=self._get_device_url(dev),
|
|
|
|
image_url=self._get_image_url(dev),
|
2022-11-30 00:55:04 +01:00
|
|
|
reachable=reachable,
|
|
|
|
)
|
|
|
|
|
|
|
|
for entity in dev_entities:
|
|
|
|
entity.parent = parent
|
|
|
|
dev_entities.append(parent)
|
|
|
|
|
|
|
|
compatible_entities += dev_entities
|
|
|
|
|
2023-02-02 23:21:12 +01:00
|
|
|
return compatible_entities
|
2022-04-05 00:07:55 +02:00
|
|
|
|
2023-01-15 20:01:47 +01:00
|
|
|
@staticmethod
|
|
|
|
def _get_device_url(device_info: dict) -> Optional[str]:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Static method that returns the zigbee2mqtt URL with the information
|
|
|
|
about a certain device, if the model is available in its definition.
|
|
|
|
"""
|
2023-01-15 20:01:47 +01:00
|
|
|
model = device_info.get('definition', {}).get('model')
|
|
|
|
if not model:
|
2023-01-29 02:34:48 +01:00
|
|
|
return None
|
2023-01-15 20:01:47 +01:00
|
|
|
|
|
|
|
return f'https://www.zigbee2mqtt.io/devices/{model}.html'
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _get_image_url(device_info: dict) -> Optional[str]:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Static method that returns the zigbee2mqtt URL of the image of a
|
|
|
|
certain device, if the model is available in its definition.
|
|
|
|
"""
|
2023-01-15 20:01:47 +01:00
|
|
|
model = device_info.get('definition', {}).get('model')
|
|
|
|
if not model:
|
2023-01-29 02:34:48 +01:00
|
|
|
return None
|
2023-01-15 20:01:47 +01:00
|
|
|
|
|
|
|
return f'https://www.zigbee2mqtt.io/images/devices/{model}.jpg'
|
|
|
|
|
2022-10-29 14:09:44 +02:00
|
|
|
def _get_network_info(self, **kwargs) -> dict:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Refreshes the network information.
|
|
|
|
"""
|
2021-02-06 02:19:15 +01:00
|
|
|
self.logger.info('Fetching Zigbee network information')
|
|
|
|
client = None
|
|
|
|
mqtt_args = self._mqtt_args(**kwargs)
|
2023-09-06 02:44:56 +02:00
|
|
|
timeout = mqtt_args.pop('timeout', DEFAULT_TIMEOUT)
|
2023-01-29 02:34:48 +01:00
|
|
|
info: Dict[str, Any] = {
|
2021-02-06 02:19:15 +01:00
|
|
|
'state': None,
|
|
|
|
'info': {},
|
|
|
|
'config': {},
|
|
|
|
'devices': [],
|
|
|
|
'groups': [],
|
|
|
|
}
|
|
|
|
|
2023-01-29 02:34:48 +01:00
|
|
|
info_ready_events = {topic: threading.Event() for topic in info}
|
2021-02-06 02:19:15 +01:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
def msg_callback(_, __, msg):
|
|
|
|
topic = msg.topic.split('/')[-1]
|
|
|
|
if topic in info:
|
|
|
|
info[topic] = (
|
|
|
|
msg.payload.decode()
|
|
|
|
if topic == 'state'
|
|
|
|
else json.loads(msg.payload.decode())
|
|
|
|
)
|
|
|
|
info_ready_events[topic].set()
|
2021-02-06 02:19:15 +01:00
|
|
|
|
|
|
|
try:
|
2023-02-02 23:21:12 +01:00
|
|
|
client = self._get_client( # pylint: disable=unexpected-keyword-arg
|
|
|
|
**mqtt_args
|
|
|
|
)
|
2023-09-06 02:44:56 +02:00
|
|
|
client.on_message = msg_callback
|
|
|
|
client.connect()
|
2023-09-07 21:32:56 +02:00
|
|
|
client.subscribe(self.topic_prefix + '/bridge/#')
|
2021-02-06 02:19:15 +01:00
|
|
|
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
|
2023-09-06 02:44:56 +02:00
|
|
|
self._info.devices.by_name = {
|
2023-01-13 02:58:47 +01:00
|
|
|
self._preferred_name(device): device
|
2021-02-08 01:45:21 +01:00
|
|
|
for device in info.get('devices', [])
|
|
|
|
}
|
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
self._info.devices.by_address = {
|
2022-04-11 21:16:45 +02:00
|
|
|
device['ieee_address']: device for device in info.get('devices', [])
|
|
|
|
}
|
|
|
|
|
2023-09-06 02:44:56 +02: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:
|
2023-01-29 02:34:48 +01:00
|
|
|
self.logger.warning('Error on MQTT client disconnection: %s', e)
|
2021-02-06 02:19:15 +01:00
|
|
|
|
2022-10-29 14:09:44 +02:00
|
|
|
return info
|
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
def _topic(self, topic: str) -> str:
|
|
|
|
"""
|
|
|
|
Utility method that construct a topic prefixed by the configured base
|
|
|
|
topic.
|
|
|
|
"""
|
2023-09-07 21:32:56 +02:00
|
|
|
return f'{self.topic_prefix}/{topic}'
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2021-02-09 02:33:43 +01:00
|
|
|
@staticmethod
|
|
|
|
def _parse_response(response: Union[dict, Response]) -> dict:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Utility method that flattens a response received on a zigbee2mqtt topic
|
|
|
|
into a dictionary.
|
|
|
|
"""
|
2021-02-09 02:33:43 +01:00
|
|
|
if isinstance(response, Response):
|
2023-02-02 23:21:12 +01:00
|
|
|
rs: dict = response.output # type: ignore
|
|
|
|
response = rs
|
2021-02-09 02:33:43 +01:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
if isinstance(response, dict):
|
|
|
|
assert response.get('status') != 'error', response.get(
|
|
|
|
'error', 'zigbee2mqtt error'
|
|
|
|
)
|
|
|
|
|
|
|
|
return response or {}
|
|
|
|
|
|
|
|
def _run_request(
|
|
|
|
self,
|
|
|
|
topic: str,
|
|
|
|
msg: Union[dict, str],
|
|
|
|
reply_topic: Optional[str] = None,
|
|
|
|
**kwargs,
|
|
|
|
) -> dict:
|
|
|
|
"""
|
2023-09-14 23:05:27 +02:00
|
|
|
Sends a request/message to the Zigbee2MQTT bridge and waits for a
|
2023-09-06 02:44:56 +02:00
|
|
|
response.
|
|
|
|
"""
|
|
|
|
return self._parse_response(
|
|
|
|
self.publish( # type: ignore
|
|
|
|
topic=topic,
|
|
|
|
msg=msg,
|
|
|
|
reply_topic=reply_topic,
|
|
|
|
**self._mqtt_args(**kwargs),
|
|
|
|
)
|
|
|
|
or {}
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
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.
|
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
: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",
|
2023-09-14 23:05:27 +02:00
|
|
|
"friendly_name": "My Light Bulb",
|
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",
|
|
|
|
],
|
|
|
|
"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
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
Enable/disable devices from joining the network.
|
|
|
|
|
|
|
|
This is not persistent (it will not be saved to ``configuration.yaml``).
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
:param permit: Set to True to allow joins, False otherwise.
|
|
|
|
:param timeout: Allow/disallow joins only for this amount of time.
|
2023-09-06 02:44:56 +02:00
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
|
|
|
if timeout:
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
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-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):
|
|
|
|
"""
|
2023-09-06 02:44:56 +02: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
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
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):
|
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
Change the log level at runtime.
|
|
|
|
|
|
|
|
This change will not be persistent.
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
:param level: Possible values: 'debug', 'info', 'warn', 'error'.
|
2023-09-06 02:44:56 +02:00
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
topic=self._topic('bridge/request/config/log_level'),
|
|
|
|
msg={'value': level},
|
|
|
|
reply_topic=self._topic('bridge/response/config/log_level'),
|
|
|
|
**self._mqtt_args(**kwargs),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def device_set_option(self, device: str, option: str, value: Any, **kwargs):
|
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
Change the options of a device.
|
|
|
|
|
|
|
|
Options can only be changed, not added or deleted.
|
2020-01-22 18:34:28 +01:00
|
|
|
|
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.
|
2023-09-06 02:44:56 +02:00
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
topic=self._topic('bridge/request/device/options'),
|
|
|
|
reply_topic=self._topic('bridge/response/device/options'),
|
|
|
|
msg={
|
|
|
|
'id': device,
|
|
|
|
'options': {
|
|
|
|
option: value,
|
2022-04-05 00:07:55 +02:00
|
|
|
},
|
2023-09-06 02:44:56 +02:00
|
|
|
},
|
|
|
|
**self._mqtt_args(**kwargs),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
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.
|
2023-09-06 02:44:56 +02:00
|
|
|
: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
|
2020-01-22 18:34:28 +01:00
|
|
|
restarted unless it's factory reset (default: False).
|
2023-09-06 02:44:56 +02:00
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
topic=self._topic('bridge/request/device/remove'),
|
|
|
|
msg={'id': device, 'force': force},
|
|
|
|
reply_topic=self._topic('bridge/response/device/remove'),
|
|
|
|
**self._mqtt_args(**kwargs),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
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.
|
2023-09-06 02:44:56 +02:00
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
topic=self._topic('bridge/request/device/ban'),
|
|
|
|
reply_topic=self._topic('bridge/response/device/ban'),
|
|
|
|
msg={'id': device},
|
|
|
|
**self._mqtt_args(**kwargs),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def device_whitelist(self, device: str, **kwargs):
|
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
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.
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
:param device: Display name of the device.
|
2023-09-06 02:44:56 +02:00
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
topic=self._topic('bridge/request/device/whitelist'),
|
|
|
|
reply_topic=self._topic('bridge/response/device/whitelist'),
|
|
|
|
msg={'id': device},
|
|
|
|
**self._mqtt_args(**kwargs),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
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.
|
2023-09-06 02:44:56 +02:00
|
|
|
: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-01-22 18:34:28 +01:00
|
|
|
"""
|
2020-02-23 22:54:50 +01:00
|
|
|
if name == device:
|
|
|
|
self.logger.info('Old and new name are the same: nothing to do')
|
2023-09-06 02:44:56 +02:00
|
|
|
return None
|
2020-02-23 22:54:50 +01:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
devices: dict = self.devices().output # type: ignore
|
2022-04-05 00:07:55 +02:00
|
|
|
assert not [
|
|
|
|
dev for dev in devices if dev.get('friendly_name') == name
|
2023-01-29 02:34:48 +01:00
|
|
|
], f'A device named {name} already exists on the network'
|
2020-02-23 22:54:50 +01:00
|
|
|
|
2021-02-08 01:45:21 +01:00
|
|
|
if device:
|
2023-01-29 02:34:48 +01:00
|
|
|
req: Dict[str, Any] = {
|
2021-02-08 01:45:21 +01:00
|
|
|
'from': device,
|
|
|
|
'to': name,
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
req = {
|
|
|
|
'last': True,
|
|
|
|
'to': name,
|
|
|
|
}
|
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
topic=self._topic('bridge/request/device/rename'),
|
|
|
|
msg=req,
|
|
|
|
reply_topic=self._topic('bridge/response/device/rename'),
|
|
|
|
**self._mqtt_args(**kwargs),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
2021-02-08 01:45:21 +01:00
|
|
|
|
|
|
|
@staticmethod
|
2023-01-29 02:34:48 +01:00
|
|
|
def _build_device_get_request(values: List[Dict[str, Any]]) -> Dict[str, Any]:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Prepares a ``device_get`` request, as a dictionary to be sent down to
|
|
|
|
the bridge.
|
|
|
|
|
|
|
|
This makes sure that the properties and options are properly mapped and
|
|
|
|
converted.
|
|
|
|
"""
|
|
|
|
|
2023-09-14 23:05:27 +02:00
|
|
|
def extract_value(val: dict, root: dict, depth: int = 0):
|
|
|
|
for feature in val.get('features', []):
|
2022-11-04 22:51:40 +01:00
|
|
|
new_root = root
|
|
|
|
if depth > 0:
|
2023-09-14 23:05:27 +02:00
|
|
|
new_root = root[val['property']] = root.get(val['property'], {})
|
2022-11-04 22:51:40 +01:00
|
|
|
|
|
|
|
extract_value(feature, new_root, depth=depth + 1)
|
2022-11-02 21:54:47 +01:00
|
|
|
|
2023-09-14 23:05:27 +02:00
|
|
|
if not val.get('access', 1) & 0x4:
|
2022-10-31 00:51:26 +01:00
|
|
|
# Property not readable/query-able
|
2021-02-08 01:45:21 +01:00
|
|
|
return
|
|
|
|
|
2023-09-14 23:05:27 +02:00
|
|
|
if 'features' not in val:
|
|
|
|
if 'property' in val:
|
|
|
|
root[val['property']] = 0 if val['type'] == 'numeric' else ''
|
2021-02-08 01:45:21 +01:00
|
|
|
return
|
|
|
|
|
2023-09-14 23:05:27 +02:00
|
|
|
if 'property' in val:
|
|
|
|
root[val['property']] = root.get(val['property'], {})
|
2021-02-08 01:45:21 +01:00
|
|
|
|
2023-01-29 02:34:48 +01:00
|
|
|
ret: Dict[str, Any] = {}
|
2021-02-08 01:45:21 +01:00
|
|
|
for value in values:
|
|
|
|
extract_value(value, root=ret)
|
|
|
|
|
|
|
|
return ret
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-01-13 02:58:47 +01:00
|
|
|
def _get_device_info(self, device: str, **kwargs) -> dict:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Get or retrieve the information about a device.
|
|
|
|
"""
|
|
|
|
device_info = self._info.devices.by_name.get(
|
2023-01-13 02:58:47 +01:00
|
|
|
# First: check by friendly name
|
|
|
|
device,
|
|
|
|
# Second: check by address
|
2023-09-06 02:44:56 +02:00
|
|
|
self._info.devices.by_address.get(device, {}),
|
2022-04-11 21:16:45 +02:00
|
|
|
)
|
|
|
|
|
2023-01-13 02:58:47 +01:00
|
|
|
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:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Utility method that returns the preferred name of a device, on the basis
|
|
|
|
of which attributes are exposed (friendly name or IEEE address).
|
|
|
|
"""
|
2023-01-13 02:58:47 +01:00
|
|
|
return device.get('friendly_name') or device.get('ieee_address') or ''
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _device_name_matches(cls, name: str, device: dict) -> bool:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Utility method that checks if either the friendly name or IEEE address
|
|
|
|
of a device match a certain string.
|
|
|
|
"""
|
2023-01-13 02:58:47 +01:00
|
|
|
name = str(cls._ieee_address(name))
|
|
|
|
return name == device.get('friendly_name') or name == device.get('ieee_address')
|
|
|
|
|
2020-01-22 18:34:28 +01:00
|
|
|
@action
|
2023-02-02 23:21:12 +01:00
|
|
|
def device_get( # pylint: disable=redefined-builtin
|
2022-04-05 00:07:55 +02:00
|
|
|
self, device: str, property: Optional[str] = None, **kwargs
|
|
|
|
) -> Dict[str, Any]:
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02: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.
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
:param device: Display name of the device.
|
2023-09-06 02:44:56 +02:00
|
|
|
: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).
|
2020-01-22 18:34:28 +01:00
|
|
|
:return: Key->value map of the device properties.
|
|
|
|
"""
|
2021-02-08 01:45:21 +01:00
|
|
|
kwargs = self._mqtt_args(**kwargs)
|
2023-01-13 02:58:47 +01:00
|
|
|
device_info = self._get_device_info(device, **kwargs)
|
|
|
|
assert device_info, f'No such device: {device}'
|
|
|
|
device = self._preferred_name(device_info)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
if property:
|
2023-09-06 02:44:56 +02:00
|
|
|
properties = self._run_request(
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic(device) + f'/get/{property}',
|
|
|
|
reply_topic=self._topic(device),
|
|
|
|
msg={property: ''},
|
|
|
|
**kwargs,
|
2023-09-06 02:44:56 +02:00
|
|
|
)
|
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]}
|
|
|
|
|
2023-01-13 02:58:47 +01:00
|
|
|
exposes = (device_info.get('definition', {}) or {}).get('exposes', [])
|
2021-02-08 01:45:21 +01:00
|
|
|
if not exposes:
|
|
|
|
return {}
|
|
|
|
|
2023-02-11 04:04:21 +01:00
|
|
|
# If the device has no queryable properties, don't specify a reply
|
2023-01-13 02:58:47 +01:00
|
|
|
# topic to listen on
|
|
|
|
req = self._build_device_get_request(exposes)
|
2023-09-14 23:05:27 +02:00
|
|
|
reply_topic = self._topic(device) if req else None
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic(device) + '/get',
|
2023-01-13 02:58:47 +01:00
|
|
|
reply_topic=reply_topic,
|
|
|
|
msg=req,
|
2022-04-05 00:07:55 +02:00
|
|
|
**kwargs,
|
2023-09-06 02:44:56 +02:00
|
|
|
)
|
2022-04-05 00:07:55 +02: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
|
|
|
|
2023-09-06 02:44:56 +02: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).
|
2021-02-19 02:54:12 +01:00
|
|
|
: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(
|
|
|
|
{
|
2023-01-13 02:58:47 +01:00
|
|
|
self._preferred_name(device)
|
2023-09-06 02:44:56 +02:00
|
|
|
for device in list(self.devices(**kwargs).output) # type: ignore
|
2023-01-13 02:58:47 +01:00
|
|
|
if self._preferred_name(device)
|
2022-10-29 14:09:44 +02:00
|
|
|
}
|
|
|
|
)
|
2021-02-19 02:54:12 +01:00
|
|
|
|
|
|
|
def worker(device: str, q: Queue):
|
2023-09-14 23:05:27 +02:00
|
|
|
q.put_nowait(self.device_get(device, **kwargs).output) # type: ignore
|
2021-02-19 02:54:12 +01:00
|
|
|
|
2023-01-29 02:34:48 +01:00
|
|
|
queues: Dict[str, Queue] = {}
|
2021-02-19 02:54:12 +01:00
|
|
|
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:
|
2023-09-14 23:05:27 +02:00
|
|
|
timeout = kwargs.get('timeout')
|
2021-02-19 02:54:12 +01:00
|
|
|
try:
|
2023-09-14 23:05:27 +02:00
|
|
|
response[device] = queues[device].get(timeout=timeout)
|
|
|
|
workers[device].join(timeout=timeout)
|
|
|
|
except Empty:
|
|
|
|
self.logger.warning(
|
|
|
|
'Could not get the status of the device %s: timeout after %f seconds',
|
|
|
|
device,
|
|
|
|
timeout,
|
|
|
|
)
|
2021-02-19 02:54:12 +01:00
|
|
|
except Exception as e:
|
2022-04-05 00:07:55 +02:00
|
|
|
self.logger.warning(
|
2023-01-29 02:34:48 +01:00
|
|
|
'An error occurred while getting the status of the device %s: %s',
|
|
|
|
device,
|
|
|
|
e,
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
2021-02-19 02:54:12 +01:00
|
|
|
|
2023-09-14 23:05:27 +02:00
|
|
|
self.logger.exception(e)
|
|
|
|
|
2021-02-19 02:54:12 +01:00
|
|
|
return response
|
|
|
|
|
2021-03-05 02:23:28 +01:00
|
|
|
@action
|
2023-01-29 02:34:48 +01:00
|
|
|
def status(self, *args, device: Optional[str] = None, **kwargs):
|
2021-03-05 02:23:28 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
Get the status of a device (by friendly name) or of all the connected
|
|
|
|
devices (it wraps :meth:`.devices_get`).
|
2021-03-05 02:23:28 +01:00
|
|
|
|
|
|
|
: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
|
2023-02-02 23:21:12 +01:00
|
|
|
def device_set( # pylint: disable=redefined-builtin
|
2022-10-12 03:00:42 +02:00
|
|
|
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
|
|
|
"""
|
2023-09-06 02:44:56 +02: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.
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
:param device: Display name of the device.
|
|
|
|
:param property: Name of the property that should be set.
|
|
|
|
:param value: New value of the property.
|
2023-09-06 02:44:56 +02:00
|
|
|
:param values: If you want to set multiple values, then pass this
|
|
|
|
mapping instead of ``property``+``value``.
|
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2022-10-12 03:00:42 +02:00
|
|
|
msg = (values or {}).copy()
|
2023-01-13 02:58:47 +01:00
|
|
|
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)
|
2022-11-11 01:46:38 +01:00
|
|
|
|
2022-10-12 03:00:42 +02:00
|
|
|
if property:
|
2023-01-13 02:58:47 +01:00
|
|
|
# Check if we're trying to set an option
|
|
|
|
stored_option = self._get_options(device_info).get(property)
|
|
|
|
if stored_option:
|
|
|
|
return self.device_set_option(device, property, value, **kwargs)
|
2023-01-09 01:02:49 +01:00
|
|
|
|
2023-01-13 02:58:47 +01:00
|
|
|
# Check if it's a property
|
|
|
|
reply_topic = self._topic(device)
|
|
|
|
stored_property = self._get_properties(device_info).get(property)
|
|
|
|
assert stored_property, f'No such property: {property}'
|
2022-11-11 01:46:38 +01:00
|
|
|
|
2023-01-13 02:58:47 +01:00
|
|
|
# Set the new value on the message
|
|
|
|
msg[property] = value
|
2023-01-09 01:02:49 +01:00
|
|
|
|
2023-01-13 02:58:47 +01:00
|
|
|
# Don't wait for an update from a value that is not readable
|
|
|
|
if self._is_write_only(stored_property):
|
2022-11-11 01:46:38 +01:00
|
|
|
reply_topic = None
|
2022-10-12 03:00:42 +02:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
properties = self._run_request(
|
2022-04-05 00:07:55 +02:00
|
|
|
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),
|
2023-09-06 02:44:56 +02:00
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2022-11-11 01:46:38 +01:00
|
|
|
if property and reply_topic:
|
2023-01-13 02:58:47 +01:00
|
|
|
assert (
|
|
|
|
property in properties
|
|
|
|
), f'Could not retrieve the new state for {property}'
|
2020-01-22 18:34:28 +01:00
|
|
|
return {property: properties[property]}
|
|
|
|
|
|
|
|
return properties
|
|
|
|
|
2022-11-11 01:46:38 +01:00
|
|
|
@action
|
2023-02-11 04:04:21 +01:00
|
|
|
# pylint: disable=redefined-builtin
|
2023-02-05 23:07:43 +01:00
|
|
|
def set_value(
|
2023-02-11 04:04:21 +01:00
|
|
|
self, device: str, property: Optional[str] = None, data=None, **kwargs
|
2022-11-11 01:46:38 +01:00
|
|
|
):
|
|
|
|
"""
|
|
|
|
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.
|
2023-09-14 23:05:27 +02:00
|
|
|
:param data: Value to set for the property.
|
2022-11-11 01:46:38 +01:00
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
|
|
|
"""
|
2023-02-02 23:21:12 +01:00
|
|
|
dev, prop = self._ieee_address_and_property(device)
|
2022-11-11 01:46:38 +01:00
|
|
|
if not property:
|
|
|
|
property = prop
|
|
|
|
|
|
|
|
self.device_set(dev, property, data, **kwargs)
|
|
|
|
|
2023-02-11 04:04:21 +01:00
|
|
|
@action
|
|
|
|
def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
|
|
|
|
return self.set_value(entity, data=value, property=attribute, **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.
|
2023-09-06 02:44:56 +02: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
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
ret = self._run_request(
|
|
|
|
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),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
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.
|
2023-09-06 02:44:56 +02: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
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
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),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
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.
|
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
: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
|
|
|
"""
|
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
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2021-02-06 02:19:15 +01:00
|
|
|
|
|
|
|
: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": {
|
2023-09-14 23:05:27 +02:00
|
|
|
"friendly_name": "My Light Bulb"
|
2021-02-06 02:19:15 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
"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
|
2023-02-02 23:21:12 +01:00
|
|
|
def group_add( # pylint: disable=redefined-builtin
|
|
|
|
self, name: str, id: Optional[int] = None, **kwargs
|
|
|
|
):
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
|
|
|
Add a new group.
|
|
|
|
|
|
|
|
:param name: Display name of the group.
|
|
|
|
:param id: Optional numeric ID (default: auto-generated).
|
2023-09-06 02:44:56 +02:00
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
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
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
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
|
2023-02-02 23:21:12 +01:00
|
|
|
def group_get( # pylint: disable=redefined-builtin
|
|
|
|
self, group: str, property: Optional[str] = None, **kwargs
|
|
|
|
) -> dict:
|
2021-02-10 02:00:52 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
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.
|
2021-02-10 02:00:52 +01:00
|
|
|
|
|
|
|
:param group: Display name of the group.
|
2023-09-06 02:44:56 +02:00
|
|
|
: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).
|
2021-02-10 02:00:52 +01:00
|
|
|
"""
|
|
|
|
msg = {}
|
|
|
|
if property:
|
|
|
|
msg = {property: ''}
|
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
properties = self._run_request(
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic(group + '/get'),
|
|
|
|
reply_topic=self._topic(group),
|
|
|
|
msg=msg,
|
|
|
|
**self._mqtt_args(**kwargs),
|
2023-09-06 02:44:56 +02:00
|
|
|
)
|
2021-02-10 02:00:52 +01:00
|
|
|
|
|
|
|
if property:
|
2023-09-06 02:44:56 +02:00
|
|
|
assert property in properties, f'No such property: {property}'
|
2021-02-10 02:00:52 +01:00
|
|
|
return {property: properties[property]}
|
|
|
|
|
|
|
|
return properties
|
|
|
|
|
2020-02-23 22:54:50 +01:00
|
|
|
@action
|
2023-02-02 23:21:12 +01:00
|
|
|
def group_set( # pylint: disable=redefined-builtin
|
|
|
|
self, group: str, property: str, value: Any, **kwargs
|
|
|
|
):
|
2020-02-23 22:54:50 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
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.
|
2020-02-23 22:54:50 +01:00
|
|
|
|
|
|
|
:param group: Display name of the group.
|
|
|
|
:param property: Name of the property that should be set.
|
|
|
|
:param value: New value of the property.
|
2023-09-06 02:44:56 +02:00
|
|
|
: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
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
properties = self._run_request(
|
2022-04-05 00:07:55 +02:00
|
|
|
topic=self._topic(group + '/set'),
|
|
|
|
reply_topic=self._topic(group),
|
|
|
|
msg={property: value},
|
|
|
|
**self._mqtt_args(**kwargs),
|
2023-09-06 02:44:56 +02:00
|
|
|
)
|
2020-02-23 22:54:50 +01:00
|
|
|
|
|
|
|
if property:
|
2023-09-06 02:44:56 +02:00
|
|
|
assert property in properties, f'No such property: {property}'
|
2020-02-23 22:54:50 +01:00
|
|
|
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.
|
2023-09-06 02:44:56 +02:00
|
|
|
: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 == group:
|
|
|
|
self.logger.info('Old and new name are the same: nothing to do')
|
2023-09-06 02:44:56 +02:00
|
|
|
return None
|
2020-02-23 22:54:50 +01:00
|
|
|
|
2022-10-29 14:09:44 +02:00
|
|
|
groups = {
|
2023-09-06 02:44:56 +02:00
|
|
|
g.get('friendly_name'): g
|
|
|
|
for g in dict(self.groups().output) # type: ignore
|
2022-10-29 14:09:44 +02:00
|
|
|
}
|
|
|
|
|
2023-01-29 02:34:48 +01:00
|
|
|
assert name not in groups, f'A group named {name} already exists on the network'
|
2020-02-23 22:54:50 +01:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
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),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
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.
|
2023-09-06 02:44:56 +02:00
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
topic=self._topic('bridge/request/group/remove'),
|
|
|
|
reply_topic=self._topic('bridge/response/group/remove'),
|
|
|
|
msg=name,
|
|
|
|
**self._mqtt_args(**kwargs),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
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.
|
2023-09-06 02:44:56 +02:00
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
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),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
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.
|
2023-09-06 02:44:56 +02:00
|
|
|
: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).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2023-01-29 02:34:48 +01:00
|
|
|
remove_suffix = '_all' if device is None else ''
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
topic=self._topic(f'bridge/request/group/members/remove{remove_suffix}'),
|
|
|
|
reply_topic=self._topic(
|
|
|
|
f'bridge/response/group/members/remove{remove_suffix}'
|
|
|
|
),
|
|
|
|
msg={
|
|
|
|
'group': group,
|
|
|
|
'device': device,
|
|
|
|
},
|
|
|
|
**self._mqtt_args(**kwargs),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
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
|
|
|
"""
|
2023-09-06 02:44:56 +02: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>`_.
|
|
|
|
You can also bind a specific device endpoint - for example
|
|
|
|
``MySensor/temperature``.
|
|
|
|
:param target: Name of the target device. You can also bind a specific
|
|
|
|
device endpoint - for example ``MyLight/state``.
|
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
topic=self._topic('bridge/request/device/bind'),
|
|
|
|
reply_topic=self._topic('bridge/response/device/bind'),
|
|
|
|
msg={'from': source, 'to': target},
|
|
|
|
**self._mqtt_args(**kwargs),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
2020-01-22 18:34:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def unbind_devices(self, source: str, target: str, **kwargs):
|
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
Remove a binding between two devices.
|
2020-01-22 18:34:28 +01:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
:param source: Name of the source device. You can also bind a specific
|
|
|
|
device endpoint - for example ``MySensor/temperature``.
|
|
|
|
:param target: Name of the target device. You can also bind a specific
|
|
|
|
device endpoint - for example ``MyLight/state``.
|
|
|
|
:param kwargs: Extra arguments to be passed to
|
|
|
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
|
|
|
the default configured device).
|
2020-01-22 18:34:28 +01:00
|
|
|
"""
|
2023-09-06 02:44:56 +02:00
|
|
|
return self._run_request(
|
|
|
|
topic=self._topic('bridge/request/device/unbind'),
|
|
|
|
reply_topic=self._topic('bridge/response/device/unbind'),
|
|
|
|
msg={'from': source, 'to': target},
|
|
|
|
**self._mqtt_args(**kwargs),
|
2022-04-05 00:07:55 +02:00
|
|
|
)
|
2021-02-19 02:54:12 +01:00
|
|
|
|
|
|
|
@action
|
2023-09-06 02:44:56 +02:00
|
|
|
def on(self, device, *_, **__): # pylint: disable=arguments-differ
|
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-13 02:58:47 +01:00
|
|
|
device, prop_info = self._get_switch_info(device)
|
2023-01-29 02:34:48 +01:00
|
|
|
return self.device_set(
|
2023-01-13 02:58:47 +01:00
|
|
|
device, prop_info['property'], prop_info.get('value_on', 'ON')
|
2023-01-29 02:34:48 +01:00
|
|
|
)
|
2021-02-19 02:54:12 +01:00
|
|
|
|
|
|
|
@action
|
2023-09-06 02:44:56 +02:00
|
|
|
def off(self, device, *_, **__): # pylint: disable=arguments-differ
|
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-13 02:58:47 +01:00
|
|
|
device, prop_info = self._get_switch_info(device)
|
2023-01-29 02:34:48 +01:00
|
|
|
return self.device_set(
|
2023-01-13 02:58:47 +01:00
|
|
|
device, prop_info['property'], prop_info.get('value_off', 'OFF')
|
2023-01-29 02:34:48 +01:00
|
|
|
)
|
2021-02-19 02:54:12 +01:00
|
|
|
|
|
|
|
@action
|
2023-09-06 02:44:56 +02:00
|
|
|
def toggle(self, device, *_, **__): # pylint: disable=arguments-differ
|
2023-01-09 01:02:49 +01:00
|
|
|
"""
|
|
|
|
Toggles the state of a switch, a binary property or an option.
|
|
|
|
"""
|
2023-01-13 02:58:47 +01:00
|
|
|
device, prop_info = self._get_switch_info(device)
|
|
|
|
prop = prop_info['property']
|
2023-09-06 02:44:56 +02:00
|
|
|
device_state: dict = self.device_get(device).output # type: ignore
|
2023-01-29 02:34:48 +01:00
|
|
|
return self.device_set(
|
2023-01-13 02:58:47 +01:00
|
|
|
device,
|
2023-01-09 01:02:49 +01:00
|
|
|
prop,
|
2023-01-13 02:58:47 +01:00
|
|
|
prop_info.get(
|
2023-01-09 01:02:49 +01:00
|
|
|
'value_toggle',
|
2023-01-13 02:58:47 +01:00
|
|
|
'OFF'
|
|
|
|
if device_state.get(prop) == prop_info.get('value_on', 'ON')
|
|
|
|
else 'ON',
|
2023-01-09 01:02:49 +01:00
|
|
|
),
|
2023-01-29 02:34:48 +01:00
|
|
|
)
|
2021-02-19 02:54:12 +01:00
|
|
|
|
2023-01-13 02:58:47 +01:00
|
|
|
def _get_switch_info(self, name: str) -> Tuple[str, dict]:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Get the information about a switch or switch-like device by name or
|
|
|
|
address.
|
|
|
|
"""
|
2023-02-02 23:21:12 +01:00
|
|
|
name, prop = self._ieee_address_and_property(name)
|
2023-01-13 02:58:47 +01:00
|
|
|
if not prop or prop == 'light':
|
|
|
|
prop = 'state'
|
2022-04-11 21:16:45 +02:00
|
|
|
|
2023-01-13 02:58:47 +01:00
|
|
|
device_info = self._get_device_info(name)
|
|
|
|
assert device_info, f'No such device: {name}'
|
|
|
|
name = self._preferred_name(device_info)
|
2022-04-11 21:16:45 +02:00
|
|
|
|
2023-02-11 04:04:21 +01:00
|
|
|
prop_info = self._get_properties(device_info).get(prop)
|
2023-01-13 02:58:47 +01:00
|
|
|
option = self._get_options(device_info).get(prop)
|
|
|
|
if option:
|
|
|
|
return name, option
|
2023-01-09 01:02:49 +01:00
|
|
|
|
2023-02-11 04:04:21 +01:00
|
|
|
assert prop_info, f'No such property on device {name}: {prop}'
|
|
|
|
return name, prop_info
|
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:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Utility method that checks if a feature is read-only on the basis of its
|
|
|
|
access flags.
|
|
|
|
"""
|
2022-11-02 16:38:17 +01:00
|
|
|
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:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Utility method that checks if a feature is write-only on the basis of
|
|
|
|
its access flags.
|
|
|
|
"""
|
2022-11-02 16:38:17 +01:00
|
|
|
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:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
2023-09-14 23:05:27 +02:00
|
|
|
Utility method that checks if a feature doesn't support programmatic
|
2023-09-06 02:44:56 +02:00
|
|
|
querying (i.e. it will only broadcast its state when available) on the
|
|
|
|
basis of its access flags.
|
|
|
|
"""
|
2022-11-11 20:40:36 +01:00
|
|
|
return bool(feature.get('access', 0) & 4) == 0
|
|
|
|
|
2022-11-02 16:38:17 +01:00
|
|
|
@staticmethod
|
2023-02-02 23:21:12 +01:00
|
|
|
def _ieee_address_and_property(
|
|
|
|
device: Union[dict, str]
|
|
|
|
) -> Tuple[str, Optional[str]]:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Given a device property, as a dictionary containing the full device
|
|
|
|
definition or a string containing the device address and property,
|
|
|
|
return a tuple in the format ``(device_address, property_name)``.
|
|
|
|
"""
|
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(':')
|
2023-09-14 23:05:27 +02:00
|
|
|
return parts[0], parts[1] if len(parts) > 1 else None
|
2022-11-11 01:46:38 +01:00
|
|
|
|
2023-09-14 23:05:27 +02:00
|
|
|
return dev, None
|
2023-02-02 23:21:12 +01:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _ieee_address(cls, device: Union[dict, str]) -> str:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
:return: The IEEE address of a device, given its full definition or
|
|
|
|
common name.
|
|
|
|
"""
|
2023-02-02 23:21:12 +01:00
|
|
|
return cls._ieee_address_and_property(device)[0]
|
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]:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
A utility method that parses the properties of a device that can be
|
|
|
|
mapped to switches (or switch-like entities).
|
|
|
|
"""
|
2023-01-09 01:02:49 +01:00
|
|
|
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-09-06 02:44:56 +02:00
|
|
|
def _get_sensors( # pylint: disable=too-many-branches
|
2023-01-09 01:02:49 +01:00
|
|
|
cls, device_info: dict, props: dict, options: dict
|
|
|
|
) -> List[Sensor]:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
A utility method that parses the properties of a device that can be
|
|
|
|
mapped to sensors (or sensor-like entities).
|
|
|
|
"""
|
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]:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
A utility method that parses the properties of a device that can be
|
|
|
|
mapped to dimmers (or dimmer-like entities).
|
|
|
|
"""
|
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]:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
A utility method that parses the properties of a device that can be
|
|
|
|
mapped to switches with enum values.
|
|
|
|
"""
|
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
|
2023-02-02 23:21:12 +01:00
|
|
|
def _to_entity( # pylint: disable=redefined-builtin
|
2023-01-09 01:02:49 +01:00
|
|
|
cls,
|
|
|
|
entity_type: Type[Entity],
|
|
|
|
device_info: dict,
|
|
|
|
property: dict,
|
|
|
|
options: dict,
|
|
|
|
**kwargs,
|
|
|
|
) -> Entity:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Give the information about a device and its properties and options, it
|
|
|
|
builds an entity of the right type.
|
|
|
|
"""
|
2023-01-09 01:02:49 +01:00
|
|
|
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:
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
Parse the attributes of a device that can be mapped to lights (or
|
|
|
|
light-like entities).
|
|
|
|
"""
|
|
|
|
# pylint: disable=too-many-nested-blocks
|
|
|
|
for exposed in (device_info.get('definition', {}) or {}).get('exposes', []):
|
2022-05-01 21:10:54 +02:00
|
|
|
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'],
|
2023-09-06 02:44:56 +02:00
|
|
|
'value_toggle': feature.get('value_toggle'),
|
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
|
2023-02-02 23:21:12 +01:00
|
|
|
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
|
2023-01-13 23:28:12 +01:00
|
|
|
lights = [str(self._ieee_address(light)) for light in lights]
|
2023-01-13 02:58:47 +01:00
|
|
|
devices = [self._get_device_info(light) for light in lights]
|
2022-05-01 21:10:54 +02:00
|
|
|
|
2023-01-13 02:58:47 +01:00
|
|
|
for i, dev in enumerate(devices):
|
|
|
|
assert dev, f'No such device: {lights[i]}'
|
2022-05-01 21:10:54 +02:00
|
|
|
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
|
|
|
|
2023-01-13 02:58:47 +01:00
|
|
|
self.device_set(self._preferred_name(dev), values=data)
|
2022-05-01 21:10:54 +02:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
def on_mqtt_message(self):
|
|
|
|
"""
|
|
|
|
Overrides :meth:`platypush.plugins.mqtt.MqttPlugin.on_mqtt_message` to
|
|
|
|
handle messages from the zigbee2mqtt integration.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def handler(client: MqttClient, _, msg: mqtt.MQTTMessage):
|
2023-09-14 23:05:27 +02:00
|
|
|
topic_idx = len(self.topic_prefix) + 1
|
|
|
|
topic = msg.topic[topic_idx:]
|
2023-09-06 02:44:56 +02:00
|
|
|
data = msg.payload.decode()
|
|
|
|
if not data:
|
|
|
|
return
|
|
|
|
|
|
|
|
with contextlib.suppress(ValueError, TypeError):
|
|
|
|
data = json.loads(data)
|
|
|
|
|
|
|
|
if topic == 'bridge/state':
|
|
|
|
self._process_state_message(client, data)
|
|
|
|
elif topic in ['bridge/log', 'bridge/logging']:
|
|
|
|
self._process_log_message(client, data)
|
|
|
|
elif topic == 'bridge/devices':
|
|
|
|
self._process_devices(client, data)
|
|
|
|
elif topic == 'bridge/groups':
|
|
|
|
self._process_groups(client, data)
|
|
|
|
elif isinstance(data, dict):
|
|
|
|
name = topic.split('/')[-1]
|
|
|
|
if name not in self._info.devices:
|
|
|
|
self.logger.debug('Skipping unknown topic: %s', topic)
|
|
|
|
return
|
|
|
|
|
|
|
|
dev = self._info.devices.get(name)
|
|
|
|
assert dev is not None, f'No such device: {name}'
|
2023-09-14 23:05:27 +02:00
|
|
|
changed_props = {
|
|
|
|
k: v for k, v in data.items() if v != dev.get('state', {}).get(k)
|
|
|
|
}
|
2023-09-06 02:44:56 +02:00
|
|
|
|
|
|
|
if changed_props:
|
|
|
|
self._process_property_update(name, data)
|
|
|
|
self._bus.post(
|
|
|
|
ZigbeeMqttDevicePropertySetEvent(
|
|
|
|
host=client.host,
|
|
|
|
port=client.port,
|
|
|
|
device=name,
|
|
|
|
properties=changed_props,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2023-09-14 23:05:27 +02:00
|
|
|
dev = self._info.devices.get(name)
|
|
|
|
if dev:
|
|
|
|
self._info.devices.set_state(
|
|
|
|
dev.get('friendly_name') or dev.get('ieee_address'), data
|
|
|
|
)
|
2023-09-06 02:44:56 +02:00
|
|
|
|
|
|
|
return handler
|
|
|
|
|
|
|
|
@property
|
|
|
|
def _bus(self) -> Bus:
|
|
|
|
"""
|
|
|
|
Utility property for the bus.
|
|
|
|
"""
|
|
|
|
return get_bus()
|
|
|
|
|
|
|
|
def _process_state_message(self, client: MqttClient, msg: str):
|
|
|
|
"""
|
|
|
|
Process a state message.
|
|
|
|
"""
|
2023-09-14 23:05:27 +02:00
|
|
|
if msg == self._info.bridge_state:
|
2023-09-06 02:44:56 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
if msg == 'online':
|
|
|
|
evt = ZigbeeMqttOnlineEvent
|
2023-09-14 23:05:27 +02:00
|
|
|
self._info.bridge_state = BridgeState.ONLINE
|
2023-09-06 02:44:56 +02:00
|
|
|
elif msg == 'offline':
|
|
|
|
evt = ZigbeeMqttOfflineEvent
|
2023-09-14 23:05:27 +02:00
|
|
|
self._info.bridge_state = BridgeState.OFFLINE
|
2023-09-06 02:44:56 +02:00
|
|
|
self.logger.warning('The zigbee2mqtt service is offline')
|
|
|
|
else:
|
|
|
|
return
|
2023-01-27 01:59:57 +01:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
self._bus.post(evt(host=client.host, port=client.port))
|
2023-01-29 02:34:48 +01:00
|
|
|
|
2023-09-06 02:44:56 +02:00
|
|
|
# pylint: disable=too-many-branches
|
|
|
|
def _process_log_message(self, client, msg):
|
|
|
|
"""
|
2023-09-14 23:05:27 +02:00
|
|
|
Process a log event.
|
2023-09-06 02:44:56 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
msg_type = msg.get('type')
|
|
|
|
text = msg.get('message')
|
|
|
|
args = {'host': client._host, 'port': client._port}
|
|
|
|
|
|
|
|
if msg_type == 'devices':
|
|
|
|
devices = {}
|
|
|
|
for dev in text or []:
|
|
|
|
devices[dev['friendly_name']] = dev
|
2023-09-07 21:32:56 +02:00
|
|
|
client.subscribe(self.topic_prefix + '/' + dev['friendly_name'])
|
2023-09-06 02:44:56 +02:00
|
|
|
elif msg_type == 'pairing':
|
|
|
|
self._bus.post(ZigbeeMqttDevicePairingEvent(device=text, **args))
|
|
|
|
elif msg_type in ['device_ban', 'device_banned']:
|
|
|
|
self._bus.post(ZigbeeMqttDeviceBannedEvent(device=text, **args))
|
|
|
|
elif msg_type in ['device_removed_failed', 'device_force_removed_failed']:
|
|
|
|
force = msg_type == 'device_force_removed_failed'
|
|
|
|
self._bus.post(
|
|
|
|
ZigbeeMqttDeviceRemovedFailedEvent(device=text, force=force, **args)
|
|
|
|
)
|
|
|
|
elif msg_type == 'device_whitelisted':
|
|
|
|
self._bus.post(ZigbeeMqttDeviceWhitelistedEvent(device=text, **args))
|
|
|
|
elif msg_type == 'device_renamed':
|
|
|
|
self._bus.post(ZigbeeMqttDeviceRenamedEvent(device=text, **args))
|
|
|
|
elif msg_type == 'device_bind':
|
|
|
|
self._bus.post(ZigbeeMqttDeviceBindEvent(device=text, **args))
|
|
|
|
elif msg_type == 'device_unbind':
|
|
|
|
self._bus.post(ZigbeeMqttDeviceUnbindEvent(device=text, **args))
|
|
|
|
elif msg_type == 'device_group_add':
|
|
|
|
self._bus.post(ZigbeeMqttGroupAddedEvent(group=text, **args))
|
|
|
|
elif msg_type == 'device_group_add_failed':
|
|
|
|
self._bus.post(ZigbeeMqttGroupAddedFailedEvent(group=text, **args))
|
|
|
|
elif msg_type == 'device_group_remove':
|
|
|
|
self._bus.post(ZigbeeMqttGroupRemovedEvent(group=text, **args))
|
|
|
|
elif msg_type == 'device_group_remove_failed':
|
|
|
|
self._bus.post(ZigbeeMqttGroupRemovedFailedEvent(group=text, **args))
|
|
|
|
elif msg_type == 'device_group_remove_all':
|
|
|
|
self._bus.post(ZigbeeMqttGroupRemoveAllEvent(group=text, **args))
|
|
|
|
elif msg_type == 'device_group_remove_all_failed':
|
|
|
|
self._bus.post(ZigbeeMqttGroupRemoveAllFailedEvent(group=text, **args))
|
|
|
|
elif msg_type == 'zigbee_publish_error':
|
|
|
|
self.logger.error('zigbee2mqtt error: {}'.format(text))
|
|
|
|
self._bus.post(ZigbeeMqttErrorEvent(error=text, **args))
|
|
|
|
elif msg.get('level') in ['warning', 'error']:
|
|
|
|
log = getattr(self.logger, msg['level'])
|
|
|
|
log(
|
|
|
|
'zigbee2mqtt {}: {}'.format(
|
|
|
|
msg['level'], text or msg.get('error', msg.get('warning'))
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
def _process_devices(self, client: MqttClient, msg):
|
|
|
|
"""
|
|
|
|
Process a list of devices received on the zigbee2mqtt bridge.
|
|
|
|
"""
|
|
|
|
devices_info = {
|
|
|
|
device.get('friendly_name', device.get('ieee_address')): device
|
|
|
|
for device in msg
|
|
|
|
}
|
|
|
|
|
|
|
|
# Subscribe to updates from all the known devices
|
|
|
|
event_args = {'host': client.host, 'port': client.port}
|
|
|
|
client.subscribe(
|
2023-09-07 21:32:56 +02:00
|
|
|
*[self.topic_prefix + '/' + device for device in devices_info.keys()]
|
2023-09-06 02:44:56 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
for name, device in devices_info.items():
|
|
|
|
# If we haven't cached this device yet, then notify about the
|
|
|
|
# connection of a new device.
|
|
|
|
if not self._info.devices.get(name):
|
|
|
|
self._bus.post(
|
|
|
|
ZigbeeMqttDeviceConnectedEvent(device=name, **event_args)
|
|
|
|
)
|
|
|
|
|
|
|
|
# Send a request to fetch all the known properties of this device
|
|
|
|
exposes = (device.get('definition', {}) or {}).get('exposes', [])
|
|
|
|
payload = self._build_device_get_request(exposes)
|
|
|
|
if payload:
|
|
|
|
client.publish(
|
2023-09-07 21:32:56 +02:00
|
|
|
self.topic_prefix + '/' + name + '/get',
|
2023-09-06 02:44:56 +02:00
|
|
|
json.dumps(payload),
|
|
|
|
)
|
|
|
|
|
|
|
|
# Send a request to fetch all the known properties of this device
|
|
|
|
for name in self._info.devices.by_name.copy():
|
|
|
|
if name not in devices_info:
|
|
|
|
self._bus.post(ZigbeeMqttDeviceRemovedEvent(device=name, **event_args))
|
|
|
|
self._info.devices.remove(name)
|
|
|
|
|
2023-09-14 23:05:27 +02:00
|
|
|
for dev in devices_info.values():
|
|
|
|
self._info.devices.add(dev)
|
2023-09-06 02:44:56 +02:00
|
|
|
|
|
|
|
def _process_groups(self, client: MqttClient, msg):
|
|
|
|
"""
|
|
|
|
Process an MQTT message containing an updated list of groups.
|
|
|
|
"""
|
|
|
|
event_args = {'host': client.host, 'port': client.port}
|
|
|
|
groups_info = {
|
|
|
|
group.get('friendly_name', group.get('id')): group for group in msg
|
|
|
|
}
|
|
|
|
|
|
|
|
# Trigger ZigbeeMqttGroupAddedEvent for each new group
|
|
|
|
for name in groups_info.keys():
|
|
|
|
if name not in self._info.groups:
|
|
|
|
self._bus.post(ZigbeeMqttGroupAddedEvent(group=name, **event_args))
|
|
|
|
|
|
|
|
# Trigger ZigbeeMqttGroupRemovedEvent for each removed group
|
|
|
|
for name in self._info.groups.copy():
|
|
|
|
if name not in groups_info:
|
|
|
|
self._bus.post(ZigbeeMqttGroupRemovedEvent(group=name, **event_args))
|
|
|
|
del self._info.groups[name]
|
|
|
|
|
|
|
|
# Reset the groups cache
|
|
|
|
self._info.groups = {group: {} for group in groups_info.keys()}
|
|
|
|
|
|
|
|
def _process_property_update(self, device_name: str, properties: Mapping):
|
|
|
|
"""
|
|
|
|
Process an MQTT message containing a device property update.
|
|
|
|
|
|
|
|
It will appropriately forward an
|
|
|
|
:class:`platypush.message.event.entities.EntityUpdateEvent` to the bus.
|
|
|
|
"""
|
2023-09-14 23:05:27 +02:00
|
|
|
device_info = self._info.devices.get(device_name)
|
2023-09-06 02:44:56 +02:00
|
|
|
if not (device_info and properties):
|
|
|
|
return
|
|
|
|
|
|
|
|
self.publish_entities(
|
|
|
|
[
|
|
|
|
{
|
|
|
|
**device_info,
|
|
|
|
'state': properties,
|
|
|
|
}
|
|
|
|
]
|
|
|
|
)
|
2023-01-27 01:59:57 +01:00
|
|
|
|
2021-02-06 14:45:50 +01:00
|
|
|
|
2021-02-19 02:54:12 +01:00
|
|
|
# vim:sw=4:ts=4:et:
|