diff --git a/docs/configuration/inverters.md b/docs/configuration/inverters.md index 1fde845..6c25100 100644 --- a/docs/configuration/inverters.md +++ b/docs/configuration/inverters.md @@ -72,3 +72,128 @@ For SunSpec over RTU, you need to modify the `connection` ### Fronius To enable SunSpec/Modbus on Fronius inverters, you'll need to access the inverter's local web UI and [enable the Modbus TCP option](https://github.com/longzheng/open-dynamic-export/wiki/Fronius-SunSpec-Modbus-configuration). + +## MQTT + +> [!WARNING] +> The MQTT inverter configuration does not support control. It is designed for systems which will monitor the API or "publish" active limit output to apply inverter control externally. + +A MQTT topic can be read to get the inveter measurements. + +To configure a MQTT inverter connection, add the following property to `config.json` + +```js +{ + "inverters": [ + { + "type": "mqtt", // (string) required: the type of inverter + "host": "mqtt://192.168.1.2", // (string) required: the MQTT broker host + "username": "user", // (string) optional: the MQTT broker username + "password": "password", // (string) optional: the MQTT broker password + "topic": "inverters/1" // (string) required: the MQTT topic to read + "pollingIntervalMs": // (number) optional: the polling interval in milliseconds, default 200 + } + ] + ... +} +``` + +The MQTT topic must contain a JSON message that meets the following schema + +```js +z.object({ + inverter: z.object({ + /** + * Positive values = inverter export (produce) power + * + * Negative values = inverter import (consume) power + * + * Value is total (net across all phases) measurement + */ + realPower: z.number(), + /** + * Positive values = inverter export (produce) power + * + * Negative values = inverter import (consume) power + * + * Value is total (net across all phases) measurement + */ + reactivePower: z.number(), + // Voltage of phase A (null if not available) + voltagePhaseA: z.number().nullable(), + // Voltage of phase B (null if not available) + voltagePhaseB: z.number().nullable(), + // Voltage of phase C (null if not available) + voltagePhaseC: z.number().nullable(), + frequency: z.number(), + }), + nameplate: z.object({ + /** + * Type of DER device Enumeration + * + * PV = 4, + * PV_STOR = 82, + */ + type: z.nativeEnum(DERTyp), + // Maximum active power output in W + maxW: z.number(), + // Maximum apparent power output in VA + maxVA: z.number(), + // Maximum reactive power output in var + maxVar: z.number(), + }), + settings: z.object({ + // Currently set active power output in W + maxW: z.number(), + // Currently set apparent power output in VA + maxVA: z.number().nullable(), + // Currently set reactive power output in var + maxVar: z.number().nullable(), + }), + status: z.object({ + // DER OperationalModeStatus value: + // 0 - Not applicable / Unknown + // 1 - Off + // 2 - Operational mode + // 3 - Test mode + operationalModeStatus: z.nativeEnum(OperationalModeStatusValue), + // DER ConnectStatus value (bitmap): + // 0 - Connected + // 1 - Available + // 2 - Operating + // 3 - Test + // 4 - Fault / Error + genConnectStatus: connectStatusValueSchema, + }), +}) +``` + +For example + +```json +{ + "inverter": { + "realPower": 4500, + "reactivePower": 1500, + "voltagePhaseA": 230.5, + "voltagePhaseB": null, + "voltagePhaseC": null, + "frequency": 50.1 + }, + "nameplate": { + "type": 4, + "maxW": 5000, + "maxVA": 5000, + "maxVar": 5000 + }, + "settings": { + "maxW": 5000, + "maxVA": 5000, + "maxVar": 5000 + }, + "status": { + "operationalModeStatus": 2, + "genConnectStatus": 7 + } +} +``` \ No newline at end of file diff --git a/src/inverter/inverterData.ts b/src/inverter/inverterData.ts index b8f0440..94c34b2 100644 --- a/src/inverter/inverterData.ts +++ b/src/inverter/inverterData.ts @@ -6,6 +6,13 @@ import { type SampleBase } from '../coordinator/helpers/sampleBase.js'; export const inverterDataSchema = z.object({ inverter: z.object({ + /** + * Positive values = inverter export (produce) power + * + * Negative values = inverter import (consume) power + * + * Value is total (net across all phases) measurement + */ realPower: z.number(), reactivePower: z.number(), voltagePhaseA: z.number().nullable(), diff --git a/src/meters/siteSample.ts b/src/meters/siteSample.ts index 72f8184..57883b5 100644 --- a/src/meters/siteSample.ts +++ b/src/meters/siteSample.ts @@ -9,9 +9,9 @@ import { z } from 'zod'; // aligns with the CSIP-AUS requirements for site sample export const siteSampleDataSchema = z.object({ /** - * Positive values = site import power + * Positive values = site import (consume) power * - * Negative values = site export power + * Negative values = site export (produce) power */ realPower: z.union([ perPhaseNetMeasurementSchema, diff --git a/src/sep2/models/operationModeStatus.ts b/src/sep2/models/operationModeStatus.ts index 41e0d37..3223753 100644 --- a/src/sep2/models/operationModeStatus.ts +++ b/src/sep2/models/operationModeStatus.ts @@ -1,11 +1,11 @@ import { z } from 'zod'; -/// DER OperationalModeStatus value: -/// 0 - Not applicable / Unknown -/// 1 - Off -/// 2 - Operational mode -/// 3 - Test mode -/// All other values reserved. +// DER OperationalModeStatus value: +// 0 - Not applicable / Unknown +// 1 - Off +// 2 - Operational mode +// 3 - Test mode +// All other values reserved. export enum OperationalModeStatusValue { NotApplicable = 0, Off = 1,