From 575635fd6b5f959cfe17efeb82f8f851777ad9af Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sat, 11 Feb 2023 04:04:21 +0100 Subject: [PATCH] Defined `set` as a base method for all plugins that implement writeable entities --- platypush/entities/_managers/switches.py | 47 +++++++++++-------- platypush/message/event/bluetooth/ble.py | 1 + platypush/plugins/smartthings/__init__.py | 30 +++++++----- platypush/plugins/switchbot/__init__.py | 23 +++++++-- .../plugins/switchbot/bluetooth/__init__.py | 24 ++++++---- platypush/plugins/zigbee/mqtt/__init__.py | 22 +++++---- platypush/plugins/zwave/_base.py | 8 +++- 7 files changed, 102 insertions(+), 53 deletions(-) diff --git a/platypush/entities/_managers/switches.py b/platypush/entities/_managers/switches.py index eef80f0f..ff3c7364 100644 --- a/platypush/entities/_managers/switches.py +++ b/platypush/entities/_managers/switches.py @@ -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. diff --git a/platypush/message/event/bluetooth/ble.py b/platypush/message/event/bluetooth/ble.py index 5289e6b9..ba3df987 100644 --- a/platypush/message/event/bluetooth/ble.py +++ b/platypush/message/event/bluetooth/ble.py @@ -36,6 +36,7 @@ class BluetoothEvent(Event): connected=connected, paired=paired, blocked=blocked, + trusted=trusted, service_uuids=service_uuids or [], **kwargs ) diff --git a/platypush/plugins/smartthings/__init__.py b/platypush/plugins/smartthings/__init__.py index 3c7dd867..f92ec91c 100644 --- a/platypush/plugins/smartthings/__init__.py +++ b/platypush/plugins/smartthings/__init__.py @@ -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 diff --git a/platypush/plugins/switchbot/__init__.py b/platypush/plugins/switchbot/__init__.py index 54ad171c..5d750fe4 100644 --- a/platypush/plugins/switchbot/__init__.py +++ b/platypush/plugins/switchbot/__init__.py @@ -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 ``:``. + :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, diff --git a/platypush/plugins/switchbot/bluetooth/__init__.py b/platypush/plugins/switchbot/bluetooth/__init__.py index 353a59f2..cf8caaf4 100644 --- a/platypush/plugins/switchbot/bluetooth/__init__.py +++ b/platypush/plugins/switchbot/bluetooth/__init__.py @@ -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( diff --git a/platypush/plugins/zigbee/mqtt/__init__.py b/platypush/plugins/zigbee/mqtt/__init__.py index 2094cde1..74505bb7 100644 --- a/platypush/plugins/zigbee/mqtt/__init__.py +++ b/platypush/plugins/zigbee/mqtt/__init__.py @@ -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 `_. @@ -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: diff --git a/platypush/plugins/zwave/_base.py b/platypush/plugins/zwave/_base.py index cdcc3705..0bd3e80a 100644 --- a/platypush/plugins/zwave/_base.py +++ b/platypush/plugins/zwave/_base.py @@ -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(