From 7ec97a06d2793bce9796bd727e0c65e366171777 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Tue, 6 Oct 2026 08:02:37 +0000 Subject: [PATCH] Ignore 0% battery readings in low-battery alerting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 0% batteryLevel is an invalid/no-data value (no battery wired or misconfigured node), not a genuinely critical battery, so it must not produce a "⚠️ battery low" alert. Add a non-positive guard to shouldAlert after the undefined check: 0 (and any non-positive) now skips, while 1-20 still alert and >20 still doesn't. Threshold/cooldown/ batching unchanged. Tests: 3 new specs in the check-threshold block (0% excluded from the batch, lone 0% sends nothing, 1% still alerts). 180 -> 183. --- CHANGELOG.md | 4 +++ package.json | 2 +- src/battery/battery.service.spec.ts | 41 +++++++++++++++++++++++++++++ src/battery/battery.service.ts | 1 + 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f59183d..bb56f20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.21.12 — 2026-10-06 08:01 + +- Battery monitoring now ignores a 0% reading: a 0 (or any non-positive) `batteryLevel` is treated as an invalid/no-data value (node has no battery wired or is misconfigured) and no longer produces a "⚠️ battery low" alert, while genuinely-low readings (1–20%) still alert exactly as before. One-line guard added to `BatteryAlertService.shouldAlert` right after the `undefined` check. Tests: 3 new specs in the `check threshold` block — a 0% node is excluded from the batch (and a lone 0% sends nothing) while a 1% node still alerts. + ## v0.21.11 — 2026-10-05 14:47 - Migrated the opencode notifier to the v2 API routes: discovery is now `GET {api}{proxyPath}/api/session` (a `{ data, cursor }` envelope; sessions filtered by `location.directory`, most-recent `time.updated` wins) and send is now `POST {api}{proxyPath}/api/session/{sid}/prompt` with a `{text}` body, instead of the v1 `GET /session` + `POST /session/{id}/message` (`{parts:[{type:"text",text}]}`) routes. CodeNomad's per-workspace instance proxy was upgraded and now only serves opencode v2 `/api/*` — every v1 `/session/…` call 403s, which was breaking all notifications. v2 returns 2xx immediately (no longer holds the response until the turn finishes), so the 15 s read-timeout-as-delivered branch is kept only as a harmless safety net. diff --git a/package.json b/package.json index bd7cde6..367b0de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mesh-bot", - "version": "0.21.11", + "version": "0.21.12", "description": "", "author": "", "private": true, diff --git a/src/battery/battery.service.spec.ts b/src/battery/battery.service.spec.ts index 3ad7516..1187ac7 100644 --- a/src/battery/battery.service.spec.ts +++ b/src/battery/battery.service.spec.ts @@ -119,6 +119,47 @@ describe('BatteryAlertService check threshold', () => { expect(sendText).not.toHaveBeenCalled(); }); + it('ignores a 0% reading (invalid/no battery) even though 0 <= threshold', async () => { + const sendText = vi.fn(async (..._args: unknown[]) => 1); + // 0% must not alert, but a genuinely-low 5% node in the same batch must + const svc = makeService([node(1, 0), node(2, 5)], sendText); + + await svc.check(); + + expect(sendText).toHaveBeenCalledTimes(1); + // only the 5% node — the 0% node is excluded entirely + expect(sendText.mock.calls[0]).toEqual([ + BROADCAST_ADDR, + '⚠️ battery low: @!00000002 5%', + undefined, + 2, + ]); + }); + + it('ignores a lone 0% reading (no broadcast at all)', async () => { + const sendText = vi.fn(async (..._args: unknown[]) => 1); + const svc = makeService([node(1, 0)], sendText); // only an invalid 0% reading + + await svc.check(); + + expect(sendText).not.toHaveBeenCalled(); + }); + + it('still alerts a genuine 1% reading (lowest valid value)', async () => { + const sendText = vi.fn(async (..._args: unknown[]) => 1); + const svc = makeService([node(1, 1)], sendText); + + await svc.check(); + + expect(sendText).toHaveBeenCalledTimes(1); + expect(sendText.mock.calls[0]).toEqual([ + BROADCAST_ADDR, + '⚠️ battery low: @!00000001 1%', + undefined, + 2, + ]); + }); + it('merges all qualifying nodes into exactly ONE broadcast with the broadcast target', async () => { const sendText = vi.fn(async (..._args: unknown[]) => 1); const svc = makeService( diff --git a/src/battery/battery.service.ts b/src/battery/battery.service.ts index 362614e..2b7b383 100644 --- a/src/battery/battery.service.ts +++ b/src/battery/battery.service.ts @@ -60,6 +60,7 @@ export class BatteryAlertService { private shouldAlert(node: NodeSummary, now: number): boolean { if (node.batteryLevel === undefined) return false; // no battery reported — skip + if (node.batteryLevel <= 0) return false; // 0/invalid reading — no battery wired or misconfigured node, not a real low-battery alert // <= on purpose: a node at exactly the threshold is already "low" (spec allowed either < or <=; tests pin this choice) if (!(node.batteryLevel <= this.threshold)) return false; const last = this.lastAlertAt.get(node.num);