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

Commit

Permalink
Add new fields to Thermostat
Browse files Browse the repository at this point in the history
Added the new fields (as of AVMs docs):
* WindowOpenEnd
* Boost
* BoostEnd
* Holiday
* Summer

As this makes the cli output quite long, I've added a verbose option to
hide most of the new fields by default.
  • Loading branch information
jayme-github committed Jan 22, 2021
1 parent 4874fa4 commit 2c65a4a
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 59 deletions.
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)
}

0 comments on commit 2c65a4a

Please sign in to comment.