mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 23:41:48 -05:00
Add cooling status/information sensors to Weheat (#181278)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ab9ccbf5b9
commit
488ad0645d
@@ -30,6 +30,24 @@
|
||||
"compressor_rpm": {
|
||||
"default": "mdi:fan"
|
||||
},
|
||||
"cooling_blocked_by": {
|
||||
"default": "mdi:snowflake-alert"
|
||||
},
|
||||
"cooling_conditions_met": {
|
||||
"default": "mdi:checkbox-multiple-marked-outline"
|
||||
},
|
||||
"cooling_pause_reason": {
|
||||
"default": "mdi:snowflake-off"
|
||||
},
|
||||
"cooling_state": {
|
||||
"default": "mdi:snowflake-thermometer"
|
||||
},
|
||||
"cooling_stop_reason": {
|
||||
"default": "mdi:snowflake-off"
|
||||
},
|
||||
"cooling_wait_until": {
|
||||
"default": "mdi:timer-sand"
|
||||
},
|
||||
"cop": {
|
||||
"default": "mdi:speedometer"
|
||||
},
|
||||
@@ -75,6 +93,9 @@
|
||||
"heat_pump_state": {
|
||||
"default": "mdi:state-machine"
|
||||
},
|
||||
"last_cooling_time": {
|
||||
"default": "mdi:snowflake-check"
|
||||
},
|
||||
"outside_temperature": {
|
||||
"default": "mdi:home-thermometer-outline"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import override
|
||||
|
||||
from weheat.abstractions.heat_pump import HeatPump
|
||||
@@ -47,7 +49,21 @@ PARALLEL_UPDATES = 0
|
||||
class WeHeatSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Weheat sensor entity."""
|
||||
|
||||
value_fn: Callable[[HeatPump], StateType]
|
||||
value_fn: Callable[[HeatPump], StateType | datetime]
|
||||
|
||||
|
||||
# The portal counts the conditions the heat pump waits on and leaves these two
|
||||
# settings out of its tally.
|
||||
COOLING_CONDITIONS_NOT_COUNTED = ("control_method", "contact_not_blocked")
|
||||
|
||||
|
||||
# A cooling state is only reported during a cooling cycle and covers every substate
|
||||
# of it, including the water check the overall heat pump state reports as its own.
|
||||
def _latched_reason(status: HeatPump, reason: Enum | None) -> str | None:
|
||||
"""Return a reason from before the cycle, which says nothing while one runs."""
|
||||
if status.cooling_state is not None:
|
||||
return "none"
|
||||
return reason.name.lower() if reason is not None else None
|
||||
|
||||
|
||||
SENSORS = [
|
||||
@@ -239,6 +255,89 @@ DHW_SENSORS = [
|
||||
),
|
||||
]
|
||||
|
||||
COOLING_SENSORS = [
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="cooling_state",
|
||||
key="cooling_state",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[activity.name.lower() for activity in HeatPump.CoolingActivity],
|
||||
value_fn=lambda status: (
|
||||
status.cooling_activity.name.lower()
|
||||
if status.cooling_activity is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="cooling_blocked_by",
|
||||
key="cooling_blocked_by",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["none", *HeatPump.COOLING_START_CONDITION_BITS],
|
||||
value_fn=lambda status: (
|
||||
None
|
||||
if status.cooling_start_conditions is None
|
||||
else "none"
|
||||
if status.cooling_state is not None
|
||||
else next(
|
||||
(
|
||||
name
|
||||
for name in HeatPump.COOLING_START_CONDITION_BITS
|
||||
if not status.cooling_start_conditions[name]
|
||||
),
|
||||
"none",
|
||||
)
|
||||
),
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="cooling_conditions_met",
|
||||
key="cooling_conditions_met",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda status: (
|
||||
None
|
||||
if status.cooling_start_conditions is None
|
||||
or status.cooling_state is not None
|
||||
else sum(
|
||||
met
|
||||
for name, met in status.cooling_start_conditions.items()
|
||||
if name not in COOLING_CONDITIONS_NOT_COUNTED
|
||||
)
|
||||
),
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="cooling_wait_until",
|
||||
key="cooling_wait_until",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=lambda status: (
|
||||
status.cooling_available_from
|
||||
if status.cooling_start_conditions is not None
|
||||
and not status.cooling_start_conditions["exponential_backoff"]
|
||||
else None
|
||||
),
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="last_cooling_time",
|
||||
key="last_cooling_time",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=lambda status: status.last_cooling_time,
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="cooling_pause_reason",
|
||||
key="cooling_pause_reason",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[reason.name.lower() for reason in HeatPump.CoolingPauseReason],
|
||||
value_fn=lambda status: _latched_reason(status, status.cooling_pause_reason),
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="cooling_stop_reason",
|
||||
key="cooling_stop_reason",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[reason.name.lower() for reason in HeatPump.CoolingStopReason],
|
||||
value_fn=lambda status: _latched_reason(status, status.cooling_stop_reason),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
ENERGY_SENSORS = [
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="electricity_used",
|
||||
@@ -360,6 +459,15 @@ async def async_setup_entry(
|
||||
for entity_description in SENSORS
|
||||
if entity_description.value_fn(weheatdata.data_coordinator.data) is not None
|
||||
)
|
||||
if weheatdata.data_coordinator.data.cooling_activity is not None:
|
||||
entities.extend(
|
||||
WeheatHeatPumpSensor(
|
||||
weheatdata.heat_pump_info,
|
||||
weheatdata.data_coordinator,
|
||||
entity_description,
|
||||
)
|
||||
for entity_description in COOLING_SENSORS
|
||||
)
|
||||
if weheatdata.heat_pump_info.has_dhw:
|
||||
entities.extend(
|
||||
WeheatHeatPumpSensor(
|
||||
@@ -412,6 +520,6 @@ class WeheatHeatPumpSensor(WeheatEntity, SensorEntity):
|
||||
|
||||
@property
|
||||
@override
|
||||
def native_value(self) -> StateType:
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
@@ -63,6 +63,73 @@
|
||||
"compressor_rpm": {
|
||||
"name": "Compressor speed"
|
||||
},
|
||||
"cooling_blocked_by": {
|
||||
"name": "Cooling blocked by",
|
||||
"state": {
|
||||
"contact_not_blocked": "Blocked by external contact",
|
||||
"control_method": "Control method does not allow cooling",
|
||||
"demand": "No cooling demand",
|
||||
"dtc": "Cooling fault active",
|
||||
"exponential_backoff": "Waiting for restart delay",
|
||||
"heat_cool_delay": "Waiting for heating to cooling delay",
|
||||
"indoor_unit_connected": "Indoor unit not connected",
|
||||
"inside_temperature": "Room not warmer than target",
|
||||
"none": "Not blocked",
|
||||
"outside_air_temperature": "Outside temperature too low",
|
||||
"water_temperature": "Water not warmer than cooling curve",
|
||||
"water_to_air": "Air not warmer than water"
|
||||
}
|
||||
},
|
||||
"cooling_conditions_met": {
|
||||
"name": "Cooling conditions met",
|
||||
"unit_of_measurement": "of 9"
|
||||
},
|
||||
"cooling_pause_reason": {
|
||||
"name": "Cooling pause reason",
|
||||
"state": {
|
||||
"contact_blocked": "Blocked by external contact",
|
||||
"demand": "No cooling demand",
|
||||
"heat_pump_control": "Paused for another function",
|
||||
"none": "Not paused",
|
||||
"outside_colder_than_water_temperature": "Outside colder than water",
|
||||
"outside_temperature_too_low": "Outside temperature too low",
|
||||
"room_temperature_too_low": "Room temperature too low",
|
||||
"water_temperature_below_dewpoint": "Water temperature below dew point",
|
||||
"water_temperature_below_setpoint": "Water temperature colder than setpoint"
|
||||
}
|
||||
},
|
||||
"cooling_state": {
|
||||
"name": "Cooling state",
|
||||
"state": {
|
||||
"active": "Cooling",
|
||||
"idle": "Idle",
|
||||
"paused": "Paused",
|
||||
"pausing": "Pausing",
|
||||
"standby": "[%key:common::state::standby%]",
|
||||
"standby_run_cp": "Standby, pump running",
|
||||
"starting": "Starting",
|
||||
"stopped": "Stopped",
|
||||
"stopping": "Stopping",
|
||||
"waiting": "Waiting to start",
|
||||
"water_check": "Checking water temperature"
|
||||
}
|
||||
},
|
||||
"cooling_stop_reason": {
|
||||
"name": "Cooling stop reason",
|
||||
"state": {
|
||||
"contact_switch_over": "External contact switched over",
|
||||
"control_method": "Stopped by control method",
|
||||
"cooling_control": "Stopped by cooling control",
|
||||
"dtc": "Stopped by diagnostics",
|
||||
"heat_pump_control": "Stopped for another function",
|
||||
"no_indoor_unit_communication": "No indoor unit communication",
|
||||
"none": "Not stopped",
|
||||
"thermostat_disabled": "Thermostat disabled"
|
||||
}
|
||||
},
|
||||
"cooling_wait_until": {
|
||||
"name": "Cooling wait until"
|
||||
},
|
||||
"cop": {
|
||||
"name": "COP"
|
||||
},
|
||||
@@ -139,6 +206,9 @@
|
||||
"water_check": "Checking water temperature"
|
||||
}
|
||||
},
|
||||
"last_cooling_time": {
|
||||
"name": "Last cooling"
|
||||
},
|
||||
"outside_temperature": {
|
||||
"name": "Outside temperature"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Fixtures for Weheat tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import UTC, datetime
|
||||
from time import time
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -136,6 +137,26 @@ 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.cooling_pause_reason_code = 4
|
||||
mock_heat_pump_instance.cooling_stop_reason_code = 0
|
||||
mock_heat_pump_instance.last_cooling_time = datetime(
|
||||
2025, 6, 21, 14, 30, tzinfo=UTC
|
||||
)
|
||||
# The heat pump only reports a cooling state during a cooling cycle, so a
|
||||
# heating one derives its cooling activity from the latched reasons instead.
|
||||
mock_heat_pump_instance.cooling_state = None
|
||||
mock_heat_pump_instance.cooling_activity = HeatPump.CoolingActivity.WAITING
|
||||
mock_heat_pump_instance.cooling_pause_reason = (
|
||||
HeatPump.CoolingPauseReason.WATER_TEMPERATURE_BELOW_SETPOINT
|
||||
)
|
||||
mock_heat_pump_instance.cooling_stop_reason = HeatPump.CoolingStopReason.NONE
|
||||
mock_heat_pump_instance.cooling_backoff = 60
|
||||
mock_heat_pump_instance.cooling_available_from = datetime(
|
||||
2025, 6, 21, 15, 30, tzinfo=UTC
|
||||
)
|
||||
mock_heat_pump_instance.cooling_start_conditions = {
|
||||
name: name != "demand" for name in HeatPump.COOLING_START_CONDITION_BITS
|
||||
}
|
||||
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
|
||||
|
||||
@@ -359,6 +359,415 @@
|
||||
'state': '100',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_cooling_blocked_by-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'none',
|
||||
'control_method',
|
||||
'dtc',
|
||||
'outside_air_temperature',
|
||||
'inside_temperature',
|
||||
'indoor_unit_connected',
|
||||
'water_to_air',
|
||||
'demand',
|
||||
'water_temperature',
|
||||
'contact_not_blocked',
|
||||
'exponential_backoff',
|
||||
'heat_cool_delay',
|
||||
]),
|
||||
}),
|
||||
'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_cooling_blocked_by',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Cooling blocked by',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cooling blocked by',
|
||||
'platform': 'weheat',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cooling_blocked_by',
|
||||
'unique_id': '0000-1111-2222-3333_cooling_blocked_by',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_cooling_blocked_by-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Model Cooling blocked by',
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'none',
|
||||
'control_method',
|
||||
'dtc',
|
||||
'outside_air_temperature',
|
||||
'inside_temperature',
|
||||
'indoor_unit_connected',
|
||||
'water_to_air',
|
||||
'demand',
|
||||
'water_temperature',
|
||||
'contact_not_blocked',
|
||||
'exponential_backoff',
|
||||
'heat_cool_delay',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_model_cooling_blocked_by',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'demand',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_cooling_conditions_met-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_cooling_conditions_met',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Cooling conditions met',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cooling conditions met',
|
||||
'platform': 'weheat',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cooling_conditions_met',
|
||||
'unique_id': '0000-1111-2222-3333_cooling_conditions_met',
|
||||
'unit_of_measurement': 'of 9',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_cooling_conditions_met-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Model Cooling conditions met',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'of 9',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_model_cooling_conditions_met',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '8',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_cooling_pause_reason-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'none',
|
||||
'room_temperature_too_low',
|
||||
'outside_temperature_too_low',
|
||||
'outside_colder_than_water_temperature',
|
||||
'water_temperature_below_setpoint',
|
||||
'heat_pump_control',
|
||||
'water_temperature_below_dewpoint',
|
||||
'contact_blocked',
|
||||
'demand',
|
||||
]),
|
||||
}),
|
||||
'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_cooling_pause_reason',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Cooling pause reason',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cooling pause reason',
|
||||
'platform': 'weheat',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cooling_pause_reason',
|
||||
'unique_id': '0000-1111-2222-3333_cooling_pause_reason',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_cooling_pause_reason-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Model Cooling pause reason',
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'none',
|
||||
'room_temperature_too_low',
|
||||
'outside_temperature_too_low',
|
||||
'outside_colder_than_water_temperature',
|
||||
'water_temperature_below_setpoint',
|
||||
'heat_pump_control',
|
||||
'water_temperature_below_dewpoint',
|
||||
'contact_blocked',
|
||||
'demand',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_model_cooling_pause_reason',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'water_temperature_below_setpoint',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_cooling_state-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'idle',
|
||||
'starting',
|
||||
'active',
|
||||
'stopping',
|
||||
'standby',
|
||||
'pausing',
|
||||
'water_check',
|
||||
'standby_run_cp',
|
||||
'paused',
|
||||
'stopped',
|
||||
'waiting',
|
||||
]),
|
||||
}),
|
||||
'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_cooling_state',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Cooling state',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cooling state',
|
||||
'platform': 'weheat',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cooling_state',
|
||||
'unique_id': '0000-1111-2222-3333_cooling_state',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_cooling_state-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Model Cooling state',
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'idle',
|
||||
'starting',
|
||||
'active',
|
||||
'stopping',
|
||||
'standby',
|
||||
'pausing',
|
||||
'water_check',
|
||||
'standby_run_cp',
|
||||
'paused',
|
||||
'stopped',
|
||||
'waiting',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_model_cooling_state',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'waiting',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_cooling_stop_reason-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'none',
|
||||
'dtc',
|
||||
'control_method',
|
||||
'no_indoor_unit_communication',
|
||||
'heat_pump_control',
|
||||
'cooling_control',
|
||||
'contact_switch_over',
|
||||
'thermostat_disabled',
|
||||
]),
|
||||
}),
|
||||
'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_cooling_stop_reason',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Cooling stop reason',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cooling stop reason',
|
||||
'platform': 'weheat',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cooling_stop_reason',
|
||||
'unique_id': '0000-1111-2222-3333_cooling_stop_reason',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_cooling_stop_reason-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Model Cooling stop reason',
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'none',
|
||||
'dtc',
|
||||
'control_method',
|
||||
'no_indoor_unit_communication',
|
||||
'heat_pump_control',
|
||||
'cooling_control',
|
||||
'contact_switch_over',
|
||||
'thermostat_disabled',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_model_cooling_stop_reason',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'none',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_cooling_wait_until-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'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_cooling_wait_until',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Cooling wait until',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cooling wait until',
|
||||
'platform': 'weheat',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cooling_wait_until',
|
||||
'unique_id': '0000-1111-2222-3333_cooling_wait_until',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_cooling_wait_until-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'timestamp',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Model Cooling wait until',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_model_cooling_wait_until',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_cop-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
@@ -1467,6 +1876,57 @@
|
||||
'state': '55',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_last_cooling-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'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_last_cooling',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Last cooling',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Last cooling',
|
||||
'platform': 'weheat',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'last_cooling_time',
|
||||
'unique_id': '0000-1111-2222-3333_last_cooling_time',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_last_cooling-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'timestamp',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Model Last cooling',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_model_last_cooling',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2025-06-21T14:30:00+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_model_output_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -5,8 +5,11 @@ from unittest.mock import AsyncMock, patch
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from weheat.abstractions.discovery import HeatPumpDiscovery
|
||||
from weheat.abstractions.heat_pump import HeatPump
|
||||
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.weheat.sensor import COOLING_CONDITIONS_NOT_COUNTED
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
@@ -33,7 +36,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, 25), (True, 32)])
|
||||
@pytest.mark.parametrize(("has_dhw", "nr_of_entities"), [(False, 32), (True, 39)])
|
||||
async def test_create_entities(
|
||||
hass: HomeAssistant,
|
||||
mock_weheat_discover: AsyncMock,
|
||||
@@ -95,3 +98,182 @@ async def test_an_unknown_dhw_control_method_keeps_the_sensor(
|
||||
assert (
|
||||
hass.states.get("sensor.test_model_dhw_control_method").state == STATE_UNKNOWN
|
||||
)
|
||||
|
||||
|
||||
# The cooling sensors a heat pump that does not cool must not get. The cooling
|
||||
# energy counters are not among them: those are reported either way, at zero.
|
||||
COOLING_SENSORS = {
|
||||
"sensor.test_model_cooling_state",
|
||||
"sensor.test_model_cooling_blocked_by",
|
||||
"sensor.test_model_cooling_conditions_met",
|
||||
"sensor.test_model_cooling_wait_until",
|
||||
"sensor.test_model_last_cooling",
|
||||
"sensor.test_model_cooling_pause_reason",
|
||||
"sensor.test_model_cooling_stop_reason",
|
||||
}
|
||||
|
||||
|
||||
CONDITIONS_COUNTED = len(HeatPump.COOLING_START_CONDITION_BITS) - len(
|
||||
COOLING_CONDITIONS_NOT_COUNTED
|
||||
)
|
||||
|
||||
|
||||
def _start_conditions(*unmet: str) -> dict[str, bool]:
|
||||
"""Build a start condition mapping with the named conditions not met."""
|
||||
return {name: name not in unmet for name in HeatPump.COOLING_START_CONDITION_BITS}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("unmet", "expected"),
|
||||
[
|
||||
pytest.param((), "9", id="all_met"),
|
||||
pytest.param(("demand",), "8", id="one_unmet"),
|
||||
pytest.param(("outside_air_temperature", "exponential_backoff"), "7", id="two"),
|
||||
pytest.param(
|
||||
COOLING_CONDITIONS_NOT_COUNTED, "9", id="settings_are_not_counted"
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_weheat_discover")
|
||||
async def test_cooling_conditions_met(
|
||||
hass: HomeAssistant,
|
||||
mock_weheat_heat_pump: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
unmet: tuple[str, ...],
|
||||
expected: str,
|
||||
) -> None:
|
||||
"""Test how many start conditions are met is counted as the portal counts."""
|
||||
mock_weheat_heat_pump.cooling_start_conditions = _start_conditions(*unmet)
|
||||
|
||||
with patch("homeassistant.components.weheat.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get("sensor.test_model_cooling_conditions_met")
|
||||
|
||||
assert state.state == expected
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == f"of {CONDITIONS_COUNTED}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("cooling_state", "heat_pump_state"),
|
||||
[
|
||||
pytest.param(HeatPump.CoolingState.ACTIVE, HeatPump.State.COOLING, id="active"),
|
||||
pytest.param(HeatPump.CoolingState.IDLE, HeatPump.State.COOLING, id="idle"),
|
||||
# the heat pump reports the water check as a state of its own, so the
|
||||
# overall state is not cooling while the cooling cycle still is
|
||||
pytest.param(
|
||||
HeatPump.CoolingState.WATER_CHECK,
|
||||
HeatPump.State.WATER_CHECK,
|
||||
id="water_check",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_weheat_discover")
|
||||
async def test_cooling_conditions_met_is_unknown_while_cooling(
|
||||
hass: HomeAssistant,
|
||||
mock_weheat_heat_pump: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
cooling_state: HeatPump.CoolingState,
|
||||
heat_pump_state: HeatPump.State,
|
||||
) -> None:
|
||||
"""Test the count is not reported once a cooling cycle is running."""
|
||||
mock_weheat_heat_pump.heat_pump_state = heat_pump_state
|
||||
mock_weheat_heat_pump.cooling_state = cooling_state
|
||||
mock_weheat_heat_pump.cooling_start_conditions = _start_conditions("demand")
|
||||
|
||||
with patch("homeassistant.components.weheat.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert (
|
||||
hass.states.get("sensor.test_model_cooling_conditions_met").state
|
||||
== STATE_UNKNOWN
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("sensor", "attribute", "stale"),
|
||||
[
|
||||
pytest.param(
|
||||
"cooling_pause_reason",
|
||||
"cooling_pause_reason",
|
||||
HeatPump.CoolingPauseReason.ROOM_TEMPERATURE_TOO_LOW,
|
||||
id="pause_reason",
|
||||
),
|
||||
pytest.param(
|
||||
"cooling_stop_reason",
|
||||
"cooling_stop_reason",
|
||||
HeatPump.CoolingStopReason.HEAT_PUMP_CONTROL,
|
||||
id="stop_reason",
|
||||
),
|
||||
pytest.param(
|
||||
"cooling_blocked_by",
|
||||
"cooling_start_conditions",
|
||||
dict.fromkeys(HeatPump.COOLING_START_CONDITION_BITS, False),
|
||||
id="blocked_by",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_weheat_discover")
|
||||
async def test_stale_cooling_reasons_are_not_reported_while_cooling(
|
||||
hass: HomeAssistant,
|
||||
mock_weheat_heat_pump: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
sensor: str,
|
||||
attribute: str,
|
||||
stale: object,
|
||||
) -> None:
|
||||
"""Test what held cooling off is not reported once a cycle is running."""
|
||||
mock_weheat_heat_pump.heat_pump_state = HeatPump.State.COOLING
|
||||
mock_weheat_heat_pump.cooling_state = HeatPump.CoolingState.ACTIVE
|
||||
setattr(mock_weheat_heat_pump, attribute, stale)
|
||||
|
||||
with patch("homeassistant.components.weheat.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert hass.states.get(f"sensor.test_model_{sensor}").state == "none"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_weheat_discover")
|
||||
async def test_a_heat_pump_without_cooling_gets_no_cooling_sensors(
|
||||
hass: HomeAssistant,
|
||||
mock_weheat_heat_pump: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test a heat pump that does not cool gets no cooling sensors at all."""
|
||||
mock_weheat_heat_pump.cooling_activity = None
|
||||
|
||||
with patch("homeassistant.components.weheat.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert [
|
||||
entity_id
|
||||
for entity_id in hass.states.async_entity_ids(SENSOR_DOMAIN)
|
||||
if entity_id in COOLING_SENSORS
|
||||
] == []
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_weheat_discover")
|
||||
async def test_cooling_without_start_conditions(
|
||||
hass: HomeAssistant,
|
||||
mock_weheat_heat_pump: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test a cooling heat pump that reports no start conditions keeps its sensors."""
|
||||
mock_weheat_heat_pump.cooling_start_conditions = None
|
||||
|
||||
with patch("homeassistant.components.weheat.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert (
|
||||
hass.states.get("sensor.test_model_cooling_blocked_by").state == STATE_UNKNOWN
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.test_model_cooling_conditions_met").state
|
||||
== STATE_UNKNOWN
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.test_model_cooling_wait_until").state == STATE_UNKNOWN
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.test_model_cooling_pause_reason").state != STATE_UNKNOWN
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user