lifx 0.0.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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