diff --git a/cmd/commands_test.go b/cmd/commands_test.go index 795fdfc..761a811 100644 --- a/cmd/commands_test.go +++ b/cmd/commands_test.go @@ -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()}, diff --git a/cmd/jsonapi/converter.go b/cmd/jsonapi/converter.go index de6d015..ae928a1 100644 --- a/cmd/jsonapi/converter.go +++ b/cmd/jsonapi/converter.go @@ -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 { diff --git a/cmd/jsonapi/model.go b/cmd/jsonapi/model.go index 36c8002..cffac5c 100644 --- a/cmd/jsonapi/model.go +++ b/cmd/jsonapi/model.go @@ -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. diff --git a/cmd/list_thermostats.go b/cmd/list_thermostats.go index db27e36..dba4e9b 100644 --- a/cmd/list_thermostats.go +++ b/cmd/list_thermostats.go @@ -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", @@ -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 } @@ -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"), diff --git a/fritz/epoch.go b/fritz/epoch.go new file mode 100644 index 0000000..f938c57 --- /dev/null +++ b/fritz/epoch.go @@ -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") +} diff --git a/fritz/next_change.go b/fritz/next_change.go index e899697..ffbb20e 100644 --- a/fritz/next_change.go +++ b/fritz/next_change.go @@ -1,7 +1,6 @@ package fritz import ( - "strconv" "time" ) @@ -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) } diff --git a/fritz/thermostat.go b/fritz/thermostat.go index 3ceb046..5ce108c 100644 --- a/fritz/thermostat.go +++ b/fritz/thermostat.go @@ -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{ "": "", @@ -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] @@ -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) +}