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 abc import ABC, abstractmethod
from typing import Any, Optional
from typing_extensions import override
from . import EntityManager 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. 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)""" """Toggle the state of a device (on->off or off->on)"""
raise NotImplementedError() raise NotImplementedError()
@override
class MultiLevelSwitchEntityManager(EntityManager, ABC): def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
""" method = self.on if value else self.off
Base class for integrations that support dimmers/multi-level/enum switches. return method(entity, **kwargs)
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()
class DimmerEntityManager(MultiLevelSwitchEntityManager, ABC): class DimmerEntityManager(WriteableEntityManager, ABC):
""" """
Base class for integrations that support dimmers/multi-level switches. 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, Base class for integrations that support switches with a pre-defined,
enum-like set of possible values. enum-like set of possible values.

View File

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

View File

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

View File

@ -1104,10 +1104,21 @@ class SwitchbotPlugin(
return self._run('post', 'scenes', scenes[0]['id'], 'execute') return self._run('post', 'scenes', scenes[0]['id'], 'execute')
@action @action
# pylint: disable=redefined-builtin,arguments-differ # pylint: disable=redefined-builtin
def set_value(self, device: str, *_, property=None, data=None, **__): 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) 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') dt = entity.data.get('device_type')
assert dt, f'Could not infer the device type for "{device}"' 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}"' assert setter_class, f'No setters found for device type "{device_type}"'
setter = setter_class(entity) 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( def _to_entity(
self, self,

View File

@ -1,5 +1,5 @@
import enum import enum
from typing import Collection from typing import Any, Collection, Optional
from uuid import UUID from uuid import UUID
from bleak.backends.device import BLEDevice from bleak.backends.device import BLEDevice
@ -98,28 +98,32 @@ class SwitchbotBluetoothPlugin(BluetoothBlePlugin, EnumSwitchEntityManager):
loop = get_or_create_event_loop() loop = get_or_create_event_loop()
return loop.run_until_complete(self._run(device, Command.OFF)) return loop.run_until_complete(self._run(device, Command.OFF))
@override
@action @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 entity: Device name or address
:param data: Command to send. Possible values are: :param value: Command to send. Possible values are:
- ``on``: Press the button and remain in the pressed state. - ``on``: Press the button and remain in the pressed state.
- ``off``: Release a previously pressed button. - ``off``: Release a previously pressed button.
- ``press``: Press and release the button. - ``press``: Press and release the button.
""" """
if data == 'on': assert device, 'No device specified'
if value == 'on':
self.on(device) self.on(device)
if data == 'off': if value == 'off':
self.off(device) self.off(device)
if data == 'press': if value == 'press':
self.press(device) 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 @override
def transform_entities( def transform_entities(

View File

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

View File

@ -325,7 +325,7 @@ class ZwaveBasePlugin(
@abstractmethod @abstractmethod
@action @action
def set_value( # pylint: disable=arguments-differ def set_value(
self, self,
data, data,
value_id: Optional[int] = None, value_id: Optional[int] = None,
@ -347,6 +347,12 @@ class ZwaveBasePlugin(
""" """
raise NotImplementedError 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 @abstractmethod
@action @action
def set_value_label( def set_value_label(