Split opentherm_gw entities between different devices (#124869)

* * Add migration from single device to multiple devices, removing all old entities
* Create new devices for Boiler and Thermostat
* Add classes for new entities based on the new devices

* Split binary_sensor entities into devices

* Split sensor entities into different devices

* Move climate entity to thermostat device

* Fix climate entity away mode

* Fix translation placeholders

* Allow sensor values with capital letters

* * Add EntityCategory
* Update and add device_classes

* Fix translation keys

* Fix climate entity category

* Update tests

* Handle `available` property in `entity.py`

* Improve GPIO state binary_sensor translations

* Fix: Updates are already subscribed to in the base entity

* Remove entity_id generation from sensor and binary_sensor entities

* * Use _attr_name on climate class instead of through entity_description
* Add type hints

* Rewrite to derive entities for all OpenTherm devices from a single base class

* Improve type annotations

* Use OpenThermDataSource to access status dict

* Move entity_category from entity_description to _attr_entity_category

* Move entity descriptions with the same translation_key closer together

* Update tests

* Add device migration test

* * Add missing sensors and binary_sensors back
* Improve migration, do not delete old entities from registry

* Add comments for migration period

* Use single lists for entity descriptions

* Avoid changing sensor values, remove translations

* * Import only required class from pyotgw
* Update tests
This commit is contained in:
mvn23 2024-09-01 13:28:08 +02:00 committed by GitHub
parent 12336f5c15
commit 2f7a396778
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1702 additions and 955 deletions

View File

@ -4,7 +4,7 @@ import asyncio
from datetime import date, datetime
import logging
import pyotgw
from pyotgw import OpenThermGateway
import pyotgw.vars as gw_vars
from serial import SerialException
import voluptuous as vol
@ -59,6 +59,8 @@ from .const import (
SERVICE_SET_MAX_MOD,
SERVICE_SET_OAT,
SERVICE_SET_SB_TEMP,
OpenThermDataSource,
OpenThermDeviceIdentifier,
)
_LOGGER = logging.getLogger(__name__)
@ -113,6 +115,23 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
del migrate_options[CONF_PRECISION]
hass.config_entries.async_update_entry(config_entry, options=migrate_options)
# Migration can be removed in 2025.4.0
dev_reg = dr.async_get(hass)
if (
migrate_device := dev_reg.async_get_device(
{(DOMAIN, config_entry.data[CONF_ID])}
)
) is not None:
dev_reg.async_update_device(
migrate_device.id,
new_identifiers={
(
DOMAIN,
f"{config_entry.data[CONF_ID]}-{OpenThermDeviceIdentifier.GATEWAY}",
)
},
)
config_entry.add_update_listener(options_updated)
try:
@ -427,10 +446,9 @@ class OpenThermGatewayHub:
self.name = config_entry.data[CONF_NAME]
self.climate_config = config_entry.options
self.config_entry_id = config_entry.entry_id
self.status = gw_vars.DEFAULT_STATUS
self.update_signal = f"{DATA_OPENTHERM_GW}_{self.hub_id}_update"
self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.hub_id}_options_update"
self.gateway = pyotgw.OpenThermGateway()
self.gateway = OpenThermGateway()
self.gw_version = None
async def cleanup(self, event=None) -> None:
@ -441,11 +459,11 @@ class OpenThermGatewayHub:
async def connect_and_subscribe(self) -> None:
"""Connect to serial device and subscribe report handler."""
self.status = await self.gateway.connect(self.device_path)
if not self.status:
status = await self.gateway.connect(self.device_path)
if not status:
await self.cleanup()
raise ConnectionError
version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
version_string = status[OpenThermDataSource.GATEWAY].get(gw_vars.OTGW_ABOUT)
self.gw_version = version_string[18:] if version_string else None
_LOGGER.debug(
"Connected to OpenTherm Gateway %s at %s", self.gw_version, self.device_path
@ -453,22 +471,69 @@ class OpenThermGatewayHub:
dev_reg = dr.async_get(self.hass)
gw_dev = dev_reg.async_get_or_create(
config_entry_id=self.config_entry_id,
identifiers={(DOMAIN, self.hub_id)},
name=self.name,
identifiers={
(DOMAIN, f"{self.hub_id}-{OpenThermDeviceIdentifier.GATEWAY}")
},
manufacturer="Schelte Bron",
model="OpenTherm Gateway",
translation_key="gateway_device",
sw_version=self.gw_version,
)
if gw_dev.sw_version != self.gw_version:
dev_reg.async_update_device(gw_dev.id, sw_version=self.gw_version)
boiler_device = dev_reg.async_get_or_create(
config_entry_id=self.config_entry_id,
identifiers={(DOMAIN, f"{self.hub_id}-{OpenThermDeviceIdentifier.BOILER}")},
translation_key="boiler_device",
)
thermostat_device = dev_reg.async_get_or_create(
config_entry_id=self.config_entry_id,
identifiers={
(DOMAIN, f"{self.hub_id}-{OpenThermDeviceIdentifier.THERMOSTAT}")
},
translation_key="thermostat_device",
)
self.hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.cleanup)
async def handle_report(status):
"""Handle reports from the OpenTherm Gateway."""
_LOGGER.debug("Received report: %s", status)
self.status = status
async_dispatcher_send(self.hass, self.update_signal, status)
dev_reg.async_update_device(
boiler_device.id,
manufacturer=status[OpenThermDataSource.BOILER].get(
gw_vars.DATA_SLAVE_MEMBERID
),
model_id=status[OpenThermDataSource.BOILER].get(
gw_vars.DATA_SLAVE_PRODUCT_TYPE
),
hw_version=status[OpenThermDataSource.BOILER].get(
gw_vars.DATA_SLAVE_PRODUCT_VERSION
),
sw_version=status[OpenThermDataSource.BOILER].get(
gw_vars.DATA_SLAVE_OT_VERSION
),
)
dev_reg.async_update_device(
thermostat_device.id,
manufacturer=status[OpenThermDataSource.THERMOSTAT].get(
gw_vars.DATA_MASTER_MEMBERID
),
model_id=status[OpenThermDataSource.THERMOSTAT].get(
gw_vars.DATA_MASTER_PRODUCT_TYPE
),
hw_version=status[OpenThermDataSource.THERMOSTAT].get(
gw_vars.DATA_MASTER_PRODUCT_VERSION
),
sw_version=status[OpenThermDataSource.THERMOSTAT].get(
gw_vars.DATA_MASTER_OT_VERSION
),
)
self.gateway.subscribe(handle_report)
@property

View File

@ -5,281 +5,387 @@ from dataclasses import dataclass
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.const import CONF_ID, EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import OpenThermGatewayHub
from .const import DATA_GATEWAYS, DATA_OPENTHERM_GW
from .const import (
BOILER_DEVICE_DESCRIPTION,
DATA_GATEWAYS,
DATA_OPENTHERM_GW,
GATEWAY_DEVICE_DESCRIPTION,
THERMOSTAT_DEVICE_DESCRIPTION,
OpenThermDataSource,
)
from .entity import OpenThermEntity, OpenThermEntityDescription
@dataclass(frozen=True, kw_only=True)
class OpenThermBinarySensorEntityDescription(
BinarySensorEntityDescription, OpenThermEntityDescription
OpenThermEntityDescription, BinarySensorEntityDescription
):
"""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 {}",
),
BINARY_SENSOR_DESCRIPTIONS: tuple[OpenThermBinarySensorEntityDescription, ...] = (
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_FAULT_IND,
translation_key="fault_indication",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_DHW_ENABLED,
friendly_name_format="Thermostat Hot Water {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_CH_ACTIVE,
translation_key="central_heating_n",
translation_placeholders={"circuit_number": "1"},
device_class=BinarySensorDeviceClass.RUNNING,
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_COOLING_ENABLED,
friendly_name_format="Thermostat Cooling {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_CH2_ACTIVE,
translation_key="central_heating_n",
translation_placeholders={"circuit_number": "2"},
device_class=BinarySensorDeviceClass.RUNNING,
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_OTC_ENABLED,
friendly_name_format="Thermostat Outside Temperature Correction {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_DHW_ACTIVE,
translation_key="hot_water",
device_class=BinarySensorDeviceClass.RUNNING,
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_CH2_ENABLED,
friendly_name_format="Thermostat Central Heating 2 {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_FLAME_ON,
translation_key="flame",
device_class=BinarySensorDeviceClass.HEAT,
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_FAULT_IND,
friendly_name_format="Boiler Fault {}",
device_class=BinarySensorDeviceClass.PROBLEM,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_COOLING_ACTIVE,
translation_key="cooling",
device_class=BinarySensorDeviceClass.RUNNING,
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_CH_ACTIVE,
friendly_name_format="Boiler Central Heating {}",
device_class=BinarySensorDeviceClass.HEAT,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_DIAG_IND,
translation_key="diagnostic_indication",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_DHW_ACTIVE,
friendly_name_format="Boiler Hot Water {}",
device_class=BinarySensorDeviceClass.HEAT,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_DHW_PRESENT,
translation_key="supports_hot_water",
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_FLAME_ON,
friendly_name_format="Boiler Flame {}",
device_class=BinarySensorDeviceClass.HEAT,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_CONTROL_TYPE,
translation_key="control_type",
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_COOLING_ACTIVE,
friendly_name_format="Boiler Cooling {}",
device_class=BinarySensorDeviceClass.COLD,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_COOLING_SUPPORTED,
translation_key="supports_cooling",
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[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,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_DHW_CONFIG,
translation_key="hot_water_config",
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_DIAG_IND,
friendly_name_format="Boiler Diagnostics {}",
device_class=BinarySensorDeviceClass.PROBLEM,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP,
translation_key="supports_pump_control",
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_DHW_PRESENT,
friendly_name_format="Boiler Hot Water Present {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_CH2_PRESENT,
translation_key="supports_ch_2",
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_CONTROL_TYPE,
friendly_name_format="Boiler Control Type {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_SERVICE_REQ,
translation_key="service_required",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_COOLING_SUPPORTED,
friendly_name_format="Boiler Cooling Support {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_REMOTE_RESET,
translation_key="supports_remote_reset",
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_DHW_CONFIG,
friendly_name_format="Boiler Hot Water Configuration {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_LOW_WATER_PRESS,
translation_key="low_water_pressure",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP,
friendly_name_format="Boiler Pump Commands Support {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_GAS_FAULT,
translation_key="gas_fault",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_CH2_PRESENT,
friendly_name_format="Boiler Central Heating 2 Present {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_AIR_PRESS_FAULT,
translation_key="air_pressure_fault",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_SERVICE_REQ,
friendly_name_format="Boiler Service Required {}",
device_class=BinarySensorDeviceClass.PROBLEM,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_WATER_OVERTEMP,
translation_key="water_overtemperature",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_REMOTE_RESET,
friendly_name_format="Boiler Remote Reset Support {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_REMOTE_TRANSFER_MAX_CH,
translation_key="supports_central_heating_setpoint_transfer",
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[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,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_REMOTE_RW_MAX_CH,
translation_key="supports_central_heating_setpoint_writing",
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_GAS_FAULT,
friendly_name_format="Boiler Gas Fault {}",
device_class=BinarySensorDeviceClass.PROBLEM,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_REMOTE_TRANSFER_DHW,
translation_key="supports_hot_water_setpoint_transfer",
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[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,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_REMOTE_RW_DHW,
translation_key="supports_hot_water_setpoint_writing",
device_description=BOILER_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_WATER_OVERTEMP,
friendly_name_format="Boiler Water Overtemperature {}",
device_class=BinarySensorDeviceClass.PROBLEM,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.OTGW_GPIO_A_STATE,
translation_key="gpio_state_n",
translation_placeholders={"gpio_id": "A"},
device_description=GATEWAY_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_REMOTE_TRANSFER_DHW,
friendly_name_format="Remote Hot Water Setpoint Transfer Support {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.OTGW_GPIO_B_STATE,
translation_key="gpio_state_n",
translation_placeholders={"gpio_id": "B"},
device_description=GATEWAY_DEVICE_DESCRIPTION,
),
(
[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 {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.OTGW_IGNORE_TRANSITIONS,
translation_key="ignore_transitions",
device_description=GATEWAY_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_REMOTE_RW_DHW,
friendly_name_format="Remote Hot Water Setpoint Write Support {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.OTGW_OVRD_HB,
translation_key="override_high_byte",
device_description=GATEWAY_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_REMOTE_RW_MAX_CH,
friendly_name_format="Remote Central Heating Setpoint Write Support {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_CH_ENABLED,
translation_key="central_heating_n",
translation_placeholders={"circuit_number": "1"},
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_ROVRD_MAN_PRIO,
friendly_name_format="Remote Override Manual Change Priority {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_CH2_ENABLED,
translation_key="central_heating_n",
translation_placeholders={"circuit_number": "2"},
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
(
[gw_vars.BOILER, gw_vars.THERMOSTAT],
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_ROVRD_AUTO_PRIO,
friendly_name_format="Remote Override Program Change Priority {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_DHW_ENABLED,
translation_key="hot_water",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
(
[gw_vars.OTGW],
OpenThermBinarySensorEntityDescription(
key=gw_vars.OTGW_GPIO_A_STATE,
friendly_name_format="Gateway GPIO A {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_COOLING_ENABLED,
translation_key="cooling",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
(
[gw_vars.OTGW],
OpenThermBinarySensorEntityDescription(
key=gw_vars.OTGW_GPIO_B_STATE,
friendly_name_format="Gateway GPIO B {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_OTC_ENABLED,
translation_key="outside_temp_correction",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
(
[gw_vars.OTGW],
OpenThermBinarySensorEntityDescription(
key=gw_vars.OTGW_IGNORE_TRANSITIONS,
friendly_name_format="Gateway Ignore Transitions {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_ROVRD_MAN_PRIO,
translation_key="override_manual_change_prio",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
(
[gw_vars.OTGW],
OpenThermBinarySensorEntityDescription(
key=gw_vars.OTGW_OVRD_HB,
friendly_name_format="Gateway Override High Byte {}",
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_ROVRD_AUTO_PRIO,
translation_key="override_program_change_prio",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_FAULT_IND,
translation_key="fault_indication",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_CH_ACTIVE,
translation_key="central_heating_n",
translation_placeholders={"circuit_number": "1"},
device_class=BinarySensorDeviceClass.RUNNING,
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_CH2_ACTIVE,
translation_key="central_heating_n",
translation_placeholders={"circuit_number": "2"},
device_class=BinarySensorDeviceClass.RUNNING,
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_DHW_ACTIVE,
translation_key="hot_water",
device_class=BinarySensorDeviceClass.RUNNING,
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_FLAME_ON,
translation_key="flame",
device_class=BinarySensorDeviceClass.HEAT,
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_COOLING_ACTIVE,
translation_key="cooling",
device_class=BinarySensorDeviceClass.RUNNING,
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_DIAG_IND,
translation_key="diagnostic_indication",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_DHW_PRESENT,
translation_key="supports_hot_water",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_CONTROL_TYPE,
translation_key="control_type",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_COOLING_SUPPORTED,
translation_key="supports_cooling",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_DHW_CONFIG,
translation_key="hot_water_config",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP,
translation_key="supports_pump_control",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_CH2_PRESENT,
translation_key="supports_ch_2",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_SERVICE_REQ,
translation_key="service_required",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_REMOTE_RESET,
translation_key="supports_remote_reset",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_LOW_WATER_PRESS,
translation_key="low_water_pressure",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_GAS_FAULT,
translation_key="gas_fault",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_AIR_PRESS_FAULT,
translation_key="air_pressure_fault",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_SLAVE_WATER_OVERTEMP,
translation_key="water_overtemperature",
device_class=BinarySensorDeviceClass.PROBLEM,
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_REMOTE_TRANSFER_MAX_CH,
translation_key="supports_central_heating_setpoint_transfer",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_REMOTE_RW_MAX_CH,
translation_key="supports_central_heating_setpoint_writing",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_REMOTE_TRANSFER_DHW,
translation_key="supports_hot_water_setpoint_transfer",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_REMOTE_RW_DHW,
translation_key="supports_hot_water_setpoint_writing",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_CH_ENABLED,
translation_key="central_heating_n",
translation_placeholders={"circuit_number": "1"},
device_description=BOILER_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_CH2_ENABLED,
translation_key="central_heating_n",
translation_placeholders={"circuit_number": "2"},
device_description=BOILER_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_DHW_ENABLED,
translation_key="hot_water",
device_description=BOILER_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_COOLING_ENABLED,
translation_key="cooling",
device_description=BOILER_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_MASTER_OTC_ENABLED,
translation_key="outside_temp_correction",
device_description=BOILER_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_ROVRD_MAN_PRIO,
translation_key="override_manual_change_prio",
device_description=BOILER_DEVICE_DESCRIPTION,
),
OpenThermBinarySensorEntityDescription(
key=gw_vars.DATA_ROVRD_AUTO_PRIO,
translation_key="override_program_change_prio",
device_description=BOILER_DEVICE_DESCRIPTION,
),
)
@ -293,35 +399,22 @@ async def async_setup_entry(
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]]
async_add_entities(
OpenThermBinarySensor(gw_hub, source, description)
for sources, description in BINARY_SENSOR_INFO
for source in sources
OpenThermBinarySensor(gw_hub, description)
for description in BINARY_SENSOR_DESCRIPTIONS
)
class OpenThermBinarySensor(OpenThermEntity, BinarySensorEntity):
"""Represent an OpenTherm Gateway binary sensor."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
entity_description: OpenThermBinarySensorEntityDescription
def __init__(
self,
gw_hub: OpenThermGatewayHub,
source: str,
description: OpenThermBinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor."""
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT,
f"{description.key}_{source}_{gw_hub.hub_id}",
hass=gw_hub.hass,
)
super().__init__(gw_hub, source, description)
@callback
def receive_report(self, status: dict[str, dict]) -> None:
def receive_report(self, status: dict[OpenThermDataSource, dict]) -> None:
"""Handle status updates from the component."""
self._attr_available = self._gateway.connected
state = status[self._source].get(self.entity_description.key)
state = status[self.entity_description.device_description.data_source].get(
self.entity_description.key
)
self._attr_is_on = None if state is None else bool(state)
self.async_write_ha_state()

View File

@ -2,50 +2,52 @@
from __future__ import annotations
from dataclasses import dataclass
import logging
from types import MappingProxyType
from typing import Any
from pyotgw import vars as gw_vars
from homeassistant.components.climate import (
ENTITY_ID_FORMAT,
PRESET_AWAY,
PRESET_NONE,
ClimateEntity,
ClimateEntityDescription,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_ID,
PRECISION_HALVES,
PRECISION_TENTHS,
PRECISION_WHOLE,
UnitOfTemperature,
)
from homeassistant.const import ATTR_TEMPERATURE, CONF_ID, UnitOfTemperature
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 . import OpenThermGatewayHub
from .const import (
CONF_FLOOR_TEMP,
CONF_READ_PRECISION,
CONF_SET_PRECISION,
CONF_TEMPORARY_OVRD_MODE,
DATA_GATEWAYS,
DATA_OPENTHERM_GW,
THERMOSTAT_DEVICE_DESCRIPTION,
OpenThermDataSource,
)
from .entity import OpenThermEntity, OpenThermEntityDescription
_LOGGER = logging.getLogger(__name__)
DEFAULT_FLOOR_TEMP = False
@dataclass(frozen=True, kw_only=True)
class OpenThermClimateEntityDescription(
ClimateEntityDescription, OpenThermEntityDescription
):
"""Describes an opentherm_gw climate entity."""
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
@ -56,6 +58,10 @@ async def async_setup_entry(
ents.append(
OpenThermClimate(
hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]],
OpenThermClimateEntityDescription(
key="thermostat_entity",
device_description=THERMOSTAT_DEVICE_DESCRIPTION,
),
config_entry.options,
)
)
@ -63,98 +69,82 @@ async def async_setup_entry(
async_add_entities(ents)
class OpenThermClimate(ClimateEntity):
class OpenThermClimate(OpenThermEntity, ClimateEntity):
"""Representation of a climate device."""
_attr_should_poll = False
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_available = False
_attr_hvac_modes = []
_attr_name = None
_attr_preset_modes = []
_attr_min_temp = 1
_attr_max_temp = 30
_hvac_mode = HVACMode.HEAT
_current_temperature: float | None = None
_new_target_temperature: float | None = None
_target_temperature: float | None = None
_attr_hvac_mode = HVACMode.HEAT
_away_mode_a: int | None = None
_away_mode_b: int | None = None
_away_state_a = False
_away_state_b = False
_current_operation: HVACAction | None = None
_enable_turn_on_off_backwards_compatibility = False
_target_temperature: float | None = None
_new_target_temperature: float | None = None
entity_description: OpenThermClimateEntityDescription
def __init__(self, gw_hub, options):
"""Initialize the device."""
self._gateway = gw_hub
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, gw_hub.hub_id, hass=gw_hub.hass
)
self.friendly_name = gw_hub.name
self._attr_name = self.friendly_name
self.floor_temp = options.get(CONF_FLOOR_TEMP, DEFAULT_FLOOR_TEMP)
self.temp_read_precision = options.get(CONF_READ_PRECISION)
self.temp_set_precision = options.get(CONF_SET_PRECISION)
def __init__(
self,
gw_hub: OpenThermGatewayHub,
description: OpenThermClimateEntityDescription,
options: MappingProxyType[str, Any],
) -> None:
"""Initialize the entity."""
super().__init__(gw_hub, description)
if CONF_READ_PRECISION in options:
self._attr_precision = options[CONF_READ_PRECISION]
self._attr_target_temperature_step = options.get(CONF_SET_PRECISION)
self.temporary_ovrd_mode = options.get(CONF_TEMPORARY_OVRD_MODE, True)
self._unsub_options = None
self._unsub_updates = None
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, gw_hub.hub_id)},
manufacturer="Schelte Bron",
model="OpenTherm Gateway",
name=gw_hub.name,
sw_version=gw_hub.gw_version,
)
self._attr_unique_id = gw_hub.hub_id
@callback
def update_options(self, entry):
"""Update climate entity options."""
self.floor_temp = entry.options[CONF_FLOOR_TEMP]
self.temp_read_precision = entry.options[CONF_READ_PRECISION]
self.temp_set_precision = entry.options[CONF_SET_PRECISION]
self._attr_precision = entry.options[CONF_READ_PRECISION]
self._attr_target_temperature_step = entry.options[CONF_SET_PRECISION]
self.temporary_ovrd_mode = entry.options[CONF_TEMPORARY_OVRD_MODE]
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Connect to the OpenTherm Gateway device."""
_LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name)
self._unsub_updates = async_dispatcher_connect(
self.hass, self._gateway.update_signal, self.receive_report
await super().async_added_to_hass()
self.async_on_remove(
async_dispatcher_connect(
self.hass, self._gateway.options_update_signal, self.update_options
)
)
self._unsub_options = async_dispatcher_connect(
self.hass, self._gateway.options_update_signal, self.update_options
)
async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from updates from the component."""
_LOGGER.debug("Removing OpenTherm Gateway climate %s", self.friendly_name)
self._unsub_options()
self._unsub_updates()
@callback
def receive_report(self, status):
def receive_report(self, status: dict[OpenThermDataSource, dict]):
"""Receive and handle a new report from the Gateway."""
self._attr_available = self._gateway.connected
ch_active = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_CH_ACTIVE)
flame_on = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_FLAME_ON)
cooling_active = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_COOLING_ACTIVE)
ch_active = status[OpenThermDataSource.BOILER].get(gw_vars.DATA_SLAVE_CH_ACTIVE)
flame_on = status[OpenThermDataSource.BOILER].get(gw_vars.DATA_SLAVE_FLAME_ON)
cooling_active = status[OpenThermDataSource.BOILER].get(
gw_vars.DATA_SLAVE_COOLING_ACTIVE
)
if ch_active and flame_on:
self._current_operation = HVACAction.HEATING
self._hvac_mode = HVACMode.HEAT
self._attr_hvac_action = HVACAction.HEATING
self._attr_hvac_mode = HVACMode.HEAT
elif cooling_active:
self._current_operation = HVACAction.COOLING
self._hvac_mode = HVACMode.COOL
self._attr_hvac_action = HVACAction.COOLING
self._attr_hvac_mode = HVACMode.COOL
else:
self._current_operation = HVACAction.IDLE
self._attr_hvac_action = HVACAction.IDLE
self._current_temperature = status[gw_vars.THERMOSTAT].get(
self._attr_current_temperature = status[OpenThermDataSource.THERMOSTAT].get(
gw_vars.DATA_ROOM_TEMP
)
temp_upd = status[gw_vars.THERMOSTAT].get(gw_vars.DATA_ROOM_SETPOINT)
temp_upd = status[OpenThermDataSource.THERMOSTAT].get(
gw_vars.DATA_ROOM_SETPOINT
)
if self._target_temperature != temp_upd:
self._new_target_temperature = None
@ -162,82 +152,35 @@ class OpenThermClimate(ClimateEntity):
# GPIO mode 5: 0 == Away
# GPIO mode 6: 1 == Away
gpio_a_state = status[gw_vars.OTGW].get(gw_vars.OTGW_GPIO_A)
if gpio_a_state == 5:
self._away_mode_a = 0
elif gpio_a_state == 6:
self._away_mode_a = 1
else:
self._away_mode_a = None
gpio_b_state = status[gw_vars.OTGW].get(gw_vars.OTGW_GPIO_B)
if gpio_b_state == 5:
self._away_mode_b = 0
elif gpio_b_state == 6:
self._away_mode_b = 1
else:
self._away_mode_b = None
if self._away_mode_a is not None:
self._away_state_a = (
status[gw_vars.OTGW].get(gw_vars.OTGW_GPIO_A_STATE) == self._away_mode_a
gpio_a_state = status[OpenThermDataSource.GATEWAY].get(gw_vars.OTGW_GPIO_A)
gpio_b_state = status[OpenThermDataSource.GATEWAY].get(gw_vars.OTGW_GPIO_B)
self._away_mode_a = gpio_a_state - 5 if gpio_a_state in (5, 6) else None
self._away_mode_b = gpio_b_state - 5 if gpio_b_state in (5, 6) else None
self._away_state_a = (
(
status[OpenThermDataSource.GATEWAY].get(gw_vars.OTGW_GPIO_A_STATE)
== self._away_mode_a
)
if self._away_mode_b is not None:
self._away_state_b = (
status[gw_vars.OTGW].get(gw_vars.OTGW_GPIO_B_STATE) == self._away_mode_b
if self._away_mode_a is not None
else False
)
self._away_state_b = (
(
status[OpenThermDataSource.GATEWAY].get(gw_vars.OTGW_GPIO_B_STATE)
== self._away_mode_b
)
if self._away_mode_b is not None
else False
)
self.async_write_ha_state()
@property
def precision(self):
"""Return the precision of the system."""
if self.temp_read_precision:
return self.temp_read_precision
if self.hass.config.units.temperature_unit == UnitOfTemperature.CELSIUS:
return PRECISION_HALVES
return PRECISION_WHOLE
@property
def hvac_action(self) -> HVACAction | None:
"""Return current HVAC operation."""
return self._current_operation
@property
def hvac_mode(self) -> HVACMode:
"""Return current HVAC mode."""
return self._hvac_mode
def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
_LOGGER.warning("Changing HVAC mode is not supported")
@property
def current_temperature(self):
"""Return the current temperature."""
if self._current_temperature is None:
return None
if self.floor_temp is True:
if self.precision == PRECISION_HALVES:
return int(2 * self._current_temperature) / 2
if self.precision == PRECISION_TENTHS:
return int(10 * self._current_temperature) / 10
return int(self._current_temperature)
return self._current_temperature
@property
def target_temperature(self):
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return self._new_target_temperature or self._target_temperature
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
if self.temp_set_precision:
return self.temp_set_precision
if self.hass.config.units.temperature_unit == UnitOfTemperature.CELSIUS:
return PRECISION_HALVES
return PRECISION_WHOLE
@property
def preset_mode(self):
def preset_mode(self) -> str:
"""Return current preset mode."""
if self._away_state_a or self._away_state_b:
return PRESET_AWAY

View File

@ -34,6 +34,7 @@ from .const import (
CONF_SET_PRECISION,
CONF_TEMPORARY_OVRD_MODE,
CONNECTION_TIMEOUT,
OpenThermDataSource,
)
@ -74,7 +75,7 @@ class OpenThermGwConfigFlow(ConfigFlow, domain=DOMAIN):
await otgw.disconnect()
if not status:
raise ConnectionError
return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
return status[OpenThermDataSource.GATEWAY].get(gw_vars.OTGW_ABOUT)
try:
async with asyncio.timeout(CONNECTION_TIMEOUT):

View File

@ -1,5 +1,10 @@
"""Constants for the opentherm_gw integration."""
from dataclasses import dataclass
from enum import StrEnum
from pyotgw import vars as gw_vars
ATTR_GW_ID = "gateway_id"
ATTR_LEVEL = "level"
ATTR_DHW_OVRD = "dhw_override"
@ -33,3 +38,41 @@ SERVICE_SET_MAX_MOD = "set_max_modulation"
SERVICE_SET_OAT = "set_outside_temperature"
SERVICE_SET_SB_TEMP = "set_setback_temperature"
SERVICE_SEND_TRANSP_CMD = "send_transparent_command"
class OpenThermDataSource(StrEnum):
"""List valid OpenTherm data sources."""
BOILER = gw_vars.BOILER
GATEWAY = gw_vars.OTGW
THERMOSTAT = gw_vars.THERMOSTAT
class OpenThermDeviceIdentifier(StrEnum):
"""List valid OpenTherm device identifiers."""
BOILER = "boiler"
GATEWAY = "gateway"
THERMOSTAT = "thermostat"
@dataclass(frozen=True, kw_only=True)
class OpenThermDeviceDescription:
"""Describe OpenTherm device properties."""
data_source: OpenThermDataSource
device_identifier: OpenThermDeviceIdentifier
BOILER_DEVICE_DESCRIPTION = OpenThermDeviceDescription(
data_source=OpenThermDataSource.BOILER,
device_identifier=OpenThermDeviceIdentifier.BOILER,
)
GATEWAY_DEVICE_DESCRIPTION = OpenThermDeviceDescription(
data_source=OpenThermDataSource.GATEWAY,
device_identifier=OpenThermDeviceIdentifier.GATEWAY,
)
THERMOSTAT_DEVICE_DESCRIPTION = OpenThermDeviceDescription(
data_source=OpenThermDataSource.THERMOSTAT,
device_identifier=OpenThermDeviceIdentifier.THERMOSTAT,
)

View File

@ -10,7 +10,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity, EntityDescription
from . import OpenThermGatewayHub
from .const import DOMAIN
from .const import DOMAIN, OpenThermDataSource, OpenThermDeviceDescription
_LOGGER = logging.getLogger(__name__)
@ -24,53 +24,49 @@ TRANSLATE_SOURCE = {
class OpenThermEntityDescription(EntityDescription):
"""Describe common opentherm_gw entity properties."""
friendly_name_format: str
device_description: OpenThermDeviceDescription
class OpenThermEntity(Entity):
"""Represent an OpenTherm Gateway entity."""
"""Represent an OpenTherm entity."""
_attr_has_entity_name = True
_attr_should_poll = False
_attr_entity_registry_enabled_default = False
_attr_available = False
entity_description: OpenThermEntityDescription
def __init__(
self,
gw_hub: OpenThermGatewayHub,
source: str,
description: OpenThermEntityDescription,
) -> None:
"""Initialize the entity."""
self.entity_description = description
self._gateway = gw_hub
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_hub.name)
self._attr_unique_id = f"{gw_hub.hub_id}-{source}-{description.key}"
self._attr_unique_id = f"{gw_hub.hub_id}-{description.device_description.device_identifier}-{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, gw_hub.hub_id)},
manufacturer="Schelte Bron",
model="OpenTherm Gateway",
name=gw_hub.name,
sw_version=gw_hub.gw_version,
identifiers={
(
DOMAIN,
f"{gw_hub.hub_id}-{description.device_description.device_identifier}",
)
},
)
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
)
)
@property
def available(self) -> bool:
"""Return connection status of the hub to indicate availability."""
return self._gateway.connected
@callback
def receive_report(self, status: dict[str, dict]) -> None:
def receive_report(self, status: dict[OpenThermDataSource, dict]) -> None:
"""Handle status updates from the component."""
# Must be implemented at the platform level.
raise NotImplementedError

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,8 @@
{
"common": {
"state_not_supported": "Not supported",
"state_supported": "Supported"
},
"config": {
"step": {
"init": {
@ -16,6 +20,297 @@
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
}
},
"device": {
"boiler_device": {
"name": "OpenTherm Boiler"
},
"gateway_device": {
"name": "OpenTherm Gateway"
},
"thermostat_device": {
"name": "OpenTherm Thermostat"
}
},
"entity": {
"binary_sensor": {
"fault_indication": {
"name": "Fault indication"
},
"central_heating_n": {
"name": "Central heating {circuit_number}"
},
"cooling": {
"name": "Cooling"
},
"flame": {
"name": "Flame"
},
"hot_water": {
"name": "Hot water"
},
"diagnostic_indication": {
"name": "Diagnostic indication"
},
"supports_hot_water": {
"name": "Hot water support",
"state": {
"off": "[%key:component::opentherm_gw::common::state_not_supported%]",
"on": "[%key:component::opentherm_gw::common::state_supported%]"
}
},
"control_type": {
"name": "Control type"
},
"supports_cooling": {
"name": "Cooling support",
"state": {
"off": "[%key:component::opentherm_gw::common::state_not_supported%]",
"on": "[%key:component::opentherm_gw::common::state_supported%]"
}
},
"hot_water_config": {
"name": "Hot water system type",
"state": {
"off": "Instantaneous or unspecified",
"on": "Storage tank"
}
},
"supports_pump_control": {
"name": "Pump control support",
"state": {
"off": "[%key:component::opentherm_gw::common::state_not_supported%]",
"on": "[%key:component::opentherm_gw::common::state_supported%]"
}
},
"supports_ch_2": {
"name": "Central heating 2 support",
"state": {
"off": "[%key:component::opentherm_gw::common::state_not_supported%]",
"on": "[%key:component::opentherm_gw::common::state_supported%]"
}
},
"service_required": {
"name": "Service required"
},
"supports_remote_reset": {
"name": "Remote reset support",
"state": {
"off": "[%key:component::opentherm_gw::common::state_not_supported%]",
"on": "[%key:component::opentherm_gw::common::state_supported%]"
}
},
"low_water_pressure": {
"name": "Low water pressure"
},
"gas_fault": {
"name": "Gas fault"
},
"air_pressure_fault": {
"name": "Air pressure fault"
},
"water_overtemperature": {
"name": "Water overtemperature"
},
"supports_central_heating_setpoint_transfer": {
"name": "Central heating setpoint transfer support",
"state": {
"off": "[%key:component::opentherm_gw::common::state_not_supported%]",
"on": "[%key:component::opentherm_gw::common::state_supported%]"
}
},
"supports_central_heating_setpoint_writing": {
"name": "Central heating setpoint write support",
"state": {
"off": "[%key:component::opentherm_gw::common::state_not_supported%]",
"on": "[%key:component::opentherm_gw::common::state_supported%]"
}
},
"supports_hot_water_setpoint_transfer": {
"name": "Hot water setpoint transfer support",
"state": {
"off": "[%key:component::opentherm_gw::common::state_not_supported%]",
"on": "[%key:component::opentherm_gw::common::state_supported%]"
}
},
"supports_hot_water_setpoint_writing": {
"name": "Hot water setpoint write support",
"state": {
"off": "[%key:component::opentherm_gw::common::state_not_supported%]",
"on": "[%key:component::opentherm_gw::common::state_supported%]"
}
},
"gpio_state_n": {
"name": "GPIO {gpio_id} state"
},
"ignore_transitions": {
"name": "Ignore transitions"
},
"override_high_byte": {
"name": "Override high byte"
},
"outside_temp_correction": {
"name": "Outside temperature correction"
},
"override_manual_change_prio": {
"name": "Manual change has priority over override"
},
"override_program_change_prio": {
"name": "Programmed change has priority over override"
}
},
"sensor": {
"control_setpoint_n": {
"name": "Control setpoint {circuit_number}"
},
"manufacturer_id": {
"name": "Manufacturer ID"
},
"oem_fault_code": {
"name": "Manufacturer-specific fault code"
},
"cooling_control": {
"name": "Cooling control signal"
},
"max_relative_mod_level": {
"name": "Maximum relative modulation level"
},
"max_capacity": {
"name": "Maximum capacity"
},
"min_mod_level": {
"name": "Minimum modulation level"
},
"relative_mod_level": {
"name": "Relative modulation level"
},
"central_heating_pressure": {
"name": "Central heating water pressure"
},
"hot_water_flow_rate": {
"name": "Hot water flow rate"
},
"central_heating_temperature_n": {
"name": "Central heating {circuit_number} water temperature"
},
"hot_water_temperature_n": {
"name": "Hot water {circuit_number} temperature"
},
"return_water_temperature": {
"name": "Return water temperature"
},
"solar_storage_temperature": {
"name": "Solar storage temperature"
},
"solar_collector_temperature": {
"name": "Solar collector temperature"
},
"exhaust_temperature": {
"name": "Exhaust temperature"
},
"max_hot_water_setpoint_upper": {
"name": "Maximum hot water setpoint upper bound"
},
"max_hot_water_setpoint_lower": {
"name": "Maximum hot water setpoint lower bound"
},
"max_central_heating_setpoint_upper": {
"name": "Maximum central heating setpoint upper bound"
},
"max_central_heating_setpoint_lower": {
"name": "Maximum central heating setpoint lower bound"
},
"hot_water_setpoint": {
"name": "Hot water setpoint"
},
"max_central_heating_setpoint": {
"name": "Maximum central heating setpoint"
},
"oem_diagnostic_code": {
"name": "Manufacturer-specific diagnostic code"
},
"total_burner_starts": {
"name": "Burner start count"
},
"central_heating_pump_starts": {
"name": "Central heating pump start count"
},
"hot_water_pump_starts": {
"name": "Hot water pump start count"
},
"hot_water_burner_starts": {
"name": "Hot water burner start count"
},
"total_burner_hours": {
"name": "Burner running time"
},
"central_heating_pump_hours": {
"name": "Central heating pump running time"
},
"hot_water_pump_hours": {
"name": "Hot water pump running time"
},
"hot_water_burner_hours": {
"name": "Hot water burner running time"
},
"opentherm_version": {
"name": "OpenTherm protocol version"
},
"product_type": {
"name": "Product type"
},
"product_version": {
"name": "Product version"
},
"operating_mode": {
"name": "Operating mode"
},
"hot_water_override_mode": {
"name": "Hot water override mode"
},
"firmware_version": {
"name": "Firmware version"
},
"firmware_build": {
"name": "Firmware build"
},
"clock_speed": {
"name": "Clock speed"
},
"led_mode_n": {
"name": "LED {led_id} mode"
},
"gpio_mode_n": {
"name": "GPIO {gpio_id} mode"
},
"setback_temperature": {
"name": "Setback temperature"
},
"room_setpoint_override_mode": {
"name": "Room setpoint override mode"
},
"smart_power_mode": {
"name": "Smart power mode"
},
"thermostat_detection_mode": {
"name": "Thermostat detection mode"
},
"reference_voltage": {
"name": "Reference voltage setting"
},
"room_setpoint_override": {
"name": "Room setpoint override"
},
"room_setpoint_n": {
"name": "Room setpoint {setpoint_id}"
},
"room_temperature": {
"name": "Room temperature"
},
"outside_temperature": {
"name": "Outside temperature"
}
}
},
"options": {
"step": {
"init": {

View File

@ -1,12 +1,15 @@
"""Test Opentherm Gateway init."""
from unittest.mock import patch
from unittest.mock import AsyncMock, MagicMock, patch
from pyotgw.vars import OTGW, OTGW_ABOUT
import pytest
from homeassistant import setup
from homeassistant.components.opentherm_gw.const import DOMAIN
from homeassistant.components.opentherm_gw.const import (
DOMAIN,
OpenThermDeviceIdentifier,
)
from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
@ -49,7 +52,9 @@ async def test_device_registry_insert(
await hass.async_block_till_done()
gw_dev = device_registry.async_get_device(identifiers={(DOMAIN, MOCK_GATEWAY_ID)})
gw_dev = device_registry.async_get_device(
identifiers={(DOMAIN, f"{MOCK_GATEWAY_ID}-{OpenThermDeviceIdentifier.GATEWAY}")}
)
assert gw_dev.sw_version == VERSION_OLD
@ -63,7 +68,9 @@ async def test_device_registry_update(
device_registry.async_get_or_create(
config_entry_id=MOCK_CONFIG_ENTRY.entry_id,
identifiers={(DOMAIN, MOCK_GATEWAY_ID)},
identifiers={
(DOMAIN, f"{MOCK_GATEWAY_ID}-{OpenThermDeviceIdentifier.GATEWAY}")
},
name="Mock Gateway",
manufacturer="Schelte Bron",
model="OpenTherm Gateway",
@ -80,5 +87,70 @@ async def test_device_registry_update(
await setup.async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
gw_dev = device_registry.async_get_device(identifiers={(DOMAIN, MOCK_GATEWAY_ID)})
gw_dev = device_registry.async_get_device(
identifiers={(DOMAIN, f"{MOCK_GATEWAY_ID}-{OpenThermDeviceIdentifier.GATEWAY}")}
)
assert gw_dev is not None
assert gw_dev.sw_version == VERSION_NEW
# Device migration test can be removed in 2025.4.0
async def test_device_migration(
hass: HomeAssistant, device_registry: dr.DeviceRegistry
) -> None:
"""Test that the device registry is updated correctly."""
MOCK_CONFIG_ENTRY.add_to_hass(hass)
device_registry.async_get_or_create(
config_entry_id=MOCK_CONFIG_ENTRY.entry_id,
identifiers={
(DOMAIN, MOCK_GATEWAY_ID),
},
name="Mock Gateway",
manufacturer="Schelte Bron",
model="OpenTherm Gateway",
sw_version=VERSION_OLD,
)
with (
patch(
"homeassistant.components.opentherm_gw.OpenThermGateway",
return_value=MagicMock(
connect=AsyncMock(return_value=MINIMAL_STATUS_UPD),
set_control_setpoint=AsyncMock(),
set_max_relative_mod=AsyncMock(),
disconnect=AsyncMock(),
),
),
):
await setup.async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
assert (
device_registry.async_get_device(identifiers={(DOMAIN, MOCK_GATEWAY_ID)})
is None
)
gw_dev = device_registry.async_get_device(
identifiers={(DOMAIN, f"{MOCK_GATEWAY_ID}-{OpenThermDeviceIdentifier.GATEWAY}")}
)
assert gw_dev is not None
assert (
device_registry.async_get_device(
identifiers={
(DOMAIN, f"{MOCK_GATEWAY_ID}-{OpenThermDeviceIdentifier.BOILER}")
}
)
is not None
)
assert (
device_registry.async_get_device(
identifiers={
(DOMAIN, f"{MOCK_GATEWAY_ID}-{OpenThermDeviceIdentifier.THERMOSTAT}")
}
)
is not None
)