From 2d7e5e3f2ed7d49a36b1b3ee5dd6afc42d84fba2 Mon Sep 17 00:00:00 2001 From: "Brett Adams (Firstmate)" Date: Tue, 10 Nov 2026 22:03:18 +1000 Subject: [PATCH 1/4] feat(node-red): run and pick any API energy command, gated on paired from the catalog --- .../node-red-energy-catalog-commands.md | 5 + .../node-red-contrib-teslemetry/README.md | 7 +- .../src/localPowerwall.ts | 25 +++++ .../src/nodes/teslemetry-config.ts | 6 ++ .../src/nodes/teslemetry-energy-command.html | 96 ++++++++++++------- .../src/nodes/teslemetry-energy-command.ts | 67 ++++++++++--- .../test/teslemetry-config.test.ts | 5 + .../teslemetry-energy-command-local.test.ts | 66 ++++++++++++- 8 files changed, 226 insertions(+), 51 deletions(-) create mode 100644 .changeset/node-red-energy-catalog-commands.md diff --git a/.changeset/node-red-energy-catalog-commands.md b/.changeset/node-red-energy-catalog-commands.md new file mode 100644 index 0000000..9d615da --- /dev/null +++ b/.changeset/node-red-energy-catalog-commands.md @@ -0,0 +1,5 @@ +--- +"@teslemetry/node-red-contrib-teslemetry": minor +--- + +The Energy Command node lists every Teslemetry API energy site command, with a field for each argument, and runs any of them from `msg.command` by its route name with its arguments in `msg.payload`. This adds backup events, gateway config and diagnostics. Commands that need a key the gateway trusts are offered for a site paired with Teslemetry or with a Local gateway, and backup reserve, operation mode, grid export, dispatch and island mode go to the Local gateway first. Nodes saved with an earlier command name keep working unchanged. diff --git a/packages/node-red-contrib-teslemetry/README.md b/packages/node-red-contrib-teslemetry/README.md index 86fbf0c..64a4978 100644 --- a/packages/node-red-contrib-teslemetry/README.md +++ b/packages/node-red-contrib-teslemetry/README.md @@ -113,9 +113,12 @@ Send commands to a Tesla Energy Site or retrieve site status. **Configuration:** - **Site ID**: Select a site or leave empty to use `msg.siteId`. - **Local gateway**: Optional. A Teslemetry Local Powerwall config node. Backup reserve, operation mode, grid export, dispatch, resume, go off-grid and reconnect to grid then go to the gateway over your local network first, and through the cloud only if the gateway does not answer. The node status shows `local` or `cloud`. -- **Command**: Select a command, or choose **From msg.command** to take it from each message. +- **Command**: Select a command, or choose **From msg.command** to take it from each message. The list holds every energy site command in the Teslemetry API, grouped as the API groups them. Commands that need a key the gateway trusts (dispatch, backup events, island mode, gateway config and diagnostics) are greyed out for a site that is neither paired with Teslemetry nor has a Local gateway on this node. A node saved with an earlier command name keeps it, under **Earlier names (still supported)**. +- **Fields**: Each argument of the selected command, as a fixed value or read from `msg`, `flow`, `global`, an environment variable or JSONata. By default each reads `msg.payload.`. -A site or command selected in the node always wins; `msg.siteId` and `msg.command` are only read when the matching dropdown is set to its **From msg…** option. Command arguments are read from the top level of the message, not from `msg.payload`. +A site or command selected in the node always wins; `msg.siteId` and `msg.command` are only read when the matching dropdown is set to its **From msg…** option. Earlier command names (`setBackupReserve`, ...) read their arguments from the top level of the message, not from `msg.payload`. + +`msg.command` also takes any energy site command from the [Teslemetry API](https://teslemetry.com/docs) by its route name, such as `backup`, `storm_mode` or `schedule_backup_event`. These read their arguments from `msg.payload` using the API's field names (a field set in the node wins over `msg.payload`), for example `msg.payload = { "backup_reserve_percent": 40 }`. With a Local gateway, `backup`, `operation`, `grid_import_export`, `dispatch_battery_power`, `resume_battery_operation` and `set_island_mode` go to the gateway first, as the earlier names do. **Inputs:** - `msg.siteId` (number): Energy Site ID (when Site ID is **From msg.siteId**). diff --git a/packages/node-red-contrib-teslemetry/src/localPowerwall.ts b/packages/node-red-contrib-teslemetry/src/localPowerwall.ts index 4dba285..7a4303e 100644 --- a/packages/node-red-contrib-teslemetry/src/localPowerwall.ts +++ b/packages/node-red-contrib-teslemetry/src/localPowerwall.ts @@ -175,6 +175,31 @@ export interface SiteCommands { ): Promise<{ response?: unknown }>; } +type Body = Record; + +/** The gateway's positional arguments for an API command body, by API route name. */ +const LOCAL_ARGS: { [M in keyof SiteCommands]: (body: Body) => Parameters } = { + backup: (b) => [b.backup_reserve_percent], + operation: (b) => [b.default_real_mode], + grid_import_export: (b) => [ + b.disallow_charge_from_grid_with_solar_installed ?? null, + b.customer_preferred_export_rule, + ], + dispatch_battery_power: (b) => [b.power_watts, b.duration_seconds], + resume_battery_operation: () => [], + set_island_mode: (b) => [b.mode, b.force ?? null], +}; + +/** The gateway method and arguments for an API command, or undefined when the gateway does not take it. */ +export function localCommand( + name: string, + body: Body, +): { method: keyof SiteCommands; args: unknown[] } | undefined { + if (!Object.prototype.hasOwnProperty.call(LOCAL_ARGS, name)) return undefined; + const method = name as keyof SiteCommands; + return { method, args: LOCAL_ARGS[method](body) }; +} + export type LocalSite = PowerwallEnergySite; /** A gateway connection that lives as long as its config node. */ diff --git a/packages/node-red-contrib-teslemetry/src/nodes/teslemetry-config.ts b/packages/node-red-contrib-teslemetry/src/nodes/teslemetry-config.ts index 6a7bab0..e0b9b9b 100644 --- a/packages/node-red-contrib-teslemetry/src/nodes/teslemetry-config.ts +++ b/packages/node-red-contrib-teslemetry/src/nodes/teslemetry-config.ts @@ -2,6 +2,7 @@ import { Node, NodeAPI, NodeDef } from "node-red"; import { Teslemetry, TeslemetryStreamOptions, + ENERGY_COMMANDS, useTeslaModel, VEHICLE_COMMANDS, } from "@teslemetry/api"; @@ -214,7 +215,12 @@ export default function (RED: NodeAPI) { }); // The vehicle model comes from the VIN, so a VIN filters the list without a request. + // Energy commands are filtered in the editor, which knows the node's Local gateway. RED.httpAdmin.get("/teslemetry/commands", canRead, (req: any, res: any) => { + if (req.query.product === "energy") { + res.json(Object.values(ENERGY_COMMANDS)); + return; + } const vin = typeof req.query.vin === "string" ? req.query.vin : ""; const model = vin ? useTeslaModel(vin) : undefined; res.json( diff --git a/packages/node-red-contrib-teslemetry/src/nodes/teslemetry-energy-command.html b/packages/node-red-contrib-teslemetry/src/nodes/teslemetry-energy-command.html index 09bf47f..b49a19a 100644 --- a/packages/node-red-contrib-teslemetry/src/nodes/teslemetry-energy-command.html +++ b/packages/node-red-contrib-teslemetry/src/nodes/teslemetry-energy-command.html @@ -1,3 +1,7 @@ + @@ -135,9 +153,10 @@ +
+