return c.exchange(ctx, "/status", nil)
}
-// Action changes one field or reads a thermostat, and returns its complete
-// readback. It rejects a response for another MAC or more than one thermostat.
-func (c *HTTPClient) Action(ctx context.Context, mac string, action Action) (State, error) {
- mac, err := normalizeMAC(mac)
- if err != nil {
- return State{}, fmt.Errorf("validate action MAC: %w", err)
- }
-
- if action.Power != nil && action.TargetTemperature != nil {
- return State{}, fmt.Errorf("action must change at most one field")
- }
-
- query := url.Values{"mac": {mac}}
- if action.Power != nil {
- query.Set("power", "0")
-
- if *action.Power {
- query.Set("power", "1")
- }
- }
-
- if action.TargetTemperature != nil {
- if !finite(*action.TargetTemperature) {
- return State{}, fmt.Errorf("target temperature must be finite")
- }
-
- query.Set("stemp", strconv.FormatFloat(*action.TargetTemperature, 'f', -1, 64))
- }
-
- states, err := c.exchange(ctx, "/acao", query)
- if err != nil {
- return State{}, fmt.Errorf("exchange action: %w", err)
- }
-
- if len(states) != 1 || states[0].MAC != mac {
- return State{}, fmt.Errorf("action response must contain the requested thermostat only")
- }
-
- return states[0], nil
-}
-
func (c *HTTPClient) exchange(ctx context.Context, path string, query url.Values) (states []State, err error) {
ctx, cancel := context.WithTimeout(ctx, httpRequestTimeout)
defer cancel()
response, err := c.client.Do(request)
if err != nil {
- if ctx.Err() != nil {
- return nil, ctx.Err()
- }
-
- return nil, fmt.Errorf("send gateway request")
+ return nil, fmt.Errorf("send gateway request: %w", safeRequestError(err))
}
defer func() {
body, err := io.ReadAll(io.LimitReader(response.Body, gatewayResponseLimit+1))
if err != nil {
- if ctx.Err() != nil {
- return nil, ctx.Err()
- }
-
- return nil, fmt.Errorf("read gateway response")
+ return nil, fmt.Errorf("read gateway response: %w", safeRequestError(err))
}
if len(body) > gatewayResponseLimit {
seen := make(map[string]bool, len(records))
for _, record := range records {
- state, recordErr := record.state()
- if recordErr != nil {
- return nil, fmt.Errorf("validate gateway record: %w", recordErr)
+ var state State
+
+ state, err = record.state()
+ if err != nil {
+ return nil, fmt.Errorf("validate gateway record: %w", err)
}
if seen[state.MAC] {
return states, nil
}
+
+// Action changes one field or reads a thermostat, and returns its complete
+// readback. It rejects a response for another MAC or more than one thermostat.
+func (c *HTTPClient) Action(ctx context.Context, mac string, action Action) (State, error) {
+ mac, err := normalizeMAC(mac)
+ if err != nil {
+ return State{}, fmt.Errorf("validate action MAC: %w", err)
+ }
+
+ if action.Power != nil && action.TargetTemperature != nil {
+ return State{}, fmt.Errorf("action must change at most one field")
+ }
+
+ query := url.Values{"mac": {mac}}
+ if action.Power != nil {
+ query.Set("power", "0")
+
+ if *action.Power {
+ query.Set("power", "1")
+ }
+ }
+
+ if action.TargetTemperature != nil {
+ if !finite(*action.TargetTemperature) {
+ return State{}, fmt.Errorf("target temperature must be finite")
+ }
+
+ query.Set("stemp", strconv.FormatFloat(*action.TargetTemperature, 'f', -1, 64))
+ }
+
+ states, err := c.exchange(ctx, "/acao", query)
+ if err != nil {
+ return State{}, fmt.Errorf("exchange action: %w", err)
+ }
+
+ if len(states) != 1 || states[0].MAC != mac {
+ return State{}, fmt.Errorf("action response must contain the requested thermostat only")
+ }
+
+ return states[0], nil
+}
+
+func safeRequestError(err error) error {
+ var joined interface{ Unwrap() []error }
+
+ if errors.As(err, &joined) {
+ var causes []error
+
+ for _, cause := range joined.Unwrap() {
+ if cause != nil {
+ causes = append(causes, safeRequestError(cause))
+ }
+ }
+
+ return errors.Join(causes...)
+ }
+
+ if errors.Is(err, context.Canceled) {
+ return context.Canceled
+ }
+
+ if errors.Is(err, context.DeadlineExceeded) {
+ return context.DeadlineExceeded
+ }
+
+ return fmt.Errorf("gateway transport failed")
+}