From 1391a8d9ba2e8fbe83a9627b8b20615aceaf754c Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 30 Sep 2026 18:51:21 +1000 Subject: [PATCH] fix(homebridge): stop the Grid Charging switch rewriting the export rule --- .changeset/grid-charging-export-rule.md | 5 +++ .../src/energy-services/grid-charging.ts | 18 ++------ .../test/energy-services.test.ts | 41 ++++++++++++------- 3 files changed, 36 insertions(+), 28 deletions(-) create mode 100644 .changeset/grid-charging-export-rule.md diff --git a/.changeset/grid-charging-export-rule.md b/.changeset/grid-charging-export-rule.md new file mode 100644 index 0000000..7a4d2b2 --- /dev/null +++ b/.changeset/grid-charging-export-rule.md @@ -0,0 +1,5 @@ +--- +"homebridge-teslemetry": patch +--- + +Stop the Powerwall "Grid Charging" switch from rewriting the site's export rule. Toggling it used to send an export rule alongside the grid charging flag, silently switching sites set to export (`battery_ok` or `pv_only`) to `never` and inventing a rule for sites with none; it now sends only the grid charging flag. diff --git a/packages/homebridge-teslemetry/src/energy-services/grid-charging.ts b/packages/homebridge-teslemetry/src/energy-services/grid-charging.ts index 8f33e92..c22be62 100644 --- a/packages/homebridge-teslemetry/src/energy-services/grid-charging.ts +++ b/packages/homebridge-teslemetry/src/energy-services/grid-charging.ts @@ -12,8 +12,6 @@ import { BaseEnergyService } from "./base.js"; * Represents grid charging permission as a switch */ export class GridChargingService extends BaseEnergyService { - private currentExportSetting: "battery_ok" | "never" | "pv_only" = "battery_ok"; - constructor( platform: import("../platform.js").TeslemetryPlatform, accessory: import("homebridge").PlatformAccessory, @@ -40,15 +38,6 @@ export class GridChargingService extends BaseEnergyService { this.platform.Characteristic.On, isAllowed, ); - - // Store current export setting for API calls - if (components.customer_preferred_export_rule !== undefined || - components.non_export_configured !== undefined) { - this.currentExportSetting = - (components.customer_preferred_export_rule ?? components.non_export_configured) - ? "never" - : "battery_ok"; - } } }); @@ -62,10 +51,11 @@ export class GridChargingService extends BaseEnergyService { `${allowed ? "Enabling" : "Disabling"} grid charging for ${site.name}`, ); - // The API requires both export and import (disallow charging) settings - // We maintain the current export setting and toggle the charging setting + // Send only the grid charging flag: the export rule is a separate + // setting this switch must never rewrite. The cast is needed because + // the SDK signature still makes the rule mandatory; the API does not. await this.command(site.api.gridImportExport( - this.currentExportSetting, + undefined as never, !allowed, // disallow_charge_from_grid )); }, diff --git a/packages/homebridge-teslemetry/test/energy-services.test.ts b/packages/homebridge-teslemetry/test/energy-services.test.ts index 0766afd..4b921bf 100644 --- a/packages/homebridge-teslemetry/test/energy-services.test.ts +++ b/packages/homebridge-teslemetry/test/energy-services.test.ts @@ -33,17 +33,30 @@ test("OperationModeService calls setOperationMode for a settable mode", async () assert.deepEqual(api.calls, [{ method: "setOperationMode", args: ["autonomous"] }]); }); -test("GridChargingService.currentExportSetting stays within the literal union accepted by gridImportExport", async () => { - const { platform } = createFakePlatform(); - const accessory = createFakeAccessory("grid-charging-site"); - const { site, api } = createFakeEnergySite(); - - new GridChargingService(platform, accessory, site); - - const characteristic = accessory.getService(Service.Switch)!.getCharacteristic(Characteristic.On); - await characteristic.handleSetRequest(true as never); - - assert.equal(api.calls.length, 1); - assert.equal(api.calls[0].method, "gridImportExport"); - assert.equal(api.calls[0].args[0], "battery_ok"); -}); +// Regression (HB01): the switch used to send an export rule derived from a +// truthiness check on site info, silently flipping exporting sites to "never" +// and inventing "battery_ok" for a site that reports no rule. +for (const [shape, components] of [ + ["battery_ok", { customer_preferred_export_rule: "battery_ok" }], + ["pv_only", { customer_preferred_export_rule: "pv_only" }], + ["never", { customer_preferred_export_rule: "never" }], + ["no rule", {}], +] as const) { + test(`GridChargingService sends only the grid charging flag, never an export rule (site: ${shape})`, async () => { + const { platform } = createFakePlatform(); + const accessory = createFakeAccessory(`grid-charging-site-${shape}`); + const { site, api } = createFakeEnergySite(); + + new GridChargingService(platform, accessory, site); + api.emit("siteInfo", { response: { components } }); + + const characteristic = accessory.getService(Service.Switch)!.getCharacteristic(Characteristic.On); + await characteristic.handleSetRequest(false as never); + await characteristic.handleSetRequest(true as never); + + assert.deepEqual(api.calls, [ + { method: "gridImportExport", args: [undefined, true] }, + { method: "gridImportExport", args: [undefined, false] }, + ]); + }); +}