diff --git a/platypush/entities/__init__.py b/platypush/entities/__init__.py
index 5697abbe16..5c9e7a7035 100644
--- a/platypush/entities/__init__.py
+++ b/platypush/entities/__init__.py
@@ -1,18 +1,26 @@
import logging
from typing import Collection, Optional
-from ._base import Entity, get_entities_registry
+from ._base import Entity, get_entities_registry, init_entities_db
from ._engine import EntitiesEngine
-from ._registry import manages, register_entity_plugin, get_plugin_entity_registry
+from ._managers import register_entity_manager, get_plugin_entity_registry
+from ._managers.lights import LightEntityManager
+from ._managers.sensors import SensorEntityManager
+from ._managers.switches import (
+ SwitchEntityManager,
+ DimmerEntityManager,
+ EnumSwitchEntityManager,
+)
_engine: Optional[EntitiesEngine] = None
logger = logging.getLogger(__name__)
def init_entities_engine() -> EntitiesEngine:
- from ._base import init_entities_db
-
- global _engine
+ """
+ Initialize and start the entities engine.
+ """
+ global _engine # pylint: disable=global-statement
init_entities_db()
_engine = EntitiesEngine()
_engine.start()
@@ -20,6 +28,17 @@ def init_entities_engine() -> EntitiesEngine:
def publish_entities(entities: Collection[Entity]):
+ """
+ Publish a collection of entities to be processed by the engine.
+
+ The engine will:
+
+ - Normalize and merge the provided entities.
+ - Trigger ``EntityUpdateEvent`` events.
+ - Persist the new state to the local database.
+
+ :param entities: Entities to be published.
+ """
if not _engine:
logger.debug('No entities engine registered')
return
@@ -28,12 +47,16 @@ def publish_entities(entities: Collection[Entity]):
__all__ = (
- 'Entity',
+ 'DimmerEntityManager',
'EntitiesEngine',
+ 'Entity',
+ 'EnumSwitchEntityManager',
+ 'LightEntityManager',
+ 'SensorEntityManager',
+ 'SwitchEntityManager',
+ 'get_entities_registry',
+ 'get_plugin_entity_registry',
'init_entities_engine',
'publish_entities',
- 'register_entity_plugin',
- 'get_plugin_entity_registry',
- 'get_entities_registry',
- 'manages',
+ 'register_entity_manager',
)
diff --git a/platypush/entities/_managers/__init__.py b/platypush/entities/_managers/__init__.py
new file mode 100644
index 0000000000..cb3cdcf356
--- /dev/null
+++ b/platypush/entities/_managers/__init__.py
@@ -0,0 +1,155 @@
+import inspect
+import json
+from abc import ABC, abstractmethod
+from datetime import datetime
+from typing import Any, Optional, Dict, Collection, Type
+
+from platypush.config import Config
+from platypush.entities import Entity
+from platypush.utils import get_plugin_name_by_class, get_redis
+
+_entity_registry_varname = '_platypush/plugin_entity_registry'
+
+
+class EntityManager(ABC):
+ """
+ Base mixin for all the integrations that support entities mapping.
+
+ The classes that implement the entity manager need to implement the
+ :meth:`.transform_entities` method, which will convert the supported
+ entities from whichever format the integration supports to a collection of
+ :class:`platypush.entities.Entity` objects.
+
+ The converted entities will then be passed to the
+ :class:`platypush.entities.EntitiesEngine` whenever
+ :meth:`.publish_entities` is called
+
+ The implemented classes should also implement the :meth:`.status` method.
+ This method should retrieve the current state of the entities and call
+ :meth:`.publish_entities`.
+ """
+
+ def __new__(cls, *_, **__) -> 'EntityManager':
+ register_entity_manager(cls)
+ return super().__new__(cls)
+
+ @abstractmethod
+ def transform_entities(self, entities: Collection[Any]) -> Collection[Entity]:
+ """
+ This method takes a list of entities in any (plugin-specific)
+ format and converts them into a standardized collection of
+ `Entity` objects. Since this method is called by
+ :meth:`.publish_entities` before entity updates are published,
+ you may usually want to extend it to pre-process the entities
+ managed by your extension into the standard format before they
+ are stored and published to all the consumers.
+ """
+ assert all(isinstance(e, Entity) for e in entities), (
+ 'Expected all the instances to be entities, got '
+ f'{[e.__class__.__name__ for e in entities]}'
+ )
+ return entities
+
+ @abstractmethod
+ def status(self, *_, **__):
+ """
+ All derived classes should implement this method.
+
+ At the very least, this method should refresh the current state of the
+ integration's entities and call :meth:`.publish_entities`.
+
+ It should also return the current state of the entities as a list of
+ serialized entities, if possible.
+ """
+ raise NotImplementedError(
+ 'The `status` method has not been implemented in '
+ f'{self.__class__.__name__}'
+ )
+
+ def _normalize_entities(self, entities: Collection[Entity]) -> Collection[Entity]:
+ for entity in entities:
+ if entity.id:
+ # Entity IDs can only refer to the internal primary key
+ entity.external_id = entity.id
+ entity.id = None # type: ignore
+
+ entity.plugin = get_plugin_name_by_class(self.__class__) # type: ignore
+ entity.updated_at = datetime.utcnow() # type: ignore
+
+ return entities
+
+ def publish_entities(self, entities: Optional[Collection[Any]]):
+ """
+ Publishes a list of entities. The downstream consumers include:
+
+ - The entity persistence manager
+ - The web server
+ - Any consumer subscribed to
+ :class:`platypush.message.event.entities.EntityUpdateEvent`
+ events (e.g. web clients)
+
+ You usually don't need to override this class (but you may want to
+ extend :meth:`.transform_entities` instead if your extension doesn't
+ natively handle `Entity` objects).
+ """
+ from platypush.entities import publish_entities
+
+ transformed_entities = self._normalize_entities(
+ self.transform_entities(entities or [])
+ )
+
+ publish_entities(transformed_entities)
+
+
+def register_entity_manager(cls: Type[EntityManager]):
+ """
+ Associates a plugin as a manager for a certain entity type.
+ You usually don't have to call this method directly.
+ """
+ entity_managers = [
+ c
+ for c in inspect.getmro(cls)
+ if issubclass(c, EntityManager) and c not in {cls, EntityManager}
+ ]
+
+ plugin_name = get_plugin_name_by_class(cls) or ''
+ redis = get_redis()
+ registry = get_plugin_entity_registry()
+ registry_by_plugin = set(registry['by_plugin'].get(plugin_name, []))
+
+ for manager in entity_managers:
+ entity_type_name = manager.__name__
+ registry_by_type = set(registry['by_type'].get(entity_type_name, []))
+ registry_by_plugin.add(entity_type_name)
+ registry_by_type.add(plugin_name)
+ registry['by_plugin'][plugin_name] = list(registry_by_plugin)
+ registry['by_type'][entity_type_name] = list(registry_by_type)
+
+ redis.mset({_entity_registry_varname: json.dumps(registry)})
+
+
+def get_plugin_entity_registry() -> Dict[str, Dict[str, Collection[str]]]:
+ """
+ Get the `plugin->entity_types` and `entity_type->plugin`
+ mappings supported by the current configuration.
+ """
+ redis = get_redis()
+ registry = redis.mget([_entity_registry_varname])[0]
+ try:
+ registry = json.loads((registry or b'').decode())
+ except (TypeError, ValueError):
+ return {'by_plugin': {}, 'by_type': {}}
+
+ enabled_plugins = set(Config.get_plugins().keys())
+
+ return {
+ 'by_plugin': {
+ plugin_name: entity_types
+ for plugin_name, entity_types in registry.get('by_plugin', {}).items()
+ if plugin_name in enabled_plugins
+ },
+ 'by_type': {
+ entity_type: [p for p in plugins if p in enabled_plugins]
+ for entity_type, plugins in registry.get('by_type', {}).items()
+ },
+ }
diff --git a/platypush/entities/_managers/lights.py b/platypush/entities/_managers/lights.py
new file mode 100644
index 0000000000..c918b6ac03
--- /dev/null
+++ b/platypush/entities/_managers/lights.py
@@ -0,0 +1,20 @@
+from abc import ABC, abstractmethod
+
+from .switches import SwitchEntityManager
+
+
+class LightEntityManager(SwitchEntityManager, ABC):
+ """
+ Base class for integrations that support light/bulbs entities.
+ """
+
+ @abstractmethod
+ def set_lights(self, *args, lights=None, **kwargs):
+ """
+ Set a set of properties on a set of lights.
+
+ :param light: List of lights to set. Each item can represent a light
+ name or ID.
+ :param kwargs: key-value list of the parameters to set.
+ """
+ raise NotImplementedError()
diff --git a/platypush/entities/_managers/sensors.py b/platypush/entities/_managers/sensors.py
new file mode 100644
index 0000000000..002e11a660
--- /dev/null
+++ b/platypush/entities/_managers/sensors.py
@@ -0,0 +1,9 @@
+from abc import ABC
+
+from . import EntityManager
+
+
+class SensorEntityManager(EntityManager, ABC):
+ """
+ Base class for integrations that support sensor entities.
+ """
diff --git a/platypush/entities/_managers/switches.py b/platypush/entities/_managers/switches.py
new file mode 100644
index 0000000000..bf0be56f5c
--- /dev/null
+++ b/platypush/entities/_managers/switches.py
@@ -0,0 +1,54 @@
+from abc import ABC, abstractmethod
+
+from . import EntityManager
+
+
+class SwitchEntityManager(EntityManager, ABC):
+ """
+ Base class for integrations that support binary switches.
+ """
+
+ @abstractmethod
+ def on(self, *_, **__):
+ """Turn on a device"""
+ raise NotImplementedError()
+
+ @abstractmethod
+ def off(self, *_, **__):
+ """Turn off a device"""
+ raise NotImplementedError()
+
+ @abstractmethod
+ def toggle(self, *_, **__):
+ """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, *entities, property=None, value=None, **__
+ ):
+ """Set a value"""
+ raise NotImplementedError()
+
+
+class DimmerEntityManager(MultiLevelSwitchEntityManager, ABC):
+ """
+ Base class for integrations that support dimmers/multi-level switches.
+ """
+
+
+class EnumSwitchEntityManager(MultiLevelSwitchEntityManager, ABC):
+ """
+ Base class for integrations that support switches with a pre-defined,
+ enum-like set of possible values.
+ """
diff --git a/platypush/entities/_registry.py b/platypush/entities/_registry.py
deleted file mode 100644
index baaeabc950..0000000000
--- a/platypush/entities/_registry.py
+++ /dev/null
@@ -1,135 +0,0 @@
-import json
-from datetime import datetime
-from typing import Optional, Dict, Collection, Type
-
-from platypush.config import Config
-from platypush.plugins import Plugin
-from platypush.utils import get_plugin_name_by_class, get_redis
-
-from ._base import Entity
-
-_entity_registry_varname = '_platypush/plugin_entity_registry'
-
-
-def register_entity_plugin(entity_type: Type[Entity], plugin: Plugin):
- """
- Associates a plugin as a manager for a certain entity type.
- If you use the `@manages` decorator then you usually don't have
- to call this method directly.
- """
- plugin_name = get_plugin_name_by_class(plugin.__class__) or ''
- entity_type_name = entity_type.__name__.lower()
- redis = get_redis()
- registry = get_plugin_entity_registry()
- registry_by_plugin = set(registry['by_plugin'].get(plugin_name, []))
-
- registry_by_entity_type = set(registry['by_entity_type'].get(entity_type_name, []))
-
- registry_by_plugin.add(entity_type_name)
- registry_by_entity_type.add(plugin_name)
- registry['by_plugin'][plugin_name] = list(registry_by_plugin)
- registry['by_entity_type'][entity_type_name] = list(registry_by_entity_type)
- redis.mset({_entity_registry_varname: json.dumps(registry)})
-
-
-def get_plugin_entity_registry() -> Dict[str, Dict[str, Collection[str]]]:
- """
- Get the `plugin->entity_types` and `entity_type->plugin`
- mappings supported by the current configuration.
- """
- redis = get_redis()
- registry = redis.mget([_entity_registry_varname])[0]
- try:
- registry = json.loads((registry or b'').decode())
- except (TypeError, ValueError):
- return {'by_plugin': {}, 'by_entity_type': {}}
-
- enabled_plugins = set(Config.get_plugins().keys())
-
- return {
- 'by_plugin': {
- plugin_name: entity_types
- for plugin_name, entity_types in registry['by_plugin'].items()
- if plugin_name in enabled_plugins
- },
- 'by_entity_type': {
- entity_type: [p for p in plugins if p in enabled_plugins]
- for entity_type, plugins in registry['by_entity_type'].items()
- },
- }
-
-
-class EntityManagerMixin:
- """
- This mixin is injected on the fly into any plugin class declared with
- the @manages decorator. The class will therefore implement the
- `publish_entities` and `transform_entities` methods, which can be
- overridden if required.
- """
-
- def transform_entities(self, entities):
- """
- This method takes a list of entities in any (plugin-specific)
- format and converts them into a standardized collection of
- `Entity` objects. Since this method is called by
- :meth:`.publish_entities` before entity updates are published,
- you may usually want to extend it to pre-process the entities
- managed by your extension into the standard format before they
- are stored and published to all the consumers.
- """
- entities = entities or []
- for entity in entities:
- if entity.id:
- # Entity IDs can only refer to the internal primary key
- entity.external_id = entity.id
- entity.id = None # type: ignore
-
- entity.plugin = get_plugin_name_by_class(self.__class__) # type: ignore
- entity.updated_at = datetime.utcnow()
-
- return entities
-
- def publish_entities(self, entities: Optional[Collection[Entity]]):
- """
- Publishes a list of entities. The downstream consumers include:
-
- - The entity persistence manager
- - The web server
- - Any consumer subscribed to
- :class:`platypush.message.event.entities.EntityUpdateEvent`
- events (e.g. web clients)
-
- If your extension class uses the `@manages` decorator then you usually
- don't need to override this class (but you may want to extend
- :meth:`.transform_entities` instead if your extension doesn't natively
- handle `Entity` objects).
- """
- from . import publish_entities
-
- transformed_entities = self.transform_entities(entities)
- publish_entities(transformed_entities)
-
-
-def manages(*entities: Type[Entity]):
- """
- This decorator is used to register a plugin/backend class as a
- manager of one or more types of entities.
- """
-
- def wrapper(plugin: Type[Plugin]):
- init = plugin.__init__
-
- def __init__(self, *args, **kwargs):
- for entity_type in entities:
- register_entity_plugin(entity_type, self)
-
- init(self, *args, **kwargs)
-
- plugin.__init__ = __init__ # type: ignore
- # Inject the EntityManagerMixin
- if EntityManagerMixin not in plugin.__bases__:
- plugin.__bases__ = (EntityManagerMixin,) + plugin.__bases__
-
- return plugin
-
- return wrapper
diff --git a/platypush/plugins/light/__init__.py b/platypush/plugins/light/__init__.py
index b683c884ad..e69de29bb2 100644
--- a/platypush/plugins/light/__init__.py
+++ b/platypush/plugins/light/__init__.py
@@ -1,53 +0,0 @@
-from abc import ABC, abstractmethod
-
-from platypush.entities import manages
-from platypush.entities.lights import Light
-from platypush.plugins import Plugin, action
-
-
-@manages(Light)
-class LightPlugin(Plugin, ABC):
- """
- Abstract plugin to interface your logic with lights/bulbs.
- """
-
- @action
- @abstractmethod
- def on(self, lights=None, *args, **kwargs):
- """Turn the light on"""
- raise NotImplementedError()
-
- @action
- @abstractmethod
- def off(self, lights=None, *args, **kwargs):
- """Turn the light off"""
- raise NotImplementedError()
-
- @action
- @abstractmethod
- def toggle(self, lights=None, *args, **kwargs):
- """Toggle the light status (on/off)"""
- raise NotImplementedError()
-
- @action
- @abstractmethod
- def set_lights(self, lights=None, *args, **kwargs):
- """
- Set a set of properties on a set of lights.
-
- :param light: List of lights to set. Each item can represent a light
- name or ID.
- :param kwargs: key-value list of the parameters to set.
- """
- raise NotImplementedError()
-
- @action
- @abstractmethod
- def status(self, *args, **kwargs):
- """
- Get the current status of the lights.
- """
- raise NotImplementedError()
-
-
-# vim:sw=4:ts=4:et:
diff --git a/platypush/plugins/light/hue/__init__.py b/platypush/plugins/light/hue/__init__.py
index 00501c2ffe..37370ca878 100644
--- a/platypush/plugins/light/hue/__init__.py
+++ b/platypush/plugins/light/hue/__init__.py
@@ -4,22 +4,29 @@ import time
from enum import Enum
from threading import Thread, Event
-from typing import Iterable, Union, Mapping, Any, Set
+from typing import (
+ Any,
+ Collection,
+ Dict,
+ Iterable,
+ Mapping,
+ Set,
+ Union,
+)
from platypush.context import get_bus
-from platypush.entities import Entity
+from platypush.entities import Entity, LightEntityManager
from platypush.entities.lights import Light as LightEntity
from platypush.message.event.light import (
LightAnimationStartedEvent,
LightAnimationStoppedEvent,
LightStatusChangeEvent,
)
-from platypush.plugins import action, RunnablePlugin
-from platypush.plugins.light import LightPlugin
+from platypush.plugins import RunnablePlugin, action
from platypush.utils import set_thread_name
-class LightHuePlugin(RunnablePlugin, LightPlugin):
+class LightHuePlugin(RunnablePlugin, LightEntityManager):
"""
Philips Hue lights plugin.
@@ -47,14 +54,22 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
_UNINITIALIZED_BRIDGE_ERR = 'The Hue bridge is not initialized'
class Animation(Enum):
+ """
+ Inner class to model light animations.
+ """
+
COLOR_TRANSITION = 'color_transition'
BLINK = 'blink'
def __eq__(self, other):
+ """
+ Check if the configuration of two light animations matches.
+ """
if isinstance(other, str):
return self.value == other
- elif isinstance(other, self.__class__):
+ if isinstance(other, self.__class__):
return self == other
+ return False
def __init__(self, bridge, lights=None, groups=None, poll_seconds: float = 20.0):
"""
@@ -76,14 +91,14 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
self.bridge_address = bridge
self.bridge = None
self.logger.info(
- 'Initializing Hue lights plugin - bridge: "{}"'.format(self.bridge_address)
+ 'Initializing Hue lights plugin - bridge: "%s"', self.bridge_address
)
self.connect()
self.lights = set()
self.groups = set()
self.poll_seconds = poll_seconds
- self._cached_lights = {}
+ self._cached_lights: Dict[str, dict] = {}
if lights:
self.lights = set(lights)
@@ -94,10 +109,10 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
self.lights = {light['name'] for light in self._get_lights().values()}
self.animation_thread = None
- self.animations = {}
+ self.animations: Dict[str, dict] = {}
self._animation_stop = Event()
self._init_animations()
- self.logger.info(f'Configured lights: {self.lights}')
+ self.logger.info('Configured lights: %s', self.lights)
def _expand_groups(self, groups: Iterable[str]) -> Set[str]:
lights = set()
@@ -147,7 +162,7 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
self.bridge = Bridge(self.bridge_address)
success = True
except PhueRegistrationException as e:
- self.logger.warning('Bridge registration error: {}'.format(str(e)))
+ self.logger.warning('Bridge registration error: %s', e)
if n_tries >= self._MAX_RECONNECT_TRIES:
self.logger.error(
@@ -392,7 +407,7 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
return self._get_lights(publish_entities=True)
@action
- def set_lights(self, lights, **kwargs):
+ def set_lights(self, lights, *_, **kwargs): # pylint: disable=arguments-differ
"""
Set a set of properties on a set of lights.
@@ -404,7 +419,7 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
{
"type": "request",
- "action": "light.hue.set_light",
+ "action": "light.hue.set_lights",
"args": {
"lights": ["Bulb 1", "Bulb 2"],
"sat": 255
@@ -434,7 +449,10 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
for arg, value in kwargs.items():
args += [arg, value]
- self.bridge.set_light(lights, *args)
+ assert len(args) > 1, 'Not enough parameters passed to set_lights'
+ param = args.pop(0)
+ value = args.pop(0)
+ self.bridge.set_light(lights, param, value, *args)
return self._get_lights(publish_entities=True)
@action
@@ -442,8 +460,9 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
"""
Set a group (or groups) property.
- :param group: Group or groups to set. It can be a string representing the
- group name, a group object, a list of strings, or a list of group objects.
+ :param group: Group or groups to set. Can be a string representing the
+ group name, a group object, a list of strings, or a list of group
+ objects.
:param kwargs: key-value list of parameters to set.
Example call::
@@ -464,7 +483,9 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
self.bridge.set_group(group, **kwargs)
@action
- def on(self, lights=None, groups=None, **kwargs):
+ def on( # pylint: disable=arguments-differ
+ self, lights=None, groups=None, **kwargs
+ ):
"""
Turn lights/groups on.
@@ -479,7 +500,9 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
return self._exec('on', True, lights=lights, groups=groups, **kwargs)
@action
- def off(self, lights=None, groups=None, **kwargs):
+ def off( # pylint: disable=arguments-differ
+ self, lights=None, groups=None, **kwargs
+ ):
"""
Turn lights/groups off.
@@ -494,7 +517,9 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
return self._exec('on', False, lights=lights, groups=groups, **kwargs)
@action
- def toggle(self, lights=None, groups=None, **kwargs):
+ def toggle( # pylint: disable=arguments-differ
+ self, lights=None, groups=None, **kwargs
+ ):
"""
Toggle lights/groups on/off.
@@ -857,34 +882,42 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
:type duration: float
:param hue_range: If you selected a ``color_transition``, this will
- specify the hue range of your color ``color_transition``. Default: [0, 65535]
+ specify the hue range of your color ``color_transition``.
+ Default: [0, 65535]
:type hue_range: list[int]
:param sat_range: If you selected a color ``color_transition``, this
- will specify the saturation range of your color ``color_transition``.
- Default: [0, 255]
+ will specify the saturation range of your color
+ ``color_transition``. Default: [0, 255]
:type sat_range: list[int]
:param bri_range: If you selected a color ``color_transition``, this
- will specify the brightness range of your color ``color_transition``.
- Default: [254, 255] :type bri_range: list[int]
+ will specify the brightness range of your color
+ ``color_transition``. Default: [254, 255]
+ :type bri_range: list[int]
- :param lights: Lights to control (names, IDs or light objects). Default: plugin default lights
- :param groups: Groups to control (names, IDs or group objects). Default: plugin default groups
+ :param lights: Lights to control (names, IDs or light objects).
+ Default: plugin default lights
+ :param groups: Groups to control (names, IDs or group objects).
+ Default: plugin default groups
:param hue_step: If you selected a color ``color_transition``, this
- will specify by how much the color hue will change between iterations.
- Default: 1000 :type hue_step: int
+ will specify by how much the color hue will change between
+ iterations. Default: 1000
+ :type hue_step: int
:param sat_step: If you selected a color ``color_transition``, this
- will specify by how much the saturation will change between iterations.
- Default: 2 :type sat_step: int
+ will specify by how much the saturation will change
+ between iterations. Default: 2
+ :type sat_step: int
:param bri_step: If you selected a color ``color_transition``, this
will specify by how much the brightness will change between iterations.
- Default: 1 :type bri_step: int
+ Default: 1
+ :type bri_step: int
- :param transition_seconds: Time between two transitions or blinks in seconds. Default: 1.0
+ :param transition_seconds: Time between two transitions or blinks in
+ seconds. Default: 1.0
:type transition_seconds: float
"""
@@ -1028,11 +1061,11 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
try:
if animation == self.Animation.COLOR_TRANSITION:
for (light, attrs) in lights.items():
- self.logger.debug('Setting {} to {}'.format(light, attrs))
+ self.logger.debug('Setting %s to %s', lights, attrs)
self.bridge.set_light(light, attrs)
elif animation == self.Animation.BLINK:
conf = lights[list(lights.keys())[0]]
- self.logger.debug('Setting lights to {}'.format(conf))
+ self.logger.debug('Setting lights to %s', conf)
if groups:
self.bridge.set_group(
@@ -1074,7 +1107,7 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
def transform_entities(
self, entities: Union[Iterable[Union[dict, Entity]], Mapping[Any, dict]]
- ) -> Iterable[Entity]:
+ ) -> Collection[Entity]:
new_entities = []
if isinstance(entities, dict):
entities = [{'id': id, **e} for id, e in entities.items()]
@@ -1108,10 +1141,7 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
'hue_max': self.MAX_HUE,
}
if entity.get('state', {}).get('hue') is not None
- else {
- 'hue_min': None,
- 'hue_max': None,
- }
+ else {}
),
**(
{
@@ -1119,10 +1149,7 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
'saturation_max': self.MAX_SAT,
}
if entity.get('state', {}).get('sat') is not None
- else {
- 'saturation_min': None,
- 'saturation_max': None,
- }
+ else {}
),
**(
{
@@ -1130,10 +1157,7 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
'brightness_max': self.MAX_BRI,
}
if entity.get('state', {}).get('bri') is not None
- else {
- 'brightness_min': None,
- 'brightness_max': None,
- }
+ else {}
),
**(
{
@@ -1141,15 +1165,12 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
'temperature_max': self.MAX_CT,
}
if entity.get('state', {}).get('ct') is not None
- else {
- 'temperature_min': None,
- 'temperature_max': None,
- }
+ else {}
),
)
)
- return super().transform_entities(new_entities) # type: ignore
+ return new_entities
def _get_lights(self, publish_entities=False) -> dict:
assert self.bridge, self._UNINITIALIZED_BRIDGE_ERR
@@ -1171,7 +1192,7 @@ class LightHuePlugin(RunnablePlugin, LightPlugin):
return {id: scene for id, scene in scenes.items() if not scene.get('recycle')}
@action
- def status(self) -> Iterable[LightEntity]:
+ def status(self, *_, **__) -> Iterable[LightEntity]:
lights = self.transform_entities(self._get_lights(publish_entities=True))
for light in lights:
light.id = light.external_id
diff --git a/platypush/plugins/mqtt/__init__.py b/platypush/plugins/mqtt/__init__.py
index 36b52d7719..3ec1eaa488 100644
--- a/platypush/plugins/mqtt/__init__.py
+++ b/platypush/plugins/mqtt/__init__.py
@@ -259,7 +259,7 @@ class MqttPlugin(Plugin):
try:
msg = Message.build(json.loads(msg))
except Exception as e:
- self.logger.debug(f'Not a valid JSON: {str(e)}')
+ self.logger.debug('Not a valid JSON: %s', e)
host = host or self.host
port = port or self.port or 1883
@@ -303,7 +303,7 @@ class MqttPlugin(Plugin):
try:
client.loop_stop()
except Exception as e:
- self.logger.warning(f'Could not stop client loop: {e}')
+ self.logger.warning('Could not stop client loop: %s', e)
client.disconnect()
diff --git a/platypush/plugins/smartthings/__init__.py b/platypush/plugins/smartthings/__init__.py
index 33aaeba07d..f3eb4b31f3 100644
--- a/platypush/plugins/smartthings/__init__.py
+++ b/platypush/plugins/smartthings/__init__.py
@@ -1,6 +1,6 @@
import asyncio
from threading import RLock
-from typing import Dict, Iterable, List, Optional, Tuple, Type, Union
+from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union
import aiohttp
from pysmartthings import (
@@ -9,10 +9,19 @@ from pysmartthings import (
Command,
DeviceEntity,
DeviceStatus,
+ Location,
+ Room,
SmartThings,
)
-from platypush.entities import Entity, manages
+from platypush.entities import (
+ DimmerEntityManager,
+ Entity,
+ EnumSwitchEntityManager,
+ LightEntityManager,
+ SensorEntityManager,
+ SwitchEntityManager,
+)
from platypush.entities.devices import Device
from platypush.entities.dimmers import Dimmer
from platypush.entities.lights import Light
@@ -24,8 +33,14 @@ from platypush.utils import camel_case_to_snake_case
from ._mappers import DeviceMapper, device_mappers
-@manages(Device, Dimmer, EnumSwitch, Light, Sensor, Switch)
-class SmartthingsPlugin(RunnablePlugin):
+class SmartthingsPlugin(
+ RunnablePlugin,
+ DimmerEntityManager,
+ EnumSwitchEntityManager,
+ LightEntityManager,
+ SensorEntityManager,
+ SwitchEntityManager,
+):
"""
Plugin to interact with devices and locations registered to a Samsung SmartThings account.
@@ -49,17 +64,17 @@ class SmartthingsPlugin(RunnablePlugin):
self._refresh_lock = RLock()
self._execute_lock = RLock()
- self._locations = []
- self._devices = []
- self._rooms_by_location = {}
+ self._locations: List[Location] = []
+ self._devices: List[DeviceEntity] = []
+ self._rooms_by_location: Dict[str, Room] = {}
- self._locations_by_id = {}
- self._locations_by_name = {}
- self._devices_by_id = {}
- self._devices_by_name = {}
- self._rooms_by_id = {}
- self._rooms_by_location_and_id = {}
- self._rooms_by_location_and_name = {}
+ self._locations_by_id: Dict[str, Location] = {}
+ self._locations_by_name: Dict[str, Location] = {}
+ self._devices_by_id: Dict[str, DeviceEntity] = {}
+ self._devices_by_name: Dict[str, DeviceEntity] = {}
+ self._rooms_by_id: Dict[str, Room] = {}
+ self._rooms_by_location_and_id: Dict[str, Dict[str, Room]] = {}
+ self._rooms_by_location_and_name: Dict[str, Dict[str, Room]] = {}
self._entities_by_id: Dict[str, Entity] = {}
async def _refresh_locations(self, api):
@@ -308,10 +323,13 @@ class SmartthingsPlugin(RunnablePlugin):
):
self.refresh_info()
- location = self._locations_by_id.get(
- location_id, self._locations_by_name.get(name)
- )
- assert location, 'Location {} not found'.format(location_id or name)
+ location: Optional[Dict[str, Any]] = {}
+ if location_id:
+ location = self._locations_by_id.get(location_id)
+ elif name:
+ location = self._locations_by_name.get(name)
+
+ assert location, f'Location {location_id or name} not found'
return self._location_to_dict(location)
def _get_device(self, device: str) -> DeviceEntity:
@@ -321,7 +339,7 @@ class SmartthingsPlugin(RunnablePlugin):
def _to_device_and_property(device: str) -> Tuple[str, Optional[str]]:
tokens = device.split(':')
if len(tokens) > 1:
- return tuple(tokens[:2])
+ return (tokens[0], tokens[1])
return tokens[0], None
def _get_existing_and_missing_devices(
@@ -397,9 +415,7 @@ class SmartthingsPlugin(RunnablePlugin):
assert (
ret
- ), 'The command {capability}={command} failed on device {device}'.format(
- capability=capability, command=command, device=device_id
- )
+ ), f'The command {capability}={command} failed on device {device_id}'
await self._get_device_status(api, device_id, publish_entities=True)
@@ -455,7 +471,9 @@ class SmartthingsPlugin(RunnablePlugin):
loop.stop()
@staticmethod
- def _property_to_entity_name(property: str) -> str:
+ def _property_to_entity_name(
+ property: str,
+ ) -> str: # pylint: disable=redefined-builtin
return ' '.join(
[
t[:1].upper() + t[1:]
@@ -464,7 +482,7 @@ class SmartthingsPlugin(RunnablePlugin):
)
@classmethod
- def _to_entity(
+ def _to_entity( # pylint: disable=redefined-builtin
cls, device: DeviceEntity, property: str, entity_type: Type[Entity], **kwargs
) -> Entity:
return entity_type(
@@ -669,7 +687,7 @@ class SmartthingsPlugin(RunnablePlugin):
# Fail if some devices haven't been found after refreshing
assert (
not missing_device_ids
- ), 'Could not find the following devices: {}'.format(list(missing_device_ids))
+ ), f'Could not find the following devices: {list(missing_device_ids)}'
async with aiohttp.ClientSession(timeout=self._timeout) as session:
api = SmartThings(session, self._access_token)
@@ -682,12 +700,11 @@ class SmartthingsPlugin(RunnablePlugin):
for device_id in device_ids
]
- # noinspection PyTypeChecker
return await asyncio.gather(*status_tasks)
@action
- def status(
- self, device: Optional[Union[str, List[str]]] = None, publish_entities=True
+ def status( # pylint: disable=arguments-differ
+ self, device: Optional[Union[str, List[str]]] = None, publish_entities=True, **_
) -> List[dict]:
"""
Refresh and return the status of one or more devices.
@@ -715,7 +732,7 @@ class SmartthingsPlugin(RunnablePlugin):
if not device:
self.refresh_info()
- devices = self._devices_by_id.keys()
+ devices = list(self._devices_by_id.keys())
elif isinstance(device, str):
devices = [device]
else:
@@ -734,7 +751,13 @@ class SmartthingsPlugin(RunnablePlugin):
loop.stop()
def _set_switch(self, device: str, value: Optional[bool] = None):
- device, property = self._to_device_and_property(device)
+ (
+ device,
+ property,
+ ) = self._to_device_and_property( # pylint: disable=redefined-builtin
+ device
+ )
+
if not property:
property = Attribute.switch
@@ -744,6 +767,7 @@ class SmartthingsPlugin(RunnablePlugin):
if property == 'light':
property = 'switch'
else:
+ assert property, 'No property specified'
assert hasattr(
dev.status, property
), f'No such property on device "{dev.label}": "{property}"'
@@ -760,7 +784,7 @@ class SmartthingsPlugin(RunnablePlugin):
return self.set_value(device, property, value)
@action
- def on(self, device: str, *_, **__):
+ def on(self, device: str, *_, **__): # pylint: disable=arguments-differ
"""
Turn on a device with ``switch`` capability.
@@ -769,7 +793,7 @@ class SmartthingsPlugin(RunnablePlugin):
return self._set_switch(device, True)
@action
- def off(self, device: str, *_, **__):
+ def off(self, device: str, *_, **__): # pylint: disable=arguments-differ
"""
Turn off a device with ``switch`` capability.
@@ -778,7 +802,7 @@ class SmartthingsPlugin(RunnablePlugin):
return self._set_switch(device, False)
@action
- def toggle(self, device: str, *_, **__):
+ def toggle(self, device: str, *_, **__): # pylint: disable=arguments-differ
"""
Toggle a device with ``switch`` capability.
@@ -799,7 +823,7 @@ class SmartthingsPlugin(RunnablePlugin):
"""
return self.set_value(device, Capability.switch_level, level, **kwargs)
- def _set_value(
+ def _set_value( # pylint: disable=redefined-builtin
self, device: str, property: Optional[str] = None, data=None, **kwargs
):
if not property:
@@ -830,14 +854,14 @@ class SmartthingsPlugin(RunnablePlugin):
device,
mapper.capability,
command,
- args=mapper.set_value_args(data),
+ args=mapper.set_value_args(data), # type: ignore
**kwargs,
)
return self.status(device)
@action
- def set_value(
+ def set_value( # pylint: disable=arguments-differ,redefined-builtin
self, device: str, property: Optional[str] = None, data=None, **kwargs
):
"""
@@ -854,10 +878,10 @@ class SmartthingsPlugin(RunnablePlugin):
return self._set_value(device, property, data, **kwargs)
except Exception as e:
self.logger.exception(e)
- raise AssertionError(e)
+ raise AssertionError(e) from e
@action
- def set_lights(
+ def set_lights( # pylint: disable=arguments-differ,redefined-builtin
self,
lights: Iterable[str],
on: Optional[bool] = None,
@@ -961,7 +985,7 @@ class SmartthingsPlugin(RunnablePlugin):
return self.status(publish_entities=False)
except Exception as e:
self.logger.exception(e)
- self.logger.error(f'Could not refresh the status: {e}')
+ self.logger.error('Could not refresh the status: %s', e)
self.wait_stop(3 * (self.poll_interval or 5))
while not self.should_stop():
diff --git a/platypush/plugins/zigbee/mqtt/__init__.py b/platypush/plugins/zigbee/mqtt/__init__.py
index 0501bbc23c..f8cca0ec34 100644
--- a/platypush/plugins/zigbee/mqtt/__init__.py
+++ b/platypush/plugins/zigbee/mqtt/__init__.py
@@ -5,6 +5,7 @@ import threading
from queue import Queue
from typing import (
Any,
+ Collection,
Dict,
List,
Optional,
@@ -13,7 +14,14 @@ from typing import (
Union,
)
-from platypush.entities import Entity, manages
+from platypush.entities import (
+ DimmerEntityManager,
+ Entity,
+ EnumSwitchEntityManager,
+ LightEntityManager,
+ SensorEntityManager,
+ SwitchEntityManager,
+)
from platypush.entities.batteries import Battery
from platypush.entities.devices import Device
from platypush.entities.dimmers import Dimmer
@@ -40,8 +48,15 @@ from platypush.plugins import RunnablePlugin
from platypush.plugins.mqtt import MqttPlugin, action
-@manages(Battery, Device, Dimmer, Light, LinkQuality, Sensor, Switch)
-class ZigbeeMqttPlugin(RunnablePlugin, MqttPlugin): # lgtm [py/missing-call-to-init]
+class ZigbeeMqttPlugin(
+ RunnablePlugin,
+ MqttPlugin,
+ DimmerEntityManager,
+ EnumSwitchEntityManager,
+ 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