diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 7c3760653e8..2d55cd9ddfb 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -1,25 +1,287 @@ """Support for OpenTherm Gateway binary sensors.""" -import logging +from dataclasses import dataclass -from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorEntity +from pyotgw import vars as gw_vars + +from homeassistant.components.binary_sensor import ( + ENTITY_ID_FORMAT, + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DOMAIN -from .const import ( - BINARY_SENSOR_INFO, - DATA_GATEWAYS, - DATA_OPENTHERM_GW, - TRANSLATE_SOURCE, -) +from . import OpenThermGatewayDevice +from .const import DATA_GATEWAYS, DATA_OPENTHERM_GW +from .entity import OpenThermEntity, OpenThermEntityDescription -_LOGGER = logging.getLogger(__name__) + +@dataclass(frozen=True, kw_only=True) +class OpenThermBinarySensorEntityDescription( + BinarySensorEntityDescription, OpenThermEntityDescription +): + """Describes opentherm_gw binary sensor entity.""" + + +BINARY_SENSOR_INFO: tuple[ + tuple[list[str], OpenThermBinarySensorEntityDescription], ... +] = ( + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_MASTER_CH_ENABLED, + friendly_name_format="Thermostat Central Heating {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_MASTER_DHW_ENABLED, + friendly_name_format="Thermostat Hot Water {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_MASTER_COOLING_ENABLED, + friendly_name_format="Thermostat Cooling {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_MASTER_OTC_ENABLED, + friendly_name_format="Thermostat Outside Temperature Correction {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_MASTER_CH2_ENABLED, + friendly_name_format="Thermostat Central Heating 2 {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_FAULT_IND, + friendly_name_format="Boiler Fault {}", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_CH_ACTIVE, + friendly_name_format="Boiler Central Heating {}", + device_class=BinarySensorDeviceClass.HEAT, + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_DHW_ACTIVE, + friendly_name_format="Boiler Hot Water {}", + device_class=BinarySensorDeviceClass.HEAT, + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_FLAME_ON, + friendly_name_format="Boiler Flame {}", + device_class=BinarySensorDeviceClass.HEAT, + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_COOLING_ACTIVE, + friendly_name_format="Boiler Cooling {}", + device_class=BinarySensorDeviceClass.COLD, + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_CH2_ACTIVE, + friendly_name_format="Boiler Central Heating 2 {}", + device_class=BinarySensorDeviceClass.HEAT, + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_DIAG_IND, + friendly_name_format="Boiler Diagnostics {}", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_DHW_PRESENT, + friendly_name_format="Boiler Hot Water Present {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_CONTROL_TYPE, + friendly_name_format="Boiler Control Type {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_COOLING_SUPPORTED, + friendly_name_format="Boiler Cooling Support {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_DHW_CONFIG, + friendly_name_format="Boiler Hot Water Configuration {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP, + friendly_name_format="Boiler Pump Commands Support {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_CH2_PRESENT, + friendly_name_format="Boiler Central Heating 2 Present {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_SERVICE_REQ, + friendly_name_format="Boiler Service Required {}", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_REMOTE_RESET, + friendly_name_format="Boiler Remote Reset Support {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_LOW_WATER_PRESS, + friendly_name_format="Boiler Low Water Pressure {}", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_GAS_FAULT, + friendly_name_format="Boiler Gas Fault {}", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_AIR_PRESS_FAULT, + friendly_name_format="Boiler Air Pressure Fault {}", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_SLAVE_WATER_OVERTEMP, + friendly_name_format="Boiler Water Overtemperature {}", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_REMOTE_TRANSFER_DHW, + friendly_name_format="Remote Hot Water Setpoint Transfer Support {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_REMOTE_TRANSFER_MAX_CH, + friendly_name_format="Remote Maximum Central Heating Setpoint Write Support {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_REMOTE_RW_DHW, + friendly_name_format="Remote Hot Water Setpoint Write Support {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_REMOTE_RW_MAX_CH, + friendly_name_format="Remote Central Heating Setpoint Write Support {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_ROVRD_MAN_PRIO, + friendly_name_format="Remote Override Manual Change Priority {}", + ), + ), + ( + [gw_vars.BOILER, gw_vars.THERMOSTAT], + OpenThermBinarySensorEntityDescription( + key=gw_vars.DATA_ROVRD_AUTO_PRIO, + friendly_name_format="Remote Override Program Change Priority {}", + ), + ), + ( + [gw_vars.OTGW], + OpenThermBinarySensorEntityDescription( + key=gw_vars.OTGW_GPIO_A_STATE, + friendly_name_format="Gateway GPIO A {}", + ), + ), + ( + [gw_vars.OTGW], + OpenThermBinarySensorEntityDescription( + key=gw_vars.OTGW_GPIO_B_STATE, + friendly_name_format="Gateway GPIO B {}", + ), + ), + ( + [gw_vars.OTGW], + OpenThermBinarySensorEntityDescription( + key=gw_vars.OTGW_IGNORE_TRANSITIONS, + friendly_name_format="Gateway Ignore Transitions {}", + ), + ), + ( + [gw_vars.OTGW], + OpenThermBinarySensorEntityDescription( + key=gw_vars.OTGW_OVRD_HB, + friendly_name_format="Gateway Override High Byte {}", + ), + ), +) async def async_setup_entry( @@ -31,65 +293,35 @@ async def async_setup_entry( gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] async_add_entities( - OpenThermBinarySensor( - gw_dev, - var, - source, - info[0], - info[1], - ) - for var, info in BINARY_SENSOR_INFO.items() - for source in info[2] + OpenThermBinarySensor(gw_dev, source, description) + for sources, description in BINARY_SENSOR_INFO + for source in sources ) -class OpenThermBinarySensor(BinarySensorEntity): +class OpenThermBinarySensor(OpenThermEntity, BinarySensorEntity): """Represent an OpenTherm Gateway binary sensor.""" - _attr_should_poll = False - _attr_entity_registry_enabled_default = False - _attr_available = False + entity_description: OpenThermBinarySensorEntityDescription - def __init__(self, gw_dev, var, source, device_class, friendly_name_format): + def __init__( + self, + gw_dev: OpenThermGatewayDevice, + source: str, + description: OpenThermBinarySensorEntityDescription, + ) -> None: """Initialize the binary sensor.""" self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, f"{var}_{source}_{gw_dev.gw_id}", hass=gw_dev.hass + ENTITY_ID_FORMAT, + f"{description.key}_{source}_{gw_dev.gw_id}", + hass=gw_dev.hass, ) - self._gateway = gw_dev - self._var = var - self._source = source - self._attr_device_class = device_class - if TRANSLATE_SOURCE[source] is not None: - friendly_name_format = ( - f"{friendly_name_format} ({TRANSLATE_SOURCE[source]})" - ) - self._attr_name = friendly_name_format.format(gw_dev.name) - self._unsub_updates = None - self._attr_unique_id = f"{gw_dev.gw_id}-{source}-{var}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, gw_dev.gw_id)}, - manufacturer="Schelte Bron", - model="OpenTherm Gateway", - name=gw_dev.name, - sw_version=gw_dev.gw_version, - ) - - async def async_added_to_hass(self) -> None: - """Subscribe to updates from the component.""" - _LOGGER.debug("Added OpenTherm Gateway binary sensor %s", self._attr_name) - self._unsub_updates = async_dispatcher_connect( - self.hass, self._gateway.update_signal, self.receive_report - ) - - async def async_will_remove_from_hass(self) -> None: - """Unsubscribe from updates from the component.""" - _LOGGER.debug("Removing OpenTherm Gateway binary sensor %s", self._attr_name) - self._unsub_updates() + super().__init__(gw_dev, source, description) @callback - def receive_report(self, status): + def receive_report(self, status: dict[str, dict]) -> None: """Handle status updates from the component.""" self._attr_available = self._gateway.connected - state = status[self._source].get(self._var) + state = status[self._source].get(self.entity_description.key) self._attr_is_on = None if state is None else bool(state) self.async_write_ha_state() diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 6b0a27aec92..1d007d86181 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -4,7 +4,6 @@ from __future__ import annotations import pyotgw.vars as gw_vars -from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( PERCENTAGE, @@ -57,168 +56,6 @@ TRANSLATE_SOURCE = { SENSOR_FLOAT_SUGGESTED_DISPLAY_PRECISION = 1 -BINARY_SENSOR_INFO: dict[str, list] = { - # [device_class, friendly_name format, [status source, ...]] - gw_vars.DATA_MASTER_CH_ENABLED: [ - None, - "Thermostat Central Heating {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_MASTER_DHW_ENABLED: [ - None, - "Thermostat Hot Water {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_MASTER_COOLING_ENABLED: [ - None, - "Thermostat Cooling {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_MASTER_OTC_ENABLED: [ - None, - "Thermostat Outside Temperature Correction {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_MASTER_CH2_ENABLED: [ - None, - "Thermostat Central Heating 2 {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_FAULT_IND: [ - BinarySensorDeviceClass.PROBLEM, - "Boiler Fault {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_CH_ACTIVE: [ - BinarySensorDeviceClass.HEAT, - "Boiler Central Heating {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_DHW_ACTIVE: [ - BinarySensorDeviceClass.HEAT, - "Boiler Hot Water {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_FLAME_ON: [ - BinarySensorDeviceClass.HEAT, - "Boiler Flame {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_COOLING_ACTIVE: [ - BinarySensorDeviceClass.COLD, - "Boiler Cooling {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_CH2_ACTIVE: [ - BinarySensorDeviceClass.HEAT, - "Boiler Central Heating 2 {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_DIAG_IND: [ - BinarySensorDeviceClass.PROBLEM, - "Boiler Diagnostics {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_DHW_PRESENT: [ - None, - "Boiler Hot Water Present {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_CONTROL_TYPE: [ - None, - "Boiler Control Type {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_COOLING_SUPPORTED: [ - None, - "Boiler Cooling Support {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_DHW_CONFIG: [ - None, - "Boiler Hot Water Configuration {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP: [ - None, - "Boiler Pump Commands Support {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_CH2_PRESENT: [ - None, - "Boiler Central Heating 2 Present {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_SERVICE_REQ: [ - BinarySensorDeviceClass.PROBLEM, - "Boiler Service Required {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_REMOTE_RESET: [ - None, - "Boiler Remote Reset Support {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_LOW_WATER_PRESS: [ - BinarySensorDeviceClass.PROBLEM, - "Boiler Low Water Pressure {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_GAS_FAULT: [ - BinarySensorDeviceClass.PROBLEM, - "Boiler Gas Fault {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_AIR_PRESS_FAULT: [ - BinarySensorDeviceClass.PROBLEM, - "Boiler Air Pressure Fault {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_SLAVE_WATER_OVERTEMP: [ - BinarySensorDeviceClass.PROBLEM, - "Boiler Water Overtemperature {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_REMOTE_TRANSFER_DHW: [ - None, - "Remote Hot Water Setpoint Transfer Support {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_REMOTE_TRANSFER_MAX_CH: [ - None, - "Remote Maximum Central Heating Setpoint Write Support {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_REMOTE_RW_DHW: [ - None, - "Remote Hot Water Setpoint Write Support {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_REMOTE_RW_MAX_CH: [ - None, - "Remote Central Heating Setpoint Write Support {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_ROVRD_MAN_PRIO: [ - None, - "Remote Override Manual Change Priority {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.DATA_ROVRD_AUTO_PRIO: [ - None, - "Remote Override Program Change Priority {}", - [gw_vars.BOILER, gw_vars.THERMOSTAT], - ], - gw_vars.OTGW_GPIO_A_STATE: [None, "Gateway GPIO A {}", [gw_vars.OTGW]], - gw_vars.OTGW_GPIO_B_STATE: [None, "Gateway GPIO B {}", [gw_vars.OTGW]], - gw_vars.OTGW_IGNORE_TRANSITIONS: [ - None, - "Gateway Ignore Transitions {}", - [gw_vars.OTGW], - ], - gw_vars.OTGW_OVRD_HB: [None, "Gateway Override High Byte {}", [gw_vars.OTGW]], -} - SENSOR_INFO: dict[str, list] = { # [device_class, unit, friendly_name, suggested_display_precision, [status source, ...]] gw_vars.DATA_CONTROL_SETPOINT: [ diff --git a/homeassistant/components/opentherm_gw/entity.py b/homeassistant/components/opentherm_gw/entity.py new file mode 100644 index 00000000000..dd023896f70 --- /dev/null +++ b/homeassistant/components/opentherm_gw/entity.py @@ -0,0 +1,68 @@ +"""Common opentherm_gw entity properties.""" + +import logging + +from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity, EntityDescription + +from . import OpenThermGatewayDevice +from .const import DOMAIN, TRANSLATE_SOURCE + +_LOGGER = logging.getLogger(__name__) + + +class OpenThermEntityDescription(EntityDescription): + """Describe common opentherm_gw entity properties.""" + + friendly_name_format: str + + +class OpenThermEntity(Entity): + """Represent an OpenTherm Gateway entity.""" + + _attr_should_poll = False + _attr_entity_registry_enabled_default = False + _attr_available = False + entity_description: OpenThermEntityDescription + + def __init__( + self, + gw_dev: OpenThermGatewayDevice, + source: str, + description: OpenThermEntityDescription, + ) -> None: + """Initialize the entity.""" + self.entity_description = description + self._gateway = gw_dev + self._source = source + friendly_name_format = ( + f"{description.friendly_name_format} ({TRANSLATE_SOURCE[source]})" + if TRANSLATE_SOURCE[source] is not None + else description.friendly_name_format + ) + self._attr_name = friendly_name_format.format(gw_dev.name) + self._attr_unique_id = f"{gw_dev.gw_id}-{source}-{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, gw_dev.gw_id)}, + manufacturer="Schelte Bron", + model="OpenTherm Gateway", + name=gw_dev.name, + sw_version=gw_dev.gw_version, + ) + + async def async_added_to_hass(self) -> None: + """Subscribe to updates from the component.""" + _LOGGER.debug("Added OpenTherm Gateway entity %s", self._attr_name) + self.async_on_remove( + async_dispatcher_connect( + self.hass, self._gateway.update_signal, self.receive_report + ) + ) + + @callback + def receive_report(self, status: dict[str, dict]) -> None: + """Handle status updates from the component.""" + # Must be implemented at the platform level. + raise NotImplementedError