mirror of
https://github.com/munin-monitoring/contrib.git
synced 2025-07-21 18:41:03 +00:00
This includes caching of the inverter info with a 12h TTL, so the graph still renders when the inverter has gone to sleep at night. Signed-off-by: Olivier Mehani <shtrom@ssji.net> squash! [plugins/solar/fronius] Monitor Solar Inverters using the Fronius Solar API Use generic caching function to also cache real-time data. This allows to avoid N/As overnight, which don't aggregate nicely on the yearly graph.
288 lines
6.9 KiB
Bash
Executable file
288 lines
6.9 KiB
Bash
Executable file
#!/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 <shtrom+munin@ssji.net>
|
|
|
|
=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:-}"
|