diff --git a/README.md b/README.md index 4ba9c6d..0e40001 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ npm i -g homebridge-fronius-inverter-lights@latest "platform": "FroniusInverterLightsPlatform", "inverterIp": "192.168.1.124", "pollInterval": 2, - "pvMaxPower": 6000, "battery": false }, @@ -39,7 +38,6 @@ npm i -g homebridge-fronius-inverter-lights@latest - `platform` (required) the name of the plugin, must be `FroniusInverterLightsPlatform` - `inverterIp` (required) the IP address of your Fronius inverter - `pollInterval` (required) the polling frequency in seconds -- `pvMaxPower` (optional) the max capacity of your PV in watts (to show the PV lightbulb brightness % as a percentage of your max capacity) - `battery` (optional) enable battery accessory to show your battery SOC and usage ## Enable Solar API on newer Fronius inverters diff --git a/config.schema.json b/config.schema.json index 82f3bde..fe77b6b 100644 --- a/config.schema.json +++ b/config.schema.json @@ -17,13 +17,6 @@ "required": true, "default": 10 }, - "pvMaxPower": { - "title": "PV max power capacity (watts)", - "description": "If configured, shows the PV lightbulb brightness % as a proportion to the max PV capacity", - "type": "integer", - "required": false, - "placeholder": "6000" - }, "battery": { "title": "Show battery accessories", "type": "boolean", diff --git a/package-lock.json b/package-lock.json index a6d5b99..d84a8d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-fronius-inverter-lights", - "version": "1.6.2", + "version": "1.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "homebridge-fronius-inverter-lights", - "version": "1.6.2", + "version": "1.7.0", "license": "ISC", "dependencies": { "axios": "^0.21.4", diff --git a/package.json b/package.json index 3b172c6..4253899 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homebridge-fronius-inverter-lights", - "version": "1.6.2", + "version": "1.7.0", "description": "Homebridge plugin for Fronius inverter with smart meter as a lightbulb accessory", "main": "dist/fronius-platform.js", "scripts": { diff --git a/src/config.d.ts b/src/config.d.ts index d8c568c..ef71b27 100644 --- a/src/config.d.ts +++ b/src/config.d.ts @@ -1,6 +1,5 @@ export type Config = { inverterIp: string; pollInterval: number; - pvMaxPower?: number; battery?: boolean; }; diff --git a/src/fronius-accessory.ts b/src/fronius-accessory.ts index 3c24c2a..3e60143 100644 --- a/src/fronius-accessory.ts +++ b/src/fronius-accessory.ts @@ -26,14 +26,25 @@ export class FroniusAccessory implements AccessoryPlugin { private brightnessValue: number | Error = 0; private luxValue: number | Error = 0; - constructor( - hap: HAP, - log: Logging, - metering: Metering, - froniusApi: FroniusApi, - pollInterval: number, - pvMaxPower?: number, - ) { + constructor({ + hap, + log, + metering, + froniusApi, + pollInterval, + pvMaxPower, + model, + serialNumber, + }: { + hap: HAP; + log: Logging; + metering: Metering; + froniusApi: FroniusApi; + pollInterval: number; + pvMaxPower?: number; + model?: string; + serialNumber?: string; + }) { this.hap = hap; this.log = log; this.name = metering.toString(); @@ -92,9 +103,25 @@ export class FroniusAccessory implements AccessoryPlugin { return this.luxValue; }); - this.informationService = new hap.Service.AccessoryInformation() - .setCharacteristic(hap.Characteristic.Manufacturer, 'Fronius') - .setCharacteristic(hap.Characteristic.Model, 'Inverter'); + this.informationService = + new hap.Service.AccessoryInformation().setCharacteristic( + hap.Characteristic.Manufacturer, + 'Fronius', + ); + + if (model) { + this.informationService.setCharacteristic( + hap.Characteristic.Model, + model, + ); + } + + if (serialNumber) { + this.informationService.setCharacteristic( + hap.Characteristic.SerialNumber, + serialNumber, + ); + } setInterval(async () => { await this.scheduledUpdate(); @@ -117,87 +144,84 @@ export class FroniusAccessory implements AccessoryPlugin { } async updateValues() { - const data = await this.froniusApi.getInverterData(); - - if (data) { - // P_Akku should be positive when discharging and negative when charging - const batteryValue = data.Site.P_Akku ?? 0; - const batteryState = - batteryValue < 0 - ? 'charging' - : batteryValue > 0 - ? 'discharging' - : 'idle'; - - switch (this.metering) { - case 'Export': - case 'Import': { - const gridValue = data.Site.P_Grid; - const autonomyValue = data.Site.rel_Autonomy; - const selfConsumptionValue = data.Site.rel_SelfConsumption || 100; - const isImport = this.metering === 'Import'; - - this.onValue = - // on/off is calculated whether autonomy/selfConsumption is less than 100 - (isImport ? autonomyValue : selfConsumptionValue) < 100; - this.brightnessValue = - // percentage of import/export is calculated from 100 - autonomy/selfConsumption - 100 - (isImport ? autonomyValue : selfConsumptionValue); - this.luxValue = isImport - ? gridValue > 0 - ? gridValue - : 0 // import watts, value must be positive - : gridValue < 0 - ? -gridValue - : 0; // export watts, value must be negative - break; - } - case 'Load': { - const loadValue = Math.abs(data.Site.P_Load); - this.brightnessValue = 100; - this.onValue = loadValue > 0; - this.luxValue = loadValue; - break; - } - case 'PV': { - const pvValue = data.Site.P_PV; - this.brightnessValue = this.pvMaxPower - ? Math.min( - ((pvValue ?? 0) / this.pvMaxPower) * 100, // calculate PV output as a percentage of PV max power - 100, - ) // cap to 100% - : 100; - this.onValue = pvValue !== null; - this.luxValue = pvValue ?? 0; - break; - } - case 'Battery %': { - // if the site has multiple inverters, average all the inverter SOCs - const socs = Object.values(data.Inverters).map((inv) => inv.SOC ?? 0); - const socAvg = socs.reduce((a, b) => a + b, 0) / socs.length; - this.brightnessValue = socAvg; - this.onValue = socAvg > 0; - break; - } - case 'Battery charging': { - const isCharging = batteryState === 'charging'; - this.brightnessValue = isCharging ? 100 : 0; - this.onValue = isCharging; - this.luxValue = Math.abs(batteryValue); - break; - } - case 'Battery discharging': { - const isDischarging = batteryState === 'discharging'; - this.brightnessValue = isDischarging ? 100 : 0; - this.onValue = isDischarging; - this.luxValue = Math.abs(batteryValue); - break; - } - } - } else { + const data = (await this.froniusApi.getPowerFlowRealtimeData())?.Body.Data; + + if (!data) { this.onValue = new Error('Error fetching value'); this.brightnessValue = new Error('Error fetching value'); this.luxValue = new Error('Error fetching value'); + return; + } + + // P_Akku should be positive when discharging and negative when charging + const batteryValue = data.Site.P_Akku ?? 0; + const batteryState = + batteryValue < 0 ? 'charging' : batteryValue > 0 ? 'discharging' : 'idle'; + + switch (this.metering) { + case 'Export': + case 'Import': { + const gridValue = data.Site.P_Grid; + const autonomyValue = data.Site.rel_Autonomy; + const selfConsumptionValue = data.Site.rel_SelfConsumption || 100; + const isImport = this.metering === 'Import'; + + this.onValue = + // on/off is calculated whether autonomy/selfConsumption is less than 100 + (isImport ? autonomyValue : selfConsumptionValue) < 100; + this.brightnessValue = + // percentage of import/export is calculated from 100 - autonomy/selfConsumption + 100 - (isImport ? autonomyValue : selfConsumptionValue); + this.luxValue = isImport + ? gridValue > 0 + ? gridValue + : 0 // import watts, value must be positive + : gridValue < 0 + ? -gridValue + : 0; // export watts, value must be negative + break; + } + case 'Load': { + const loadValue = Math.abs(data.Site.P_Load); + this.brightnessValue = 100; + this.onValue = loadValue > 0; + this.luxValue = loadValue; + break; + } + case 'PV': { + const pvValue = data.Site.P_PV; + this.brightnessValue = this.pvMaxPower + ? Math.min( + ((pvValue ?? 0) / this.pvMaxPower) * 100, // calculate PV output as a percentage of PV max power + 100, + ) // cap to 100% + : 100; + this.onValue = pvValue !== null; + this.luxValue = pvValue ?? 0; + break; + } + case 'Battery %': { + // if the site has multiple inverters, average all the inverter SOCs + const socs = Object.values(data.Inverters).map((inv) => inv.SOC ?? 0); + const socAvg = socs.reduce((a, b) => a + b, 0) / socs.length; + this.brightnessValue = socAvg; + this.onValue = socAvg > 0; + break; + } + case 'Battery charging': { + const isCharging = batteryState === 'charging'; + this.brightnessValue = isCharging ? 100 : 0; + this.onValue = isCharging; + this.luxValue = Math.abs(batteryValue); + break; + } + case 'Battery discharging': { + const isDischarging = batteryState === 'discharging'; + this.brightnessValue = isDischarging ? 100 : 0; + this.onValue = isDischarging; + this.luxValue = Math.abs(batteryValue); + break; + } } } diff --git a/src/fronius-api.ts b/src/fronius-api.ts index ebe08e0..13103f7 100644 --- a/src/fronius-api.ts +++ b/src/fronius-api.ts @@ -6,7 +6,7 @@ export class FroniusApi { private readonly http: AxiosInstance; private readonly inverterIp: string; private readonly log: Logging; - private request: Promise | undefined; // cache the current request to prevent concurrent requests + private requestQueue = new Map>(); constructor(inverterIp: string, log: Logging) { this.inverterIp = inverterIp; @@ -23,83 +23,114 @@ export class FroniusApi { }); } - public getInverterData = async () => { + private requestWithDedup = async (url: string): Promise => { + const existingRequest = this.requestQueue.get(url) as Promise; + // if request is already in operation, return the previous request - if (this.request) { - return this.request; + if (existingRequest) { + return existingRequest; } - this.request = new Promise((resolve) => { - const url = `http://${this.inverterIp}/solar_api/v1/GetPowerFlowRealtimeData.fcgi`; - - this.log.debug(`Getting inverter data: ${url}`); + const request = new Promise((resolve) => { + this.log.debug(`Making request: ${url}`); this.http - .get(url) + .get(url) .then((response) => { - // clear existing request - this.request = undefined; - - if (response.status === 200) { - return resolve(response.data.Body.Data); - } else { - this.log.error(`Received invalid status code: ${response.status}`); + if (response.status !== 200) { + this.log.error( + `${url}: received invalid status code: ${response.status}`, + ); return resolve(null); } + + return resolve(response.data); }) .catch((error: Error) => { this.log.error(error.message); - this.request = undefined; return resolve(null); + }) + .finally(() => { + this.requestQueue.delete(url); }); }); - return this.request; + this.requestQueue.set(url, request); + + return request; }; -} -export interface FroniusRealtimeData { - Body: Body; - Head: Head; -} -export interface Body { - Data: Data; -} -export interface Data { - Inverters: Record; - Site: Site; - Version: string; -} -export interface Inverter { - DT: number; - E_Day: number; - E_Total: number; - E_Year: number; - P: number; - SOC?: number; // percentage load of Battery/Akku -} -export interface Site { - E_Day: number; - E_Total: number; - E_Year: number; - Meter_Location: string; - Mode: string; - P_Akku: number | null; - P_Grid: number; - P_Load: number; - P_PV: number | null; - rel_Autonomy: number; - rel_SelfConsumption: number | null; + public getPowerFlowRealtimeData = async () => { + const url = `http://${this.inverterIp}/solar_api/v1/GetPowerFlowRealtimeData.fcgi`; + + return this.requestWithDedup(url); + }; + + public getInverterInfo = async () => { + const url = `http://${this.inverterIp}/solar_api/v1/GetInverterInfo.cgi`; + + return this.requestWithDedup(url); + }; } -export interface Head { +type PowerFlowRealtimeData = { + Body: { + Data: { + Inverters: Record< + string, + { + DT: number; + E_Day: number; + E_Total: number; + E_Year: number; + P: number; + SOC?: number; + } + >; + Site: { + E_Day: number; + E_Total: number; + E_Year: number; + Meter_Location: string; + Mode: string; + P_Akku: number | null; + P_Grid: number; + P_Load: number; + P_PV: number | null; + rel_Autonomy: number; + rel_SelfConsumption: number | null; + }; + Version: string; + }; + }; + Head: ResponseHead; +}; + +type InverterInfo = { + Body: { + Data: Record< + string, + { + CustomName: string; + DT: number; + ErrorCode: number; + PVPower: number; + Show: number; + StatusCode: number; + UniqueID: string; + } + >; + }; + Head: ResponseHead; +}; + +type ResponseHead = { RequestArguments: Record; - Status: Status; + Status: { + Code: number; + Reason: string; + UserMessage: string; + }; Timestamp: string; -} -export interface Status { - Code: number; - Reason: string; - UserMessage: string; -} +}; \ No newline at end of file diff --git a/src/fronius-deviceType.ts b/src/fronius-deviceType.ts new file mode 100644 index 0000000..a244191 --- /dev/null +++ b/src/fronius-deviceType.ts @@ -0,0 +1,202 @@ +export const froniusDeviceTypes: Record = { + 1: 'Fronius GEN24/Tauro', + 42: 'Symo Advanced 10.0-3-M', + 43: 'Symo Advanced 20.0-3-M', + 44: 'Symo Advanced 17.5-3-M', + 45: 'Symo Advanced 15.0-3-M', + 46: 'Symo Advanced 12.5-3-M', + 47: 'Symo Advanced 24.0-3 480', + 48: 'Symo Advanced 22.7-3 480', + 49: 'Symo Advanced 20.0-3 480', + 50: 'Symo Advanced 15.0-3 480', + 51: 'Symo Advanced 12.0-3 208-240', + 52: 'Symo Advanced 10.0-3 208-240', + 67: 'Fronius Primo 15.0-1 208-240', + 68: 'Fronius Primo 12.5-1 208-240', + 69: 'Fronius Primo 11.4-1 208-240', + 70: 'Fronius Primo 10.0-1 208-240', + 71: 'Fronius Symo 15.0-3 208', + 72: 'Fronius Eco 27.0-3-S', + 73: 'Fronius Eco 25.0-3-S', + 75: 'Fronius Primo 6.0-1', + 76: 'Fronius Primo 5.0-1', + 77: 'Fronius Primo 4.6-1', + 78: 'Fronius Primo 4.0-1', + 79: 'Fronius Primo 3.6-1', + 80: 'Fronius Primo 3.5-1', + 81: 'Fronius Primo 3.0-1', + 82: 'Fronius Symo Hybrid 4.0-3-S', + 83: 'Fronius Symo Hybrid 3.0-3-S', + 84: 'Fronius IG Plus 120 V-1', + 85: 'Fronius Primo 3.8-1 208-240', + 86: 'Fronius Primo 5.0-1 208-240', + 87: 'Fronius Primo 6.0-1 208-240', + 88: 'Fronius Primo 7.6-1 208-240', + 89: 'Fronius Symo 24.0-3 USA Dummy', + 90: 'Fronius Symo 24.0-3 480', + 91: 'Fronius Symo 22.7-3 480', + 92: 'Fronius Symo 20.0-3 480', + 93: 'Fronius Symo 17.5-3 480', + 94: 'Fronius Symo 15.0-3 480', + 95: 'Fronius Symo 12.5-3 480', + 96: 'Fronius Symo 10.0-3 480', + 97: 'Fronius Symo 12.0-3 208-240', + 98: 'Fronius Symo 10.0-3 208-240', + 99: 'Fronius Symo Hybrid 5.0-3-S', + 100: 'Fronius Primo 8.2-1 Dummy', + 101: 'Fronius Primo 8.2-1 208-240', + 102: 'Fronius Primo 8.2-1', + 103: 'Fronius Agilo TL 360.0-3', + 104: 'Fronius Agilo TL 460.0-3', + 105: 'Fronius Symo 7.0-3-M', + 106: 'Fronius Galvo 3.1-1 208-240', + 107: 'Fronius Galvo 2.5-1 208-240', + 108: 'Fronius Galvo 2.0-1 208-240', + 109: 'Fronius Galvo 1.5-1 208-240', + 110: 'Fronius Symo 6.0-3-M', + 111: 'Fronius Symo 4.5-3-M', + 112: 'Fronius Symo 3.7-3-M', + 113: 'Fronius Symo 3.0-3-M', + 114: 'Fronius Symo 17.5-3-M', + 115: 'Fronius Symo 15.0-3-M', + 116: 'Fronius Agilo 75.0-3 Outdoor', + 117: 'Fronius Agilo 100.0-3 Outdoor', + 118: 'Fronius IG Plus 55 V-1', + 119: 'Fronius IG Plus 55 V-2', + 120: 'Fronius Symo 20.0-3 Dummy', + 121: 'Fronius Symo 20.0-3-M', + 122: 'Fronius Symo 5.0-3-M', + 123: 'Fronius Symo 8.2-3-M', + 124: 'Fronius Symo 6.7-3-M', + 125: 'Fronius Symo 5.5-3-M', + 126: 'Fronius Symo 4.5-3-S', + 127: 'Fronius Symo 3.7-3-S', + 128: 'Fronius IG Plus 60 V-2', + 129: 'Fronius IG Plus 60 V-1', + 130: 'SPR 8001F-3 EU', + 131: 'Fronius IG Plus 25 V-1', + 132: 'Fronius IG Plus 100 V-3', + 133: 'Fronius Agilo 100.0-3', + 134: 'SPR 3001F-1 EU', + 135: 'Fronius IG Plus V/A 10.0-3 Delta', + 136: 'Fronius IG 50', + 137: 'Fronius IG Plus 30 V-1', + 138: 'SPR-11401f-1 UNI', + 139: 'SPR-12001f-3 WYE277', + 140: 'SPR-11401f-3 Delta', + 141: 'SPR-10001f-1 UNI', + 142: 'SPR-7501f-1 UNI', + 143: 'SPR-6501f-1 UNI', + 144: 'SPR-3801f-1 UNI', + 145: 'SPR-3301f-1 UNI', + 146: 'SPR 12001F-3 EU', + 147: 'SPR 10001F-3 EU', + 148: 'SPR 8001F-2 EU', + 149: 'SPR 6501F-2 EU', + 150: 'SPR 4001F-1 EU', + 151: 'SPR 3501F-1 EU', + 152: 'Fronius CL 60.0 WYE277 Dummy', + 153: 'Fronius CL 55.5 Delta Dummy', + 154: 'Fronius CL 60.0 Dummy', + 155: 'Fronius IG Plus V 12.0-3 Dummy', + 156: 'Fronius IG Plus V 7.5-1 Dummy', + 157: 'Fronius IG Plus V 3.8-1 Dummy', + 158: 'Fronius IG Plus 150 V-3 Dummy', + 159: 'Fronius IG Plus 100 V-2 Dummy', + 160: 'Fronius IG Plus 50 V-1 Dummy', + 161: 'Fronius IG Plus V/A 12.0-3 WYE', + 162: 'Fronius IG Plus V/A 11.4-3 Delta', + 163: 'Fronius IG Plus V/A 11.4-1 UNI', + 164: 'Fronius IG Plus V/A 10.0-1 UNI', + 165: 'Fronius IG Plus V/A 7.5-1 UNI', + 166: 'Fronius IG Plus V/A 6.0-1 UNI', + 167: 'Fronius IG Plus V/A 5.0-1 UNI', + 168: 'Fronius IG Plus V/A 3.8-1 UNI', + 169: 'Fronius IG Plus V/A 3.0-1 UNI', + 170: 'Fronius IG Plus 150 V-3', + 171: 'Fronius IG Plus 120 V-3', + 172: 'Fronius IG Plus 100 V-2', + 173: 'Fronius IG Plus 100 V-1', + 174: 'Fronius IG Plus 70 V-2', + 175: 'Fronius IG Plus 70 V-1', + 176: 'Fronius IG Plus 50 V-1', + 177: 'Fronius IG Plus 35 V-1', + 178: 'SPR 11400f-3 208/240', + 179: 'SPR 12000f-277', + 180: 'SPR 10000f', + 181: 'SPR 10000F EU', + 182: 'Fronius CL 33.3 Delta', + 183: 'Fronius CL 44.4 Delta', + 184: 'Fronius CL 55.5 Delta', + 185: 'Fronius CL 36.0 WYE277', + 186: 'Fronius CL 48.0 WYE277', + 187: 'Fronius CL 60.0 WYE277', + 188: 'Fronius CL 36.0', + 189: 'Fronius CL 48.0', + 190: 'Fronius IG TL 3.0', + 191: 'Fronius IG TL 4.0', + 192: 'Fronius IG TL 5.0', + 193: 'Fronius IG TL 3.6', + 194: 'Fronius IG TL Dummy', + 195: 'Fronius IG TL 4.6', + 196: 'SPR 12000F EU', + 197: 'SPR 8000F EU', + 198: 'SPR 6500F EU', + 199: 'SPR 4000F EU', + 200: 'SPR 3300F EU', + 201: 'Fronius CL 60.0', + 202: 'SPR 12000f', + 203: 'SPR 8000f', + 204: 'SPR 6500f', + 205: 'SPR 4000f', + 206: 'SPR 3300f', + 207: 'Fronius IG Plus 12.0-3 WYE277', + 208: 'Fronius IG Plus 50', + 209: 'Fronius IG Plus 100', + 210: 'Fronius IG Plus 100', + 211: 'Fronius IG Plus 150', + 212: 'Fronius IG Plus 35', + 213: 'Fronius IG Plus 70', + 214: 'Fronius IG Plus 70', + 215: 'Fronius IG Plus 120', + 216: 'Fronius IG Plus 3.0-1 UNI', + 217: 'Fronius IG Plus 3.8-1 UNI', + 218: 'Fronius IG Plus 5.0-1 UNI', + 219: 'Fronius IG Plus 6.0-1 UNI', + 220: 'Fronius IG Plus 7.5-1 UNI', + 221: 'Fronius IG Plus 10.0-1 UNI', + 222: 'Fronius IG Plus 11.4-1 UNI', + 223: 'Fronius IG Plus 11.4-3 Delta', + 224: 'Fronius Galvo 3.0-1', + 225: 'Fronius Galvo 2.5-1', + 226: 'Fronius Galvo 2.0-1', + 227: 'Fronius IG 4500-LV', + 228: 'Fronius Galvo 1.5-1', + 229: 'Fronius IG 2500-LV', + 230: 'Fronius Agilo 75.0-3', + 231: 'Fronius Agilo 100.0-3 Dummy', + 232: 'Fronius Symo 10.0-3-M', + 233: 'Fronius Symo 12.5-3-M', + 234: 'Fronius IG 5100', + 235: 'Fronius IG 4000', + 236: 'Fronius Symo 8.2-3-M Dummy', + 237: 'Fronius IG 3000', + 238: 'Fronius IG 2000', + 239: 'Fronius Galvo 3.1-1 Dummy', + 240: 'Fronius IG Plus 80 V-3', + 241: 'Fronius IG Plus 60 V-3', + 242: 'Fronius IG Plus 55 V-3', + 243: 'Fronius IG 60 ADV', + 244: 'Fronius IG 500', + 245: 'Fronius IG 400', + 246: 'Fronius IG 300', + 247: 'Fronius Symo 3.0-3-S', + 248: 'Fronius Galvo 3.1-1', + 249: 'Fronius IG 60 HV', + 250: 'Fronius IG 40', + 251: 'Fronius IG 30 Dummy', + 252: 'Fronius IG 30', + 253: 'Fronius IG 20', + 254: 'Fronius IG 15', +}; + \ No newline at end of file diff --git a/src/fronius-platform.ts b/src/fronius-platform.ts index 5bf0694..7edacf5 100644 --- a/src/fronius-platform.ts +++ b/src/fronius-platform.ts @@ -9,6 +9,7 @@ import { import { Config } from './config'; import { FroniusAccessory } from './fronius-accessory'; import { FroniusApi } from './fronius-api'; +import { froniusDeviceTypes } from './fronius-deviceType'; const PLATFORM_NAME = 'FroniusInverterLightsPlatform'; let hap: HAP; @@ -23,7 +24,6 @@ class FroniusInverterLightsStaticPlatform implements StaticPlatformPlugin { private readonly log: Logging; private readonly froniusApi: FroniusApi; private readonly pollInterval: number; - private readonly pvMaxPower?: number; private readonly battery?: boolean; constructor(log: Logging, config: PlatformConfig) { @@ -34,7 +34,6 @@ class FroniusInverterLightsStaticPlatform implements StaticPlatformPlugin { // probably parse config or something here this.froniusApi = new FroniusApi(pluginConfig.inverterIp, this.log); this.pollInterval = pluginConfig.pollInterval || 10; - this.pvMaxPower = pluginConfig.pvMaxPower; this.battery = pluginConfig.battery; } @@ -45,64 +44,120 @@ class FroniusInverterLightsStaticPlatform implements StaticPlatformPlugin { * The set of exposed accessories CANNOT change over the lifetime of the plugin! */ accessories(callback: (foundAccessories: AccessoryPlugin[]) => void): void { - const accessories = [ - new FroniusAccessory( - hap, - this.log, - 'Import', - this.froniusApi, - this.pollInterval, - ), - new FroniusAccessory( - hap, - this.log, - 'Export', - this.froniusApi, - this.pollInterval, - ), - new FroniusAccessory( - hap, - this.log, - 'Load', - this.froniusApi, - this.pollInterval, - ), - new FroniusAccessory( - hap, - this.log, - 'PV', - this.froniusApi, - this.pollInterval, - this.pvMaxPower, - ), - ]; + (async () => { + const deviceMetadata = await this.getDeviceMetadata(); - if (this.battery) { - accessories.push( - new FroniusAccessory( + const accessories = [ + new FroniusAccessory({ hap, - this.log, - 'Battery charging', - this.froniusApi, - this.pollInterval, - ), - new FroniusAccessory( + log: this.log, + metering: 'Import', + froniusApi: this.froniusApi, + pollInterval: this.pollInterval, + model: deviceMetadata?.model, + serialNumber: deviceMetadata?.serialNumber, + }), + new FroniusAccessory({ hap, - this.log, - 'Battery discharging', - this.froniusApi, - this.pollInterval, - ), - new FroniusAccessory( + log: this.log, + metering: 'Export', + froniusApi: this.froniusApi, + pollInterval: this.pollInterval, + model: deviceMetadata?.model, + serialNumber: deviceMetadata?.serialNumber, + }), + new FroniusAccessory({ hap, - this.log, - 'Battery %', - this.froniusApi, - this.pollInterval, - ), - ); + log: this.log, + metering: 'Load', + froniusApi: this.froniusApi, + pollInterval: this.pollInterval, + model: deviceMetadata?.model, + serialNumber: deviceMetadata?.serialNumber, + }), + new FroniusAccessory({ + hap, + log: this.log, + metering: 'PV', + froniusApi: this.froniusApi, + pollInterval: this.pollInterval, + pvMaxPower: deviceMetadata?.pvPower, + model: deviceMetadata?.model, + serialNumber: deviceMetadata?.serialNumber, + }), + ]; + + if (this.battery) { + accessories.push( + new FroniusAccessory({ + hap, + log: this.log, + metering: 'Battery charging', + froniusApi: this.froniusApi, + pollInterval: this.pollInterval, + model: deviceMetadata?.model, + serialNumber: deviceMetadata?.serialNumber, + }), + new FroniusAccessory({ + hap, + log: this.log, + metering: 'Battery discharging', + froniusApi: this.froniusApi, + pollInterval: this.pollInterval, + model: deviceMetadata?.model, + serialNumber: deviceMetadata?.serialNumber, + }), + new FroniusAccessory({ + hap, + log: this.log, + metering: 'Battery %', + froniusApi: this.froniusApi, + pollInterval: this.pollInterval, + model: deviceMetadata?.model, + serialNumber: deviceMetadata?.serialNumber, + }), + ); + } + + callback(accessories); + })(); + } + + private async getDeviceMetadata(): Promise< + { model: string; serialNumber: string, pvPower: number } | undefined + > { + const inverterInfo = (await this.froniusApi.getInverterInfo())?.Body.Data; + + if (!inverterInfo) { + return; } - callback(accessories); + const model = Array.from( + // dedduplicate multiple inverters + new Set( + Object.values(inverterInfo).map( + (inverter) => + froniusDeviceTypes[inverter.DT] + // remove Fronius from the name since it's already in the manufacturer field + ?.replace('Fronius', '') + .trim() ?? 'Unknown inverter', + ), + ), + ).join(' & '); + + const serialNumber = Object.values(inverterInfo) + .map((inverter) => inverter.UniqueID) + .join(' & '); + + const pvPower = Object.values(inverterInfo).reduce( + (acc, inverter) => acc + inverter.PVPower ?? 0, + 0, + ); + + return { + model, + serialNumber, + pvPower, + }; } }