"use strict"; // SPDX-License-Identifier: GPL-3.0-or-later // This program will connect to one or more Fronius Symo Inverters. // to get the Solar Power Generated (current, today). // example configuration in netdata/conf.d/node.d/fronius.conf.md require("url"); require("http"); var netdata = require("netdata"); netdata.debug("loaded " + __filename + " plugin"); var fronius = { name: "Fronius", enable_autodetect: false, update_every: 5, base_priority: 60000, charts: {}, powerGridId: "p_grid", powerPvId: "p_pv", powerAccuId: "p_akku", // not my typo! Using the ID from the AP consumptionLoadId: "p_load", autonomyId: "rel_autonomy", consumptionSelfId: "rel_selfconsumption", solarConsumptionId: "solar_consumption", energyTodayId: "e_day", energyYearId: "e_year", createBasicDimension: function (id, name, divisor) { return { id: id, // the unique id of the dimension name: name, // the name of the dimension algorithm: netdata.chartAlgorithms.absolute,// the id of the netdata algorithm multiplier: 1, // the multiplier divisor: divisor, // the divisor hidden: false // is hidden (boolean) }; }, // Gets the site power chart. Will be created if not existing. getSitePowerChart: function (service, suffix) { var id = this.getChartId(service, suffix); var chart = fronius.charts[id]; if (fronius.isDefined(chart)) return chart; var dim = {}; dim[fronius.powerGridId] = this.createBasicDimension(fronius.powerGridId, "grid", 1); dim[fronius.powerPvId] = this.createBasicDimension(fronius.powerPvId, "photovoltaics", 1); dim[fronius.powerAccuId] = this.createBasicDimension(fronius.powerAccuId, "accumulator", 1); chart = { id: id, // the unique id of the chart name: "", // the unique name of the chart title: service.name + " Current Site Power", // the title of the chart units: "W", // the units of the chart dimensions family: "power", // the family of the chart context: "fronius.power", // the context of the chart type: netdata.chartTypes.area, // the type of the chart priority: fronius.base_priority + 1, // the priority relative to others in the same family update_every: service.update_every, // the expected update frequency of the chart dimensions: dim }; chart = service.chart(id, chart); fronius.charts[id] = chart; return chart; }, // Gets the site consumption chart. Will be created if not existing. getSiteConsumptionChart: function (service, suffix) { var id = this.getChartId(service, suffix); var chart = fronius.charts[id]; if (fronius.isDefined(chart)) return chart; var dim = {}; dim[fronius.consumptionLoadId] = this.createBasicDimension(fronius.consumptionLoadId, "load", 1); chart = { id: id, // the unique id of the chart name: "", // the unique name of the chart title: service.name + " Current Load", // the title of the chart units: "W", // the units of the chart dimensions family: "consumption", // the family of the chart context: "fronius.consumption", // the context of the chart type: netdata.chartTypes.area, // the type of the chart priority: fronius.base_priority + 2, // the priority relative to others in the same family update_every: service.update_every, // the expected update frequency of the chart dimensions: dim }; chart = service.chart(id, chart); fronius.charts[id] = chart; return chart; }, // Gets the site consumption chart. Will be created if not existing. getSiteAutonomyChart: function (service, suffix) { var id = this.getChartId(service, suffix); var chart = fronius.charts[id]; if (fronius.isDefined(chart)) return chart; var dim = {}; dim[fronius.autonomyId] = this.createBasicDimension(fronius.autonomyId, "autonomy", 1); dim[fronius.consumptionSelfId] = this.createBasicDimension(fronius.consumptionSelfId, "self_consumption", 1); dim[fronius.solarConsumptionId] = this.createBasicDimension(fronius.solarConsumptionId, "solar_consumption", 1); chart = { id: id, // the unique id of the chart name: "", // the unique name of the chart title: service.name + " Current Autonomy", // the title of the chart units: "%", // the units of the chart dimensions family: "autonomy", // the family of the chart context: "fronius.autonomy", // the context of the chart type: netdata.chartTypes.area, // the type of the chart priority: fronius.base_priority + 3, // the priority relative to others in the same family update_every: service.update_every, // the expected update frequency of the chart dimensions: dim }; chart = service.chart(id, chart); fronius.charts[id] = chart; return chart; }, // Gets the site energy chart for today. Will be created if not existing. getSiteEnergyTodayChart: function (service, suffix) { var chartId = this.getChartId(service, suffix); var chart = fronius.charts[chartId]; if (fronius.isDefined(chart)) return chart; var dim = {}; dim[fronius.energyTodayId] = this.createBasicDimension(fronius.energyTodayId, "today", 1000); chart = { id: chartId, // the unique id of the chart name: "", // the unique name of the chart title: service.name + " Energy production for today",// the title of the chart units: "kWh", // the units of the chart dimensions family: "energy", // the family of the chart context: "fronius.energy.today", // the context of the chart type: netdata.chartTypes.area, // the type of the chart priority: fronius.base_priority + 4, // the priority relative to others in the same family update_every: service.update_every, // the expected update frequency of the chart dimensions: dim }; chart = service.chart(chartId, chart); fronius.charts[chartId] = chart; return chart; }, // Gets the site energy chart for today. Will be created if not existing. getSiteEnergyYearChart: function (service, suffix) { var chartId = this.getChartId(service, suffix); var chart = fronius.charts[chartId]; if (fronius.isDefined(chart)) return chart; var dim = {}; dim[fronius.energyYearId] = this.createBasicDimension(fronius.energyYearId, "year", 1000); chart = { id: chartId, // the unique id of the chart name: "", // the unique name of the chart title: service.name + " Energy production for this year",// the title of the chart units: "kWh", // the units of the chart dimensions family: "energy", // the family of the chart context: "fronius.energy.year", // the context of the chart type: netdata.chartTypes.area, // the type of the chart priority: fronius.base_priority + 5, // the priority relative to others in the same family update_every: service.update_every, // the expected update frequency of the chart dimensions: dim }; chart = service.chart(chartId, chart); fronius.charts[chartId] = chart; return chart; }, // Gets the inverter power chart. Will be created if not existing. // Needs the array of inverters in order to create a chart with all inverters as dimensions getInverterPowerChart: function (service, suffix, inverters) { var chartId = this.getChartId(service, suffix); var chart = fronius.charts[chartId]; if (fronius.isDefined(chart)) return chart; var dim = {}; for (var key in inverters) { if (inverters.hasOwnProperty(key)) { var name = key; if (!isNaN(key)) name = "inverter_" + key; dim[key] = this.createBasicDimension("inverter_" + key, name, 1); } } chart = { id: chartId, // the unique id of the chart name: "", // the unique name of the chart title: service.name + " Current Inverter Output",// the title of the chart units: "W", // the units of the chart dimensions family: "inverters", // the family of the chart context: "fronius.inverter.output", // the context of the chart type: netdata.chartTypes.stacked, // the type of the chart priority: fronius.base_priority + 6, // the priority relative to others in the same family update_every: service.update_every, // the expected update frequency of the chart dimensions: dim }; chart = service.chart(chartId, chart); fronius.charts[chartId] = chart; return chart; }, processResponse: function (service, content) { var json = fronius.convertToJson(content); if (json === null) return; // add the service service.commit(); var chartDefinitions = fronius.parseCharts(service, json); var chartCount = chartDefinitions.length; while (chartCount--) { var chartObj = chartDefinitions[chartCount]; service.begin(chartObj.chart); var dimCount = chartObj.dimensions.length; while (dimCount--) { var dim = chartObj.dimensions[dimCount]; service.set(dim.name, dim.value); } service.end(); } }, parseCharts: function (service, json) { var site = json.Body.Data.Site; return [ this.parsePowerChart(service, site), this.parseConsumptionChart(service, site), this.parseAutonomyChart(service, site), this.parseEnergyTodayChart(service, site), this.parseEnergyYearChart(service, site), this.parseInverterChart(service, json.Body.Data.Inverters) ]; }, parsePowerChart: function (service, site) { return this.getChart(this.getSitePowerChart(service, "power"), [ this.getDimension(this.powerGridId, Math.round(site.P_Grid)), this.getDimension(this.powerPvId, Math.round(Math.max(site.P_PV, 0))), this.getDimension(this.powerAccuId, Math.round(site.P_Akku)) ] ); }, parseConsumptionChart: function (service, site) { return this.getChart(this.getSiteConsumptionChart(service, "consumption"), [this.getDimension(this.consumptionLoadId, Math.round(Math.abs(site.P_Load)))] ); }, parseAutonomyChart: function (service, site) { var selfConsumption = site.rel_SelfConsumption; var solarConsumption = 0; var load = Math.abs(site.P_Load); var power = Math.max(site.P_PV, 0); if (power <= 0) solarConsumption = 0; else if (load >= power) solarConsumption = 100; else solarConsumption = 100 / power * load; return this.getChart(this.getSiteAutonomyChart(service, "autonomy"), [ this.getDimension(this.autonomyId, Math.round(site.rel_Autonomy)), this.getDimension(this.consumptionSelfId, Math.round(selfConsumption === null ? 100 : selfConsumption)), this.getDimension(this.solarConsumptionId, Math.round(solarConsumption)) ] ); }, parseEnergyTodayChart: function (service, site) { return this.getChart(this.getSiteEnergyTodayChart(service, "energy.today"), [this.getDimension(this.energyTodayId, Math.round(Math.max(site.E_Day, 0)))] ); }, parseEnergyYearChart: function (service, site) { return this.getChart(this.getSiteEnergyYearChart(service, "energy.year"), [this.getDimension(this.energyYearId, Math.round(Math.max(site.E_Year, 0)))] ); }, parseInverterChart: function (service, inverters) { var dimensions = []; for (var key in inverters) { if (inverters.hasOwnProperty(key)) { dimensions.push(this.getDimension(key, Math.round(inverters[key].P))); } } return this.getChart(this.getInverterPowerChart(service, "inverters.output", inverters), dimensions); }, getDimension: function (name, value) { return { name: name, value: value }; }, getChart: function (chart, dimensions) { return { chart: chart, dimensions: dimensions }; }, getChartId: function (service, suffix) { return "fronius_" + service.name + "." + suffix; }, convertToJson: function (httpBody) { if (httpBody === null) return null; var json = httpBody; // can't parse if it's already a json object, // the check enables easier testing if the httpBody is already valid JSON. if (typeof httpBody !== "object") { try { json = JSON.parse(httpBody); } catch (error) { netdata.error("fronius: Got a response, but it is not valid JSON. Ignoring. Error: " + error.message); return null; } } return this.isResponseValid(json) ? json : null; }, // some basic validation isResponseValid: function (json) { if (this.isUndefined(json.Body)) return false; if (this.isUndefined(json.Body.Data)) return false; if (this.isUndefined(json.Body.Data.Site)) return false; return this.isDefined(json.Body.Data.Inverters); }, // module.serviceExecute() // this function is called only from this module // its purpose is to prepare the request and call // netdata.serviceExecute() serviceExecute: function (name, uri, update_every) { netdata.debug(this.name + ": " + name + ": url: " + uri + ", update_every: " + update_every); var service = netdata.service({ name: name, request: netdata.requestFromURL("http://" + uri), update_every: update_every, module: this }); service.execute(this.processResponse); }, configure: function (config) { if (fronius.isUndefined(config.servers)) return 0; var added = 0; var len = config.servers.length; while (len--) { var server = config.servers[len]; if (fronius.isUndefined(server.update_every)) server.update_every = this.update_every; if (fronius.areUndefined([server.name, server.hostname, server.api_path])) continue; var url = server.hostname + server.api_path; this.serviceExecute(server.name, url, server.update_every); added++; } return added; }, // module.update() // this is called repeatedly to collect data, by calling // netdata.serviceExecute() update: function (service, callback) { service.execute(function (serv, data) { service.module.processResponse(serv, data); callback(); }); }, isUndefined: function (value) { return typeof value === "undefined"; }, areUndefined: function (valueArray) { var i = 0; for (i; i < valueArray.length; i++) { if (this.isUndefined(valueArray[i])) return true; } return false; }, isDefined: function (value) { return typeof value !== "undefined"; } }; module.exports = fronius;