X-Git-Url: https://git.giacomolli.uk/onehouse/blobdiff_plain/db3091ee257788c92421a5b8b1af49d429ce8f8e..HEAD:/driver/alamo/http_client.go diff --git a/driver/alamo/http_client.go b/driver/alamo/http_client.go index ac00dfc..4c645f4 100644 --- a/driver/alamo/http_client.go +++ b/driver/alamo/http_client.go @@ -14,8 +14,8 @@ import ( ) const ( - gatewayRequestTimeout = 5 * time.Second - gatewayResponseLimit = 64 * 1024 + httpRequestTimeout = 5 * time.Second + gatewayResponseLimit = 64 * 1024 ) // HTTPClient exchanges bounded GET requests with one HTTP gateway origin. Each @@ -54,49 +54,8 @@ func (c *HTTPClient) Status(ctx context.Context) ([]State, error) { 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, gatewayRequestTimeout) + ctx, cancel := context.WithTimeout(ctx, httpRequestTimeout) defer cancel() address := c.origin + path @@ -111,11 +70,7 @@ func (c *HTTPClient) exchange(ctx context.Context, path string, query url.Values 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() { @@ -131,11 +86,7 @@ func (c *HTTPClient) exchange(ctx context.Context, path string, query url.Values 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 { @@ -152,9 +103,11 @@ func (c *HTTPClient) exchange(ctx context.Context, path string, query url.Values 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] { @@ -167,3 +120,70 @@ func (c *HTTPClient) exchange(ctx context.Context, path string, query url.Values 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") +}