13 type commandHandler struct {
15 thermostat *thermostatState
18 func (h *commandHandler) handle(ctx context.Context, commands []driver.Command) error {
19 for _, command := range commands {
20 err := h.one(ctx, command)
22 return fmt.Errorf("process thermostat command: %w", err)
29 func (h *commandHandler) one(ctx context.Context, command driver.Command) error {
31 if command.DeviceID != s.settings.DeviceID {
32 return fmt.Errorf("command device is not owned by this thermostat")
35 record, exists := s.ledger.Lookup(command.Metadata.MessageID)
37 err := s.publisher.Result(ctx, s.settings.DeviceID, command.Metadata.MessageID, record.Metadata, record.Payload)
39 return fmt.Errorf("replay thermostat result: %w", err)
46 settings, err := decodeControl(command.Payload.Value)
49 case command.Expired(time.Now()):
50 reason = "Command expired"
51 case command.Capability != "control":
52 reason = "Capability is not the thermostat control"
54 reason = "Thermostat settings are invalid"
58 err = s.result(ctx, command, protocol.ResultPayload{
63 return fmt.Errorf("reject thermostat command: %w", err)
69 state, observedAt, applyErr := h.apply(ctx, command, settings)
70 payload := protocol.ResultPayload{Status: "completed"}
72 payload.Status = "failed"
73 payload.Reason = "Thermostat request or readback failed"
74 if command.Expired(time.Now()) {
75 payload.Reason = "Command expired"
79 record, err = s.ledger.Remember(command.Metadata.MessageID, payload)
81 return fmt.Errorf("remember applied thermostat result: %w", err)
86 return fmt.Errorf("stop thermostat command: %w", errors.Join(ctx.Err(), applyErr))
92 if !observedAt.IsZero() {
93 err = s.record(ctx, state, observedAt, command.Metadata.MessageID)
95 return fmt.Errorf("record command feedback: %w", err)
99 err = s.publisher.Result(ctx, s.settings.DeviceID, command.Metadata.MessageID, record.Metadata, record.Payload)
101 return fmt.Errorf("publish applied thermostat result: %w", err)
107 func (h *commandHandler) apply(ctx context.Context, command driver.Command, settings controlSettings) (State, time.Time, error) {
108 ctx, cancel := context.WithDeadline(ctx, *command.Metadata.ExpiresAt)
111 mac := h.thermostat.settings.MAC
112 state, err := h.gateway.action(ctx, mac, Action{})
114 return State{}, time.Time{}, fmt.Errorf("read command baseline: %w", err)
117 observedAt := time.Now().UTC()
121 wantPower := settings.Mode == "heat"
122 if state.Power && (!wantPower || state.TargetTemperature != settings.TargetTemperature) {
124 next, err = h.gateway.action(ctx, mac, Action{Power: &power})
126 return state, observedAt, fmt.Errorf("turn thermostat off: %w", err)
130 observedAt = time.Now().UTC()
132 return state, observedAt, fmt.Errorf("thermostat remains on before target change")
136 if state.TargetTemperature != settings.TargetTemperature {
137 next, err = h.gateway.action(ctx, mac, Action{TargetTemperature: &settings.TargetTemperature})
139 return state, observedAt, fmt.Errorf("set thermostat target: %w", err)
143 observedAt = time.Now().UTC()
144 if state.TargetTemperature != settings.TargetTemperature {
145 return state, observedAt, fmt.Errorf("thermostat target readback differs")
149 if wantPower && !state.Power {
151 next, err = h.gateway.action(ctx, mac, Action{Power: &power})
153 return state, observedAt, fmt.Errorf("turn thermostat on: %w", err)
157 observedAt = time.Now().UTC()
160 if state.Power != wantPower || state.TargetTemperature != settings.TargetTemperature {
161 return state, observedAt, fmt.Errorf("thermostat settings readback differs")
164 return state, observedAt, nil