diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/EnumSwitch.vue b/platypush/backend/http/webapp/src/components/panels/Entities/EnumSwitch.vue
new file mode 100644
index 0000000000..501c87bdf4
--- /dev/null
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/EnumSwitch.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
index 4fba884441..8158aa4881 100644
--- a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
@@ -87,6 +87,14 @@
}
},
+ "enum_switch": {
+ "name": "Switch",
+ "name_plural": "Switches",
+ "icon": {
+ "class": "fas fa-gauge"
+ }
+ },
+
"switch": {
"name": "Switch",
"name_plural": "Switches",
diff --git a/platypush/entities/switches.py b/platypush/entities/switches.py
index 86a72e9085..6adc9d51f0 100644
--- a/platypush/entities/switches.py
+++ b/platypush/entities/switches.py
@@ -1,4 +1,4 @@
-from sqlalchemy import Column, Integer, ForeignKey, Boolean
+from sqlalchemy import Column, Integer, ForeignKey, Boolean, String, JSON
from .devices import Device, entity_types_registry
@@ -20,3 +20,23 @@ if not entity_types_registry.get('Switch'):
entity_types_registry['Switch'] = Switch
else:
Switch = entity_types_registry['Switch']
+
+
+if not entity_types_registry.get('EnumSwitch'):
+
+ class EnumSwitch(Device):
+ __tablename__ = 'enum_switch'
+
+ id = Column(
+ Integer, ForeignKey(Device.id, ondelete='CASCADE'), primary_key=True
+ )
+ value = Column(String)
+ values = Column(JSON)
+
+ __mapper_args__ = {
+ 'polymorphic_identity': __tablename__,
+ }
+
+ entity_types_registry['EnumSwitch'] = EnumSwitch
+else:
+ EnumSwitch = entity_types_registry['EnumSwitch']
diff --git a/platypush/plugins/zigbee/mqtt/__init__.py b/platypush/plugins/zigbee/mqtt/__init__.py
index 9fe0970fcc..1db41ec644 100644
--- a/platypush/plugins/zigbee/mqtt/__init__.py
+++ b/platypush/plugins/zigbee/mqtt/__init__.py
@@ -3,7 +3,7 @@ import re
import threading
from queue import Queue
-from typing import Optional, List, Any, Dict, Union
+from typing import Optional, List, Any, Dict, Union, Tuple
from platypush.entities import manages
from platypush.entities.batteries import Battery
@@ -17,7 +17,7 @@ from platypush.entities.humidity import HumiditySensor
from platypush.entities.lights import Light
from platypush.entities.linkquality import LinkQuality
from platypush.entities.sensors import Sensor, BinarySensor, NumericSensor
-from platypush.entities.switches import Switch
+from platypush.entities.switches import Switch, EnumSwitch
from platypush.entities.temperature import TemperatureSensor
from platypush.message import Mapping
from platypush.message.response import Response
@@ -173,8 +173,6 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
}
def transform_entities(self, devices):
- from platypush.entities.switches import Switch
-
compatible_entities = []
for dev in devices:
if not dev:
@@ -197,6 +195,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
light_info = self._get_light_meta(dev)
switch_info = self._get_switch_meta(dev)
sensors = self._get_sensors(dev)
+ enum_switches = self._get_enum_switches(dev)
if light_info:
compatible_entities.append(
@@ -263,6 +262,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
if sensors:
compatible_entities += sensors
+ if enum_switches:
+ compatible_entities += enum_switches
return super().transform_entities(compatible_entities) # type: ignore
@@ -906,22 +907,63 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
(default: query the default configured device).
"""
msg = (values or {}).copy()
+ reply_topic = self._topic(device)
+
if property:
msg[property] = value
+ stored_property = next(
+ iter(
+ exposed
+ for exposed in (
+ self._info.get('devices_by_addr', {})
+ .get(device, {})
+ .get('definition', {})
+ .get('exposes', {})
+ )
+ if exposed.get('property') == property
+ ),
+ None,
+ )
+
+ if stored_property and self._is_write_only(stored_property):
+ # Don't wait for an update from a value that is not readable
+ reply_topic = None
properties = self.publish(
topic=self._topic(device + '/set'),
- reply_topic=self._topic(device),
+ reply_topic=reply_topic,
msg=msg,
**self._mqtt_args(**kwargs),
).output # type: ignore[reportGeneralTypeIssues]
- if property:
+ if property and reply_topic:
assert property in properties, 'No such property: ' + property
return {property: properties[property]}
return properties
+ @action
+ def set_value(
+ self, device: str, property: Optional[str] = None, data=None, **kwargs
+ ):
+ """
+ Entity-compatible way of setting a value on a node.
+
+ :param device: Device friendly name, IEEE address or internal entity ID
+ in ``
:`` format.
+ :param property: Name of the property to set. If not specified here, it
+ should be specified on ``device`` in ``:``
+ format.
+ :param kwargs: Extra arguments to be passed to
+ :meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
+ the default configured device).
+ """
+ dev, prop = self._ieee_address(device, with_property=True)
+ if not property:
+ property = prop
+
+ self.device_set(dev, property, data, **kwargs)
+
@action
def device_check_ota_updates(self, device: str, **kwargs) -> dict:
"""
@@ -1436,7 +1478,9 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
)
@staticmethod
- def _ieee_address(device: Union[dict, str]) -> str:
+ def _ieee_address(
+ device: Union[dict, str], with_property=False
+ ) -> Union[str, Tuple[str, Optional[str]]]:
# Entity value IDs are stored in the `:`
# format. Therefore, we need to split by `:` if we want to
# retrieve the original address.
@@ -1447,8 +1491,14 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
# IEEE address + property format
if re.search(r'^0x[0-9a-fA-F]{16}:', dev):
- return dev.split(':')[0]
- return dev
+ parts = dev.split(':')
+ return (
+ (parts[0], parts[1] if len(parts) > 1 else None)
+ if with_property
+ else parts[0]
+ )
+
+ return (dev, None) if with_property else dev
@classmethod
def _get_switch_meta(cls, device_info: dict) -> dict:
@@ -1538,6 +1588,41 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
return sensors
+ @classmethod
+ def _get_enum_switches(cls, device_info: dict) -> List[Sensor]:
+ devices = []
+ exposes = [
+ exposed
+ for exposed in (device_info.get('definition', {}) or {}).get('exposes', [])
+ if (
+ exposed.get('property')
+ and exposed.get('access', 0) & 2
+ and exposed.get('type') == 'enum'
+ and exposed.get('values')
+ )
+ ]
+
+ for exposed in exposes:
+ devices.append(
+ EnumSwitch(
+ id=f'{device_info["ieee_address"]}:{exposed["property"]}',
+ name=(
+ device_info.get('friendly_name', '[Unnamed device]')
+ + ' ['
+ + exposed.get('description', '')
+ + ']'
+ ),
+ value=device_info.get(exposed['property']),
+ values=exposed.get('values', []),
+ description=exposed.get('description'),
+ is_read_only=cls._is_read_only(exposed),
+ is_write_only=cls._is_write_only(exposed),
+ data=device_info,
+ )
+ )
+
+ return devices
+
@classmethod
def _get_light_meta(cls, device_info: dict) -> dict:
exposes = (device_info.get('definition', {}) or {}).get('exposes', [])