Commit Graph

309 Commits (9402d69d491108acc49555a1448c3b713e0727b4)

Author SHA1 Message Date
SchiZzA 9402d69d49
fix(document): Document duplicate-route failure in migration warning 2026-07-26 23:36:57 +02:00
SchiZzA 0407cb049d
fix(review): fix(review): lead repair notice with warning, cap entity list 2026-07-26 23:22:12 +02:00
SchiZzA c510b64260
fix(review): fix(review): pick station device by entity count, drop ghost devices 2026-07-26 23:14:14 +02:00
SchiZzA 8c297cf90c
fix(review): fix(review): keep refused entities, split route errors, warn on stalled migration 2026-07-26 23:03:09 +02:00
SchiZzA 0400a9cdee feat(migration): adopt predecessor integration entities, keeping history
The integration is moving to a new repository under a new domain. Home Assistant
keys recorder history and long-term statistics on entity_id, not on the domain,
so the move is free provided the registry entries are carried over instead of
recreated. A recreated entity gets a fresh entity_id and its history is orphaned.

predecessor.py does the carry-over with async_update_entity_platform, which
rewrites platform and config_entry_id and leaves entity_id alone. Everything the
user set by hand rides along, because the registry row itself survives: renames,
icons, areas, labels, categories, hidden and disabled state.

Guards, each one covered by a test that fails when it is removed:

- The startup placeholder state (unavailable + ATTR_RESTORED) is cleared first,
  because async_update_entity_platform accepts only a missing or unknown state.
- A state that is not a placeholder means an integration is still driving the
  entity, so it is refused rather than moved out from under a live platform.
- A unique_id already taken under our own domain is refused. Home Assistant does
  not check this itself: the duplicate guard in _async_update_entity only runs
  when new_unique_id is passed.
- Devices are repointed before the old entry is removed. A device that loses its
  last config entry is deleted, and that takes its entities with it.
- The old config entry is removed only when every entity made it across.
  Otherwise it stays, a retry stays possible, and nothing is lost.

Options are inherited in a separate, earlier pass. Adopting the registry alone
is not enough: SENSORS_TO_LOAD decides which entities the platform creates, so
an empty one leaves every adopted entry showing "no longer provided" until a
payload arrives, and derived sensors are never in a payload at all. The merge is
predecessor-fills-gaps, so anything just entered in the config flow wins. The
version 1 spelling of the Pocasi Meteo flag is translated on the way in, since
async_migrate_entry runs against this entry's own version and would never look
at a key copied in afterwards.

The two passes run at different points in async_setup_entry. Options first,
before the coordinator and routes read them, or an inherited WSLINK flag would
leave the entry listening on the wrong endpoint until a reload. Adoption after
the routes are proven but before async_forward_entry_setups, so the one
destructive step cannot run in a setup that then fails, and so our platforms
bind to the adopted registry entries rather than to freshly created ones.

Two problems the migration surfaced, fixed here as well:

- register_path now catches ValueError alongside RuntimeError and raises
  ConfigEntryNotReady with a message naming HACS. aiohttp rejects a duplicate
  route name with ValueError, and the route names are fixed rather than
  domain-scoped, so a user who has not yet removed the old version hit an
  unhandled traceback.
- add_new_sensors applies the derived-sensor expansion that async_setup_entry
  already applied. Derived sensors are computed, never sent, so discovery can
  only offer their inputs; the azimut unlocked by a newly discovered wind
  direction went missing until the next restart. Only the sensors this batch
  unlocks are added, so nothing is created twice.

PREDECESSOR_DOMAIN equals DOMAIN for now, which short-circuits the whole pass.
It stays correct until the day the domain changes.
2026-07-26 22:08:01 +02:00
SchiZzA 935953f20f
fix(document): Drop stale WSLink TODO preamble, refresh binary sensor docs 2026-07-26 16:28:05 +02:00
SchiZzA 3cdaffc81f
fix(review): parse battery flags via to_int, release routes on removal 2026-07-26 15:53:03 +02:00
SchiZzA d2ae26eab5
fix(review): persist WSLink probe types and harden flag parsing 2026-07-26 15:41:30 +02:00
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 4b85d2e393
fix(lint): Track pytest config and add ruff CI lint job 2026-07-26 14:38:43 +02:00
SchiZzA 83ca9b7f67
fix(lint): Add repo ruff config matching Home Assistant core conventions 2026-07-26 13:54:57 +02:00
SchiZzA 56ea41c143
fix(document): Document Pocasi disable notification and timeout retry exclusion 2026-07-26 13:46:00 +02:00
SchiZzA c33f9454f7
fix(review): stop counting timeouts, notify on disable, drop WSLink indoor 2026-07-26 13:25:39 +02:00
SchiZzA 2c6b698ca9
fix(review): clamp heat index, unify Windy uv, bound forward timeouts 2026-07-26 13:13:05 +02:00
SchiZzA 4e4c3b8f7c
merge: reconcile with the cancelled run's pushed head
The cancelled run 01KYDJNXGJ pushed 1bb8864 and 2827eac, then the branch continued
from before them. Both were cherry-picked back onto the current head, so this merge
carries no new content - it only makes the branch a descendant of what the pipeline
gate still holds, so validation can run again.

config_flow.py keeps the branch version: it already contains both sides (the
blank/whitespace credential rejection and NoURLAvailableError fallback, plus the
webhook-id validation from the pipeline commit).

# Conflicts:
#	custom_components/sws12500/config_flow.py
2026-07-26 12:09:14 +02:00
SchiZzA 7cc3a99abb
fix(document): Refresh runtime route docs 2026-07-26 12:03:37 +02:00
SchiZzA 0dfdd6c9e9
fix(review): Disable stale routes and validate credentials 2026-07-26 12:03:36 +02:00
SchiZzA 2b266f33b4
feat(sws12500): forward Ecowitt payloads to Windy and Pocasi Meteo 2026-07-26 11:50:11 +02:00
SchiZzA 0e5a8ce10c
fix(wslink): compute heat and wind chill index when the station omits them
The WU/PWS platform derives both indices from temperature, humidity and wind
(value_from_data_fn=heat_index / chill_index), but the WSLink descriptions only
read the station's own t1heat / t1chill via value_fn=to_float. Stations that do
not report those fields still got both entities, because
_auto_enable_derived_sensors creates them as soon as the raw inputs arrive, so
they stayed Unavailable forever.

Add wslink_heat_index / wslink_chill_index: prefer the station's reading, compute
it otherwise. The conversions are the subtle part. The WSLink payload is metric
and both entities are declared in Celsius, while the NWS formulas are defined for
Fahrenheit, and chill_index converts the temperature but not the wind speed even
though its formula needs mph. So the temperature goes C -> F in, the wind m/s ->
mph, and the result F -> C back out. For the reported payload (6.2 C, 40 %,
30.6 m/s) that is -1.94 C; leaving the wind in m/s would have reported 0.34 C.

The native unit stays Celsius on purpose: switching it to Fahrenheit to match the
WU platform would misread the values of stations that do send t1heat / t1chill.

Also guard on missing temperature/humidity/wind before delegating, so a payload
without those fields no longer logs an error on every push.
2026-07-26 01:20:56 +02:00
SchiZzA 7c9343f90d
fix(sws12500): harden credential validation, URL fallback and push timeouts
- 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.
2026-07-26 00:39:15 +02:00
SchiZzA 2827eac158
fix(document): Refresh runtime route docs 2026-07-26 00:01:15 +02:00
SchiZzA 1bb8864426
fix(review): Disable stale routes and validate credentials 2026-07-25 23:42:17 +02:00
SchiZzA 5adc684558
refactor(sws12500): centralize battery sensor classification 2026-07-25 23:24:45 +02:00
SchiZzA 7f3e6bd5df
no-mistakes(document): Clarify WSLink add-on port default in README 2026-07-25 22:46:12 +02:00
SchiZzA 6914d87f5c
no-mistakes(document): Update README for Ecowitt, Windy and diagnostics changes 2026-07-25 22:37:28 +02:00
SchiZzA 1b69737408
no-mistakes(review): Migrate Pocasi option key, fix typos and Windy status 2026-07-25 22:12:55 +02:00
SchiZzA 5e4b02fd6e
fix: address remaining review findings across forwarders and utils
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.
2026-07-25 21:47:54 +02:00
SchiZzA 8b38db6951
Add tests for legacy/Ecowitt protocol conflict
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.
2026-07-25 21:47:54 +02:00
SchiZzA 2f30e161a6
fix: block enabling legacy and Ecowitt protocols together 2026-07-25 21:47:53 +02:00
SchiZzA a9098555ba
fix(Pocasi): Add retry limit and response handling
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.
2026-07-25 21:47:53 +02:00
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 b2fbc33821
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-07-25 21:47:52 +02:00
SchiZzA 242f2ee1b7
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-07-25 21:47:52 +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 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