Skip to content
This repository has been archived by the owner on Apr 14, 2021. It is now read-only.

Add device capabilities and read humidity sensor #237

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions cmd/list_smarthome.go
Original file line number Diff line number Diff line change
@@ -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)
}
36 changes: 35 additions & 1 deletion fritz/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,6 +20,13 @@ const (
Microphone
_
HANFUNUnit
_
SwitchableDevice
DimmableDevice
ColorSettableDevice
_
_
HumiditySensor
)

// Device models a smart home device. This corresponds to
Expand All @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions fritz/device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
7 changes: 7 additions & 0 deletions fritz/devicelist.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions fritz/humidity.go
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 11 additions & 0 deletions fritz/humidity_test.go
Original file line number Diff line number Diff line change
@@ -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())
}