]> git.giacomolli.uk - onehouse/blob - driver/alamo/fake_helper_test.go
Poll and command shared Alamo thermostats
[onehouse] / driver / alamo / fake_helper_test.go
1 package alamo
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "sync"
8 "testing"
9 "time"
10
11 "github.com/stretchr/testify/require"
12
13 "onehouse/driver"
14 "onehouse/protocol"
15 )
16
17 const (
18 validRecord = `{"mac":"FC610E7F98AA","power":0,"stemp":20,"temp":19.5,"sensor":23,"ativo":0,"bloqueio":1}`
19
20 testMAC = "fc610e7f98aa"
21 testDeviceID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
22 testSecondID = "01ARZ3NDEKTSV4RRFFQ69G5FAW"
23 testCommandID = "01ARZ3NDEKTSV4RRFFQ69G5FAX"
24 )
25
26 type fakeGateway struct {
27 mu sync.Mutex
28 failActionAt int
29 ignorePower bool
30 ignoreTarget bool
31 states []State
32 starts []time.Time
33 actions []Action
34 err error
35 delay time.Duration
36 }
37
38 func (g *fakeGateway) Status(ctx context.Context) ([]State, error) {
39 g.mu.Lock()
40 defer g.mu.Unlock()
41
42 err := g.request(ctx)
43 if err != nil {
44 return nil, err
45 }
46
47 return append([]State(nil), g.states...), nil
48 }
49
50 func (g *fakeGateway) request(ctx context.Context) error {
51 g.starts = append(g.starts, time.Now())
52 if g.delay > 0 {
53 timer := time.NewTimer(g.delay)
54 defer timer.Stop()
55
56 select {
57 case <-ctx.Done():
58 return ctx.Err()
59 case <-timer.C:
60 }
61 }
62
63 return g.err
64 }
65
66 func (g *fakeGateway) Action(ctx context.Context, mac string, action Action) (State, error) {
67 g.mu.Lock()
68 defer g.mu.Unlock()
69
70 g.actions = append(g.actions, action)
71
72 err := g.request(ctx)
73 if err != nil {
74 return State{}, err
75 }
76
77 if len(g.actions) == g.failActionAt {
78 return State{}, errors.New("action unavailable")
79 }
80
81 for index := range g.states {
82 state := &g.states[index]
83 if state.MAC != mac {
84 continue
85 }
86
87 if action.Power != nil && !g.ignorePower {
88 state.Power = *action.Power
89 }
90
91 if action.TargetTemperature != nil && !g.ignoreTarget {
92 state.TargetTemperature = *action.TargetTemperature
93 }
94
95 return *state, nil
96 }
97
98 return State{}, nil
99 }
100
101 func (g *fakeGateway) startCount() int {
102 g.mu.Lock()
103 defer g.mu.Unlock()
104
105 return len(g.starts)
106 }
107
108 type fakeTransport struct {
109 mu sync.Mutex
110 publications []protocol.Publication
111 failKind protocol.Kind
112 failure error
113 }
114
115 func (p *fakeTransport) Publish(_ context.Context, publication protocol.Publication) error {
116 p.mu.Lock()
117 defer p.mu.Unlock()
118
119 p.publications = append(p.publications, publication)
120 message, err := protocol.Parse(publication.Topic, publication.Data, publication.Retain)
121 if err != nil {
122 return fmt.Errorf("parse fake publication: %w", err)
123 }
124
125 if message.Address.Kind == p.failKind {
126 return p.failure
127 }
128
129 return nil
130 }
131
132 func (p *fakeTransport) messages(t *testing.T, kind protocol.Kind) []protocol.Message[any] {
133 t.Helper()
134 p.mu.Lock()
135 defer p.mu.Unlock()
136
137 var messages []protocol.Message[any]
138
139 for _, publication := range p.publications {
140 message, err := protocol.Parse(publication.Topic, publication.Data, publication.Retain)
141 require.NoError(t, err)
142
143 if message.Address.Kind == kind {
144 messages = append(messages, message)
145 }
146 }
147
148 return messages
149 }
150
151 type cancelingGateway struct {
152 cancel context.CancelFunc
153 failure error
154 }
155
156 func (g *cancelingGateway) Status(_ context.Context) ([]State, error) {
157 g.cancel()
158
159 return nil, errors.Join(context.Canceled, g.failure)
160 }
161
162 func (g *cancelingGateway) Action(_ context.Context, _ string, _ Action) (State, error) {
163 g.cancel()
164
165 return State{}, errors.Join(context.Canceled, g.failure)
166 }
167
168 func testDriver(t *testing.T, client Client, transport *fakeTransport, thermostats ...Thermostat) (*Driver, *driver.Router) {
169 t.Helper()
170
171 if len(thermostats) == 0 {
172 thermostats = []Thermostat{{
173 DeviceID: testDeviceID,
174 MAC: testMAC,
175 }}
176 }
177
178 var ids []string
179
180 for _, thermostat := range thermostats {
181 ids = append(ids, thermostat.DeviceID)
182 }
183
184 router, err := driver.NewRouter(ids)
185 require.NoError(t, err)
186
187 integration, err := New(Settings{Thermostats: thermostats}, driver.NewPublisher(transport), router, client)
188 require.NoError(t, err)
189
190 return integration, router
191 }
192
193 func testCommand() driver.Command {
194 expires := time.Now().Add(time.Minute).UTC()
195
196 return driver.Command{
197 DeviceID: testDeviceID,
198 Capability: "control",
199 Metadata: protocol.Metadata{
200 MessageID: testCommandID,
201 ExpiresAt: &expires,
202 },
203 Payload: protocol.ValuePayload{Value: []byte(`{"mode":"heat","target_temperature":22}`)},
204 }
205 }
206
207 func testState() State {
208 return State{
209 MAC: testMAC,
210 TargetTemperature: 20,
211 CurrentTemperature: 19.5,
212 FloorTemperature: 23,
213 }
214 }