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

Add new fields to Thermostat #235

Open
wants to merge 1 commit 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
1 change: 1 addition & 0 deletions cmd/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func TestCommands(t *testing.T) {
{cmd: listSwitchesCmd, srv: mock.New().UnstartedServer()},
{cmd: listSwitchesCmd, args: []string{"--output=json"}, srv: mock.New().UnstartedServer()},
{cmd: listThermostatsCmd, srv: mock.New().UnstartedServer()},
{cmd: listThermostatsCmd, args: []string{"--verbose"}, srv: mock.New().UnstartedServer()},
{cmd: listThermostatsCmd, args: []string{"--output=json"}, srv: mock.New().UnstartedServer()},
{cmd: docManCmd, srv: mock.New().UnstartedServer()},
{cmd: boxInfoCmd, srv: mock.New().UnstartedServer()},
Expand Down
9 changes: 9 additions & 0 deletions cmd/jsonapi/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,16 @@ func (m *mapper) mapThermostat(target *State, src *fritz.Device) {
if src.Thermostat.NextChange.Goal != "" {
m.mapNextChange(tc, src)
}
tc.BatteryLow = src.Thermostat.BatteryLow
tc.BatteryChargeLevel = src.Thermostat.BatteryChargeLevel
tc.Window = windowStateLookup[src.Thermostat.WindowOpen]
if src.Thermostat.WindowOpenEnd != 0 {
tc.WindowOpenEnd = time.Unix(src.Thermostat.WindowOpenEnd, 0).Format((time.RFC3339))
}
tc.Boost = src.Thermostat.Boost
if src.Thermostat.BoostEnd != 0 {
tc.BoostEnd = time.Unix(src.Thermostat.BoostEnd, 0).Format((time.RFC3339))
}
target.TemperatureControl = tc

switch src.Thermostat.BatteryLow {
Expand Down
18 changes: 13 additions & 5 deletions cmd/jsonapi/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,19 @@ type State struct {

// TemperatureControl applies to AHA devices capable of adjusting room temperature.
type TemperatureControl struct {
Goal string `json:"goal,omitempty"` // Desired temperature, user controlled.
Saving string `json:"saving,omitempty"` // Energy saving temperature.
Comfort string `json:"comfort,omitempty"` // Comfortable temperature.
NextChange *NextChange `json:"nextChange,omitempty"` // Comfortable temperature.
Window string `json:"window,omitempty"` // "OPEN", "CLOSED" or "" (if unknown).
Goal string `json:"goal,omitempty"` // Desired temperature, user controlled.
Saving string `json:"saving,omitempty"` // Energy saving temperature.
Comfort string `json:"comfort,omitempty"` // Comfortable temperature.
NextChange *NextChange `json:"nextChange,omitempty"` // Comfortable temperature.
BatteryLow string `json:"batteryLow,omitempty"` // "0" if the battery is OK, "1" if it is running low on capacity.
BatteryChargeLevel string `json:"batteryPercent,omitempty"` // Battery charging percentage
Window string `json:"window,omitempty"` // "OPEN", "CLOSED" or "" (if unknown).
WindowOpenEnd string `json:"windowOpenEndtime,omitempty"` // Scheduled end of window-open state (seconds since 1970)
Boost bool `json:"boost,omitempty"` // true if boost mode is active, false if not.
BoostEnd string `json:"boostActiveEndtime,omitempty"` // Scheduled end of boost time (seconds since 1970)
Holiday bool `json:"holidayactive"` // true if device is in holiday-mode, false if not.
Summer bool `json:"summeractive"` // true if device is in summer mode (heating off), false if not.

}

// NextChange indicates the upcoming scheduled temperature change.
Expand Down
61 changes: 53 additions & 8 deletions cmd/list_thermostats.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,27 @@ fritzctl list thermostats --output=json`,

func init() {
listThermostatsCmd.Flags().StringP("output", "o", "", "specify output format")
listThermostatsCmd.Flags().BoolP("verbose", "v", false, "output all values")
listCmd.AddCommand(listThermostatsCmd)
}

func listThermostats(cmd *cobra.Command, _ []string) error {
devs := mustList()
data := selectFmt(cmd, devs.Thermostats(), thermostatsTable)
defaultF := func(devs []fritz.Device) interface{} {
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
verbose = false
}
return thermostatsTable(devs, verbose)
}
data := selectFmt(cmd, devs.Thermostats(), defaultF)
logger.Success("Device data:")
printer.Print(data, os.Stdout)
return nil
}

func thermostatsTable(devs []fritz.Device) interface{} {
table := console.NewTable(console.Headers(
func thermostatsTable(devs []fritz.Device, verbose bool) interface{} {
headers := []string{
"NAME",
"PRODUCT",
"PRESENT",
Expand All @@ -48,24 +56,37 @@ func thermostatsTable(devs []fritz.Device) interface{} {
"NEXT",
"STATE",
"BATTERY",
))
appendThermostats(devs, table)
}
if verbose {
headers = append(headers,
"MODE (HOLIDAY/SUMMER)",
"WINDOW (OPEN/UNTIL)",
"BOOST (ACTIVE/UNTIL)",
)
}
table := console.NewTable(console.Headers(headers...))
appendThermostats(devs, table, verbose)
return table
}

func appendThermostats(devs []fritz.Device, table *console.Table) {
func appendThermostats(devs []fritz.Device, table *console.Table, verbose bool) {
for _, dev := range devs {
columns := thermostatColumns(dev)
columns := thermostatColumns(dev, verbose)
table.Append(columns)
}
}

func thermostatColumns(dev fritz.Device) []string {
func thermostatColumns(dev fritz.Device, verbose bool) []string {
var columnValues []string
columnValues = appendMetadata(columnValues, dev)
columnValues = appendRuntimeFlags(columnValues, dev)
columnValues = appendTemperatureValues(columnValues, dev)
columnValues = appendRuntimeWarnings(columnValues, dev)
if verbose {
columnValues = appendModeValues(columnValues, dev)
columnValues = appendWindowValues(columnValues, dev)
columnValues = appendBoostValues(columnValues, dev)
}
return columnValues
}

Expand All @@ -83,6 +104,30 @@ func appendRuntimeWarnings(cols []string, dev fritz.Device) []string {
return append(cols, errorCode(dev.Thermostat.ErrorCode), batteryState(dev.Thermostat))
}

func appendModeValues(cols []string, dev fritz.Device) []string {
return append(cols,
fmt.Sprintf("%s/%s",
console.Btoc(dev.Thermostat.Holiday).String(),
console.Btoc(dev.Thermostat.Summer).String(),
))
}

func appendWindowValues(cols []string, dev fritz.Device) []string {
return append(cols,
fmt.Sprintf("%s/%s",
console.Stoc(dev.Thermostat.WindowOpen).String(),
dev.Thermostat.FmtWindowOpenEndTimestamp(time.Now()),
))
}

func appendBoostValues(cols []string, dev fritz.Device) []string {
return append(cols,
fmt.Sprintf("%s/%s",
console.Btoc(dev.Thermostat.Boost).String(),
dev.Thermostat.FmtBoostEndTimestamp(time.Now()),
))
}

func appendTemperatureValues(cols []string, dev fritz.Device) []string {
return append(cols,
fmtUnit(dev.Thermostat.FmtMeasuredTemperature, "°C"),
Expand Down
43 changes: 43 additions & 0 deletions fritz/epoch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package fritz

import (
"strconv"
"time"
)

// FmtEpochSecond takes a int64, and formats it according to FmtCompact.
func FmtEpochSecond(t int64, ref time.Time) string {
return FmtCompact(time.Unix(1, 0), ref)
}

// FmtEpochSecondString takes a string, parses to to an epoch second and formats it according to FmtCompact.
func FmtEpochSecondString(timeStamp string, ref time.Time) string {
t, err := EpochToUnix(timeStamp)
if err != nil {
return ""
}
return FmtCompact(t, ref)
}

// EpochToUnix is equivalent to time.unix with zero nanoseconds, where the string argument passed to this function is parsed
// as a base-10, 64-bit integer. It returns an error iff the argument could not be parsed.
func EpochToUnix(epoch string) (time.Time, error) {
i, err := strconv.ParseInt(epoch, 10, 64)
return time.Unix(i, 0), err
}

// FmtCompact formats a given time t to a short form, given a reference time ref. It particular:
// A simple time HH:MM:SS is displayed if t is in the same day as ref.
// Day, month and time is returned if t is in the same year as ref.
// Year, day, month and time is returned in all other cases.
func FmtCompact(t, ref time.Time) string {
refYear, refMonth, refDay := ref.Date()
tYear, tMonth, tDay := t.Date()
if refYear != tYear {
return t.Format("Mon Jan 2 15:04:05 2006")
}
if refMonth != tMonth || refDay != tDay {
return t.Format("Mon Jan 2 15:04:05")
}
return t.Format("15:04:05")
}
37 changes: 2 additions & 35 deletions fritz/next_change.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package fritz

import (
"strconv"
"time"
)

Expand All @@ -20,42 +19,10 @@ func (n *NextChange) FmtGoalTemperature() string {
return fmtTemperatureHkr(n.Goal)
}

// FmtTimestamp formats the epoch timestamp into a compact readable form. See fmtEpochSecondString.
// FmtTimestamp formats the epoch timestamp into a compact readable form. See FmtEpochSecondString.
func (n *NextChange) FmtTimestamp(ref time.Time) string {
if n.TimeStamp == "0" {
return ""
}
return n.fmtEpochSecondString(ref)
}

// fmtEpochSecondString takes a string, parses to to an epoch second and formats it according to fmtCompact.
func (n *NextChange) fmtEpochSecondString(ref time.Time) string {
t, err := n.unix(n.TimeStamp)
if err != nil {
return ""
}
return n.fmtCompact(t, ref)
}

// unix is equivalent to time.unix with zero nanoseconds, where the string argument passed to this function is parsed
// as a base-10, 64-bit integer. It returns an error iff the argument could not be parsed.
func (n *NextChange) unix(epoch string) (time.Time, error) {
i, err := strconv.ParseInt(epoch, 10, 64)
return time.Unix(i, 0), err
}

// fmtCompact formats a given time t to a short form, given a reference time ref. It particular:
// A simple time HH:MM:SS is displayed if t is in the same day as ref.
// Day, month and time is returned if t is in the same year as ref.
// Year, day, month and time is returned in all other cases.
func (n *NextChange) fmtCompact(t, ref time.Time) string {
refYear, refMonth, refDay := ref.Date()
tYear, tMonth, tDay := t.Date()
if refYear != tYear {
return t.Format("Mon Jan 2 15:04:05 2006")
}
if refMonth != tMonth || refDay != tDay {
return t.Format("Mon Jan 2 15:04:05")
}
return t.Format("15:04:05")
return FmtEpochSecondString(n.TimeStamp, ref)
}
45 changes: 34 additions & 11 deletions fritz/thermostat.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package fritz

import "time"

// HkrErrorDescriptions has a translation of error code to a warning/error/status description.
var HkrErrorDescriptions = map[string]string{
"": "",
Expand All @@ -15,17 +17,22 @@ var HkrErrorDescriptions = map[string]string{
// Thermostat models the "HKR" device.
// codebeat:disable[TOO_MANY_IVARS]
type Thermostat struct {
Measured string `xml:"tist"` // Measured temperature.
Goal string `xml:"tsoll"` // Desired temperature, user controlled.
Saving string `xml:"absenk"` // Energy saving temperature.
Comfort string `xml:"komfort"` // Comfortable temperature.
NextChange NextChange `xml:"nextchange"` // The next scheduled temperature change.
Lock string `xml:"lock"` // Switch locked (box defined)? 1/0 (empty if not known or if there was an error).
DeviceLock string `xml:"devicelock"` // Switch locked (device defined)? 1/0 (empty if not known or if there was an error).
ErrorCode string `xml:"errorcode"` // Error codes: 0 = OK, 1 = ... see https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf.
BatteryLow string `xml:"batterylow"` // "0" if the battery is OK, "1" if it is running low on capacity.
WindowOpen string `xml:"windowopenactiv"` // "1" if detected an open window (usually turns off heating), "0" if not.
BatteryChargeLevel string `xml:"battery"` // Battery charge level in percent.
Measured string `xml:"tist"` // Measured temperature.
Goal string `xml:"tsoll"` // Desired temperature, user controlled.
Saving string `xml:"absenk"` // Energy saving temperature.
Comfort string `xml:"komfort"` // Comfortable temperature.
NextChange NextChange `xml:"nextchange"` // The next scheduled temperature change.
Lock string `xml:"lock"` // Switch locked (box defined)? 1/0 (empty if not known or if there was an error).
DeviceLock string `xml:"devicelock"` // Switch locked (device defined)? 1/0 (empty if not known or if there was an error).
ErrorCode string `xml:"errorcode"` // Error codes: 0 = OK, 1 = ... see https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf.
BatteryLow string `xml:"batterylow"` // "0" if the battery is OK, "1" if it is running low on capacity.
WindowOpen string `xml:"windowopenactiv"` // "1" if detected an open window (usually turns off heating), "0" if not.
BatteryChargeLevel string `xml:"battery"` // Battery charge level in percent.
WindowOpenEnd int64 `xml:"windowopenactiveendtime"` // Scheduled end of window-open state (seconds since 1970)
Boost bool `xml:"boostactive"` // true if boost mode is active, false if not.
BoostEnd int64 `xml:"boostactiveendtime"` // Scheduled end of boost time (seconds since 1970)
Holiday bool `xml:"holidayactive"` // true if device is in holiday-mode, false if not.
Summer bool `xml:"summeractive"` // true if device is in summer mode (heating off), false if not.
}

// codebeat:enable[TOO_MANY_IVARS]
Expand Down Expand Up @@ -65,3 +72,19 @@ func (t *Thermostat) FmtSavingTemperature() string {
func (t *Thermostat) FmtComfortTemperature() string {
return fmtTemperatureHkr(t.Comfort)
}

// FmtWindowOpenEndTimestamp formats the epoch timestamp into a compact readable form. See FmtEpochSecondString.
func (t *Thermostat) FmtWindowOpenEndTimestamp(ref time.Time) string {
if t.WindowOpenEnd == 0 {
return ""
}
return FmtEpochSecond(t.WindowOpenEnd, ref)
}

// FmtBoostEndTimestamp formats the epoch timestamp into a compact readable form. See FmtEpochSecondString.
func (t *Thermostat) FmtBoostEndTimestamp(ref time.Time) string {
if t.BoostEnd == 0 {
return ""
}
return FmtEpochSecond(t.WindowOpenEnd, ref)
}