feat(device): merge all entities into one shared device with station-type model
All entities (weather, battery, health, native Ecowitt) now report through a
single device under the existing {(DOMAIN,)} identifier, removing the separate
"Ecowitt {model}" device that appeared whenever Ecowitt was enabled. This
collapses the previous two-device layout (and the duplicate/untranslated native
Ecowitt sensors living on the second device) into one.
The device model reflects the running station type via _station_model():
"Ecowitt <model>" when Ecowitt is active (model learned from the payload's
`model` field and stored on runtime_data.ecowitt_model), else "WSLink", else
"PWS". Device identity is centralised in data.build_device_info() and reused by
every platform.
No device-registry migration is required: the identifier is unchanged; only the
model/grouping change. Full device unification under a renamed hub is deferred
to the rename round.
Tests: device-info assertions updated to the shared device across the entity
suites; new _station_model/build_device_info branch coverage in test_data.py;
received_ecowitt now asserts the learned model. 320 passed, 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent
3fdebc6f20
commit
80c944fc5a
|
|
@ -11,11 +11,10 @@ from typing import Any
|
|||
from py_typecheck import checked_or
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorEntityDescription
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .data import build_device_info
|
||||
|
||||
|
||||
class BatteryBinarySensor( # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
|
@ -62,12 +61,5 @@ class BatteryBinarySensor( # pyright: ignore[reportIncompatibleVariableOverride
|
|||
|
||||
@cached_property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Device info."""
|
||||
return DeviceInfo(
|
||||
connections=set(),
|
||||
name="Weather Station SWS 12500",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN,)}, # type: ignore[arg-type]
|
||||
manufacturer="Schizza",
|
||||
model="Weather Station SWS 12500",
|
||||
)
|
||||
"""Device info (single shared device for the whole integration)."""
|
||||
return build_device_info(self.coordinator.config)
|
||||
|
|
|
|||
|
|
@ -198,6 +198,11 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
|||
post_data = await webdata.post()
|
||||
data: dict[str, Any] = dict(post_data)
|
||||
|
||||
# Record the Ecowitt station model (used as the device model) before parsing,
|
||||
# so native entities created during process_payload report it.
|
||||
if model := checked(data.get("model"), str):
|
||||
self.config.runtime_data.ecowitt_model = model
|
||||
|
||||
mapped_data = await self.ecowitt_bridge.process_payload(data)
|
||||
|
||||
if mapped_data:
|
||||
|
|
|
|||
|
|
@ -11,10 +11,16 @@ from dataclasses import dataclass, field
|
|||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from py_typecheck import checked_or
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN, ECOWITT_ENABLED, WSLINK
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntityDescription
|
||||
|
||||
|
|
@ -51,6 +57,38 @@ class SWSRuntimeData:
|
|||
started_at: datetime = field(default_factory=dt_util.utcnow)
|
||||
last_seen: dict[str, datetime] = field(default_factory=dict)
|
||||
|
||||
# Ecowitt station model (e.g. "GW1000"), learned from the first Ecowitt payload.
|
||||
ecowitt_model: str | None = None
|
||||
|
||||
|
||||
# Type alias for typed ConfigEntry
|
||||
type SWSConfigEntry = ConfigEntry[SWSRuntimeData]
|
||||
|
||||
|
||||
def _station_model(entry: SWSConfigEntry) -> str:
|
||||
"""Return the device model label reflecting the running station type.
|
||||
|
||||
Ecowitt (with the learned model when available), else WSLink, else PWS.
|
||||
"""
|
||||
if checked_or(entry.options.get(ECOWITT_ENABLED), bool, False):
|
||||
runtime = getattr(entry, "runtime_data", None)
|
||||
model = getattr(runtime, "ecowitt_model", None) if runtime is not None else None
|
||||
return f"Ecowitt {model}" if model else "Ecowitt"
|
||||
if checked_or(entry.options.get(WSLINK), bool, False):
|
||||
return "WSLink"
|
||||
return "PWS"
|
||||
|
||||
|
||||
def build_device_info(entry: SWSConfigEntry) -> DeviceInfo:
|
||||
"""Single device shared by all entities (SWS, battery, health, native Ecowitt).
|
||||
|
||||
Keeps the existing ``{(DOMAIN,)}`` identifier so no device-registry migration is
|
||||
needed; the model reflects the active station type.
|
||||
"""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN,)}, # type: ignore[arg-type]
|
||||
name="Weather Station SWS 12500",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
manufacturer="Schizza",
|
||||
model=_station_model(entry),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,14 +19,12 @@ from aioecowitt import EcoWittListener, EcoWittSensor, EcoWittSensorTypes
|
|||
from aioecowitt.sensor import SENSOR_MAP
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity, SensorStateClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DOMAIN, REMAP_ECOWITT_COMPAT
|
||||
from .const import REMAP_ECOWITT_COMPAT
|
||||
from .data import SWSConfigEntry, build_device_info
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -216,7 +214,7 @@ class EcowittBridge:
|
|||
and we are just using parsing/discovery logic.
|
||||
"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConfigEntry) -> None:
|
||||
def __init__(self, hass: HomeAssistant, config: SWSConfigEntry) -> None:
|
||||
"""Initialize bridge."""
|
||||
|
||||
self.hass = hass
|
||||
|
|
@ -308,7 +306,7 @@ class EcowittBridge:
|
|||
return
|
||||
|
||||
self._know_native_keys.add(sensor.key)
|
||||
entity = EcoWittNativeSensor(sensor)
|
||||
entity = EcoWittNativeSensor(sensor, self.config)
|
||||
self._add_entities_cb([entity])
|
||||
|
||||
_LOGGER.info("New native Ecowitt sensor %s (type=%s)", sensor.name, sensor.stype.name)
|
||||
|
|
@ -336,7 +334,7 @@ class EcoWittNativeSensor(SensorEntity):
|
|||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, sensor: EcoWittSensor) -> None:
|
||||
def __init__(self, sensor: EcoWittSensor, config: SWSConfigEntry) -> None:
|
||||
"""Initialize native EcoWittSensor."""
|
||||
|
||||
self._ecowitt_sensor = sensor
|
||||
|
|
@ -358,17 +356,9 @@ class EcoWittNativeSensor(SensorEntity):
|
|||
self._attr_native_unit_of_measurement = unit
|
||||
self._attr_state_class = state_class
|
||||
|
||||
# Always attach device info so the entity is grouped under the Ecowitt
|
||||
# station device even when its sensor type has no HA mapping.
|
||||
station = sensor.station
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections=set(),
|
||||
name=f"Ecowitt {station.model}" if station else "Ecowitt station",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, f"ecowitt_{station.key}" if station else "ecowitt")},
|
||||
manufacturer="Ecowitt impl. from Schizza for SWS12500",
|
||||
model=station.model if station else None,
|
||||
)
|
||||
# Share the single integration device (see data.build_device_info); the station
|
||||
# type is reflected in the device model rather than a separate Ecowitt device.
|
||||
self._attr_device_info = build_device_info(config)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime: # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
|
|
|||
|
|
@ -12,14 +12,12 @@ from py_typecheck import checked, checked_or
|
|||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity, SensorEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .data import SWSConfigEntry
|
||||
from .data import SWSConfigEntry, build_device_info
|
||||
from .health_coordinator import HealthCoordinator, public_health_snapshot
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -265,12 +263,5 @@ class HealthDiagnosticSensor( # pyright: ignore[reportIncompatibleVariableOverr
|
|||
|
||||
@cached_property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Device info."""
|
||||
return DeviceInfo(
|
||||
connections=set(),
|
||||
name="Weather Station SWS 12500",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN,)}, # type: ignore[arg-type]
|
||||
manufacturer="Schizza",
|
||||
model="Weather Station SWS 12500",
|
||||
)
|
||||
"""Device info (single shared device for the whole integration)."""
|
||||
return build_device_info(self.coordinator.config)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ from py_typecheck import checked_or
|
|||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo, generate_entity_id
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
|
@ -33,7 +32,6 @@ from . import health_sensor
|
|||
from .const import (
|
||||
CHILL_INDEX,
|
||||
DEV_DBG,
|
||||
DOMAIN,
|
||||
HEAT_INDEX,
|
||||
OUTSIDE_HUMIDITY,
|
||||
OUTSIDE_TEMP,
|
||||
|
|
@ -43,7 +41,7 @@ from .const import (
|
|||
WIND_SPEED,
|
||||
WSLINK,
|
||||
)
|
||||
from .data import SWSConfigEntry
|
||||
from .data import SWSConfigEntry, build_device_info
|
||||
from .sensors_common import WeatherSensorEntityDescription
|
||||
from .sensors_weather import SENSOR_TYPES_WEATHER_API
|
||||
from .sensors_wslink import SENSOR_TYPES_WSLINK
|
||||
|
|
@ -225,12 +223,5 @@ class WeatherSensor( # pyright: ignore[reportIncompatibleVariableOverride]
|
|||
|
||||
@cached_property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Device info."""
|
||||
return DeviceInfo(
|
||||
connections=set(),
|
||||
name="Weather Station SWS 12500",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN,)}, # type: ignore[arg-type]
|
||||
manufacturer="Schizza",
|
||||
model="Weather Station SWS 12500",
|
||||
)
|
||||
"""Device info (single shared device for the whole integration)."""
|
||||
return build_device_info(self.coordinator.config)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ class _CoordinatorStub:
|
|||
|
||||
def __init__(self, data: dict[str, Any] | None = None) -> None:
|
||||
self.data: dict[str, Any] = data if data is not None else {}
|
||||
# device_info reads coordinator.config for the shared device model.
|
||||
self.config = SimpleNamespace(options={})
|
||||
|
||||
|
||||
def _make_entry(
|
||||
|
|
@ -213,7 +215,7 @@ def test_device_info():
|
|||
info = sensor.device_info
|
||||
assert info["name"] == "Weather Station SWS 12500"
|
||||
assert info["manufacturer"] == "Schizza"
|
||||
assert info["model"] == "Weather Station SWS 12500"
|
||||
assert info["model"] == "PWS" # no ecowitt/wslink in stub options -> PWS
|
||||
assert info["identifiers"] == {(DOMAIN,)}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,14 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from custom_components.sws12500.data import SWSRuntimeData
|
||||
from types import SimpleNamespace
|
||||
|
||||
from custom_components.sws12500.const import DOMAIN, ECOWITT_ENABLED, WSLINK
|
||||
from custom_components.sws12500.data import (
|
||||
SWSRuntimeData,
|
||||
_station_model,
|
||||
build_device_info,
|
||||
)
|
||||
|
||||
|
||||
def test_runtime_data_defaults():
|
||||
|
|
@ -44,3 +51,64 @@ def test_runtime_data_collections_are_independent_per_instance():
|
|||
assert b.sensor_descriptions == {}
|
||||
assert b.added_binary_keys == set()
|
||||
assert b.last_seen == {}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# _station_model / build_device_info (single shared device)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_station_model_pws_when_no_flags():
|
||||
"""No ecowitt / wslink flags -> the legacy PWS push station."""
|
||||
entry = SimpleNamespace(options={})
|
||||
assert _station_model(entry) == "PWS" # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_station_model_wslink_when_flag_set():
|
||||
"""The WSLINK flag (without ecowitt) yields the WSLink model."""
|
||||
entry = SimpleNamespace(options={WSLINK: True})
|
||||
assert _station_model(entry) == "WSLink" # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_station_model_ecowitt_without_learned_model():
|
||||
"""Ecowitt enabled but no model learned yet -> bare "Ecowitt".
|
||||
|
||||
Covers both the missing-runtime_data and the model-is-None paths.
|
||||
"""
|
||||
no_runtime = SimpleNamespace(options={ECOWITT_ENABLED: True})
|
||||
assert _station_model(no_runtime) == "Ecowitt" # type: ignore[arg-type]
|
||||
|
||||
runtime_no_model = SimpleNamespace(
|
||||
options={ECOWITT_ENABLED: True},
|
||||
runtime_data=SimpleNamespace(ecowitt_model=None),
|
||||
)
|
||||
assert _station_model(runtime_no_model) == "Ecowitt" # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_station_model_ecowitt_with_learned_model():
|
||||
"""Ecowitt enabled with a learned model -> "Ecowitt <model>"."""
|
||||
entry = SimpleNamespace(
|
||||
options={ECOWITT_ENABLED: True},
|
||||
runtime_data=SimpleNamespace(ecowitt_model="GW1000"),
|
||||
)
|
||||
assert _station_model(entry) == "Ecowitt GW1000" # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_station_model_ecowitt_takes_precedence_over_wslink():
|
||||
"""When both flags are set the ecowitt model wins."""
|
||||
entry = SimpleNamespace(
|
||||
options={ECOWITT_ENABLED: True, WSLINK: True},
|
||||
runtime_data=SimpleNamespace(ecowitt_model="WS3900"),
|
||||
)
|
||||
assert _station_model(entry) == "Ecowitt WS3900" # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_build_device_info_shared_identity():
|
||||
"""All entities share one device under the legacy ``{(DOMAIN,)}`` identifier."""
|
||||
entry = SimpleNamespace(options={})
|
||||
info = build_device_info(entry) # type: ignore[arg-type]
|
||||
|
||||
assert info["identifiers"] == {(DOMAIN,)}
|
||||
assert info["name"] == "Weather Station SWS 12500"
|
||||
assert info["manufacturer"] == "Schizza"
|
||||
assert info["model"] == "PWS"
|
||||
|
|
|
|||
|
|
@ -20,9 +20,24 @@ from aioecowitt import EcoWittSensor, EcoWittSensorTypes
|
|||
from aioecowitt.station import EcoWittStation
|
||||
import pytest
|
||||
|
||||
from custom_components.sws12500.const import DOMAIN, REMAP_ECOWITT_COMPAT
|
||||
from custom_components.sws12500.const import DOMAIN, ECOWITT_ENABLED, REMAP_ECOWITT_COMPAT
|
||||
from custom_components.sws12500.ecowitt import STYPE_TO_HA, EcowittBridge, EcoWittNativeSensor
|
||||
|
||||
# Default config stub for native entities: the integration shares a single device,
|
||||
# so EcoWittNativeSensor only needs the entry to resolve the device model. A bare
|
||||
# PWS config (no ecowitt/wslink flags) is enough for the non-device assertions.
|
||||
_PWS_CONFIG = SimpleNamespace(options={})
|
||||
# An ecowitt config that yields model "Ecowitt GW1000" for device-info assertions.
|
||||
_ECOWITT_CONFIG = SimpleNamespace(
|
||||
options={ECOWITT_ENABLED: True},
|
||||
runtime_data=SimpleNamespace(ecowitt_model="GW1000"),
|
||||
)
|
||||
|
||||
|
||||
def _native(sensor: Any, config: Any = _PWS_CONFIG) -> EcoWittNativeSensor:
|
||||
"""Build a native sensor with a default (PWS) config stub."""
|
||||
return EcoWittNativeSensor(sensor, config)
|
||||
|
||||
# A realistic Ecowitt POST payload. `model` is required by aioecowitt's
|
||||
# station extraction. Contains both internally mapped fields (tempf, humidity,
|
||||
# windspeedmph, baromrelin, dewpointf) and unmapped fields (pm25_ch1, co2).
|
||||
|
|
@ -238,7 +253,7 @@ def test_native_sensor_init_with_mapped_stype() -> None:
|
|||
)
|
||||
sensor = _make_sensor(key="pm25_ch1", stype=EcoWittSensorTypes.PM25, station=station)
|
||||
|
||||
entity = EcoWittNativeSensor(sensor)
|
||||
entity = _native(sensor, _ECOWITT_CONFIG)
|
||||
|
||||
assert entity._attr_unique_id == "ecowitt_pm25_ch1"
|
||||
assert entity._attr_name == "PM2.5 CH1"
|
||||
|
|
@ -249,12 +264,13 @@ def test_native_sensor_init_with_mapped_stype() -> None:
|
|||
assert entity._attr_native_unit_of_measurement == unit
|
||||
assert entity._attr_state_class == state_class
|
||||
|
||||
# Device info groups the entity under the station device.
|
||||
# Native sensors join the single shared integration device; the running station
|
||||
# type is reflected in the model, not in a separate Ecowitt device.
|
||||
info = entity._attr_device_info
|
||||
assert info["name"] == "Ecowitt GW1000"
|
||||
assert info["model"] == "GW1000"
|
||||
assert (DOMAIN, "ecowitt_ABC123") in info["identifiers"]
|
||||
assert info["manufacturer"] == "Ecowitt impl. from Schizza for SWS12500"
|
||||
assert info["name"] == "Weather Station SWS 12500"
|
||||
assert info["model"] == "Ecowitt GW1000"
|
||||
assert info["identifiers"] == {(DOMAIN,)}
|
||||
assert info["manufacturer"] == "Schizza"
|
||||
|
||||
|
||||
def test_native_sensor_init_with_unmapped_stype() -> None:
|
||||
|
|
@ -274,37 +290,40 @@ def test_native_sensor_init_with_unmapped_stype() -> None:
|
|||
value="1000",
|
||||
)
|
||||
|
||||
entity = EcoWittNativeSensor(sensor)
|
||||
entity = _native(sensor, _ECOWITT_CONFIG)
|
||||
|
||||
# No HA metadata set for unknown types.
|
||||
assert getattr(entity, "_attr_device_class", None) is None
|
||||
assert getattr(entity, "_attr_native_unit_of_measurement", None) is None
|
||||
assert getattr(entity, "_attr_state_class", None) is None
|
||||
|
||||
# Device info is still present.
|
||||
# Device info is still present and points at the shared device.
|
||||
info = entity._attr_device_info
|
||||
assert info["name"] == "Ecowitt GW1000"
|
||||
assert (DOMAIN, "ecowitt_ABC123") in info["identifiers"]
|
||||
assert info["name"] == "Weather Station SWS 12500"
|
||||
assert info["identifiers"] == {(DOMAIN,)}
|
||||
|
||||
|
||||
def test_native_sensor_init_without_station() -> None:
|
||||
"""A sensor with no station falls back to generic device info."""
|
||||
def test_native_sensor_device_model_follows_config_not_station() -> None:
|
||||
"""Device model comes from the entry config, independent of the sensor station.
|
||||
|
||||
A native sensor whose ``station`` is missing still lands on the shared device,
|
||||
and the model reflects the configured station type (here a bare PWS config).
|
||||
"""
|
||||
sensor = _make_sensor(key="pm25_ch1", stype=EcoWittSensorTypes.PM25)
|
||||
# Force the no-station branch.
|
||||
sensor.station = None # type: ignore[assignment]
|
||||
|
||||
entity = EcoWittNativeSensor(sensor)
|
||||
entity = _native(sensor) # default PWS config
|
||||
|
||||
info = entity._attr_device_info
|
||||
assert info["name"] == "Ecowitt station"
|
||||
assert info["model"] is None
|
||||
assert (DOMAIN, "ecowitt") in info["identifiers"]
|
||||
assert info["name"] == "Weather Station SWS 12500"
|
||||
assert info["model"] == "PWS"
|
||||
assert info["identifiers"] == {(DOMAIN,)}
|
||||
|
||||
|
||||
def test_native_value_returns_value() -> None:
|
||||
"""native_value returns the underlying sensor value."""
|
||||
sensor = _make_sensor(value=42.0)
|
||||
entity = EcoWittNativeSensor(sensor)
|
||||
entity = _native(sensor)
|
||||
assert entity.native_value == 42.0
|
||||
|
||||
|
||||
|
|
@ -312,7 +331,7 @@ def test_native_value_returns_value() -> None:
|
|||
def test_native_value_none_or_empty(empty: Any) -> None:
|
||||
"""native_value maps None and "" to None."""
|
||||
sensor = _make_sensor(value=empty)
|
||||
entity = EcoWittNativeSensor(sensor)
|
||||
entity = _native(sensor)
|
||||
assert entity.native_value is None
|
||||
|
||||
|
||||
|
|
@ -320,7 +339,7 @@ def test_native_value_none_or_empty(empty: Any) -> None:
|
|||
async def test_added_and_removed_callback_lifecycle() -> None:
|
||||
"""async_added/async_will_remove register and unregister the update cb."""
|
||||
sensor = _make_sensor()
|
||||
entity = EcoWittNativeSensor(sensor)
|
||||
entity = _native(sensor)
|
||||
|
||||
assert entity._handle_update not in sensor.update_cb
|
||||
|
||||
|
|
@ -335,7 +354,7 @@ async def test_added_and_removed_callback_lifecycle() -> None:
|
|||
async def test_will_remove_when_callback_absent() -> None:
|
||||
"""async_will_remove is safe when the callback was never registered."""
|
||||
sensor = _make_sensor()
|
||||
entity = EcoWittNativeSensor(sensor)
|
||||
entity = _native(sensor)
|
||||
|
||||
# Not added; removal must not raise and must not touch the list.
|
||||
assert entity._handle_update not in sensor.update_cb
|
||||
|
|
@ -346,7 +365,7 @@ async def test_will_remove_when_callback_absent() -> None:
|
|||
def test_handle_update_writes_ha_state() -> None:
|
||||
"""_handle_update forwards to async_write_ha_state."""
|
||||
sensor = _make_sensor()
|
||||
entity = EcoWittNativeSensor(sensor)
|
||||
entity = _native(sensor)
|
||||
entity.async_write_ha_state = MagicMock() # type: ignore[method-assign]
|
||||
|
||||
entity._handle_update()
|
||||
|
|
@ -416,14 +435,14 @@ def test_on_new_sensor_skips_already_created_twin() -> None:
|
|||
|
||||
|
||||
def test_native_sensor_translation_key_for_curated() -> None:
|
||||
ent = EcoWittNativeSensor(
|
||||
ent = _native(
|
||||
_make_sensor(key="baromabsin", name="Absolute Pressure", stype=EcoWittSensorTypes.PRESSURE_INHG)
|
||||
)
|
||||
assert ent._attr_translation_key == "ecowitt_absolute_pressure"
|
||||
|
||||
|
||||
def test_native_sensor_name_fallback_for_unknown() -> None:
|
||||
ent = EcoWittNativeSensor(
|
||||
ent = _native(
|
||||
_make_sensor(key="air_ch1", name="Air Gap 1", stype=EcoWittSensorTypes.INTERNAL)
|
||||
)
|
||||
assert ent._attr_name == "Air Gap 1"
|
||||
|
|
|
|||
|
|
@ -675,8 +675,11 @@ def _description(key: str) -> hs.HealthSensorEntityDescription:
|
|||
|
||||
|
||||
def _stub_coordinator(data: dict[str, Any]) -> Any:
|
||||
"""CoordinatorEntity.__init__ only stores the coordinator; a stub suffices."""
|
||||
return SimpleNamespace(data=data)
|
||||
"""CoordinatorEntity.__init__ only stores the coordinator; a stub suffices.
|
||||
|
||||
device_info reads coordinator.config for the shared device model.
|
||||
"""
|
||||
return SimpleNamespace(data=data, config=SimpleNamespace(options={}))
|
||||
|
||||
|
||||
def test_sensor_native_value_without_value_fn() -> None:
|
||||
|
|
@ -745,7 +748,7 @@ def test_sensor_device_info() -> None:
|
|||
info = sensor.device_info
|
||||
assert info["name"] == "Weather Station SWS 12500"
|
||||
assert info["manufacturer"] == "Schizza"
|
||||
assert info["model"] == "Weather Station SWS 12500"
|
||||
assert info["model"] == "PWS" # no ecowitt/wslink in stub options -> PWS
|
||||
|
||||
|
||||
def test_sensor_unique_id_and_category() -> None:
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ async def test_received_ecowitt_success_full_pipeline_with_health_autodiscovery_
|
|||
coordinator.async_set_updated_data = MagicMock()
|
||||
|
||||
request = _EcowittRequestStub(
|
||||
match_info={"webhook_id": "hook"}, post_data={"tempf": "68"}
|
||||
match_info={"webhook_id": "hook"}, post_data={"tempf": "68", "model": "GW2000A"}
|
||||
)
|
||||
resp = await coordinator.received_ecowitt_data(request) # type: ignore[arg-type]
|
||||
|
||||
|
|
@ -248,6 +248,9 @@ async def test_received_ecowitt_success_full_pipeline_with_health_autodiscovery_
|
|||
|
||||
coordinator.ecowitt_bridge.process_payload.assert_awaited_once()
|
||||
|
||||
# Station model is learned from the payload and stored for the shared device model.
|
||||
assert entry.runtime_data.ecowitt_model == "GW2000A"
|
||||
|
||||
# Autodiscovery side-effects.
|
||||
update_options.assert_awaited_once()
|
||||
args, _kwargs = update_options.await_args
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ def test_device_info_contains_expected_identifiers_and_domain():
|
|||
# DeviceInfo is mapping-like; access defensively.
|
||||
assert info.get("name") == "Weather Station SWS 12500"
|
||||
assert info.get("manufacturer") == "Schizza"
|
||||
assert info.get("model") == "Weather Station SWS 12500"
|
||||
assert info.get("model") == "PWS" # no ecowitt/wslink in stub options -> PWS
|
||||
|
||||
identifiers = info.get("identifiers")
|
||||
assert isinstance(identifiers, set)
|
||||
|
|
|
|||
Loading…
Reference in New Issue