Skip to content

feat(binary_sensor): battery out-of-spec alert with hybrid debounce (#78)#116

Merged
dewet22 merged 1 commit into
mainfrom
feat/battery-out-of-spec-binary-sensor
Jun 4, 2026
Merged

feat(binary_sensor): battery out-of-spec alert with hybrid debounce (#78)#116
dewet22 merged 1 commit into
mainfrom
feat/battery-out-of-spec-binary-sensor

Conversation

@dewet22

@dewet22 dewet22 commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a single plant-level binary_sensor (device_class: problem) — battery_out_of_spec — rather than per-cell/per-metric sensors, so a consumer reads one entity and drills into its attributes.
  • Trips when any cell voltage (LFP soft band 3.0–3.5 V) or cell-group temperature (default 0–50 °C) across any pack has been out of spec for both ≥ 5 min wall-clock and ≥ 3 distinct polls.
  • The hybrid debounce is deliberately sized to comfortably exceed the ~2-minute persistence that dongle bad-read garbage can exhibit (modbus#78), so a sustained fake read can't trip the alert — and the dual form survives both fast and slow pollers (the drawback each single form has on its own).
  • Unpopulated cell slots (which read ~0 V on smaller packs) and dropped (None) reads are excluded from the checks. Attributes enumerate the current offenders (battery, metric, value, polls, seconds out of spec).
  • Created only when the plant actually has batteries.

Notes

Test plan

  • Debounce state machine: in-spec never trips; sustained low-voltage trips only after both thresholds; time-met-but-too-few-polls stays off; transient 2-poll excursion clears and resets the counter
  • Unpopulated ~0 V cell and None reads ignored; sustained over-temperature trips; offender attributes correct
  • Created + off for an in-spec plant; absent on a battery-less (PV-only) plant
  • full suite green (204 passed), mypy clean

🤖 Generated with Claude Code

)

Adds a single plant-level binary_sensor (device_class: problem) that trips when
any cell voltage (LFP 3.0-3.5 V band) or cell-group temperature (0-50 C) across
any pack has been out of spec for both >= 5 minutes wall-clock AND >= 3 distinct
polls. The dual debounce comfortably exceeds the ~2-minute dongle bad-read
persistence (modbus#78), so transient garbage can't trip it, while surviving both
fast and slow pollers. Unpopulated cell slots (~0 V) and dropped reads are
excluded. Attributes enumerate the current offenders for adaptive automations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@dewet22, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 32 minutes and 10 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 35cb3965-83bf-48e9-a426-bb66c24f1ed5

📥 Commits

Reviewing files that changed from the base of the PR and between 46640a8 and 249ed31.

📒 Files selected for processing (3)
  • custom_components/givenergy_local/binary_sensor.py
  • custom_components/givenergy_local/const.py
  • tests/test_binary_sensor.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/battery-out-of-spec-binary-sensor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new binary sensor platform to monitor and alert on out-of-spec battery cell voltages and temperatures, utilizing a hybrid debounce mechanism to filter out transient bad reads. The review feedback identifies a critical issue where transient dropped reads (which return None) mistakenly reset the debounce state machine because they are skipped during evaluation. To address this, the reviewer suggests only clearing offenders when they are explicitly read and confirmed to be back in spec, and provides a corresponding test case to verify this behavior.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +139 to +160
current: dict[str, _Reading] = {
f"{r.battery}:{r.metric}": r
for r in _iter_readings(self.coordinator.data.batteries)
if _out_of_spec(r)
}
# Drop anything that has returned to spec.
for key in list(self._offenders):
if key not in current:
del self._offenders[key]
# Record/advance current offenders.
for key, reading in current.items():
existing = self._offenders.get(key)
if existing is None:
self._offenders[key] = _Offender(
battery=reading.battery,
metric=reading.metric,
value=reading.value,
first_seen=refresh,
)
else:
existing.value = reading.value
existing.poll_count += 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Issue: Transient dropped reads (None) reset the debounce state machine

In the current implementation, any dropped read (None) or unpopulated slot (which are skipped in _iter_readings) will not be present in current. As a result, key not in current evaluates to True, and the offender is immediately deleted from self._offenders.

This means a single transient dropped read (which is common with flaky Modbus dongles) will completely reset the debounce counter for an active excursion, potentially preventing the alert from ever tripping or causing it to flap.

Solution

Instead of clearing offenders that are missing from current, we should only clear them if they are successfully read and confirmed to be back in spec. If a reading is missing (e.g., due to a dropped read), we should preserve the offender's state without incrementing its poll count.

        readings = {
            f"{r.battery}:{r.metric}": r
            for r in _iter_readings(self.coordinator.data.batteries)
        }
        # Drop anything that has returned to spec.
        for key in list(self._offenders):
            if key in readings and not _out_of_spec(readings[key]):
                del self._offenders[key]
        # Record/advance current offenders.
        for key, reading in readings.items():
            if _out_of_spec(reading):
                existing = self._offenders.get(key)
                if existing is None:
                    self._offenders[key] = _Offender(
                        battery=reading.battery,
                        metric=reading.metric,
                        value=reading.value,
                        first_seen=refresh,
                    )
                else:
                    existing.value = reading.value
                    existing.poll_count += 1

entity, coordinator = _entity([bat])
for n in range(5):
_poll(entity, coordinator, BASE + timedelta(seconds=n * DEBOUNCE_SECONDS))
assert entity.is_on is False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Improvement: Add test for transient dropped reads

To ensure that transient dropped reads (None) do not reset the debounce state machine, we should add a test case verifying that the offender state and poll count are preserved when a None reading is encountered during an active excursion.

Suggested change
assert entity.is_on is False
assert entity.is_on is False
def test_transient_none_reading_does_not_reset_debounce():
bad = [_battery(cells=[2.5] + [3.30] * 15)]
dropped = [_battery(cells=[None] + [3.30] * 15)]
entity, coordinator = _entity(bad)
_poll(entity, coordinator, BASE)
assert entity._offenders["BT1:cell_01_voltage"].poll_count == 1
# A dropped read should preserve the offender and its poll count
_poll(entity, coordinator, BASE + timedelta(seconds=150), batteries=dropped)
assert "BT1:cell_01_voltage" in entity._offenders
assert entity._offenders["BT1:cell_01_voltage"].poll_count == 1
# The next out-of-spec read resumes the counter
_poll(entity, coordinator, BASE + timedelta(seconds=300), batteries=bad)
assert entity._offenders["BT1:cell_01_voltage"].poll_count == 2

@deepsource-io

deepsource-io Bot commented Jun 3, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 46640a8...249ed31 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Python Jun 3, 2026 11:37p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@dewet22 dewet22 merged commit f5181db into main Jun 4, 2026
10 checks passed
@dewet22 dewet22 deleted the feat/battery-out-of-spec-binary-sensor branch June 4, 2026 00:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant