Commit Graph

14 Commits (a09e2aec9f32124272621810399dfafc6d13c0f4)

Author SHA1 Message Date
SchiZzA a09e2aec9f
fix: Prevent addon probe from failing config entry
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.
2026-07-25 21:47:53 +02:00
SchiZzA 793ea1991a
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>
2026-07-25 21:47:53 +02:00
SchiZzA 0c9f7fbc68
feat(ecowitt): dedupe unit-variant sensors and translate common native ones
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>
2026-07-25 21:47:53 +02:00
SchiZzA 2e3c2e0ba8
fix(health): reflect Ecowitt as a protocol in the diagnostics summary
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>
2026-07-25 21:47:53 +02:00
SchiZzA f6088e86ab
chore(quality): manifest keys, ConfigEntryNotReady, remove dead sqlite code
- 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>
2026-07-25 21:47:52 +02:00
SchiZzA f4da5fdb27
fix(security): harden forwarders, cap native ecowitt entities, reject empty creds
- 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>
2026-07-25 21:47:52 +02:00
SchiZzA 14466cbe60
fix(correctness): reload-window guards, rate-limit races, ecowitt flush, dt_util
- 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>
2026-07-25 21:47:52 +02:00
SchiZzA 4fa86a5bb0
fix(security)+i18n: stop health-attribute leak, mask secrets in logs, sync translations
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>
2026-07-25 21:47:52 +02:00
SchiZzA a467c4b6e5
fix(security): authenticate health endpoint and stop webhook-id leak
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>
2026-07-25 21:47:52 +02:00
SchiZzA 526d5e4f6e
test: reach 100% coverage of custom_components/sws12500
Add focused test modules and extend existing ones to cover every line of the
integration (1588 stmts, 0 missing; 295 tests):

- test_diagnostics.py: redaction + health_data fallback.
- test_binary_battery.py: binary_sensor platform setup / add_new + BatteryBinarySensor.
- test_staleness_legacy.py: warmup/threshold stale detection + legacy orphan issue.
- test_ecowitt_bridge.py: EcowittBridge parsing/discovery + EcoWittNativeSensor.
- test_health.py: HealthCoordinator (online/offline refresh, summary, ingress,
  forwarding, HTTP endpoint) + HealthDiagnosticSensor.
- test_received_ecowitt.py: received_ecowitt_data paths + received_data health branches.
- test_windy_more.py: duplicate (409) / rate-limit (429) / 3-strike disable.
- test_routes_more.py: RouteInfo.__str__, ingress observer, canonical resolve.
- test_utils_conv.py: to_int/to_float edge cases.
- config_flow: wslink_port_setup step + initial ecowitt flow.
- init: _check_stale time-interval callback.
- lifecycle: register_path idempotency (existing-dispatcher branch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 21:47:51 +02:00
SchiZzA 4dcd76da62
test: align suite with v2.0 runtime_data architecture
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>
2026-07-25 21:47:51 +02:00
SchiZzA f0d24ebde3
Make routes method-aware and update related tests
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.
2026-07-25 21:47:47 +02:00
SchiZzA 0060369d8a
Add health diagnostic sensor and extensive tests for sws12500
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
2026-07-25 21:45:08 +02:00
SchiZzA 93fd85a487
Add HCHO / VOC air-quality sensors (T9 module)
Support the WSLink t9 air-quality module:
  - t9hcho (formaldehyde, ppb),
  - t9voclv (VOC level 1-5 -> ENUM state)
  - t9bat (0-5 battery -> percentage).

  The t9hcho/t9voclv/t9bat values are dropped when
  t9cn reports the module as disconnected,
  so no empty entities are created.

  - const: HCHO/VOC/T9_BATTERY keys, VOCLevel enum +
  VOC_LEVEL_MAP, BATTERY_NON_BINARY, CONNECTION_GATED_SENSORS,
  REMAP_WSLINK_ITEMS entries
  - utils: voc_level_to_text(),
  battery_5step_to_pct(), connection gating
  - sensors_wslink: HCHO / VOC / T9_BATTERY entity
  descriptions
  - translations (en, cs): hcho, voc (+ states),
  t9_battery
  - tests: tests/conftest.py +
  tests/test_t9_air_quality.py
2026-05-11 23:55:36 +02:00