mirror of
https://github.com/home-assistant/core.git
synced 2026-09-25 07:51:46 -05:00
Add diagnostic subsystem sensors to Duco (#174577)
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"""Binary sensor platform for the Duco integration."""
|
||||
|
||||
from typing import override
|
||||
|
||||
from duco_connectivity.models import DiagStatus, Node
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import BOX_NODE_ID
|
||||
from .coordinator import DucoConfigEntry, DucoCoordinator
|
||||
from .entity import DucoEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
DIAGNOSTIC_STATUS_TO_PROBLEM = {
|
||||
DiagStatus.DISABLED: True,
|
||||
DiagStatus.ERROR: True,
|
||||
DiagStatus.OK: False,
|
||||
}
|
||||
|
||||
|
||||
# Ventilation and filter problems are directly actionable. Model-specific
|
||||
# subsystem diagnostics remain opt-in.
|
||||
DIAGNOSTIC_BINARY_SENSOR_DESCRIPTIONS: dict[str, BinarySensorEntityDescription] = {
|
||||
"Filter": BinarySensorEntityDescription(
|
||||
key="filter",
|
||||
translation_key="diagnostic_filter",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"SunCtrl": BinarySensorEntityDescription(
|
||||
key="sun_control",
|
||||
translation_key="diagnostic_sun_control",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
"VentCool": BinarySensorEntityDescription(
|
||||
key="ventilation_cooling",
|
||||
translation_key="diagnostic_ventilation_cooling",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
"Ventilation": BinarySensorEntityDescription(
|
||||
key="ventilation",
|
||||
translation_key="diagnostic_ventilation",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: DucoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Duco diagnostic binary sensors."""
|
||||
coordinator = entry.runtime_data
|
||||
added_components: set[str] = set()
|
||||
|
||||
@callback
|
||||
def _async_add_new_entities() -> None:
|
||||
"""Add newly reported diagnostic subsystems."""
|
||||
if (box_node := coordinator.data.nodes.get(BOX_NODE_ID)) is None:
|
||||
return
|
||||
|
||||
new_entities: list[DucoDiagnosticBinarySensorEntity] = []
|
||||
for component in coordinator.data.diagnostic_subsystems:
|
||||
if component in added_components:
|
||||
continue
|
||||
# Only expose components whose problem semantics are confirmed.
|
||||
if (
|
||||
description := DIAGNOSTIC_BINARY_SENSOR_DESCRIPTIONS.get(component)
|
||||
) is None:
|
||||
continue
|
||||
added_components.add(component)
|
||||
new_entities.append(
|
||||
DucoDiagnosticBinarySensorEntity(
|
||||
coordinator, box_node, component, description
|
||||
)
|
||||
)
|
||||
|
||||
if new_entities:
|
||||
async_add_entities(new_entities)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_entities))
|
||||
_async_add_new_entities()
|
||||
|
||||
|
||||
class DucoDiagnosticBinarySensorEntity(DucoEntity, BinarySensorEntity):
|
||||
"""Binary sensor for a Duco diagnostic subsystem."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DucoCoordinator,
|
||||
node: Node,
|
||||
component: str,
|
||||
description: BinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the diagnostic binary sensor."""
|
||||
self.entity_description = description
|
||||
self._component = component
|
||||
super().__init__(coordinator, node)
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.config_entry.unique_id}_{node.node_id}_{description.key}"
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""Return whether current diagnostic data is available."""
|
||||
return super().available and self.coordinator.data.diagnostics_available
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return whether the diagnostic subsystem reports a problem."""
|
||||
if (
|
||||
status := self.coordinator.data.diagnostic_subsystems.get(self._component)
|
||||
) is None:
|
||||
return None
|
||||
return DIAGNOSTIC_STATUS_TO_PROBLEM.get(status)
|
||||
@@ -7,7 +7,13 @@ from duco_connectivity.models import NodeType
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "duco"
|
||||
PLATFORMS = [Platform.FAN, Platform.NUMBER, Platform.SELECT, Platform.SENSOR]
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.FAN,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
BOX_NODE_ID = 1
|
||||
VENTILATION_CAPABLE_NODE_TYPES: tuple[NodeType, ...] = (
|
||||
|
||||
@@ -15,6 +15,7 @@ from duco_connectivity.exceptions import (
|
||||
from duco_connectivity.models import (
|
||||
BoardInfo,
|
||||
BypassSupplyTemperatureTarget,
|
||||
DiagStatus,
|
||||
Node,
|
||||
NodeListActionItemList,
|
||||
NodeName,
|
||||
@@ -41,6 +42,8 @@ class DucoData:
|
||||
|
||||
nodes: dict[int, Node]
|
||||
node_actions: NodeListActionItemList
|
||||
diagnostics_available: bool
|
||||
diagnostic_subsystems: dict[str, DiagStatus | None]
|
||||
rssi_wifi: int | None
|
||||
time_filter_remain: int | None
|
||||
ventilation_temperatures: VentilationTemperatureInfo | None
|
||||
@@ -227,6 +230,25 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
|
||||
else:
|
||||
rssi_wifi = lan_info.rssi_wifi
|
||||
|
||||
# Diagnostics only back optional binary sensors. Preserve known components
|
||||
# but mark their data unavailable without failing the shared coordinator.
|
||||
diagnostics_were_available = (
|
||||
self.data is None or self.data.diagnostics_available
|
||||
)
|
||||
diagnostics_available = True
|
||||
diagnostics_error: DucoError | None = None
|
||||
diagnostic_subsystems = self.data.diagnostic_subsystems if self.data else {}
|
||||
try:
|
||||
diagnostic_info = await self.client.async_get_diagnostics_info()
|
||||
except DucoError as err:
|
||||
diagnostics_available = False
|
||||
diagnostics_error = err
|
||||
else:
|
||||
diagnostic_subsystems = {
|
||||
diagnostic.component: diagnostic.status
|
||||
for diagnostic in diagnostic_info.diagnostic_subsystems
|
||||
}
|
||||
|
||||
# Heat recovery info only backs the optional filter timer sensor, so
|
||||
# failures on this supplemental endpoint should not make the primary
|
||||
# node entities unavailable. A None result leaves the sensor absent
|
||||
@@ -261,9 +283,16 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
|
||||
translation_key="api_error",
|
||||
) from err
|
||||
|
||||
if diagnostics_available and not diagnostics_were_available:
|
||||
_LOGGER.info("Duco diagnostics are available again")
|
||||
elif not diagnostics_available and diagnostics_were_available:
|
||||
_LOGGER.info("Duco diagnostics are unavailable: %s", diagnostics_error)
|
||||
|
||||
return DucoData(
|
||||
nodes={node.node_id: node for node in nodes},
|
||||
node_actions=node_actions,
|
||||
diagnostics_available=diagnostics_available,
|
||||
diagnostic_subsystems=diagnostic_subsystems,
|
||||
rssi_wifi=rssi_wifi,
|
||||
time_filter_remain=time_filter_remain,
|
||||
ventilation_temperatures=ventilation_temperatures,
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"diagnostic_filter": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
"diagnostic_sun_control": {
|
||||
"default": "mdi:blinds-horizontal-closed"
|
||||
},
|
||||
"diagnostic_ventilation": {
|
||||
"default": "mdi:fan-alert"
|
||||
},
|
||||
"diagnostic_ventilation_cooling": {
|
||||
"default": "mdi:home-thermometer-outline"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"filter_remaining": {
|
||||
"default": "mdi:air-filter"
|
||||
|
||||
@@ -42,7 +42,7 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable:
|
||||
status: done
|
||||
comment: Handled by the DataUpdateCoordinator.
|
||||
comment: Handled by the coordinator, including isolated supplemental endpoints.
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
|
||||
@@ -35,6 +35,20 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"diagnostic_filter": {
|
||||
"name": "Filter"
|
||||
},
|
||||
"diagnostic_sun_control": {
|
||||
"name": "Sun control"
|
||||
},
|
||||
"diagnostic_ventilation": {
|
||||
"name": "Ventilation"
|
||||
},
|
||||
"diagnostic_ventilation_cooling": {
|
||||
"name": "Ventilation cooling"
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"ventilation": {
|
||||
"state_attributes": {
|
||||
|
||||
@@ -16,6 +16,7 @@ from duco_connectivity import (
|
||||
ConfigNodeOverview,
|
||||
ConfigValueString,
|
||||
DiagComponent,
|
||||
DiagInfo,
|
||||
KnownActionName,
|
||||
LanInfo,
|
||||
Node,
|
||||
@@ -327,6 +328,9 @@ def mock_duco_client(
|
||||
client.async_get_diagnostics.return_value = [
|
||||
DiagComponent(component="Ventilation", status="Ok")
|
||||
]
|
||||
client.async_get_diagnostics_info.return_value = DiagInfo(
|
||||
diagnostic_subsystems=(DiagComponent(component="Ventilation", status="Ok"),)
|
||||
)
|
||||
client.async_get_write_requests_remaining.return_value = 100
|
||||
yield client
|
||||
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
"""Tests for the Duco binary sensor platform."""
|
||||
|
||||
import logging
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from duco_connectivity import (
|
||||
DiagComponent,
|
||||
DiagInfo,
|
||||
DucoConnectionError,
|
||||
DucoError,
|
||||
Node,
|
||||
)
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.duco.const import BOX_NODE_ID, SCAN_INTERVAL
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_platform_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
VENTILATION_PROBLEM_ENTITY_ID = "binary_sensor.living_ventilation"
|
||||
|
||||
DIAGNOSTIC_ERROR_TYPES = [
|
||||
pytest.param(DucoConnectionError, id="connection_error"),
|
||||
pytest.param(DucoError, id="duco_error"),
|
||||
]
|
||||
|
||||
|
||||
async def _async_refresh(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None:
|
||||
"""Trigger a coordinator refresh."""
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
|
||||
async def test_diagnostic_binary_sensor_entity_registry_defaults(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the diagnostic binary sensor entity registry defaults."""
|
||||
mock_duco_client.async_get_diagnostics_info.return_value = DiagInfo(
|
||||
diagnostic_subsystems=(
|
||||
DiagComponent(component="Ventilation", status="Ok"),
|
||||
DiagComponent(component="Filter", status="Ok"),
|
||||
DiagComponent(component="VentCool", status="Ok"),
|
||||
DiagComponent(component="SunCtrl", status="Ok"),
|
||||
)
|
||||
)
|
||||
|
||||
await setup_platform_integration(hass, mock_config_entry, [Platform.BINARY_SENSOR])
|
||||
|
||||
for entity_id, disabled_by in (
|
||||
(
|
||||
"binary_sensor.living_sun_control",
|
||||
er.RegistryEntryDisabler.INTEGRATION,
|
||||
),
|
||||
(
|
||||
"binary_sensor.living_ventilation_cooling",
|
||||
er.RegistryEntryDisabler.INTEGRATION,
|
||||
),
|
||||
("binary_sensor.living_filter", None),
|
||||
(VENTILATION_PROBLEM_ENTITY_ID, None),
|
||||
):
|
||||
assert (entry := entity_registry.async_get(entity_id)) is not None
|
||||
assert entry.disabled_by is disabled_by
|
||||
assert entry.original_device_class is BinarySensorDeviceClass.PROBLEM
|
||||
|
||||
|
||||
async def test_unknown_diagnostic_subsystem_is_ignored(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test an unknown diagnostic subsystem is not exposed."""
|
||||
mock_duco_client.async_get_diagnostics_info.return_value = DiagInfo(
|
||||
diagnostic_subsystems=(DiagComponent(component="Future Mode", status="Error"),)
|
||||
)
|
||||
|
||||
await setup_platform_integration(hass, mock_config_entry, [Platform.BINARY_SENSOR])
|
||||
|
||||
assert entity_registry.async_get("binary_sensor.living_future_mode") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw_status", "expected_state"),
|
||||
[
|
||||
pytest.param("Ok", STATE_OFF, id="ok"),
|
||||
pytest.param("Error", STATE_ON, id="error"),
|
||||
pytest.param("Disable", STATE_ON, id="disabled"),
|
||||
pytest.param("FutureState", STATE_UNKNOWN, id="unknown"),
|
||||
],
|
||||
)
|
||||
async def test_diagnostic_binary_sensor_problem_state(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
raw_status: str,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test diagnostic statuses map to the expected problem state."""
|
||||
mock_duco_client.async_get_diagnostics_info.return_value = DiagInfo(
|
||||
diagnostic_subsystems=(
|
||||
DiagComponent(component="Ventilation", status=raw_status),
|
||||
)
|
||||
)
|
||||
|
||||
await setup_platform_integration(hass, mock_config_entry, [Platform.BINARY_SENSOR])
|
||||
|
||||
assert (state := hass.states.get(VENTILATION_PROBLEM_ENTITY_ID)) is not None
|
||||
assert state.state == expected_state
|
||||
assert state.attributes["device_class"] == BinarySensorDeviceClass.PROBLEM
|
||||
|
||||
|
||||
async def test_diagnostic_binary_sensors_added_after_initial_empty_response(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test diagnostic binary sensors can be added after an empty response."""
|
||||
mock_duco_client.async_get_diagnostics_info.return_value = DiagInfo()
|
||||
|
||||
await setup_platform_integration(hass, mock_config_entry, [Platform.BINARY_SENSOR])
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert hass.states.get(VENTILATION_PROBLEM_ENTITY_ID) is None
|
||||
|
||||
mock_duco_client.async_get_diagnostics_info.return_value = DiagInfo(
|
||||
diagnostic_subsystems=(DiagComponent(component="Ventilation", status="Error"),)
|
||||
)
|
||||
|
||||
await _async_refresh(hass, freezer)
|
||||
|
||||
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_ON)
|
||||
|
||||
mock_duco_client.async_get_diagnostics_info.return_value = DiagInfo(
|
||||
diagnostic_subsystems=(
|
||||
DiagComponent(component="Ventilation", status="Error"),
|
||||
DiagComponent(component="Filter", status="Ok"),
|
||||
)
|
||||
)
|
||||
|
||||
await _async_refresh(hass, freezer)
|
||||
|
||||
assert hass.states.is_state("binary_sensor.living_filter", STATE_OFF)
|
||||
|
||||
|
||||
async def test_diagnostic_binary_sensors_wait_for_box_node(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
mock_sensor_nodes: list[Node],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test diagnostic binary sensors are added when the box reappears."""
|
||||
mock_duco_client.async_get_nodes.return_value = [
|
||||
node for node in mock_sensor_nodes if node.node_id != BOX_NODE_ID
|
||||
]
|
||||
|
||||
await setup_platform_integration(hass, mock_config_entry, [Platform.BINARY_SENSOR])
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert hass.states.get(VENTILATION_PROBLEM_ENTITY_ID) is None
|
||||
|
||||
mock_duco_client.async_get_nodes.return_value = mock_sensor_nodes
|
||||
await _async_refresh(hass, freezer)
|
||||
|
||||
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_OFF)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"diagnostic_subsystems",
|
||||
[
|
||||
pytest.param((), id="missing"),
|
||||
pytest.param(
|
||||
(DiagComponent(component="Ventilation", status="Unexpected"),),
|
||||
id="unknown_status",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_diagnostic_binary_sensor_becomes_unknown_without_known_status(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
diagnostic_subsystems: tuple[DiagComponent, ...],
|
||||
) -> None:
|
||||
"""Test diagnostic binary sensors report unknown without a known status."""
|
||||
await setup_platform_integration(hass, mock_config_entry, [Platform.BINARY_SENSOR])
|
||||
|
||||
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_OFF)
|
||||
|
||||
mock_duco_client.async_get_diagnostics_info.return_value = DiagInfo(
|
||||
diagnostic_subsystems=diagnostic_subsystems
|
||||
)
|
||||
|
||||
await _async_refresh(hass, freezer)
|
||||
|
||||
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_UNKNOWN)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exception_type", DIAGNOSTIC_ERROR_TYPES)
|
||||
async def test_diagnostics_refresh_failure_is_isolated_and_recovers(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
mock_sensor_nodes: list[Node],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
exception_type: type[DucoError],
|
||||
) -> None:
|
||||
"""Test a diagnostics refresh failure is isolated and recovers."""
|
||||
mock_duco_client.async_get_nodes.return_value = mock_sensor_nodes
|
||||
await setup_platform_integration(
|
||||
hass, mock_config_entry, [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
)
|
||||
|
||||
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_OFF)
|
||||
|
||||
mock_duco_client.async_get_diagnostics_info.side_effect = exception_type("error")
|
||||
|
||||
await _async_refresh(hass, freezer)
|
||||
|
||||
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_UNAVAILABLE)
|
||||
assert hass.states.is_state("sensor.office_co2_carbon_dioxide", "405")
|
||||
|
||||
mock_duco_client.async_get_diagnostics_info.side_effect = None
|
||||
await _async_refresh(hass, freezer)
|
||||
|
||||
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_OFF)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exception_type", DIAGNOSTIC_ERROR_TYPES)
|
||||
async def test_initial_diagnostics_failure_is_isolated_and_recovers(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
mock_sensor_nodes: list[Node],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
exception_type: type[DucoError],
|
||||
) -> None:
|
||||
"""Test an initial diagnostics failure is isolated and recovers."""
|
||||
mock_duco_client.async_get_nodes.return_value = mock_sensor_nodes
|
||||
mock_duco_client.async_get_diagnostics_info.side_effect = exception_type("error")
|
||||
|
||||
await setup_platform_integration(
|
||||
hass, mock_config_entry, [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert hass.states.get(VENTILATION_PROBLEM_ENTITY_ID) is None
|
||||
assert hass.states.is_state("sensor.office_co2_carbon_dioxide", "405")
|
||||
|
||||
mock_duco_client.async_get_diagnostics_info.side_effect = None
|
||||
await _async_refresh(hass, freezer)
|
||||
|
||||
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_OFF)
|
||||
|
||||
|
||||
async def test_diagnostics_availability_transitions_logged(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test diagnostics availability transitions are logged once."""
|
||||
caplog.set_level(logging.INFO, logger="homeassistant.components.duco.coordinator")
|
||||
mock_duco_client.async_get_diagnostics_info.side_effect = DucoError("error")
|
||||
|
||||
await setup_platform_integration(hass, mock_config_entry, [Platform.BINARY_SENSOR])
|
||||
await _async_refresh(hass, freezer)
|
||||
|
||||
mock_duco_client.async_get_diagnostics_info.side_effect = None
|
||||
await _async_refresh(hass, freezer)
|
||||
await _async_refresh(hass, freezer)
|
||||
|
||||
assert [
|
||||
record.message
|
||||
for record in caplog.records
|
||||
if record.name == "homeassistant.components.duco.coordinator"
|
||||
] == [
|
||||
"Duco diagnostics are unavailable: error",
|
||||
"Duco diagnostics are available again",
|
||||
]
|
||||
@@ -195,15 +195,14 @@ async def test_iaq_sensor_entities_disabled_by_default(
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_diagnostic_sensor_entities_disabled_by_default(
|
||||
async def test_rssi_sensor_disabled_by_default(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test that diagnostic sensor entities are disabled by default."""
|
||||
for entity_id in ("sensor.living_signal_strength",):
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry is not None
|
||||
assert entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION
|
||||
"""Test that the RSSI sensor is disabled by default."""
|
||||
entry = entity_registry.async_get("sensor.living_signal_strength")
|
||||
assert entry is not None
|
||||
assert entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
|
||||
Reference in New Issue
Block a user