Repository: bpicode/fritzctl
Pull request #185 (2018-12-17 13:29:00)

Issue 167 alert sensor report


Patch:
diff --git a/Makefile b/Makefile
index 61d83a9..d6e0d3e 100644
--- a/Makefile
+++ b/Makefile
@@ -85,7 +85,7 @@ fasttest: build
 	    echo -n "     ";\
 		$(GO) test  -run '(Test|Example)' $(BUILDFLAGS) $(TESTFLAGS) -coverprofile=coverage.out $(pkg) || exit 1;\
 		tail -n +2 coverage.out >> coverage-all.out;)
-	@$(GO) tool cover -html=coverage-all.out
+	@$(GO) tool cover -html=coverage-all.out -o coverage-all.html
 
 completion_bash:
 	@echo -n ">> BASH COMPLETION, output = $(BASH_COMPLETION_OUTPUT)"
diff --git a/cmd/client.go b/cmd/client.go
index 8a00a58..3d99846 100644
--- a/cmd/client.go
+++ b/cmd/client.go
@@ -87,3 +87,10 @@ func cfg(places ...config.Place) (*config.Config, error) {
 	p := config.NewParser(places...)
 	return p.Parse()
 }
+
+func mustList() *fritz.Devicelist {
+	c := homeAutoClient()
+	devs, err := c.List()
+	assertNoErr(err, "cannot obtain device data")
+	return devs
+}
diff --git a/cmd/jsonapi/converter.go b/cmd/jsonapi/converter.go
index f8d24e9..cd92c60 100644
--- a/cmd/jsonapi/converter.go
+++ b/cmd/jsonapi/converter.go
@@ -22,7 +22,7 @@ type mapper struct {
 
 // Convert translates a slice of Devices into a ThermostatList
 func (m *mapper) Convert(ds []fritz.Device) DeviceList {
-	l := DeviceList{}
+	l := DeviceList{Devices: []Device{}}
 	l.NumberOfItems = len(ds)
 	for _, d := range ds {
 		l.Devices = append(l.Devices, m.convertOne(d))
@@ -91,6 +91,11 @@ func mapLock(target *Properties, src *fritz.Device) {
 	}
 }
 
+var alertSignalLookup = map[string]string{
+	"0": "OFF",
+	"1": "ON",
+}
+
 func (m *mapper) mapMeasurements(target *Device, src *fritz.Device) {
 	meas := &Measurements{}
 	if src.Temperature.Celsius != "" {
@@ -102,6 +107,7 @@ func (m *mapper) mapMeasurements(target *Device, src *fritz.Device) {
 	if src.Powermeter.Energy != "" {
 		meas.EnergyConsumption = src.Powermeter.FmtEnergyWh()
 	}
+	meas.AlertSignal = alertSignalLookup[src.AlertSensor.State]
 	target.Measurements = meas
 }
 
diff --git a/cmd/jsonapi/model.go b/cmd/jsonapi/model.go
index b6551a8..0c69d4e 100644
--- a/cmd/jsonapi/model.go
+++ b/cmd/jsonapi/model.go
@@ -43,6 +43,7 @@ type Measurements struct {
 	Temperature       string `json:"temperature,omitempty"`       // Temperature measured in °C.
 	PowerConsumption  string `json:"powerConsumption,omitempty"`  // Current power in W.
 	EnergyConsumption string `json:"energyConsumption,omitempty"` // Absolute energy consumption in Wh since the device started operating.
+	AlertSignal       string `json:"alertSignal"`                 // "ON", "OFF" (if the device reports an alert) or "" (if unknown or does not apply).
 }
 
 // State contains the core domain of the device, e.g. "is the switch on?", "what is the room temperature supposed to be?".
diff --git a/cmd/list_alerts.go b/cmd/list_alerts.go
new file mode 100644
index 0000000..91cbcc6
--- /dev/null
+++ b/cmd/list_alerts.go
@@ -0,0 +1,49 @@
+package cmd
+
+import (
+	"os"
+
+	"github.com/bpicode/fritzctl/cmd/printer"
+	"github.com/bpicode/fritzctl/fritz"
+	"github.com/bpicode/fritzctl/internal/console"
+	"github.com/bpicode/fritzctl/internal/stringutils"
+	"github.com/bpicode/fritzctl/logger"
+	"github.com/spf13/cobra"
+)
+
+var listAlertsCmd = &cobra.Command{
+	Use:     "alerts",
+	Short:   "List recent alert sensor reports",
+	Long:    "List the most recently reported state of all devices with an alert sensor.",
+	Example: "fritzctl list alerts",
+	RunE:    listAlerts,
+}
+
+func init() {
+	listAlertsCmd.Flags().StringP("output", "o", "", "specify output format")
+	listCmd.AddCommand(listAlertsCmd)
+}
+
+func listAlerts(cmd *cobra.Command, _ []string) error {
+	devs := mustList()
+	data := selectFmt(cmd, devs.AlertSensors(), alertSensorsTable)
+	logger.Success("Device data:")
+	printer.Print(data, os.Stdout)
+	return nil
+}
+
+func alertSensorsTable(devs []fritz.Device) interface{} {
+	table := console.NewTable(console.Headers(
+		"NAME",
+		"ALERT",
+	))
+	symbols := stringutils.MapWithDefault(map[string]string{
+		"0": console.Green("\u2714"),
+		"1": console.Yellow("\u26a0"),
+	}, console.Yellow("?"))
+	for _, dev := range devs {
+		columns := []string{dev.Name, symbols(dev.AlertSensor.State)}
+		table.Append(columns)
+	}
+	return table
+}
diff --git a/cmd/list_groups.go b/cmd/list_groups.go
index da21010..672b713 100644
--- a/cmd/list_groups.go
+++ b/cmd/list_groups.go
@@ -24,9 +24,7 @@ func init() {
 }
 
 func listGroups(_ *cobra.Command, _ []string) error {
-	c := homeAutoClient()
-	list, err := c.List()
-	assertNoErr(err, "cannot obtain data for smart home groups")
+	list := mustList()
 	printGroups(list)
 	return nil
 }
diff --git a/cmd/list_switches.go b/cmd/list_switches.go
index 30221a6..86e736f 100644
--- a/cmd/list_switches.go
+++ b/cmd/list_switches.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/bpicode/fritzctl/cmd/jsonapi"
 	"github.com/bpicode/fritzctl/cmd/printer"
 	"github.com/bpicode/fritzctl/fritz"
 	"github.com/bpicode/fritzctl/internal/console"
@@ -27,25 +26,14 @@ func init() {
 }
 
 func listSwitches(cmd *cobra.Command, _ []string) error {
-	c := homeAutoClient()
-	devs, err := c.List()
-	assertNoErr(err, "cannot obtain data for smart home switches")
+	devs := mustList()
 	logger.Success("Device data:")
-	data := remapSwitches(cmd, devs.Switches())
+	data := selectFmt(cmd, devs.Switches(), switchTable)
 	printer.Print(data, os.Stdout)
 	return nil
 }
 
-func remapSwitches(cmd *cobra.Command, ds []fritz.Device) interface{} {
-	switch cmd.Flag("output").Value.String() {
-	case "json":
-		return jsonapi.NewMapper().Convert(ds)
-	default:
-		return switchTable(ds)
-	}
-}
-
-func switchTable(devs []fritz.Device) *console.Table {
+func switchTable(devs []fritz.Device) interface{} {
 	table := console.NewTable(console.Headers(
 		"NAME",
 		"PRODUCT",
diff --git a/cmd/list_thermostats.go b/cmd/list_thermostats.go
index 83a45b6..9e59954 100644
--- a/cmd/list_thermostats.go
+++ b/cmd/list_thermostats.go
@@ -5,7 +5,6 @@ import (
 	"os"
 	"time"
 
-	"github.com/bpicode/fritzctl/cmd/jsonapi"
 	"github.com/bpicode/fritzctl/cmd/printer"
 	"github.com/bpicode/fritzctl/fritz"
 	"github.com/bpicode/fritzctl/internal/console"
@@ -28,25 +27,14 @@ func init() {
 }
 
 func listThermostats(cmd *cobra.Command, _ []string) error {
-	c := homeAutoClient()
-	devs, err := c.List()
-	assertNoErr(err, "cannot obtain thermostats device data")
-	data := remapThermostats(cmd, devs.Thermostats())
+	devs := mustList()
+	data := selectFmt(cmd, devs.Thermostats(), thermostatsTable)
 	logger.Success("Device data:")
 	printer.Print(data, os.Stdout)
 	return nil
 }
 
-func remapThermostats(cmd *cobra.Command, ds []fritz.Device) interface{} {
-	switch cmd.Flag("output").Value.String() {
-	case "json":
-		return jsonapi.NewMapper().Convert(ds)
-	default:
-		return thermostatsTable(ds)
-	}
-}
-
-func thermostatsTable(devs []fritz.Device) *console.Table {
+func thermostatsTable(devs []fritz.Device) interface{} {
 	table := console.NewTable(console.Headers(
 		"NAME",
 		"PRODUCT",
diff --git a/cmd/manifest_export.go b/cmd/manifest_export.go
index 92bd360..8df9fba 100644
--- a/cmd/manifest_export.go
+++ b/cmd/manifest_export.go
@@ -20,9 +20,7 @@ func init() {
 }
 
 func export(_ *cobra.Command, _ []string) error {
-	h := homeAutoClient()
-	l, err := h.List()
-	assertNoErr(err, "cannot obtain device data")
+	l := mustList()
 	plan := manifest.ConvertDevicelist(l)
 	manifest.ExporterTo(os.Stdout).Export(plan)
 	return nil
diff --git a/cmd/output_format_selector.go b/cmd/output_format_selector.go
new file mode 100644
index 0000000..f00e979
--- /dev/null
+++ b/cmd/output_format_selector.go
@@ -0,0 +1,16 @@
+package cmd
+
+import (
+	"github.com/bpicode/fritzctl/cmd/jsonapi"
+	"github.com/bpicode/fritzctl/fritz"
+	"github.com/spf13/cobra"
+)
+
+func selectFmt(cmd *cobra.Command, ds []fritz.Device, defaultF func([]fritz.Device) interface{}) interface{} {
+	switch cmd.Flag("output").Value.String() {
+	case "json":
+		return jsonapi.NewMapper().Convert(ds)
+	default:
+		return defaultF(ds)
+	}
+}
diff --git a/fritz/alertsensor.go b/fritz/alertsensor.go
new file mode 100644
index 0000000..9106c4f
--- /dev/null
+++ b/fritz/alertsensor.go
@@ -0,0 +1,6 @@
+package fritz
+
+// AlertSensor collects data from devices that have an alert sensor.
+type AlertSensor struct {
+	State string `xml:"state"` // Last transmitted alert state, "0" - no alert, "1" - alert, "" if unknown or upon errors.
+}
diff --git a/fritz/device.go b/fritz/device.go
index c62347f..5f0ce0a 100644
--- a/fritz/device.go
+++ b/fritz/device.go
@@ -27,7 +27,7 @@ const (
 type Device struct {
 	Identifier      string      `xml:"identifier,attr"`      // A unique ID like AIN, MAC address, etc.
 	ID              string      `xml:"id,attr"`              // Internal device ID of the FRITZ!Box.
-	Functionbitmask string      `xml:"functionbitmask,attr"` // Bitmask determining the functionality of the device: bit 6: Comet DECT, HKR, "thermostat", bit 7: energy measurment device, bit 8: temperature sensor, bit 9: switch, bit 10: AVM DECT repeater
+	Functionbitmask string      `xml:"functionbitmask,attr"` // Bitmask determining the functionality of the device: bit 6: Comet DECT, HKR, "thermostat", bit 7: energy measurement device, bit 8: temperature sensor, bit 9: switch, bit 10: AVM DECT repeater
 	Fwversion       string      `xml:"fwversion,attr"`       // Firmware version of the device.
 	Manufacturer    string      `xml:"manufacturer,attr"`    // Manufacturer of the device, usually set to "AVM".
 	Productname     string      `xml:"productname,attr"`     // Name of the product, empty for unknown or undefined devices.
@@ -37,6 +37,7 @@ type Device struct {
 	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.
 	Thermostat      Thermostat  `xml:"hkr"`                  // Thermostat data, only filled with sensible data for HKR devices.
+	AlertSensor     AlertSensor `xml:"alert"`
 }
 
 // codebeat:enable[TOO_MANY_IVARS]
diff --git a/fritz/devicelist.go b/fritz/devicelist.go
index 921a1c7..e5ef286 100644
--- a/fritz/devicelist.go
+++ b/fritz/devicelist.go
@@ -17,24 +17,33 @@ type DeviceGroup struct {
 
 // Switches returns the devices which satisfy IsSwitch.
 func (l *Devicelist) Switches() []Device {
-	var switches []Device
-	for _, d := range l.Devices {
-		if d.IsSwitch() {
-			switches = append(switches, d)
-		}
-	}
-	return switches
+	return l.filter(func(d Device) bool {
+		return d.IsSwitch()
+	})
 }
 
 // Thermostats returns the devices which satisfy IsThermostat.
 func (l *Devicelist) Thermostats() []Device {
-	var thermostats []Device
+	return l.filter(func(d Device) bool {
+		return d.IsThermostat()
+	})
+}
+
+// AlertSensors returns the devices which satisfy HasAlertSensor.
+func (l *Devicelist) AlertSensors() []Device {
+	return l.filter(func(d Device) bool {
+		return d.HasAlertSensor()
+	})
+}
+
+func (l *Devicelist) filter(predicate func(Device) bool) []Device {
+	var filtered []Device
 	for _, d := range l.Devices {
-		if d.IsThermostat() {
-			thermostats = append(thermostats, d)
+		if predicate(d) {
+			filtered = append(filtered, d)
 		}
 	}
-	return thermostats
+	return filtered
 }
 
 // DeviceGroups returns a slice of DeviceGroup by joining Group.Members() on Device.ID.
diff --git a/internal/stringutils/stringutils.go b/internal/stringutils/stringutils.go
index 6b5fad7..f5558af 100644
--- a/internal/stringutils/stringutils.go
+++ b/internal/stringutils/stringutils.go
@@ -58,3 +58,14 @@ func ErrorMessages(errs []error) []string {
 	}
 	return ms
 }
+
+// MapWithDefault provides a closure that does the regular map lookup, and defaults to the passed value no value is found.
+func MapWithDefault(m map[string]string, def string) func(string) string {
+	return func(key string) string {
+		v, ok := m[key]
+		if !ok {
+			return def
+		}
+		return v
+	}
+}
diff --git a/mock/devicelist.xml b/mock/devicelist.xml
index df2c670..e9f1323 100644
--- a/mock/devicelist.xml
+++ b/mock/devicelist.xml
@@ -148,6 +148,24 @@
         </hkr>
     </device>
 
+    <device identifier="8888 23523626" id="17" functionbitmask="16" fwversion="03.54" manufacturer="AVM"
+            productname="Motion Detector">
+        <present>1</present>
+        <name>SEC_1</name>
+        <alert>
+            <state>1</state>
+        </alert>
+    </device>
+
+    <device identifier="32623646 23523626" id="18" functionbitmask="16" fwversion="03.54" manufacturer="AVM"
+            productname="Motion Detector">
+        <present>1</present>
+        <name>SEC_2</name>
+        <alert>
+            <state>0</state>
+        </alert>
+    </device>
+
     <group identifier="45:FF:99-900" id="960" functionbitmask="320" fwversion="1.0"
            manufacturer="AVM" productname="">
         <present>1</present>


Test patch:
diff --git a/cmd/commands_test.go b/cmd/commands_test.go
index fa2d81b..3f58081 100644
--- a/cmd/commands_test.go
+++ b/cmd/commands_test.go
@@ -37,6 +37,8 @@ func TestCommands(t *testing.T) {
 		{cmd: planManifestCmd, args: []string{"../testdata/devicelist_fritzos06.83_plan.yml"}, srv: mock.New().UnstartedServer()},
 		{cmd: exportManifestCmd, srv: mock.New().UnstartedServer()},
 		{cmd: applyManifestCmd, args: []string{"../testdata/devicelist_fritzos06.83_plan.yml"}, srv: mock.New().UnstartedServer()},
+		{cmd: listAlertsCmd, srv: mock.New().UnstartedServer()},
+		{cmd: listAlertsCmd, args: []string{"--output=json"}, srv: mock.New().UnstartedServer()},
 		{cmd: listGroupsCmd, srv: mock.New().UnstartedServer()},
 		{cmd: listLanDevicesCmd, srv: mock.New().UnstartedServer()},
 		{cmd: listLogsCmd, srv: mock.New().UnstartedServer()},
diff --git a/cmd/jsonapi/converter_test.go b/cmd/jsonapi/converter_test.go
index 7ce31c0..ac79161 100644
--- a/cmd/jsonapi/converter_test.go
+++ b/cmd/jsonapi/converter_test.go
@@ -16,6 +16,7 @@ func Test_thMapper_Convert(t *testing.T) {
 		simpleHkr(),
 		problematicPlungerHkr(),
 		simpleSwitch(),
+		alertSensor(),
 	}
 	l := m.Convert(devices)
 	bs, err := json.Marshal(l)
@@ -52,3 +53,13 @@ func simpleSwitch() fritz.Device {
 		Temperature:     fritz.Temperature{Celsius: "222"},
 	}
 }
+
+func alertSensor() fritz.Device {
+	return fritz.Device{
+		Name:            "motion_detector_restricted_area",
+		Functionbitmask: "16",
+		AlertSensor: fritz.AlertSensor{
+			State: "1",
+		},
+	}
+}
diff --git a/fritz/devicelist_test.go b/fritz/devicelist_test.go
index c215135..24e4a10 100644
--- a/fritz/devicelist_test.go
+++ b/fritz/devicelist_test.go
@@ -28,6 +28,12 @@ func TestSwitchAndThermostatFilteringIssue56(t *testing.T) {
 	assert.Equal(t, len(l.Devices), len(l.Switches())+len(l.Thermostats()))
 }
 
+// TestAlertSensorFilter tests filtering by alert sensor capabilities.
+func TestAlertSensorFilter(t *testing.T) {
+	l := mustUnmarshall(t, "../testdata/devicelist_fritzos06.83.xml")
+	assert.Len(t, l.AlertSensors(), 0)
+}
+
 // TestGroupsIssue56 tests the group un-marshalling.
 func TestGroupsIssue56(t *testing.T) {
 	l := mustUnmarshall(t, "../testdata/devicelist_issue_59.xml")
diff --git a/internal/stringutils/stringutils_test.go b/internal/stringutils/stringutils_test.go
index 8321460..152d953 100644
--- a/internal/stringutils/stringutils_test.go
+++ b/internal/stringutils/stringutils_test.go
@@ -101,3 +101,11 @@ func TestContract(t *testing.T) {
 		})
 	}
 }
+
+// TestMapWithDefault probes the default valued map.
+func TestMapWithDefault(t *testing.T) {
+	f := MapWithDefault(map[string]string{"a": "b", "foo": "bar"}, "x")
+	assert.Equal(t, "b", f("a"))
+	assert.Equal(t, "bar", f("foo"))
+	assert.Equal(t, "x", f("everything else"))
+}