mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Add binary_sensor platform to IntelliClima (#178154)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
dd58d215e1
commit
e2990016e4
@@ -7,9 +7,14 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import LOGGER
|
||||
from .coordinator import IntelliClimaConfigEntry, IntelliClimaCoordinator
|
||||
from .coordinator import (
|
||||
IntelliClimaConfigEntry,
|
||||
IntelliClimaCoordinator,
|
||||
IntelliClimaData,
|
||||
IntelliClimaFilterCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS = [Platform.FAN, Platform.SELECT, Platform.SENSOR]
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.FAN, Platform.SELECT, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -25,18 +30,24 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
# Create coordinator
|
||||
coordinator = IntelliClimaCoordinator(hass, entry, api)
|
||||
devices_coordinator = IntelliClimaCoordinator(hass, entry, api)
|
||||
|
||||
# Fetch initial data
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
await devices_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
LOGGER.debug(
|
||||
"Discovered %d IntelliClima VMC device(s)",
|
||||
len(coordinator.data.ecocomfort2_devices),
|
||||
len(devices_coordinator.data.ecocomfort2_devices),
|
||||
)
|
||||
|
||||
# Store coordinator
|
||||
entry.runtime_data = coordinator
|
||||
device_serials = [
|
||||
device.crono_sn
|
||||
for device in devices_coordinator.data.ecocomfort2_devices.values()
|
||||
]
|
||||
filter_coordinator = IntelliClimaFilterCoordinator(hass, entry, api, device_serials)
|
||||
await filter_coordinator.async_refresh()
|
||||
|
||||
entry.runtime_data = IntelliClimaData(devices_coordinator, filter_coordinator)
|
||||
|
||||
# Set up platforms
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Support for IntelliClima Binary Sensors."""
|
||||
|
||||
from typing import override
|
||||
|
||||
from pyintelliclima.intelliclima_types import IntelliClimaECO
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import IntelliClimaConfigEntry, IntelliClimaFilterCoordinator
|
||||
from .entity import eco_device_info
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: IntelliClimaConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the IntelliClima binary sensor platform."""
|
||||
data = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
IntelliClimaFilterCleaningBinarySensor(
|
||||
coordinator=data.filter_coordinator, device=ecocomfort2
|
||||
)
|
||||
for ecocomfort2 in data.devices_coordinator.data.ecocomfort2_devices.values()
|
||||
)
|
||||
|
||||
|
||||
class IntelliClimaFilterCleaningBinarySensor(
|
||||
CoordinatorEntity[IntelliClimaFilterCoordinator], BinarySensorEntity
|
||||
):
|
||||
"""Binary sensor indicating whether the device's filter needs cleaning."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "filter_cleaning"
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_device_class = BinarySensorDeviceClass.PROBLEM
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: IntelliClimaFilterCoordinator,
|
||||
device: IntelliClimaECO,
|
||||
) -> None:
|
||||
"""Class initializer."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_device_info = eco_device_info(device)
|
||||
self._device_sn = device.crono_sn
|
||||
self._attr_unique_id = f"{device.id}_filter_cleaning"
|
||||
|
||||
@property
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
device_data = (self.coordinator.data or {}).get(self._device_sn)
|
||||
return super().available and device_data is not None and device_data.is_active
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the filter needs cleaning."""
|
||||
device_data = (self.coordinator.data or {}).get(self._device_sn)
|
||||
if device_data is None or not device_data.is_active:
|
||||
return None
|
||||
return device_data.change_filter
|
||||
@@ -9,3 +9,6 @@ DOMAIN = "intelliclima"
|
||||
|
||||
# Update interval
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
# Filter status is expensive to compute cloud-side, so it's polled far less often.
|
||||
FILTER_SCAN_INTERVAL = timedelta(days=1)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
"""DataUpdateCoordinator for IntelliClima."""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from typing import override
|
||||
|
||||
from pyintelliclima import IntelliClimaAPI, IntelliClimaAPIError, IntelliClimaDevices
|
||||
from pyintelliclima.intelliclima_types import IntelliClimaFilterStatus
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, FILTER_SCAN_INTERVAL, LOGGER
|
||||
|
||||
type IntelliClimaConfigEntry = ConfigEntry[IntelliClimaCoordinator]
|
||||
type IntelliClimaConfigEntry = ConfigEntry[IntelliClimaData]
|
||||
|
||||
|
||||
class IntelliClimaCoordinator(DataUpdateCoordinator[IntelliClimaDevices]):
|
||||
@@ -47,3 +50,56 @@ class IntelliClimaCoordinator(DataUpdateCoordinator[IntelliClimaDevices]):
|
||||
|
||||
except IntelliClimaAPIError as err:
|
||||
raise UpdateFailed(f"Failed to update data: {err}") from err
|
||||
|
||||
|
||||
class IntelliClimaFilterCoordinator(
|
||||
DataUpdateCoordinator[dict[str, IntelliClimaFilterStatus]]
|
||||
):
|
||||
"""Coordinator to manage fetching IntelliClima filter status, polled once a day."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: IntelliClimaConfigEntry,
|
||||
api: IntelliClimaAPI,
|
||||
device_serials: list[str],
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
name=f"{DOMAIN}_filter",
|
||||
update_interval=FILTER_SCAN_INTERVAL,
|
||||
config_entry=entry,
|
||||
)
|
||||
self.api = api
|
||||
self._device_serials = device_serials
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> dict[str, IntelliClimaFilterStatus]:
|
||||
"""Fetch filter status for all devices, isolating per-device failures."""
|
||||
results = await asyncio.gather(
|
||||
*(self.api.get_filter_status(serial) for serial in self._device_serials),
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
statuses: dict[str, IntelliClimaFilterStatus] = {}
|
||||
for serial, result in zip(self._device_serials, results, strict=True):
|
||||
if isinstance(result, IntelliClimaAPIError):
|
||||
LOGGER.warning(
|
||||
"Failed to update filter status for %s: %s", serial, result
|
||||
)
|
||||
continue
|
||||
if isinstance(result, BaseException):
|
||||
raise result
|
||||
statuses[serial] = result
|
||||
|
||||
return statuses
|
||||
|
||||
|
||||
@dataclass
|
||||
class IntelliClimaData:
|
||||
"""Runtime data for the IntelliClima config entry."""
|
||||
|
||||
devices_coordinator: IntelliClimaCoordinator
|
||||
filter_coordinator: IntelliClimaFilterCoordinator
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import override
|
||||
|
||||
from pyintelliclima.intelliclima_types import IntelliClimaC800, IntelliClimaECO
|
||||
|
||||
from homeassistant.const import ATTR_CONNECTIONS, ATTR_MODEL, ATTR_SW_VERSION
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_BLUETOOTH,
|
||||
CONNECTION_NETWORK_MAC,
|
||||
@@ -16,6 +15,22 @@ from .const import DOMAIN
|
||||
from .coordinator import IntelliClimaCoordinator
|
||||
|
||||
|
||||
def eco_device_info(device: IntelliClimaECO) -> DeviceInfo:
|
||||
"""Return the device info shared by all entities of an ECOCOMFORT 2.0."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, device.id)},
|
||||
manufacturer="Fantini Cosmi",
|
||||
name=device.name,
|
||||
serial_number=device.crono_sn,
|
||||
model="ECOCOMFORT 2.0",
|
||||
sw_version=device.fw,
|
||||
connections={
|
||||
(CONNECTION_BLUETOOTH, device.mac),
|
||||
(CONNECTION_NETWORK_MAC, device.macwifi),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class IntelliClimaEntity(CoordinatorEntity[IntelliClimaCoordinator]):
|
||||
"""Define a generic class for IntelliClima entities."""
|
||||
|
||||
@@ -53,12 +68,7 @@ class IntelliClimaECOEntity(IntelliClimaEntity):
|
||||
"""Class initializer."""
|
||||
super().__init__(coordinator, device)
|
||||
|
||||
self._attr_device_info[ATTR_MODEL] = "ECOCOMFORT 2.0"
|
||||
self._attr_device_info[ATTR_SW_VERSION] = device.fw
|
||||
self._attr_device_info[ATTR_CONNECTIONS] = {
|
||||
(CONNECTION_BLUETOOTH, device.mac),
|
||||
(CONNECTION_NETWORK_MAC, device.macwifi),
|
||||
}
|
||||
self._attr_device_info = eco_device_info(device)
|
||||
|
||||
@property
|
||||
def _device_data(self) -> IntelliClimaECO:
|
||||
|
||||
@@ -28,7 +28,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up IntelliClima VMC fans."""
|
||||
coordinator = entry.runtime_data
|
||||
coordinator = entry.runtime_data.devices_coordinator
|
||||
|
||||
entities: list[IntelliClimaVMCFan] = [
|
||||
IntelliClimaVMCFan(
|
||||
|
||||
@@ -31,7 +31,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up IntelliClima VMC fan mode select."""
|
||||
coordinator = entry.runtime_data
|
||||
coordinator = entry.runtime_data.devices_coordinator
|
||||
|
||||
entities: list[IntelliClimaVMCFanModeSelect] = [
|
||||
IntelliClimaVMCFanModeSelect(
|
||||
|
||||
@@ -61,7 +61,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a IntelliClima Sensors."""
|
||||
coordinator = entry.runtime_data
|
||||
coordinator = entry.runtime_data.devices_coordinator
|
||||
|
||||
entities: list[IntelliClimaSensor] = [
|
||||
IntelliClimaSensor(
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"filter_cleaning": {
|
||||
"name": "Filter cleaning required"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"fan_mode": {
|
||||
"name": "Fan direction mode",
|
||||
|
||||
@@ -8,6 +8,8 @@ from pyintelliclima.const import FanMode, FanSpeed
|
||||
from pyintelliclima.intelliclima_types import (
|
||||
IntelliClimaDevices,
|
||||
IntelliClimaECO,
|
||||
IntelliClimaFilterStatsEntry,
|
||||
IntelliClimaFilterStatus,
|
||||
IntelliClimaModelType,
|
||||
)
|
||||
import pytest
|
||||
@@ -39,17 +41,16 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def single_eco_device() -> IntelliClimaDevices:
|
||||
"""Create IntelliClimaDevices with one ECOCOMFORT 2.0 and no C800."""
|
||||
eco = IntelliClimaECO(
|
||||
id="56789",
|
||||
crono_sn="11223344",
|
||||
def create_eco_device(device_id: str, crono_sn: str, name: str) -> IntelliClimaECO:
|
||||
"""Create an ECOCOMFORT 2.0 device."""
|
||||
return IntelliClimaECO(
|
||||
id=device_id,
|
||||
crono_sn=crono_sn,
|
||||
status="OK",
|
||||
online="OK",
|
||||
command="OK",
|
||||
model=IntelliClimaModelType(modello="ECO", tipo="wifi"),
|
||||
name="Test VMC",
|
||||
name=name,
|
||||
houses_id="12345",
|
||||
mode_set=FanMode.inward,
|
||||
mode_state="1",
|
||||
@@ -109,11 +110,30 @@ def single_eco_device() -> IntelliClimaDevices:
|
||||
online_status_debug="mock",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def single_eco_device() -> IntelliClimaDevices:
|
||||
"""Create IntelliClimaDevices with one ECOCOMFORT 2.0 and no C800."""
|
||||
eco = create_eco_device("56789", "11223344", "Test VMC")
|
||||
|
||||
return IntelliClimaDevices(ecocomfort2_devices={eco.id: eco}, c800_devices={})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_cloud_interface(single_eco_device) -> Generator[AsyncMock]:
|
||||
def two_eco_devices() -> IntelliClimaDevices:
|
||||
"""Create IntelliClimaDevices with two ECOCOMFORT 2.0 devices and no C800."""
|
||||
first = create_eco_device("56789", "11223344", "Test VMC")
|
||||
second = create_eco_device("98765", "55667788", "Other VMC")
|
||||
|
||||
return IntelliClimaDevices(
|
||||
ecocomfort2_devices={first.id: first, second.id: second}, c800_devices={}
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_cloud_interface(
|
||||
single_eco_device: IntelliClimaDevices,
|
||||
) -> Generator[AsyncMock]:
|
||||
"""Mock IntelliClimaAPI for tests."""
|
||||
|
||||
with (
|
||||
@@ -132,6 +152,22 @@ def mock_cloud_interface(single_eco_device) -> Generator[AsyncMock]:
|
||||
# Mock other async methods if needed
|
||||
mock_client.authenticate.return_value = True
|
||||
mock_client.get_all_device_status.return_value = single_eco_device
|
||||
mock_client.get_filter_status.return_value = IntelliClimaFilterStatus(
|
||||
serial="11223344",
|
||||
is_active=True,
|
||||
from_date="2025-11-18 10:22:51",
|
||||
stats=[
|
||||
IntelliClimaFilterStatsEntry(
|
||||
night_tot_hour="10",
|
||||
low_tot_hour="20",
|
||||
medium_tot_hour="30",
|
||||
high_tot_hour="5",
|
||||
boost_tot_hour="1",
|
||||
)
|
||||
],
|
||||
totale=66.0,
|
||||
change_filter=True,
|
||||
)
|
||||
|
||||
# Sub-API used by the fan entity
|
||||
mock_client.ecocomfort = SimpleNamespace(
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_binary_sensor_entities.2
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'bluetooth',
|
||||
'00:11:22:33:44:55',
|
||||
),
|
||||
tuple(
|
||||
'mac',
|
||||
'00:11:22:33:44:55',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'intelliclima',
|
||||
'56789',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Fantini Cosmi',
|
||||
'model': 'ECOCOMFORT 2.0',
|
||||
'model_id': None,
|
||||
'name': 'Test VMC',
|
||||
'name_by_user': None,
|
||||
'serial_number': '11223344',
|
||||
'sw_version': '0.6.8',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_binary_sensor_entities[binary_sensor.test_vmc_filter_cleaning_required-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': 'binary_sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'binary_sensor.test_vmc_filter_cleaning_required',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Filter cleaning required',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Filter cleaning required',
|
||||
'platform': 'intelliclima',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'filter_cleaning',
|
||||
'unique_id': '56789_filter_cleaning',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_binary_sensor_entities[binary_sensor.test_vmc_filter_cleaning_required-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test VMC Filter cleaning required',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.test_vmc_filter_cleaning_required',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Test IntelliClima Binary Sensors."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from pyintelliclima.intelliclima_types import IntelliClimaFilterStatus
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_intelliclima_binary_sensor_only(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
) -> AsyncGenerator[None]:
|
||||
"""Set up IntelliClima integration with only the binary sensor platform."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.intelliclima.PLATFORMS", [Platform.BINARY_SENSOR]
|
||||
),
|
||||
):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
yield
|
||||
|
||||
|
||||
async def test_all_binary_sensor_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
binary_sensor_entries = [
|
||||
entry
|
||||
for entry in entity_registry.entities.values()
|
||||
if entry.platform == "intelliclima" and entry.domain == BINARY_SENSOR_DOMAIN
|
||||
]
|
||||
assert len(binary_sensor_entries) == 1
|
||||
|
||||
for entity_entry in binary_sensor_entries:
|
||||
assert entity_entry.device_id
|
||||
assert (device_entry := device_registry.async_get(entity_entry.device_id))
|
||||
assert device_entry == snapshot
|
||||
|
||||
|
||||
async def test_filter_cleaning_unavailable_when_tracking_disabled(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the filter_cleaning sensor is unavailable when the vendor disables filter tracking.
|
||||
|
||||
The vendor API keeps returning `change_filter: false` in this state, which
|
||||
would otherwise misreport a "clean filter" the integration can't actually vouch for.
|
||||
"""
|
||||
mock_cloud_interface.get_filter_status.return_value = IntelliClimaFilterStatus(
|
||||
serial="11223344",
|
||||
is_active=False,
|
||||
from_date="2025-11-18 10:22:51",
|
||||
stats=[],
|
||||
totale=0,
|
||||
change_filter=False,
|
||||
)
|
||||
await hass.config_entries.async_reload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_vmc_filter_cleaning_required")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Test the IntelliClima integration setup."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from pyintelliclima.api import IntelliClimaAPIError
|
||||
from pyintelliclima.intelliclima_types import (
|
||||
IntelliClimaDevices,
|
||||
IntelliClimaFilterStatus,
|
||||
)
|
||||
|
||||
from homeassistant.components.intelliclima.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup_succeeds_when_filter_status_unavailable(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the config entry still loads when the filter-status endpoint fails.
|
||||
|
||||
Filter status only backs a diagnostic binary sensor, so a transient
|
||||
failure of that ancillary endpoint must not block the fan, select, and
|
||||
sensor platforms from being set up.
|
||||
"""
|
||||
mock_cloud_interface.get_filter_status.side_effect = IntelliClimaAPIError(
|
||||
"cannot compute filter status"
|
||||
)
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert hass.states.get("fan.test_vmc") is not None
|
||||
|
||||
state = hass.states.get("binary_sensor.test_vmc_filter_cleaning_required")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_filter_status_failure_isolated_per_device(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
two_eco_devices: IntelliClimaDevices,
|
||||
) -> None:
|
||||
"""Test a filter-status failure for one device does not affect the others."""
|
||||
mock_cloud_interface.get_all_device_status.return_value = two_eco_devices
|
||||
working_device, failing_device = two_eco_devices.ecocomfort2_devices.values()
|
||||
filter_status = mock_cloud_interface.get_filter_status.return_value
|
||||
|
||||
def _get_filter_status(serial: str) -> IntelliClimaFilterStatus:
|
||||
if serial == failing_device.crono_sn:
|
||||
raise IntelliClimaAPIError("cannot compute filter status")
|
||||
return filter_status
|
||||
|
||||
mock_cloud_interface.get_filter_status.side_effect = _get_filter_status
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
working_entity_id = entity_registry.async_get_entity_id(
|
||||
Platform.BINARY_SENSOR, DOMAIN, f"{working_device.id}_filter_cleaning"
|
||||
)
|
||||
assert working_entity_id is not None
|
||||
state = hass.states.get(working_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
failing_entity_id = entity_registry.async_get_entity_id(
|
||||
Platform.BINARY_SENSOR, DOMAIN, f"{failing_device.id}_filter_cleaning"
|
||||
)
|
||||
assert failing_entity_id is not None
|
||||
state = hass.states.get(failing_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
Reference in New Issue
Block a user