Defined `set` as a base method for all plugins that implement writeable entities

This commit is contained in:
Fabio Manganiello 2023-02-11 04:04:21 +01:00
parent 4365352331
commit 575635fd6b
Signed by: blacklight
GPG Key ID: D90FBA7F76362774
7 changed files with 102 additions and 53 deletions

View File

@ -1,9 +1,30 @@
from abc import ABC, abstractmethod
from typing import Any, Optional
from typing_extensions import override
from . import EntityManager
class SwitchEntityManager(EntityManager, ABC):
class WriteableEntityManager(EntityManager, ABC):
"""
Base class for integrations that support entities whose values can be set.
"""
@abstractmethod
def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
"""
Set the value of an entity.
:param entity: The entity to set the value for. It's usually the ID of
the entity provided by the plugin.
:param value: The value to set the entity to.
:param attribute: The name of the attribute to set for the entity, if
required by the integration.
"""
raise NotImplementedError()
class SwitchEntityManager(WriteableEntityManager, ABC):
"""
Base class for integrations that support binary switches.
"""
@ -23,31 +44,19 @@ class SwitchEntityManager(EntityManager, ABC):
"""Toggle the state of a device (on->off or off->on)"""
raise NotImplementedError()
class MultiLevelSwitchEntityManager(EntityManager, ABC):
"""
Base class for integrations that support dimmers/multi-level/enum switches.
Don't extend this class directly. Instead, use on of the available
intermediate abstract classes - like ``DimmerEntityManager`` or
``EnumSwitchEntityManager``.
"""
@abstractmethod
def set_value( # pylint: disable=redefined-builtin
self, device=None, property=None, *, data=None, **__
):
"""Set a value"""
raise NotImplementedError()
@override
def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
method = self.on if value else self.off
return method(entity, **kwargs)
class DimmerEntityManager(MultiLevelSwitchEntityManager, ABC):
class DimmerEntityManager(WriteableEntityManager, ABC):
"""
Base class for integrations that support dimmers/multi-level switches.
"""
class EnumSwitchEntityManager(MultiLevelSwitchEntityManager, ABC):
class EnumSwitchEntityManager(WriteableEntityManager, ABC):
"""
Base class for integrations that support switches with a pre-defined,
enum-like set of possible values.

View File

@ -36,6 +36,7 @@ class BluetoothEvent(Event):
connected=connected,
paired=paired,
blocked=blocked,
trusted=trusted,
service_uuids=service_uuids or [],
**kwargs
)

View File

@ -33,6 +33,7 @@ from platypush.utils import camel_case_to_snake_case
from ._mappers import DeviceMapper, device_mappers
# pylint: disable=too-many-ancestors
class SmartthingsPlugin(
RunnablePlugin,
DimmerEntityManager,
@ -817,18 +818,18 @@ class SmartthingsPlugin(
:param device: Device ID or name.
:param level: Level, usually a percentage value between 0 and 1.
:param kwarsg: Extra arguments that should be passed to :meth:`.execute`.
:param kwargs: Extra arguments that should be passed to :meth:`.execute`.
"""
return self.set_value(device, Capability.switch_level, level, **kwargs)
def _set_value( # pylint: disable=redefined-builtin
self, device: str, property: Optional[str] = None, data=None, **kwargs
self, device: str, property: Optional[str] = None, value: Any = None, **kwargs
):
if not property:
device, property = self._to_device_and_property(device)
assert property, 'No property name specified'
assert data is not None, 'No value specified'
assert value is not None, 'No value specified'
entity_id = f'{device}:{property}'
entity = self._entities_by_id.get(entity_id)
assert entity, f'No such entity ID: {entity_id}'
@ -837,13 +838,15 @@ class SmartthingsPlugin(
iter([m for m in device_mappers if m.attribute == property]), None
)
assert mapper, f'No mappers found to set {property}={data} on device "{device}"'
assert (
mapper
), f'No mappers found to set {property}={value} on device "{device}"'
assert (
mapper.set_command
), f'The property "{property}" on the device "{device}" cannot be set'
command = (
mapper.set_command(data)
mapper.set_command(value)
if callable(mapper.set_command)
else mapper.set_command
)
@ -852,16 +855,20 @@ class SmartthingsPlugin(
device,
mapper.capability,
command,
args=mapper.set_value_args(data), # type: ignore
args=mapper.set_value_args(value), # type: ignore
**kwargs,
)
return self.status(device)
@action
# pylint: disable=redefined-builtin,arguments-differ
def set_value(
self, device: str, *_, property: Optional[str] = None, data=None, **kwargs
def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
super().set(entity, value, attribute, **kwargs)
return self.set_value(entity, property=attribute, value=value, **kwargs)
@action
def set_value( # pylint: disable=redefined-builtin
self, device: str, property: Optional[str] = None, value=None, **kwargs
):
"""
Set the value of a device. It is compatible with the generic
@ -871,10 +878,11 @@ class SmartthingsPlugin(
``device_id:property``.
:param property: Name of the property to be set. If not specified here
then it should be specified on the ``device`` level.
:param data: Value to be set.
:param value: Value to set.
"""
assert device, 'No device specified'
try:
return self._set_value(device, property, data, **kwargs)
return self._set_value(device, property, value, **kwargs)
except Exception as e:
self.logger.exception(e)
raise AssertionError(e) from e

View File

@ -1104,10 +1104,21 @@ class SwitchbotPlugin(
return self._run('post', 'scenes', scenes[0]['id'], 'execute')
@action
# pylint: disable=redefined-builtin,arguments-differ
def set_value(self, device: str, *_, property=None, data=None, **__):
# pylint: disable=redefined-builtin
def set_value(
self, device: str, property: Optional[str] = None, value: Any = None, **__
):
"""
Set the value of a property of a device.
:param device: Device name or ID, or entity (external) ID.
:param property: Property to set. It should be present if you are
passing a root device ID to ``device`` and not an atomic entity in
the format ``<device_id>:<property_name>``.
:param value: Value to set.
"""
entity = self._to_entity(device, property)
assert entity, f'No such entity: "{device}"'
assert entity, f'No such device: "{device}"'
dt = entity.data.get('device_type')
assert dt, f'Could not infer the device type for "{device}"'
@ -1117,7 +1128,11 @@ class SwitchbotPlugin(
assert setter_class, f'No setters found for device type "{device_type}"'
setter = setter_class(entity)
return setter(property=property, value=data)
return setter(property=property, value=value)
@action
def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
return self.set_value(entity, property=attribute, value=value, **kwargs)
def _to_entity(
self,

View File

@ -1,5 +1,5 @@
import enum
from typing import Collection
from typing import Any, Collection, Optional
from uuid import UUID
from bleak.backends.device import BLEDevice
@ -98,28 +98,32 @@ class SwitchbotBluetoothPlugin(BluetoothBlePlugin, EnumSwitchEntityManager):
loop = get_or_create_event_loop()
return loop.run_until_complete(self._run(device, Command.OFF))
@override
@action
def set_value(self, device: str, *_, data: str, **__):
def set_value(self, device: Optional[str] = None, value: Optional[str] = None, **_):
"""
Entity-compatible ``set_value`` method to send a command to a device.
Send a command to a device as a value.
:param device: Device name or address
:param data: Command to send. Possible values are:
:param entity: Device name or address
:param value: Command to send. Possible values are:
- ``on``: Press the button and remain in the pressed state.
- ``off``: Release a previously pressed button.
- ``press``: Press and release the button.
"""
if data == 'on':
assert device, 'No device specified'
if value == 'on':
self.on(device)
if data == 'off':
if value == 'off':
self.off(device)
if data == 'press':
if value == 'press':
self.press(device)
self.logger.warning('Unknown command for SwitchBot "%s": "%s"', device, data)
self.logger.warning('Unknown command for SwitchBot "%s": "%s"', device, value)
@override
def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
return self.set_value(entity, value, **kwargs)
@override
def transform_entities(

View File

@ -13,6 +13,7 @@ from typing import (
Type,
Union,
)
from typing_extensions import override
from platypush.entities import (
DimmerEntityManager,
@ -48,6 +49,7 @@ from platypush.plugins import RunnablePlugin
from platypush.plugins.mqtt import MqttPlugin, action
# pylint: disable=too-many-ancestors
class ZigbeeMqttPlugin(
RunnablePlugin,
MqttPlugin,
@ -56,7 +58,7 @@ class ZigbeeMqttPlugin(
LightEntityManager,
SensorEntityManager,
SwitchEntityManager,
): # lgtm [py/missing-call-to-init]
):
"""
This plugin allows you to interact with Zigbee devices over MQTT through any Zigbee sniffer and
`zigbee2mqtt <https://www.zigbee2mqtt.io/>`_.
@ -616,7 +618,6 @@ class ZigbeeMqttPlugin(
"genLevelCtrl",
"touchlink",
"lightingColorCtrl",
"manuSpecificUbisysDimmerSetup"
],
"output": [
"genOta"
@ -925,7 +926,7 @@ class ZigbeeMqttPlugin(
if not exposes:
return {}
# If the device has no queriable properties, don't specify a reply
# If the device has no queryable properties, don't specify a reply
# topic to listen on
req = self._build_device_get_request(exposes)
reply_topic = self._topic(device)
@ -1072,9 +1073,9 @@ class ZigbeeMqttPlugin(
return properties
@action
# pylint: disable=redefined-builtin,arguments-differ
# pylint: disable=redefined-builtin
def set_value(
self, device: str, *_, property: Optional[str] = None, data=None, **kwargs
self, device: str, property: Optional[str] = None, data=None, **kwargs
):
"""
Entity-compatible way of setting a value on a node.
@ -1094,6 +1095,11 @@ class ZigbeeMqttPlugin(
self.device_set(dev, property, data, **kwargs)
@override
@action
def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
return self.set_value(entity, data=value, property=attribute, **kwargs)
@action
def device_check_ota_updates(self, device: str, **kwargs) -> dict:
"""
@ -1578,13 +1584,13 @@ class ZigbeeMqttPlugin(
assert device_info, f'No such device: {name}'
name = self._preferred_name(device_info)
prop = self._get_properties(device_info).get(prop)
prop_info = self._get_properties(device_info).get(prop)
option = self._get_options(device_info).get(prop)
if option:
return name, option
assert prop, f'No such property on device {name}: {prop}'
return name, prop
assert prop_info, f'No such property on device {name}: {prop}'
return name, prop_info
@staticmethod
def _is_read_only(feature: dict) -> bool:

View File

@ -325,7 +325,7 @@ class ZwaveBasePlugin(
@abstractmethod
@action
def set_value( # pylint: disable=arguments-differ
def set_value(
self,
data,
value_id: Optional[int] = None,
@ -347,6 +347,12 @@ class ZwaveBasePlugin(
"""
raise NotImplementedError
@action
def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
return self.set_value(
value_id=entity, id_on_network=entity, data=value, **kwargs
)
@abstractmethod
@action
def set_value_label(