diff --git a/config.schema.json b/config.schema.json index ca22c79..04b81c4 100644 --- a/config.schema.json +++ b/config.schema.json @@ -228,6 +228,11 @@ "maximum": 255, "default": 1, "description": "The unit/slave ID of the Modbus device. Defaults to 1." + }, + "pollingIntervalMs": { + "type": "number", + "description": "The minimum number of seconds between polling, subject to the latency of the polling loop.", + "default": 200 } }, "required": [ @@ -253,6 +258,9 @@ }, "unitId": { "$ref": "#/definitions/config/properties/inverters/items/anyOf/0/properties/unitId" + }, + "pollingIntervalMs": { + "$ref": "#/definitions/config/properties/inverters/items/anyOf/0/properties/pollingIntervalMs" } }, "required": [ @@ -280,10 +288,10 @@ "description": "How many seconds of inverter and site data to sample to make control decisions.\nA shorter time will increase responsiveness to load changes but may introduce oscillations.\nA longer time will smooth out load changes but may result in overshoot.", "default": 5 }, - "controlFrequencyMinimumSeconds": { + "intervalSeconds": { "type": "number", "minimum": 0, - "description": "The number of seconds between control commands", + "description": "The minimum number of seconds between control commands, subject to the latency of the control loop.", "default": 1 } }, @@ -313,6 +321,9 @@ }, "unitId": { "$ref": "#/definitions/config/properties/inverters/items/anyOf/0/properties/unitId" + }, + "pollingIntervalMs": { + "$ref": "#/definitions/config/properties/inverters/items/anyOf/0/properties/pollingIntervalMs" } }, "required": [ @@ -339,6 +350,9 @@ }, "unitId": { "$ref": "#/definitions/config/properties/inverters/items/anyOf/0/properties/unitId" + }, + "pollingIntervalMs": { + "$ref": "#/definitions/config/properties/inverters/items/anyOf/0/properties/pollingIntervalMs" } }, "required": [ @@ -369,6 +383,11 @@ "type": "number", "description": "Request timeout in seconds", "default": 2 + }, + "pollingIntervalMs": { + "type": "number", + "description": "The minimum number of seconds between polling, subject to the latency of the polling loop.", + "default": 200 } }, "required": [ @@ -401,6 +420,11 @@ "topic": { "type": "string", "description": "The topic to pull meter readings from" + }, + "pollingIntervalMs": { + "type": "number", + "description": "The minimum number of seconds between polling, subject to the latency of the polling loop.", + "default": 200 } }, "required": [ diff --git a/docs/configuration/inverter-control.md b/docs/configuration/inverter-control.md index 0583287..3b23c92 100644 --- a/docs/configuration/inverter-control.md +++ b/docs/configuration/inverter-control.md @@ -26,14 +26,14 @@ The inverter control loop will attempt to aggregate and average a window of meas ## Frequency -The inverter control loop will attempt to control the inverter with a rate limit to prevent over-correcting and leading to osscilations. The `config.json` option `inverterControl.controlFrequencyMinimumSeconds` can be used to adjust the frequency in seconds. +The inverter control loop will attempt to control the inverter with a rate limit to prevent over-correcting and leading to osscilations. The `config.json` option `inverterControl.intervalSeconds` can be used to adjust the frequency in seconds. The rate limit will be influenced by the latency of sending commands to the inverter (e.g. over WiFi) and the inverter's response time, so the frequency configuration is a minimum and not a guaranteed rate. ```js { "inverterControl": { - "controlFrequencyMinimumSeconds": 5 // (number) optional: the frequency in seconds to control the inverter + "intervalSeconds": 5 // (number) optional: the frequency in seconds to control the inverter }, ... } diff --git a/docs/configuration/inverters.md b/docs/configuration/inverters.md index f1990e5..1fde845 100644 --- a/docs/configuration/inverters.md +++ b/docs/configuration/inverters.md @@ -23,7 +23,8 @@ To configure a SMA inverter connection, add the following property to `config.js "ip": "192.168.1.6", // (string) required: the IP address of the inverter "port": 502 // (number) required: the Modbus TCP port of the inverter }, - "unitId": 1 // (number) required: the Modbus unit ID of the inverter + "unitId": 1 // (number) required: the Modbus unit ID of the inverter, + "pollingIntervalMs": // (number) optional: the polling interval in milliseconds, default 200 } ], ... @@ -50,7 +51,8 @@ To configure a SunSpec inverter connection over TCP, add the following property "ip": "192.168.1.6", // (string) required: the IP address of the inverter "port": 502 // (number) required: the Modbus TCP port of the inverter }, - "unitId": 1 // (number) required: the Modbus unit ID of the inverter + "unitId": 1 // (number) required: the Modbus unit ID of the inverter, + "pollingIntervalMs": // (number) optional: the polling interval in milliseconds, default 200 } ], ... diff --git a/docs/configuration/meter.md b/docs/configuration/meter.md index 73703ab..7a8b7ca 100644 --- a/docs/configuration/meter.md +++ b/docs/configuration/meter.md @@ -18,6 +18,7 @@ To configure a MQTT meter connection, add the following property to `config.json "username": "user", // (string) optional: the MQTT broker username "password": "password", // (string) optional: the MQTT broker password "topic": "site" // (string) required: the MQTT topic to read + "pollingIntervalMs": // (number) optional: the polling interval in milliseconds, default 200 } ... } @@ -147,7 +148,8 @@ To configure a SMA inverter with meter connection, add the following property to "port": 502 // (number) required: the Modbus TCP port of the inverter }, "unitId": 240 // (number) required: the SunSpec unit ID of the meter - "location": "feedin" // (string) optional: the location of the meter (feedin or consumption) + "location": "feedin" // (string) optional: the location of the meter (feedin or consumption), + "pollingIntervalMs": // (number) optional: the polling interval in milliseconds, default 200 } ... } @@ -175,7 +177,8 @@ To configure a SunSpec meter connection over TCP, add the following property to "port": 502 // (number) required: the Modbus TCP port of the inverter }, "unitId": 240 // (number) required: the SunSpec unit ID of the meter - "location": "feedin" // (string) optional: the location of the meter (feedin or consumption) + "location": "feedin" // (string) optional: the location of the meter (feedin or consumption), + "pollingIntervalMs": // (number) optional: the polling interval in milliseconds, default 200 } ... } @@ -208,7 +211,8 @@ Sites with a Tesla Powerwall 2 can use the Backup Gateway 2's meter and local AP "meter": { "type": "powerwall2", // (string) required: the type of meter "ip": "192.168.1.68", // (string) required: the IP address of the Powerwall 2 Gateway - "password": "ABCDE" // (string) required: the password to access the Powerwall 2 API (the last 5 characters of the password sticker) + "password": "ABCDE" // (string) required: the password to access the Powerwall 2 API (the last 5 characters of the password sticker), + "pollingIntervalMs": // (number) optional: the polling interval in milliseconds, default 200 } ... ``` \ No newline at end of file diff --git a/src/coordinator/helpers/inverterController.ts b/src/coordinator/helpers/inverterController.ts index b58c38b..b8b6be4 100644 --- a/src/coordinator/helpers/inverterController.ts +++ b/src/coordinator/helpers/inverterController.ts @@ -94,7 +94,7 @@ export class InverterController { // we don't want to rely on the latest readings to make decisions since it will lead to oscillating control values // we take a time weighted average of the last few seconds to smooth out the control values private secondsToSample: number; - private controlFrequencyMinimumSeconds: number; + private intervalSeconds: number; constructor({ config, @@ -109,8 +109,7 @@ export class InverterController { }) { this.publish = new Publish({ config }); this.secondsToSample = config.inverterControl.sampleSeconds; - this.controlFrequencyMinimumSeconds = - config.inverterControl.controlFrequencyMinimumSeconds; + this.intervalSeconds = config.inverterControl.intervalSeconds; this.limiters = limiters; this.logger = pinoLogger.child({ module: 'InverterController' }); this.onControl = onControl; @@ -234,10 +233,7 @@ export class InverterController { writeLatency({ field: 'applyControlLoop', duration }); - const delay = Math.max( - this.controlFrequencyMinimumSeconds * 1000 - duration, - 0, - ); + const delay = Math.max(this.intervalSeconds * 1000 - duration, 0); setTimeout(() => { void this.applyControlLoop(); diff --git a/src/helpers/config.ts b/src/helpers/config.ts index fd80655..9d1fc94 100644 --- a/src/helpers/config.ts +++ b/src/helpers/config.ts @@ -32,6 +32,13 @@ const modbusSchema = z.object({ .max(255) .default(1) .describe('The unit/slave ID of the Modbus device. Defaults to 1.'), + pollingIntervalMs: z + .number() + .optional() + .describe( + 'The minimum number of seconds between polling, subject to the latency of the polling loop.', + ) + .default(200), }); export type ModbusSchema = z.infer; @@ -171,10 +178,12 @@ A longer time will smooth out load changes but may result in overshoot.`, ) .optional() .default(5), - controlFrequencyMinimumSeconds: z + intervalSeconds: z .number() .min(0) - .describe(`The number of seconds between control commands`) + .describe( + `The minimum number of seconds between control commands, subject to the latency of the control loop.`, + ) .optional() .default(1), }), @@ -213,6 +222,13 @@ A longer time will smooth out load changes but may result in overshoot.`, .optional() .describe('Request timeout in seconds') .default(2), + pollingIntervalMs: z + .number() + .optional() + .describe( + 'The minimum number of seconds between polling, subject to the latency of the polling loop.', + ) + .default(200), }) .describe('Powerwall 2 meter configuration'), z @@ -234,6 +250,13 @@ A longer time will smooth out load changes but may result in overshoot.`, topic: z .string() .describe('The topic to pull meter readings from'), + pollingIntervalMs: z + .number() + .optional() + .describe( + 'The minimum number of seconds between polling, subject to the latency of the polling loop.', + ) + .default(200), }) .describe('MQTT meter configuration'), ]), diff --git a/src/inverter/sma/index.ts b/src/inverter/sma/index.ts index 913e66e..a87cf88 100644 --- a/src/inverter/sma/index.ts +++ b/src/inverter/sma/index.ts @@ -40,7 +40,7 @@ export class SmaInverterDataPoller extends InverterDataPollerBase { }) { super({ name: 'SmaInverterDataPoller', - pollingIntervalMs: 200, + pollingIntervalMs: smaInverterConfig.pollingIntervalMs, applyControl, inverterIndex, }); diff --git a/src/inverter/sunspec/index.ts b/src/inverter/sunspec/index.ts index 1d62c5b..c23674d 100644 --- a/src/inverter/sunspec/index.ts +++ b/src/inverter/sunspec/index.ts @@ -48,7 +48,7 @@ export class SunSpecInverterDataPoller extends InverterDataPollerBase { }) { super({ name: 'SunSpecInverterDataPoller', - pollingIntervalMs: 200, + pollingIntervalMs: sunspecInverterConfig.pollingIntervalMs, applyControl, inverterIndex, }); diff --git a/src/meters/mqtt/index.ts b/src/meters/mqtt/index.ts index 055eb13..8ae90e2 100644 --- a/src/meters/mqtt/index.ts +++ b/src/meters/mqtt/index.ts @@ -15,7 +15,7 @@ export class MqttSiteSamplePoller extends SiteSamplePollerBase { }) { super({ name: 'mqtt', - pollingIntervalMs: 200, + pollingIntervalMs: mqttConfig.pollingIntervalMs, }); this.client = mqtt.connect(mqttConfig.host, { diff --git a/src/meters/powerwall2/index.ts b/src/meters/powerwall2/index.ts index 148c03b..ff7cd68 100644 --- a/src/meters/powerwall2/index.ts +++ b/src/meters/powerwall2/index.ts @@ -16,7 +16,7 @@ export class Powerwall2SiteSamplePoller extends SiteSamplePollerBase { }) { super({ name: 'powerwall2', - pollingIntervalMs: 200, + pollingIntervalMs: powerwall2Config.pollingIntervalMs, }); this.client = getPowerwall2Client({ diff --git a/src/meters/sma/index.ts b/src/meters/sma/index.ts index 6339491..65e8c33 100644 --- a/src/meters/sma/index.ts +++ b/src/meters/sma/index.ts @@ -11,7 +11,10 @@ export class SmaMeterSiteSamplePoller extends SiteSamplePollerBase { private model: SmaMeterConfig['model']; constructor({ smaMeterConfig }: { smaMeterConfig: SmaMeterConfig }) { - super({ name: 'sma', pollingIntervalMs: 200 }); + super({ + name: 'sma', + pollingIntervalMs: smaMeterConfig.pollingIntervalMs, + }); this.smaConnection = new SmaConnection(smaMeterConfig); this.model = smaMeterConfig.model; diff --git a/src/meters/sunspec/index.ts b/src/meters/sunspec/index.ts index f81ea73..9581c29 100644 --- a/src/meters/sunspec/index.ts +++ b/src/meters/sunspec/index.ts @@ -24,7 +24,10 @@ export class SunSpecMeterSiteSamplePoller extends SiteSamplePollerBase { sunspecMeterConfig: SunSpecMeterConfig; invertersPoller: InvertersPoller; }) { - super({ name: 'sunspec', pollingIntervalMs: 200 }); + super({ + name: 'sunspec', + pollingIntervalMs: sunspecMeterConfig.pollingIntervalMs, + }); this.meterConnection = new MeterSunSpecConnection(sunspecMeterConfig); this.location = sunspecMeterConfig.location;