Add comprehensive tests (tests/test_conflicts.py) that verify
effective_protocols, protocols_conflict, issue registry updates,
options flow guards, and route wiring to prevent running legacy and
Ecowitt concurrently. Update existing tests to account for the
LEGACY_ENABLED flag in Ecowitt-only setups.
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>
Two long-standing rough edges with Ecowitt running alongside the SWS device:
- Dedup (#1): aioecowitt emits both metric and imperial sensors per quantity
(tempc/tempf, rainratemm/rainratein, ...). Only the imperial twin is mapped to the
SWS sensors, so the metric twin (and other unmapped extras) showed up as duplicate
native entities. _on_new_sensor now skips a key whose unit-variant twin is already
mapped or already created (twin groups derived from aioecowitt's SENSOR_MAP). HA
converts units via device_class, so a single variant suffices.
- Translation (#2): native sensors used the raw English aioecowitt name. Common
single-instance families (rain rate / hourly / event / 24h / weekly / monthly /
yearly / total rain, absolute pressure, feels-like, indoor dew point, CO2) now use
curated translation_keys (added to strings/en/cs); long-tail / multi-channel
sensors keep the English name fallback.
- Also add the imperial RAIN_RATE_INCHES / RAIN_COUNT_INCHES to STYPE_TO_HA so those
native rain sensors get a device class and unit.
Device unification (one "WeatherHub" device + station-type sensor) stays for the
rename round, as agreed.
314 passing, 100% coverage; ruff + basedpyright clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- S8: received_data now rejects empty configured credentials (IncorrectDataError)
and present-but-empty incoming credentials (HTTPUnauthorized) instead of relying
solely on the config flow. Credential validation extracted into
_validate_credentials() (also resolves the C901 complexity warning and unifies
the WU/WSLink branches).
- S6: cap auto-created native Ecowitt entities (MAX_NATIVE_ECOWITT_SENSORS) to bound
entity-registry growth from a fabricated multi-key payload.
- S5: store only the exception class name in Windy/Pocasi last_error (surfaced via
entity attributes; str(ex) could embed the request URL).
- S7: verified safe - the native-sensor INFO log is parametrized (%s), so no
format-string injection; left as-is.
308 passing, 100% coverage; ruff + basedpyright clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- C1: received_data / received_ecowitt_data return 503 (not 500) when the entry is
mid-reload (runtime_data gone); add_new_sensors / add_new_binary_sensors are
defensive no-ops in that window too.
- C2/C3: Windy and Pocasi reserve their next send window *before* the await, so
concurrent webhooks can no longer both pass the rate-limit check and double-send
(which could falsely trip the auto-disable threshold).
- C5: EcowittBridge.set_add_entities flushes unmapped sensors parsed before the
platform was ready (aioecowitt fires new_sensor_cb only once per key).
- C6: a forwarder auto-disabling itself from the hot path no longer forces a full
config-entry reload (update_listener now skips reload for live-read flags
SENSORS_TO_LOAD / WINDY_ENABLED / POCASI_CZ_ENABLED).
- C7: to_int accepts decimal-formatted integers ("180.0").
- C8: forwarders use dt_util.utcnow() (UTC-aware) instead of naive datetime.now().
Tests updated; 305 passing, 100% coverage; ruff + basedpyright clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>