Skip to content

Commit a22a92a

Browse files
committed
Add more metrics from thermostats
* fritzbox_thermostat_battery_charge_level * fritzbox_thermostat_comfort * fritzbox_thermostat_goal * fritzbox_thermostat_saving * fritzbox_thermostat_window_open
1 parent ad85361 commit a22a92a

File tree

6 files changed

+445
-84
lines changed

6 files changed

+445
-84
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,27 @@ fritzbox_temperature{device_id="12345 0000000",device_name="HKR 1",device_type="
5656
# TYPE fritzbox_temperature_offset gauge
5757
fritzbox_temperature_offset{device_id="01111 0111111",device_name="Switch 1",device_type="FRITZ!DECT 200"} -1
5858
fritzbox_temperature_offset{device_id="12345 0000000",device_name="HKR 1",device_type="Comet DECT"} -0.5
59+
# HELP fritzbox_thermostat_battery_charge_level Battery charge level in percent
60+
# TYPE fritzbox_thermostat_battery_charge_level gauge
61+
fritzbox_thermostat_battery_charge_level{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 70
5962
# HELP fritzbox_thermostat_batterylow 0 if the battery is OK, 1 if it is running low on capacity (this seems to be very unreliable)
6063
# TYPE fritzbox_thermostat_batterylow gauge
6164
fritzbox_thermostat_batterylow{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 0
65+
# HELP fritzbox_thermostat_comfort Comfort temperature configured in units of 0.1 °C
66+
# TYPE fritzbox_thermostat_comfort gauge
67+
fritzbox_thermostat_comfort{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 19
6268
# HELP fritzbox_thermostat_errorcode Thermostat error code (0 = OK), see https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf
6369
# TYPE fritzbox_thermostat_errorcode gauge
6470
fritzbox_thermostat_errorcode{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 0
71+
# HELP fritzbox_thermostat_goal Desired temperature (user controlled) in units of 0.1 °C
72+
# TYPE fritzbox_thermostat_goal gauge
73+
fritzbox_thermostat_goal{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 17
74+
# HELP fritzbox_thermostat_saving Configured energy saving temperature in units of 0.1 °C
75+
# TYPE fritzbox_thermostat_saving gauge
76+
fritzbox_thermostat_saving{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 16
77+
# HELP fritzbox_thermostat_window_open 1 if detected an open window (usually turns off heating), 0 if not.
78+
# TYPE fritzbox_thermostat_window_open gauge
79+
fritzbox_thermostat_window_open{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 0
6580
```
6681

6782

collector.go

Lines changed: 119 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,43 @@ var (
1616
)
1717

1818
type fritzCollector struct {
19-
InfoDesc *prometheus.Desc
20-
PresentDesc *prometheus.Desc
21-
TemperatureDesc *prometheus.Desc
22-
TemperatureOffsetDesc *prometheus.Desc
23-
EnergyWhDesc *prometheus.Desc
24-
PowerWDesc *prometheus.Desc
25-
SwitchState *prometheus.Desc
26-
SwitchMode *prometheus.Desc
27-
SwitchBoxLock *prometheus.Desc
28-
SwitchDeviceLock *prometheus.Desc
29-
ThermostatBatteryLow *prometheus.Desc
30-
ThermostatErrorCode *prometheus.Desc
19+
Info *prometheus.Desc
20+
Present *prometheus.Desc
21+
Temperature *prometheus.Desc
22+
TemperatureOffset *prometheus.Desc
23+
EnergyWh *prometheus.Desc
24+
PowerW *prometheus.Desc
25+
SwitchState *prometheus.Desc
26+
SwitchMode *prometheus.Desc
27+
SwitchBoxLock *prometheus.Desc
28+
SwitchDeviceLock *prometheus.Desc
29+
ThermostatBatteryChargeLevel *prometheus.Desc
30+
ThermostatBatteryLow *prometheus.Desc
31+
ThermostatErrorCode *prometheus.Desc
32+
ThermostatTempComfort *prometheus.Desc
33+
ThermostatTempGoal *prometheus.Desc
34+
ThermostatTempSaving *prometheus.Desc
35+
ThermostatWindowOpen *prometheus.Desc
3136
}
3237

3338
func (fc *fritzCollector) Describe(ch chan<- *prometheus.Desc) {
34-
ch <- fc.InfoDesc
35-
ch <- fc.PresentDesc
36-
ch <- fc.TemperatureDesc
37-
ch <- fc.TemperatureOffsetDesc
38-
ch <- fc.EnergyWhDesc
39-
ch <- fc.PowerWDesc
39+
ch <- fc.Info
40+
ch <- fc.Present
41+
ch <- fc.Temperature
42+
ch <- fc.TemperatureOffset
43+
ch <- fc.EnergyWh
44+
ch <- fc.PowerW
4045
ch <- fc.SwitchState
4146
ch <- fc.SwitchMode
4247
ch <- fc.SwitchBoxLock
4348
ch <- fc.SwitchDeviceLock
49+
ch <- fc.ThermostatBatteryChargeLevel
4450
ch <- fc.ThermostatBatteryLow
4551
ch <- fc.ThermostatErrorCode
52+
ch <- fc.ThermostatTempComfort
53+
ch <- fc.ThermostatTempGoal
54+
ch <- fc.ThermostatTempSaving
55+
ch <- fc.ThermostatWindowOpen
4656
}
4757

4858
func (fc *fritzCollector) Collect(ch chan<- prometheus.Metric) {
@@ -51,24 +61,29 @@ func (fc *fritzCollector) Collect(ch chan<- prometheus.Metric) {
5161

5262
if err != nil {
5363
log.Println("Unable to collect data:", err)
54-
ch <- prometheus.NewInvalidMetric(fc.InfoDesc, err)
55-
ch <- prometheus.NewInvalidMetric(fc.PresentDesc, err)
56-
ch <- prometheus.NewInvalidMetric(fc.TemperatureDesc, err)
57-
ch <- prometheus.NewInvalidMetric(fc.TemperatureOffsetDesc, err)
58-
ch <- prometheus.NewInvalidMetric(fc.EnergyWhDesc, err)
59-
ch <- prometheus.NewInvalidMetric(fc.PowerWDesc, err)
64+
ch <- prometheus.NewInvalidMetric(fc.Info, err)
65+
ch <- prometheus.NewInvalidMetric(fc.Present, err)
66+
ch <- prometheus.NewInvalidMetric(fc.Temperature, err)
67+
ch <- prometheus.NewInvalidMetric(fc.TemperatureOffset, err)
68+
ch <- prometheus.NewInvalidMetric(fc.EnergyWh, err)
69+
ch <- prometheus.NewInvalidMetric(fc.PowerW, err)
6070
ch <- prometheus.NewInvalidMetric(fc.SwitchState, err)
6171
ch <- prometheus.NewInvalidMetric(fc.SwitchMode, err)
6272
ch <- prometheus.NewInvalidMetric(fc.SwitchBoxLock, err)
6373
ch <- prometheus.NewInvalidMetric(fc.SwitchDeviceLock, err)
74+
ch <- prometheus.NewInvalidMetric(fc.ThermostatBatteryChargeLevel, err)
6475
ch <- prometheus.NewInvalidMetric(fc.ThermostatBatteryLow, err)
6576
ch <- prometheus.NewInvalidMetric(fc.ThermostatErrorCode, err)
77+
ch <- prometheus.NewInvalidMetric(fc.ThermostatTempComfort, err)
78+
ch <- prometheus.NewInvalidMetric(fc.ThermostatTempGoal, err)
79+
ch <- prometheus.NewInvalidMetric(fc.ThermostatTempSaving, err)
80+
ch <- prometheus.NewInvalidMetric(fc.ThermostatWindowOpen, err)
6681
return
6782
}
6883

6984
for _, dev := range l.Devices {
7085
ch <- prometheus.MustNewConstMetric(
71-
fc.InfoDesc,
86+
fc.Info,
7287
prometheus.GaugeValue,
7388
1.0,
7489
dev.Identifier,
@@ -81,7 +96,7 @@ func (fc *fritzCollector) Collect(ch chan<- prometheus.Metric) {
8196
)
8297

8398
ch <- prometheus.MustNewConstMetric(
84-
fc.PresentDesc,
99+
fc.Present,
85100
prometheus.GaugeValue,
86101
float64(dev.Present),
87102
dev.Identifier,
@@ -90,59 +105,58 @@ func (fc *fritzCollector) Collect(ch chan<- prometheus.Metric) {
90105
)
91106

92107
if dev.Present == 1 && dev.CanMeasureTemp() {
93-
if err := stringToFloatMetric(ch, fc.TemperatureDesc, dev.Temperature.FmtCelsius(), &dev); err != nil {
108+
if err := mustStringToFloatMetric(ch, fc.Temperature, dev.Temperature.FmtCelsius(), &dev); err != nil {
94109
log.Printf("Unable to parse temperature data of \"%s\" : %v\n", dev.Name, err)
95110
}
96111

97-
if err := stringToFloatMetric(ch, fc.TemperatureOffsetDesc, dev.Temperature.FmtOffset(), &dev); err != nil {
112+
if err := mustStringToFloatMetric(ch, fc.TemperatureOffset, dev.Temperature.FmtOffset(), &dev); err != nil {
98113
log.Printf("Unable to parse temperature offset data of \"%s\" : %v\n", dev.Name, err)
99114
}
100115
}
101116

102117
if dev.Present == 1 && dev.CanMeasurePower() {
103-
if err := stringToFloatMetric(ch, fc.EnergyWhDesc, dev.Powermeter.FmtEnergyWh(), &dev); err != nil {
118+
if err := mustStringToFloatMetric(ch, fc.EnergyWh, dev.Powermeter.FmtEnergyWh(), &dev); err != nil {
104119
log.Printf("Unable to parse energy data of \"%s\" : %v\n", dev.Name, err)
105120
}
106121

107-
if err := stringToFloatMetric(ch, fc.PowerWDesc, dev.Powermeter.FmtPowerW(), &dev); err != nil {
122+
if err := mustStringToFloatMetric(ch, fc.PowerW, dev.Powermeter.FmtPowerW(), &dev); err != nil {
108123
log.Printf("Unable to parse power data of \"%s\" : %v\n", dev.Name, err)
109124
}
110125
}
111126

112127
if dev.IsThermostat() {
113-
if batteryLow, err := strconv.ParseFloat(dev.Thermostat.BatteryLow, 64); err != nil {
114-
ch <- prometheus.NewInvalidMetric(fc.ThermostatBatteryLow, err)
128+
// Battery charge level is optional
129+
if err := canStringToFloatMetric(ch, fc.ThermostatBatteryChargeLevel, dev.Thermostat.BatteryChargeLevel, &dev); err != nil {
130+
log.Printf("Unable to parse battery charge level of \"%s\" : %v\n", dev.Name, err)
131+
}
132+
133+
if err := mustStringToFloatMetric(ch, fc.ThermostatBatteryLow, dev.Thermostat.BatteryLow, &dev); err != nil {
115134
log.Printf("Unable to parse battery low state of \"%s\" : %v\n", dev.Name, err)
116-
} else {
117-
ch <- prometheus.MustNewConstMetric(
118-
fc.ThermostatBatteryLow,
119-
prometheus.GaugeValue,
120-
batteryLow,
121-
dev.Identifier,
122-
dev.Productname,
123-
dev.Name,
124-
)
125135
}
126136

127-
var errCode float64
128-
// Reset err so it can be used later to decide if we need to send the ThermostatErrCode metric
129-
err = nil
130-
if dev.Thermostat.ErrorCode != "" {
131-
errCode, err = strconv.ParseFloat(dev.Thermostat.ErrorCode, 64)
132-
if err != nil {
133-
ch <- prometheus.NewInvalidMetric(fc.ThermostatErrorCode, err)
134-
log.Printf("Unable to parse thermostat error code of \"%s\" : %v\n", dev.Name, err)
135-
}
137+
// Handle no error like error code 0
138+
errCodeStr := dev.Thermostat.ErrorCode
139+
if errCodeStr == "" {
140+
errCodeStr = "0"
141+
}
142+
if err := mustStringToFloatMetric(ch, fc.ThermostatErrorCode, errCodeStr, &dev); err != nil {
143+
log.Printf("Unable to parse thermostat error code of \"%s\" : %v\n", dev.Name, err)
144+
}
145+
146+
// Comfort, Goal and Saving temperature are optional
147+
if err := canStringToFloatMetric(ch, fc.ThermostatTempComfort, dev.Thermostat.FmtComfortTemperature(), &dev); err != nil {
148+
log.Printf("Unable to parse comfort temperature of \"%s\" : %v\n", dev.Name, err)
149+
}
150+
if err := canStringToFloatMetric(ch, fc.ThermostatTempGoal, dev.Thermostat.FmtGoalTemperature(), &dev); err != nil {
151+
log.Printf("Unable to parse goal temperature of \"%s\" : %v\n", dev.Name, err)
152+
}
153+
if err := canStringToFloatMetric(ch, fc.ThermostatTempSaving, dev.Thermostat.FmtSavingTemperature(), &dev); err != nil {
154+
log.Printf("Unable to parse saving temperature of \"%s\" : %v\n", dev.Name, err)
136155
}
137-
if err == nil {
138-
ch <- prometheus.MustNewConstMetric(
139-
fc.ThermostatErrorCode,
140-
prometheus.GaugeValue,
141-
errCode,
142-
dev.Identifier,
143-
dev.Productname,
144-
dev.Name,
145-
)
156+
157+
// Window Open is optional
158+
if err := canStringToFloatMetric(ch, fc.ThermostatWindowOpen, dev.Thermostat.WindowOpen, &dev); err != nil {
159+
log.Printf("Unable to parse window open state of \"%s\" : %v\n", dev.Name, err)
146160
}
147161
}
148162

@@ -165,39 +179,39 @@ func (fc *fritzCollector) Collect(ch chan<- prometheus.Metric) {
165179

166180
func NewFritzCollector() *fritzCollector {
167181
return &fritzCollector{
168-
InfoDesc: prometheus.NewDesc(
182+
Info: prometheus.NewDesc(
169183
"fritzbox_device_info",
170184
"Device information",
171185
append(genericLabels,
172186
"internal_id", "fw_version", "manufacturer", "functionbitmask",
173187
),
174188
prometheus.Labels{},
175189
),
176-
PresentDesc: prometheus.NewDesc(
190+
Present: prometheus.NewDesc(
177191
"fritzbox_device_present",
178192
"Device connected (1) or not (0)",
179193
genericLabels,
180194
prometheus.Labels{},
181195
),
182-
TemperatureDesc: prometheus.NewDesc(
196+
Temperature: prometheus.NewDesc(
183197
"fritzbox_temperature",
184198
"Temperature measured at the device sensor in units of 0.1 °C",
185199
genericLabels,
186200
prometheus.Labels{},
187201
),
188-
TemperatureOffsetDesc: prometheus.NewDesc(
202+
TemperatureOffset: prometheus.NewDesc(
189203
"fritzbox_temperature_offset",
190204
"Temperature offset (set by the user) in units of 0.1 °C",
191205
genericLabels,
192206
prometheus.Labels{},
193207
),
194-
EnergyWhDesc: prometheus.NewDesc(
208+
EnergyWh: prometheus.NewDesc(
195209
"fritzbox_energy",
196210
"Absolute energy consumption (in Wh) since the device started operating",
197211
genericLabels,
198212
prometheus.Labels{},
199213
),
200-
PowerWDesc: prometheus.NewDesc(
214+
PowerW: prometheus.NewDesc(
201215
"fritzbox_power",
202216
"Current power (in W), refreshed approx every 2 minutes",
203217
genericLabels,
@@ -227,6 +241,12 @@ func NewFritzCollector() *fritzCollector {
227241
genericLabels,
228242
prometheus.Labels{},
229243
),
244+
ThermostatBatteryChargeLevel: prometheus.NewDesc(
245+
"fritzbox_thermostat_battery_charge_level",
246+
"Battery charge level in percent",
247+
genericLabels,
248+
prometheus.Labels{},
249+
),
230250
ThermostatBatteryLow: prometheus.NewDesc(
231251
"fritzbox_thermostat_batterylow",
232252
"0 if the battery is OK, 1 if it is running low on capacity (this seems to be very unreliable)",
@@ -239,14 +259,40 @@ func NewFritzCollector() *fritzCollector {
239259
genericLabels,
240260
prometheus.Labels{},
241261
),
262+
ThermostatTempComfort: prometheus.NewDesc(
263+
"fritzbox_thermostat_comfort",
264+
"Comfort temperature configured in units of 0.1 °C",
265+
genericLabels,
266+
prometheus.Labels{},
267+
),
268+
ThermostatTempGoal: prometheus.NewDesc(
269+
"fritzbox_thermostat_goal",
270+
"Desired temperature (user controlled) in units of 0.1 °C",
271+
genericLabels,
272+
prometheus.Labels{},
273+
),
274+
ThermostatTempSaving: prometheus.NewDesc(
275+
"fritzbox_thermostat_saving",
276+
"Configured energy saving temperature in units of 0.1 °C",
277+
genericLabels,
278+
prometheus.Labels{},
279+
),
280+
ThermostatWindowOpen: prometheus.NewDesc(
281+
"fritzbox_thermostat_window_open",
282+
"1 if detected an open window (usually turns off heating), 0 if not.",
283+
genericLabels,
284+
prometheus.Labels{},
285+
),
242286
}
243287
}
244288

245289
// stringToFloatMetric converts a string `val` into a valid float metric
246-
func stringToFloatMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, value string, dev *fritz.Device) error {
290+
func stringToFloatMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, value string, dev *fritz.Device, optional bool) error {
247291
val, err := strconv.ParseFloat(value, 64)
248292
if err != nil {
249-
ch <- prometheus.NewInvalidMetric(desc, err)
293+
if !optional {
294+
ch <- prometheus.NewInvalidMetric(desc, err)
295+
}
250296
return err
251297
}
252298
ch <- prometheus.MustNewConstMetric(
@@ -259,6 +305,12 @@ func stringToFloatMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, val
259305
)
260306
return nil
261307
}
308+
func canStringToFloatMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, value string, dev *fritz.Device) error {
309+
return stringToFloatMetric(ch, desc, value, dev, true)
310+
}
311+
func mustStringToFloatMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, value string, dev *fritz.Device) error {
312+
return stringToFloatMetric(ch, desc, value, dev, false)
313+
}
262314

263315
// parseSwitchStrings parses state strings of switches into floats
264316
func parseSwitchStrings(val string) (float64, error) {

0 commit comments

Comments
 (0)