Commit Graph

4 Commits (624de521c4ee03ff1741da43b4466d65a784d5f3)

Author SHA1 Message Date
SchiZzA 624de521c4
feat(wslink): implement every sensor the WSLink API v0.6 defines
Coverage went from 54 of 107 upload parameters to all 107, adding roughly 34
entities. New families: Type5 lightning, Type6 water leak (CH1-7), Type8
particulate matter, Type10 CO2 and Type11 CO, plus the parameters missing from
existing families - absolute pressure, feels-like temperature, the 10-minute
average wind speed, and the per-channel probe type.

Device classes follow Home Assistant's own list (PM25, PM10, AQI, CO2, CO,
DISTANCE, ATMOSPHERIC_PRESSURE, WIND_SPEED, and binary MOISTURE for leaks), so
the readings work with statistics and unit conversion without template helpers.

Batteries were the trap. The API annotates two incompatible conventions -
"(Normal=1, Low battery=0)" and "(0~5) remark: 5 is full" - and mixing them up is
silent, because a level battery read as a flag reports "not low" for every level
above empty. Eight new binary batteries (t5lsbat, t6c1-7bat) and three new level
batteries (t8bat, t10bat, t11bat) are classified from the document, and
tests/test_wslink_api_coverage.py pins each one to the wording it came from.

Water leak and battery use opposite conventions in the same family (leak 1 means
wet, battery 0 means low), so `on_value` moved into the description and one
entity class now serves both instead of two that differ by a comparison.

`t1cn` was in the API but wired to nothing, so a disconnected outdoor probe kept
publishing its last readings. Gating it required fixing the gate first: an absent
connection flag was treated as "disconnected", which would have blanked
temperature, wind and rain outright on any firmware that omits `t1cn`. Absence is
now treated as no information, and only an explicit non-1 drops the readings.

The probe type (`t234cXtp`) decides whether a channel's humidity reading is air
humidity or soil moisture, which are different device classes. It is resolved
once, when the entity is created, because Home Assistant records the device class
in the entity registry; swapping the physical probe means reinstalling.

Two parameters are handled with a documented caveat: `inbat` is the only battery
whose scale the API does not state and stays a 0/1 flag, and `t5lst` has no unit
(the vendor example uses 9999) so it is read as minutes with 9999 meaning
"nothing recorded".

Also replaces the now-stale TODO block in const.py - it listed exactly these
sensors as unimplemented - and documents the complete coverage in the README.
2026-07-26 15:20:07 +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 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 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