diff --git a/plugins/isp/internode_usage b/plugins/isp/internode_usage index 93f51054..1d9cf035 100755 --- a/plugins/isp/internode_usage +++ b/plugins/isp/internode_usage @@ -38,7 +38,7 @@ multiple times, by symlinking it as 'internode_usage_SERVICEID'. =head1 CACHING As the API is sometimes flakey, the initial service information is cached -locally, with a one-hour lifetime, before hitting the base API again. However, +locally, with a day's lifetime, before hitting the base API again. However, if hitting the API to refresh the cache fails, the stale cache is used anyway, to have a better chance of getting the data out nonetheless. diff --git a/plugins/solar/example_graphs/fronius-week.png b/plugins/solar/example_graphs/fronius-week.png new file mode 100644 index 00000000..1c56e079 Binary files /dev/null and b/plugins/solar/example_graphs/fronius-week.png differ diff --git a/plugins/solar/fronius b/plugins/solar/fronius new file mode 100755 index 00000000..c31cacce --- /dev/null +++ b/plugins/solar/fronius @@ -0,0 +1,288 @@ +#!/bin/sh +# -*- sh -*- + +: << =cut + +=head1 NAME + +fronius - Plugin to monitor Fronius Solar inverter using the JSON Solar API. + +The Solar API reports both an immediate power output reading at +time-of-request, and an incremental sum of daily and yearly produced energy. +This plugin uses the yearly energy sum as a DERIVE value, and calculates the +average power output during the last measurement interval. This will likely be +lower than the immediate reading, but the aggregation in weekly/monthly/yearly +graphs will be more correct. The immediate power output is output as extra +information. + +=head1 CONFIGURATION + + [fronius] + env.inverter_base_url http://fronius # this is the default + env.host_name solar_inverter # optional, host name to report data as in munin + env.connect_timeout 1 # optional, amount to wait for requests, in seconds + +=head1 CACHING + +As the inverter may go to sleep at night, the initial service information is cached +locally, with a twelve-hour lifetime, before hitting the Solar API again. However, +if hitting the API to refresh the cache fails, the stale cache is used anyway, +to have a better chance of getting the config data out nonetheless. + +=head1 CAVEAT + +Only tested on a Fronius Primo. + +=head1 AUTHOR + +Olivier Mehani + +Copyright (C) 2020 Olivier Mehani + +=head1 LICENSE + +SPDX-License-Identifier: GPL-3.0-or-later + +=head1 MAGIC MARKERS + + #%# family=manual + +=cut + +# Example outputs +# +## http://fronius/solar_api/v1/GetInverterInfo.cgi +#GetInverterInfo=' +#{ +# "Body" : { +# "Data" : { +# "1" : { +# "CustomName" : "Primo 5.0-1 (1)", +# "DT" : 76, +# "ErrorCode" : 0, +# "PVPower" : 5200, +# "Show" : 1, +# "StatusCode" : 7, +# "UniqueID" : "1098861" +# } +# } +# }, +# "Head" : { +# "RequestArguments" : {}, +# "Status" : { +# "Code" : 0, +# "Reason" : "", +# "UserMessage" : "" +# }, +# "Timestamp" : "2020-06-11T10:55:23+10:00" +# } +#} +#' +# +## http://fronius/solar_api/v1/GetPowerFlowRealtimeData.fcgi +#GetPowerFlowRealtimeData=' +#{ +# "Body" : { +# "Data" : { +# "Inverters" : { +# "1" : { +# "DT" : 76, +# "E_Day" : 1201, +# "E_Total" : 1201, +# "E_Year" : 1201.4000244140625, +# "P" : 2521 +# } +# }, +# "Site" : { +# "E_Day" : 1201, +# "E_Total" : 1201, +# "E_Year" : 1201.4000244140625, +# "Meter_Location" : "unknown", +# "Mode" : "produce-only", +# "P_Akku" : null, +# "P_Grid" : null, +# "P_Load" : null, +# "P_PV" : 2521, +# "rel_Autonomy" : null, +# "rel_SelfConsumption" : null +# }, +# "Version" : "12" +# } +# }, +# "Head" : { +# "RequestArguments" : {}, +# "Status" : { +# "Code" : 0, +# "Reason" : "", +# "UserMessage" : "" +# }, +# "Timestamp" : "2020-06-11T10:55:21+10:00" +# } +#} +#' +# +## http://fronius/solar_api/v1/GetActiveDeviceInfo.cgi?DeviceClass=SensorCard +#GetActiveDeviceInfo=' +#{ +# "Body" : { +# "Data" : {} +# }, +# "Head" : { +# "RequestArguments" : { +# "DeviceClass" : "SensorCard" +# }, +# "Status" : { +# "Code" : 0, +# "Reason" : "", +# "UserMessage" : "" +# }, +# "Timestamp" : "2020-06-11T10:55:24+10:00" +# } +#} +#' +# +## http://fronius/solar_api/v1/GetLoggerConnectionInfo.cgi +#GetLoggerConnectionInfo=' +#{ +# "Body" : { +# "Data" : { +# "SolarNetConnectionState" : 2, +# "SolarWebConnectionState" : 2, +# "WLANConnectionState" : 2 +# } +# }, +# "Head" : { +# "RequestArguments" : {}, +# "Status" : { +# "Code" : 0, +# "Reason" : "", +# "UserMessage" : "" +# }, +# "Timestamp" : "2020-06-11T10:55:25+10:00" +# } +#} +#' + +set -eu + +# shellcheck disable=SC1090 +. "${MUNIN_LIBDIR}/plugins/plugin.sh" + +if [ "${MUNIN_DEBUG:-0}" = 1 ]; then + set -x +fi + +INVERTER_BASE_URL=${inverter_base_url:-http://fronius} +HOST_NAME=${host_name:-} +CONNECT_TIMEOUT=${connect_timeout:-1} + +check_deps() { + for CMD in curl jq recode; do + if ! command -v "${CMD}" >/dev/null; then + echo "no (${CMD} not found)" + fi + done +} + +CURL_ARGS="-s --connect-timeout ${CONNECT_TIMEOUT}" +fetch() { + # shellcheck disable=SC2086 + curl -f ${CURL_ARGS} "$@" \ + || { echo "error fetching ${*}" >&2; false; } +} + +get_inverter_info() { + fetch "${INVERTER_BASE_URL}/solar_api/v1/GetInverterInfo.cgi" \ + | recode html..ascii +} + +get_power_flow_realtime_data() { + fetch "${INVERTER_BASE_URL}/solar_api/v1/GetPowerFlowRealtimeData.fcgi" + #echo "${GetPowerFlowRealtimeData} +} + +# Run the command and arguments passed as arguments to this method, and cache +# the response. The first argument is a timeout (in minutes) after which the +# cache is ignored and a new request is attempted. If the request fails, the +# cache is used. If the timeout is 0, the request is always attempted, using +# the cache as a backup on failure. +cached() { + timeout="${1}" + shift + fn="${1}" + shift + # shellcheck disable=SC2124 + args="${@}" + + # shellcheck disable=SC2039 + api_data='' + # shellcheck disable=SC2039 + cachefile="${MUNIN_PLUGSTATE}/$(basename "${0}").${fn}.cache.json" + if [ -n "$(find "${cachefile}" -mmin "-${timeout}" 2>/dev/null)" ]; then + api_data=$(cat "${cachefile}") + else + # shellcheck disable=SC2086 + api_data="$("${fn}" ${args} \ + || true)" + + if [ -n "${api_data}" ]; then + echo "${api_data}" > "${cachefile}" + else + api_data=$(cat "${cachefile}") + fi + fi + echo "${api_data}" +} + +config() { + if test -n "${HOST_NAME}"; then + echo "host_name ${HOST_NAME}" + fi + # graph_period is not a shell variable + cat <<'EOF' +graph_title Solar Inverter Output +graph_info Power generated from solar inverters +graph_total Total output +graph_category sensors +graph_vlabel Average output [W] +graph_args -l 0 --base 1000 +EOF +cached 720 get_inverter_info | jq -r '.Body.Data + | to_entries[] + | @text " +inverter\(.key).label \(.value.CustomName) +inverter\(.key).info Power generated by the solar array (total size \(.value.PVPower / 1000) kW) connected to inverter \(.value.CustomName) (ID: \(.value.UniqueID)) +inverter\(.key).cdef inverter\(.key),3600,* +inverter\(.key).type DERIVE +inverter\(.key).min 0 +inverter\(.key).max \(.value.PVPower) +inverter\(.key).draw AREASTACK +"' +} + +get_data() { +cached 0 get_power_flow_realtime_data | jq -r '.Body.Data.Inverters + | to_entries[] + | @text " +inverter\(.key).value \(.value.E_Year | round) +inverter\(.key).extinfo Immediate output: \(.value.P) W; Daily total: \(.value.E_Day | round) Wh; Yearly total: \(.value.E_Year / 1000 | round) kWh +"' +} + +main () { + check_deps + + case ${1:-} in + config) + config + if [ "${MUNIN_CAP_DIRTYCONFIG:-0}" = "1" ]; then + get_data + fi + ;; + *) + get_data + ;; + esac +} + +main "${1:-}"