Commit Graph

257 Commits (72c383ffbab352e845da165777646ce08ec91353)

Author SHA1 Message Date
SchiZzA 72c383ffba
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-06-21 21:47:05 +02:00
SchiZzA 514ab823d6
feat(config-flow): mask secret fields with password selectors (Q7)
API_KEY, WINDY_STATION_PWD and POCASI_CZ_API_KEY now use a password TextSelector so
the values are masked in the UI. Field keys are unchanged, so stored options and the
flow contract are unaffected.

308 passing, 100% coverage; ruff + basedpyright clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:10:31 +02:00
SchiZzA 6a72ce0f51
i18n(cs): add missing CH5-CH8 temp/humidity and CH3-CH8 battery translations
Every entity translation_key used in code now resolves in cs.json (Czech users with
channels 5-8 or the deprecated enum battery sensors no longer see raw keys).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:04:17 +02:00
SchiZzA df8e503d2c
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-06-21 19:01:32 +02:00
SchiZzA 418d0c7546
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-06-21 18:48:50 +02:00
SchiZzA 038d2459b9
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-06-21 18:32:00 +02:00
SchiZzA 1fe4d6da45
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-06-21 18:08:34 +02:00
SchiZzA bf6a02c5b3
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-06-21 13:42:56 +02:00
SchiZzA b3aec61fc2
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-06-21 13:23:34 +02:00
SchiZzA acda94bb03
fix(utils): widen wind_dir_to_text param to float | str | None
The azimut value_from_data_fn now passes dir.get(WIND_DIR) (typed Any | None),
which tripped basedpyright reportArgumentType against the float-only signature.
The body already coerces via to_float() and handles None, so widen the
annotation to match the real inputs (raw str payload, float, or None).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:53:46 +02:00
SchiZzA 50d0e9fae7
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-06-21 12:51:52 +02:00
SchiZzA 25341f79b3
fix: address review findings across forwarders and helpers
- wind_dir_to_text: treat 0/missing direction as None (calm) so a missing
  wind direction no longer renders as North; azimut lambdas pass None too.
- windy: log response status (was logging an un-awaited coroutine method);
  count "unexpected response" toward the disable threshold even when logging
  is off.
- pocasi: disable threshold >3 -> >=3 to match Windy's 3-strike behavior.
- ecowitt: always attach device_info to native sensors, including unknown
  sensor types (previously left orphaned without a device).
- const: stop remapping WSLink connection flags (t1cn/t234cXcn) into the
  payload; they had no entity and leaked ghost *_connection keys into data
  and persisted SENSORS_TO_LOAD. Gating uses the raw keys and is unaffected.
- config_flow: fix WSLink port fallback typo 433 -> 443; drop mutable default
  argument on async_step_init.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:51:35 +02:00
SchiZzA b72efa5985
fix(coordinator): pass config_entry to DataUpdateCoordinator
Pass config_entry explicitly to super().__init__() in both
WeatherDataUpdateCoordinator and HealthCoordinator instead of relying on the
implicit ContextVar (deprecated, breaks in HA 2026.8 for core and was breaking
async_config_entry_first_refresh outside the setup context).

Also resolve each discovered sensor's display name once in received_data
(the previous comprehension awaited translations() twice per key).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:51:25 +02:00
SchiZzA e20c23a3af
fix(routes): rebind sticky health route on reload; defensive resolve
After a config reload a new HealthCoordinator is created, but the sticky
/station/health route kept calling the previous (stale) instance. Add
Routes.rebind_handler() and call it from async_setup_entry's reload branch
so the endpoint always serves the current coordinator.

Also make _resolve_route() tolerant of requests without match_info/route/
resource instead of raising AttributeError.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:51:14 +02:00
SchiZzA b777296b52
fix(ecowitt): Connect Ecowitt bridge to sensor platform earlier 2026-06-21 11:28:24 +02:00
SchiZzA e3d712bd54
fix(wslink): Definiton of HCHO, VOC, T9 battery diplicates. 2026-06-21 11:17:38 +02:00
SchiZzA 2f7d96f8d3
Add stall sensor detection. 2026-06-01 17:47:30 +02:00
SchiZzA 0142c5412b
fix: coordinator reuse, route dispatcher, binary sensor translations & test suite alignment
- Reuse existing `WeatherDataUpdateCoordinator` instance across reloads instead of
  creating a new one; routes keep pointing to the same coordinator, so entities
  stay subscribed and updates never silently drop
- `update_listener` now skips full reload when only `SENSORS_TO_LOAD` changes
  (auto-discovery); avoids the "frozen UI" window caused by temporary platform unload
- Replaced direct `route._handler` mutation with a proper `Routes` dispatcher;
  all aiohttp routes are registered once and an internal dispatcher decides which
  handler is active — safe to switch at runtime without touching the router
- Fixed `self.config_entry` / `self.config` inconsistency in coordinator

- `BatteryBinarySensor` now carries correct `device_info` (same identifiers as
  `WeatherSensor`) → single device card in UI instead of two
- Translations for binary sensors must live under `entity.binary_sensor.*`,
  not `entity.sensor.*`

- Fixed all tests broken by new `register_path(hass, coordinator, coordinator_h, entry)` signature
- Updated `_RouterStub` to accept `name=` kwargs and return objects with `.method`
- Added `async post()` to `_RequestStub` stubs
- Aligned `Routes` tests with new `method:path` dispatch key format and `show_enabled()` output
- Replaced `WindyApiKeyError` with `WindyPasswordMissing` to match actual exceptions
- Updated `_FakeResponse` to use HTTP status codes instead of response text strings
- Patched `persistent_notification.create` in Windy push tests to avoid `SimpleNamespace` errors
2026-06-01 17:01:40 +02:00
SchiZzA 4001ff0ba4
fix: Fixed type errors 2026-05-28 11:30:05 +02:00
SchiZzA 7ea47e8591
fix DEV_DBG as constant not string literal 2026-05-27 21:50:14 +02:00
SchiZzA 463b922e83
fix bool truthy 2026-05-27 21:49:39 +02:00
SchiZzA e570ff0014
hmac compare ID / KEY 2026-05-27 21:41:40 +02:00
SchiZzA 1ad10b4b1e
fixed: Menu options 2026-05-27 21:34:32 +02:00
SchiZzA b9815713a0
fix value in wslink sensors
Fix VOCLevel value
2026-05-27 21:30:11 +02:00
SchiZzA 211965dd52
Ecowitt support
Update setiing (config_flow and translations) for Ecowitt settings.
Also updated utils.py to accept None for value.
2026-05-27 21:27:13 +02:00
SchiZzA 9288ae4a64
Updated config flow
Updated config flow's initial setup.
Ecowitt stations does not require `API_ID` and `API_KEY`, so we need to
separate Ecowitt setup from PWS/WSLink setup.
2026-05-27 17:27:10 +02:00
schizza 7abfedc1ca
```
docs: Update README for Ecowitt pre-release, expanded sensor support, and guidance

Update the README to reflect current development and provide clearer user guidance:
- Announce active Ecowitt support in the v2.0.0pre1 pre-release version.
- Expand the "Supported sensors" section with details on WSLink protocol specific sensors,
  including additional channels (CH2/CH3), WBGT, heat index, wind chill,
  sensor battery levels, and HCHO/VOC air-quality sensors (WH46).
- Clarify WSLink configuration, warnings, and quick recommendations.
- Correct the manual installation path for custom components and improve general readability.
```
2026-05-25 22:24:42 +02:00
SchiZzA cc50f9babb
fix: Corrected RAIN and DAILYRAIN for PWS protocol.
The `RAININ` suppose to be accumulated rainfall in 60 minutes. So the
unit must be in `mm/h` or `in/h`.
In the opposite `DAILYRAIN` is rain in the day do far, represented in
accumulated `mm` or `in`.
2026-05-24 22:19:53 +02:00
SchiZzA 21dfa61cd5
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-24 22:19:30 +02:00
SchiZzA 1d2c1b4be3
Introduce SWSRuntimeData dataclass for typed entry.runtime_data.
Adds the SWSRuntimeData dataclass and SWSConfigEntry type alias so
  per-entry state can move from loosely-typed hass.data[DOMAIN][entry_id]
  dicts to HA 2025+ typed entry.runtime_data. The legacy ENTRY_* string
  keys are kept in place for now so callers can be migrated incrementally.

  This commit is not final update. Just preparing for future complete refactor.
2026-05-11 13:49:40 +02:00
SchiZzA 202447405f
Typos corrected. 2026-04-11 17:37:53 +02:00
schizza fed00b437a
Add test-station-server as a submodule.
Include the test station server tool in the tools directory to facilitate local development and testing of weather station protocols.
2026-04-11 17:24:49 +02:00
schizza 15e9df00ca
Implement Ecowitt protocol support and dynamic binary sensor discovery.
- Integrate aioecowitt to parse incoming weather station payloads and map sensors to the SWS pipeline.
- Add a new webhook endpoint at /weatherhub/{webhook_id} with support for dynamic route resolution.
- Expand battery binary sensor definitions and implement logic for dynamic entity creation.
- Bump version to 2.0.0-pre1.
2026-04-11 16:37:17 +02:00
SchiZzA 50fe9e35fe
Add Ecowitt compatibility and deprecate legacy battery sensors. 2026-04-10 18:35:33 +02:00
SchiZzA 7f72497e9e
Update version to 2.0.pre0 2026-03-23 18:35:29 +01:00
SchiZzA e737fb16d3
Use configured WSLink add-on port for health/status endpoints. 2026-03-23 18:32:02 +01:00
SchiZzA 159d465db5
Improve sensor value parsing and add battery binary sensors.
- Introduce `to_int`/`to_float` helpers to safely handle None/blank payload values.
- Use the converters across weather/wslink sensor descriptions and simplify wind direction handling.
- Add low-battery binary sensor entities and definitions.
2026-03-23 18:21:05 +01:00
SchiZzA 9d5fafa8d0
Add configuration options for WSLink Addon port. 2026-03-23 18:14:46 +01:00
SchiZzA 63660006ea
Add health diagnostics coordinator and routing snapshot
Track ingress/forwarding status, expose detailed health sensors and translations, and include redacted diagnostics data.
2026-03-14 17:39:52 +01:00
SchiZzA 39b16afcbc
Update constants to more readable form. 2026-03-05 11:47:52 +01:00
SchiZzA f0554573ce
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-03-04 07:53:26 +01:00
SchiZzA 995f607cf7
Improve Windy error handling and retry logic 2026-03-03 14:17:34 +01:00
SchiZzA 3e573087a2
Add multiple health sensors and device info
Introduce HealthSensorEntityDescription and a tuple of sensor
descriptions for integration status, source IP, base URL and addon
response. Instantiate one HealthDiagnosticSensor per description in
async_setup_entry. Update HealthDiagnosticSensor to accept a
description, derive unique_id from description.key and add a cached
device_info returning a SERVICE-type device. Adjust imports.
2026-03-02 22:08:40 +01:00
SchiZzA 6a4eed2ff9
Validate hass data with py_typecheck.checked
Replace manual isinstance checks and casts with py_typecheck.checked()
to validate hass and entry data and return early on errors. Simplify
add_new_sensors by unwrapping values, renaming vars, and passing the
coordinator to WeatherSensor
2026-03-02 22:08:01 +01:00
SchiZzA b3aae77132
Replace casts with checked type helpers
Use checked and checked_or to validate option and hass.data types,
remove unsafe typing.cast calls, simplify coordinator and entry_data
handling, and cache boolean option flags for Windy and Pocasí checks
2026-03-02 22:06:09 +01:00
SchiZzA 7d1494f29b
Removed extended debugging info from sensors. Added descriptive method on route info. 2026-03-01 17:32:36 +01:00
SchiZzA 01058a07b4
Add WSLink support for additional sensor channels
- Extend constants and WSLink key remapping for channels 3–8 (temp, humidity, battery and connection)
- Add new WSLink sensor entity descriptions for the extra channel readings
- Update English translations for the newly added channel sensors and battery states
2026-03-01 16:56:46 +01:00
SchiZzA f06f8b31ae
Align Windy resend with Stations API response handling
- Add WINDY_MAX_RETRIES constant and use it consistently when deciding to disable resending
- Refactor Windy response verification to rely on HTTP status codes per stations.windy.com API
- Improve error handling for missing password, duplicate payloads and rate limiting
- Enhance retry logging and disable Windy resend via persistent notification on repeated failures
2026-03-01 13:51:17 +01:00
SchiZzA 95663fd78b
Stabilize webhook routing and config updates
- Register aiohttp webhook routes once and switch the active dispatcher handler on option changes
- Make the internal route registry method-aware (GET/POST) and improve enabled-route logging
- Fix OptionsFlow initialization by passing the config entry and using safe defaults for credentials
- Harden Windy resend by validating credentials early, auto-disabling the feature on invalid responses, and notifying the user
- Update translations for Windy credential validation errors
2026-03-01 12:48:23 +01:00
SchiZzA cc1afaa218
Organize imports in `__init__.py` 2026-02-26 17:59:25 +01:00