forked from platypush/platypush
Implemented EnumSwitch
entity type
Done for `zigbee.mqtt`, other plugins will follow
This commit is contained in:
parent
801ed05684
commit
00a43dd1f8
4 changed files with 252 additions and 10 deletions
|
@ -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>
|
|
@ -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",
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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', [])
|
||||||
|
|
Loading…
Reference in a new issue