battery: merge multiple low-battery nodes into one broadcast per cycle
CI / build + unit (pull_request) Successful in 1m0s
CI / build + unit (pull_request) Successful in 1m0s
Owner follow-up: 'if multiple ppl shows at once — merge messages to one'.
check() now collects every qualifying node (at/below threshold and not
within its per-node cooldown) and sends AT MOST ONE broadcast per cycle
containing all of them, recording the alert timestamp for each after
sending; nothing is sent when none qualify. Format: '⚠️ battery low:
@!<hex> (<name>) <X>%[, @!<hex> (<name>) <Y>%]' — single-node case uses
the same one-entry template. Cooldown semantics unchanged.
This commit is contained in:
@@ -47,21 +47,27 @@ describe('mention', () => {
|
||||
});
|
||||
|
||||
describe('formatAlert', () => {
|
||||
it('builds the full single-packet message with longName', () => {
|
||||
expect(formatAlert(node(0x1234abcd, 15, 'Kamil SP8MK/P'))).toBe(
|
||||
'⚠️ @!1234abcd battery low: 15% (Kamil SP8MK/P)',
|
||||
it('builds the single-node message with longName', () => {
|
||||
expect(formatAlert([node(0x1234abcd, 15, 'Kamil SP8MK/P')])).toBe(
|
||||
'⚠️ battery low: @!1234abcd (Kamil SP8MK/P) 15%',
|
||||
);
|
||||
});
|
||||
|
||||
it('appends additional nodes as ", <entry>" in one line', () => {
|
||||
expect(
|
||||
formatAlert([node(0xabc, 5, 'A'), node(0xdef, 3, undefined, 'B')]),
|
||||
).toBe('⚠️ battery low: @!00000abc (A) 5%, @!00000def (B) 3%');
|
||||
});
|
||||
|
||||
it('falls back to shortName and omits the name part when neither is known', () => {
|
||||
expect(formatAlert(node(0xabc, 9, undefined, 'KK'))).toBe(
|
||||
'⚠️ @!00000abc battery low: 9% (KK)',
|
||||
expect(formatAlert([node(0xabc, 9, undefined, 'KK')])).toBe(
|
||||
'⚠️ battery low: @!00000abc (KK) 9%',
|
||||
);
|
||||
expect(formatAlert(node(0xabc, 9))).toBe('⚠️ @!00000abc battery low: 9%');
|
||||
expect(formatAlert([node(0xabc, 9)])).toBe('⚠️ battery low: @!00000abc 9%');
|
||||
});
|
||||
|
||||
it('stays under the ~190-char single-packet limit', () => {
|
||||
const msg = formatAlert(node(0xffffffff, 5, 'A'.repeat(64)));
|
||||
const msg = formatAlert([node(0xffffffff, 5, 'A'.repeat(64))]);
|
||||
expect(msg.length).toBeLessThan(190);
|
||||
});
|
||||
});
|
||||
@@ -73,16 +79,11 @@ describe('BatteryAlertService check threshold', () => {
|
||||
|
||||
await svc.check();
|
||||
|
||||
expect(sendText).toHaveBeenCalledTimes(2); // <= choice: exactly 20 alerts too
|
||||
expect(sendText).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
// <= choice: exactly 20 qualifies too — both in ONE batched message
|
||||
expect(sendText).toHaveBeenCalledTimes(1);
|
||||
expect(sendText).toHaveBeenCalledWith(
|
||||
BROADCAST_ADDR,
|
||||
'⚠️ @!00000001 battery low: 19%',
|
||||
);
|
||||
expect(sendText).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
BROADCAST_ADDR,
|
||||
'⚠️ @!00000002 battery low: 20%',
|
||||
'⚠️ battery low: @!00000001 19%, @!00000002 20%',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -97,7 +98,7 @@ describe('BatteryAlertService check threshold', () => {
|
||||
expect(sendText).toHaveBeenCalledTimes(1);
|
||||
expect(sendText).toHaveBeenCalledWith(
|
||||
BROADCAST_ADDR,
|
||||
'⚠️ @!00000001 battery low: 50%',
|
||||
'⚠️ battery low: @!00000001 50%',
|
||||
);
|
||||
} finally {
|
||||
delete process.env.BATTERY_ALERT_THRESHOLD;
|
||||
@@ -113,7 +114,7 @@ describe('BatteryAlertService check threshold', () => {
|
||||
expect(sendText).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('alerts once per qualifying node with the broadcast target', async () => {
|
||||
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(
|
||||
[node(0xabc, 5, 'A'), node(0xdef, 3, undefined, 'B')],
|
||||
@@ -122,14 +123,10 @@ describe('BatteryAlertService check threshold', () => {
|
||||
|
||||
await svc.check();
|
||||
|
||||
expect(sendText).toHaveBeenCalledTimes(2);
|
||||
expect(sendText).toHaveBeenCalledTimes(1); // never one message per node
|
||||
expect(sendText.mock.calls[0]).toEqual([
|
||||
BROADCAST_ADDR,
|
||||
'⚠️ @!00000abc battery low: 5% (A)',
|
||||
]);
|
||||
expect(sendText.mock.calls[1]).toEqual([
|
||||
BROADCAST_ADDR,
|
||||
'⚠️ @!00000def battery low: 3% (B)',
|
||||
'⚠️ battery low: @!00000abc (A) 5%, @!00000def (B) 3%',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -169,6 +166,34 @@ describe('BatteryAlertService check cooldown', () => {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('excludes a node inside its cooldown from the batch while another outside it still triggers one message', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const sendText = vi.fn(async (..._args: unknown[]) => 1);
|
||||
// only B is known at first — alert it, then A appears while B is still in cooldown
|
||||
const nodes = [node(0xdef, 3)];
|
||||
const svc = makeService(nodes, sendText);
|
||||
|
||||
await svc.check();
|
||||
expect(sendText).toHaveBeenCalledTimes(1);
|
||||
expect(sendText.mock.calls[0][1]).toBe('⚠️ battery low: @!00000def 3%');
|
||||
|
||||
nodes.push(node(0xabc, 5)); // A is due, B is inside its window
|
||||
await svc.check();
|
||||
expect(sendText).toHaveBeenCalledTimes(2); // one message — only A
|
||||
expect(sendText.mock.calls[1][1]).toBe('⚠️ battery low: @!00000abc 5%');
|
||||
|
||||
vi.advanceTimersByTime(6 * 3_600_000); // B's cooldown expires too
|
||||
await svc.check();
|
||||
expect(sendText).toHaveBeenCalledTimes(3); // both due again — one batched message with both
|
||||
expect(sendText.mock.calls[2][1]).toBe(
|
||||
'⚠️ battery low: @!00000def 3%, @!00000abc 5%',
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('BatteryAlertService check end-to-end (real MeshService)', () => {
|
||||
@@ -222,7 +247,7 @@ describe('BatteryAlertService check end-to-end (real MeshService)', () => {
|
||||
? pkt.payloadVariant.value
|
||||
: undefined;
|
||||
expect(new TextDecoder().decode(data?.payload ?? new Uint8Array())).toBe(
|
||||
'⚠️ @!00000abc battery low: 5% (Alpha)',
|
||||
'⚠️ battery low: @!00000abc (Alpha) 5%',
|
||||
);
|
||||
} finally {
|
||||
delete process.env.MESH_HOST;
|
||||
|
||||
Reference in New Issue
Block a user