Commit Graph

271 Commits (14466cbe601ac732d3db8a6fd1b725cd783051e8)

Author SHA1 Message Date
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 36591b32cf
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-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 2b4e1a5f8c
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-07-25 21:47:51 +02:00
SchiZzA 6167a3a536
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-07-25 21:47:51 +02:00
SchiZzA 9e8eff16d8
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-07-25 21:47:51 +02:00
SchiZzA 4e597c1fd5
fix(ecowitt): Connect Ecowitt bridge to sensor platform earlier 2026-07-25 21:47:50 +02:00
SchiZzA b578142099
fix(wslink): Definiton of HCHO, VOC, T9 battery diplicates. 2026-07-25 21:47:50 +02:00
SchiZzA 10717aba8b
Add stall sensor detection. 2026-07-25 21:47:50 +02:00
SchiZzA 6363351d9c
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-07-25 21:47:50 +02:00
SchiZzA e3d00389de
fix: Fixed type errors 2026-07-25 21:47:50 +02:00
SchiZzA 16a593e2a0
fix DEV_DBG as constant not string literal 2026-07-25 21:47:50 +02:00
SchiZzA e843e39ae4
fix bool truthy 2026-07-25 21:47:50 +02:00
SchiZzA 047bd5f6c7
hmac compare ID / KEY 2026-07-25 21:47:49 +02:00
SchiZzA 4b251b5998
fixed: Menu options 2026-07-25 21:47:49 +02:00
SchiZzA f00f1e9860
fix value in wslink sensors
Fix VOCLevel value
2026-07-25 21:47:49 +02:00
SchiZzA ddfd75d985
Ecowitt support
Update setiing (config_flow and translations) for Ecowitt settings.
Also updated utils.py to accept None for value.
2026-07-25 21:47:49 +02:00
SchiZzA fda7555e81
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-07-25 21:47:49 +02:00
schizza cf8b0d122c
```
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-07-25 21:47:49 +02:00
SchiZzA 7bba644bec
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-07-25 21:47:49 +02:00
SchiZzA c71d1b33b5
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-07-25 21:47:48 +02:00
SchiZzA 7025d8e193
Typos corrected. 2026-07-25 21:47:48 +02:00
schizza 0d8a5a884a
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-07-25 21:47:48 +02:00
schizza 9c9bca0c70
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-07-25 21:47:48 +02:00
SchiZzA dc69d20c3b
Add Ecowitt compatibility and deprecate legacy battery sensors. 2026-07-25 21:47:48 +02:00
SchiZzA 527162ded9
Update version to 2.0.pre0 2026-07-25 21:47:48 +02:00
SchiZzA e6d0c49e81
Use configured WSLink add-on port for health/status endpoints. 2026-07-25 21:47:47 +02:00
SchiZzA 1486238c5c
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-07-25 21:47:47 +02:00
SchiZzA 64a5bdb1d7
Add configuration options for WSLink Addon port. 2026-07-25 21:47:47 +02:00
SchiZzA affd26e7a5
Add health diagnostics coordinator and routing snapshot
Track ingress/forwarding status, expose detailed health sensors and translations, and include redacted diagnostics data.
2026-07-25 21:47:47 +02:00
SchiZzA 95b3452318
Update constants to more readable form. 2026-07-25 21:47:47 +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 56950b5e1a
Improve Windy error handling and retry logic 2026-07-25 21:47:47 +02:00
SchiZzA 970828320b
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-07-25 21:47:46 +02:00
SchiZzA 8ecbc8c38d
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-07-25 21:47:46 +02:00
SchiZzA fa5d268004
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-07-25 21:47:46 +02:00
SchiZzA 0f9bb0dab7
Removed extended debugging info from sensors. Added descriptive method on route info. 2026-07-25 21:47:46 +02:00
SchiZzA 68555160c7
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-07-25 21:47:46 +02:00
SchiZzA 38854698af
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-07-25 21:47:46 +02:00
SchiZzA 9ed0a10ffa
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-07-25 21:47:45 +02:00
SchiZzA 03d4e43a08
Organize imports in `__init__.py` 2026-07-25 21:45:08 +02:00
SchiZzA dfd2a2c05a
Added support for GET and POST endpoints. 2026-07-25 21:45:08 +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 56c73a46a2
Remove numpy dependency and simplify range checks in heat_index function 2026-07-25 21:45:08 +02:00
SchiZzA 8aaa1aa2df
Fix typo in fallback handler name from unregistred to unregistered 2026-07-25 21:45:08 +02:00
SchiZzA 68302c3cfb
Anonymize data payload in PocasiPush logging 2026-07-25 21:45:08 +02:00
SchiZzA e6e173e229
Remove verify_ssl=False from async_get_clientsession calls 2026-07-25 21:45:07 +02:00