From f066b03aff2a95c1f755c4de7b69d478550366d9 Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:44:17 +0200 Subject: [PATCH] fix(smarthome): count GC gateway rules by name prefix, not DB column Cancel/reset chains create secondary #reset/#cancel rules whose ids are not stored in smarthome_rules.deconz_rule_id, so gateway-count attributed them to external_rules. Count GC-owned rules by the GC: name prefix over the live getRules() response instead. --- CHANGELOG.md | 7 +++++++ src/services/smarthome/smarthomeRules.js | 8 ++++++-- tests/smarthome_rules_service.test.js | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc90d4f6..a18276aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [Unreleased] + +### Fixed +- Smart Home: `GET /rules/gateway-count` no longer miscounts GateControl's own secondary rules (`#reset`/`#cancel` chains) as external — GC-owned rules are now counted by the `GC:` name prefix over the live gateway response instead of the DB `deconz_rule_id` column + +--- + ## [1.113.0] — 2026-07-01 ### Features diff --git a/src/services/smarthome/smarthomeRules.js b/src/services/smarthome/smarthomeRules.js index 9100e6ce..ba690345 100644 --- a/src/services/smarthome/smarthomeRules.js +++ b/src/services/smarthome/smarthomeRules.js @@ -162,8 +162,12 @@ async function setEnabled(id, on) { } async function gatewayRuleCount(gatewayId) { - const total = Object.keys(await clientFactory(gatewayId).getRules() || {}).length; - const gc = getDb().prepare('SELECT COUNT(*) c FROM smarthome_rules WHERE gateway_id = ? AND deconz_rule_id IS NOT NULL').get(gatewayId).c; + const rules = await clientFactory(gatewayId).getRules() || {}; + const total = Object.keys(rules).length; + // GC-owned rules carry the "GC:" name prefix (primary + #reset/#cancel secondary rules). + // Counting by prefix over the live response — not the DB deconz_rule_id column, which only holds + // the primary rule id — so cancel/reset chains are attributed to gc, not miscounted as external. + const gc = Object.values(rules).filter((r) => r && typeof r.name === 'string' && r.name.startsWith('GC:')).length; return { total_rules: total, gc_rules: gc, external_rules: Math.max(0, total - gc) }; } diff --git a/tests/smarthome_rules_service.test.js b/tests/smarthome_rules_service.test.js index cd6f2531..11bb757d 100644 --- a/tests/smarthome_rules_service.test.js +++ b/tests/smarthome_rules_service.test.js @@ -88,3 +88,20 @@ test('DECONZ_RULE_LIMIT_REACHED mapped from 503/507', async () => { const def = { triggers: [{ kind: 'motion', resourceId: motion, event: 'detected' }], actions: [{ kind: 'group', resourceId: group, set: { on: true } }] }; await assert.rejects(() => rules.create(gw.id, 'L', def), (e) => e.code === 'DECONZ_RULE_LIMIT_REACHED'); }); + +test('gatewayRuleCount attributes GC-named rules (incl #reset/#cancel) to gc, not external', async () => { + rules._setClientFactoryForTest(() => ({ + getRules: async () => ({ + '1': { name: 'GC:5:Flur' }, // primary GC rule + '2': { name: 'GC:5:Flur#reset' }, // secondary GC rule (reset chain) + '3': { name: 'GC:8:Bad#cancel' }, // secondary GC rule (cancel chain) + '4': { name: 'pir-fsm-reset' }, // external (Phoscon-created) + '5': { name: 'my hue rule' }, // external + }), + })); + const { gw } = mkGatewayAndMotionGroup(); + const c = await rules.gatewayRuleCount(gw.id); + assert.equal(c.total_rules, 5); + assert.equal(c.gc_rules, 3); // GC: prefix, incl #reset/#cancel — not the DB deconz_rule_id column + assert.equal(c.external_rules, 2); // only the two genuinely external rules +});