Repository navigation
Releases
v0.10.1: 7-day and 14-day snoozes
Longer snoozes
- The snooze dialog (single entity and group) now offers 7d and 14d alongside 15m, 1h, 8h and 1d.
sensor_sentinel.snoozeaccepts up to 20160 minutes (14 days); it was 10080.- Snooze confirmations show the preset label ("for 14d") instead of raw minutes.
v0.10.0: honest numbers
Honest numbers
recovered_todaycounts per device. A device that comes back counts once, however many entities it has, and it isn't counted again until 5 minutes pass with no further recovery. A single flapping speaker with 13 entities had pushed it past 9,000 in a day. Entities without a device still count individually.- Dropped, not recovered. A down entity that leaves because it, or its whole integration, was disabled or removed now fires
sensor_sentinel.entity_droppedwithreasonset tointegration_disabled,entity_disabledorremoved. It also shows up for 24 h in a newdroppedattribute on the count sensor and as a note on the card. Before, it just vanished from the count, which looked exactly like a recovery. - Fix: changing options during the startup warmup no longer seeds early, which used to count every boot-time
unknownas down.
v0.9.0: dead Z-Wave nodes
Dead Z-Wave nodes
- Dead nodes are flagged. A Z-Wave node that the Z-Wave JS driver has marked dead (its
Node statussensor readsdead) now raises an incident, just like an unavailable entity. The incident is named after the device, usesreason: dead, and clears when the node comes back. - New option (opt-in): "Ping quiet mains Z-Wave nodes every (hours)". The driver only notices a dead node when a message to it fails, and nothing ever sends a repeater or an idle plug anything, so one can sit dead while still reading
alive. With this set, Sentinel pings each mains node it hasn't heard from in that many hours, at most once per interval and 5 per 5-minute tick. A dead node fails the ping and gets flagged. Default 0 (off); 24 suits most homes. - Auto-recovery, when enabled, now pings dead nodes as well.
v0.8.2 — silent check is battery-only; header icon alignment
Follow-up to v0.8.1.
Silent check: battery devices only
Mains-powered devices (fans, plugs, Z-Wave extenders, sirens) could land in Silent - Check Battery. Mains nodes aren't polled and only report when something changes, so a device nobody touches for a couple of days goes quiet while being perfectly healthy. In the field, 15 of them were flagged at once, all with the same last_seen: the last Z-Wave JS restart.
The silent check now covers only devices that have a battery entity. A disabled battery entity still counts. If a mains device really does die, it goes unavailable on the first failed command, and the normal check reports that.
Card: silent-group icon alignment
The battery icon in the Silent - Check Battery header is now centred on the text line at any font size. It also uses mdi:battery-alert, which has the "!" inside the battery.
Upgrading
Update through HACS, then restart Home Assistant. The restart loads the integration change and makes browsers fetch the new card.
v0.8.1 — "Silent - Check Battery" group in the card
Card-only follow-up to v0.8.0. The integration itself is unchanged.
Silent devices get their own group
Silent devices (dead batteries that never went unavailable) were mixed in with their integration or area groups. They all need the same fix, so they now have one group of their own, Silent - Check Battery:
- Pinned first, whether the card groups by integration or by area.
- Longest-silent first, so the battery to change first is at the top.
- A warning-coloured header with a battery-alert icon.
- The usual snooze-all and exclude-all buttons, but no reload button, since a reload can't fix a battery.
- Searchable: typing "silent" or "battery" in the search box shows just this group.
After updating, hard-refresh the browser (or clear the app cache on mobile) to load the new card. Python is unchanged, so no restart is needed.
v0.8.0 — silent devices, and restarts no longer inflate the counts
Two changes since v0.7.10. The new check is opt-in and off by default, so upgrading changes nothing until you turn it on.
New: silent devices
Sentinel only noticed entities that went unavailable or unknown. A battery device that dies never gets there. Z-Wave JS marks a node dead only after a failed send, and it never sends to a sleeping node. So the node stays "asleep" forever, and its contact keeps reading "closed" at 100% battery. One real door sensor went 11 months like this without being flagged.
With Flag devices not heard from in (hours) set, any Z-Wave JS or Zigbee2MQTT device whose last_seen sensor is older than that becomes a single incident:
- It's named after the device and shows its last reported battery.
- Its duration counts from when the device went quiet.
- It clears the moment the device is heard from again.
- It appears with a silent badge in the card, and in a separate Silent section of notifications.
48 hours suits most battery sensors. Exempt devices that are normally quiet, such as button remotes, with Devices exempt from the silent check.
Details:
- Events:
entity_downandentity_recoverednow carry areasonfield (stateorsilent), plusdevice_id. Silent incidents also carrylast_seenandsilent_for_hours. Existing automations keep working unchanged. - Exclusions: entity, glob, integration and snooze exclusions apply. Domain exclusions don't, because they describe which states are normal for buttons and events, not whether a device is alive.
- No double reporting: a device that already has an unavailable entity is reported by that incident alone.
- Auto-recovery never acts on a silent device.
- Performance: the check runs in the existing 5-minute housekeeping tick, with no new listeners.
- Requirement: the device's
Last seensensor must be enabled. It's disabled by default on some Z-Wave JS installs. For Zigbee2MQTT, turn on itslast_seenoption.
Fix: restarts no longer inflate the counts (was v0.7.11, unreleased)
The startup warmup only delayed the initial scan. The state listener was already live, so during boot any entity that stayed unavailable longer than the grace period was counted as down, then as recovered a few seconds later. On a busy system, every restart added hundreds of MQTT and Sonos entities to recovered_today. Sentinel now ignores state changes until the warmup ends, then records whatever is still bad, exactly as before.
Tests
24 tests, 10 of them new. Each new safeguard was mutation-checked: removing it makes a test fail.
v0.7.10 — one reload per config entry, not one per entity
Follow-up to v0.7.9, found by reading the live logs after installing it.
What 0.7.9 missed
0.7.9 budgets recovery attempts per entity, but a config-entry reload acts on
the whole entry. A real Sonos log shows what that means: one offline speaker
owns fourteen entities, and all fourteen dispatched a reload of the same config
entry within ten milliseconds of each other.
0.7.9 already bounded this — each entity spends its budget and stops, so it can't
run all night any more. But fourteen simultaneous reloads of one entry is still a
storm where a single reload would do, and reload storms are their own hazard.
The fix
At most one reload per config entry per housekeeping pass, however many of its
entities are down.
Every sibling that wanted the reload still spends its attempt. That detail
matters: charging only the entity that happened to dispatch would leave the other
thirteen to take turns reloading the entry, one per pass, for hours — trading a
burst for a slow drip.
Nothing changes for single-entity integrations, and nothing changes about the
budget or refund rules introduced in 0.7.9: a reload that works is still free and
still repeatable.
Tests
The new test models the real shape — fourteen entities on one entry. Without the
guard it dispatches 14 reloads, matching the field log line-for-line; with it, 1.
Full changelog: v0.7.9...v0.7.10
v0.7.9 — auto-recovery can no longer reload a config entry forever
A field bug, found because Sonos looked broken and a manual reload didn't fix it.
What happened
Two battery Sonos speakers were powered off, so their entities sat permanently
unavailable. Auto-recovery responded the only way it can for a non-Z-Wave
entity — by reloading the owning config entry — which restarts every speaker
that entry owns. It repeated every ~15 minutes, all night. Manual reloads
appeared not to work because Sentinel simply broke them again minutes later.
RECOVERY_MAX_ATTEMPTS = 3 was supposed to stop this after three tries. It could
never fire: reloading an entry removes its entities from the state machine,
Sentinel read that removal as a recovery, and a recovery refunds the attempt
budget. The reload reset the counter meant to stop it reloading again. As a
side effect it also fired entity_recovered and inflated
sensor.sentinel_recovered_today for entities that never came back.
This affected every config-entry reload recovery, not just Sonos. Only the
Z-Wave ping path — which removes nothing — had a cap that actually held.
The fix
Attempts are now budgeted properly: spent on failure, refunded the moment the
entity is seen healthy again.
- Entity removal is no longer treated as recovery. The incident is dropped, but
nothing is banked and the budget survives. - The budget is refunded when the entity next reports healthy, including when
it is no longer tracked — which is exactly how it comes back after a reload.
Without this, an integration that a reload genuinely fixes (GE Home) would get
one successful reload and never another. - The budget scales to blast radius: 3 attempts normally, 1 when the entry
still owns working entities. Deliberately not a veto — reloading a shared
entry to fix one entity is legitimate, and blocking it outright would break
the integrations this feature exists for. Doing it 25 times a night is not. - A declined attempt no longer burns budget.
- Sentinel now logs when it gives up, and why, at
info. "Gave up" and "never
tried" were previously indistinguishable in the log.
Behaviour change worth knowing
If you rely on auto-recovery reloading a shared config entry, it now gets one
attempt instead of three before backing off until the entity recovers on its own.
A reload that works needs only one attempt, so integrations it actually fixes are
unaffected.
Auto-recovery remains opt-in and off by default. If you have it on and an
entity is offline because it is simply switched off — a battery speaker on its
charger, a seasonal device — an exclusion rule is a better fit than leaving it
for auto-recovery to discover.
Tests
New tests/test_recovery.py covers both directions, including the case that
must keep working. Against the pre-fix coordinator the runaway test reloads 10
times in 10 passes (unbounded, matching the field behaviour) and a removal
inflates recovered_today; after the fix, 3 and 0.
Full changelog: v0.7.8...v0.7.9
v0.7.8 — two interaction fixes: collapse-by-default and swallowed clicks
Two bugs in how the card handles interaction, both reported from daily use.
Groups reopened expanded despite "Collapse groups by default"
Expanding a group to triage an incident wrote that choice to localStorage
permanently. Nothing ever removed it — so after every entity in the group
recovered and the group disappeared, the override survived. The next entity to
go down brought the group back already expanded, ignoring the setting.
"Expand all" pinned every group open the same way.
- Collapse overrides are dropped once a group has no incidents left, so a group
that comes back starts at your configured default. Expanding a group that is
still down persists across reloads exactly as before. - Only deviations from
collapse_by_defaultare stored, so changing the setting
in the card editor now takes effect on groups you'd previously touched. - Stored state is namespaced per group by mode — an unresolved integration
(unknown) and an area-less entity (unassigned) no longer share an entry.
One-time effect: the first load after updating discards the old stored blob
(it holds exactly the stale entries this fixes), so any deliberately-expanded
groups reset to your default once.
Buttons needing two clicks, especially in the confirm dialogs
A browser only delivers a click if the press and the release land on the same
element. The card rebuilds its entire innerHTML on render, so any repaint
arriving between the two destroyed the button under the cursor and the click was
never dispatched.
Repaints were arriving constantly: Home Assistant hands every card a new hass
object on every state change anywhere in the instance, and the card repainted
unconditionally for all of them — thousands of full rebuilds an hour on a large
system, none of which its own output depended on.
- Repaint only when Sentinel's own sensor actually changed.
- Hold repaints for the duration of a press and flush on release, so an incident
update landing mid-click can no longer swallow it. - Relative ages ("4m", "2h") now refresh on their own 30s tick instead of
relying on those incidental repaints — skipped over an open dialog, a hidden
tab, or a focused filter box.
Measured in a browser with real clicks and a repaint firing mid-press:
0/1 delivered before, 4/4 after.
Side benefit: the card no longer rebuilds its DOM for unrelated state changes,
which is a meaningful drop in frontend work on instances with a lot of entities.
Full changelog: v0.7.7...v0.7.8
v0.7.7 — fix the last "Configuration error" trigger: a swallowed define()
The root cause, finally captured
v0.7.5's self-reporting trap caught the failure live: on affected page loads, Lovelace rebuilt the card into "Custom element doesn't exist: sensor-sentinel-card" over and over — while the card's JavaScript was demonstrably loaded and running.
How that's possible: the scoped custom-element-registry polyfill (pulled in by cards like tabbed-card) can make customElements.define() throw at a moment when get() still returns undefined. This card's define guard (added in v0.6.18) treated any define() throw as "another copy already registered it" and swallowed it. Under the polyfill race that assumption is wrong — the element ends up genuinely unregistered, so Home Assistant's every rebuild attempt re-fails and the red card sticks (or flashes on slow devices, where views build before the module arrives; HA only hides the placeholder error for 2 seconds).
Fixes
- Registration is now retried with backoff (up to 8 attempts) until
customElements.get()confirms the element is truly defined — adefine()throw is never again assumed benign. - The self-heal sweep re-defines the element first when it finds a stuck error card while the tag is undefined, then triggers the rebuild — previously the rebuild was doomed to re-fail.
- First sweep moved up to 300 ms (shorter visible flash on slow devices, e.g. the iPhone Companion app) and a 45 s tail sweep added.
- Better diagnostics: the
sensor-sentinel-error-logring now records the real error message and whether the tag was defined at heal time.
Note on updating
After updating via HACS, restart Core, then reload each device once (on the iPhone Companion app: Settings → Companion App → Debugging → Reset frontend cache if it's been showing stale behavior). Console banner should read SENSOR-SENTINEL-CARD v0.7.7.