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..b9a8a52 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 not paired with Teslemetry, except dispatch, resume and island mode when this node has a Local gateway for the site, which signs those itself. 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/localCommands.ts b/packages/node-red-contrib-teslemetry/src/localCommands.ts new file mode 100644 index 0000000..af11f71 --- /dev/null +++ b/packages/node-red-contrib-teslemetry/src/localCommands.ts @@ -0,0 +1,29 @@ +import type { SiteCommands } from "./localPowerwall"; + +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) }; +} + +/** The API route names a gateway takes. */ +export const LOCAL_COMMANDS = new Set(Object.keys(LOCAL_ARGS)); 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..27a056e 100644 --- a/packages/node-red-contrib-teslemetry/src/nodes/teslemetry-config.ts +++ b/packages/node-red-contrib-teslemetry/src/nodes/teslemetry-config.ts @@ -2,10 +2,12 @@ import { Node, NodeAPI, NodeDef } from "node-red"; import { Teslemetry, TeslemetryStreamOptions, + ENERGY_COMMANDS, useTeslaModel, VEHICLE_COMMANDS, } from "@teslemetry/api"; import { createLogger, instances, getErrorMessage, Instance } from "../shared"; +import { LOCAL_COMMANDS } from "../localCommands"; import pkg from "../../package.json" with { type: "json" }; const CLIENT = `${pkg.name}/${pkg.version}`; @@ -214,7 +216,17 @@ 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).map((spec) => ({ + ...spec, + local: LOCAL_COMMANDS.has(spec.name), + })), + ); + 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..670587b 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 +155,10 @@ +
+