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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/Gemfile +10 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +71 -13
  6. data/Rakefile +12 -0
  7. data/bin/lifx-console +15 -0
  8. data/bin/lifx-snoop +50 -0
  9. data/examples/auto-off/Gemfile +3 -0
  10. data/examples/auto-off/auto-off.rb +35 -0
  11. data/examples/identify/Gemfile +3 -0
  12. data/examples/identify/identify.rb +70 -0
  13. data/examples/travis-build-light/Gemfile +4 -0
  14. data/examples/travis-build-light/build-light.rb +57 -0
  15. data/lib/bindata_ext/bool.rb +29 -0
  16. data/lib/bindata_ext/record.rb +11 -0
  17. data/lib/lifx/client.rb +136 -0
  18. data/lib/lifx/color.rb +190 -0
  19. data/lib/lifx/config.rb +12 -0
  20. data/lib/lifx/firmware.rb +55 -0
  21. data/lib/lifx/gateway_connection.rb +177 -0
  22. data/lib/lifx/light.rb +406 -0
  23. data/lib/lifx/light_collection.rb +105 -0
  24. data/lib/lifx/light_target.rb +189 -0
  25. data/lib/lifx/logging.rb +11 -0
  26. data/lib/lifx/message.rb +166 -0
  27. data/lib/lifx/network_context.rb +200 -0
  28. data/lib/lifx/observable.rb +46 -0
  29. data/lib/lifx/protocol/address.rb +21 -0
  30. data/lib/lifx/protocol/device.rb +225 -0
  31. data/lib/lifx/protocol/header.rb +24 -0
  32. data/lib/lifx/protocol/light.rb +110 -0
  33. data/lib/lifx/protocol/message.rb +17 -0
  34. data/lib/lifx/protocol/metadata.rb +21 -0
  35. data/lib/lifx/protocol/payload.rb +7 -0
  36. data/lib/lifx/protocol/sensor.rb +29 -0
  37. data/lib/lifx/protocol/type.rb +134 -0
  38. data/lib/lifx/protocol/wan.rb +50 -0
  39. data/lib/lifx/protocol/wifi.rb +76 -0
  40. data/lib/lifx/protocol_path.rb +84 -0
  41. data/lib/lifx/routing_manager.rb +110 -0
  42. data/lib/lifx/routing_table.rb +33 -0
  43. data/lib/lifx/seen.rb +15 -0
  44. data/lib/lifx/site.rb +89 -0
  45. data/lib/lifx/tag_manager.rb +105 -0
  46. data/lib/lifx/tag_table.rb +47 -0
  47. data/lib/lifx/target.rb +23 -0
  48. data/lib/lifx/timers.rb +18 -0
  49. data/lib/lifx/transport/tcp.rb +81 -0
  50. data/lib/lifx/transport/udp.rb +67 -0
  51. data/lib/lifx/transport.rb +41 -0
  52. data/lib/lifx/transport_manager/lan.rb +140 -0
  53. data/lib/lifx/transport_manager.rb +34 -0
  54. data/lib/lifx/utilities.rb +33 -0
  55. data/lib/lifx/version.rb +1 -1
  56. data/lib/lifx.rb +15 -1
  57. data/lifx.gemspec +11 -7
  58. data/spec/color_spec.rb +45 -0
  59. data/spec/gateway_connection_spec.rb +32 -0
  60. data/spec/integration/client_spec.rb +40 -0
  61. data/spec/integration/light_spec.rb +43 -0
  62. data/spec/integration/tags_spec.rb +31 -0
  63. data/spec/message_spec.rb +163 -0
  64. data/spec/protocol_path_spec.rb +109 -0
  65. data/spec/routing_manager_spec.rb +22 -0
  66. data/spec/spec_helper.rb +52 -0
  67. data/spec/transport/udp_spec.rb +38 -0
  68. data/spec/transport_spec.rb +14 -0
  69. 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
@@ -0,0 +1,11 @@
1
+ module LIFX
2
+ module Logging
3
+ def self.included(mod)
4
+ mod.extend(self)
5
+ end
6
+
7
+ def logger
8
+ LIFX::Config.logger
9
+ end
10
+ end
11
+ end