lifx 0.0.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +1 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +1 -1
- data/README.md +71 -13
- data/Rakefile +12 -0
- data/bin/lifx-console +15 -0
- data/bin/lifx-snoop +50 -0
- data/examples/auto-off/Gemfile +3 -0
- data/examples/auto-off/auto-off.rb +35 -0
- data/examples/identify/Gemfile +3 -0
- data/examples/identify/identify.rb +70 -0
- data/examples/travis-build-light/Gemfile +4 -0
- data/examples/travis-build-light/build-light.rb +57 -0
- data/lib/bindata_ext/bool.rb +29 -0
- data/lib/bindata_ext/record.rb +11 -0
- data/lib/lifx/client.rb +136 -0
- data/lib/lifx/color.rb +190 -0
- data/lib/lifx/config.rb +12 -0
- data/lib/lifx/firmware.rb +55 -0
- data/lib/lifx/gateway_connection.rb +177 -0
- data/lib/lifx/light.rb +406 -0
- data/lib/lifx/light_collection.rb +105 -0
- data/lib/lifx/light_target.rb +189 -0
- data/lib/lifx/logging.rb +11 -0
- data/lib/lifx/message.rb +166 -0
- data/lib/lifx/network_context.rb +200 -0
- data/lib/lifx/observable.rb +46 -0
- data/lib/lifx/protocol/address.rb +21 -0
- data/lib/lifx/protocol/device.rb +225 -0
- data/lib/lifx/protocol/header.rb +24 -0
- data/lib/lifx/protocol/light.rb +110 -0
- data/lib/lifx/protocol/message.rb +17 -0
- data/lib/lifx/protocol/metadata.rb +21 -0
- data/lib/lifx/protocol/payload.rb +7 -0
- data/lib/lifx/protocol/sensor.rb +29 -0
- data/lib/lifx/protocol/type.rb +134 -0
- data/lib/lifx/protocol/wan.rb +50 -0
- data/lib/lifx/protocol/wifi.rb +76 -0
- data/lib/lifx/protocol_path.rb +84 -0
- data/lib/lifx/routing_manager.rb +110 -0
- data/lib/lifx/routing_table.rb +33 -0
- data/lib/lifx/seen.rb +15 -0
- data/lib/lifx/site.rb +89 -0
- data/lib/lifx/tag_manager.rb +105 -0
- data/lib/lifx/tag_table.rb +47 -0
- data/lib/lifx/target.rb +23 -0
- data/lib/lifx/timers.rb +18 -0
- data/lib/lifx/transport/tcp.rb +81 -0
- data/lib/lifx/transport/udp.rb +67 -0
- data/lib/lifx/transport.rb +41 -0
- data/lib/lifx/transport_manager/lan.rb +140 -0
- data/lib/lifx/transport_manager.rb +34 -0
- data/lib/lifx/utilities.rb +33 -0
- data/lib/lifx/version.rb +1 -1
- data/lib/lifx.rb +15 -1
- data/lifx.gemspec +11 -7
- data/spec/color_spec.rb +45 -0
- data/spec/gateway_connection_spec.rb +32 -0
- data/spec/integration/client_spec.rb +40 -0
- data/spec/integration/light_spec.rb +43 -0
- data/spec/integration/tags_spec.rb +31 -0
- data/spec/message_spec.rb +163 -0
- data/spec/protocol_path_spec.rb +109 -0
- data/spec/routing_manager_spec.rb +22 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/transport/udp_spec.rb +38 -0
- data/spec/transport_spec.rb +14 -0
- metadata +143 -26
data/lib/lifx/light.rb
ADDED
@@ -0,0 +1,406 @@
|
|
1
|
+
require 'lifx/seen'
|
2
|
+
require 'lifx/color'
|
3
|
+
require 'lifx/target'
|
4
|
+
require 'lifx/light_target'
|
5
|
+
require 'lifx/firmware'
|
6
|
+
|
7
|
+
module LIFX
|
8
|
+
# LIFX::Light represents a Light device
|
9
|
+
class Light
|
10
|
+
include Seen
|
11
|
+
include LightTarget
|
12
|
+
include Logging
|
13
|
+
include Utilities
|
14
|
+
|
15
|
+
# @return [NetworkContext] NetworkContext the Light belongs to
|
16
|
+
attr_reader :context
|
17
|
+
|
18
|
+
# @return [String] Device ID
|
19
|
+
attr_reader :id
|
20
|
+
|
21
|
+
# @param context: [NetworkContext] {NetworkContext} the Light belongs to
|
22
|
+
# @param id: [String] Device ID of the Light
|
23
|
+
# @param site_id: [String] Site ID of the Light. Avoid using when possible.
|
24
|
+
# @param label: [String] Label of Light to prepopulate
|
25
|
+
def initialize(context:, id:, site_id: nil, label: nil)
|
26
|
+
@context = context
|
27
|
+
@id = id
|
28
|
+
@site_id = site_id
|
29
|
+
@label = label
|
30
|
+
@power = nil
|
31
|
+
@message_hooks = Hash.new { |h, k| h[k] = [] }
|
32
|
+
@context.register_device(self)
|
33
|
+
@message_signal = ConditionVariable.new
|
34
|
+
|
35
|
+
add_hooks
|
36
|
+
end
|
37
|
+
|
38
|
+
# Handles updating the internal state of the Light from incoming
|
39
|
+
# protocol messages.
|
40
|
+
# @api private
|
41
|
+
def handle_message(message, ip, transport)
|
42
|
+
payload = message.payload
|
43
|
+
|
44
|
+
@message_hooks[payload.class].each do |hook|
|
45
|
+
hook.call(payload)
|
46
|
+
end
|
47
|
+
@message_signal.broadcast
|
48
|
+
seen!
|
49
|
+
end
|
50
|
+
|
51
|
+
# Adds a block to be run when a payload of class `payload_class` is received
|
52
|
+
# @param payload_class [Class] Payload type to execute block on
|
53
|
+
# @param &hook [Proc] Hook to run
|
54
|
+
# @return [void]
|
55
|
+
def add_hook(payload_class, hook_arg = nil, &hook_block)
|
56
|
+
hook = block_given? ? hook_block : hook_arg
|
57
|
+
if !hook || !hook.is_a?(Proc)
|
58
|
+
raise "MUst pass a proc either as an argument or a block"
|
59
|
+
end
|
60
|
+
@message_hooks[payload_class] << hook
|
61
|
+
end
|
62
|
+
|
63
|
+
# Removes a hook added by {#add_hook}
|
64
|
+
# @param payload_class [Class] Payload type to delete hook from
|
65
|
+
# @param hook [Proc] The original hook passed into {#add_hook}
|
66
|
+
# @return [void]
|
67
|
+
def remove_hook(payload_class, hook)
|
68
|
+
@message_hooks[payload_class].delete(hook)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the color of the device.
|
72
|
+
# @param refresh: [Boolean] If true, will request for current color
|
73
|
+
# @param fetch: [Boolean] If false, it will not request current color if it's not cached
|
74
|
+
# @return [Color] Color
|
75
|
+
def color(refresh: false, fetch: true)
|
76
|
+
@color = nil if refresh
|
77
|
+
send_message!(Protocol::Light::Get.new, wait_for: Protocol::Light::Get) if fetch && !@color
|
78
|
+
@color
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the label of the light
|
82
|
+
# @param refresh: [Boolean] If true, will request for current label
|
83
|
+
# @param fetch: [Boolean] If false, it will not request current label if it's not cached
|
84
|
+
# @return [String] Label
|
85
|
+
def label(refresh: false, fetch: true)
|
86
|
+
@label = nil if refresh
|
87
|
+
send_message!(Protocol::Light::Get.new, wait_for: Protocol::Light::Get) if fetch && !@label
|
88
|
+
@label
|
89
|
+
end
|
90
|
+
|
91
|
+
MAX_LABEL_LENGTH = 32
|
92
|
+
class LabelTooLong < ArgumentError; end
|
93
|
+
|
94
|
+
# Sets the label of the light
|
95
|
+
# @param label [String] Desired label
|
96
|
+
# @raise [LabelTooLong] if label is greater than {MAX_LABEL_LENGTH}
|
97
|
+
# @return [Light] self
|
98
|
+
def set_label(label)
|
99
|
+
if label.length > MAX_LABEL_LENGTH
|
100
|
+
raise LabelTooLong.new("Label length must be below or equal to #{MAX_LABEL_LENGTH}")
|
101
|
+
end
|
102
|
+
while self.label != label
|
103
|
+
send_message!(Protocol::Device::SetLabel.new(label: label), wait_for: Protocol::Device::StateLabel)
|
104
|
+
end
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
# Set the power state to `level` synchronously.
|
109
|
+
# This method cannot guarantee the message was received.
|
110
|
+
# @param level [0, 1] 0 for off, 1 for on
|
111
|
+
# @return [Light, LightCollection] self for chaining
|
112
|
+
def set_power!(level)
|
113
|
+
send_message!(Protocol::Device::SetPower.new(level: level), wait_for: Protocol::Device::StatePower) do |payload|
|
114
|
+
if level.zero?
|
115
|
+
payload.level == 0
|
116
|
+
else
|
117
|
+
payload.level > 0
|
118
|
+
end
|
119
|
+
end
|
120
|
+
self
|
121
|
+
end
|
122
|
+
|
123
|
+
# Turns the light(s) on synchronously
|
124
|
+
# @return [Light, LightCollection] self for chaining
|
125
|
+
def turn_on!
|
126
|
+
set_power!(1)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Turns the light(s) off synchronously
|
130
|
+
# @return [Light, LightCollection]
|
131
|
+
def turn_off!
|
132
|
+
set_power!(0)
|
133
|
+
end
|
134
|
+
|
135
|
+
# @return [Boolean] Returns true if device is on
|
136
|
+
def on?(**kwargs)
|
137
|
+
power(**kwargs) == :on
|
138
|
+
end
|
139
|
+
|
140
|
+
# @return [Boolean] Returns true if device is off
|
141
|
+
def off?(**kwargs)
|
142
|
+
power(**kwargs) == :off
|
143
|
+
end
|
144
|
+
|
145
|
+
# @param refresh: see #label
|
146
|
+
# @param fetch: see #label
|
147
|
+
# @return [:unknown, :off, :on] Light power state
|
148
|
+
def power(refresh: false, fetch: true)
|
149
|
+
@power = nil if refresh
|
150
|
+
send_message!(Protocol::Device::GetPower.new, wait_for: Protocol::Device::StatePower) if !@power && fetch
|
151
|
+
case @power
|
152
|
+
when nil
|
153
|
+
:unknown
|
154
|
+
when 0
|
155
|
+
:off
|
156
|
+
else
|
157
|
+
:on
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Returns the local time of the light
|
162
|
+
# @return [Time]
|
163
|
+
def time
|
164
|
+
send_message!(Protocol::Device::GetTime.new, wait_for: Protocol::Device::StateTime) do |payload|
|
165
|
+
Time.at(payload.time.to_f / NSEC_IN_SEC)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns the difference between the device time and time on the current machine
|
170
|
+
# Positive values means device time is further in the future.
|
171
|
+
# @return [Float]
|
172
|
+
def time_delta
|
173
|
+
device_time = time
|
174
|
+
delta = device_time - Time.now
|
175
|
+
end
|
176
|
+
|
177
|
+
# Pings the device and measures response time.
|
178
|
+
# @return [Float] Latency from sending a message to receiving a response.
|
179
|
+
def latency
|
180
|
+
start = Time.now.to_f
|
181
|
+
send_message!(Protocol::Device::GetTime.new, wait_for: Protocol::Device::StateTime)
|
182
|
+
Time.now.to_f - start
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns the mesh firmware details
|
186
|
+
# @api private
|
187
|
+
# @return [Hash] firmware details
|
188
|
+
def mesh_firmware(fetch: true)
|
189
|
+
@mesh_firmware ||= begin
|
190
|
+
send_message!(Protocol::Device::GetMeshFirmware.new,
|
191
|
+
wait_for: Protocol::Device::StateMeshFirmware) do |payload|
|
192
|
+
Firmware.new(payload)
|
193
|
+
end if fetch
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Returns the wifi firmware details
|
198
|
+
# @api private
|
199
|
+
# @return [Hash] firmware details
|
200
|
+
def wifi_firmware(fetch: true)
|
201
|
+
@wifi_firmware ||= begin
|
202
|
+
send_message!(Protocol::Device::GetWifiFirmware.new,
|
203
|
+
wait_for: Protocol::Device::StateWifiFirmware) do |payload|
|
204
|
+
Firmware.new(payload)
|
205
|
+
end if fetch
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Returns the temperature of the device
|
210
|
+
# @return [Float] Temperature in Celcius
|
211
|
+
def temperature
|
212
|
+
send_message!(Protocol::Light::GetTemperature.new,
|
213
|
+
wait_for: Protocol::Light::StateTemperature) do |payload|
|
214
|
+
payload.temperature / 100.0
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Returns mesh network info
|
219
|
+
# @api private
|
220
|
+
# @return [Hash] Mesh network info
|
221
|
+
def mesh_info
|
222
|
+
send_message!(Protocol::Device::GetMeshInfo.new,
|
223
|
+
wait_for: Protocol::Device::StateMeshInfo) do |payload|
|
224
|
+
{
|
225
|
+
signal: payload.signal, # This is in Milliwatts
|
226
|
+
tx: payload.tx,
|
227
|
+
rx: payload.rx
|
228
|
+
}
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Returns wifi network info
|
233
|
+
# @api private
|
234
|
+
# @return [Hash] wifi network info
|
235
|
+
def wifi_info
|
236
|
+
send_message!(Protocol::Device::GetWifiInfo.new,
|
237
|
+
wait_for: Protocol::Device::StateWifiInfo) do |payload|
|
238
|
+
{
|
239
|
+
signal: payload.signal, # This is in Milliwatts
|
240
|
+
tx: payload.tx,
|
241
|
+
rx: payload.rx
|
242
|
+
}
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Returns version info
|
247
|
+
# @api private
|
248
|
+
# @return [Hash] version info
|
249
|
+
def version
|
250
|
+
send_message!(Protocol::Device::GetVersion.new,
|
251
|
+
wait_for: Protocol::Device::StateVersion) do |payload|
|
252
|
+
{
|
253
|
+
vendor: payload.vendor,
|
254
|
+
product: payload.product,
|
255
|
+
version: payload.version
|
256
|
+
}
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Return device uptime
|
261
|
+
# @api private
|
262
|
+
# @return [Float] Device uptime in seconds
|
263
|
+
def uptime
|
264
|
+
send_message!(Protocol::Device::GetInfo.new,
|
265
|
+
wait_for: Protocol::Device::StateInfo) do |payload|
|
266
|
+
payload.uptime.to_f / NSEC_IN_SEC
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Return device last downtime
|
271
|
+
# @api private
|
272
|
+
# @return [Float] Device's last downtime in secodns
|
273
|
+
def last_downtime
|
274
|
+
send_message!(Protocol::Device::GetInfo.new,
|
275
|
+
wait_for: Protocol::Device::StateInfo) do |payload|
|
276
|
+
payload.downtime.to_f / NSEC_IN_SEC
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# Returns the `site_id` the Light belongs to.
|
281
|
+
# @api private
|
282
|
+
# @return [String]
|
283
|
+
def site_id
|
284
|
+
if @site_id.nil?
|
285
|
+
# FIXME: This is ugly.
|
286
|
+
context.routing_manager.routing_table.site_id_for_device_id(id)
|
287
|
+
else
|
288
|
+
@site_id
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# Returns the tags uint64 bitfield for protocol use.
|
293
|
+
# @api private
|
294
|
+
# @return [Integer]
|
295
|
+
def tags_field
|
296
|
+
try_until -> { @tags_field } do
|
297
|
+
send_message(Protocol::Device::GetTags.new)
|
298
|
+
end
|
299
|
+
@tags_field
|
300
|
+
end
|
301
|
+
|
302
|
+
# Add tag to the Light
|
303
|
+
# @param tag [String] The tag to add
|
304
|
+
# @return [Light] self
|
305
|
+
def add_tag(tag)
|
306
|
+
context.add_tag_to_device(tag: tag, device: self)
|
307
|
+
self
|
308
|
+
end
|
309
|
+
|
310
|
+
# Remove tag from the Light
|
311
|
+
# @param tag [String] The tag to remove
|
312
|
+
# @return [Light] self
|
313
|
+
def remove_tag(tag)
|
314
|
+
context.remove_tag_from_device(tag: tag, device: self)
|
315
|
+
self
|
316
|
+
end
|
317
|
+
|
318
|
+
# Returns the tags that are associated with the Light
|
319
|
+
# @return [Array<String>] tags
|
320
|
+
def tags
|
321
|
+
context.tags_for_device(self)
|
322
|
+
end
|
323
|
+
|
324
|
+
# Returns a nice string representation of the Light
|
325
|
+
def to_s
|
326
|
+
%Q{#<LIFX::Light id=#{id} label=#{label(fetch: false)} power=#{power(fetch: false)}>}.force_encoding(Encoding.default_external)
|
327
|
+
end
|
328
|
+
alias_method :inspect, :to_s
|
329
|
+
|
330
|
+
# Compare current Light to another light
|
331
|
+
# @param other [Light]
|
332
|
+
# @return [-1, 0, 1] Comparison value
|
333
|
+
def <=>(other)
|
334
|
+
raise ArgumentError.new("Comparison of #{self} with #{other} failed") unless other.is_a?(LIFX::Light)
|
335
|
+
[label, id, 0] <=> [other.label, other.id, 0]
|
336
|
+
end
|
337
|
+
|
338
|
+
# Queues a message to be sent the Light
|
339
|
+
# @param payload [Protocol::Payload] the payload to send
|
340
|
+
# @param acknowledge: [Boolean] whether the device should respond
|
341
|
+
# @return [Light] returns self for chaining
|
342
|
+
def send_message(payload, acknowledge: true)
|
343
|
+
context.send_message(target: Target.new(device_id: id, site_id: @site_id), payload: payload, acknowledge: acknowledge)
|
344
|
+
self
|
345
|
+
end
|
346
|
+
|
347
|
+
# Queues a message to be sent to the Light and waits for a response
|
348
|
+
# @param payload [Protocol::Payload] the payload to send
|
349
|
+
# @param wait_for: [Class] the payload class to wait for
|
350
|
+
# @param wait_timeout: [Numeric] wait timeout
|
351
|
+
# @param block: [Proc] the block that is executed when the expected `wait_for` payload comes back. If the return value is false or nil, it will try to send the message again.
|
352
|
+
# @return [Object] the truthy result of `block` is returned.
|
353
|
+
# @raise [Timeout::Error]
|
354
|
+
def send_message!(payload, wait_for:, wait_timeout: 3, timeout_exception: Timeout::Error, &block)
|
355
|
+
if Thread.current[:sync_enabled]
|
356
|
+
raise "Cannot use synchronous methods inside a sync block"
|
357
|
+
end
|
358
|
+
|
359
|
+
result = nil
|
360
|
+
begin
|
361
|
+
block ||= Proc.new { |msg| true }
|
362
|
+
proc = -> (payload) {
|
363
|
+
result = block.call(payload)
|
364
|
+
}
|
365
|
+
add_hook(wait_for, proc)
|
366
|
+
try_until -> { result }, signal: @message_signal, timeout_exception: timeout_exception do
|
367
|
+
send_message(payload)
|
368
|
+
end
|
369
|
+
result
|
370
|
+
ensure
|
371
|
+
remove_hook(wait_for, proc)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
protected
|
376
|
+
|
377
|
+
def add_hooks
|
378
|
+
add_hook(Protocol::Device::StateLabel) do |payload|
|
379
|
+
@label = payload.label.to_s
|
380
|
+
end
|
381
|
+
|
382
|
+
add_hook(Protocol::Light::State) do |payload|
|
383
|
+
@label = payload.label.to_s
|
384
|
+
@color = Color.from_struct(payload.color.snapshot)
|
385
|
+
@power = payload.power.to_i
|
386
|
+
@tags_field = payload.tags
|
387
|
+
end
|
388
|
+
|
389
|
+
add_hook(Protocol::Device::StateTags) do |payload|
|
390
|
+
@tags_field = payload.tags
|
391
|
+
end
|
392
|
+
|
393
|
+
add_hook(Protocol::Device::StatePower) do |payload|
|
394
|
+
@power = payload.level.to_i
|
395
|
+
end
|
396
|
+
|
397
|
+
add_hook(Protocol::Device::StateMeshFirmware) do |payload|
|
398
|
+
@mesh_firmware = Firmware.new(payload)
|
399
|
+
end
|
400
|
+
|
401
|
+
add_hook(Protocol::Device::StateWifiFirmware) do |payload|
|
402
|
+
@wifi_firmware = Firmware.new(payload)
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'lifx/light_target'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
# LightCollection represents a collection of {Light}s, which can either refer to
|
5
|
+
# all lights on a {NetworkContext}, or lights
|
6
|
+
class LightCollection
|
7
|
+
include LightTarget
|
8
|
+
include Enumerable
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
class TagNotFound < ArgumentError; end
|
12
|
+
|
13
|
+
# Refers to {NetworkContext} the instance belongs to
|
14
|
+
# @return [NetworkContext]
|
15
|
+
attr_reader :context
|
16
|
+
|
17
|
+
# Tag of the collection. `nil` represents all lights
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :tag
|
20
|
+
|
21
|
+
# Creates a {LightCollection} instance. Should not be used directly.
|
22
|
+
# @api private
|
23
|
+
# @param context: [NetworkContext] NetworkContext this collection belongs to
|
24
|
+
# @param tag: [String] Tag
|
25
|
+
def initialize(context:, tag: nil)
|
26
|
+
@context = context
|
27
|
+
@tag = tag
|
28
|
+
end
|
29
|
+
|
30
|
+
# Queues a {Protocol::Payload} to be sent to bulbs in the collection
|
31
|
+
# @param payload [Protocol::Payload] Payload to be sent
|
32
|
+
# @param acknowledge: [Boolean] whether recipients should acknowledge message
|
33
|
+
# @api private
|
34
|
+
# @return [LightCollection] self for chaining
|
35
|
+
def send_message(payload, acknowledge: false)
|
36
|
+
if tag
|
37
|
+
context.send_message(target: Target.new(tag: tag), payload: payload, acknowledge: acknowledge)
|
38
|
+
else
|
39
|
+
context.send_message(target: Target.new(broadcast: true), payload: payload, acknowledge: acknowledge)
|
40
|
+
end
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns a {Light} with device id matching `id`
|
45
|
+
# @param id [String] Device ID
|
46
|
+
# @return [Light]
|
47
|
+
def with_id(id)
|
48
|
+
lights.find { |l| l.id == id}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a {Light} with its label matching `label`
|
52
|
+
# @param label [String, Regexp] Label
|
53
|
+
# @return [Light]
|
54
|
+
def with_label(label)
|
55
|
+
if label.is_a?(Regexp)
|
56
|
+
lights.find { |l| l.label(fetch: false) =~ label }
|
57
|
+
else
|
58
|
+
lights.find { |l| l.label(fetch: false) == label }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns a {LightCollection} of {Light}s tagged with `tag`
|
63
|
+
# @param tag [String] Tag
|
64
|
+
# @return [LightCollection]
|
65
|
+
def with_tag(tag)
|
66
|
+
if context.tags.include?(tag)
|
67
|
+
self.class.new(context: context, tag: tag)
|
68
|
+
else
|
69
|
+
raise TagNotFound.new("No such tag '#{tag}'")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns an Array of {Light}s
|
74
|
+
# @return [Array<Light>]
|
75
|
+
def lights
|
76
|
+
if tag
|
77
|
+
context.all_lights.select { |l| l.tags.include?(tag) }
|
78
|
+
else
|
79
|
+
context.all_lights
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
DEFAULT_ALIVE_THRESHOLD = 30 # seconds
|
84
|
+
# Returns an Array of {Light}s considered alive
|
85
|
+
# @param threshold: The maximum number of seconds a {Light} was last seen to be considered alive
|
86
|
+
def alive(threshold: DEFAULT_ALIVE_THRESHOLD)
|
87
|
+
lights.select { |l| l.seconds_since_seen <= threshold }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns an Array of {Light}s considered stale
|
91
|
+
# @param threshold: The minimum number of seconds since a {Light} was last seen to be considered stale
|
92
|
+
def stale(threshold: DEFAULT_ALIVE_THRESHOLD)
|
93
|
+
lights.select { |l| l.seconds_since_seen > threshold }
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns a nice string representation of itself
|
97
|
+
# @return [String]
|
98
|
+
def to_s
|
99
|
+
%Q{#<#{self.class.name} lights=#{lights}#{tag ? " tag=#{tag}" : ''}>}
|
100
|
+
end
|
101
|
+
alias_method :inspect, :to_s
|
102
|
+
|
103
|
+
def_delegators :lights, :empty?, :each
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
module LIFX
|
2
|
+
# LightTarget is a module that contains Light commands that can work
|
3
|
+
# with either a single {Light} or multiple Lights via a {LightCollection}
|
4
|
+
module LightTarget
|
5
|
+
MSEC_PER_SEC = 1000
|
6
|
+
|
7
|
+
# Attempts to set the color of the light(s) to `color` asynchronously.
|
8
|
+
# This method cannot guarantee that the message was received.
|
9
|
+
# @param color [Color] The color to be set
|
10
|
+
# @param duration: [Numeric] Transition time in seconds
|
11
|
+
# @return [Light, LightCollection] self for chaining
|
12
|
+
def set_color(color, duration: LIFX::Config.default_duration)
|
13
|
+
send_message(Protocol::Light::Set.new(
|
14
|
+
color: color.to_hsbk,
|
15
|
+
duration: (duration * MSEC_PER_SEC).to_i,
|
16
|
+
stream: 0,
|
17
|
+
))
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# Attempts to apply a waveform to the light(s) asynchronously.
|
22
|
+
# @note Don't use this directly.
|
23
|
+
# @api private
|
24
|
+
def set_waveform(color, waveform:,
|
25
|
+
cycles:,
|
26
|
+
stream: 0,
|
27
|
+
transient: true,
|
28
|
+
period: 1.0,
|
29
|
+
duty_cycle: 0.5,
|
30
|
+
acknowledge: false)
|
31
|
+
send_message(Protocol::Light::SetWaveform.new(
|
32
|
+
color: color.to_hsbk,
|
33
|
+
waveform: waveform,
|
34
|
+
cycles: cycles,
|
35
|
+
stream: stream,
|
36
|
+
transient: transient,
|
37
|
+
period: (period * 1_000).to_i,
|
38
|
+
duty_cycle: (duty_cycle * 65535).round - 32768
|
39
|
+
), acknowledge: acknowledge)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Attempts to make the light(s) pulse `color` and then back to its original color. Asynchronous.
|
43
|
+
# @param color [Color] Color to pulse
|
44
|
+
# @param duty_cycle: [Float] Percentage of a cycle the light(s) is set to `color`
|
45
|
+
# @param cycles: [Integer] Number of cycles
|
46
|
+
# @param transient: [Boolean] If false, the light will remain at the color the waveform is at when it ends
|
47
|
+
# @param period: [Integer] Number of seconds a cycle. Must be above 1.0 (?)
|
48
|
+
# @param stream: [Integer] Unused
|
49
|
+
def pulse(color, cycles: 1,
|
50
|
+
duty_cycle: 0.5,
|
51
|
+
transient: true,
|
52
|
+
period: 1.0,
|
53
|
+
stream: 0)
|
54
|
+
set_waveform(color, waveform: Protocol::Light::Waveform::PULSE,
|
55
|
+
cycles: cycles,
|
56
|
+
duty_cycle: duty_cycle,
|
57
|
+
stream: stream,
|
58
|
+
transient: transient,
|
59
|
+
period: period)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Attempts to make the light(s) transition to `color` and back in a smooth sine wave. Asynchronous.
|
63
|
+
# @param color [Color] Color
|
64
|
+
# @param cycles: [Integer] Number of cycles
|
65
|
+
# @param transient: [Boolean] If false, the light will remain at the color the waveform is at when it ends
|
66
|
+
# @param period: [Integer] Number of seconds a cycle. Must be above 1.0 (?)
|
67
|
+
# @param stream: [Integer] Unused
|
68
|
+
def sine(color, cycles: 1,
|
69
|
+
period: 1.0,
|
70
|
+
transient: true,
|
71
|
+
stream: 0)
|
72
|
+
set_waveform(color, waveform: Protocol::Light::Waveform::SINE,
|
73
|
+
cycles: cycles,
|
74
|
+
duty_cycle: 0,
|
75
|
+
stream: stream,
|
76
|
+
transient: transient,
|
77
|
+
period: period)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Attempts to make the light(s) transition to `color` smoothly, then immediately back to its original color. Asynchronous.
|
81
|
+
# @param color [Color] Color
|
82
|
+
# @param cycles: [Integer] Number of cycles
|
83
|
+
# @param transient: [Boolean] If false, the light will remain at the color the waveform is at when it ends
|
84
|
+
# @param period: [Integer] Number of seconds a cycle. Must be above 1.0 (?)
|
85
|
+
# @param stream: [Integer] Unused
|
86
|
+
def half_sine(color, cycles: 1,
|
87
|
+
period: 1.0,
|
88
|
+
transient: true,
|
89
|
+
stream: 0)
|
90
|
+
set_waveform(color, waveform: Protocol::Light::Waveform::HALF_SINE,
|
91
|
+
cycles: cycles,
|
92
|
+
stream: stream,
|
93
|
+
transient: transient,
|
94
|
+
period: period)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Attempts to make the light(s) transition to `color` linearly and back. Asynchronous.
|
98
|
+
# @param color [Color] Color to pulse
|
99
|
+
# @param cycles: [Integer] Number of cycles
|
100
|
+
# @param transient: [Boolean] If false, the light will remain at the color the waveform is at when it ends
|
101
|
+
# @param period: [Integer] Number of seconds a cycle. Must be above 1.0 (?)
|
102
|
+
# @param stream: [Integer] Unused
|
103
|
+
def triangle(color, cycles: 1,
|
104
|
+
period: 1.0,
|
105
|
+
transient: true,
|
106
|
+
stream: 0)
|
107
|
+
set_waveform(color, waveform: Protocol::Light::Waveform::TRIANGLE,
|
108
|
+
cycles: cycles,
|
109
|
+
stream: stream,
|
110
|
+
transient: transient,
|
111
|
+
period: period)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Attempts to make the light(s) transition to `color` linearly, then instantly back. Asynchronous.
|
115
|
+
# @param color [Color] Color to saw wave
|
116
|
+
# @param cycles: [Integer] Number of cycles
|
117
|
+
# @param transient: [Boolean] If false, the light will remain at the color the waveform is at when it ends
|
118
|
+
# @param period: [Integer] Number of seconds a cycle. Must be above 1.0 (?)
|
119
|
+
# @param stream: [Integer] Unused
|
120
|
+
def saw(color, cycles: 1,
|
121
|
+
period: 1.0,
|
122
|
+
transient: true,
|
123
|
+
stream: 0)
|
124
|
+
set_waveform(color, waveform: Protocol::Light::Waveform::SAW,
|
125
|
+
cycles: cycles,
|
126
|
+
stream: stream,
|
127
|
+
transient: transient,
|
128
|
+
period: period)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Attempts to set the power state to `value` asynchronously.
|
132
|
+
# This method cannot guarantee the message was received.
|
133
|
+
# @param value [0, 1] 0 for off, 1 for on
|
134
|
+
# @return [Light, LightCollection] self for chaining
|
135
|
+
def set_power(value)
|
136
|
+
send_message(Protocol::Device::SetPower.new(level: value))
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
# Attempts to turn the light(s) on asynchronously.
|
141
|
+
# This method cannot guarantee the message was received.
|
142
|
+
# @return [Light, LightCollection] self for chaining
|
143
|
+
def turn_on
|
144
|
+
set_power(1)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Attempts to turn the light(s) off asynchronously.
|
148
|
+
# This method cannot guarantee the message was received.
|
149
|
+
# @return [Light, LightCollection] self for chaining
|
150
|
+
def turn_off
|
151
|
+
set_power(0)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Requests light(s) to report their state
|
155
|
+
# This method cannot guarantee the message was received.
|
156
|
+
# @return [Light, LightCollection] self for chaining
|
157
|
+
def refresh
|
158
|
+
send_message(Protocol::Light::Get.new)
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
# Attempts to set the site id of the light.
|
163
|
+
# Will clear label and tags. This method cannot guarantee message receipt.
|
164
|
+
# @note Don't use this unless you know what you're doing.
|
165
|
+
# @api private
|
166
|
+
# @param site_id [String] Site ID
|
167
|
+
# @return [void]
|
168
|
+
def set_site_id(site_id)
|
169
|
+
send_message(Protocol::Device::SetSite.new(site: [site_id].pack('H*')))
|
170
|
+
end
|
171
|
+
|
172
|
+
NSEC_IN_SEC = 1_000_000_000
|
173
|
+
# Attempts to set the device time on the targets
|
174
|
+
# @api private
|
175
|
+
# @param time [Time] The time to set
|
176
|
+
# @return [void]
|
177
|
+
def set_time(time = Time.now)
|
178
|
+
send_message(Protocol::Device::SetTime.new(time: (time.to_f * NSEC_IN_SEC).round))
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
# Attempts to reboots the light(s).
|
183
|
+
# This method cannot guarantee the message was received.
|
184
|
+
# @return [Light, LightCollection] self for chaining
|
185
|
+
def reboot!
|
186
|
+
send_message(Protocol::Device::Reboot.new)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|