-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlight.go
More file actions
203 lines (171 loc) · 6.96 KB
/
Copy pathlight.go
File metadata and controls
203 lines (171 loc) · 6.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package foxfire
import (
"context"
"fmt"
)
// On is the power state sub-resource.
type On struct {
On bool `json:"on"`
}
// Dimming carries brightness as a percentage in (0, 100]. Note that zero is
// not "off": the bridge clamps to MinDimLevel, and turning a light off is
// done through On.
type Dimming struct {
Brightness float64 `json:"brightness"`
MinDimLevel *float64 `json:"min_dim_level,omitempty"`
}
// XY is a CIE 1931 chromaticity coordinate. The bridge speaks xy natively;
// RGB and HSV conversions belong in the caller or in a helper package, not
// in the wire types.
type XY struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
// Gamut describes the color volume a particular light can actually reproduce.
// Sending an xy outside the gamut is not an error; the light silently clamps
// to the nearest reproducible point, which is a common source of "the color
// is wrong" bug reports.
type Gamut struct {
Red XY `json:"red"`
Green XY `json:"green"`
Blue XY `json:"blue"`
}
type Color struct {
XY XY `json:"xy"`
Gamut *Gamut `json:"gamut,omitempty"`
GamutType string `json:"gamut_type,omitempty"`
}
// ColorTemperature is expressed in mireds, not kelvin. Mirek = 1e6 / kelvin.
type ColorTemperature struct {
Mirek int `json:"mirek"`
MirekValid bool `json:"mirek_valid"`
Schema *struct {
MirekMinimum int `json:"mirek_minimum"`
MirekMaximum int `json:"mirek_maximum"`
} `json:"mirek_schema,omitempty"`
}
// Light is a single controllable light service. It is owned by a device;
// a multi-head fixture presents several lights owned by one device.
type Light struct {
ID ID `json:"id"`
IDv1 string `json:"id_v1,omitempty"`
Type string `json:"type"`
Owner Ref `json:"owner"`
Metadata Metadata `json:"metadata"`
On On `json:"on"`
Dimming *Dimming `json:"dimming,omitempty"`
Color *Color `json:"color,omitempty"`
ColorTemperature *ColorTemperature `json:"color_temperature,omitempty"`
Mode string `json:"mode,omitempty"`
}
// Name is a convenience for the common case of wanting the user-facing label.
func (l Light) Name() string { return l.Metadata.Name }
// IsPlug reports whether this "light" is really a smart plug or outlet. The
// bridge models a plug as a light with archetype "plug" and no dimming or
// color -- so it turns on and off through exactly the same path as a bulb, but
// a UI that shows a brightness slider or color picker should hide those for a
// plug. Keying off the archetype is the only signal the model gives.
func (l Light) IsPlug() bool {
return l.Metadata.Archetype == "plug"
}
// LightUpdate is a partial update. Every field is optional; absent fields are
// left untouched by the bridge. Construct with the Bool/Float helpers:
//
// LightUpdate{On: &On{On: true}, Dimming: &DimmingUpdate{Brightness: Float(40)}}
type LightUpdate struct {
On *On `json:"on,omitempty"`
Dimming *DimmingUpdate `json:"dimming,omitempty"`
Color *ColorUpdate `json:"color,omitempty"`
ColorTemperature *ColorTemperatureUpdate `json:"color_temperature,omitempty"`
Dynamics *Dynamics `json:"dynamics,omitempty"`
Alert *Alert `json:"alert,omitempty"`
}
type DimmingUpdate struct {
Brightness *float64 `json:"brightness,omitempty"`
}
type ColorUpdate struct {
XY *XY `json:"xy,omitempty"`
}
type ColorTemperatureUpdate struct {
Mirek *int `json:"mirek,omitempty"`
}
// Dynamics controls the transition into the requested state. Duration is in
// milliseconds and is the single most useful field in the whole API: without
// it, every change is an abrupt step.
type Dynamics struct {
Duration *int `json:"duration,omitempty"`
}
// Alert triggers the identify behaviour, which is how you find out which
// physical bulb a UUID corresponds to.
type Alert struct {
Action string `json:"action"` // "breathe"
}
// LightService is the /resource/light endpoint.
type LightService struct{ c *Client }
func (s *LightService) List(ctx context.Context) ([]Light, error) {
return getMany[Light](ctx, s.c, "/resource/light")
}
func (s *LightService) Get(ctx context.Context, id ID) (Light, error) {
return getOne[Light](ctx, s.c, "/resource/light", id)
}
// ByName resolves a light by its user-facing label. Like Room.ByName, names
// are neither unique nor stable, so this is a convenience for scripts and the
// CLI rather than a foundation for a daemon. The first match wins.
func (s *LightService) ByName(ctx context.Context, name string) (Light, error) {
lights, err := s.List(ctx)
if err != nil {
return Light{}, err
}
for _, l := range lights {
if l.Name() == name {
return l, nil
}
}
return Light{}, fmt.Errorf("%w: no light named %q", ErrNotFound, name)
}
// Update applies a partial change, spending a token from the per-light bucket.
func (s *LightService) Update(ctx context.Context, id ID, u LightUpdate) error {
return put(ctx, s.c, "/resource/light", id, u, s.c.lightLim)
}
// SetOn is the shorthand everyone writes on day one.
func (s *LightService) SetOn(ctx context.Context, id ID, on bool) error {
return s.Update(ctx, id, LightUpdate{On: &On{On: on}})
}
// SetBrightness sets brightness as a percentage, optionally fading over
// transition milliseconds. Pass 0 for an immediate change.
func (s *LightService) SetBrightness(ctx context.Context, id ID, pct float64, transitionMS int) error {
u := LightUpdate{Dimming: &DimmingUpdate{Brightness: Float(pct)}}
if transitionMS > 0 {
u.Dynamics = &Dynamics{Duration: Int(transitionMS)}
}
return s.Update(ctx, id, u)
}
// Identify makes the light breathe so a human can pick it out of a ceiling.
func (s *LightService) Identify(ctx context.Context, id ID) error {
return s.Update(ctx, id, LightUpdate{Alert: &Alert{Action: "breathe"}})
}
// GroupedLight is the actuation service for a room or zone. Writing to it is
// dramatically cheaper than iterating members, because the bridge issues a
// Zigbee multicast rather than N unicasts.
type GroupedLight struct {
ID ID `json:"id"`
Type string `json:"type"`
Owner Ref `json:"owner"`
On On `json:"on"`
Dimming *Dimming `json:"dimming,omitempty"`
}
type GroupedLightService struct{ c *Client }
func (s *GroupedLightService) List(ctx context.Context) ([]GroupedLight, error) {
return getMany[GroupedLight](ctx, s.c, "/resource/grouped_light")
}
func (s *GroupedLightService) Get(ctx context.Context, id ID) (GroupedLight, error) {
return getOne[GroupedLight](ctx, s.c, "/resource/grouped_light", id)
}
// Update spends from the grouped-light bucket, which is an order of magnitude
// tighter than the per-light one.
func (s *GroupedLightService) Update(ctx context.Context, id ID, u LightUpdate) error {
return put(ctx, s.c, "/resource/grouped_light", id, u, s.c.groupLim)
}
func (s *GroupedLightService) SetOn(ctx context.Context, id ID, on bool) error {
return s.Update(ctx, id, LightUpdate{On: &On{On: on}})
}