- Reject blank or whitespace-only PWS credentials in both the config flow and
the options flow. INVALID_CREDENTIALS only held placeholder strings, so an
empty API ID or key passed validation, the entry was created, and
_validate_credentials then rejected every incoming packet.
- Fall back to "UNKNOWN" host/port when get_url raises NoURLAvailableError.
The values are shown as Ecowitt setup instructions only, so an unresolvable
Home Assistant URL must not make the step unreachable.
- Catch TimeoutError alongside ClientError in the Windy and Pocasi push paths.
A timeout is not a ClientError and used to escape into the webhook handler,
answering the station with HTTP 500 even though the measured data was
already stored.
- Add regression tests for all three and pin that 0-5 battery descriptions
really reach the WSLink sensor platform.
Forwarders (Windy / Pocasi):
- `enabled` becomes a property reading the option live. Toggling it does not
reload the entry, so the cached copy left the diagnostics sensor reporting a
stale value indefinitely for a disabled forwarder.
- Reject empty credentials explicitly: an empty string is still a `str`, so
`checked` alone let an unconfigured forwarder send a request that could only
ever be refused.
- Use `persistent_notification.async_create` instead of the sync variant.
- Use WINDY_MAX_RETRIES consistently; add POCASI_CZ_MAX_RETRIES.
Utils:
- `voc_level_to_text` / `battery_5step_to_pct` go through `to_int` like every
other value_fn. A bare `int()` raised on a garbage payload value, which
`WeatherSensor.native_value` then logged with a full traceback on every push.
Out-of-range battery steps are clamped to a valid percentage.
- Simplify `check_disabled` (drop the redundant camelCase flag).
Config flow:
- `retain_data` re-reads SENSORS_TO_LOAD at submit time. Auto-discovery appends
to it from the webhook handler, so writing back the snapshot taken when the
step opened silently rolled back any sensor discovered meanwhile.
Coordinator:
- Drop the per-push staleness recalculation; the hourly timer already covers it.
Ecowitt:
- Tolerate aioecowitt moving/renaming the internal SENSOR_MAP rather than
failing the whole module import; unit-variant dedup degrades instead.
Translations:
- Battery state key `unknown` -> `drained`: the entity emits UnitOfBat.UNKNOWN,
whose value is "drained", so the old key never matched and the raw state
leaked into the UI. cs.json was already correct.
- Remove the dead `migration` step; `async_step_migration` does not exist.
Also: remove verified-dead constants/helpers and fix ~30 docstring typos.
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.
Add POCASI_CZ_MAX_RETRIES and map HTTP status codes to explicit send
outcomes. Use the status (not the empty body) to classify sends as
"ok", "auth_error", or "unexpected_response". Introduce _disable_pocasi
to persistently turn off resending and record errors. Count unexpected
responses and disable resends after the retry limit; adjust client-error
handling to reuse the same disable logic. Update tests and config flow
expectations to match the new behavior.
Extract WSLink add-on probe and treat expected network errors and
NoURLAvailableError as "offline" instead of raising. Log unexpected
errors and avoid raising ConfigEntryNotReady from diagnostics probes.
Add tests for probe robustness, add additional sensitive purge keys,
and fix config_flow host fallback handling.
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>
The health model only knew "wu"/"wslink", so with an Ecowitt setup the
active_protocol / integration_status fell back to "PWS/WU" even while Ecowitt data
was flowing.
- Replace _protocol_name() with _configured_protocol() which returns
wu / wslink / ecowitt (legacy endpoint takes precedence; ecowitt-only -> ecowitt).
- _refresh_summary: treat "ecowitt" as a real accepted protocol (active_protocol and
online_<proto> now track it); the WU-vs-WSLink mismatch -> degraded check is scoped
to the legacy pair so coexisting Ecowitt is never falsely degraded.
- Add the "ecowitt" / "online_ecowitt" entity states to strings/en/cs.
310 passing, 100% coverage; ruff + basedpyright clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- manifest: add single_config_entry, integration_type "device", loggers
["aioecowitt"]; drop empty homekit/ssdp/zeroconf placeholders.
- Q10: raise ConfigEntryNotReady (not PlatformNotReady) when route registration
fails in async_setup_entry; drop the now-unused import.
- Q11: delete the ~105-line commented-out sqlite3 statistics-migration block in
utils.py (would have been blocking I/O in the event loop) and its dead import.
Deferred (with reason): quality_scale (needs a rule audit + quality_scale.yaml),
device-identifier 2-tuple and suggested_entity_id (entity-naming/identity changes,
to land with the planned integration rename), CoordinatorEntity generics in
sensor.py (would introduce a circular import - intentionally untyped).
Note: the health-probe protocol gate was reverted - the proxy add-on can front
WU/WSLink/Ecowitt, so polling must stay unconditional.
308 passing, 100% coverage; ruff 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>
Security (from the deep audit):
- S1: health "integration_health" sensor no longer dumps the full snapshot in
extra_state_attributes (readable by any HA user). public_health_snapshot()
strips internal network details (source IP, internal URLs, raw add-on status);
full data stays on the authenticated endpoint and admin-only diagnostics.
- S2: anonymize() now masks the Ecowitt passkey/PASSKEY.
- S3: Windy station id masked before logging the dataset.
- S4: diagnostics redacts internal network fields (source IP, URLs, raw_status).
i18n (Q1/Q2):
- Sync strings.json + en.json with the actual flow: add wslink_port_setup step,
the ecowitt/pocasi/wslink_port_setup menu entries, the pocasi_* and
windy_key_required error keys, the stale_sensors_detected issue; fix wrong data
keys (WSLINK->wslink + legacy_enabled, POCASI_CZ_SEND_INTERVAL->POCASI_SEND_INTERVAL,
pocasi_enabled_checkbox->pocasi_enabled_chcekbox); translate the English ecowitt
options step (was Czech); de-duplicate the legacy-battery issue description.
- Q2: rename wbgt_index -> wbgt_temp to match the entity translation_key.
- cs.json: add windy_key_required and fix the pocasi data/data_description keys.
Tests: 300 passing, 100% coverage maintained; basedpyright clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The /station/health route is registered directly on the aiohttp router, so HA's
auth middleware only flags requests without blocking them - the endpoint was
reachable unauthenticated and returned the full health snapshot.
- health_status now requires HA authentication (bearer token or signed request)
via KEY_AUTHENTICATED and returns 401 otherwise.
- Mask the Ecowitt webhook id (the endpoint's only credential) in last_ingress
paths via _sanitize_path, so it never enters the snapshot exposed by the health
endpoint or the diagnostics download.
- Redact ECOWITT_WEBHOOK_ID in diagnostics.
- Compare the Ecowitt webhook id in constant time (hmac.compare_digest), matching
the WU/WSLink credential checks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The suite predated the coordinator extraction and the typed runtime_data
migration: 5 modules failed to import and 18 tests failed.
- Import IncorrectDataError/WeatherDataUpdateCoordinator from .coordinator.
- Point received_data monkeypatch targets at custom_components.sws12500.coordinator.*
- Stub config entries with async_on_unload + runtime_data (SWSRuntimeData).
- Rewrite test_data, test_sensor_platform and test_integration_lifecycle off the
removed ENTRY_* hass.data keys onto runtime_data (route dispatcher reuse, ecowitt
POST route, no hass.data pop on unload, health first_refresh mocked).
- Update config_flow tests for the user menu (pws step) and the options menu
gaining wslink_port_setup.
- Fix VOCLevel.EXCELENT typo and stale t9 expectations (tuple battery list,
HCHO int coercion, connection-gated subset).
Result: 168 passed, 0 collection errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Include HTTP method in route keys and dispatch, and fix
Routes.show_enabled.
Update register_path to accept a HealthCoordinator and adjust router
stubs in tests. Update WindyPush tests to use response objects
(status/text)
and adapt related exception/notification expectations.
integration
- Introduce HealthDiagnosticSensor for device health status reporting
- Add new constants and data keys for health sensor integration
- Wire health_sensor module into sensor platform setup
- Refactor sensor descriptions to improve derived sensor handling
- Implement pytest fixtures and comprehensive tests covering:
- Config flows and options validation
- Data reception and authentication
- Sensor platform setup and dynamic sensor addition
- Push integration with Pocasi.cz and Windy API
- Route dispatching and error handling
- Utilities, conversions, and translation functions