diff --git a/cmd/list_smarthome.go b/cmd/list_smarthome.go new file mode 100644 index 0000000..b6315c1 --- /dev/null +++ b/cmd/list_smarthome.go @@ -0,0 +1,115 @@ +package cmd + +import ( + "github.com/bpicode/fritzctl/cmd/printer" + "github.com/bpicode/fritzctl/fritz" + "github.com/bpicode/fritzctl/internal/console" + "github.com/bpicode/fritzctl/logger" + "github.com/spf13/cobra" + "os" +) + +var listSmarthomeCmd = &cobra.Command{ + Use: "smarthome", + Short: "List the available smart home devices", + Long: "List the available smart home devices and associated data.", + Example: `fritzctl list smarthome +fritzctl list smarthome --output=json`, + RunE: listSmarthome, +} + +func init() { + listSmarthomeCmd.Flags().StringP("output", "o", "", "specify output format") + listCmd.AddCommand(listSmarthomeCmd) +} + +func listSmarthome(cmd *cobra.Command, _ []string) error { + devs := mustList() + data := selectFmt(cmd, devs.Smarthome(), smarthomeTable) + logger.Success("Device data:") + printer.Print(data, os.Stdout) + return nil +} + +func smarthomeTable(devs []fritz.Device) interface{} { + table := console.NewTable(console.Headers( + "NAME", + "PRODUCT", + "PRESENT", + "LOCK (BOX/DEV)", + "MEASURED", + "OFFSET", + "WANT", + "SAVING", + "COMFORT", + "NEXT", + "HUMIDITY", + "STATE", + "BATTERY", + )) + appendSmarthome(devs, table) + return table +} + +func appendSmarthome(devs []fritz.Device, table *console.Table) { + for _, dev := range devs { + columns := smarthomeColumns(dev) + table.Append(columns) + } +} + +func smarthomeColumns(dev fritz.Device) []string { + var columnValues []string + columnValues = appendMetadata(columnValues, dev) + columnValues = appendSmarthomeRuntimeFlags(columnValues, dev) + columnValues = appendSmarthomeTemperatureValues(columnValues, dev) + columnValues = appendSmarthomeHumidityValues(columnValues, dev) + columnValues = appendSmarthomeRuntimeWarnings(columnValues, dev) + return columnValues +} + +func appendSmarthomeRuntimeFlags(cols []string, dev fritz.Device) []string { + if dev.IsThermostat() { + return append(cols, + console.IntToCheckmark(dev.Present), + console.StringToCheckmark(dev.Thermostat.Lock)+"/"+console.StringToCheckmark(dev.Thermostat.DeviceLock)) + } else { + return append(cols, + console.IntToCheckmark(dev.Present), "") + } +} + +func appendSmarthomeRuntimeWarnings(cols []string, dev fritz.Device) []string { + if dev.IsThermostat() { + return append(cols, errorCode(dev.Thermostat.ErrorCode), batteryState(dev.Thermostat)) + } else { + return append(cols, "", "") + } +} + +func appendSmarthomeHumidityValues(cols []string, dev fritz.Device) []string { + if dev.CanMeasureHumidity() { + return append(cols, fmtUnit(dev.Humidity.FmtRelativeHumidity, "%")) + } else { + return append(cols, "") + } +} + +func appendSmarthomeTemperatureValues(cols []string, dev fritz.Device) []string { + var measured func() string + var nextChange string + if dev.IsThermostat() { + measured = dev.Thermostat.FmtMeasuredTemperature + nextChange = fmtNextChange(dev.Thermostat.NextChange) + } else { + measured = dev.Temperature.FmtCelsius + } + + return append(cols, + fmtUnit(measured, "°C"), + fmtUnit(dev.Temperature.FmtOffset, "°C"), + fmtUnit(dev.Thermostat.FmtGoalTemperature, "°C"), + fmtUnit(dev.Thermostat.FmtSavingTemperature, "°C"), + fmtUnit(dev.Thermostat.FmtComfortTemperature, "°C"), + nextChange) +} diff --git a/fritz/device.go b/fritz/device.go index 818b4a8..4a68251 100644 --- a/fritz/device.go +++ b/fritz/device.go @@ -4,13 +4,14 @@ package fritz type Capability int // Known (specified) device capabilities. +// see https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf section 3.2 for full list const ( HANFUNCompatibility Capability = iota _ _ _ AlertTrigger - _ + AVMButton HeatControl PowerSensor TemperatureSensor @@ -19,6 +20,13 @@ const ( Microphone _ HANFUNUnit + _ + SwitchableDevice + DimmableDevice + ColorSettableDevice + _ + _ + HumiditySensor ) // Device models a smart home device. This corresponds to @@ -36,6 +44,7 @@ type Device struct { Switch Switch `xml:"switch"` // Only filled with sensible data for switch devices. Powermeter Powermeter `xml:"powermeter"` // Only filled with sensible data for devices with an energy actuator. Temperature Temperature `xml:"temperature"` // Only filled with sensible data for devices with a temperature sensor. + Humidity Humidity `xml:"humidity"` // Only filled with sensible data for devices with a humidity sensor. Thermostat Thermostat `xml:"hkr"` // Thermostat data, only filled with sensible data for HKR devices. AlertSensor AlertSensor `xml:"alert"` // Only filled with sensible data for devices with an alert sensor. Button Button `xml:"button"` // Button data, only filled with sensible data for button devices. @@ -53,6 +62,11 @@ func (d *Device) HasAlertSensor() bool { return d.Has(AlertTrigger) } +// IsAVMButton returns true if the device is an AVM button like the FRITZ!DECT 440 and returns false otherwise. +func (d *Device) IsAVMButton() bool { + return d.Has(AVMButton) +} + // IsThermostat returns true if the device is recognized to be a HKR device and returns false otherwise. func (d *Device) IsThermostat() bool { return d.Has(HeatControl) @@ -88,6 +102,26 @@ func (d *Device) HasHANFUNUnit() bool { return d.Has(HANFUNUnit) } +// IsSwitchableDevice returns true if the device is a switchable device/power plug/actor. +func (d *Device) IsSwitchableDevice() bool { + return d.Has(SwitchableDevice) +} + +// CanBeDimmed returns true if the device can be dimmed somehow (e.g. light intensity, height level, etc.). +func (d *Device) CanBeDimmed() bool { + return d.Has(DimmableDevice) +} + +// CanSetColors returns true if the device can set colors. +func (d *Device) CanSetColors() bool { + return d.Has(ColorSettableDevice) +} + +// CanMeasureHumidity returns true if the device has humidity functionality. Returns false otherwise. +func (d *Device) CanMeasureHumidity() bool { + return d.Has(HumiditySensor) +} + // Has checks the passed capabilities and returns true iff the device supports all capabilities. func (d *Device) Has(cs ...Capability) bool { for _, c := range cs { diff --git a/fritz/device_test.go b/fritz/device_test.go index 8a16894..e431847 100644 --- a/fritz/device_test.go +++ b/fritz/device_test.go @@ -31,6 +31,8 @@ func TestParsingFunctionBitMask(t *testing.T) { {name: "320 has no microphone", mask: "320", fct: (*Device).HasMicrophone, expect: false}, {name: "320 has no hanfun unit", mask: "320", fct: (*Device).HasHANFUNUnit, expect: false}, {name: "320 does not speak hanfun protocol", mask: "320", fct: (*Device).IsHANFUNCompatible, expect: false}, + {name: "1048864 is an AVM button", mask: "1048864", fct: (*Device).IsAVMButton, expect: true}, + {name: "1048864 can measure humidity", mask: "1048864", fct: (*Device).CanMeasureHumidity, expect: true}, } { t.Run(tc.name, func(t *testing.T) { device := &Device{Functionbitmask: tc.mask} diff --git a/fritz/devicelist.go b/fritz/devicelist.go index 05a276c..1ceea83 100644 --- a/fritz/devicelist.go +++ b/fritz/devicelist.go @@ -29,6 +29,13 @@ func (l *Devicelist) Thermostats() []Device { }) } +// Smarthome returns the devices which satisfy any of IsThermostat, CanMeasureTemp, CanMeasureHumidity. +func (l *Devicelist) Smarthome() []Device { + return l.filter(func(d Device) bool { + return d.IsThermostat() || d.CanMeasureTemp() || d.CanMeasureHumidity() + }) +} + // AlertSensors returns the devices which satisfy HasAlertSensor. func (l *Devicelist) AlertSensors() []Device { return l.filter(func(d Device) bool { diff --git a/fritz/humidity.go b/fritz/humidity.go new file mode 100644 index 0000000..1cdf2f3 --- /dev/null +++ b/fritz/humidity.go @@ -0,0 +1,11 @@ +package fritz + +// Humidity models a humidity measurement. +type Humidity struct { + RelHumidity string `xml:"rel_humidity"` // Relative humidity measured as full percentile. +} + +// FmtRelativeHumidity formats the value of p.RelHumidity as obtained on the http interface as a string, units are percentile. +func (p *Humidity) FmtRelativeHumidity() string { + return p.RelHumidity +} diff --git a/fritz/humidity_test.go b/fritz/humidity_test.go new file mode 100644 index 0000000..1cca606 --- /dev/null +++ b/fritz/humidity_test.go @@ -0,0 +1,11 @@ +package fritz + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFormattingOfRelativeHumidity(t *testing.T) { + assert.Equal(t, "56", (&Humidity{RelHumidity: "56"}).FmtRelativeHumidity()) +}