UNPKG

iobroker.kostal-piko-ba

Version:
951 lines (950 loc) 52 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); // The adapter-core module gives you access to the core ioBroker functions you need to create an adapter const utils = __importStar(require("@iobroker/adapter-core")); const axios_1 = __importDefault(require("axios")); const xml2js_1 = __importDefault(require("xml2js")); const adapterTimeouts = {}; // state const ID_OperatingState = 16780032; // 0 = aus; 1 = Leerlauf(?); 2 = Anfahren, DC Spannung noch zu klein(?); 3 = Einspeisen(MPP); 4 = Einspeisen(abgeregelt) const ID_InverterType = 16780544; // - Inverter type const ID_InfoUIVersion = 16779267; // - Info version const ID_InverterName = 16777984; // - Inverter name // statistics - daily const ID_StatDay_Yield = 251658754; // in Wh - Total yield this operational day const ID_StatDay_HouseConsumption = 251659010; // in Wh const ID_StatDay_SelfConsumption = 251659266; // in Wh const ID_StatDay_SelfConsumptionRate = 251659278; // in % const ID_StatDay_Autarky = 251659279; // in % // statistics - total const ID_StatTot_OperatingTime = 251658496; // in h - Total operating time of inverter const ID_StatTot_Yield = 251658753; // in kWh - Total yield in inverter life time const ID_StatTot_HouseConsumption = 251659009; // in kWh const ID_StatTot_SelfConsumption = 251659265; // in kWh const ID_StatTot_SelfConsumptionRate = 251659280; // in % const ID_StatTot_Autarky = 251659281; // in % // live values - PV generator power const ID_Power_SolarDC = 33556736; // in W - DC Power PV generator in total const ID_Power_DC1Current = 33555201; // in A - DC current line 1 const ID_Power_DC1Voltage = 33555202; // in V - DC voltage line 1 const ID_Power_DC1Power = 33555203; // in W - DC power line 1 const ID_Power_DC2Current = 33555457; // in A - DC current line 2 const ID_Power_DC2Voltage = 33555458; // in V - DC voltage line 2 const ID_Power_DC2Power = 33555459; // in W - DC power line 2 const ID_Power_DC3Current = 33555713; // in A - DC current line 3 (equals to battery current in case of Pico BA) const ID_Power_DC3Voltage = 33555714; // in V - DC voltage line 3 (equals to battery voltage in case of Pico BA) const ID_Power_DC3Power = 33555715; // in W - DC power line 3 (equals to battery power in case of Pico BA) // live values - home //const ID_Power_HouseConsumptionSolar = 83886336; // in W - Act Home Consumption Solar - not implemented //const ID_Power_HouseConsumptionBat = 83886592; // in W - Act Home Consumption Bat - not implemented //const ID_Power_HouseConsumptionGrid = 83886848; // in W - Act Home Consumption Grid - not implemented const ID_Power_HouseConsumptionPhase1 = 83887106; // in W - Act Home Consumption Phase 1 const ID_Power_HouseConsumptionPhase2 = 83887362; // in W - Act Home Consumption Phase 2 const ID_Power_HouseConsumptionPhase3 = 83887618; // in W - Act Home Consumption Phase 3 const ID_Power_HouseConsumption = 83887872; // in W - Consumption of your home, measured by PIKO sensor const ID_Power_SelfConsumption = 83888128; // in W - Self Consumption // live values power output const ID_Power_GridAC = 67109120; // in W - GridOutputPower excluding power for battery charging // live values - grid parameter const ID_GridLimitation = 67110144; // in % - Grid Limitation //const ID_GridFrequency = 67110400; // in Hz - Grid Frequency - not implemented //const ID_GridCosPhi = 67110656; // - Grid CosPhi - not implemented // live values - grid phase 1 const ID_L1GridCurrent = 67109377; // in A - Grid Output Current Phase 1 const ID_L1GridVoltage = 67109378; // in V - Grid Output Voltage Phase 1 const ID_L1GridPower = 67109379; // in W - Grid Output Power Phase 1 // live values - grid phase 2 const ID_L2GridCurrent = 67109633; // in A - Grid Output Current Phase 2 const ID_L2GridVoltage = 67109634; // in V - Grid Output Voltage Phase 2 const ID_L2GridPower = 67109635; // in W - Grid Output Power Phase 2 // live values - grid phase 3 const ID_L3GridCurrent = 67109889; // in A - Grid Output Current Phase 3 const ID_L3GridVoltage = 67109890; // in V - Grid Output Voltage Phase 3 const ID_L3GridPower = 67109891; // in W - Grid Output Power Phase 3 // live values - Battery const ID_BatVoltage = 33556226; // in V const ID_BatTemperature = 33556227; // in °C const ID_BatChargeCycles = 33556228; // in 1 const ID_BatStateOfCharge = 33556229; // in % const ID_BatCurrentDir = 33556230; // 1 = discharge; 0 = charge const ID_BatCurrent = 33556238; // in A // live values - inputs const ID_InputAnalog1 = 167772417; // in V - 10bit resolution const ID_InputAnalog2 = 167772673; // in V - 10bit resolution const ID_InputAnalog3 = 167772929; // in V - 10bit resolution const ID_InputAnalog4 = 167773185; // in V - 10bit resolution //const ID_Input_S0_count = 184549632; // in 1 - not implemented //const ID_Input_S0_seconds = 150995968; // in sec - not implemented let InverterType = "unknown"; // Inverter type let InverterAPIPiko = false; // Inverter API of Piko or Piko BA inverters; Kostal Piko 6.0BA, 8.0BA, 10.0BA, 3.0, 5.5, 7.0, 10, 12, 15, 17, 20 let InverterAPIPikoMP = false; // Inverter API of Piko MP inverters; Kostal PIKO 3.0-1 MP plus let InverterUIVersion = "unknown"; // Inverter UI Version let KostalRequestOnce = ""; // IP request-string for one time request of system type etc. let KostalRequest1 = ""; // IP request-string 1 for Pico live data let KostalRequest2 = ""; // IP request-string 2 for Pico live data let KostalRequestDay = ""; // IP request-string for PicoBA daily statistics let KostalRequestTotal = ""; // IP request-string for PicoBA total statistics function resolveAfterXSeconds(x) { return new Promise(resolve => { setTimeout(() => { resolve(x); }, x * 1000); }); } class KostalPikoBA extends utils.Adapter { /**************************************************************************************** * @param {Partial<utils.AdapterOptions>} [options={}] */ constructor(options = {}) { super({ ...options, name: "kostal-piko-ba", }); this.on("ready", this.onReady.bind(this)); // this.on('objectChange', this.onObjectChange.bind(this)); // this.on('stateChange', this.onStateChange.bind(this)); // this.on('message', this.onMessage.bind(this)); this.on("unload", this.onUnload.bind(this)); } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { if (!this.config.ipaddress) { this.log.error(`Kostal Piko IP address not set`); } else { this.log.info(`IP address found in config: ${this.config.ipaddress}`); // Validate IP address ... if (!/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(this.config.ipaddress)) { this.log.error(`You have entered an invalid IP address! ${this.config.ipaddress}`); this.log.info(`Stopping adapter`); await this.stop?.({ exitCode: 11, reason: `invalid config` }); } } if (this.config.ipaddress) { // get general info of connected inverter KostalRequestOnce = `http://${this.config.ipaddress}/api/dxs.json` + `?dxsEntries=${ID_InverterType}&dxsEntries=${ID_InfoUIVersion}&dxsEntries=${ID_InverterName}`; await this.ReadPikoOnce(); await resolveAfterXSeconds(5); this.log.debug(`Initial read of general info for inverter IP ${this.config.ipaddress} done`); if (!InverterAPIPiko && !InverterAPIPikoMP) { // no inverter type detected this.log.error(`Error in detecting Kostal inverter`); this.log.info(`Stopping adapter`); void this.stop; } } //#region *** sentry.io ping *** if (this.supportsFeature && this.supportsFeature("PLUGINS")) { const sentryInstance = this.getPluginInstance("sentry"); const today = new Date(); const last = await this.getStateAsync("LastSentryLogDay"); if (last?.val != today.getDate()) { if (sentryInstance) { const Sentry = sentryInstance.getSentryObject(); Sentry && Sentry.withScope((scope) => { scope.setLevel("info"); scope.setTag("SentryDay", today.getDate()); scope.setTag("Inverter", this.config.ipaddress); scope.setTag("Inverter-Type", InverterType); scope.setTag("Inverter-UI", InverterUIVersion); Sentry.captureMessage("Adapter kostal-piko-ba started", "info"); }); } await this.setState("LastSentryLoggedError", { val: "unknown", ack: true }); // Clean last error every adapter start await this.setState("LastSentryLogDay", { val: today.getDate(), ack: true }); } } //#endregion //#region *** setup polltimes *** if (!this.config.polltimelive) { this.config.polltimelive = 10000; this.log.warn(`Polltime not set or zero - will be set to ${this.config.polltimelive / 1000} seconds`); } if (this.config.polltimelive < 5000) { this.config.polltimelive = 5000; this.log.warn(`Polltime has to be minimum 5000 will be set to ${this.config.polltimelive / 1000} seconds`); } this.log.info(`Polltime set to: ${this.config.polltimelive / 1000} seconds`); if (!this.config.polltimedaily) { this.config.polltimedaily = 60000; this.log.warn(`Polltime daily statistics data not set or zero - will be set to ${this.config.polltimedaily / 1000} seconds`); } if (this.config.polltimedaily < this.config.polltimelive * 5) { this.config.polltimedaily = this.config.polltimelive * 5; this.log.warn(`Polltime daily statistics should be min. 5 times the standard poll - will be set to ${this.config.polltimedaily / 1000} seconds`); } this.log.info(`Polltime daily statistics set to: ${this.config.polltimedaily / 1000} seconds`); if (!this.config.polltimetotal) { this.config.polltimetotal = 200000; this.log.warn(`Polltime alltime statistics not set or zero - will be set to ${this.config.polltimetotal / 1000} seconds`); } if (this.config.polltimetotal < this.config.polltimedaily * 2) { this.config.polltimetotal = this.config.polltimedaily * 2; this.log.warn(`Polltime for all-time statistics should be at least double the daily statistics poll time - it will be set to ${this.config.polltimetotal / 1000} seconds`); } this.log.info(`Polltime for alltime statistics set to: ${this.config.polltimetotal / 1000} seconds`); //#endregion if (this.config.ipaddress) { KostalRequest1 = `http://${this.config.ipaddress}/api/dxs.json` + `?dxsEntries=${ID_Power_SolarDC}&dxsEntries=${ID_Power_GridAC}` + `&dxsEntries=${ID_Power_DC1Power}&dxsEntries=${ID_Power_DC1Current}` + `&dxsEntries=${ID_Power_DC1Voltage}&dxsEntries=${ID_Power_DC2Power}` + `&dxsEntries=${ID_Power_DC2Current}&dxsEntries=${ID_Power_DC2Voltage}` + `&dxsEntries=${ID_Power_DC3Power}&dxsEntries=${ID_Power_DC3Current}` + `&dxsEntries=${ID_Power_DC3Voltage}` + `&dxsEntries=${ID_Power_SelfConsumption}&dxsEntries=${ID_Power_HouseConsumption}` + `&dxsEntries=${ID_OperatingState}&dxsEntries=${ID_BatVoltage}` + `&dxsEntries=${ID_BatTemperature}&dxsEntries=${ID_BatStateOfCharge}` + `&dxsEntries=${ID_BatCurrent}&dxsEntries=${ID_BatCurrentDir}` + `&dxsEntries=${ID_GridLimitation}`; KostalRequest2 = `http://${this.config.ipaddress}/api/dxs.json` + `?dxsEntries=${ID_L1GridCurrent}&dxsEntries=${ID_L1GridVoltage}` + `&dxsEntries=${ID_L1GridPower}&dxsEntries=${ID_L2GridCurrent}` + `&dxsEntries=${ID_L2GridVoltage}&dxsEntries=${ID_L2GridPower}` + `&dxsEntries=${ID_L3GridCurrent}&dxsEntries=${ID_L3GridVoltage}` + `&dxsEntries=${ID_L3GridPower}&dxsEntries=${ID_Power_HouseConsumptionPhase1}` + `&dxsEntries=${ID_Power_HouseConsumptionPhase2}&dxsEntries=${ID_Power_HouseConsumptionPhase3}`; if (this.config.readanalogs) { KostalRequest2 = `${KostalRequest2}` + `&dxsEntries=${ID_InputAnalog1}&dxsEntries=${ID_InputAnalog2}` + `&dxsEntries=${ID_InputAnalog3}&dxsEntries=${ID_InputAnalog4}`; if (this.config.normAn1Max != 10 || this.config.normAn1Min != 0) { await this.extendObject("Inputs.Analog1", { common: { unit: "" } }); } if (this.config.normAn2Max != 10 || this.config.normAn2Min != 0) { await this.extendObject("Inputs.Analog2", { common: { unit: "" } }); } if (this.config.normAn3Max != 10 || this.config.normAn3Min != 0) { await this.extendObject("Inputs.Analog3", { common: { unit: "" } }); } if (this.config.normAn4Max != 10 || this.config.normAn4Min != 0) { await this.extendObject("Inputs.Analog4", { common: { unit: "" } }); } } KostalRequestDay = `http://${this.config.ipaddress}/api/dxs.json` + `?dxsEntries=${ID_StatDay_SelfConsumption}&dxsEntries=${ID_StatDay_SelfConsumptionRate}` + `&dxsEntries=${ID_StatDay_Yield}&dxsEntries=${ID_StatDay_HouseConsumption}` + `&dxsEntries=${ID_StatDay_Autarky}`; KostalRequestTotal = `http://${this.config.ipaddress}/api/dxs.json` + `?dxsEntries=${ID_StatTot_SelfConsumption}&dxsEntries=${ID_StatTot_SelfConsumptionRate}` + `&dxsEntries=${ID_StatTot_Yield}&dxsEntries=${ID_StatTot_HouseConsumption}` + `&dxsEntries=${ID_StatTot_Autarky}&dxsEntries=${ID_StatTot_OperatingTime}`; if (this.config.readbattery) { KostalRequestTotal = `${KostalRequestTotal}&dxsEntries=${ID_BatChargeCycles}`; } this.log.debug(`OnReady done`); this.ReadPikoTotal(); await resolveAfterXSeconds(3); this.ReadPikoDaily(); await resolveAfterXSeconds(3); this.Scheduler(); this.log.debug(`Initial ReadPiko done`); } else { this.log.error(`No IP address configured, adapter is shutting down`); void this.stop; } } /** * Is called when adapter shuts down - callback has to be called under any circumstances! * * @param callback - void */ onUnload(callback) { try { if (adapterTimeouts) { Object.keys(adapterTimeouts).forEach(timeout => { if (adapterTimeouts[timeout]) { clearTimeout(adapterTimeouts[timeout]); } }); } void this.setState("info.connection", { val: false, ack: true }); this.log.info(`Adapter Kostal-Piko-BA cleaned up everything...`); callback(); } catch (error) { this.log.error(`Error in onUnload adapter: ${error}`); callback(); } } /**************************************************************************************** * Scheduler ****************************************************************************/ Scheduler() { this.ReadPiko(); this.ReadPiko2(); try { clearTimeout(adapterTimeouts.live); adapterTimeouts.live = setTimeout(this.Scheduler.bind(this), this.config.polltimelive); } catch (error) { this.log.error(`Error in setting adapter schedule: ${error}`); void this.restart; } // END try catch } /**************************************************************************************** * ReadPikoOnce ***************************************************************************/ async ReadPikoOnce() { await axios_1.default .get(KostalRequestOnce, { transformResponse: r => r }) .then(response => { //.status == 200 // access parsed JSON response data using response.data field if (!response.data) { throw new Error(`Empty answer from Piko.`); } this.log.debug(`Piko-BA general info updated - Kostal response data: ${response.data}`); const result = JSON.parse(response.data).dxsEntries; InverterType = result[0].value; void this.setState("Info.InverterType", { val: InverterType, ack: true }); InverterAPIPiko = true; void this.setState("info.connection", { val: true, ack: true }); InverterUIVersion = result[1].value; void this.setState("Info.InverterUIVersion", { val: InverterUIVersion, ack: true, }); void this.setState("Info.InverterName", { val: result[2].value, ack: true }); }) .catch(error => { void this.HandleConnectionError(error, `Piko(-BA) API for general info`, `BA0`); }); await resolveAfterXSeconds(2); if (InverterAPIPiko) { // inverter type detected yet this.log.info(`Detected inverter type: ${InverterType}`); } else { this.log.warn(`Error in polling with Piko(-BA)-API: ${InverterType}`); this.log.info(`Trying to detect inverter with Piko-MP-API`); } if (!InverterAPIPiko) { // no inverter type detected yet -> try to detect Piko MP Inverter axios_1.default .get(`http://${this.config.ipaddress}/versions.xml`, { transformResponse: r => r, }) .then(response => { xml2js_1.default.parseString(response.data, (err, result) => { if (err) { this.log.error(`Error when calling Piko MP API with axios for general info: ${err}`); } else { const MPType = result.root.Device[0].$.Name; if (MPType) { this.log.info(`Discovered Piko MP API, type of inverter: ${MPType}`); InverterType = MPType; void this.setState("Info.InverterType", { val: InverterType, ack: true, }); InverterAPIPikoMP = true; void this.setState("info.connection", { val: true, ack: true }); InverterUIVersion = "MP"; void this.setState("Info.InverterUIVersion", { val: InverterUIVersion, ack: true, }); void this.setState("Info.InverterName", { val: result.root.Device[0].$.NetBiosName, ack: true, }); } } }); }) .catch(error => { void this.HandleConnectionError(error, `Piko MP API for general info`, `MP0`); }); } } // END ReadPikoOnce /**************************************************************************************** * ReadPiko *****************************************************************************/ ReadPiko() { if (InverterAPIPiko) { // code for Piko(-BA) axios_1.default .get(KostalRequest1, { timeout: 3500, transformResponse: r => r }) .then(response => { //.status == 200 // access parsed JSON response data using response.data field this.log.debug(`Piko-BA live data 1 update - Kostal response data: ${response.data}`); if (!response.data) { throw new Error(`Empty answer from Piko.`); } const result = JSON.parse(response.data).dxsEntries; if (result && result.length > 0) { void this.setState("Power.SolarDC", { val: result[0].value ? Math.round(result[0].value) : 0, ack: true, }); void this.setState("Power.GridAC", { val: result[1].value ? Math.round(result[1].value) : 0, ack: true, }); void this.setState("Power.Surplus", { val: result[1].value ? Math.round(result[1].value - result[11].value) : 0, ack: true, }); void this.setState("Power.DC1Power", { val: result[4].value ? Math.round(result[2].value) : 0, ack: true, }); void this.setState("Power.DC1Current", { val: result[4].value ? Math.round(1000 * result[3].value) / 1000 : 0, ack: true, }); void this.setState("Power.DC1Voltage", { val: result[4].value ? Math.round(result[4].value) : 0, ack: true, }); void this.setState("Power.DC2Power", { val: result[7].value ? Math.round(result[5].value) : 0, ack: true, }); void this.setState("Power.DC2Current", { val: result[7].value ? Math.round(1000 * result[6].value) / 1000 : 0, ack: true, }); void this.setState("Power.DC2Voltage", { val: result[7].value ? Math.round(result[7].value) : 0, ack: true, }); void this.setState("Power.DC3Power", { val: result[10].value ? Math.round(result[8].value) : 0, ack: true, }); void this.setState("Power.DC3Current", { val: result[10].value ? Math.round(1000 * result[9].value) / 1000 : 0, ack: true, }); void this.setState("Power.DC3Voltage", { val: result[10].value ? Math.round(result[10].value) : 0, ack: true, }); void this.setState("Power.SelfConsumption", { val: Math.round(result[11].value), ack: true, }); void this.setState("Power.HouseConsumption", { val: Math.floor(result[12].value), ack: true, }); void this.setState("State", { val: result[13].value, ack: true }); switch (result[13].value) { case 0: void this.setState("StateAsString", { val: "OFF", ack: true }); break; case 1: void this.setState("StateAsString", { val: "Idling", ack: true }); break; case 2: void this.setState("StateAsString", { val: "Start up, DC voltage still too low for feed-in", ack: true, }); break; case 3: void this.setState("StateAsString", { val: "Feeding (MPP)", ack: true, }); break; case 4: void this.setState("StateAsString", { val: "Feeding (limited)", ack: true, }); break; default: void this.setState("StateAsString", { val: "Undefined", ack: true }); } if (result[14].value) { void this.setState("Battery.Voltage", { val: Math.round(result[14].value), ack: true, }); void this.setState("Battery.Temperature", { val: Math.round(10 * result[15].value) / 10, ack: true, }); void this.setState("Battery.SoC", { val: result[16].value, ack: true, }); if (result[18].value) { // result[18] = 'Battery current direction; 1=Load; 0=Unload' void this.setState("Battery.Current", { val: result[17].value, ack: true, }); void this.setState("Battery.Power", { val: Math.round(result[14].value * result[17].value), ack: true, }); } else { // discharge void this.setState("Battery.Current", { val: result[17].value * -1, ack: true, }); void this.setState("Battery.Power", { val: Math.round(result[14].value * result[17].value * -1), ack: true, }); } } } else { this.log.error(`Got no answer from inverter, please verify IP address: ${this.config.ipaddress} !! (e1.1)`); } void this.setState("GridLimitation", { val: result.length >= 20 ? result[19].value : 100, ack: true, }); // not existent for Piko3.0 or if no limitation defined }) .catch(error => { void this.HandleConnectionError(error, `Piko(-BA) API for live data`, `BA1`); }); } // END InverterAPIPiko if (InverterAPIPikoMP) { // code for Piko MP Plus axios_1.default .get(`http://${this.config.ipaddress}/measurements.xml`, { transformResponse: r => r, }) .then(response => { xml2js_1.default.parseString(response.data, (err, result) => { if (err) { this.log.error(`Error when calling Piko MP API with axios for measurements info: ${err}`); } else { const measurements = result?.root.Device[0].Measurements[0].Measurement; const DC_Voltage = measurements?.find(measurement => measurement.$.Type === "DC_Voltage"); if (DC_Voltage && DC_Voltage.$) { void this.setState("Power.DC1Voltage", { val: Math.round(DC_Voltage.$.Value), ack: true, }); } else { const DC_Voltage1 = measurements.find(measurement => measurement.$.Type === "DC_Voltage1"); const DC_Voltage2 = measurements.find(measurement => measurement.$.Type === "DC_Voltage2"); if (DC_Voltage1 && DC_Voltage1.$) { void this.setState("Power.DC1Voltage", { val: Math.round(DC_Voltage1.$.Value), ack: true, }); } if (DC_Voltage2 && DC_Voltage2.$) { void this.setState("Power.DC2Voltage", { val: Math.round(DC_Voltage2.$.Value), ack: true, }); } } const DC_Current = measurements.find(measurement => measurement.$.Type === "DC_Current"); if (DC_Current && DC_Current.$) { void this.setState("Power.DC1Current", { val: Math.round(1000 * DC_Current.$.Value) / 1000, ack: true, }); } else { const DC_Current1 = measurements.find(measurement => measurement.$.Type === "DC_Current1"); const DC_Current2 = measurements.find(measurement => measurement.$.Type === "DC_Current2"); if (DC_Current1 && DC_Current1.$) { void this.setState("Power.DC1Current", { val: Math.round(1000 * DC_Current1.$.Value) / 1000, ack: true, }); } if (DC_Current2 && DC_Current2.$) { void this.setState("Power.DC2Current", { val: Math.round(1000 * DC_Current2.$.Value) / 1000, ack: true, }); } } if (DC_Current && DC_Voltage) { void this.setState("Power.DC1Power", { val: Math.round(DC_Voltage.$.Value * DC_Current.$.Value), ack: true, }); } else { const DC_Power1 = measurements.find(measurement => measurement.$.Type === "DC_Power1"); const DC_Power2 = measurements.find(measurement => measurement.$.Type === "DC_Power2"); if (DC_Power1 && DC_Power1.$) { void this.setState("Power.DC1Power", { val: Math.round(DC_Power1.$.Value), ack: true, }); } if (DC_Power2 && DC_Power2.$) { void this.setState("Power.DC2Power", { val: Math.round(DC_Power2.$.Value), ack: true, }); } } const AC_Voltage = measurements.find(measurement => measurement.$.Type === "AC_Voltage"); if (AC_Voltage && AC_Voltage.$) { void this.setState("Power.AC1Voltage", { val: Math.round(AC_Voltage.$.Value), ack: true, }); } const AC_Current = measurements.find(measurement => measurement.$.Type === "AC_Current"); if (AC_Current && AC_Current.$) { void this.setState("Power.AC1Current", { val: Math.round(1000 * AC_Current.$.Value) / 1000, ack: true, }); } const AC_Power = measurements.find(measurement => measurement.$.Type === "AC_Power"); if (AC_Power && AC_Power.$) { void this.setState("Power.AC1Power", { val: Math.round(AC_Power.$.Value), ack: true, }); } } }); }) .catch(error => { void this.HandleConnectionError(error, `Piko MP API for live data`, `MP1`); }); } // END InverterAPIPikoMP } // END ReadPiko /**************************************************************************************** * ReadPiko2 ****************************************************************************/ ReadPiko2() { if (InverterAPIPiko) { // code for Piko(-BA) axios_1.default .get(KostalRequest2, { timeout: 3500, transformResponse: r => r }) .then(response => { //.status == 200 // access parsed JSON response data using response.data field this.log.debug(`Piko-BA live data 2 update - Kostal response data: ${response.data}`); if (!response.data) { throw new Error(`Empty answer from Piko.`); } const result = JSON.parse(response.data).dxsEntries; void this.setState("Power.AC1Current", { val: result[1].value ? Math.round(1000 * result[0].value) / 1000 : 0, ack: true, }); void this.setState("Power.AC1Voltage", { val: result[1].value ? Math.round(result[1].value) : 0, ack: true, }); void this.setState("Power.AC1Power", { val: result[1].value ? Math.round(result[2].value) : 0, ack: true, }); void this.setState("Power.AC2Current", { val: result[4].value ? Math.round(1000 * result[3].value) / 1000 : 0, ack: true, }); void this.setState("Power.AC2Voltage", { val: result[4].value ? Math.round(result[4].value) : 0, ack: true, }); void this.setState("Power.AC2Power", { val: result[4].value ? Math.round(result[5].value) : 0, ack: true, }); void this.setState("Power.AC3Current", { val: result[7].value ? Math.round(1000 * result[6].value) / 1000 : 0, ack: true, }); void this.setState("Power.AC3Voltage", { val: result[7].value ? Math.round(result[7].value) : 0, ack: true, }); void this.setState("Power.AC3Power", { val: result[7].value ? Math.round(result[8].value) : 0, ack: true, }); if (result[9].value) { void this.setState("Power.HouseConsumptionPhase1", { val: Math.round(result[9].value), ack: true, }); void this.setState("Power.HouseConsumptionPhase2", { val: Math.round(result[10].value), ack: true, }); void this.setState("Power.HouseConsumptionPhase3", { val: Math.round(result[11].value), ack: true, }); } if (this.config.readanalogs) { void this.setState("Inputs.Analog1", { val: Math.round(100 * ((result[12].value / 10) * (this.config.normAn1Max - this.config.normAn1Min) + this.config.normAn1Min)) / 100, ack: true, }); void this.setState("Inputs.Analog2", { val: Math.round(100 * ((result[13].value / 10) * (this.config.normAn2Max - this.config.normAn2Min) + this.config.normAn2Min)) / 100, ack: true, }); void this.setState("Inputs.Analog3", { val: Math.round(100 * ((result[14].value / 10) * (this.config.normAn3Max - this.config.normAn3Min) + this.config.normAn3Min)) / 100, ack: true, }); void this.setState("Inputs.Analog4", { val: Math.round(100 * ((result[15].value / 10) * (this.config.normAn4Max - this.config.normAn4Min) + this.config.normAn4Min)) / 100, ack: true, }); } }) .catch(error => { void this.HandleConnectionError(error, `Piko(-BA) API for live data`, `BA2`); }); } // END InverterAPIPiko if (InverterAPIPikoMP) { // currently no code for Piko MP Plus - less data to poll , so handled in ReadPiko() } } // END ReadPiko2 /** * ReadPikoDaily * Reads daily statistics from Piko inverters and updates the corresponding states. * * This method uses the Axios library to fetch daily data from Piko inverters. It supports two types of inverters: * - Piko(-BA): If the `InverterAPIPiko` flag is set, it fetches the data from the `KostalRequestDay` URL and updates the statistics for self-consumption, self-consumption rate, yield, house consumption, and autarky. * - Piko MP Plus: Currently, no daily values are fetched or handled for this inverter type (`InverterAPIPikoMP`), cause it looks like there are no daily values for MP Plus inverters available. * * The method handles successful responses by logging the data and updating states with the retrieved values. * In case of errors, it calls `HandleConnectionError` with the appropriate parameters. * * After fetching the data, the method sets a timeout to call itself again based on the `polltimedaily` configuration. */ ReadPikoDaily() { if (InverterAPIPiko) { // code for Piko(-BA) axios_1.default .get(KostalRequestDay, { transformResponse: r => r }) .then(response => { //.status == 200 // access parsed JSON response data using response.data field this.log.debug(`Piko-BA daily statistics update - Kostal response data: ${response.data}`); if (!response.data) { throw new Error(`Empty answer from Piko.`); } const result = JSON.parse(response.data).dxsEntries; void this.setState("Statistics_Daily.SelfConsumption", { val: Math.round(result[0].value) / 1000, ack: true, }); void this.setState("Statistics_Daily.SelfConsumptionRate", { val: Math.round(result[1].value), ack: true, }); void this.setState("Statistics_Daily.Yield", { val: Math.round(result[2].value) / 1000, ack: true, }); void this.setState("Statistics_Daily.HouseConsumption", { val: Math.round(result[3].value) / 1000, ack: true, }); void this.setState("Statistics_Daily.Autarky", { val: Math.round(result[4].value), ack: true, }); }) .catch(error => { void this.HandleConnectionError(error, `Piko(-BA) API for daily statistics`, `BA3`); }); } // END InverterAPIPiko if (InverterAPIPikoMP) { /* empty */ } try { clearTimeout(adapterTimeouts.daily); adapterTimeouts.daily = setTimeout(this.ReadPikoDaily.bind(this), this.config.polltimedaily); } catch (error) { this.log.error(`Error in setting adapter schedule for daily statistics: ${error}`); } // END try catch } // END ReadPikoDaily /**************************************************************************************** * ReadPikoTotal ************************************************************************/ ReadPikoTotal() { if (InverterAPIPiko) { // code for Piko(-BA) axios_1.default .get(KostalRequestTotal, { transformResponse: r => r }) .then(response => { //.status == 200 // access parsed JSON response data using response.data field this.log.debug(`Piko-BA lifetime statistics updated - Kostal response data: ${response.data}`); if (!response.data) { throw new Error(`Empty answer from Piko.`); } const result = JSON.parse(response.data).dxsEntries; void this.setState("Statistics_Total.SelfConsumption", { val: Math.round(result[0].value), ack: true, }); void this.setState("Statistics_Total.SelfConsumptionRate", { val: Math.round(result[1].value), ack: true, }); void this.setState("Statistics_Total.Yield", { val: Math.round(result[2].value), ack: true, }); void this.setState("Statistics_Total.HouseConsumption", { val: Math.round(result[3].value), ack: true, }); void this.setState("Statistics_Total.Autarky", { val: Math.round(result[4].value), ack: true, }); void this.setState("Statistics_Total.OperatingTime", { val: result[5].value, ack: true, }); if (this.config.readbattery) { void this.setState("Battery.ChargeCycles", { val: result[6].value, ack: true, }); } }) .catch(error => { void this.HandleConnectionError(error, `Piko-(BA) API for total statistics`, `BA4`); }); } // END InverterAPIPiko if (InverterAPIPikoMP) { // code for Piko MP Plus axios_1.default .get(`http://${this.config.ipaddress}/yields.xml`, { transformResponse: r => r, }) .then(response => { xml2js_1.default.parseString(response.data, (err, result) => { //xml2js.parseString(response.data, (err, result) => { if (err) { this.log.error(`Error when calling Piko MP API with axios for measurements info: ${err}`); } else { const yields = result.root.Device[0].Yields[0].Yield; const yieldProduced = yields?.find(oyield => oyield.$.Type === "Produced"); if (yieldProduced && yieldProduced.$) { if (yieldProduced.$.Slot === "Total") { const yieldProducedValue = yieldProduced.YieldValue?.[0]?.$?.Value; if (yieldProducedValue !== undefined) { void this.setState("Statistics_Total.Yield", { val: Math.round(yieldProducedValue / 1000), ack: true, }); } } else { this.log.warn(`total yield produced value not found`); } } else { this.log.warn(`total yield produced field not found`); } } }); }) .catch(error => { void this.HandleConnectionError(error, `Piko MP API for total statistics`, `MP4`); }); } // END InverterAPIPikoMP try { clearTimeout(adapterTimeouts.total); adapterTimeouts.total = setTimeout(this.ReadPikoTotal.bind(this), this.config.polltimetotal); } catch (e) { this.log.error(`Error in setting adapter schedule for total statistics: ${e}`); } } // END ReadPikoTotal /*****************************************************************************************/ async HandleConnectionError(stError, sOccasion, sErrorOccInt) { if (stError.response) { //get HTTP error code switch (stError.response.status) { case 401: //this.SendSentryError(stError.Message); this.log.error(`The Inverter request has not been completed because it lacks valid authentication credentials.`); this.log.error(`HTTP error 401 when calling ${sOccasion}!! (e${sErrorOccInt}.0)`); this.log.warn(`Authenticated access is not supported so far by Kostal Adapter`); this.log.warn(`Please provide feedback in GitHub to get this done`); this.log.error(`Adapter is shutting down`); void this.stop; break; default: this.log.error(`HTTP error ${stError.response.status} when polling ${sOccasion}!! (e${sErrorOccInt}.1)`); } } else if (stError.code) { //get error code switch (stError.code) { case "ETIMEDOUT": this.log.warn(`Connection timeout error when calling ${sOccasion}`); this.log.warn(`Please verify the IP address: ${this.config.ipaddress} !! (e${sErrorOccInt}.2)`); break; case "EHOSTUNREACH": this.log.warn(`Inverter not reachable error when calling ${sOccasion}`); this.log.warn(`Please verify the IP address: ${this.config.ipaddress} !! (e${sErrorOccInt}.2)`); break; case "ENETUNREACH": this.log.warn(`Inverter network not reachable error when calling ${sOccasion}`); this.log.warn(`Please verify the IP address: ${this.config.ipaddress} !! (e${sErrorOccInt}.2)`); break; }