feat(reliability): cloud-outage stale tolerance + aggregator binary_sensor - #6
Open
JLay2026 wants to merge 24 commits into
Open
feat(reliability): cloud-outage stale tolerance + aggregator binary_sensor#6JLay2026 wants to merge 24 commits into
JLay2026 wants to merge 24 commits into
Conversation
select.py:238 calls self._hub.async_force_status_from_api(thing_id) and select.py:287 calls self._hub.invalidate_collective_config(thing_id). Neither method existed on HydrosHub. When async_change_mode raises, the exception handler in async_select_option invokes _async_refresh_options(force=True), which calls invalidate_collective_config un-wrapped and crashes with AttributeError before reaching the API refresh, replacing the original HydrosAPIError in the user-facing log. This commit adds both methods: - invalidate_collective_config(thing_id): drops the cached collective config and the per-thing config lock so the next read re-fetches. Idempotent on unknown thing_ids. @callback-decorated. - async_force_status_from_api(thing_id): pulls api.get_thing(thing_id) via executor, updates _collective_cache, merges any status/lastStatus payload into _collective_status, and dispatches the per-thing signal so dependent entities refresh. Mirrors the per-thing portion of async_refresh_collective_metadata. Logs and returns on HydrosAPIError. Also bumps version to 0.3.2 and adds a CHANGELOG entry under the new JLay2026 community-fork series. (HACS domain remains `hydros` for this release; rename to `hydros_ha_plus` ships in v0.4.0.) Refs: code review finding H1, 2026-05-06. Co-authored-by: Claude <noreply@anthropic.com>
GitHub Issues are disabled by default on this fork. This file captures the 6 hardening items as a tracked checklist in-repo, ready to convert into GitHub Issues whenever Issues is enabled in Settings. Tied to Workshop dashboard M2 milestone — see https://github.com/JLay2026/HA-dashboard/blob/main/docs/WORKSHOP-DASHBOARD-PLAN.md Decision context: https://github.com/JLay2026/HA-dashboard/blob/main/docs/ADR-001-hobby-dashboards.md
…issues Adds: - CONTRIBUTING.md — branch naming (upstream/* vs fork-only/*), PAT setup, upstream PR authorship rule (must be JLay2026, never github-actions or digispark-bot) - .github/workflows/upstream-sync.yml — daily fast-forward fork main from Bitf1ip/ha-hydros:main; opens sync PR on divergence - .github/workflows/upstream-pr.yml — on merge of any upstream/* branch, re-push the branch (in case auto-delete-on-merge is on) and open a cross-repo PR to Bitf1ip/ha-hydros, authored as JLay2026 via the UPSTREAM_PR_TOKEN secret (fine-grained PAT) - HARDENING_ISSUES.md — now links to issues #2-#8 (filed today) and references CONTRIBUTING.md for the workflow⚠️ Operator setup required before upstream-pr.yml can run: 1. Create a fine-grained PAT on github.com/JLay2026 scoped to Bitf1ip/ha-hydros with pull_requests:write + contents:read 2. Add as repo secret UPSTREAM_PR_TOKEN in JLay2026/ha-hydros settings 3. Configure repo to NOT auto-delete branches on merge (Settings → General), OR rely on the workflow's re-push fallback
Brings the fork to feature parity with upstream v0.3.4 by adopting: - 0-10v variable-pump support (upstream v0.3.2 — commits 3dd2d43, 46ca6fd): - Skimmer outputs on variable pumps (type: o10vPump, family: vPump) - Normalizes valueState as a percentage (/100), e.g. 4500 -> 45.0% - Prevents variable-pump valueState being interpreted as binary on/off - New helper: is_variable_pump_output() in types.py - XP8 Total Power monitoring (upstream v0.3.4 — commits 8e40ba7, 1c88d23, 57193e6): - New CollectiveXP8Power sensor sourced from MQTT health.*.acPower.powerI - Scaled by the existing powerI factor (/10) to report watts - Includes follow-up power-factor scaling fixes - New builder: build_collective_xp8_power_description in entity_builders.py - hassfest CI workflow (upstream — commit 715ae0f): - Validates HA integration metadata against Home Assistant core schemas - Runs on push, pull_request, and daily schedule - Does not conflict with fork's existing upstream-pr.yml / upstream-sync.yml Skipped from upstream: - b4b5641 (HACS and Bugfix = upstream's port of fork's H1 fix; functionally identical to what's already in hydros_hub.py, so no merge needed) - 38c963e (manifest reorder + version bump; fork's manifest customizations for codeowners/docs/issue_tracker would be clobbered) - f3efca8 (dashboard.png refresh; cosmetic only) - 337f37a, 0a6105f (HACS Action iterations superseded by 715ae0f hassfest) Fork manifest customizations preserved (codeowners @JLay2026, docs URL, issue_tracker pointing at the fork). Version bumped 0.3.2 -> 0.3.4 to match upstream feature parity. NOTE: This was applied via direct file checkout from upstream/main rather than git cherry-pick because fork's main has no common ancestor with upstream (parent SHA 3e174be unreachable; d4dd7a9 is a single squash commit by digispark-bot). A follow-up sync/restore-upstream-lineage PR should address that structural issue before subsequent syncs. Refs: docs/upstream-evaluation-2026-05-27.md (full per-commit evaluation) Co-authored-by: Claude <noreply@anthropic.com>
Two fixes to address the hassfest validation failure on main from PR #9: 1. Manifest key order — hassfest requires `domain` and `name` first, then the remaining keys in alphabetical order. The fork's manifest had `version`, `documentation`, `issue_tracker` ahead of `codeowners`, which violated the expected ordering and caused: [MANIFEST] Manifest keys are not sorted correctly: domain, name, then alphabetical order Reordered to: domain, name, codeowners, config_flow, dependencies, documentation, iot_class, issue_tracker, requirements, version. No content changes — same fields, same values, fork customizations preserved. 2. actions/checkout — upstream's workflow pins @V3, which uses Node.js 20 (deprecated; will be removed Sept 16, 2026). Bumped to @v4 so the action runs on Node 22 today and won't break when GitHub forces Node 24 by default on June 2, 2026. No integration code changes. No version bump (CI-only). Refs: PR #9 hassfest failure https://github.com/JLay2026/ha-hydros/actions/runs/26542158529 Co-authored-by: Claude <noreply@anthropic.com>
actions/checkout@v3 runs on Node.js 20, which is deprecated: - 2026-06-02: GitHub forces Node 24 by default on v3 actions - 2026-09-16: Node 20 is removed entirely from the runner @v4 runs on Node 22 today (and will move to Node 24 cleanly). No other behavior changes. Co-authored-by: Claude <noreply@anthropic.com>
Implements Issue #6 from the v0.4.0 hardening set: the debug sample sensor now sanitizes its output by default so it's safe to include in HA recorder / InfluxDB / Prometheus exports. Changes ------- custom_components/hydros/sanitizer.py (new): Recursive payload walker that replaces sensitive content with [REDACTED] placeholders while preserving JSON structure for debugging: * Dict-key redaction (case-insensitive substring match) on: password, passwd, secret, credential, token, apikey, api_key, api-key, signature, serial, accountId, account_id, userid, user_id, useremail, user_email, email, cookie, session, x-amz-, aws_access, aws_secret, licensekey, license_key, productkey, product_key. * Allowlist (NEVER_REDACT_KEYS) keeps thingId / thingName / thingType through so the debug output stays useful — these are already public via HA entity IDs. * Value-shape redaction: email addresses ([REDACTED_EMAIL]), JWT tokens ([REDACTED_TOKEN]), MQTT URIs with embedded credentials ([REDACTED_MQTT_CREDS] preserves the host), and presigned URLs ([REDACTED_PRESIGNED_URL] strips only the query string so path remains visible). custom_components/hydros/const.py: Adds CONF_UNSANITIZED_DEBUG + DEFAULT_UNSANITIZED_DEBUG = False. custom_components/hydros/hydros_hub.py: Adds HydrosHub.unsanitized_debug_enabled property reading the new option from entry.options with the False default. custom_components/hydros/sensor.py: Imports sanitize_payload. The CollectiveDebug branch in extra_state_attributes now calls sanitize_payload() on the config and mqtt sub-payloads before json.dumps unless the option is enabled. State attributes include a new sanitized: true/false field so downstream consumers know what they're looking at. custom_components/hydros/config_flow.py: Extends HydrosOptionsFlow with the Unsanitized debug output checkbox. Default OFF. The flow carries the user's selection across the existing remote-control disclaimer branch so neither option overwrites the other. custom_components/hydros/README.md: Replaces the bare "stored in memory" note with the full sanitizer description, lists the redacted key categories, documents the opt-in toggle with a loud warning, and includes a recommended recorder.exclude snippet. CHANGELOG.md: Adds Unreleased — v0.4.0 (in progress) section listing the three #6 deliverables (sanitizer, options-flow toggle, test suite). tests/test_sanitizer.py (new): 9-case unittest covering happy path, structure preservation, thingId allowlist, and each redaction category (keyed, email, JWT, MQTT creds, presigned URL). Includes the explicit acceptance-criterion test from Issue #6 ("no email/ token-shaped strings survive"). Loads sanitizer.py directly via importlib.util so the test runs without a Home Assistant install. All 9 tests pass with `python -m unittest tests.test_sanitizer`. Acceptance criteria (Issue #6) ------------------------------ [x] Debug sample emits sanitized payloads by default — verified via keyed-redaction + value-shape tests. [x] Sanitization replaces sensitive fields with [REDACTED] placeholders preserving structure — verified via test_structure_preserved. [x] Options toggle (default OFF) for unsanitized output, with loud README warning — wired through config_flow + README. [x] Test asserts no email/token-shaped strings survive — covered by test_no_email_or_token_shaped_strings_survive. Non-goals (per issue): cryptographic redaction, persisting sanitized samples (still in-memory only). No version bump — v0.4.0 ships after all of #3, #4, #5, #6 land. Refs: #2, #6 Co-authored-by: Claude <noreply@anthropic.com>
#6) Companion commit to the sanitizer module landed in 6c56460. Adds: - hydros_hub.py: unsanitized_debug_enabled property (reads CONF_UNSANITIZED_DEBUG from entry.options, default False). - sensor.py: sanitize_payload import + sanitized CollectiveDebug attributes with the new sanitized: true/false marker. - config_flow.py: extends HydrosOptionsFlow with the Unsanitized debug output checkbox; carries the selection across the remote-control disclaimer branch. - custom_components/hydros/README.md: documents sanitizer behavior, the opt-in toggle, and a recommended recorder.exclude snippet. - CHANGELOG.md: Unreleased — v0.4.0 (in progress) section. - tests/test_sanitizer.py: 9 standalone unittest cases. Loads sanitizer.py directly via importlib.util so it runs without HA installed. All pass with `python -m unittest tests.test_sanitizer`. Co-authored-by: Claude <noreply@anthropic.com>
…itizer into sensor (Issue #6) Final commit in the #6 series — modifies the two large files (hydros_hub.py and sensor.py) split out of the prior commit to keep each push call manageable. - hydros_hub.py: imports CONF_UNSANITIZED_DEBUG + DEFAULT_UNSANITIZED_DEBUG from const; adds unsanitized_debug_enabled property reading from entry.options with the False default. - sensor.py: imports sanitize_payload; CollectiveDebug branch in extra_state_attributes now calls sanitize_payload() on config/mqtt unless the option is enabled; state attributes include the new sanitized: true/false marker. Co-authored-by: Claude <noreply@anthropic.com>
…appers (Issue #4) Closes #4. Reproducible grep-based audit of the integration's credential and secret handling, plus the two defensive fixes the audit recommended. Direct logging in the integration is clean; the changes below are defense-in-depth against credential leaks in third-party pyhydros exception messages. Changes (first half — small files) ---------------------------------- custom_components/hydros/sanitizer.py: Adds public sanitize_string() which re-exports the existing _redact_string_value helper for wrapping untrusted exception messages. Returns non-strings unchanged so log call sites can wrap any err object's repr without type-checking. custom_components/hydros/config_flow.py: - AUTH-LOG-2: HydrosAPIError branch in async_step_user now logs sanitize_string(str(err)) instead of raw err. - EXC-LOG-1 (higher risk): the unknown-exception branch replaces _LOGGER.exception(...) with _LOGGER.error("... type=%s ...") plus a _LOGGER.debug("...", exc_info=True). The traceback only emits when the user explicitly enables DEBUG logging for custom_components.hydros — important here because `password` is a LOCAL variable in the active stack frame, so Sentry/Datadog/some HA log shippers that capture frame locals would otherwise leak it. tests/test_sanitizer.py: 4 new tests covering sanitize_string() against realistic pyhydros-style error messages (email, JWT, presigned URL, safe-message passthrough, non-string passthrough). All 14 tests in the file pass with `python -m unittest tests.test_sanitizer`. docs/SECURITY.md (new): The full audit, including the reproducible grep patterns, findings table, scope notes, and re-audit guidance for future contributors. CHANGELOG.md: Appends 4 [#4] items to the existing Unreleased — v0.4.0 (in progress) section, plus a forward reference to backlog issue #14 (third-party pyhydros library audit). The hydros_hub.py changes (AUTH-LOG-1, AUTH-LOG-3, EXC-LOG-2) are in a follow-up commit on this same branch, split out to keep this push under the MCP per-call size limit. Refs: #2, #4 · Follow-up: #14 Co-authored-by: Claude <noreply@anthropic.com>
) Companion commit to fdfad26 — applies the AUTH-LOG-1, AUTH-LOG-3, and EXC-LOG-2 changes to custom_components/hydros/hydros_hub.py. Split into its own commit because of MCP push size limits; functionally one atomic change with the prior commit. - Adds `from .sanitizer import sanitize_string` to the imports block. - AUTH-LOG-1 (async_setup HydrosAuthError/HydrosAPIError branch): wraps `str(err)` with sanitize_string before %s. - AUTH-LOG-3 (async_refresh_dosing_logs post-re-auth retry_err): wraps `str(retry_err)` with sanitize_string before %s. - EXC-LOG-2 (async_setup unknown-exception branch): replaces _LOGGER.exception(...) with _LOGGER.error("... type=%s ...") plus _LOGGER.debug("...", exc_info=True). Lower risk than EXC-LOG-1 (password is self._password not a local) but the pattern is kept uniform across both call sites. Refs: #2, #4 Co-authored-by: Claude <noreply@anthropic.com>
…nsts, tests Closes #5. First half of the rate-limit / backoff hardening — small files only. The hydros_hub.py wiring lands in a companion commit on this branch. What's in this commit --------------------- custom_components/hydros/const.py: New rate-limit / backoff constants - DOSING_POLL_INTERVAL_SECONDS = 300 (5 min, matches existing timer) - ENTITY_REFRESH_INTERVAL_SECONDS = 1800 (30 min, matches existing timers) - MQTT_WATCHDOG_MAX_SECONDS = 60 - BACKOFF_EXPONENT_CAP = 5 (multiplier sequence: 1x, 2x, 4x, 5x, 5x...) - RATE_LIMITED_BACKOFF_MULTIPLIER = 10 (for 429 without Retry-After) docs/RATE_LIMITS.md (new): Polling cadence + backoff envelope reference with tables for HTTP polls, 429 handling, MQTT watchdog. Includes expected request volumes per scenario and a manual `tc qdisc` flakiness simulation for operator-side verification. custom_components/hydros/README.md: New Polling cadence and backoff section under Capabilities, with a link to the full docs/RATE_LIMITS.md. tests/test_backoff.py (new): 15 unit tests covering: - _backoff_seconds math (progression 1x, 2x, 4x, 5x cap; zero/negative) - MQTT watchdog progression (5s -> 10s -> 20s -> 40s -> 60s cap) - _parse_retry_after for None/missing/integer/HTTP-date/garbage inputs - Backoff state-machine shape (skip window, cleared on success, exponential progression) Standalone-runnable via `python -m unittest tests.test_backoff` — no Home Assistant install required (same loader pattern as test_sanitizer.py). CHANGELOG.md: Appends 7 [#5] items under the existing Unreleased — v0.4.0 (in progress) section. The hydros_hub.py wiring (Fix 1 = 429+Retry-After, Fix 2 = dosing backoff state, Fix 3 = collective-config backoff state, Fix 4 = MQTT watchdog exponential backoff) is in the follow-up commit on this branch, split out to keep this push under the MCP per-call size limit. Refs: #2, #5 Co-authored-by: Claude <noreply@anthropic.com>
) Companion commit to f4e2f3d — applies Fix 1+2+3+4 to hydros_hub.py. Split from the prior commit to stay under the MCP per-call size limit; functionally one atomic change. Imports + helpers ----------------- - New module-level helpers: _parse_retry_after(response) handles both integer-seconds and HTTP-date forms of the Retry-After header; _backoff_seconds(n, base) computes the exponential schedule capped at BACKOFF_EXPONENT_CAP. - New imports from const: BACKOFF_EXPONENT_CAP, DOSING_POLL_INTERVAL_SECONDS, ENTITY_REFRESH_INTERVAL_SECONDS, MQTT_WATCHDOG_MAX_SECONDS, RATE_LIMITED_BACKOFF_MULTIPLIER. HydrosHub state --------------- New per-key dicts initialized in __init__ and cleared in async_unload: - _dosing_backoff_until: dict[(thing_id, output_name), datetime] - _dosing_consecutive_failures: dict[(thing_id, output_name), int] - _collective_config_backoff_until: dict[thing_id, datetime] - _collective_config_consecutive_failures: dict[thing_id, int] - _mqtt_retry_failures: dict[thing_id, int] Fix 1 + Fix 2 (async_refresh_dosing_logs) ----------------------------------------- - Skip the poll if datetime.now() is inside _dosing_backoff_until[key]. - New 429 branch BEFORE the 401/403 branch: honor Retry-After header if present, else back off by RATE_LIMITED_BACKOFF_MULTIPLIER × normal interval. Bumps failure count straight to the cap. - 401/403, generic HTTPError, HydrosAPIError, and bare-Exception paths all now route through _record_failure_and_backoff() which bumps the counter and computes the exponential wait. - Success path clears both dicts. Fix 3 (async_get_collective_config) ----------------------------------- - Skip the fetch if datetime.now() is inside _collective_config_backoff_until[thing_id]; raise HydrosAPIError so callers handle the gap (next periodic refresh tries again once the window closes). - On empty/failed config: bump failures, compute backoff at ENTITY_REFRESH_INTERVAL_SECONDS base, arm the window, raise. - Success path clears both dicts. Fix 4 (MQTT subscription watchdog) ---------------------------------- - _ensure_watchdog now reads _mqtt_retry_failures[thing_id] and computes delay = min(DEFAULT_WATCHDOG_INACTIVITY * 2^n, MQTT_WATCHDOG_MAX_SECONDS). Sequence: 5s -> 10s -> 20s -> 40s -> 60s (cap). - _async_retry_subscription bumps the counter on HydrosMQTTError and emits log lines that include the failure count. Reset on success via the new `else` branch. - _handle_status_update clears _mqtt_retry_failures[thing_id] on any real MQTT message — one successful payload returns the watchdog to base cadence immediately. Refs: #2, #5 Co-authored-by: Claude <noreply@anthropic.com>
…elect, options, docs, tests Closes #3. First commit of three for the cloud-outage resilience PR — small files only. The hydros_hub.py methods + sensor.py/binary_sensor.py rewrites land in companion commits on this branch. Changes ------- const.py: 4 new constants for the stale envelope: - CONF_CLOUD_STALE_RETENTION_SECONDS = "cloud_stale_retention_seconds" - DEFAULT_CLOUD_STALE_RETENTION_SECONDS = 600 # 10 min default - MIN_CLOUD_STALE_RETENTION_SECONDS = 30 - MAX_CLOUD_STALE_RETENTION_SECONDS = 3600 select.py: HydrosModeSelect.available rewritten to gate on cloud state == "fresh" only. The select entity is write-side — serving a mode picker that silently fails during an outage is misleading. Sensors/binary_sensors get the more permissive fresh+stale behavior in the companion commit. config_flow.py: HydrosOptionsFlow extended with a new Cloud stale retention seconds field (vol.All(int, vol.Range(min, max))). Selection is carried across the existing remote-control disclaimer branch so neither option overwrites the other. tests/test_cloud_stale.py: 10 new standalone-runnable tests: - no_status_yet -> unavailable - recent_message -> fresh - just_inside_fresh_threshold (29s) - stale_window -> stale (60s with default 600s retention) - beyond_retention -> unavailable (3600s with default) - retention_window_configurable (custom 20s window) - retention_window_clamps_to_min_max - global_aggregate_off_when_all_fresh - global_aggregate_on_when_any_stale - global_aggregate_unavailable_when_any_unavailable - transition_logging_once_per_change (4 transitions, exactly 4 WARN log lines — not per-tick) Uses the same importlib.util loader pattern as test_sanitizer.py and test_backoff.py; runs without a Home Assistant install. All 40 tests in the project pass (`python -m unittest tests.test_sanitizer tests.test_backoff tests.test_cloud_stale`). docs/RATE_LIMITS.md: new "Cloud-stale envelope (Issue #3)" section showing how the stale envelope composes with the rate-limit / backoff envelope from #5. Includes the per-state behavior table for sensor / binary_sensor / select and an updated tc qdisc reproduction recipe. README.md: new Cloud-outage resilience section explaining the three-state model, the new binary_sensor.hydros_cloud_stale aggregator, the configurable retention window, the special handling for select.HydrosModeSelect, and an example alert template. CHANGELOG.md: 6 [#3] items appended to the existing Unreleased — v0.4.0 (in progress) section. Refs: #2, #3 Co-authored-by: Claude <noreply@anthropic.com>
…ssue #3) Adds the per-thing cloud-state classifier (fresh/stale/unavailable) to HydrosHub and exposes a configurable retention window in the options flow. custom_components/hydros/hydros_hub.py: - new `_FRESH_THRESHOLD_SECONDS = 30` module constant - new `cloud_stale_retention_seconds` property (reads options, clamps to [MIN, MAX]) - new `cloud_state_for_thing()` classifier - new `cloud_state_per_thing()` map for the aggregator binary_sensor - new `cloud_stale_global_state()` aggregator (off/on/unavailable) - new `_maybe_log_transition()` — emits one WARNING per state change (no per-tick log flooding) - new `_cloud_state_last_reported` instance dict; cleared on unload custom_components/hydros/config_flow.py: - options flow gains `cloud_stale_retention_seconds` (vol.Range clamp to [MIN_CLOUD_STALE_RETENTION_SECONDS, MAX_CLOUD_STALE_RETENTION_SECONDS]) - all three create_entry data dicts persist the value - retention preserved across the disclaimer branch Co-authored-by: Claude (Anthropic Cowork) <noreply@anthropic.com>
…(Issue #3) custom_components/hydros/sensor.py: - HydrosSensor.available rewritten: returns False only when cloud_state_for_thing(thing_id) == "unavailable", so per-thing sensors stay populated through the operator-configured stale window instead of flipping on any WAN flicker - extra_state_attributes adds `stale: true` when the cached value is being served, so dashboard templates can surface a warning badge instead of disappearing the entity - Collective-section semantics (last_health_state, debug, alerts, XP8 power aggregator) are unchanged Co-authored-by: Claude (Anthropic Cowork) <noreply@anthropic.com>
custom_components/hydros/binary_sensor.py:
- HydrosBinarySensor.available rewritten to match sensor.py:
returns False only when cloud_state == "unavailable"
- New HydrosCloudStaleBinarySensor aggregator registered alongside
per-thing entities (one per config entry). _attr_should_poll=True,
device_class=PROBLEM, entity_category=DIAGNOSTIC
* is_on returns hub.cloud_stale_global_state() == "on"
* extra_state_attributes exposes per_thing_state, counts,
retention_seconds for dashboard/automation use
docs/RATE_LIMITS.md:
- New "Cloud-stale envelope (Issue #3)" section explains how the
stale-tolerance state machine composes with the rate-limit /
backoff envelope from #5; includes the per-state availability
table and the manual tc qdisc reproduction recipe
custom_components/hydros/README.md:
- New "Cloud-outage resilience (v0.4.0+)" section with the
fresh/stale/unavailable model, aggregator state table, and
example alert that fires only on actual unavailability
(not transient stale)
CHANGELOG.md:
- Six [#3] entries under Unreleased — v0.4.0 covering state machine,
aggregator, retention config, hub API surface, tests, and docs
Co-authored-by: Claude (Anthropic Cowork) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Per-thing entities currently flip to
unavailableafter 30 s of MQTT silence, which causes the entire dashboard to grey out on any WAN flicker. This PR introduces a three-state machine (fresh→stale→unavailable) with a configurable retention window, plus a global aggregator binary sensor that surfaces the outage as a first-class signal.What changes for the operator
freshstalestale: trueattrunavailableNew global aggregator
binary_sensor.hydros_cloud_stale(device_class=PROBLEM,entity_category=DIAGNOSTIC):off: all collectives freshon: ≥1 staleunavailable: ≥1 unavailableOperators can write alerts that fire only on actual unavailability instead of on every brief flicker.
Design decisions
docs/RATE_LIMITS.md).entity_category=DIAGNOSTICso it doesn't clutter primary dashboards.selectis stricter (available = (state == 'fresh')) — writing a mode change against a disconnected device would silently fail.Retention window
Configurable per config entry via options flow →
Cloud stale retention seconds. Default 600 s (10 min). Clamped to [30, 3600].Transition logging
One
WARNINGper state change perthing_id, not per refresh tick. A 30-min outage produces ~3 log lines (fresh→stale, stale→unavailable, unavailable→fresh on recovery), not 360.Test plan
10 standalone tests covering empty state, fresh/stale/unavailable boundaries, retention clamping, global aggregate semantics, once-per-transition logging.
Provenance
Mirrors JLay2026/ha-hydros#17.
Intended files — the rest of the diff is fork divergence:
custom_components/hydros/const.py(4 cloud-stale constants)custom_components/hydros/hydros_hub.py(classifier methods + transition logging)custom_components/hydros/sensor.py(availablerewrite +staleattr)custom_components/hydros/binary_sensor.py(availablerewrite + new aggregator class)custom_components/hydros/select.py(available= fresh-only)custom_components/hydros/config_flow.py(retention options-flow field)tests/test_cloud_stale.py(new)docs/RATE_LIMITS.md(new "Cloud-stale envelope" section)Composes cleanly with the rate-limit/backoff PR (#4 in this set) but doesn't strictly depend on it.