diff --git a/README.md b/README.md index afefee2fe..41ed2a55a 100644 --- a/README.md +++ b/README.md @@ -28,44 +28,36 @@ If your device is not supported, it will show the following message in the debug `DEBUG (MainThread) [custom_components.tahoma] Unsupported Tahoma device (internal:TSKAlarmComponent).` -| Somfy uiClass | Home Assistant platform | -| ----------------- | ----------------------- | -| HeatingSystem | climate | -| Awning | cover | -| Curtain | cover | -| ExteriorScreen | cover | -| Gate | cover | -| GarageDoor | cover | -| Pergola | cover | -| RollerShutter | cover | -| SwingingShutter | cover | -| Window | cover | -| AirSensor | sensor | -| ElectricitySensor | sensor | -| HumiditySensor | sensor | -| LightSensor | sensor | -| TemperatureSensor | sensor | -| DoorLock | lock | -| OnOff | switch | -| ContactSensor | binary_sensor | -| OccupancySensor | binary_sensor | -| SmokeSensor | binary_sensor | -| WindowHandle | binary_sensor | -| Light | light | - -## Not supported (yet) - -| Somfy uiClass | -| --------------------- | -| RemoteController | -| Alarm | -| EvoHome | -| HitachiHeatingSystem | -| ExteriorHeatingSystem | -| Fan | -| Siren | -| MusicPlayer | -| VentilationSystem | +| Somfy uiClass | Home Assistant platform | +| -------------------- | ----------------------- | +| HeatingSystem | climate | +| Awning | cover | +| Curtain | cover | +| ExteriorScreen | cover | +| Gate | cover | +| GarageDoor | cover | +| Pergola | cover | +| RollerShutter | cover | +| SwingingShutter | cover | +| Window | cover | +| AirSensor | sensor | +| ElectricitySensor | sensor | +| HumiditySensor | sensor | +| LightSensor | sensor | +| SunSensor | sensor | +| TemperatureSensor | sensor | +| WindSensor | sensor | +| DoorLock | lock | +| OnOff | switch | +| Siren | switch | +| ContactSensor | binary_sensor | +| MotionSensor | binary_sensor | +| OccupancySensor | binary_sensor | +| RainSensor | binary_sensor | +| SmokeSensor | binary_sensor | +| WindowHandle | binary_sensor | +| WaterDetectionSensor | binary_sensor | +| Light | light | ## Advanced diff --git a/custom_components/tahoma/binary_sensor.py b/custom_components/tahoma/binary_sensor.py index a7c686947..88c88d70f 100644 --- a/custom_components/tahoma/binary_sensor.py +++ b/custom_components/tahoma/binary_sensor.py @@ -1,15 +1,24 @@ """Support for TaHoma binary sensors.""" from datetime import timedelta import logging +from typing import Optional from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON from .const import ( + CORE_BUTTON_STATE, CORE_CONTACT_STATE, + CORE_GAS_DETECTION_STATE, CORE_OCCUPANCY_STATE, + CORE_RAIN_STATE, CORE_SMOKE_STATE, + CORE_WATER_DETECTION_STATE, + DEVICE_CLASS_GAS, + DEVICE_CLASS_RAIN, + DEVICE_CLASS_WATER, DOMAIN, + IO_VIBRATION_STATE, TAHOMA_BINARY_SENSOR_DEVICE_CLASSES, TAHOMA_TYPES, ) @@ -25,12 +34,13 @@ async def async_setup_entry(hass, entry, async_add_entities): data = hass.data[DOMAIN][entry.entry_id] - entities = [] controller = data.get("controller") - for device in data.get("devices"): - if TAHOMA_TYPES[device.uiclass] == "binary_sensor": - entities.append(TahomaBinarySensor(device, controller)) + entities = [ + TahomaBinarySensor(device, controller) + for device in data.get("devices") + if TAHOMA_TYPES[device.uiclass] == "binary_sensor" + ] async_add_entities(entities) @@ -58,6 +68,23 @@ def device_class(self): or None ) + @property + def icon(self) -> Optional[str]: + """Return the icon to use in the frontend, if any.""" + + if self.device_class == DEVICE_CLASS_WATER: + if self.is_on: + return "mdi:water" + else: + return "mdi:water-off" + + icons = { + DEVICE_CLASS_GAS: "mdi:waves", + DEVICE_CLASS_RAIN: "mdi:weather-rainy", + } + + return icons.get(self.device_class) + def update(self): """Update the state.""" if self.should_wait(): @@ -66,21 +93,31 @@ def update(self): self.controller.get_states([self.tahoma_device]) - if CORE_CONTACT_STATE in self.tahoma_device.active_states: - self.current_value = ( - self.tahoma_device.active_states.get(CORE_CONTACT_STATE) == "open" - ) - - if CORE_OCCUPANCY_STATE in self.tahoma_device.active_states: - self.current_value = ( - self.tahoma_device.active_states.get(CORE_OCCUPANCY_STATE) - == "personInside" - ) - - if CORE_SMOKE_STATE in self.tahoma_device.active_states: - self.current_value = ( - self.tahoma_device.active_states.get(CORE_SMOKE_STATE) == "detected" - ) + states = self.tahoma_device.active_states + + if CORE_CONTACT_STATE in states: + self.current_value = states.get(CORE_CONTACT_STATE) == "open" + + if CORE_OCCUPANCY_STATE in states: + self.current_value = states.get(CORE_OCCUPANCY_STATE) == "personInside" + + if CORE_SMOKE_STATE in states: + self.current_value = states.get(CORE_SMOKE_STATE) == "detected" + + if CORE_RAIN_STATE in states: + self.current_value = states.get(CORE_RAIN_STATE) == "detected" + + if CORE_WATER_DETECTION_STATE in states: + self.current_value = states.get(CORE_WATER_DETECTION_STATE) == "detected" + + if CORE_GAS_DETECTION_STATE in states: + self.current_value = states.get(CORE_GAS_DETECTION_STATE) == "detected" + + if IO_VIBRATION_STATE in states: + self.current_value = states.get(IO_VIBRATION_STATE) == "detected" + + if CORE_BUTTON_STATE in states: + self.current_value = states.get(CORE_BUTTON_STATE) == "pressed" if self.current_value: self._state = STATE_ON diff --git a/custom_components/tahoma/const.py b/custom_components/tahoma/const.py index b0304ab1b..447336d65 100644 --- a/custom_components/tahoma/const.py +++ b/custom_components/tahoma/const.py @@ -1,6 +1,7 @@ """Constants for the TaHoma integration.""" from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, @@ -23,6 +24,16 @@ DOMAIN = "tahoma" +DEVICE_CLASS_CO = "co" +DEVICE_CLASS_CO2 = "co2" +DEVICE_CLASS_BUTTON = "button" +DEVICE_CLASS_GAS = "gas" +DEVICE_CLASS_RAIN = "rain" +DEVICE_CLASS_SIREN = "siren" +DEVICE_CLASS_SUN_ENERGY = "sun_energy" +DEVICE_CLASS_WATER = "water" +DEVICE_CLASS_WIND_SPEED = "wind_speed" + # Used to map the Somfy uiClass to the Home Assistant platform TAHOMA_TYPES = { "Light": "light", @@ -35,11 +46,16 @@ "LightSensor": "sensor", "DoorLock": "lock", "OnOff": "switch", + "AirFlowSensor": "binary_sensor", # widgetName, uiClass is AirSensor (sensor) + "WaterDetectionSensor": "binary_sensor", # widgetName, uiClass is HumiditySensor (sensor) "HumiditySensor": "sensor", "GarageDoor": "cover", + "CarButtonSensor": "binary_sensor", "ContactSensor": "binary_sensor", + "RainSensor": "binary_sensor", "SmokeSensor": "binary_sensor", "OccupancySensor": "binary_sensor", + "MotionSensor": "binary_sensor", "WindowHandle": "binary_sensor", "ExteriorVenetianBlind": "cover", "Awning": "cover", @@ -49,6 +65,9 @@ "SwingingShutter": "cover", "ElectricitySensor": "sensor", "AirSensor": "sensor", + "Siren": "switch", + "WindSensor": "sensor", + "SunSensor": "sensor", } @@ -68,13 +87,17 @@ "SwingingShutter": DEVICE_CLASS_SHUTTER, } - # Used to map the Somfy widget or uiClass to the Home Assistant device classes TAHOMA_BINARY_SENSOR_DEVICE_CLASSES = { + "AirFlowSensor": DEVICE_CLASS_GAS, + "CarButtonSensor": DEVICE_CLASS_BUTTON, "SmokeSensor": DEVICE_CLASS_SMOKE, "OccupancySensor": DEVICE_CLASS_OCCUPANCY, + "MotionSensor": DEVICE_CLASS_MOTION, "ContactSensor": DEVICE_CLASS_OPENING, "WindowHandle": DEVICE_CLASS_OPENING, + "RainSensor": DEVICE_CLASS_RAIN, + "WaterDetectionSensor": DEVICE_CLASS_WATER, } # Used to map the Somfy widget or uiClass to the Home Assistant device classes @@ -83,7 +106,11 @@ "HumiditySensor": DEVICE_CLASS_HUMIDITY, "LightSensor": DEVICE_CLASS_ILLUMINANCE, "ElectricitySensor": DEVICE_CLASS_POWER, - "AirSensor": "carbon dioxide", + "COSensor": DEVICE_CLASS_CO, + "CO2Sensor": DEVICE_CLASS_CO2, + "RelativeHumiditySensor": DEVICE_CLASS_HUMIDITY, + "WindSensor": DEVICE_CLASS_WIND_SPEED, + "SunSensor": DEVICE_CLASS_SUN_ENERGY, } # TaHoma Attributes @@ -96,13 +123,16 @@ # TaHoma internal device states CORE_BLUE_COLOR_INTENSITY_STATE = "core:BlueColorIntensityState" +CORE_BUTTON_STATE = "core:ButtonState" CORE_CLOSURE_STATE = "core:ClosureState" +CORE_CO_CONCENTRATION_STATE = "core:COConcentrationState" CORE_CO2_CONCENTRATION_STATE = "core:CO2ConcentrationState" CORE_CONTACT_STATE = "core:ContactState" CORE_DEPLOYMENT_STATE = "core:DeploymentState" CORE_DEROGATED_TARGET_TEMPERATURE_STATE = "core:DerogatedTargetTemperatureState" CORE_ELECTRIC_ENERGY_CONSUMPTION_STATE = "core:ElectricEnergyConsumptionState" CORE_ELECTRIC_POWER_CONSUMPTION_STATE = "core:ElectricPowerConsumptionState" +CORE_GAS_DETECTION_STATE = "core:GasDetectionState" CORE_GREEN_COLOR_INTENSITY_STATE = "core:GreenColorIntensityState" CORE_LUMINANCE_STATE = "core:LuminanceState" CORE_MEMORIZED_1_POSITION_STATE = "core:Memorized1PositionState" @@ -111,6 +141,7 @@ CORE_ON_OFF_STATE = "core:OnOffState" CORE_PEDESTRIAN_POSITION_STATE = "core:PedestrianPositionState" CORE_PRIORITY_LOCK_TIMER_STATE = "core:PriorityLockTimerState" +CORE_RAIN_STATE = "core:RainState" CORE_RED_COLOR_INTENSITY_STATE = "core:RedColorIntensityState" CORE_RELATIVE_HUMIDITY_STATE = "core:RelativeHumidityState" CORE_RSSI_LEVEL_STATE = "core:RSSILevelState" @@ -118,10 +149,13 @@ CORE_SLATS_ORIENTATION_STATE = "core:SlatsOrientationState" CORE_SMOKE_STATE = "core:SmokeState" CORE_STATUS_STATE = "core:StatusState" +CORE_SUN_ENERGY_STATE = "core:SunEnergyState" CORE_TARGET_CLOSURE_STATE = "core:TargetClosureState" CORE_TARGET_TEMPERATURE_STATE = "core:TargetTemperatureState" CORE_TEMPERATURE_STATE = "core:TemperatureState" CORE_VERSION_STATE = "core:VersionState" +CORE_WATER_DETECTION_STATE = "core:WaterDetectionState" +CORE_WINDSPEED_STATE = "core:WindSpeedState" # IO Devices specific states IO_MAXIMUM_HEATING_LEVEL_STATE = "io:MaximumHeatingLevelState" @@ -129,6 +163,7 @@ IO_PRIORITY_LOCK_ORIGINATOR_STATE = "io:PriorityLockOriginatorState" IO_TARGET_HEATING_LEVEL_STATE = "io:TargetHeatingLevelState" IO_TIMER_FOR_TRANSITORY_STATE_STATE = "io:TimerForTransitoryStateState" +IO_VIBRATION_STATE = "io:VibrationDetectedState" # Somfy Thermostat specific states ST_DEROGATION_TYPE_STATE = "somfythermostat:DerogationTypeState" diff --git a/custom_components/tahoma/scene.py b/custom_components/tahoma/scene.py index dd647e2d8..22a42d787 100644 --- a/custom_components/tahoma/scene.py +++ b/custom_components/tahoma/scene.py @@ -45,8 +45,3 @@ def unique_id(self) -> str: def name(self): """Return the name of the scene.""" return self._name - - @property - def device_state_attributes(self): - """Return the state attributes of the scene.""" - return {"tahoma_scene_oid": self.tahoma_scene.oid} diff --git a/custom_components/tahoma/sensor.py b/custom_components/tahoma/sensor.py index c1010f7d5..9feec2fd6 100644 --- a/custom_components/tahoma/sensor.py +++ b/custom_components/tahoma/sensor.py @@ -15,11 +15,18 @@ from .const import ( CORE_CO2_CONCENTRATION_STATE, + CORE_CO_CONCENTRATION_STATE, CORE_ELECTRIC_ENERGY_CONSUMPTION_STATE, CORE_ELECTRIC_POWER_CONSUMPTION_STATE, CORE_LUMINANCE_STATE, CORE_RELATIVE_HUMIDITY_STATE, + CORE_SUN_ENERGY_STATE, CORE_TEMPERATURE_STATE, + CORE_WINDSPEED_STATE, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, + DEVICE_CLASS_SUN_ENERGY, + DEVICE_CLASS_WIND_SPEED, DOMAIN, TAHOMA_SENSOR_DEVICE_CLASSES, TAHOMA_TYPES, @@ -36,12 +43,13 @@ async def async_setup_entry(hass, entry, async_add_entities): data = hass.data[DOMAIN][entry.entry_id] - entities = [] controller = data.get("controller") - for device in data.get("devices"): - if TAHOMA_TYPES[device.uiclass] == "sensor": - entities.append(TahomaSensor(device, controller)) + entities = [ + TahomaSensor(device, controller) + for device in data.get("devices") + if TAHOMA_TYPES[device.uiclass] == "sensor" + ] async_add_entities(entities) @@ -64,23 +72,28 @@ def state(self): def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - if self.tahoma_device.uiclass == "TemperatureSensor": + states = self.tahoma_device.active_states + + if CORE_TEMPERATURE_STATE in states: # TODO Retrieve core:MeasuredValueType to understand if it is Celsius or Kelvin return TEMP_CELSIUS - if self.tahoma_device.uiclass == "HumiditySensor": + if CORE_RELATIVE_HUMIDITY_STATE in states: return UNIT_PERCENTAGE - if self.tahoma_device.uiclass == "LightSensor": + if CORE_LUMINANCE_STATE in states: return "lx" - if CORE_ELECTRIC_POWER_CONSUMPTION_STATE in self.tahoma_device.active_states: + if CORE_ELECTRIC_POWER_CONSUMPTION_STATE in states: return POWER_WATT - if CORE_ELECTRIC_ENERGY_CONSUMPTION_STATE in self.tahoma_device.active_states: + if CORE_ELECTRIC_ENERGY_CONSUMPTION_STATE in states: return ENERGY_KILO_WATT_HOUR - if self.tahoma_device.uiclass == "AirSensor": + if ( + CORE_CO_CONCENTRATION_STATE in states + or CORE_CO2_CONCENTRATION_STATE in states + ): return CONCENTRATION_PARTS_PER_MILLION return None @@ -89,10 +102,14 @@ def unit_of_measurement(self): def icon(self) -> Optional[str]: """Return the icon to use in the frontend, if any.""" - if self.tahoma_device.uiclass == "AirSensor": - return "mdi:periodic-table-co2" + icons = { + DEVICE_CLASS_CO: "mdi:air-filter", + DEVICE_CLASS_CO2: "mdi:periodic-table-co2", + DEVICE_CLASS_WIND_SPEED: "mdi:weather-windy", + DEVICE_CLASS_SUN_ENERGY: "mdi:solar-power", + } - return None + return icons.get(self.device_class) @property def device_class(self) -> Optional[str]: @@ -111,47 +128,46 @@ def update(self): self.controller.get_states([self.tahoma_device]) - if CORE_LUMINANCE_STATE in self.tahoma_device.active_states: - self.current_value = self.tahoma_device.active_states.get( - CORE_LUMINANCE_STATE - ) + states = self.tahoma_device.active_states + + if CORE_LUMINANCE_STATE in states: + self.current_value = states.get(CORE_LUMINANCE_STATE) - if CORE_RELATIVE_HUMIDITY_STATE in self.tahoma_device.active_states: + if CORE_RELATIVE_HUMIDITY_STATE in states: self.current_value = float( - "{:.2f}".format( - self.tahoma_device.active_states.get(CORE_RELATIVE_HUMIDITY_STATE) - ) + "{:.2f}".format(states.get(CORE_RELATIVE_HUMIDITY_STATE)) ) - if CORE_TEMPERATURE_STATE in self.tahoma_device.active_states: + if CORE_TEMPERATURE_STATE in states: self.current_value = float( - "{:.2f}".format( - self.tahoma_device.active_states.get(CORE_TEMPERATURE_STATE) - ) + "{:.2f}".format(states.get(CORE_TEMPERATURE_STATE)) ) - if CORE_ELECTRIC_POWER_CONSUMPTION_STATE in self.tahoma_device.active_states: + if CORE_ELECTRIC_POWER_CONSUMPTION_STATE in states: self.current_value = float( - "{:.2f}".format( - self.tahoma_device.active_states.get( - CORE_ELECTRIC_POWER_CONSUMPTION_STATE - ) - ) + "{:.2f}".format(states.get(CORE_ELECTRIC_POWER_CONSUMPTION_STATE)) ) - if CORE_ELECTRIC_ENERGY_CONSUMPTION_STATE in self.tahoma_device.active_states: + if CORE_ELECTRIC_ENERGY_CONSUMPTION_STATE in states: self.current_value = ( float( - "{:.2f}".format( - self.tahoma_device.active_states.get( - CORE_ELECTRIC_ENERGY_CONSUMPTION_STATE - ) - ) + "{:.2f}".format(states.get(CORE_ELECTRIC_ENERGY_CONSUMPTION_STATE)) ) / 1000 ) - if CORE_CO2_CONCENTRATION_STATE in self.tahoma_device.active_states: - self.current_value = int( - self.tahoma_device.active_states.get(CORE_CO2_CONCENTRATION_STATE) + if CORE_CO_CONCENTRATION_STATE in states: + self.current_value = int(states.get(CORE_CO_CONCENTRATION_STATE)) + + if CORE_CO2_CONCENTRATION_STATE in states: + self.current_value = int(states.get(CORE_CO2_CONCENTRATION_STATE)) + + if CORE_WINDSPEED_STATE in states: + self.current_value = float( + "{:.2f}".format(states.get(CORE_WINDSPEED_STATE)) + ) + + if CORE_SUN_ENERGY_STATE in states: + self.current_value = float( + "{:.2f}".format(states.get(CORE_SUN_ENERGY_STATE)) ) diff --git a/custom_components/tahoma/switch.py b/custom_components/tahoma/switch.py index 92dd143a1..e65675266 100644 --- a/custom_components/tahoma/switch.py +++ b/custom_components/tahoma/switch.py @@ -1,10 +1,11 @@ """Support for TaHoma switches.""" import logging +from typing import Optional from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity from homeassistant.const import STATE_OFF, STATE_ON -from .const import DOMAIN, TAHOMA_TYPES +from .const import CORE_ON_OFF_STATE, DEVICE_CLASS_SIREN, DOMAIN, TAHOMA_TYPES from .tahoma_device import TahomaDevice _LOGGER = logging.getLogger(__name__) @@ -31,47 +32,67 @@ class TahomaSwitch(TahomaDevice, SwitchEntity): def __init__(self, tahoma_device, controller): """Initialize the switch.""" super().__init__(tahoma_device, controller) - self._state = STATE_OFF - self._skip_update = False + + self._state = None def update(self): """Update method.""" - # Postpone the immediate state check for changes that take time. + if self.should_wait(): self.schedule_update_ha_state(True) return - if self._skip_update: - self._skip_update = False - return - self.controller.get_states([self.tahoma_device]) - _LOGGER.debug("Update %s, state: %s", self._name, self._state) + if CORE_ON_OFF_STATE in self.tahoma_device.active_states: + self.current_value = ( + self.tahoma_device.active_states.get(CORE_ON_OFF_STATE) == "on" + ) @property def device_class(self): """Return the class of the device.""" + if self.tahoma_device.uiclass == "Siren": + return DEVICE_CLASS_SIREN + return DEVICE_CLASS_SWITCH + @property + def icon(self) -> Optional[str]: + """Return the icon to use in the frontend, if any.""" + + if self.device_class == DEVICE_CLASS_SIREN: + if self.is_on: + return "mdi:bell-ring" + else: + return "mdi:bell-off" + + return None + def turn_on(self, **kwargs): """Send the on command.""" - _LOGGER.debug("Turn on: %s", self._name) - self.apply_action("on") - self._skip_update = True - self._state = STATE_ON + if "on" in self.tahoma_device.command_definitions: + return self.apply_action("on") + + if "ringWithSingleSimpleSequence" in self.tahoma_device.command_definitions: + # Values taken from iosiren.js (tahomalink.com). Parameter usage is currently unknown. + return self.apply_action( + "ringWithSingleSimpleSequence", 120000, 75, 2, "memorizedVolume" + ) def turn_off(self, **kwargs): """Send the off command.""" - self.apply_action("off") - self._skip_update = True - self._state = STATE_OFF + + if "off" in self.tahoma_device.command_definitions: + return self.apply_action("off") def toggle(self, **kwargs): """Click the switch.""" - self.apply_action("cycle") + + if "cycle" in self.tahoma_device.command_definitions: + return self.apply_action("cycle") @property def is_on(self):