mirror of
https://github.com/home-assistant/core.git
synced 2026-08-30 18:16:17 +01:00
Fix Duco VLV nodes not creating CO2 and humidity sensors (#171182)
This commit is contained in:
@@ -95,7 +95,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
value_fn=lambda node: node.sensor.co2 if node.sensor else None,
|
||||
node_types=(NodeType.UCCO2,),
|
||||
node_types=(NodeType.UCCO2, NodeType.VLVCO2, NodeType.VLVCO2RH),
|
||||
),
|
||||
DucoSensorEntityDescription(
|
||||
key="iaq_co2",
|
||||
@@ -104,7 +104,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda node: node.sensor.iaq_co2 if node.sensor else None,
|
||||
node_types=(NodeType.UCCO2,),
|
||||
node_types=(NodeType.UCCO2, NodeType.VLVCO2, NodeType.VLVCO2RH),
|
||||
),
|
||||
DucoSensorEntityDescription(
|
||||
key="humidity",
|
||||
@@ -112,7 +112,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda node: node.sensor.rh if node.sensor else None,
|
||||
node_types=(NodeType.BSRH, NodeType.UCRH),
|
||||
node_types=(NodeType.BSRH, NodeType.UCRH, NodeType.VLVRH, NodeType.VLVCO2RH),
|
||||
),
|
||||
DucoSensorEntityDescription(
|
||||
key="iaq_rh",
|
||||
@@ -121,7 +121,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda node: node.sensor.iaq_rh if node.sensor else None,
|
||||
node_types=(NodeType.BSRH, NodeType.UCRH),
|
||||
node_types=(NodeType.BSRH, NodeType.UCRH, NodeType.VLVRH, NodeType.VLVCO2RH),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Fixtures for Duco tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from duco_connectivity import (
|
||||
@@ -12,6 +13,7 @@ from duco_connectivity import (
|
||||
LanInfo,
|
||||
Node,
|
||||
NodeGeneralInfo,
|
||||
NodeMotorStateInfo,
|
||||
NodeSensorInfo,
|
||||
NodeVentilationInfo,
|
||||
)
|
||||
@@ -21,7 +23,7 @@ from homeassistant.components.duco.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, load_json_array_fixture
|
||||
|
||||
TEST_HOST = "192.168.1.100"
|
||||
TEST_MAC = "aa:bb:cc:dd:ee:ff"
|
||||
@@ -29,6 +31,30 @@ TEST_MAC = "aa:bb:cc:dd:ee:ff"
|
||||
USER_INPUT = {CONF_HOST: TEST_HOST}
|
||||
|
||||
|
||||
def _node_from_dict(data: dict[str, Any]) -> Node:
|
||||
"""Convert a node fixture payload into a Duco node model."""
|
||||
ventilation = data.get("ventilation")
|
||||
sensor = data.get("sensor")
|
||||
motor_state = data.get("motor_state")
|
||||
|
||||
return Node(
|
||||
node_id=data["node_id"],
|
||||
general=NodeGeneralInfo(**data["general"]),
|
||||
ventilation=NodeVentilationInfo(**ventilation)
|
||||
if ventilation is not None
|
||||
else None,
|
||||
sensor=NodeSensorInfo(**sensor) if sensor is not None else None,
|
||||
motor_state=NodeMotorStateInfo(**motor_state)
|
||||
if motor_state is not None
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
def load_nodes_fixture(filename: str) -> list[Node]:
|
||||
"""Load nodes from a JSON fixture file."""
|
||||
return [_node_from_dict(node) for node in load_json_array_fixture(filename, DOMAIN)]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
@@ -199,6 +225,20 @@ def mock_nodes() -> list[Node]:
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_sensor_nodes(mock_nodes: list[Node]) -> list[Node]:
|
||||
"""Return sensor test nodes including VLV examples."""
|
||||
return [*mock_nodes, *load_nodes_fixture("sensor_nodes.json")]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dynamic_sensor_nodes() -> dict[int, Node]:
|
||||
"""Return dynamic sensor test nodes keyed by node ID."""
|
||||
return {
|
||||
node.node_id: node for node in load_nodes_fixture("dynamic_sensor_nodes.json")
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_duco_client(
|
||||
mock_api_info: ApiInfo,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
[
|
||||
{
|
||||
"node_id": 200,
|
||||
"general": {
|
||||
"node_type": "BSRH",
|
||||
"sub_type": 0,
|
||||
"network_type": "RF",
|
||||
"parent": 1,
|
||||
"asso": 1,
|
||||
"name": "New RH sensor",
|
||||
"identify": 0
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "AUTO",
|
||||
"time_state_remain": 0,
|
||||
"time_state_end": 0,
|
||||
"mode": "-",
|
||||
"flow_lvl_tgt": null
|
||||
},
|
||||
"sensor": {
|
||||
"co2": null,
|
||||
"iaq_co2": null,
|
||||
"rh": 55.0,
|
||||
"iaq_rh": 70,
|
||||
"temp": 21.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"node_id": 201,
|
||||
"general": {
|
||||
"node_type": "VLVCO2",
|
||||
"sub_type": 0,
|
||||
"network_type": "RF",
|
||||
"parent": 1,
|
||||
"asso": 1,
|
||||
"name": "New valve",
|
||||
"identify": 0
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "AUTO",
|
||||
"time_state_remain": 0,
|
||||
"time_state_end": 0,
|
||||
"mode": "-",
|
||||
"flow_lvl_tgt": null
|
||||
},
|
||||
"sensor": {
|
||||
"co2": 575,
|
||||
"iaq_co2": 82,
|
||||
"rh": null,
|
||||
"iaq_rh": null,
|
||||
"temp": 20.5
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,80 @@
|
||||
[
|
||||
{
|
||||
"node_id": 60,
|
||||
"general": {
|
||||
"node_type": "VLVRH",
|
||||
"sub_type": 0,
|
||||
"network_type": "WI",
|
||||
"parent": 1,
|
||||
"asso": 1,
|
||||
"name": "Bedroom valve",
|
||||
"identify": 0
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "AUTO",
|
||||
"time_state_remain": 0,
|
||||
"time_state_end": 0,
|
||||
"mode": "-",
|
||||
"flow_lvl_tgt": null
|
||||
},
|
||||
"sensor": {
|
||||
"co2": null,
|
||||
"iaq_co2": null,
|
||||
"rh": 48.0,
|
||||
"iaq_rh": 88,
|
||||
"temp": 21.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"node_id": 61,
|
||||
"general": {
|
||||
"node_type": "VLVCO2",
|
||||
"sub_type": 0,
|
||||
"network_type": "WI",
|
||||
"parent": 1,
|
||||
"asso": 1,
|
||||
"name": "Hall valve",
|
||||
"identify": 0
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "AUTO",
|
||||
"time_state_remain": 0,
|
||||
"time_state_end": 0,
|
||||
"mode": "-",
|
||||
"flow_lvl_tgt": null
|
||||
},
|
||||
"sensor": {
|
||||
"co2": 512,
|
||||
"iaq_co2": 76,
|
||||
"rh": null,
|
||||
"iaq_rh": null,
|
||||
"temp": 20.1
|
||||
}
|
||||
},
|
||||
{
|
||||
"node_id": 62,
|
||||
"general": {
|
||||
"node_type": "VLVCO2RH",
|
||||
"sub_type": 0,
|
||||
"network_type": "WI",
|
||||
"parent": 1,
|
||||
"asso": 1,
|
||||
"name": "Study valve",
|
||||
"identify": 0
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "AUTO",
|
||||
"time_state_remain": 0,
|
||||
"time_state_end": 0,
|
||||
"mode": "-",
|
||||
"flow_lvl_tgt": null
|
||||
},
|
||||
"sensor": {
|
||||
"co2": 645,
|
||||
"iaq_co2": 95,
|
||||
"rh": 54.0,
|
||||
"iaq_rh": 92,
|
||||
"temp": 20.7
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -108,6 +108,224 @@
|
||||
'state': '85',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bedroom_valve_humidity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'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.bedroom_valve_humidity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Humidity',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Humidity',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_60_humidity',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bedroom_valve_humidity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'humidity',
|
||||
'friendly_name': 'Bedroom valve Humidity',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.bedroom_valve_humidity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '48.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bedroom_valve_humidity_air_quality_index-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'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.bedroom_valve_humidity_air_quality_index',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Humidity air quality index',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Humidity air quality index',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'iaq_rh',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_60_iaq_rh',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bedroom_valve_humidity_air_quality_index-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Bedroom valve Humidity air quality index',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.bedroom_valve_humidity_air_quality_index',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '88',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.hall_valve_carbon_dioxide-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'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.hall_valve_carbon_dioxide',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Carbon dioxide',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.CO2: 'carbon_dioxide'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Carbon dioxide',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_61_co2',
|
||||
'unit_of_measurement': 'ppm',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.hall_valve_carbon_dioxide-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'carbon_dioxide',
|
||||
'friendly_name': 'Hall valve Carbon dioxide',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'ppm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hall_valve_carbon_dioxide',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '512',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.hall_valve_co2_air_quality_index-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'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.hall_valve_co2_air_quality_index',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'CO2 air quality index',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'CO2 air quality index',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'iaq_co2',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_61_iaq_co2',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.hall_valve_co2_air_quality_index-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Hall valve CO2 air quality index',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hall_valve_co2_air_quality_index',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '76',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.kitchen_rh_humidity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
@@ -581,3 +799,221 @@
|
||||
'state': '80',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_carbon_dioxide-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'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.study_valve_carbon_dioxide',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Carbon dioxide',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.CO2: 'carbon_dioxide'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Carbon dioxide',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_62_co2',
|
||||
'unit_of_measurement': 'ppm',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_carbon_dioxide-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'carbon_dioxide',
|
||||
'friendly_name': 'Study valve Carbon dioxide',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'ppm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.study_valve_carbon_dioxide',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '645',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_co2_air_quality_index-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'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.study_valve_co2_air_quality_index',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'CO2 air quality index',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'CO2 air quality index',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'iaq_co2',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_62_iaq_co2',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_co2_air_quality_index-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Study valve CO2 air quality index',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.study_valve_co2_air_quality_index',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '95',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_humidity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'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.study_valve_humidity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Humidity',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Humidity',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_62_humidity',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_humidity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'humidity',
|
||||
'friendly_name': 'Study valve Humidity',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.study_valve_humidity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '54.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_humidity_air_quality_index-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'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.study_valve_humidity_air_quality_index',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Humidity air quality index',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Humidity air quality index',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'iaq_rh',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_62_iaq_rh',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_humidity_air_quality_index-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Study valve Humidity air quality index',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.study_valve_humidity_air_quality_index',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '92',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -30,8 +30,10 @@ async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
mock_sensor_nodes: list[Node],
|
||||
) -> MockConfigEntry:
|
||||
"""Set up only the sensor platform for testing."""
|
||||
mock_duco_client.async_get_nodes.return_value = mock_sensor_nodes
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.duco.PLATFORMS", [Platform.SENSOR]):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
@@ -58,8 +60,12 @@ async def test_iaq_sensor_entities_disabled_by_default(
|
||||
"""Test that IAQ sensor entities are disabled by default."""
|
||||
for entity_id in (
|
||||
"sensor.bathroom_rh_humidity_air_quality_index",
|
||||
"sensor.bedroom_valve_humidity_air_quality_index",
|
||||
"sensor.hall_valve_co2_air_quality_index",
|
||||
"sensor.kitchen_rh_humidity_air_quality_index",
|
||||
"sensor.office_co2_co2_air_quality_index",
|
||||
"sensor.study_valve_co2_air_quality_index",
|
||||
"sensor.study_valve_humidity_air_quality_index",
|
||||
):
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry is not None
|
||||
@@ -136,58 +142,52 @@ async def test_lan_info_duco_error_marks_unavailable(
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("node_id", "expected_entity_id", "expected_state"),
|
||||
[
|
||||
(
|
||||
200,
|
||||
"sensor.new_rh_sensor_humidity",
|
||||
"55.0",
|
||||
),
|
||||
(
|
||||
201,
|
||||
"sensor.new_valve_carbon_dioxide",
|
||||
"575",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_new_node_added_dynamically(
|
||||
hass: HomeAssistant,
|
||||
mock_duco_client: AsyncMock,
|
||||
mock_nodes: list[Node],
|
||||
mock_sensor_nodes: list[Node],
|
||||
dynamic_sensor_nodes: dict[int, Node],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
node_id: int,
|
||||
expected_entity_id: str,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test a new node appearing in coordinator data creates entities automatically."""
|
||||
assert hass.states.get("sensor.new_rh_sensor_humidity") is None
|
||||
assert hass.states.get(expected_entity_id) is None
|
||||
|
||||
new_node = Node(
|
||||
node_id=200,
|
||||
general=NodeGeneralInfo(
|
||||
node_type="BSRH",
|
||||
sub_type=0,
|
||||
network_type="RF",
|
||||
parent=1,
|
||||
asso=1,
|
||||
name="New RH sensor",
|
||||
identify=0,
|
||||
),
|
||||
ventilation=NodeVentilationInfo(
|
||||
state="AUTO",
|
||||
time_state_remain=0,
|
||||
time_state_end=0,
|
||||
mode="-",
|
||||
flow_lvl_tgt=None,
|
||||
),
|
||||
sensor=NodeSensorInfo(
|
||||
co2=None,
|
||||
iaq_co2=None,
|
||||
rh=55.0,
|
||||
iaq_rh=70,
|
||||
temp=21.0,
|
||||
),
|
||||
)
|
||||
mock_duco_client.async_get_nodes.return_value = [*mock_nodes, new_node]
|
||||
new_node = dynamic_sensor_nodes[node_id]
|
||||
mock_duco_client.async_get_nodes.return_value = [*mock_sensor_nodes, new_node]
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("sensor.new_rh_sensor_humidity")
|
||||
state = hass.states.get(expected_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == "55.0"
|
||||
assert state.state == expected_state
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_deregistered_node_removes_device(
|
||||
hass: HomeAssistant,
|
||||
mock_duco_client: AsyncMock,
|
||||
mock_nodes: list[Node],
|
||||
mock_sensor_nodes: list[Node],
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
@@ -202,7 +202,7 @@ async def test_deregistered_node_removes_device(
|
||||
|
||||
# Simulate the firmware removing the deregistered node from the API response.
|
||||
mock_duco_client.async_get_nodes.return_value = [
|
||||
node for node in mock_nodes if node.node_id != 2
|
||||
node for node in mock_sensor_nodes if node.node_id != 2
|
||||
]
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
@@ -221,7 +221,7 @@ async def test_unknown_node_type_logs_warning_and_creates_no_entities(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
mock_nodes: list[Node],
|
||||
mock_sensor_nodes: list[Node],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
@@ -242,7 +242,7 @@ async def test_unknown_node_type_logs_warning_and_creates_no_entities(
|
||||
sensor=None,
|
||||
)
|
||||
|
||||
mock_duco_client.async_get_nodes.return_value = [*mock_nodes, unknown_node]
|
||||
mock_duco_client.async_get_nodes.return_value = [*mock_sensor_nodes, unknown_node]
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
@@ -262,7 +262,7 @@ async def test_previously_unknown_node_gets_entities_after_type_becomes_known(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
mock_nodes: list[Node],
|
||||
mock_sensor_nodes: list[Node],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test UNKNOWN type node is retried and gets entities once the type resolves."""
|
||||
@@ -291,7 +291,7 @@ async def test_previously_unknown_node_gets_entities_after_type_becomes_known(
|
||||
|
||||
# First poll: UNKNOWN type — no entities created.
|
||||
mock_duco_client.async_get_nodes.return_value = [
|
||||
*mock_nodes,
|
||||
*mock_sensor_nodes,
|
||||
_make_node(NodeType.UNKNOWN),
|
||||
]
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
@@ -301,7 +301,10 @@ async def test_previously_unknown_node_gets_entities_after_type_becomes_known(
|
||||
assert hass.states.get("sensor.future_sensor_humidity") is None
|
||||
|
||||
# Second poll: type now resolved — entities must be created.
|
||||
mock_duco_client.async_get_nodes.return_value = [*mock_nodes, _make_node("BSRH")]
|
||||
mock_duco_client.async_get_nodes.return_value = [
|
||||
*mock_sensor_nodes,
|
||||
_make_node("BSRH"),
|
||||
]
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
@@ -316,7 +319,7 @@ async def test_unknown_node_logged_at_debug(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
mock_nodes: list[Node],
|
||||
mock_sensor_nodes: list[Node],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
@@ -341,7 +344,7 @@ async def test_unknown_node_logged_at_debug(
|
||||
),
|
||||
sensor=NodeSensorInfo(co2=None, iaq_co2=None, rh=None, iaq_rh=None, temp=None),
|
||||
)
|
||||
mock_duco_client.async_get_nodes.return_value = [*mock_nodes, unknown_node]
|
||||
mock_duco_client.async_get_nodes.return_value = [*mock_sensor_nodes, unknown_node]
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="homeassistant.components.duco"):
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
@@ -362,11 +365,11 @@ async def test_unknown_node_logged_at_debug(
|
||||
async def test_ventilation_state_unknown_returns_state_unknown(
|
||||
hass: HomeAssistant,
|
||||
mock_duco_client: AsyncMock,
|
||||
mock_nodes: list[Node],
|
||||
mock_sensor_nodes: list[Node],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test that VentilationState.UNKNOWN makes the sensor report unknown."""
|
||||
box_node = next(n for n in mock_nodes if n.general.node_type == NodeType.BOX)
|
||||
box_node = next(n for n in mock_sensor_nodes if n.general.node_type == NodeType.BOX)
|
||||
updated_nodes = [
|
||||
Node(
|
||||
node_id=box_node.node_id,
|
||||
@@ -382,7 +385,7 @@ async def test_ventilation_state_unknown_returns_state_unknown(
|
||||
)
|
||||
if n is box_node
|
||||
else n
|
||||
for n in mock_nodes
|
||||
for n in mock_sensor_nodes
|
||||
]
|
||||
mock_duco_client.async_get_nodes.return_value = updated_nodes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user