Add the air outlet, DHW and indoor unit sensors to WeHeat (#181265)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Barry vd. Heuvel
2026-09-04 09:29:15 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent f501eaed55
commit 7f6b995473
7 changed files with 358 additions and 7 deletions
@@ -33,6 +33,9 @@
"cop": {
"default": "mdi:speedometer"
},
"dhw_control_method": {
"default": "mdi:tune"
},
"dhw_flow_volume": {
"default": "mdi:pump"
},
@@ -51,6 +54,9 @@
"electricity_used_heating": {
"default": "mdi:flash"
},
"electricity_used_indoor_unit": {
"default": "mdi:flash"
},
"energy_output": {
"default": "mdi:flash"
},
@@ -74,7 +74,7 @@ rules:
status: todo
comment: |
While unlikely to happen. Check if it is easily integrated.
entity-category: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
+42 -4
View File
@@ -15,6 +15,7 @@ from homeassistant.components.sensor import (
from homeassistant.const import (
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
EntityCategory,
UnitOfEnergy,
UnitOfPower,
UnitOfTemperature,
@@ -111,6 +112,15 @@ SENSORS = [
suggested_display_precision=DISPLAY_PRECISION_WATER_TEMP,
value_fn=lambda status: status.air_inlet_temperature,
),
WeHeatSensorEntityDescription(
translation_key="air_outlet_temperature",
key="air_outlet_temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=DISPLAY_PRECISION_WATER_TEMP,
value_fn=lambda status: status.air_outlet_temperature,
),
WeHeatSensorEntityDescription(
translation_key="thermostat_water_setpoint",
key="thermostat_water_setpoint",
@@ -203,6 +213,30 @@ DHW_SENSORS = [
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
value_fn=lambda status: status.dhw_flow_volume,
),
WeHeatSensorEntityDescription(
translation_key="dhw_target_temperature",
key="dhw_target_temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=DISPLAY_PRECISION_WATER_TEMP,
# A target of zero is how the heat pump says DHW control is off.
value_fn=lambda status: status.dhw_target_temperature or None,
),
WeHeatSensorEntityDescription(
translation_key="dhw_control_method",
key="dhw_control_method",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options=[method.name.lower() for method in HeatPump.DhwControlMethod],
value_fn=(
lambda status: (
status.dhw_control_method.name.lower()
if status.dhw_control_method is not None
else None
)
),
),
]
ENERGY_SENSORS = [
@@ -214,6 +248,14 @@ ENERGY_SENSORS = [
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda status: status.energy_total,
),
WeHeatSensorEntityDescription(
translation_key="electricity_used_indoor_unit",
key="electricity_used_indoor_unit",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda status: status.energy_in_indoor_unit,
),
WeHeatSensorEntityDescription(
translation_key="energy_output",
key="energy_output",
@@ -326,8 +368,6 @@ async def async_setup_entry(
entity_description,
)
for entity_description in DHW_SENSORS
if entity_description.value_fn(weheatdata.data_coordinator.data)
is not None
)
entities.extend(
WeheatHeatPumpSensor(
@@ -336,8 +376,6 @@ async def async_setup_entry(
entity_description,
)
for entity_description in DHW_ENERGY_SENSORS
if entity_description.value_fn(weheatdata.energy_coordinator.data)
is not None
)
entities.extend(
WeheatHeatPumpSensor(
@@ -48,6 +48,9 @@
}
},
"sensor": {
"air_outlet_temperature": {
"name": "Air outlet temperature"
},
"central_heating_flow_volume": {
"name": "Central heating pump flow"
},
@@ -66,9 +69,22 @@
"dhw_bottom_temperature": {
"name": "DHW bottom temperature"
},
"dhw_control_method": {
"name": "DHW control method",
"state": {
"boost": "Boost",
"fixed": "Fixed setpoint",
"none": "None",
"schedule": "Schedule",
"weheat_intelligence": "Weheat Intelligence"
}
},
"dhw_flow_volume": {
"name": "DHW pump flow"
},
"dhw_target_temperature": {
"name": "DHW target temperature"
},
"dhw_top_temperature": {
"name": "DHW top temperature"
},
@@ -87,6 +103,9 @@
"electricity_used_heating": {
"name": "Electricity used heating"
},
"electricity_used_indoor_unit": {
"name": "Electricity used indoor unit"
},
"electricity_used_standby": {
"name": "Electricity used standby"
},
+5
View File
@@ -111,6 +111,7 @@ def mock_weheat_heat_pump_instance() -> MagicMock:
mock_heat_pump_instance.water_outlet_temperature = 22
mock_heat_pump_instance.water_house_in_temperature = 33
mock_heat_pump_instance.air_inlet_temperature = 44
mock_heat_pump_instance.air_outlet_temperature = 50
mock_heat_pump_instance.power_input = 55
mock_heat_pump_instance.power_output = 66
mock_heat_pump_instance.dhw_top_temperature = 77
@@ -126,6 +127,7 @@ def mock_weheat_heat_pump_instance() -> MagicMock:
mock_heat_pump_instance.energy_in_cooling = 9000
mock_heat_pump_instance.energy_in_standby = 684
mock_heat_pump_instance.energy_total = 28689
mock_heat_pump_instance.energy_in_indoor_unit = 1042
mock_heat_pump_instance.energy_out_heating = 10000
mock_heat_pump_instance.energy_out_dhw = 6677
mock_heat_pump_instance.energy_out_defrost = -1200
@@ -134,6 +136,9 @@ def mock_weheat_heat_pump_instance() -> MagicMock:
mock_heat_pump_instance.compressor_rpm = 4500
mock_heat_pump_instance.compressor_percentage = 100
mock_heat_pump_instance.dhw_flow_volume = 1.12
mock_heat_pump_instance.dhw_target_temperature = 55
mock_heat_pump_instance.dhw_control_method = HeatPump.DhwControlMethod.FIXED
mock_heat_pump_instance.dhw_control_method_code = 1
mock_heat_pump_instance.central_heating_flow_volume = 1.23
mock_heat_pump_instance.indoor_unit_water_pump_state = False
mock_heat_pump_instance.indoor_unit_auxiliary_pump_state = False
@@ -77,6 +77,64 @@
'state': 'heating',
})
# ---
# name: test_all_entities[sensor.test_model_air_outlet_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_model_air_outlet_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Air outlet temperature',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Air outlet temperature',
'platform': 'weheat',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'air_outlet_temperature',
'unique_id': '0000-1111-2222-3333_air_outlet_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_all_entities[sensor.test_model_air_outlet_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Model Air outlet temperature',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_model_air_outlet_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '50',
})
# ---
# name: test_all_entities[sensor.test_model_central_heating_inlet_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -473,6 +531,72 @@
'state': '88',
})
# ---
# name: test_all_entities[sensor.test_model_dhw_control_method-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'none',
'fixed',
'schedule',
'weheat_intelligence',
'boost',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_model_dhw_control_method',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'DHW control method',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'DHW control method',
'platform': 'weheat',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'dhw_control_method',
'unique_id': '0000-1111-2222-3333_dhw_control_method',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[sensor.test_model_dhw_control_method-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Model DHW control method',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'none',
'fixed',
'schedule',
'weheat_intelligence',
'boost',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.test_model_dhw_control_method',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'fixed',
})
# ---
# name: test_all_entities[sensor.test_model_dhw_pump_flow-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -531,6 +655,64 @@
'state': '1.12',
})
# ---
# name: test_all_entities[sensor.test_model_dhw_target_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_model_dhw_target_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'DHW target temperature',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'DHW target temperature',
'platform': 'weheat',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'dhw_target_temperature',
'unique_id': '0000-1111-2222-3333_dhw_target_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_all_entities[sensor.test_model_dhw_target_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Model DHW target temperature',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_model_dhw_target_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '55',
})
# ---
# name: test_all_entities[sensor.test_model_dhw_top_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -879,6 +1061,64 @@
'state': '12345',
})
# ---
# name: test_all_entities[sensor.test_model_electricity_used_indoor_unit-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_model_electricity_used_indoor_unit',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Electricity used indoor unit',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Electricity used indoor unit',
'platform': 'weheat',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'electricity_used_indoor_unit',
'unique_id': '0000-1111-2222-3333_electricity_used_indoor_unit',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_all_entities[sensor.test_model_electricity_used_indoor_unit-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Model Electricity used indoor unit',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_model_electricity_used_indoor_unit',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1042',
})
# ---
# name: test_all_entities[sensor.test_model_electricity_used_standby-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
+45 -2
View File
@@ -6,7 +6,7 @@ import pytest
from syrupy.assertion import SnapshotAssertion
from weheat.abstractions.discovery import HeatPumpDiscovery
from homeassistant.const import Platform
from homeassistant.const import STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -33,7 +33,7 @@ async def test_all_entities(
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(("has_dhw", "nr_of_entities"), [(False, 23), (True, 28)])
@pytest.mark.parametrize(("has_dhw", "nr_of_entities"), [(False, 25), (True, 32)])
async def test_create_entities(
hass: HomeAssistant,
mock_weheat_discover: AsyncMock,
@@ -52,3 +52,46 @@ async def test_create_entities(
await hass.async_block_till_done()
assert len(hass.states.async_all()) == nr_of_entities
@pytest.mark.parametrize(
("target", "expected"),
[
pytest.param(55, "55", id="a_target_it_aims_for"),
# DHW control off reports a target of zero, which is no target at all
pytest.param(0, STATE_UNKNOWN, id="dhw_control_off"),
],
)
@pytest.mark.usefixtures("mock_weheat_discover")
async def test_dhw_target_temperature(
hass: HomeAssistant,
mock_weheat_heat_pump: AsyncMock,
mock_config_entry: MockConfigEntry,
target: int,
expected: str,
) -> None:
"""Test the DHW target is only reported when the heat pump has one."""
mock_weheat_heat_pump.dhw_target_temperature = target
with patch("homeassistant.components.weheat.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry)
assert hass.states.get("sensor.test_model_dhw_target_temperature").state == expected
@pytest.mark.usefixtures("mock_weheat_discover")
async def test_an_unknown_dhw_control_method_keeps_the_sensor(
hass: HomeAssistant,
mock_weheat_heat_pump: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test a control method this library cannot name still gets a sensor."""
mock_weheat_heat_pump.dhw_control_method = None
mock_weheat_heat_pump.dhw_control_method_code = 99
with patch("homeassistant.components.weheat.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry)
assert (
hass.states.get("sensor.test_model_dhw_control_method").state == STATE_UNKNOWN
)