Skip to content

Commit

Permalink
Add more metrics from thermostats
Browse files Browse the repository at this point in the history
* fritzbox_thermostat_battery_charge_level
* fritzbox_thermostat_comfort
* fritzbox_thermostat_goal
* fritzbox_thermostat_saving
* fritzbox_thermostat_window_open
  • Loading branch information
jayme-github committed Dec 26, 2021
1 parent ad85361 commit a22a92a
Show file tree
Hide file tree
Showing 6 changed files with 445 additions and 84 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,27 @@ fritzbox_temperature{device_id="12345 0000000",device_name="HKR 1",device_type="
# TYPE fritzbox_temperature_offset gauge
fritzbox_temperature_offset{device_id="01111 0111111",device_name="Switch 1",device_type="FRITZ!DECT 200"} -1
fritzbox_temperature_offset{device_id="12345 0000000",device_name="HKR 1",device_type="Comet DECT"} -0.5
# HELP fritzbox_thermostat_battery_charge_level Battery charge level in percent
# TYPE fritzbox_thermostat_battery_charge_level gauge
fritzbox_thermostat_battery_charge_level{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 70
# HELP fritzbox_thermostat_batterylow 0 if the battery is OK, 1 if it is running low on capacity (this seems to be very unreliable)
# TYPE fritzbox_thermostat_batterylow gauge
fritzbox_thermostat_batterylow{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 0
# HELP fritzbox_thermostat_comfort Comfort temperature configured in units of 0.1 °C
# TYPE fritzbox_thermostat_comfort gauge
fritzbox_thermostat_comfort{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 19
# HELP fritzbox_thermostat_errorcode Thermostat error code (0 = OK), see https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf
# TYPE fritzbox_thermostat_errorcode gauge
fritzbox_thermostat_errorcode{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 0
# HELP fritzbox_thermostat_goal Desired temperature (user controlled) in units of 0.1 °C
# TYPE fritzbox_thermostat_goal gauge
fritzbox_thermostat_goal{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 17
# HELP fritzbox_thermostat_saving Configured energy saving temperature in units of 0.1 °C
# TYPE fritzbox_thermostat_saving gauge
fritzbox_thermostat_saving{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 16
# HELP fritzbox_thermostat_window_open 1 if detected an open window (usually turns off heating), 0 if not.
# TYPE fritzbox_thermostat_window_open gauge
fritzbox_thermostat_window_open{device_id="44363 2777777",device_name="HKR_1",device_type="Comet DECT"} 0
```


Expand Down
186 changes: 119 additions & 67 deletions collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,43 @@ var (
)

type fritzCollector struct {
InfoDesc *prometheus.Desc
PresentDesc *prometheus.Desc
TemperatureDesc *prometheus.Desc
TemperatureOffsetDesc *prometheus.Desc
EnergyWhDesc *prometheus.Desc
PowerWDesc *prometheus.Desc
SwitchState *prometheus.Desc
SwitchMode *prometheus.Desc
SwitchBoxLock *prometheus.Desc
SwitchDeviceLock *prometheus.Desc
ThermostatBatteryLow *prometheus.Desc
ThermostatErrorCode *prometheus.Desc
Info *prometheus.Desc
Present *prometheus.Desc
Temperature *prometheus.Desc
TemperatureOffset *prometheus.Desc
EnergyWh *prometheus.Desc
PowerW *prometheus.Desc
SwitchState *prometheus.Desc
SwitchMode *prometheus.Desc
SwitchBoxLock *prometheus.Desc
SwitchDeviceLock *prometheus.Desc
ThermostatBatteryChargeLevel *prometheus.Desc
ThermostatBatteryLow *prometheus.Desc
ThermostatErrorCode *prometheus.Desc
ThermostatTempComfort *prometheus.Desc
ThermostatTempGoal *prometheus.Desc
ThermostatTempSaving *prometheus.Desc
ThermostatWindowOpen *prometheus.Desc
}

func (fc *fritzCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- fc.InfoDesc
ch <- fc.PresentDesc
ch <- fc.TemperatureDesc
ch <- fc.TemperatureOffsetDesc
ch <- fc.EnergyWhDesc
ch <- fc.PowerWDesc
ch <- fc.Info
ch <- fc.Present
ch <- fc.Temperature
ch <- fc.TemperatureOffset
ch <- fc.EnergyWh
ch <- fc.PowerW
ch <- fc.SwitchState
ch <- fc.SwitchMode
ch <- fc.SwitchBoxLock
ch <- fc.SwitchDeviceLock
ch <- fc.ThermostatBatteryChargeLevel
ch <- fc.ThermostatBatteryLow
ch <- fc.ThermostatErrorCode
ch <- fc.ThermostatTempComfort
ch <- fc.ThermostatTempGoal
ch <- fc.ThermostatTempSaving
ch <- fc.ThermostatWindowOpen
}

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

if err != nil {
log.Println("Unable to collect data:", err)
ch <- prometheus.NewInvalidMetric(fc.InfoDesc, err)
ch <- prometheus.NewInvalidMetric(fc.PresentDesc, err)
ch <- prometheus.NewInvalidMetric(fc.TemperatureDesc, err)
ch <- prometheus.NewInvalidMetric(fc.TemperatureOffsetDesc, err)
ch <- prometheus.NewInvalidMetric(fc.EnergyWhDesc, err)
ch <- prometheus.NewInvalidMetric(fc.PowerWDesc, err)
ch <- prometheus.NewInvalidMetric(fc.Info, err)
ch <- prometheus.NewInvalidMetric(fc.Present, err)
ch <- prometheus.NewInvalidMetric(fc.Temperature, err)
ch <- prometheus.NewInvalidMetric(fc.TemperatureOffset, err)
ch <- prometheus.NewInvalidMetric(fc.EnergyWh, err)
ch <- prometheus.NewInvalidMetric(fc.PowerW, err)
ch <- prometheus.NewInvalidMetric(fc.SwitchState, err)
ch <- prometheus.NewInvalidMetric(fc.SwitchMode, err)
ch <- prometheus.NewInvalidMetric(fc.SwitchBoxLock, err)
ch <- prometheus.NewInvalidMetric(fc.SwitchDeviceLock, err)
ch <- prometheus.NewInvalidMetric(fc.ThermostatBatteryChargeLevel, err)
ch <- prometheus.NewInvalidMetric(fc.ThermostatBatteryLow, err)
ch <- prometheus.NewInvalidMetric(fc.ThermostatErrorCode, err)
ch <- prometheus.NewInvalidMetric(fc.ThermostatTempComfort, err)
ch <- prometheus.NewInvalidMetric(fc.ThermostatTempGoal, err)
ch <- prometheus.NewInvalidMetric(fc.ThermostatTempSaving, err)
ch <- prometheus.NewInvalidMetric(fc.ThermostatWindowOpen, err)
return
}

for _, dev := range l.Devices {
ch <- prometheus.MustNewConstMetric(
fc.InfoDesc,
fc.Info,
prometheus.GaugeValue,
1.0,
dev.Identifier,
Expand All @@ -81,7 +96,7 @@ func (fc *fritzCollector) Collect(ch chan<- prometheus.Metric) {
)

ch <- prometheus.MustNewConstMetric(
fc.PresentDesc,
fc.Present,
prometheus.GaugeValue,
float64(dev.Present),
dev.Identifier,
Expand All @@ -90,59 +105,58 @@ func (fc *fritzCollector) Collect(ch chan<- prometheus.Metric) {
)

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

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

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

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

if dev.IsThermostat() {
if batteryLow, err := strconv.ParseFloat(dev.Thermostat.BatteryLow, 64); err != nil {
ch <- prometheus.NewInvalidMetric(fc.ThermostatBatteryLow, err)
// Battery charge level is optional
if err := canStringToFloatMetric(ch, fc.ThermostatBatteryChargeLevel, dev.Thermostat.BatteryChargeLevel, &dev); err != nil {
log.Printf("Unable to parse battery charge level of \"%s\" : %v\n", dev.Name, err)
}

if err := mustStringToFloatMetric(ch, fc.ThermostatBatteryLow, dev.Thermostat.BatteryLow, &dev); err != nil {
log.Printf("Unable to parse battery low state of \"%s\" : %v\n", dev.Name, err)
} else {
ch <- prometheus.MustNewConstMetric(
fc.ThermostatBatteryLow,
prometheus.GaugeValue,
batteryLow,
dev.Identifier,
dev.Productname,
dev.Name,
)
}

var errCode float64
// Reset err so it can be used later to decide if we need to send the ThermostatErrCode metric
err = nil
if dev.Thermostat.ErrorCode != "" {
errCode, err = strconv.ParseFloat(dev.Thermostat.ErrorCode, 64)
if err != nil {
ch <- prometheus.NewInvalidMetric(fc.ThermostatErrorCode, err)
log.Printf("Unable to parse thermostat error code of \"%s\" : %v\n", dev.Name, err)
}
// Handle no error like error code 0
errCodeStr := dev.Thermostat.ErrorCode
if errCodeStr == "" {
errCodeStr = "0"
}
if err := mustStringToFloatMetric(ch, fc.ThermostatErrorCode, errCodeStr, &dev); err != nil {
log.Printf("Unable to parse thermostat error code of \"%s\" : %v\n", dev.Name, err)
}

// Comfort, Goal and Saving temperature are optional
if err := canStringToFloatMetric(ch, fc.ThermostatTempComfort, dev.Thermostat.FmtComfortTemperature(), &dev); err != nil {
log.Printf("Unable to parse comfort temperature of \"%s\" : %v\n", dev.Name, err)
}
if err := canStringToFloatMetric(ch, fc.ThermostatTempGoal, dev.Thermostat.FmtGoalTemperature(), &dev); err != nil {
log.Printf("Unable to parse goal temperature of \"%s\" : %v\n", dev.Name, err)
}
if err := canStringToFloatMetric(ch, fc.ThermostatTempSaving, dev.Thermostat.FmtSavingTemperature(), &dev); err != nil {
log.Printf("Unable to parse saving temperature of \"%s\" : %v\n", dev.Name, err)
}
if err == nil {
ch <- prometheus.MustNewConstMetric(
fc.ThermostatErrorCode,
prometheus.GaugeValue,
errCode,
dev.Identifier,
dev.Productname,
dev.Name,
)

// Window Open is optional
if err := canStringToFloatMetric(ch, fc.ThermostatWindowOpen, dev.Thermostat.WindowOpen, &dev); err != nil {
log.Printf("Unable to parse window open state of \"%s\" : %v\n", dev.Name, err)
}
}

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

func NewFritzCollector() *fritzCollector {
return &fritzCollector{
InfoDesc: prometheus.NewDesc(
Info: prometheus.NewDesc(
"fritzbox_device_info",
"Device information",
append(genericLabels,
"internal_id", "fw_version", "manufacturer", "functionbitmask",
),
prometheus.Labels{},
),
PresentDesc: prometheus.NewDesc(
Present: prometheus.NewDesc(
"fritzbox_device_present",
"Device connected (1) or not (0)",
genericLabels,
prometheus.Labels{},
),
TemperatureDesc: prometheus.NewDesc(
Temperature: prometheus.NewDesc(
"fritzbox_temperature",
"Temperature measured at the device sensor in units of 0.1 °C",
genericLabels,
prometheus.Labels{},
),
TemperatureOffsetDesc: prometheus.NewDesc(
TemperatureOffset: prometheus.NewDesc(
"fritzbox_temperature_offset",
"Temperature offset (set by the user) in units of 0.1 °C",
genericLabels,
prometheus.Labels{},
),
EnergyWhDesc: prometheus.NewDesc(
EnergyWh: prometheus.NewDesc(
"fritzbox_energy",
"Absolute energy consumption (in Wh) since the device started operating",
genericLabels,
prometheus.Labels{},
),
PowerWDesc: prometheus.NewDesc(
PowerW: prometheus.NewDesc(
"fritzbox_power",
"Current power (in W), refreshed approx every 2 minutes",
genericLabels,
Expand Down Expand Up @@ -227,6 +241,12 @@ func NewFritzCollector() *fritzCollector {
genericLabels,
prometheus.Labels{},
),
ThermostatBatteryChargeLevel: prometheus.NewDesc(
"fritzbox_thermostat_battery_charge_level",
"Battery charge level in percent",
genericLabels,
prometheus.Labels{},
),
ThermostatBatteryLow: prometheus.NewDesc(
"fritzbox_thermostat_batterylow",
"0 if the battery is OK, 1 if it is running low on capacity (this seems to be very unreliable)",
Expand All @@ -239,14 +259,40 @@ func NewFritzCollector() *fritzCollector {
genericLabels,
prometheus.Labels{},
),
ThermostatTempComfort: prometheus.NewDesc(
"fritzbox_thermostat_comfort",
"Comfort temperature configured in units of 0.1 °C",
genericLabels,
prometheus.Labels{},
),
ThermostatTempGoal: prometheus.NewDesc(
"fritzbox_thermostat_goal",
"Desired temperature (user controlled) in units of 0.1 °C",
genericLabels,
prometheus.Labels{},
),
ThermostatTempSaving: prometheus.NewDesc(
"fritzbox_thermostat_saving",
"Configured energy saving temperature in units of 0.1 °C",
genericLabels,
prometheus.Labels{},
),
ThermostatWindowOpen: prometheus.NewDesc(
"fritzbox_thermostat_window_open",
"1 if detected an open window (usually turns off heating), 0 if not.",
genericLabels,
prometheus.Labels{},
),
}
}

// stringToFloatMetric converts a string `val` into a valid float metric
func stringToFloatMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, value string, dev *fritz.Device) error {
func stringToFloatMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, value string, dev *fritz.Device, optional bool) error {
val, err := strconv.ParseFloat(value, 64)
if err != nil {
ch <- prometheus.NewInvalidMetric(desc, err)
if !optional {
ch <- prometheus.NewInvalidMetric(desc, err)
}
return err
}
ch <- prometheus.MustNewConstMetric(
Expand All @@ -259,6 +305,12 @@ func stringToFloatMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, val
)
return nil
}
func canStringToFloatMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, value string, dev *fritz.Device) error {
return stringToFloatMetric(ch, desc, value, dev, true)
}
func mustStringToFloatMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, value string, dev *fritz.Device) error {
return stringToFloatMetric(ch, desc, value, dev, false)
}

// parseSwitchStrings parses state strings of switches into floats
func parseSwitchStrings(val string) (float64, error) {
Expand Down
Loading

0 comments on commit a22a92a

Please sign in to comment.