generated from rmcguire/go-server-with-otel
add set mode option
This commit is contained in:
@@ -113,6 +113,53 @@ func (c *Client) Refresh(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMode changes a device's operating mode. It resolves the normalized mode
|
||||
// slug to the device's @MODE enum index and publishes a desired-state message
|
||||
// to the EcoNet cloud over ClearBlade's REST publish endpoint (the HTTP
|
||||
// equivalent of the app's MQTT publish). The change is applied asynchronously;
|
||||
// the new mode is only reflected after a subsequent Refresh.
|
||||
func (c *Client) SetMode(ctx context.Context, serial, mode string) error {
|
||||
c.mu.RLock()
|
||||
d, account := c.devices[serial], c.accountID
|
||||
c.mu.RUnlock()
|
||||
if d == nil {
|
||||
return fmt.Errorf("econet: unknown device %q", serial)
|
||||
}
|
||||
idx, ok := d.modeIndex(mode)
|
||||
if !ok {
|
||||
return fmt.Errorf("econet: device %q does not support mode %q (supported: %s)",
|
||||
serial, mode, strings.Join(d.SupportedModes(), ", "))
|
||||
}
|
||||
|
||||
if err := c.ensureAuth(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desired, err := json.Marshal(map[string]any{
|
||||
"transactionId": transactionID(),
|
||||
"device_name": d.DeviceID,
|
||||
"serial_number": d.SerialNumber,
|
||||
"@MODE": idx,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publish := map[string]any{
|
||||
"topic": "user/" + account + "/device/desired",
|
||||
"body": string(desired),
|
||||
"qos": 1,
|
||||
}
|
||||
|
||||
path := "/message/" + clearBladeSystemKey + "/publish"
|
||||
if err := c.do(ctx, path, c.userToken(), publish, nil); err != nil {
|
||||
c.clearToken() // token may have expired; re-auth next call
|
||||
return fmt.Errorf("econet: set mode: %w", err)
|
||||
}
|
||||
c.log.Info().Str("serial", serial).Str("mode", mode).Int("modeIndex", idx).
|
||||
Msg("econet: published mode change")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) clearToken() {
|
||||
c.mu.Lock()
|
||||
c.token = ""
|
||||
@@ -274,6 +321,11 @@ func (c *Client) do(ctx context.Context, path, token string, body, out any) erro
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, bytes.TrimSpace(raw))
|
||||
}
|
||||
// The publish endpoint replies 200 with an empty (or non-JSON) body; callers
|
||||
// that don't need the response pass out == nil.
|
||||
if out == nil || len(bytes.TrimSpace(raw)) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(raw, out)
|
||||
}
|
||||
|
||||
@@ -285,6 +337,12 @@ func (c *Client) userToken() string {
|
||||
|
||||
// --- usage helpers -------------------------------------------------------
|
||||
|
||||
// transactionID mirrors the identifier the Rheem Android app stamps on each
|
||||
// desired-state publish: "ANDROID_" + a local second-resolution timestamp.
|
||||
func transactionID() string {
|
||||
return "ANDROID_" + time.Now().Format("2006-01-02T15:04:05")
|
||||
}
|
||||
|
||||
// dayWindow returns ISO-8601 start/end strings covering the current local day,
|
||||
// matching pyeconet's formats: energy includes the timezone offset, water does
|
||||
// not (the upstream parser is picky).
|
||||
|
||||
@@ -26,6 +26,11 @@ type Device struct {
|
||||
Running bool // true when actively heating (decoded from @RUNNING over REST)
|
||||
RunningState string // normalized @RUNNING label: "compressor-running" | "element-running" | "idle" | ...
|
||||
|
||||
// modeEnumText is the raw @MODE enum label list; the slice index is the
|
||||
// integer value published to change the mode. Empty for units without a
|
||||
// settable @MODE (mode changes are unsupported for those).
|
||||
modeEnumText []string
|
||||
|
||||
Setpoint int
|
||||
SetpointMin int
|
||||
SetpointMax int
|
||||
@@ -62,9 +67,31 @@ func newDevice(info map[string]json.RawMessage, now time.Time) *Device {
|
||||
d.FriendlyName = friendlyName(info, d.DeviceID)
|
||||
d.Setpoint, d.SetpointMin, d.SetpointMax = setpoint(info["@SETPOINT"])
|
||||
d.Running, d.RunningState = running(info)
|
||||
d.modeEnumText = modeLabels(info)
|
||||
return d
|
||||
}
|
||||
|
||||
// SupportedModes returns the normalized mode slugs this device can be set to,
|
||||
// in publish-index order. Empty for units without a settable @MODE.
|
||||
func (d *Device) SupportedModes() []string {
|
||||
out := make([]string, 0, len(d.modeEnumText))
|
||||
for _, label := range d.modeEnumText {
|
||||
out = append(out, modeFromEnumText(label))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// modeIndex maps a normalized mode slug to the integer value the EcoNet cloud
|
||||
// expects in a @MODE publish, matching pyeconet's enumText-position lookup.
|
||||
func (d *Device) modeIndex(slug string) (int, bool) {
|
||||
for i, label := range d.modeEnumText {
|
||||
if modeFromEnumText(label) == slug {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// --- field decoders ------------------------------------------------------
|
||||
|
||||
func connected(info map[string]json.RawMessage) bool {
|
||||
|
||||
Reference in New Issue
Block a user