UNPKG

@yyberi/fronius-comm

Version:

Library for communicating with Fronius solar inverters

213 lines 9.09 kB
import * as http from "http"; import Logger, { LogLevel } from "@yyberi/logger"; import moment from "moment-timezone"; const rootLog = Logger('fronius-comm'); const log = rootLog.child({ name: 'fronius-comm.ts' }); export class FroniusClient { ip; scope; deviceId; deviceClass; logLevel; constructor(ip, scope, deviceId, deviceClass, logLevel = LogLevel.INFO) { this.ip = ip; this.scope = scope; this.deviceId = deviceId; this.deviceClass = deviceClass; this.logLevel = logLevel; log.setLogLevel(logLevel); } /** * Get realtime data from the inverter. * @returns {Promise<SimpleInverterRealtimeData>} - Returns an object with totalEnergy, yearEnergy, dayEnergy and actPower. */ async getRealtimeData() { return new Promise((resolve, reject) => { const queryObj = { Scope: this.scope, DeviceId: this.deviceId, DataCollection: `CumulationInverterData` }; const path = `/solar_api/v1/GetInverterRealtimeData.cgi`; const searchParams = new URLSearchParams(queryObj); const options = { hostname: this.ip, port: 80, method: `GET`, path: `${path}?${searchParams.toString()}`, timeout: 5000 }; http.get(options, resp => { let data = ""; resp.on("data", chunk => { data += chunk; }); resp.on("end", () => { try { const json = JSON.parse(data); const body = json.Body; if (!body || !body.Data) throw new Error("Missing Body.Data"); const pd = body.Data; const extract = (paramName) => { const p = pd[paramName]; if (!p) throw new Error(`Param ${paramName} missing`); if (typeof p.Value === 'number') return p.Value; if (p.Values) { const firstKey = Object.keys(p.Values)[0]; const val = p.Values[firstKey]; if (typeof val === 'number') return val; } throw new Error(`Param ${paramName} is not a valid number`); }; const result = {}; try { result.totalEnergy = extract("TOTAL_ENERGY"); } catch { /* leave result.totalEnergy undefined */ } try { result.yearEnergy = extract("YEAR_ENERGY"); } catch { /* leave result.yearEnergy undefined */ } try { result.dayEnergy = extract("DAY_ENERGY"); } catch { /* leave result.dayEnergy undefined */ } try { result.actPower = extract("PAC"); } catch { // leave result.actPower undefined; } resolve(result); } catch (err) { log.error(`Error parsing inverter data: ${err}`); reject(new Error(`Failed to parse inverter data: ${err}`)); } finally { ; } }); }) .on("timeout", () => reject(new Error('Timeout while connecting to inverter'))) .on("error", err => reject(new Error('Connection error: ' + err.message))); }); } async getProductionData(dateOrStart, endDate) { log.debug(`getProductionData(${dateOrStart}, ${endDate})`); const start = moment(dateOrStart).utc().toISOString(); const end = endDate ? moment(endDate).utc().toISOString() : moment(dateOrStart).add(1, 'days').utc().toISOString(); return new Promise((resolve, reject) => { const queryObj = { Scope: this.scope, DeviceId: this.deviceId, DeviceClass: this.deviceClass, StartDate: start, EndDate: end, Channel: 'EnergyReal_WAC_Sum_Produced' }; const path = '/solar_api/v1/GetArchiveData.cgi'; const searchParams = new URLSearchParams(queryObj); const queryString = decodeURIComponent(path + '?' + searchParams.toString()); const options = { hostname: this.ip, port: 80, method: 'GET', path: queryString, headers: { Host: this.ip }, timeout: 30000 }; http.get(options, (resp) => { let data = ""; resp.on("data", (chunk) => { data += chunk; }); resp.on("end", () => { try { const json = JSON.parse(data); const invData = json?.Body?.Data?.["inverter/1"]; if (invData?.Start && invData?.Data?.EnergyReal_WAC_Sum_Produced?.Values) { resolve({ start: invData.Start, values: invData.Data.EnergyReal_WAC_Sum_Produced.Values }); } else { resolve({ start, values: {} }); } } catch (e) { reject(new Error("Invalid JSON response")); } }); }).on("timeout", () => { reject(new Error('Timeout')); }).on("error", (err) => { reject(err); }); }); } async parseEnergyReal_WAC_Sum_Produced_5min(dateOrStart, endDate) { log.debug(`parseEnergyReal_WAC_Sum_Produced_5min(${dateOrStart}, ${endDate})`); const { start, values } = endDate ? await this.getProductionData(dateOrStart, endDate) : await this.getProductionData(dateOrStart); const startTime = new Date(start); const productionData = []; const sortedKeys = Object.keys(values) .map(k => parseInt(k)) .sort((a, b) => a - b); for (const sec of sortedKeys) { if (sec === 0) continue; const value = values[sec]; const intervalStart = new Date(startTime); intervalStart.setSeconds(sec); productionData.push({ time: intervalStart.toISOString(), value: value }); } return productionData; } async parseEnergyReal_WAC_Sum_Produced_15min(dateOrStart, endDate) { log.debug(`parseEnergyReal_WAC_Sum_Produced_15min(${dateOrStart}, ${endDate})`); const data5min = endDate ? await this.parseEnergyReal_WAC_Sum_Produced_5min(dateOrStart, endDate) : await this.parseEnergyReal_WAC_Sum_Produced_5min(dateOrStart); const data15min = []; for (let i = 0; i < data5min.length; i += 3) { const group = data5min.slice(i, i + 3); if (group.length === 3) { const total = group.reduce((sum, item) => sum + item.value, 0); data15min.push({ time: group[2].time, value: total }); } } return { data5min, data15min }; } async parseEnergyReal_WAC_Sum_Produced_All(dateOrStart, endDate) { log.debug(`parseEnergyReal_WAC_Sum_Produced_All(${dateOrStart}, ${endDate})`); const { data5min, data15min } = endDate ? await this.parseEnergyReal_WAC_Sum_Produced_15min(dateOrStart, endDate) : await this.parseEnergyReal_WAC_Sum_Produced_15min(dateOrStart); const data60min = []; for (let i = 0; i < data5min.length; i += 12) { const group = data5min.slice(i, i + 12); if (group.length === 12) { data60min.push({ time: group[11].time, value: group.reduce((sum, item) => sum + item.value, 0) }); } } return { data5min, data15min, data60min }; } } //# sourceMappingURL=fronius-comm.js.map