Implemented `EnumSwitch` entity type

Done for `zigbee.mqtt`, other plugins will follow
This commit is contained in:
Fabio Manganiello 2022-11-11 01:46:38 +01:00
parent 801ed05684
commit 00a43dd1f8
Signed by: blacklight
GPG Key ID: D90FBA7F76362774
4 changed files with 252 additions and 10 deletions

View File

@ -0,0 +1,129 @@
<template>
<div class="entity switch-container">
<div class="head" :class="{expanded: expanded}">
<div class="col-1 icon">
<EntityIcon
:icon="this.value.meta?.icon || {}"
:loading="loading"
:error="error" />
</div>
<div class="col-s-8 col-m-9 label">
<div class="name" v-text="value.name" />
</div>
<div class="col-s-3 col-m-2 buttons pull-right">
<button @click.stop="expanded = !expanded" v-if="hasValues">
<i class="fas"
:class="{'fa-angle-up': expanded, 'fa-angle-down': !expanded}" />
</button>
<span class="value"
v-text="value.value"
v-if="value?.value != null" />
</div>
</div>
<div class="body" v-if="expanded" @click.stop="prevent">
<div class="row">
<div class="input">
<select @input="setValue" ref="values">
<option value="" v-if="value.is_write_only" selected>--</option>
<option :value="v" v-for="v in value.values" :key="v" v-text="v" />
</select>
</div>
</div>
</div>
</div>
</template>
<script>
import EntityMixin from "./EntityMixin"
import EntityIcon from "./EntityIcon"
export default {
name: 'EnumSwitch',
components: {EntityIcon},
mixins: [EntityMixin],
data() {
return {
expanded: false,
}
},
computed: {
hasValues() {
return !!this?.value?.values?.length
}
},
methods: {
prevent(event) {
event.stopPropagation()
return false
},
async setValue(event) {
if (!event.target.value?.length)
return
this.$emit('loading', true)
if (this.value.is_write_only) {
const self = this;
setTimeout(() => {
self.$refs.values.value = ''
}, 1000)
}
try {
await this.request('entities.execute', {
id: this.value.id,
action: 'set_value',
data: event.target.value,
})
} finally {
this.$emit('loading', false)
}
},
},
}
</script>
<style lang="scss" scoped>
@import "common";
.switch-container {
.head {
.buttons {
button {
margin-right: 0.5em;
}
}
.value {
font-size: 1.1em;
font-weight: bold;
opacity: 0.7;
}
}
.body {
.row {
display: flex;
.icon {
width: 2em;
text-align: center;
}
.input {
width: calc(100% - 2em);
select {
width: 100%;
}
}
}
}
}
</style>

View File

@ -87,6 +87,14 @@
} }
}, },
"enum_switch": {
"name": "Switch",
"name_plural": "Switches",
"icon": {
"class": "fas fa-gauge"
}
},
"switch": { "switch": {
"name": "Switch", "name": "Switch",
"name_plural": "Switches", "name_plural": "Switches",

View File

@ -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 from .devices import Device, entity_types_registry
@ -20,3 +20,23 @@ if not entity_types_registry.get('Switch'):
entity_types_registry['Switch'] = Switch entity_types_registry['Switch'] = Switch
else: else:
Switch = entity_types_registry['Switch'] 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']

View File

@ -3,7 +3,7 @@ import re
import threading import threading
from queue import Queue 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 import manages
from platypush.entities.batteries import Battery 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.lights import Light
from platypush.entities.linkquality import LinkQuality from platypush.entities.linkquality import LinkQuality
from platypush.entities.sensors import Sensor, BinarySensor, NumericSensor 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.entities.temperature import TemperatureSensor
from platypush.message import Mapping from platypush.message import Mapping
from platypush.message.response import Response from platypush.message.response import Response
@ -173,8 +173,6 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
} }
def transform_entities(self, devices): def transform_entities(self, devices):
from platypush.entities.switches import Switch
compatible_entities = [] compatible_entities = []
for dev in devices: for dev in devices:
if not dev: if not dev:
@ -197,6 +195,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
light_info = self._get_light_meta(dev) light_info = self._get_light_meta(dev)
switch_info = self._get_switch_meta(dev) switch_info = self._get_switch_meta(dev)
sensors = self._get_sensors(dev) sensors = self._get_sensors(dev)
enum_switches = self._get_enum_switches(dev)
if light_info: if light_info:
compatible_entities.append( compatible_entities.append(
@ -263,6 +262,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
if sensors: if sensors:
compatible_entities += sensors compatible_entities += sensors
if enum_switches:
compatible_entities += enum_switches
return super().transform_entities(compatible_entities) # type: ignore 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). (default: query the default configured device).
""" """
msg = (values or {}).copy() msg = (values or {}).copy()
reply_topic = self._topic(device)
if property: if property:
msg[property] = value 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( properties = self.publish(
topic=self._topic(device + '/set'), topic=self._topic(device + '/set'),
reply_topic=self._topic(device), reply_topic=reply_topic,
msg=msg, msg=msg,
**self._mqtt_args(**kwargs), **self._mqtt_args(**kwargs),
).output # type: ignore[reportGeneralTypeIssues] ).output # type: ignore[reportGeneralTypeIssues]
if property: if property and reply_topic:
assert property in properties, 'No such property: ' + property assert property in properties, 'No such property: ' + property
return {property: properties[property]} return {property: properties[property]}
return properties 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 ``<address>:<property>`` format.
:param property: Name of the property to set. If not specified here, it
should be specified on ``device`` in ``<address>:<property>``
format.
:param kwargs: Extra arguments to be passed to
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
the default configured device).
"""
dev, prop = self._ieee_address(device, with_property=True)
if not property:
property = prop
self.device_set(dev, property, data, **kwargs)
@action @action
def device_check_ota_updates(self, device: str, **kwargs) -> dict: def device_check_ota_updates(self, device: str, **kwargs) -> dict:
""" """
@ -1436,7 +1478,9 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
) )
@staticmethod @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 `<address>:<property>` # Entity value IDs are stored in the `<address>:<property>`
# format. Therefore, we need to split by `:` if we want to # format. Therefore, we need to split by `:` if we want to
# retrieve the original address. # retrieve the original address.
@ -1447,8 +1491,14 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
# IEEE address + property format # IEEE address + property format
if re.search(r'^0x[0-9a-fA-F]{16}:', dev): if re.search(r'^0x[0-9a-fA-F]{16}:', dev):
return dev.split(':')[0] parts = dev.split(':')
return dev 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 @classmethod
def _get_switch_meta(cls, device_info: dict) -> dict: def _get_switch_meta(cls, device_info: dict) -> dict:
@ -1538,6 +1588,41 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
return sensors 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 @classmethod
def _get_light_meta(cls, device_info: dict) -> dict: def _get_light_meta(cls, device_info: dict) -> dict:
exposes = (device_info.get('definition', {}) or {}).get('exposes', []) exposes = (device_info.get('definition', {}) or {}).get('exposes', [])