Added support for more entities in switchbot

This commit is contained in:
Fabio Manganiello 2023-02-05 15:34:50 +01:00
parent 64e9bf17cf
commit 06dfd1a152
Signed by: blacklight
GPG key ID: D90FBA7F76362774
5 changed files with 693 additions and 76 deletions

View file

@ -35,7 +35,7 @@ class MultiLevelSwitchEntityManager(EntityManager, ABC):
@abstractmethod @abstractmethod
def set_value( # pylint: disable=redefined-builtin def set_value( # pylint: disable=redefined-builtin
self, *entities, property=None, value=None, **__ self, *entities, property=None, data=None, **__
): ):
"""Set a value""" """Set a value"""
raise NotImplementedError() raise NotImplementedError()

View file

@ -1,6 +1,6 @@
import queue import queue
import threading import threading
from typing import Any, Collection, Dict, List, Optional, Union from typing import Any, Collection, Dict, List, Optional, Tuple, Union
import requests import requests
@ -8,24 +8,32 @@ from platypush.entities import (
DimmerEntityManager, DimmerEntityManager,
EnumSwitchEntityManager, EnumSwitchEntityManager,
Entity, Entity,
LightEntityManager,
SwitchEntityManager, SwitchEntityManager,
) )
from platypush.entities.devices import Device from platypush.entities.devices import Device
from platypush.entities.dimmers import Dimmer from platypush.entities.dimmers import Dimmer
from platypush.entities.electricity import CurrentSensor, PowerSensor, VoltageSensor
from platypush.entities.lights import Light
from platypush.entities.humidity import HumiditySensor from platypush.entities.humidity import HumiditySensor
from platypush.entities.motion import MotionSensor from platypush.entities.motion import MotionSensor
from platypush.entities.sensors import BinarySensor, EnumSensor, Sensor from platypush.entities.sensors import BinarySensor, EnumSensor, NumericSensor
from platypush.entities.switches import EnumSwitch from platypush.entities.switches import EnumSwitch, Switch
from platypush.entities.temperature import TemperatureSensor from platypush.entities.temperature import TemperatureSensor
from platypush.plugins import RunnablePlugin, action from platypush.plugins import RunnablePlugin, action
from platypush.schemas.switchbot import DeviceSchema, DeviceStatusSchema, SceneSchema from platypush.schemas.switchbot import DeviceSchema, DeviceStatusSchema, SceneSchema
from ._constants import DeviceType
from ._setters import entity_setters
# pylint: disable=too-many-ancestors
class SwitchbotPlugin( class SwitchbotPlugin(
RunnablePlugin, RunnablePlugin,
SwitchEntityManager,
DimmerEntityManager, DimmerEntityManager,
EnumSwitchEntityManager, EnumSwitchEntityManager,
LightEntityManager,
SwitchEntityManager,
): ):
""" """
Plugin to interact with the devices registered to a Switchbot Plugin to interact with the devices registered to a Switchbot
@ -88,15 +96,22 @@ class SwitchbotPlugin(
return response.get('body') return response.get('body')
def _get_device(self, device: str, use_cache=True): @staticmethod
def _split_device_id_and_property(device: str) -> Tuple[str, Optional[str]]:
tokens = device.split(':')[:2]
return tokens[0], (tokens[1] if len(tokens) == 2 else None)
def _get_device(self, device: str, use_cache=True) -> dict:
if not use_cache: if not use_cache:
self.devices() self.devices()
if device in self._devices_by_id:
return self._devices_by_id[device]
if device in self._devices_by_name: if device in self._devices_by_name:
return self._devices_by_name[device] return self._devices_by_name[device]
device, _ = self._split_device_id_and_property(device)
if device in self._devices_by_id:
return self._devices_by_id[device]
assert use_cache, f'Device not found: {device}' assert use_cache, f'Device not found: {device}'
return self._get_device(device, use_cache=False) return self._get_device(device, use_cache=False)
@ -152,6 +167,12 @@ class SwitchbotPlugin(
**args, **args,
) )
@staticmethod
def _matches_device_types(device: dict, *device_types: DeviceType) -> bool:
return device.get('device_type') in {
device_type.value for device_type in device_types
}
@classmethod @classmethod
def _get_bots(cls, *entities: dict) -> List[EnumSwitch]: def _get_bots(cls, *entities: dict) -> List[EnumSwitch]:
return [ return [
@ -164,7 +185,29 @@ class SwitchbotPlugin(
data=cls._get_device_metadata(dev), data=cls._get_device_metadata(dev),
) )
for dev in (entities or []) for dev in (entities or [])
if dev.get('device_type') == 'Bot' if cls._matches_device_types(dev, DeviceType.BOT)
]
@classmethod
def _get_lights(cls, *entities: dict) -> List[Light]:
return [
Light(
id=dev["id"],
name=dev["name"],
on="on" if dev.get("on") else "off",
brightness=dev.get("brightness"),
color_temperature=dev.get("color_temperature"),
color=dev.get("color"),
data=cls._get_device_metadata(dev),
)
for dev in (entities or [])
if cls._matches_device_types(
dev,
DeviceType.CEILING_LIGHT,
DeviceType.CEILING_LIGHT_PRO,
DeviceType.COLOR_BULB,
DeviceType.STRIP_LIGHT,
)
] ]
@classmethod @classmethod
@ -180,7 +223,7 @@ class SwitchbotPlugin(
data=cls._get_device_metadata(dev), data=cls._get_device_metadata(dev),
) )
for dev in (entities or []) for dev in (entities or [])
if dev.get('device_type') == 'Curtain' if cls._matches_device_types(dev, DeviceType.CURTAIN)
] ]
@classmethod @classmethod
@ -256,22 +299,207 @@ class SwitchbotPlugin(
return devices return devices
@classmethod @classmethod
def _get_sensors(cls, *entities: dict) -> List[Sensor]: def _get_sensors(cls, *entities: dict) -> List[Device]:
sensors: List[Sensor] = [] sensors: List[Entity] = []
for dev in entities: for dev in entities:
if dev.get('device_type') in {'Meter', 'Meter Plus'}: if cls._matches_device_types(dev, DeviceType.METER, DeviceType.METER_PLUS):
sensors.extend(cls._get_meters(dev)) sensors.extend(cls._get_meters(dev))
elif dev.get('device_type') == 'Motion Sensor': elif cls._matches_device_types(dev, DeviceType.MOTION_SENSOR):
sensors.extend(cls._get_motion_sensors(dev)) sensors.extend(cls._get_motion_sensors(dev))
elif dev.get('device_type') == 'Contact Sensor': elif cls._matches_device_types(dev, DeviceType.CONTACT_SENSOR):
sensors.extend(cls._get_contact_sensors(dev)) sensors.extend(cls._get_contact_sensors(dev))
return sensors return sensors
@classmethod
def _get_humidifiers(cls, *entities: dict) -> List[Device]:
humidifiers = [
dev
for dev in entities
if cls._matches_device_types(dev, DeviceType.HUMIDIFIER)
]
devs = [Device(**cls._get_device_base(dev)) for dev in humidifiers]
for dev_dict, entity in zip(humidifiers, devs):
if dev_dict.get('power') is not None:
entity.children.append(
Switch(
id=f'{dev_dict["id"]}:state',
name='State',
state=cls._is_on(dev_dict['power']),
)
)
if dev_dict.get('auto') is not None:
entity.children.append(
Switch(
id=f'{dev_dict["id"]}:auto',
name='Automatic Mode',
state=cls._is_on(dev_dict['auto']),
)
)
if dev_dict.get('child_lock') is not None:
entity.children.append(
Switch(
id=f'{dev_dict["id"]}:child_lock',
name='Child Lock',
state=cls._is_on(dev_dict['child_lock']),
)
)
if dev_dict.get('nebulization_efficiency') is not None:
entity.children.append(
Dimmer(
id=f'{dev_dict["id"]}:nebulization_efficiency',
name='Nebulization Efficiency',
value=cls._is_on(dev_dict['nebulization_efficiency']),
min=0,
max=100,
)
)
if dev_dict.get('low_water') is not None:
entity.children.append(
BinarySensor(
id=f'{dev_dict["id"]}:low_water',
name='Low Water',
value=cls._is_on(dev_dict['low_water']),
)
)
if dev_dict.get('temperature') is not None:
entity.children.append(
TemperatureSensor(
id=f'{dev_dict["id"]}:temperature',
name='temperature',
value=dev_dict['temperature'],
)
)
if dev_dict.get('humidity') is not None:
entity.children.append(
HumiditySensor(
id=f'{dev_dict["id"]}:humidity',
name='humidity',
value=dev_dict['humidity'],
)
)
return devs
@classmethod
def _get_locks(cls, *entities: dict) -> List[Device]:
locks = [
dev
for dev in (entities or [])
if cls._matches_device_types(dev, DeviceType.LOCK)
]
devices = [Device(**cls._get_device_base(plug)) for plug in locks]
for plug, device in zip(locks, devices):
if plug.get('locked') is not None:
device.children.append(
Switch(
id=f'{plug["id"]}:locked',
name='Locked',
state=cls._is_on(plug['locked']),
)
)
if plug.get('door_open') is not None:
device.children.append(
BinarySensor(
id=f'{plug["id"]}:door_open',
name='Door Open',
value=cls._is_on(plug['door_open']),
)
)
return devices
@classmethod
def _get_plugs(cls, *entities: dict) -> List[Device]:
plugs = [
dev
for dev in (entities or [])
if cls._matches_device_types(
dev, DeviceType.PLUG, DeviceType.PLUG_MINI_JP, DeviceType.PLUG_MINI_US
)
]
devices = [Device(**cls._get_device_base(plug)) for plug in plugs]
for plug, device in zip(plugs, devices):
if plug.get('on') is not None:
device.children.append(
Switch(
id=f'{plug["id"]}:state',
name='State',
state=cls._is_on(plug['on']),
)
)
if plug.get('power') is not None:
device.children.append(
PowerSensor(
id=f'{plug["id"]}:power',
name='Power',
value=plug['power'],
unit='W',
)
)
if plug.get('voltage') is not None:
device.children.append(
VoltageSensor(
id=f'{plug["id"]}:voltage',
name='Voltage',
value=plug['voltage'],
unit='V',
)
)
if plug.get('current') is not None:
device.children.append(
CurrentSensor(
id=f'{plug["id"]}:current',
name='Current',
value=plug['current'],
unit='A',
)
)
if plug.get('active_time') is not None:
device.children.append(
NumericSensor(
id=f'{plug["id"]}:active_time',
name='Active Time',
value=plug['active_time'],
unit='min',
)
)
return devices
@staticmethod
def _is_on(state: Union[bool, str, int]) -> bool:
if isinstance(state, str):
state = state.lower()
else:
state = bool(state)
return state in {'on', 'true', '1', True}
def transform_entities(self, entities: Collection[dict]) -> Collection[Entity]: def transform_entities(self, entities: Collection[dict]) -> Collection[Entity]:
return [ return [
*self._get_bots(*entities), *self._get_bots(*entities),
*self._get_curtains(*entities), *self._get_curtains(*entities),
*self._get_humidifiers(*entities),
*self._get_lights(*entities),
*self._get_locks(*entities),
*self._get_plugs(*entities),
*self._get_sensors(*entities), *self._get_sensors(*entities),
] ]
@ -367,8 +595,8 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run('post', 'commands', device=device, json={'command': 'press'}) return self._run('post', 'commands', device=dev, json={'command': 'press'})
@action @action
def toggle(self, device: str, **_): # pylint: disable=arguments-differ def toggle(self, device: str, **_): # pylint: disable=arguments-differ
@ -386,8 +614,8 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run('post', 'commands', device=device, json={'command': 'turnOn'}) return self._run('post', 'commands', device=dev, json={'command': 'turnOn'})
@action @action
def off(self, device: str, **_): # pylint: disable=arguments-differ def off(self, device: str, **_): # pylint: disable=arguments-differ
@ -396,8 +624,28 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run('post', 'commands', device=device, json={'command': 'turnOff'}) return self._run('post', 'commands', device=dev, json={'command': 'turnOff'})
@action
def lock(self, device: str, **_):
"""
Lock a compatible lock device.
:param device: Device name or ID.
"""
dev = self._get_device(device)
return self._run('post', 'commands', device=dev, json={'command': 'lock'})
@action
def unlock(self, device: str, **_):
"""
Unlock a compatible lock device.
:param device: Device name or ID.
"""
dev = self._get_device(device)
return self._run('post', 'commands', device=dev, json={'command': 'unlock'})
@action @action
def set_curtain_position(self, device: str, position: int): def set_curtain_position(self, device: str, position: int):
@ -407,11 +655,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
:param position: An integer between 0 (open) and 100 (closed). :param position: An integer between 0 (open) and 100 (closed).
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'setPosition', 'command': 'setPosition',
'commandType': 'command', 'commandType': 'command',
@ -425,13 +673,17 @@ class SwitchbotPlugin(
Set the nebulization efficiency of a humidifier device. Set the nebulization efficiency of a humidifier device.
:param device: Device name or ID. :param device: Device name or ID.
:param efficiency: An integer between 0 and 100, or `auto`. :param efficiency: Possible values:
- ``auto``: Automatic mode.
- A value between ``0`` and ``100``.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'setMode', 'command': 'setMode',
'commandType': 'command', 'commandType': 'command',
@ -598,11 +850,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
:param channel: Channel number. :param channel: Channel number.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'SetChannel', 'command': 'SetChannel',
'commandType': 'command', 'commandType': 'command',
@ -617,11 +869,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'volumeAdd', 'command': 'volumeAdd',
'commandType': 'command', 'commandType': 'command',
@ -635,11 +887,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'volumeSub', 'command': 'volumeSub',
'commandType': 'command', 'commandType': 'command',
@ -653,11 +905,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'setMute', 'command': 'setMute',
'commandType': 'command', 'commandType': 'command',
@ -671,11 +923,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'channelAdd', 'command': 'channelAdd',
'commandType': 'command', 'commandType': 'command',
@ -689,11 +941,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'channelSub', 'command': 'channelSub',
'commandType': 'command', 'commandType': 'command',
@ -707,11 +959,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'Play', 'command': 'Play',
'commandType': 'command', 'commandType': 'command',
@ -725,11 +977,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'Pause', 'command': 'Pause',
'commandType': 'command', 'commandType': 'command',
@ -743,11 +995,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'Stop', 'command': 'Stop',
'commandType': 'command', 'commandType': 'command',
@ -761,11 +1013,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'FastForward', 'command': 'FastForward',
'commandType': 'command', 'commandType': 'command',
@ -779,11 +1031,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'Rewind', 'command': 'Rewind',
'commandType': 'command', 'commandType': 'command',
@ -797,11 +1049,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'Next', 'command': 'Next',
'commandType': 'command', 'commandType': 'command',
@ -815,11 +1067,11 @@ class SwitchbotPlugin(
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'Previous', 'command': 'Previous',
'commandType': 'command', 'commandType': 'command',
@ -854,37 +1106,105 @@ class SwitchbotPlugin(
@action @action
# pylint: disable=redefined-builtin,arguments-differ # pylint: disable=redefined-builtin,arguments-differ
def set_value(self, device: str, property=None, data=None, **__): def set_value(self, device: str, property=None, data=None, **__):
entity = self._to_entity(device, property)
assert entity, f'No such entity: "{device}"'
dt = entity.data.get('device_type')
assert dt, f'Could not infer the device type for "{device}"'
device_type = DeviceType(dt)
setter_class = entity_setters.get(device_type)
assert setter_class, f'No setters found for device type "{device_type}"'
setter = setter_class(entity)
return setter(property=property, value=data)
def _to_entity(
self,
device: str,
property: Optional[str] = None, # pylint: disable=redefined-builtin
) -> Optional[Entity]:
dev = self._get_device(device) dev = self._get_device(device)
entities = list(self.transform_entities([dev])) entities = list(self.transform_entities([dev]))
assert entities, f'The device {device} is not mapped to a compatible entity' if not entities:
return None
if len(entities) == 1:
return entities[0]
if not property:
device, property = self._split_device_id_and_property(device)
assert property, 'No property specified'
entity = entities[0] entity_id = f'{device}:{property}'
return next(iter([e for e in entities if e.id == entity_id]), None)
# SwitchBot case @action
if isinstance(entity, EnumSwitch): def set_lights(
method = getattr(self, data, None) self,
assert method, f'No such action available for device "{device}": "{data}"' *_,
lights: Collection[str],
on: Optional[bool] = None,
brightness: Optional[int] = None,
hex: Optional[str] = None, # pylint: disable=redefined-builtin
temperature: Optional[int] = None,
**__,
):
"""
Change the settings for compatible lights.
return method(entity.id) :param lights: Light names or IDs.
:param on: Turn on the lights.
:param brightness: Set the brightness of the lights.
:param hex: Set the color of the lights.
:param temperature: Set the temperature of the lights.
"""
devices = [self._get_device(light) for light in lights]
for dev in devices:
if on is not None:
method = self.on if on else self.off
method(dev['id'])
# Curtain case if brightness is not None:
if isinstance(entity, Dimmer): self._run(
return self.set_curtain_position(entity.id, data) 'post',
'commands',
device=dev,
json={
'command': 'setBrightness',
'commandType': 'command',
'parameter': brightness,
},
)
self.logger.warning( if hex is not None:
'Could not find a suitable action for device "%s" of type "%s"', self._run(
device, 'post',
type(entity.__class__.__name__), 'commands',
device=dev,
json={
'command': 'setColor',
'commandType': 'command',
'parameter': hex,
},
)
if temperature is not None:
self._run(
'post',
'commands',
device=dev,
json={
'command': 'setColorTemperature',
'commandType': 'command',
'parameter': temperature,
},
) )
def main(self): def main(self):
entities = {} entities = {}
while not self.should_stop(): while not self.should_stop():
new_entities = { status = self.status(publish_entities=False).output
e['id']: e for e in self.status(publish_entities=False).output new_entities = {e['id']: e for e in status}
}
updated_entities = { updated_entities = {
id: e id: e
for id, e in new_entities.items() for id, e in new_entities.items()

View file

@ -0,0 +1,30 @@
from enum import Enum
class DeviceType(Enum):
"""
Constants used for the `device_type` attribute.
Reference: https://github.com/OpenWonderLabs/SwitchBotAPI
"""
BLIND_TILT = 'Blind Tilt'
BOT = 'Bot'
CEILING_LIGHT = 'Ceiling Light'
CEILING_LIGHT_PRO = 'Ceiling Light Pro'
COLOR_BULB = 'Color Bulb'
CONTACT_SENSOR = 'Contact Sensor'
CURTAIN = 'Curtain'
HUMIDIFIER = 'Humidifier'
KEYPAD = 'Keypad'
KEYPAD_TOUCH = 'Keypad Touch'
LOCK = 'Smart Lock'
METER = 'Meter'
METER_PLUS = 'Meter Plus'
MOTION_SENSOR = 'Motion Sensor'
PLUG = 'Plug'
PLUG_MINI_JP = 'Plug Mini (JP)'
PLUG_MINI_US = 'Plug Mini (US)'
ROBOT_VACUUM_CLEANER_S1 = 'Robot Vacuum Cleaner S1'
ROBOT_VACUUM_CLEANER_S1_PLUS = 'Robot Vacuum Cleaner S1 Plus'
STRIP_LIGHT = 'Strip Light'

View file

@ -0,0 +1,157 @@
# pylint: disable=too-few-public-methods
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional, Type
from platypush.context import get_plugin
from platypush.entities import Entity
from ._constants import DeviceType
class EntitySetter(ABC):
"""
Base class for entity setters.
The purpose of entity setters is to map property/values passed to
:meth:`platypush.plugins.switchbot.SwitchbotPlugin.set_value` to native
Switchbot device commands.
"""
def __init__(self, entity: Entity):
self.entity = entity
self.device_id, self.property = self._plugin._split_device_id_and_property(
self.entity.id
)
@abstractmethod
def _set(
self,
value: Any,
*args: Any,
property: Optional[str] = None, # pylint: disable=redefined-builtin
**kwargs: Any,
):
raise NotImplementedError()
def __call__(
self,
value: Any,
*args: Any,
property: Optional[str] = None, # pylint: disable=redefined-builtin
**kwargs: Any,
):
return self._set(value, *args, property=property, **kwargs)
@property
def _plugin(self):
return get_plugin('switchbot')
class EntitySetterWithBinaryState(EntitySetter):
"""
Base setter for entities with a binary on/off state.
"""
def _set(
self,
value: Any,
*_: Any,
property: Optional[str] = None, # pylint: disable=redefined-builtin
**__: Any,
):
if property == 'state':
action = self._plugin.on if value else self._plugin.off
return action(self.device_id)
return None
class EntitySetterWithValueAsMethod(EntitySetter):
"""
This mapper maps the value passed to
:meth:`platypush.plugins.switchbot.SwitchbotPlugin.set_value` to plugin
actions.
In this case, the action value has a 1-1 mapping with the name of the
associated plugin action.
"""
def _set(self, value: Any, *_: Any, **__: Any):
method = getattr(self._plugin, value, None)
assert (
method
), f'No such action available for device "{self.device_id}": "{value}"'
return method(self.device_id)
class CurtainEntitySetter(EntitySetter):
"""
Curtain entity setter.
"""
def _set(self, value: Any, *_: Any, **__: Any):
return self._plugin.set_curtain_position(self.device_id, int(value))
class HumidifierEntitySetter(EntitySetterWithBinaryState):
"""
Humidifier entity setter.
"""
def _set(
self,
value: Any,
*args: Any,
property: Optional[str] = None, # pylint: disable=redefined-builtin
**kwargs: Any,
):
if property == 'state':
return super()._set(value, *args, property=property, **kwargs)
if property == 'child_lock':
action = self._plugin.lock if value else self._plugin.unlock
return action(self.device_id)
if property in {'auto', 'nebulization_efficiency'}:
return self._plugin.set_humidifier_efficiency(self.device_id, value)
return None
class PlugEntitySetter(EntitySetterWithBinaryState):
"""
Plug entity setter.
"""
class LightEntitySetter(EntitySetter):
"""
Light entity setter.
"""
def _set(
self,
value: Any,
*_: Any,
property: Optional[str] = None, # pylint: disable=redefined-builtin
**__: Any,
):
assert property, 'No light property specified'
return self._plugin.set_curtain_position(self.device_id, int(value))
# A static map of device types -> entity setters functors.
entity_setters: Dict[DeviceType, Type[EntitySetter]] = {
DeviceType.BOT: EntitySetterWithValueAsMethod,
DeviceType.CEILING_LIGHT: LightEntitySetter,
DeviceType.CEILING_LIGHT_PRO: LightEntitySetter,
DeviceType.COLOR_BULB: LightEntitySetter,
DeviceType.CURTAIN: CurtainEntitySetter,
DeviceType.HUMIDIFIER: HumidifierEntitySetter,
DeviceType.LOCK: EntitySetterWithValueAsMethod,
DeviceType.PLUG: PlugEntitySetter,
DeviceType.PLUG_MINI_US: PlugEntitySetter,
DeviceType.PLUG_MINI_JP: PlugEntitySetter,
DeviceType.STRIP_LIGHT: LightEntitySetter,
}

View file

@ -1,5 +1,6 @@
from marshmallow import fields from marshmallow import fields, EXCLUDE
from marshmallow.schema import Schema from marshmallow.schema import Schema
from marshmallow.validate import Range
device_types = [ device_types = [
@ -47,11 +48,48 @@ remote_types = [
] ]
class ColorField(fields.Field):
"""
Utility field class for color values.
"""
def _serialize(self, value: str, *_, **__):
"""
Convert a hex native color value (``ff0000``) to the format exposed by
the SwitchBot API (``255:0:0``).
"""
if not value:
return None
# fmt: off
return ''.join([f'{int(i):02x}' for i in value.split(':')])
def _deserialize(self, value: str, *_, **__):
"""
Convert a SwitchBot API color value (``255:0:0``) to the hex native
format (``ff0000``).
"""
if not value:
return None
value = value.lstrip('#')
# fmt: off
return ':'.join(
[str(int(value[i:i+2], 16)) for i in range(0, len(value) - 1, 2)]
)
class DeviceSchema(Schema): class DeviceSchema(Schema):
""" """
Base class for SwitchBot device schemas. Base class for SwitchBot device schemas.
""" """
class Meta:
"""
Ignore unknown fields.
"""
unknown = EXCLUDE
id = fields.String( id = fields.String(
attribute='deviceId', attribute='deviceId',
required=True, required=True,
@ -112,6 +150,36 @@ class DeviceStatusSchema(DeviceSchema):
attribute='power', attribute='power',
metadata={'description': 'True if the device is on, False otherwise'}, metadata={'description': 'True if the device is on, False otherwise'},
) )
voltage = fields.Float(
allow_none=True,
metadata={
'description': '[Plug devices only] Voltage of the device, measured '
'in volts'
},
)
power = fields.Float(
attribute='weight',
allow_none=True,
metadata={
'description': '[Plug devices only] Consumed power, measured in watts'
},
)
current = fields.Float(
attribute='electricCurrent',
allow_none=True,
metadata={
'description': '[Plug devices only] Device current at the moment, '
'measured in amperes'
},
)
active_time = fields.Int(
attribute='electricityOfDay',
allow_none=True,
metadata={
'description': '[Plug devices only] How long the device has been '
'absorbing during a day, measured in minutes'
},
)
moving = fields.Boolean( moving = fields.Boolean(
metadata={ metadata={
'description': '[Curtain devices only] True if the device is ' 'description': '[Curtain devices only] True if the device is '
@ -120,25 +188,61 @@ class DeviceStatusSchema(DeviceSchema):
) )
position = fields.Int( position = fields.Int(
attribute='slidePosition', attribute='slidePosition',
allow_none=True,
metadata={ metadata={
'description': '[Curtain devices only] Position of the device on ' 'description': '[Curtain devices only] Position of the device on '
'the curtain rail, between 0 (open) and 1 (closed)' 'the curtain rail, between 0% (open) and 100% (closed)'
},
)
locked = fields.Boolean(
attribute='lockState',
metadata={'description': '[Lock devices only] True if the lock is on'},
)
door_open = fields.Boolean(
attribute='doorState',
metadata={
'description': '[Lock devices only] True if the door is open, False otherwise'
},
)
brightness = fields.Int(
metadata={
'description': '[Light devices only] Light brightness, between 1 and 100'
},
allow_none=True,
validate=Range(min=1, max=100),
)
color = ColorField(
allow_none=True,
metadata={
'description': '[Light devices only] Color, expressed as a hex string (e.g. FF0000)'
},
)
color_temperature = fields.Int(
attribute='colorTemperature',
allow_none=True,
validate=Range(min=2700, max=6500),
metadata={
'description': '[Light devices only] Color temperature, between 2700 and 6500'
}, },
) )
temperature = fields.Float( temperature = fields.Float(
allow_none=True,
metadata={ metadata={
'description': '[Meter/humidifier/Air conditioner devices only] ' 'description': '[Meter/humidifier/Air conditioner devices only] '
'Temperature in Celsius' 'Temperature in Celsius'
} },
) )
humidity = fields.Float( humidity = fields.Float(
metadata={'description': '[Meter/humidifier devices only] Humidity in %'} allow_none=True,
metadata={'description': '[Meter/humidifier devices only] Humidity in %'},
) )
fan_speed = fields.Int( fan_speed = fields.Int(
metadata={'description': '[Air conditioner devices only] Speed of the fan'} allow_none=True,
metadata={'description': '[Air conditioner devices only] Speed of the fan'},
) )
nebulization_efficiency = fields.Float( nebulization_efficiency = fields.Float(
attribute='nebulizationEfficiency', attribute='nebulizationEfficiency',
allow_none=True,
metadata={ metadata={
'description': '[Humidifier devices only] Nebulization efficiency in %' 'description': '[Humidifier devices only] Nebulization efficiency in %'
}, },
@ -153,6 +257,12 @@ class DeviceStatusSchema(DeviceSchema):
sound = fields.Boolean( sound = fields.Boolean(
metadata={'description': '[Humidifier devices only] True if sound is muted'} metadata={'description': '[Humidifier devices only] True if sound is muted'}
) )
low_water = fields.Boolean(
attribute='lackWater',
metadata={
'description': '[Humidifier devices only] True if the device is low on water'
},
)
mode = fields.Int( mode = fields.Int(
metadata={'description': '[Fan/Air conditioner devices only] Fan mode'} metadata={'description': '[Fan/Air conditioner devices only] Fan mode'}
) )