Projet

Général

Profil

Paste
Télécharger au format
Statistiques
| Branche: | Révision:

root / plugins / solar / fronius @ 520c436c

Historique | Voir | Annoter | Télécharger (6,93 ko)

1
#!/bin/sh
2
# -*- sh -*-
3

    
4
: << =cut
5

    
6
=head1 NAME
7

    
8
fronius - Plugin to monitor Fronius Solar inverter using the JSON Solar API.
9

    
10
The Solar API reports both an immediate power output reading at
11
time-of-request, and an incremental sum of daily and yearly produced energy.
12
This plugin uses the yearly energy sum as a DERIVE value, and calculates the
13
average power output during the last measurement interval. This will likely be
14
lower than the immediate reading, but the aggregation in weekly/monthly/yearly
15
graphs will be more correct.  The immediate power output is output as extra
16
information.
17

    
18
=head1 CONFIGURATION
19

    
20
	[fronius]
21
	env.inverter_base_url http://fronius	# this is the default
22
	env.host_name solar_inverter		# optional, host name to report data as in munin
23
	env.connect_timeout 1			# optional, amount to wait for requests, in seconds
24

    
25
=head1 CACHING
26

    
27
As the inverter may go to sleep at night, the initial service information is cached
28
locally, with a twelve-hour lifetime, before hitting the Solar API again. However,
29
if hitting the API to refresh the cache fails, the stale cache is used anyway,
30
to have a better chance of getting the config data out nonetheless.
31

    
32
=head1 CAVEAT
33

    
34
Only tested on a Fronius Primo and Fronius Symo.
35

    
36
=head1 AUTHOR
37

    
38
Olivier Mehani
39

    
40
Copyright (C) 2020 Olivier Mehani <shtrom+munin@ssji.net>
41

    
42
=head1 LICENSE
43

    
44
SPDX-License-Identifier: GPL-3.0-or-later
45

    
46
=head1 MAGIC MARKERS
47

    
48
 #%# family=manual
49

    
50
=cut
51

    
52
# Example outputs
53
#
54
## http://fronius/solar_api/v1/GetInverterInfo.cgi
55
#GetInverterInfo='
56
#{
57
#   "Body" : {
58
#      "Data" : {
59
#         "1" : {
60
#            "CustomName" : "&#80;&#114;&#105;&#109;&#111;&#32;&#53;&#46;&#48;&#45;&#49;&#32;&#40;&#49;&#41;",
61
#            "DT" : 76,
62
#            "ErrorCode" : 0,
63
#            "PVPower" : 5200,
64
#            "Show" : 1,
65
#            "StatusCode" : 7,
66
#            "UniqueID" : "1098861"
67
#         }
68
#      }
69
#   },
70
#   "Head" : {
71
#      "RequestArguments" : {},
72
#      "Status" : {
73
#         "Code" : 0,
74
#         "Reason" : "",
75
#         "UserMessage" : ""
76
#      },
77
#      "Timestamp" : "2020-06-11T10:55:23+10:00"
78
#   }
79
#}
80
#'
81
#
82
## http://fronius/solar_api/v1/GetPowerFlowRealtimeData.fcgi
83
#GetPowerFlowRealtimeData='
84
#{
85
#   "Body" : {
86
#      "Data" : {
87
#         "Inverters" : {
88
#            "1" : {
89
#               "DT" : 76,
90
#               "E_Day" : 1201,
91
#               "E_Total" : 1201,
92
#               "E_Year" : 1201.4000244140625,
93
#               "P" : 2521
94
#            }
95
#         },
96
#         "Site" : {
97
#            "E_Day" : 1201,
98
#            "E_Total" : 1201,
99
#            "E_Year" : 1201.4000244140625,
100
#            "Meter_Location" : "unknown",
101
#            "Mode" : "produce-only",
102
#            "P_Akku" : null,
103
#            "P_Grid" : null,
104
#            "P_Load" : null,
105
#            "P_PV" : 2521,
106
#            "rel_Autonomy" : null,
107
#            "rel_SelfConsumption" : null
108
#         },
109
#         "Version" : "12"
110
#      }
111
#   },
112
#   "Head" : {
113
#      "RequestArguments" : {},
114
#      "Status" : {
115
#         "Code" : 0,
116
#         "Reason" : "",
117
#         "UserMessage" : ""
118
#      },
119
#      "Timestamp" : "2020-06-11T10:55:21+10:00"
120
#   }
121
#}
122
#'
123
#
124
## http://fronius/solar_api/v1/GetActiveDeviceInfo.cgi?DeviceClass=SensorCard
125
#GetActiveDeviceInfo='
126
#{
127
#   "Body" : {
128
#      "Data" : {}
129
#   },
130
#   "Head" : {
131
#      "RequestArguments" : {
132
#         "DeviceClass" : "SensorCard"
133
#      },
134
#      "Status" : {
135
#         "Code" : 0,
136
#         "Reason" : "",
137
#         "UserMessage" : ""
138
#      },
139
#      "Timestamp" : "2020-06-11T10:55:24+10:00"
140
#   }
141
#}
142
#'
143
#
144
## http://fronius/solar_api/v1/GetLoggerConnectionInfo.cgi
145
#GetLoggerConnectionInfo='
146
#{
147
#   "Body" : {
148
#      "Data" : {
149
#         "SolarNetConnectionState" : 2,
150
#         "SolarWebConnectionState" : 2,
151
#         "WLANConnectionState" : 2
152
#      }
153
#   },
154
#   "Head" : {
155
#      "RequestArguments" : {},
156
#      "Status" : {
157
#         "Code" : 0,
158
#         "Reason" : "",
159
#         "UserMessage" : ""
160
#      },
161
#      "Timestamp" : "2020-06-11T10:55:25+10:00"
162
#   }
163
#}
164
#'
165

    
166
set -eu
167

    
168
# shellcheck disable=SC1090
169
. "${MUNIN_LIBDIR}/plugins/plugin.sh"
170

    
171
if [ "${MUNIN_DEBUG:-0}" = 1 ]; then
172
    set -x
173
fi
174

    
175
INVERTER_BASE_URL=${inverter_base_url:-http://fronius}
176
HOST_NAME=${host_name:-}
177
CONNECT_TIMEOUT=${connect_timeout:-1}
178

    
179
check_deps() {
180
	for CMD in curl jq recode; do
181
		if ! command -v "${CMD}" >/dev/null; then
182
			echo "no (${CMD} not found)"
183
			exit 0
184
		fi
185
	done
186
}
187

    
188
CURL_ARGS="-s --connect-timeout ${CONNECT_TIMEOUT}"
189
fetch() {
190
	# shellcheck disable=SC2086
191
	curl -f ${CURL_ARGS} "$@" \
192
		|| { echo "error fetching ${*}" >&2; false; }
193
}
194

    
195
get_inverter_info() {
196
	fetch "${INVERTER_BASE_URL}/solar_api/v1/GetInverterInfo.cgi" \
197
		| recode html..ascii
198
}
199

    
200
get_power_flow_realtime_data() {
201
	fetch "${INVERTER_BASE_URL}/solar_api/v1/GetPowerFlowRealtimeData.fcgi"
202
	#echo "${GetPowerFlowRealtimeData}
203
}
204

    
205
# Run the command and arguments passed as arguments to this method, and cache
206
# the response.  The first argument is a timeout (in minutes) after which the
207
# cache is ignored and a new request is attempted.  If the request fails, the
208
# cache is used. If the timeout is 0, the request is always attempted, using
209
# the cache as a backup on failure.
210
cached() {
211
	timeout="${1}"
212
	shift
213
	fn="${1}"
214
	shift
215
	# shellcheck disable=SC2124
216
	args="${@}"
217

    
218
	# shellcheck disable=SC2039
219
	api_data=''
220
	# shellcheck disable=SC2039
221
	cachefile="${MUNIN_PLUGSTATE}/$(basename "${0}").${fn}.cache.json"
222
	if [ -n "$(find "${cachefile}" -mmin "-${timeout}" 2>/dev/null)" ]; then
223
		api_data=$(cat "${cachefile}")
224
	else
225
		# shellcheck disable=SC2086
226
		api_data="$("${fn}" ${args} \
227
			|| true)"
228

    
229
		if [ -n "${api_data}" ]; then
230
			echo "${api_data}" > "${cachefile}"
231
		else
232
			api_data=$(cat "${cachefile}")
233
		fi
234
	fi
235
	echo "${api_data}"
236
}
237

    
238
config() {
239
	if test -n "${HOST_NAME}"; then
240
		echo "host_name ${HOST_NAME}"
241
	fi
242
	# graph_period is not a shell variable
243
	cat <<'EOF'
244
graph_title Solar Inverter Output
245
graph_info Power generated from solar inverters
246
graph_total Total output
247
graph_category sensors
248
graph_vlabel Average output [W]
249
graph_args -l 0 --base 1000
250
EOF
251
cached 720 get_inverter_info | jq -r '.Body.Data
252
			   | to_entries[]
253
			   | @text "
254
inverter\(.key).label \(.value.CustomName)
255
inverter\(.key).info Power generated by the solar array (total size \(.value.PVPower / 1000) kW) connected to inverter \(.value.CustomName) (ID: \(.value.UniqueID))
256
inverter\(.key).cdef inverter\(.key),3600,*
257
inverter\(.key).type DERIVE
258
inverter\(.key).min 0
259
inverter\(.key).max \(.value.PVPower)
260
inverter\(.key).draw AREASTACK
261
"'
262
}
263

    
264
get_data() {
265
cached 0 get_power_flow_realtime_data | jq -r 'def roundit: . + 0.5 | floor;
266
			   .Body.Data.Inverters
267
			   | to_entries[]
268
			   | @text "
269
inverter\(.key).value \(.value.E_Year | roundit)
270
inverter\(.key).extinfo Immediate output: \(.value.P) W; Daily total: \(.value.E_Day | roundit) Wh; Yearly total: \(.value.E_Year / 1000 | roundit) kWh
271
"'
272
}
273

    
274
main () {
275
	check_deps
276

    
277
	case ${1:-} in
278
		config)
279
			config
280
			if [ "${MUNIN_CAP_DIRTYCONFIG:-0}" = "1" ]; then
281
				get_data
282
			fi
283
			;;
284
		*)
285
			get_data
286
			;;
287
	esac
288
}
289

    
290
main "${1:-}"