SWS-12500-custom-component/tests/test_sensor_platform.py

283 lines
8.1 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from unittest.mock import MagicMock
import pytest
from custom_components.sws12500.const import (
CHILL_INDEX,
HEAT_INDEX,
OUTSIDE_HUMIDITY,
OUTSIDE_TEMP,
SENSORS_TO_LOAD,
WIND_AZIMUT,
WIND_DIR,
WIND_SPEED,
WSLINK,
)
from custom_components.sws12500.data import (
ENTRY_ADD_ENTITIES,
ENTRY_COORDINATOR,
ENTRY_DESCRIPTIONS,
)
from custom_components.sws12500.sensor import (
WeatherSensor,
_auto_enable_derived_sensors,
add_new_sensors,
async_setup_entry,
)
from custom_components.sws12500.sensors_weather import SENSOR_TYPES_WEATHER_API
from custom_components.sws12500.sensors_wslink import SENSOR_TYPES_WSLINK
@dataclass(slots=True)
class _ConfigEntryStub:
entry_id: str
options: dict[str, Any]
class _CoordinatorStub:
"""Minimal coordinator stub for WeatherSensor and platform setup."""
def __init__(
self, data: dict[str, Any] | None = None, *, config: Any | None = None
) -> None:
self.data = data if data is not None else {}
self.config = config
@pytest.fixture
def hass():
# Use a very small hass-like object; sensor platform uses only `hass.data`.
class _Hass:
def __init__(self) -> None:
self.data: dict[str, Any] = {}
return _Hass()
@pytest.fixture
def config_entry() -> _ConfigEntryStub:
return _ConfigEntryStub(entry_id="test_entry_id", options={})
def _capture_add_entities():
captured: list[Any] = []
def _add_entities(entities: list[Any]) -> None:
captured.extend(entities)
return captured, _add_entities
def test_auto_enable_derived_sensors_wind_azimut():
requested = {WIND_DIR}
expanded = _auto_enable_derived_sensors(requested)
assert WIND_DIR in expanded
assert WIND_AZIMUT in expanded
def test_auto_enable_derived_sensors_heat_index():
requested = {OUTSIDE_TEMP, OUTSIDE_HUMIDITY}
expanded = _auto_enable_derived_sensors(requested)
assert HEAT_INDEX in expanded
def test_auto_enable_derived_sensors_chill_index():
requested = {OUTSIDE_TEMP, WIND_SPEED}
expanded = _auto_enable_derived_sensors(requested)
assert CHILL_INDEX in expanded
@pytest.mark.asyncio
async def test_sensor_async_setup_entry_no_coordinator_is_noop(hass, config_entry):
# No entry dict created by integration yet; async_setup_entry should be defensive and no-op.
captured, add_entities = _capture_add_entities()
await async_setup_entry(hass, config_entry, add_entities)
assert captured == []
@pytest.mark.asyncio
async def test_sensor_async_setup_entry_stores_callback_and_descriptions_even_if_no_sensors_to_load(
hass, config_entry
):
# Prepare runtime entry data and coordinator like integration does.
hass.data.setdefault("sws12500", {})
hass.data["sws12500"][config_entry.entry_id] = {
ENTRY_COORDINATOR: _CoordinatorStub()
}
captured, add_entities = _capture_add_entities()
# No SENSORS_TO_LOAD set -> early return, but it should still store callback + descriptions.
await async_setup_entry(hass, config_entry, add_entities)
entry_data = hass.data["sws12500"][config_entry.entry_id]
assert entry_data[ENTRY_ADD_ENTITIES] is add_entities
assert isinstance(entry_data[ENTRY_DESCRIPTIONS], dict)
assert captured == []
@pytest.mark.asyncio
async def test_sensor_async_setup_entry_selects_weather_api_descriptions_when_wslink_disabled(
hass, config_entry
):
hass.data.setdefault("sws12500", {})
hass.data["sws12500"][config_entry.entry_id] = {
ENTRY_COORDINATOR: _CoordinatorStub()
}
captured, add_entities = _capture_add_entities()
# Explicitly disabled WSLINK
config_entry.options[WSLINK] = False
await async_setup_entry(hass, config_entry, add_entities)
descriptions = hass.data["sws12500"][config_entry.entry_id][ENTRY_DESCRIPTIONS]
assert set(descriptions.keys()) == {d.key for d in SENSOR_TYPES_WEATHER_API}
assert captured == []
@pytest.mark.asyncio
async def test_sensor_async_setup_entry_selects_wslink_descriptions_when_wslink_enabled(
hass, config_entry
):
hass.data.setdefault("sws12500", {})
hass.data["sws12500"][config_entry.entry_id] = {
ENTRY_COORDINATOR: _CoordinatorStub()
}
captured, add_entities = _capture_add_entities()
config_entry.options[WSLINK] = True
await async_setup_entry(hass, config_entry, add_entities)
descriptions = hass.data["sws12500"][config_entry.entry_id][ENTRY_DESCRIPTIONS]
assert set(descriptions.keys()) == {d.key for d in SENSOR_TYPES_WSLINK}
assert captured == []
@pytest.mark.asyncio
async def test_sensor_async_setup_entry_adds_requested_entities_and_auto_enables_derived(
hass, config_entry
):
hass.data.setdefault("sws12500", {})
coordinator = _CoordinatorStub()
hass.data["sws12500"][config_entry.entry_id] = {ENTRY_COORDINATOR: coordinator}
captured, add_entities = _capture_add_entities()
# Request WIND_DIR, OUTSIDE_TEMP, OUTSIDE_HUMIDITY, WIND_SPEED -> should auto-add derived keys too.
config_entry.options[WSLINK] = False
config_entry.options[SENSORS_TO_LOAD] = [
WIND_DIR,
OUTSIDE_TEMP,
OUTSIDE_HUMIDITY,
WIND_SPEED,
]
await async_setup_entry(hass, config_entry, add_entities)
# We should have at least those requested + derived in the added entities.
keys_added = {
e.entity_description.key for e in captured if isinstance(e, WeatherSensor)
}
assert WIND_DIR in keys_added
assert OUTSIDE_TEMP in keys_added
assert OUTSIDE_HUMIDITY in keys_added
assert WIND_SPEED in keys_added
# Derived:
assert WIND_AZIMUT in keys_added
assert HEAT_INDEX in keys_added
assert CHILL_INDEX in keys_added
def test_add_new_sensors_is_noop_when_domain_missing(hass, config_entry):
called = False
def add_entities(_entities: list[Any]) -> None:
nonlocal called
called = True
# No hass.data["sws12500"] at all.
add_new_sensors(hass, config_entry, keys=["anything"])
assert called is False
def test_add_new_sensors_is_noop_when_entry_missing(hass, config_entry):
hass.data["sws12500"] = {}
called = False
def add_entities(_entities: list[Any]) -> None:
nonlocal called
called = True
add_new_sensors(hass, config_entry, keys=["anything"])
assert called is False
def test_add_new_sensors_is_noop_when_callback_or_descriptions_missing(
hass, config_entry
):
hass.data["sws12500"] = {
config_entry.entry_id: {ENTRY_COORDINATOR: _CoordinatorStub()}
}
called = False
def add_entities(_entities: list[Any]) -> None:
nonlocal called
called = True
# Missing ENTRY_ADD_ENTITIES + ENTRY_DESCRIPTIONS -> no-op.
add_new_sensors(hass, config_entry, keys=["anything"])
assert called is False
def test_add_new_sensors_ignores_unknown_keys(hass, config_entry):
hass.data["sws12500"] = {
config_entry.entry_id: {
ENTRY_COORDINATOR: _CoordinatorStub(),
ENTRY_ADD_ENTITIES: MagicMock(),
ENTRY_DESCRIPTIONS: {}, # nothing known
}
}
add_new_sensors(hass, config_entry, keys=["unknown_key"])
hass.data["sws12500"][config_entry.entry_id][ENTRY_ADD_ENTITIES].assert_not_called()
def test_add_new_sensors_adds_known_keys(hass, config_entry):
coordinator = _CoordinatorStub()
add_entities = MagicMock()
# Use one known description from the weather API list.
known_desc = SENSOR_TYPES_WEATHER_API[0]
hass.data["sws12500"] = {
config_entry.entry_id: {
ENTRY_COORDINATOR: coordinator,
ENTRY_ADD_ENTITIES: add_entities,
ENTRY_DESCRIPTIONS: {known_desc.key: known_desc},
}
}
add_new_sensors(hass, config_entry, keys=[known_desc.key])
add_entities.assert_called_once()
(entities_arg,) = add_entities.call_args.args
assert isinstance(entities_arg, list)
assert len(entities_arg) == 1
assert isinstance(entities_arg[0], WeatherSensor)
assert entities_arg[0].entity_description.key == known_desc.key