Skip to content

Commit

Permalink
Add binary sensors dynamically based on available device state (#505)
Browse files Browse the repository at this point in the history
* Refactor binary sensor

* Make import relative

* Style fix

* Update custom_components/tahoma/__init__.py

Co-authored-by: Thibaut <thibaut@etienne.pw>

* Add extra binary sensors

* Clean for final PR

* Feedback applied

Co-authored-by: Thibaut <thibaut@etienne.pw>
  • Loading branch information
iMicknl and tetienne committed Aug 10, 2021
1 parent da5b26e commit e7a4554
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 100 deletions.
9 changes: 8 additions & 1 deletion custom_components/tahoma/__init__.py
Expand Up @@ -6,6 +6,7 @@
import logging

from aiohttp import ClientError, ServerDisconnectedError
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.scene import DOMAIN as SCENE
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_EXCLUDE, CONF_PASSWORD, CONF_SOURCE, CONF_USERNAME
Expand Down Expand Up @@ -186,6 +187,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"update_listener": entry.add_update_listener(update_listener),
}

# Map Overkiz device to Home Assistant platform
for device in tahoma_coordinator.data.values():
platform = TAHOMA_DEVICE_TO_PLATFORM.get(
device.widget
Expand All @@ -202,7 +204,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if device.widget == HOMEKIT_STACK:
print_homekit_setup_code(device)

for platform in platforms:
supported_platforms = set(platforms.keys())

# Sensor and Binary Sensor will be added dynamically, based on the device states
supported_platforms.add(BINARY_SENSOR)

for platform in supported_platforms:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
Expand Down
194 changes: 106 additions & 88 deletions custom_components/tahoma/binary_sensor.py
@@ -1,64 +1,94 @@
"""Support for TaHoma binary sensors."""
from typing import Optional

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable

from homeassistant.components import binary_sensor
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_MOTION,
DEVICE_CLASS_OCCUPANCY,
DEVICE_CLASS_OPENING,
DEVICE_CLASS_SMOKE,
DOMAIN as BINARY_SENSOR,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import DOMAIN
from .coordinator import TahomaDataUpdateCoordinator
from .tahoma_entity import TahomaEntity

CORE_ASSEMBLY_STATE = "core:AssemblyState"
CORE_BUTTON_STATE = "core:ButtonState"
CORE_CONTACT_STATE = "core:ContactState"
CORE_GAS_DETECTION_STATE = "core:GasDetectionState"
CORE_OCCUPANCY_STATE = "core:OccupancyState"
CORE_OPENING_STATE = "core:OpeningState"
CORE_OPEN_CLOSED_TILT_STATE = "core:OpenClosedTiltState"
CORE_RAIN_STATE = "core:RainState"
CORE_SMOKE_STATE = "core:SmokeState"
CORE_THREE_WAY_HANDLE_DIRECTION_STATE = "core:ThreeWayHandleDirectionState"
CORE_VIBRATION_STATE = "core:VibrationState"
CORE_WATER_DETECTION_STATE = "core:WaterDetectionState"

DEVICE_CLASS_BUTTON = "button"
DEVICE_CLASS_GAS = "gas"
DEVICE_CLASS_RAIN = "rain"
DEVICE_CLASS_WATER = "water"

ICON_WATER = "mdi:water"
ICON_WATER_OFF = "mdi:water-off"
ICON_WAVES = "mdi:waves"
ICON_WEATHER_RAINY = "mdi:weather-rainy"

IO_VIBRATION_DETECTED_STATE = "io:VibrationDetectedState"

STATE_OPEN = "open"
STATE_PERSON_INSIDE = "personInside"
STATE_DETECTED = "detected"
STATE_PRESSED = "pressed"

TAHOMA_BINARY_SENSOR_DEVICE_CLASSES = {
"AirFlowSensor": DEVICE_CLASS_GAS,
"CarButtonSensor": DEVICE_CLASS_BUTTON,
"ContactSensor": DEVICE_CLASS_OPENING,
"MotionSensor": DEVICE_CLASS_MOTION,
"OccupancySensor": DEVICE_CLASS_OCCUPANCY,
"RainSensor": DEVICE_CLASS_RAIN,
"SirenStatus": DEVICE_CLASS_OPENING,
"SmokeSensor": DEVICE_CLASS_SMOKE,
"WaterDetectionSensor": DEVICE_CLASS_WATER,
"WaterSensor": DEVICE_CLASS_WATER,
"WindowHandle": DEVICE_CLASS_OPENING,
}


@dataclass
class OverkizBinarySensorDescription(BinarySensorEntityDescription):
"""Class to describe a Overkiz binary sensor."""

is_on: Callable[[Any], Any] = lambda state: state


BINARY_SENSOR_DESCRIPTIONS = [
# RainSensor/RainSensor
OverkizBinarySensorDescription(
key="core:RainState",
name="Rain",
icon="mdi:weather-rainy",
is_on=lambda state: state == STATE_DETECTED,
),
# SmokeSensor/SmokeSensor
OverkizBinarySensorDescription(
key="core:SmokeState",
name="Smoke",
device_class=binary_sensor.DEVICE_CLASS_SMOKE,
is_on=lambda state: state == STATE_DETECTED,
),
# WaterSensor/WaterDetectionSensor
OverkizBinarySensorDescription(
key="core:WaterDetectionState",
name="Water",
icon="mdi:water",
is_on=lambda state: state == STATE_DETECTED,
),
# AirSensor/AirFlowSensor
OverkizBinarySensorDescription(
key="core:GasDetectionState",
name="Gas",
device_class=binary_sensor.DEVICE_CLASS_GAS,
is_on=lambda state: state == STATE_DETECTED,
),
# OccupancySensor/OccupancySensor
# OccupancySensor/MotionSensor
OverkizBinarySensorDescription(
key="core:OccupancyState",
name="Occupancy",
device_class=binary_sensor.DEVICE_CLASS_OCCUPANCY,
is_on=lambda state: state == STATE_PERSON_INSIDE,
),
# ContactSensor/WindowWithTiltSensor
OverkizBinarySensorDescription(
key="core:VibrationState",
name="Vibration",
device_class=binary_sensor.DEVICE_CLASS_VIBRATION,
is_on=lambda state: state == STATE_DETECTED,
),
# ContactSensor/ContactSensor
OverkizBinarySensorDescription(
key="core:ContactState",
name="Contact",
device_class=binary_sensor.DEVICE_CLASS_DOOR,
is_on=lambda state: state == STATE_OPEN,
),
# Unknown
OverkizBinarySensorDescription(
key="io:VibrationDetectedState",
name="Vibration",
device_class=binary_sensor.DEVICE_CLASS_VIBRATION,
is_on=lambda state: state == STATE_DETECTED,
),
]


async def async_setup_entry(
Expand All @@ -69,55 +99,43 @@ async def async_setup_entry(
"""Set up the TaHoma sensors from a config entry."""
data = hass.data[DOMAIN][entry.entry_id]
coordinator = data["coordinator"]
entities = []

key_supported_states = {
description.key: description for description in BINARY_SENSOR_DESCRIPTIONS
}

for device in coordinator.data.values():
for state in device.states:
description = key_supported_states.get(state.name)

if description:
entities.append(
TahomaBinarySensor(
device.deviceurl,
coordinator,
description,
)
)

entities = [
TahomaBinarySensor(device.deviceurl, coordinator)
for device in data["platforms"][BINARY_SENSOR]
]
async_add_entities(entities)


class TahomaBinarySensor(TahomaEntity, BinarySensorEntity):
"""Representation of a TaHoma Binary Sensor."""

def __init__(
self,
device_url: str,
coordinator: TahomaDataUpdateCoordinator,
description: OverkizBinarySensorDescription,
):
"""Initialize the device."""
super().__init__(device_url, coordinator)
self.entity_description = description

@property
def is_on(self):
"""Return the state of the sensor."""

return (
self.select_state(
CORE_ASSEMBLY_STATE,
CORE_BUTTON_STATE,
CORE_CONTACT_STATE,
CORE_GAS_DETECTION_STATE,
CORE_OCCUPANCY_STATE,
CORE_OPENING_STATE,
CORE_OPEN_CLOSED_TILT_STATE,
CORE_RAIN_STATE,
CORE_SMOKE_STATE,
CORE_THREE_WAY_HANDLE_DIRECTION_STATE,
CORE_VIBRATION_STATE,
CORE_WATER_DETECTION_STATE,
IO_VIBRATION_DETECTED_STATE,
)
in [STATE_OPEN, STATE_PERSON_INSIDE, STATE_DETECTED, STATE_PRESSED]
)

@property
def device_class(self):
"""Return the class of the device."""
return TAHOMA_BINARY_SENSOR_DEVICE_CLASSES.get(
self.device.widget
) or TAHOMA_BINARY_SENSOR_DEVICE_CLASSES.get(self.device.ui_class)

@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 ICON_WATER
return ICON_WATER_OFF

icons = {DEVICE_CLASS_GAS: ICON_WAVES, DEVICE_CLASS_RAIN: ICON_WEATHER_RAINY}

return icons.get(self.device_class)
state = self.device.states[self.entity_description.key]
return self.entity_description.is_on(state)
20 changes: 9 additions & 11 deletions custom_components/tahoma/const.py
@@ -1,6 +1,5 @@
"""Constants for the TaHoma integration."""
from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_CONTROL_PANEL
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.climate import DOMAIN as CLIMATE
from homeassistant.components.cover import DOMAIN as COVER
from homeassistant.components.light import DOMAIN as LIGHT
Expand All @@ -20,12 +19,19 @@
IGNORED_TAHOMA_DEVICES = [
"ProtocolGateway",
"Pod",
# entries mapped to Binary Sensor based on available states
"AirFlowSensor", # widgetName, uiClass is AirSensor (sensor)
"ContactSensor",
"MotionSensor",
"OccupancySensor",
"RainSensor",
"SmokeSensor",
"WaterDetectionSensor", # widgetName, uiClass is HumiditySensor (sensor)
]

# Used to map the Somfy widget and ui_class to the Home Assistant platform
TAHOMA_DEVICE_TO_PLATFORM = {
"AdjustableSlatsRollerShutter": COVER,
"AirFlowSensor": BINARY_SENSOR, # widgetName, uiClass is AirSensor (sensor)
"AirSensor": SENSOR,
"Alarm": ALARM_CONTROL_PANEL,
"AtlanticElectricalHeater": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported)
Expand All @@ -35,9 +41,7 @@
"AtlanticPassAPCHeatingAndCoolingZone": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported)
"AtlanticPassAPCZoneControl": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported)
"Awning": COVER,
"CarButtonSensor": BINARY_SENSOR,
"ConsumptionSensor": SENSOR,
"ContactSensor": BINARY_SENSOR,
"Curtain": COVER,
"DimmerExteriorHeating": CLIMATE, # widgetName, uiClass is ExteriorHeatingSystem (not supported)
"DomesticHotWaterProduction": WATER_HEATER, # widgetName, uiClass is WaterHeatingSystem (not supported)
Expand All @@ -58,19 +62,15 @@
"HumiditySensor": SENSOR,
"Light": LIGHT,
"LightSensor": SENSOR,
"MotionSensor": BINARY_SENSOR,
"MyFoxSecurityCamera": COVER, # widgetName, uiClass is Camera (not supported)
"OccupancySensor": BINARY_SENSOR,
"OnOff": SWITCH,
"Pergola": COVER,
"RainSensor": BINARY_SENSOR,
"RollerShutter": COVER,
"RTSGeneric": COVER, # widgetName, uiClass is Generic (not supported)
"Screen": COVER,
"Shutter": COVER,
"Siren": SWITCH,
"SirenStatus": BINARY_SENSOR, # widgetName, uiClass is Siren (switch)
"SmokeSensor": BINARY_SENSOR,
"SirenStatus": None, # widgetName, uiClass is Siren (switch)
"SomfyThermostat": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported)
"StatelessExteriorHeating": CLIMATE, # widgetName, uiClass is ExteriorHeatingSystem.
"SunIntensitySensor": SENSOR,
Expand All @@ -80,12 +80,10 @@
"TemperatureSensor": SENSOR,
"ThermalEnergySensor": SENSOR,
"VenetianBlind": COVER,
"WaterDetectionSensor": BINARY_SENSOR, # widgetName, uiClass is HumiditySensor (sensor)
"WaterSensor": SENSOR,
"WeatherSensor": SENSOR,
"WindSensor": SENSOR,
"Window": COVER,
"WindowHandle": BINARY_SENSOR,
}

CORE_ON_OFF_STATE = "core:OnOffState"
Expand Down

0 comments on commit e7a4554

Please sign in to comment.