diff --git a/platypush/backend/http/webapp/src/components/Sensor.vue b/platypush/backend/http/webapp/src/components/Sensor.vue index 60f86b6d..397b4a1a 100644 --- a/platypush/backend/http/webapp/src/components/Sensor.vue +++ b/platypush/backend/http/webapp/src/components/Sensor.vue @@ -60,6 +60,8 @@ export default { if (this.isBoolean) return this.parseBoolean(this.value) + if (Array.isArray(this.value) || typeof(this.value) === 'object') + return JSON.stringify(this.value) let value = parseFloat(this.value) if (this.decimals != null) diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/Button.vue b/platypush/backend/http/webapp/src/components/panels/Entities/Button.vue new file mode 120000 index 00000000..bb735fa0 --- /dev/null +++ b/platypush/backend/http/webapp/src/components/panels/Entities/Button.vue @@ -0,0 +1 @@ +EnumSensor.vue \ No newline at end of file diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/MultiValueSensor.vue b/platypush/backend/http/webapp/src/components/panels/Entities/MultiValueSensor.vue new file mode 120000 index 00000000..70b94460 --- /dev/null +++ b/platypush/backend/http/webapp/src/components/panels/Entities/MultiValueSensor.vue @@ -0,0 +1 @@ +Sensor.vue \ No newline at end of file diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json index d8bc125e..2957dbb4 100644 --- a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json +++ b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json @@ -7,6 +7,14 @@ } }, + "button": { + "name": "Button", + "name_plural": "Buttons", + "icon": { + "class": "fas fa-circle-dot" + } + }, + "current_sensor": { "name": "Sensor", "name_plural": "Sensors", @@ -143,6 +151,14 @@ } }, + "multi_value_sensor": { + "name": "Sensor", + "name_plural": "Sensors", + "icon": { + "class": "fas fa-thermometer" + } + }, + "binary_sensor": { "name": "Sensor", "name_plural": "Sensors", diff --git a/platypush/entities/buttons.py b/platypush/entities/buttons.py new file mode 100644 index 00000000..3909d234 --- /dev/null +++ b/platypush/entities/buttons.py @@ -0,0 +1,27 @@ +import logging + +from sqlalchemy import ( + Column, + ForeignKey, + Integer, +) + +from platypush.common.db import Base + +from .sensors import EnumSensor + +logger = logging.getLogger(__name__) + + +if 'button' not in Base.metadata: + + class Button(EnumSensor): + __tablename__ = 'button' + + id = Column( + Integer, ForeignKey(EnumSensor.id, ondelete='CASCADE'), primary_key=True + ) + + __mapper_args__ = { + 'polymorphic_identity': __tablename__, + } diff --git a/platypush/entities/sensors.py b/platypush/entities/sensors.py index a47c91c8..f3489ac9 100644 --- a/platypush/entities/sensors.py +++ b/platypush/entities/sensors.py @@ -66,9 +66,9 @@ if 'binary_sensor' not in Base.metadata: if isinstance(value, str): value = value.lower() - if value in {True, 1, '1', 't', 'true', 'on'}: + if value in {True, 1, '1', 't', 'true', 'on', 'ON'}: value = True - elif value in {False, 0, '0', 'f', 'false', 'off'}: + elif value in {False, 0, '0', 'f', 'false', 'off', 'OFF'}: value = False elif value is not None: logger.warning(f'Unsupported value for BinarySensor type: {value}') @@ -100,3 +100,18 @@ if 'enum_sensor' not in Base.metadata: __mapper_args__ = { 'polymorphic_identity': __tablename__, } + + +if 'multi_value_sensor' not in Base.metadata: + + class MultiValueSensor(Sensor): + __tablename__ = 'multi_value_sensor' + + id = Column( + Integer, ForeignKey(Device.id, ondelete='CASCADE'), primary_key=True + ) + value = Column(JSON) + + __mapper_args__ = { + 'polymorphic_identity': __tablename__, + } diff --git a/platypush/plugins/smartthings/__init__.py b/platypush/plugins/smartthings/__init__.py index 49dd219b..33aaeba0 100644 --- a/platypush/plugins/smartthings/__init__.py +++ b/platypush/plugins/smartthings/__init__.py @@ -1,32 +1,30 @@ import asyncio -import aiohttp - from threading import RLock -from typing import Optional, Dict, List, Tuple, Type, Union, Iterable +from typing import Dict, Iterable, List, Optional, Tuple, Type, Union +import aiohttp from pysmartthings import ( Attribute, Capability, Command, - Device, + DeviceEntity, DeviceStatus, SmartThings, ) from platypush.entities import Entity, manages - -from platypush.entities.devices import Device as PDevice +from platypush.entities.devices import Device from platypush.entities.dimmers import Dimmer from platypush.entities.lights import Light from platypush.entities.sensors import Sensor -from platypush.entities.switches import Switch +from platypush.entities.switches import EnumSwitch, Switch from platypush.plugins import RunnablePlugin, action from platypush.utils import camel_case_to_snake_case -from ._mappers import device_mappers +from ._mappers import DeviceMapper, device_mappers -@manages(PDevice, Dimmer, Sensor, Switch, Light) +@manages(Device, Dimmer, EnumSwitch, Light, Sensor, Switch) class SmartthingsPlugin(RunnablePlugin): """ Plugin to interact with devices and locations registered to a Samsung SmartThings account. @@ -316,7 +314,7 @@ class SmartthingsPlugin(RunnablePlugin): assert location, 'Location {} not found'.format(location_id or name) return self._location_to_dict(location) - def _get_device(self, device: str) -> Device: + def _get_device(self, device: str) -> DeviceEntity: return self._get_devices(device)[0] @staticmethod @@ -328,7 +326,7 @@ class SmartthingsPlugin(RunnablePlugin): def _get_existing_and_missing_devices( self, *devices: str - ) -> Tuple[List[Device], List[str]]: + ) -> Tuple[List[DeviceEntity], List[str]]: # Split the external_id:type indicators and always return the parent device devices = tuple(self._to_device_and_property(dev)[0] for dev in devices) @@ -341,7 +339,7 @@ class SmartthingsPlugin(RunnablePlugin): missing_devs = {dev for dev in devices if dev not in found_devs} return list(found_devs.values()), list(missing_devs) # type: ignore - def _get_devices(self, *devices: str) -> List[Device]: + def _get_devices(self, *devices: str) -> List[DeviceEntity]: devs, missing_devs = self._get_existing_and_missing_devices(*devices) if missing_devs: self.refresh_info() @@ -376,8 +374,8 @@ class SmartthingsPlugin(RunnablePlugin): } """ - device = self._get_device(device) - return self._device_to_dict(device) + dev = self._get_device(device) + return self._device_to_dict(dev) async def _execute( self, @@ -457,58 +455,92 @@ class SmartthingsPlugin(RunnablePlugin): loop.stop() @staticmethod + def _property_to_entity_name(property: str) -> str: + return ' '.join( + [ + t[:1].upper() + t[1:] + for t in camel_case_to_snake_case(property).split('_') + ] + ) + + @classmethod def _to_entity( - device: Device, property: str, entity_type: Type[Entity], **kwargs + cls, device: DeviceEntity, property: str, entity_type: Type[Entity], **kwargs ) -> Entity: return entity_type( id=f'{device.device_id}:{property}', - name=entity_type.__name__, + name=cls._property_to_entity_name(property), **kwargs, ) - @staticmethod - def _get_status_attr_info(device: Device, attr: str) -> dict: - status = device.status.attributes.get(attr) + @classmethod + def _get_status_attr_info(cls, device: DeviceEntity, mapper: DeviceMapper) -> dict: + status = device.status.attributes.get(mapper.attribute) info = {} if status: - if getattr(status, 'unit', None) is not None: - info['unit'] = status.unit - if getattr(status, 'min', None) is not None: - info['min'] = status.min - if getattr(status, 'max', None) is not None: - info['max'] = status.max + info.update( + { + attr: getattr(status, attr, None) + for attr in ('unit', 'min', 'max') + if getattr(status, attr, None) is not None + } + ) + + supported_values = mapper.values + if isinstance(mapper.value_type, str): + # The list of supported values is actually contained on a + # device attribute + try: + supported_values = getattr( + device.status, mapper.value_type, mapper.values + ) + except Exception: + pass + + if supported_values: + info['values'] = mapper.values return info + @staticmethod + def _merge_dicts(*dicts: dict) -> dict: + ret = {} + for d in dicts: + ret.update(d) + return ret + @classmethod def _get_supported_entities( cls, - device: Device, + device: DeviceEntity, entity_type: Optional[Type[Entity]] = None, entity_value_attr: str = 'value', **default_entity_args, ) -> List[Entity]: mappers = [ - m - for m in device_mappers - if (entity_type is None or issubclass(m.entity_type, entity_type)) - and m.capability in device.capabilities + mapper + for mapper in device_mappers + if (entity_type is None or issubclass(mapper.entity_type, entity_type)) + and mapper.capability in device.capabilities ] return [ cls._to_entity( device, - property=m.attribute, - entity_type=m.entity_type, - **{entity_value_attr: m.get_value(device)}, - **default_entity_args, - **cls._get_status_attr_info(device, m.attribute), + property=mapper.attribute, + entity_type=mapper.entity_type, + **cls._merge_dicts( + {entity_value_attr: mapper.get_value(device)}, + default_entity_args, + mapper.entity_args, + cls._get_status_attr_info(device, mapper), + ), ) - for m in mappers + for mapper in mappers ] @classmethod - def _get_lights(cls, device: Device) -> Iterable[Light]: + def _get_lights(cls, device: DeviceEntity) -> Iterable[Light]: if not ( {Capability.color_control, Capability.color_temperature}.intersection( device.capabilities @@ -517,33 +549,39 @@ class SmartthingsPlugin(RunnablePlugin): return [] light_attrs = {} + status = device.status + if Capability.switch in device.capabilities: - light_attrs['on'] = device.status.switch + light_attrs['on'] = status.switch if Capability.switch_level in device.capabilities: - light_attrs['brightness'] = device.status.level + light_attrs['brightness'] = status.level light_attrs['brightness_min'] = 0 light_attrs['brightness_max'] = 100 if Capability.color_temperature in device.capabilities: - light_attrs['temperature'] = device.status.color_temperature + light_attrs['temperature'] = status.color_temperature light_attrs['temperature_min'] = 1 light_attrs['temperature_max'] = 30000 - if getattr(device.status, 'hue', None) is not None: - light_attrs['hue'] = device.status.hue + if getattr(status, 'hue', None) is not None: + light_attrs['hue'] = status.hue light_attrs['hue_min'] = 0 light_attrs['hue_max'] = 100 - if getattr(device.status, 'saturation', None) is not None: - light_attrs['saturation'] = device.status.saturation + if getattr(status, 'saturation', None) is not None: + light_attrs['saturation'] = status.saturation light_attrs['saturation_min'] = 0 light_attrs['saturation_max'] = 100 return [cls._to_entity(device, 'light', Light, **light_attrs)] @classmethod - def _get_switches(cls, device: Device) -> Iterable[Switch]: + def _get_switches(cls, device: DeviceEntity) -> Iterable[Switch]: return cls._get_supported_entities(device, Switch, entity_value_attr='state') @classmethod - def _get_dimmers(cls, device: Device) -> Iterable[Dimmer]: + def _get_enum_switches(cls, device: DeviceEntity) -> Iterable[Switch]: + return cls._get_supported_entities(device, EnumSwitch) + + @classmethod + def _get_dimmers(cls, device: DeviceEntity) -> Iterable[Dimmer]: return cls._get_supported_entities(device, Dimmer, min=0, max=100) @classmethod @@ -703,12 +741,21 @@ class SmartthingsPlugin(RunnablePlugin): if value is None: # Toggle case dev = self._get_device(device) - assert hasattr( - dev.status, property - ), f'No such property on device "{dev.label}": "{property}"' + if property == 'light': + property = 'switch' + else: + assert hasattr( + dev.status, property + ), f'No such property on device "{dev.label}": "{property}"' + value = getattr(dev.status, property, None) + assert value is not None, ( + f'Could not get the current value of "{property}" for the ' + f'device "{dev.device_id}"' + ) + + value = not value # Toggle device = dev.device_id - value = getattr(dev.status, property) is not True return self.set_value(device, property, value) @@ -769,10 +816,10 @@ class SmartthingsPlugin(RunnablePlugin): ) assert mapper, f'No mappers found to set {property}={data} on device "{device}"' - assert ( mapper.set_command ), f'The property "{property}" on the device "{device}" cannot be set' + command = ( mapper.set_command(data) if callable(mapper.set_command) diff --git a/platypush/plugins/smartthings/_mappers.py b/platypush/plugins/smartthings/_mappers.py index 1b5c5142..666fe9de 100644 --- a/platypush/plugins/smartthings/_mappers.py +++ b/platypush/plugins/smartthings/_mappers.py @@ -1,14 +1,26 @@ +from enum import Enum from typing import Any, Callable, List, Optional, Type, Union -from pysmartthings import Attribute, Capability, Command, Device +from pysmartthings import Attribute, Capability, Command, DeviceEntity from platypush.entities import Entity from platypush.entities.audio import Muted, Volume from platypush.entities.batteries import Battery from platypush.entities.dimmers import Dimmer +from platypush.entities.electricity import EnergySensor, PowerSensor, VoltageSensor +from platypush.entities.humidity import HumiditySensor +from platypush.entities.illuminance import IlluminanceSensor +from platypush.entities.linkquality import LinkQuality from platypush.entities.motion import MotionSensor -from platypush.entities.switches import Switch +from platypush.entities.sensors import ( + BinarySensor, + EnumSensor, + MultiValueSensor, + NumericSensor, +) +from platypush.entities.switches import EnumSwitch, Switch +from platypush.entities.temperature import TemperatureSensor class DeviceMapper: @@ -22,10 +34,11 @@ class DeviceMapper: entity_type: Type[Entity], capability: str, attribute: str, - value_type: Type, - set_command: Optional[Union[str, Callable[[Any], List[Any]]]] = None, - get_value: Optional[Callable[[Device], Any]] = None, + value_type: Union[Type, str], + set_command: Optional[Union[str, Callable[[Any], str]]] = None, + get_value: Optional[Callable[[DeviceEntity], Any]] = None, set_value_args: Optional[Callable[..., Any]] = None, + **kwargs ): self.entity_type = entity_type self.capability = capability @@ -33,31 +46,79 @@ class DeviceMapper: self.attribute = attribute self.value_type = value_type self.get_value = get_value if get_value else self._default_get_value + self.values = [] + self.entity_args = kwargs + + if isinstance(value_type, Enum): + self.values = [v.name for v in entity_type] # type: ignore self.set_value_args = ( set_value_args if set_value_args else self._default_set_value_args ) - def _default_get_value(self, device: Device) -> Any: + def _cast_value(self, value: Any) -> Any: + if not isinstance(self.value_type, (str, Enum)): + try: + value = self.value_type(value) + except Exception: + # Set the value to None in case of cast errors + value = None + + return value + + def _default_get_value(self, device: DeviceEntity) -> Any: if hasattr(device.status, self.attribute): value = getattr(device.status, self.attribute) else: value = device.status.attributes[self.attribute].value - return self.value_type(value) + return self._cast_value(value) def _default_set_value_args(self, *values: Any) -> List[Any]: - return [self.value_type(v) for v in values] + return [self._cast_value(v) for v in values] device_mappers: List[DeviceMapper] = [ + # acceleration_sensor DeviceMapper( - entity_type=Volume, - capability=Capability.audio_volume, - attribute=Attribute.volume, - value_type=int, - set_command=Command.set_volume, + entity_type=BinarySensor, + capability=Capability.acceleration_sensor, + attribute=Attribute.acceleration, + value_type=bool, ), + # air_conditioner_fan_mode + DeviceMapper( + entity_type=EnumSwitch, + capability=Capability.air_conditioner_fan_mode, + attribute=Attribute.fan_mode, + value_type=Attribute.supported_ac_fan_modes, + set_command=Command.set_fan_mode, + ), + # air_conditioner_mode + DeviceMapper( + entity_type=EnumSwitch, + capability=Capability.air_conditioner_mode, + attribute=Attribute.air_conditioner_mode, + value_type=Attribute.supported_ac_modes, + set_command=Command.set_air_conditioner_mode, + ), + # air_quality_sensor + DeviceMapper( + entity_type=NumericSensor, + capability=Capability.air_quality_sensor, + attribute=Attribute.air_quality, + value_type=int, + ), + # alarm + DeviceMapper( + entity_type=EnumSwitch, + capability=Capability.alarm, + attribute=Attribute.alarm, + value_type=Enum('AlarmValues', ['both', 'off', 'siren', 'strobe']), + set_command=lambda value: value, + set_value_args=lambda *_: [], + ), + # audio_mute DeviceMapper( entity_type=Muted, capability=Capability.audio_mute, @@ -66,25 +127,285 @@ device_mappers: List[DeviceMapper] = [ set_command=lambda value: Command.mute if value else Command.unmute, set_value_args=lambda *_: [], ), + # audio_volume DeviceMapper( - entity_type=MotionSensor, - capability=Capability.motion_sensor, - attribute=Attribute.motion, - value_type=bool, + entity_type=Volume, + capability=Capability.audio_volume, + attribute=Attribute.volume, + value_type=int, + set_command=Command.set_volume, ), + # battery DeviceMapper( entity_type=Battery, capability=Capability.battery, attribute=Attribute.battery, value_type=int, ), + # body_mass_index_measurement + DeviceMapper( + entity_type=NumericSensor, + capability=Capability.body_mass_index_measurement, + attribute=Attribute.body_weight_measurement, + value_type=float, + ), + # body_weight_measurement + DeviceMapper( + entity_type=NumericSensor, + capability=Capability.body_weight_measurement, + attribute=Attribute.body_weight_measurement, + value_type=float, + ), + # button + DeviceMapper( + entity_type=EnumSensor, + capability=Capability.button, + attribute=Attribute.button, + value_type=Enum( + 'ButtonValues', + [ + 'pushed', + 'held', + 'double', + 'pushed_2x', + 'pushed_3x', + 'pushed_4x', + 'pushed_5x', + 'pushed_6x', + 'down', + 'down_2x', + 'down_3x', + 'down_4x', + 'down_5x', + 'down_6x', + 'down_hold', + 'up', + 'up_2x', + 'up_3x', + 'up_4x', + 'up_5x', + 'up_6x', + 'up_hold', + 'swipe_up', + 'swipe_down', + 'swipe_left', + 'swipe_right', + 'unknown', + ], + ), + ), + # carbon_dioxide_measurement + DeviceMapper( + entity_type=NumericSensor, + capability=Capability.carbon_dioxide_measurement, + attribute=Attribute.carbon_dioxide, + value_type=float, + ), + # carbon_monoxide_detector + DeviceMapper( + entity_type=EnumSensor, + capability=Capability.carbon_monoxide_detector, + attribute=Attribute.carbon_monoxide, + value_type=Enum( + 'CarbonMonoxideValues', ['clear', 'detected', 'tested', 'unknown'] + ), + ), + # carbon_monoxide_measurement + DeviceMapper( + entity_type=NumericSensor, + capability=Capability.carbon_monoxide_measurement, + attribute=Attribute.carbon_monoxide_level, + value_type=float, + ), + # contact_sensor + DeviceMapper( + entity_type=BinarySensor, + capability=Capability.contact_sensor, + attribute=Attribute.contact, + value_type=bool, + ), + # dishwasher_operating_state + DeviceMapper( + entity_type=EnumSensor, + capability=Capability.dishwasher_operating_state, + attribute=Attribute.machine_state, + value_type=Attribute.supported_machine_states, + ), + # door_control + DeviceMapper( + entity_type=Switch, + capability=Capability.door_control, + attribute=Attribute.door, + value_type=bool, + get_value=lambda device: device.status.door in {'open', 'opening'}, + set_command=lambda value: Command.open if value else Command.close, + ), + # dryer_operating_state + DeviceMapper( + entity_type=EnumSensor, + capability=Capability.dryer_operating_state, + attribute=Attribute.machine_state, + value_type=Attribute.supported_machine_states, + ), + # dust_sensor + DeviceMapper( + entity_type=NumericSensor, + capability=Capability.dust_sensor, + attribute=Attribute.dust_level, + value_type=float, + ), + # energy_meter + DeviceMapper( + entity_type=EnergySensor, + capability=Capability.energy_meter, + attribute=Attribute.energy, + value_type=float, + ), + # equivalent_carbon_dioxide_measurement + DeviceMapper( + entity_type=NumericSensor, + capability=Capability.equivalent_carbon_dioxide_measurement, + attribute=Attribute.equivalent_carbon_dioxide_measurement, + value_type=float, + ), + # fan_speed DeviceMapper( entity_type=Dimmer, - capability=Capability.switch_level, - attribute=Attribute.level, + capability=Capability.fan_speed, + attribute=Attribute.fan_speed, value_type=int, - set_command=Command.set_level, + set_command=Command.set_fan_speed, ), + # formaldehyde_measurement + DeviceMapper( + entity_type=NumericSensor, + capability=Capability.formaldehyde_measurement, + attribute=Attribute.formaldehyde_level, + value_type=float, + ), + # gas_meter + DeviceMapper( + entity_type=NumericSensor, + capability=Capability.gas_meter, + attribute=Attribute.gas_meter, + value_type=float, + ), + # illuminance_measurement + DeviceMapper( + entity_type=IlluminanceSensor, + capability=Capability.illuminance_measurement, + attribute=Attribute.illuminance, + value_type=float, + ), + # infrared_level + DeviceMapper( + entity_type=NumericSensor, + capability=Capability.infrared_level, + attribute=Attribute.infrared_level, + value_type=int, + min=0, + max=100, + ), + # lock + DeviceMapper( + entity_type=Switch, + capability=Capability.lock, + attribute=Attribute.lock, + value_type=bool, + get_value=lambda device: device.status.lock in {True, 'locked'}, + set_command=lambda value: Command.lock if value else Command.unlock, + set_value_args=lambda *_: [], + ), + # media_input_source + DeviceMapper( + entity_type=EnumSwitch, + capability=Capability.media_input_source, + attribute=Attribute.input_source, + value_type=Attribute.supported_input_sources, + set_command=Command.set_input_source, + ), + # motion_sensor + DeviceMapper( + entity_type=MotionSensor, + capability=Capability.motion_sensor, + attribute=Attribute.motion, + value_type=bool, + ), + # odor_sensor + DeviceMapper( + entity_type=NumericSensor, + capability=Capability.odor_sensor, + attribute=Attribute.odor_level, + value_type=int, + ), + # oven_operating_state + DeviceMapper( + entity_type=EnumSensor, + capability=Capability.oven_operating_state, + attribute=Attribute.machine_state, + value_type=Attribute.supported_machine_states, + ), + # power_consumption_report + DeviceMapper( + entity_type=PowerSensor, + capability=Capability.power_consumption_report, + attribute=Attribute.power_consumption, + value_type=float, + ), + # power_meter + DeviceMapper( + entity_type=PowerSensor, + capability=Capability.power_meter, + attribute=Attribute.power, + value_type=float, + ), + # power_source + DeviceMapper( + entity_type=EnumSensor, + capability=Capability.power_source, + attribute=Attribute.power_source, + value_type=Enum('PowerSourceValues', ['battery', 'dc', 'mains', 'unknown']), + ), + # presence_sensor + DeviceMapper( + entity_type=BinarySensor, + capability=Capability.presence_sensor, + attribute=Attribute.presence, + value_type=bool, + ), + # relative_humidity_measurement + DeviceMapper( + entity_type=HumiditySensor, + capability=Capability.relative_humidity_measurement, + attribute=Attribute.humidity, + value_type=int, + min=0, + max=100, + ), + # signal_strength + DeviceMapper( + entity_type=LinkQuality, + capability=Capability.signal_strength, + attribute=Attribute.lqi, + value_type=int, + min=0, + max=255, + ), + # smoke_detector + DeviceMapper( + entity_type=EnumSensor, + capability=Capability.smoke_detector, + attribute=Attribute.smoke, + value_type=Enum('SmokeDetectorValues', ['clear', 'detected', 'tested']), + ), + # sound_sensor + DeviceMapper( + entity_type=BinarySensor, + capability=Capability.sound_sensor, + attribute=Attribute.sound, + value_type=bool, + ), + # switch DeviceMapper( entity_type=Switch, capability=Capability.switch, @@ -93,6 +414,153 @@ device_mappers: List[DeviceMapper] = [ set_command=lambda value: Command.on if value else Command.off, set_value_args=lambda *_: [], ), + # switch_level + DeviceMapper( + entity_type=Dimmer, + capability=Capability.switch_level, + attribute=Attribute.level, + value_type=int, + set_command=Command.set_level, + ), + # tamper_alert + DeviceMapper( + entity_type=BinarySensor, + capability=Capability.tamper_alert, + attribute=Attribute.tamper, + value_type=bool, + ), + # temperature_measurement + DeviceMapper( + entity_type=TemperatureSensor, + capability=Capability.temperature_measurement, + attribute=Attribute.temperature, + value_type=float, + ), + # thermostat_cooling_setpoint + DeviceMapper( + entity_type=Dimmer, + capability=Capability.thermostat_cooling_setpoint, + attribute=Attribute.cooling_setpoint, + value_type=float, + min=-460, + max=10000, + set_command=Command.set_cooling_setpoint, + ), + # thermostat_fan_mode + DeviceMapper( + entity_type=EnumSwitch, + capability=Capability.thermostat_fan_mode, + attribute=Attribute.thermostat_fan_mode, + value_type=Attribute.supported_thermostat_fan_modes, + set_command=Command.set_thermostat_fan_mode, + ), + # thermostat_heating_setpoint + DeviceMapper( + entity_type=Dimmer, + capability=Capability.thermostat_heating_setpoint, + attribute=Attribute.heating_setpoint, + value_type=float, + min=-460, + max=10000, + set_command=Command.set_heating_setpoint, + ), + # thermostat_mode + DeviceMapper( + entity_type=EnumSwitch, + capability=Capability.thermostat_mode, + attribute=Attribute.thermostat_mode, + value_type=Attribute.supported_thermostat_modes, + set_command=Command.set_thermostat_mode, + ), + # thermostat_operating_state + DeviceMapper( + entity_type=EnumSensor, + capability=Capability.thermostat_operating_state, + attribute=Attribute.thermostat_operating_state, + value_type=Enum( + 'ThermostatOperatingState', + [ + 'cooling', + 'fan only', + 'heating', + 'idle', + 'pending cool', + 'pending heat', + 'vent economizer', + ], + ), + ), + # three_axis + DeviceMapper( + entity_type=MultiValueSensor, + capability=Capability.three_axis, + attribute=Attribute.three_axis, + value_type=list, + ), + # tv_channel + DeviceMapper( + entity_type=Dimmer, + capability=Capability.tv_channel, + attribute=Attribute.tv_channel, + value_type=int, + set_command=Command.set_tv_channel, + min=1, + max=1000, + ), + # tvoc_measurement + DeviceMapper( + entity_type=NumericSensor, + capability=Capability.tvoc_measurement, + attribute=Attribute.tvoc_level, + value_type=float, + ), + # ultraviolet_index + DeviceMapper( + entity_type=NumericSensor, + capability=Capability.ultraviolet_index, + attribute=Attribute.ultraviolet_index, + value_type=float, + ), + # valve + DeviceMapper( + entity_type=Switch, + capability=Capability.valve, + attribute=Attribute.valve, + value_type=bool, + set_command=lambda value: Command.open if value else Command.close, + set_value_args=lambda *_: [], + ), + # voltage_measurement + DeviceMapper( + entity_type=VoltageSensor, + capability=Capability.voltage_measurement, + attribute=Attribute.voltage, + value_type=float, + ), + # washer_operating_state + DeviceMapper( + entity_type=EnumSensor, + capability=Capability.washer_operating_state, + attribute=Attribute.machine_state, + value_type=Attribute.supported_machine_states, + ), + # water_sensor + DeviceMapper( + entity_type=BinarySensor, + capability=Capability.water_sensor, + attribute=Attribute.water, + value_type=bool, + ), + # window_shade + DeviceMapper( + entity_type=Dimmer, + capability=Capability.window_shade, + attribute=Attribute.window_shade, + value_type=int, + set_command='setWindowShade', + min=0, + max=100, + ), ]