lifx-lan 0.1.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 (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +8 -0
  4. data/.yardopts +3 -0
  5. data/CHANGES.md +45 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +23 -0
  8. data/README.md +15 -0
  9. data/Rakefile +20 -0
  10. data/bin/lifx-snoop +50 -0
  11. data/examples/auto-off/auto-off.rb +34 -0
  12. data/examples/blink/blink.rb +19 -0
  13. data/examples/identify/identify.rb +69 -0
  14. data/examples/travis-build-light/build-light.rb +57 -0
  15. data/lib/bindata_ext/bool.rb +30 -0
  16. data/lib/bindata_ext/record.rb +11 -0
  17. data/lib/lifx-lan.rb +27 -0
  18. data/lib/lifx/lan/client.rb +149 -0
  19. data/lib/lifx/lan/color.rb +199 -0
  20. data/lib/lifx/lan/config.rb +17 -0
  21. data/lib/lifx/lan/firmware.rb +60 -0
  22. data/lib/lifx/lan/gateway_connection.rb +185 -0
  23. data/lib/lifx/lan/light.rb +440 -0
  24. data/lib/lifx/lan/light_collection.rb +111 -0
  25. data/lib/lifx/lan/light_target.rb +185 -0
  26. data/lib/lifx/lan/logging.rb +14 -0
  27. data/lib/lifx/lan/message.rb +168 -0
  28. data/lib/lifx/lan/network_context.rb +188 -0
  29. data/lib/lifx/lan/observable.rb +66 -0
  30. data/lib/lifx/lan/protocol/address.rb +25 -0
  31. data/lib/lifx/lan/protocol/device.rb +387 -0
  32. data/lib/lifx/lan/protocol/header.rb +24 -0
  33. data/lib/lifx/lan/protocol/light.rb +142 -0
  34. data/lib/lifx/lan/protocol/message.rb +19 -0
  35. data/lib/lifx/lan/protocol/metadata.rb +23 -0
  36. data/lib/lifx/lan/protocol/payload.rb +12 -0
  37. data/lib/lifx/lan/protocol/sensor.rb +31 -0
  38. data/lib/lifx/lan/protocol/type.rb +204 -0
  39. data/lib/lifx/lan/protocol/wan.rb +51 -0
  40. data/lib/lifx/lan/protocol/wifi.rb +102 -0
  41. data/lib/lifx/lan/protocol_path.rb +85 -0
  42. data/lib/lifx/lan/required_keyword_arguments.rb +12 -0
  43. data/lib/lifx/lan/routing_manager.rb +114 -0
  44. data/lib/lifx/lan/routing_table.rb +48 -0
  45. data/lib/lifx/lan/seen.rb +25 -0
  46. data/lib/lifx/lan/site.rb +97 -0
  47. data/lib/lifx/lan/tag_manager.rb +111 -0
  48. data/lib/lifx/lan/tag_table.rb +49 -0
  49. data/lib/lifx/lan/target.rb +24 -0
  50. data/lib/lifx/lan/thread.rb +13 -0
  51. data/lib/lifx/lan/timers.rb +29 -0
  52. data/lib/lifx/lan/transport.rb +46 -0
  53. data/lib/lifx/lan/transport/tcp.rb +91 -0
  54. data/lib/lifx/lan/transport/udp.rb +87 -0
  55. data/lib/lifx/lan/transport_manager.rb +43 -0
  56. data/lib/lifx/lan/transport_manager/lan.rb +169 -0
  57. data/lib/lifx/lan/utilities.rb +36 -0
  58. data/lib/lifx/lan/version.rb +5 -0
  59. data/lifx-lan.gemspec +26 -0
  60. data/spec/color_spec.rb +43 -0
  61. data/spec/gateway_connection_spec.rb +30 -0
  62. data/spec/integration/client_spec.rb +42 -0
  63. data/spec/integration/light_spec.rb +56 -0
  64. data/spec/integration/tags_spec.rb +42 -0
  65. data/spec/light_collection_spec.rb +37 -0
  66. data/spec/message_spec.rb +183 -0
  67. data/spec/protocol_path_spec.rb +109 -0
  68. data/spec/routing_manager_spec.rb +25 -0
  69. data/spec/routing_table_spec.rb +23 -0
  70. data/spec/spec_helper.rb +56 -0
  71. data/spec/transport/udp_spec.rb +44 -0
  72. data/spec/transport_spec.rb +14 -0
  73. metadata +187 -0
@@ -0,0 +1,185 @@
1
+ require 'lifx/lan/observable'
2
+ require 'lifx/lan/timers'
3
+
4
+ module LIFX
5
+ module LAN
6
+ # @api private
7
+ # @private
8
+ class GatewayConnection
9
+ # GatewayConnection handles the UDP and TCP connections to the gateway
10
+ # A GatewayConnection is created when a new device sends a StateService
11
+ include Timers
12
+ include Logging
13
+ include Observable
14
+
15
+ MAX_TCP_ATTEMPTS = 3
16
+ def initialize
17
+ @threads = []
18
+ @tcp_attempts = 0
19
+ @threads << initialize_write_queue
20
+ end
21
+
22
+ def handle_message(message, ip, transport)
23
+ payload = message.payload
24
+ case payload
25
+ when Protocol::Device::StateService
26
+ if use_udp? && !udp_connected? && payload.service == Protocol::Device::Service::UDP
27
+ # UDP transport here is only for sending directly to bulb
28
+ # We receive responses via UDP transport listening to broadcast in Network
29
+ connect_udp(ip, payload.port.to_i)
30
+ elsif use_tcp? && !tcp_connected? && payload.service == Protocol::Device::Service::TCP && (port = payload.port.snapshot) > 0
31
+ connect_tcp(ip, port)
32
+ end
33
+ else
34
+ logger.error("#{self}: Unhandled message: #{message}")
35
+ end
36
+ end
37
+
38
+ def use_udp?
39
+ Config.allowed_transports.include?(:udp)
40
+ end
41
+
42
+ def use_tcp?
43
+ Config.allowed_transports.include?(:tcp)
44
+ end
45
+
46
+ def udp_connected?
47
+ @udp_transport && @udp_transport.connected?
48
+ end
49
+
50
+ def tcp_connected?
51
+ @tcp_transport && @tcp_transport.connected?
52
+ end
53
+
54
+ def connected?
55
+ udp_connected? || tcp_connected?
56
+ end
57
+
58
+ def connect_udp(ip, port)
59
+ @udp_transport = Transport::UDP.new(ip, port)
60
+ end
61
+
62
+ def connect_tcp(ip, port)
63
+ if @tcp_attempts > MAX_TCP_ATTEMPTS
64
+ logger.info("#{self}: Ignoring TCP service of #{ip}:#{port} due to too many failed attempts.")
65
+ return
66
+ end
67
+ @tcp_attempts += 1
68
+ logger.info("#{self}: Establishing connection to #{ip}:#{port}")
69
+ @tcp_transport = Transport::TCP.new(ip, port)
70
+ @tcp_transport.add_observer(self, :message_received) do |message: nil, ip: nil, transport: nil|
71
+ notify_observers(:message_received, message: message, ip: ip, transport: @tcp_transport)
72
+ end
73
+ @tcp_transport.listen
74
+ end
75
+
76
+ def write(message)
77
+ @queue.push(message)
78
+ end
79
+
80
+ def close
81
+ @threads.each do |thr|
82
+ thr.abort
83
+ end
84
+ [@tcp_transport, @udp_transport].compact.each(&:close)
85
+ end
86
+
87
+ def flush(timeout: nil)
88
+ proc = lambda do
89
+ while !@queue.empty?
90
+ sleep 0.05
91
+ end
92
+ end
93
+ if timeout
94
+ Timeout.timeout(timeout, TimeoutError) do
95
+ proc.call
96
+ end
97
+ else
98
+ proc.call
99
+ end
100
+ end
101
+
102
+ def to_s
103
+ "#<LIFX::GatewayConnection tcp=#{@tcp_transport} tcp_attempts=#{@tcp_attempts} udp=#{@udp_transport}>"
104
+ end
105
+ alias_method :inspect, :to_s
106
+
107
+ def set_message_rate(rate)
108
+ @message_rate = rate
109
+ end
110
+ protected
111
+
112
+ MAXIMUM_QUEUE_LENGTH = 10
113
+ DEFAULT_MESSAGE_RATE = 5
114
+ def message_rate
115
+ @message_rate || DEFAULT_MESSAGE_RATE
116
+ end
117
+
118
+ def initialize_write_queue
119
+ @queue = SizedQueue.new(MAXIMUM_QUEUE_LENGTH)
120
+ @last_write = Time.now
121
+ Thread.start do
122
+ loop do
123
+ if !connected?
124
+ sleep 0.1
125
+ next
126
+ end
127
+ delay = [(1.0 / message_rate) - (Time.now - @last_write), 0].max
128
+ logger.debug("#{self}: Sleeping for #{delay}")
129
+ sleep(delay)
130
+ message = @queue.pop
131
+ if !message.is_a?(Message)
132
+ raise ArgumentError.new("Unexpected object in message queue: #{message.inspect}")
133
+ end
134
+ if !actually_write(message)
135
+ logger.error("#{self}: Couldn't write, pushing back onto queue.")
136
+ @queue << message
137
+ end
138
+ @last_write = Time.now
139
+ end
140
+ end
141
+ end
142
+
143
+ def check_connections
144
+ if @tcp_transport && !tcp_connected?
145
+ @tcp_transport = nil
146
+ logger.info("#{self}: TCP connection dropped, clearing.")
147
+ end
148
+
149
+ if @udp_transport && !udp_connected?
150
+ @udp_transport = nil
151
+ logger.info("#{self}: UDP connection dropped, clearing.")
152
+ end
153
+ end
154
+
155
+ def actually_write(message)
156
+ check_connections
157
+
158
+ # TODO: Support force sending over UDP
159
+ if tcp_connected?
160
+ if @tcp_transport.write(message)
161
+ logger.debug("-> #{self} #{@tcp_transport}: #{message}")
162
+ logger.debug("-> #{message.to_hex}")
163
+ return true
164
+ end
165
+ end
166
+
167
+ if udp_connected?
168
+ if @udp_transport.write(message)
169
+ logger.debug("-> #{self} #{@udp_transport}: #{message}")
170
+ logger.debug("-> #{message.to_hex}")
171
+ return true
172
+ end
173
+ end
174
+
175
+ false
176
+ end
177
+
178
+ def observer_callback_definition
179
+ {
180
+ message_received: -> (message: nil, ip: nil, transport: nil) {}
181
+ }
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,440 @@
1
+ require 'lifx/lan/seen'
2
+ require 'lifx/lan/color'
3
+ require 'lifx/lan/target'
4
+ require 'lifx/lan/light_target'
5
+ require 'lifx/lan/firmware'
6
+
7
+ module LIFX
8
+ module LAN
9
+ # LIFX::LAN::Light represents a Light device
10
+ class Light
11
+ include Seen
12
+ include LightTarget
13
+ include Logging
14
+ include Utilities
15
+ include RequiredKeywordArguments
16
+
17
+ # @return [NetworkContext] NetworkContext the Light belongs to
18
+ attr_reader :context
19
+
20
+ # @return [String] Device ID
21
+ attr_reader :id
22
+
23
+ # @param context: [NetworkContext] {NetworkContext} the Light belongs to
24
+ # @param id: [String] Device ID of the Light
25
+ # @param site_id: [String] Site ID of the Light. Avoid using when possible.
26
+ # @param label: [String] Label of Light to prepopulate
27
+ def initialize(context: required!(:context), id: self.id, site_id: nil, label: nil)
28
+ @context = context
29
+ @id = id
30
+ @site_id = site_id
31
+ @label = label
32
+ @power = nil
33
+ @message_hooks = Hash.new { |h, k| h[k] = [] }
34
+ @context.register_device(self)
35
+ @message_signal = ConditionVariable.new
36
+
37
+ add_hooks
38
+ end
39
+
40
+ # Handles updating the internal state of the Light from incoming
41
+ # protocol messages.
42
+ # @api private
43
+ def handle_message(message, ip, transport)
44
+ payload = message.payload
45
+
46
+ @message_hooks[payload.class].each do |hook|
47
+ hook.call(payload)
48
+ end
49
+ @message_signal.broadcast
50
+ end
51
+
52
+ # Adds a block to be run when a payload of class `payload_class` is received
53
+ # @param payload_class [Class] Payload type to execute block on
54
+ # @param &hook [Proc] Hook to run
55
+ # @api private
56
+ # @return [void]
57
+ def add_hook(payload_class, hook_arg = nil, &hook_block)
58
+ hook = block_given? ? hook_block : hook_arg
59
+ if !hook || !hook.is_a?(Proc)
60
+ raise "Must pass a proc either as an argument or a block"
61
+ end
62
+ @message_hooks[payload_class] << hook
63
+ end
64
+
65
+ # Removes a hook added by {#add_hook}
66
+ # @param payload_class [Class] Payload type to delete hook from
67
+ # @param hook [Proc] The original hook passed into {#add_hook}
68
+ # @api private
69
+ # @return [void]
70
+ def remove_hook(payload_class, hook)
71
+ @message_hooks[payload_class].delete(hook)
72
+ end
73
+
74
+ # Returns the color of the device.
75
+ # @param refresh: [Boolean] If true, will request for current color
76
+ # @param fetch: [Boolean] If false, it will not request current color if it's not cached
77
+ # @return [Color] Color
78
+ def color(refresh: false, fetch: true)
79
+ @color = nil if refresh
80
+ send_message!(Protocol::Light::Get.new, wait_for: Protocol::Light::State) if fetch && !@color
81
+ @color
82
+ end
83
+
84
+ # Returns the label of the light
85
+ # @param refresh: [Boolean] If true, will request for current label
86
+ # @param fetch: [Boolean] If false, it will not request current label if it's not cached
87
+ # @return [String, nil] Label
88
+ def label(refresh: false, fetch: true)
89
+ @label = nil if refresh
90
+ send_message!(Protocol::Light::Get.new, wait_for: Protocol::Light::State) if fetch && !@label
91
+ @label
92
+ end
93
+
94
+ MAX_LABEL_LENGTH = 32
95
+ class LabelTooLong < ArgumentError; end
96
+
97
+ # Sets the label of the light
98
+ # @param label [String] Desired label
99
+ # @raise [LabelTooLong] if label is greater than {MAX_LABEL_LENGTH}
100
+ # @return [Light] self
101
+ def set_label(label)
102
+ if label.bytes.length > MAX_LABEL_LENGTH
103
+ raise LabelTooLong.new("Label length in bytes must be below or equal to #{MAX_LABEL_LENGTH}")
104
+ end
105
+ while self.label != label
106
+ send_message!(Protocol::Device::SetLabel.new(label: label.encode('utf-8')), wait_for: Protocol::Device::StateLabel)
107
+ end
108
+ self
109
+ end
110
+
111
+ # Set the power state to `state` synchronously.
112
+ # @param state [:on, :off]
113
+ # @return [Light, LightCollection] self for chaining
114
+ def set_power!(state)
115
+ level = case state
116
+ when :on
117
+ 1
118
+ when :off
119
+ 0
120
+ else
121
+ raise ArgumentError.new("Must pass in either :on or :off")
122
+ end
123
+ send_message!(Protocol::Device::SetPower.new(level: level), wait_for: Protocol::Device::StatePower) do |payload|
124
+ if level == 0
125
+ payload.level == 0
126
+ else
127
+ payload.level > 0
128
+ end
129
+ end
130
+ self
131
+ end
132
+
133
+ # Turns the light(s) on synchronously
134
+ # @return [Light, LightCollection] self for chaining
135
+ def turn_on!
136
+ set_power!(:on)
137
+ end
138
+
139
+ # Turns the light(s) off synchronously
140
+ # @return [Light, LightCollection]
141
+ def turn_off!
142
+ set_power!(:off)
143
+ end
144
+
145
+ # @see #power
146
+ # @return [Boolean] Returns true if device is on
147
+ def on?(refresh: false, fetch: true)
148
+ power(refresh: refresh, fetch: fetch) == :on
149
+ end
150
+
151
+ # @see #power
152
+ # @return [Boolean] Returns true if device is off
153
+ def off?(refresh: false, fetch: true)
154
+ power(refresh: refresh, fetch: fetch) == :off
155
+ end
156
+
157
+ # @param refresh: see #label
158
+ # @param fetch: see #label
159
+ # @return [:unknown, :off, :on] Light power state
160
+ def power(refresh: false, fetch: true)
161
+ @power = nil if refresh
162
+ send_message!(Protocol::Light::Get.new, wait_for: Protocol::Light::State) if !@power && fetch
163
+ case @power
164
+ when nil
165
+ :unknown
166
+ when 0
167
+ :off
168
+ else
169
+ :on
170
+ end
171
+ end
172
+
173
+ # Returns the local time of the light
174
+ # @return [Time]
175
+ def time
176
+ send_message!(Protocol::Device::GetTime.new, wait_for: Protocol::Device::StateTime) do |payload|
177
+ Time.at(payload.time.to_f / NSEC_IN_SEC)
178
+ end
179
+ end
180
+
181
+ # Returns the difference between the device time and time on the current machine
182
+ # Positive values means device time is further in the future.
183
+ # @return [Float]
184
+ def time_delta
185
+ device_time = time
186
+ delta = device_time - Time.now
187
+ end
188
+
189
+ def ambience(fetch: true)
190
+ @ambience ||= begin
191
+ send_message!(Protocol::Sensor::GetAmbientLight.new,
192
+ wait_for: Protocol::Sensor::StateAmbientLight) do |payload|
193
+ payload.inspect
194
+ end
195
+ end
196
+ end
197
+
198
+ def power_level(fetch: true)
199
+ @power_level ||= begin
200
+ send_message!(Protocol::Device::GetPower.new,
201
+ wait_for: Protocol::Device::StatePower) do |payload|
202
+ payload.inspect
203
+ end
204
+ end
205
+ end
206
+
207
+ # Pings the device and measures response time.
208
+ # @return [Float] Latency from sending a message to receiving a response.
209
+ def latency
210
+ start = Time.now.to_f
211
+ send_message!(Protocol::Device::GetTime.new, wait_for: Protocol::Device::StateTime)
212
+ Time.now.to_f - start
213
+ end
214
+
215
+ # Returns the wifi firmware details
216
+ # @api private
217
+ # @return [Hash] firmware details
218
+ def wifi_firmware(fetch: true)
219
+ @wifi_firmware ||= begin
220
+ send_message!(Protocol::Device::GetWifiFirmware.new,
221
+ wait_for: Protocol::Device::StateWifiFirmware) do |payload|
222
+ Firmware.new(payload)
223
+ end if fetch
224
+ end
225
+ end
226
+
227
+ def mcu_firmware(fetch: true)
228
+ @mcu_firmware ||= begin
229
+ send_message!(Protocol::Device::GetHostFirmware.new,
230
+ wait_for: Protocol::Device::StateHostFirmware) do |payload|
231
+ Firmware.new(payload)
232
+ end if fetch
233
+ end
234
+ end
235
+
236
+ # Returns the temperature of the device
237
+ # @return [Float] Temperature in Celcius
238
+ def temperature
239
+ send_message!(Protocol::Light::GetTemperature.new,
240
+ wait_for: Protocol::Light::StateTemperature) do |payload|
241
+ payload.temperature / 100.0
242
+ end
243
+ end
244
+
245
+ # Returns wifi network info
246
+ # @api private
247
+ # @return [Hash] wifi network info
248
+ def wifi_info
249
+ send_message!(Protocol::Device::GetWifiInfo.new,
250
+ wait_for: Protocol::Device::StateWifiInfo) do |payload|
251
+ {
252
+ signal: payload.signal, # This is in Milliwatts
253
+ tx: payload.tx,
254
+ rx: payload.rx
255
+ }
256
+ end
257
+ end
258
+
259
+ # Returns version info
260
+ # @api private
261
+ # @return [Hash] version info
262
+ def version
263
+ send_message!(Protocol::Device::GetVersion.new,
264
+ wait_for: Protocol::Device::StateVersion) do |payload|
265
+ {
266
+ vendor: payload.vendor,
267
+ product: payload.product,
268
+ version: payload.version
269
+ }
270
+ end
271
+ end
272
+
273
+ # Return device uptime
274
+ # @api private
275
+ # @return [Float] Device uptime in seconds
276
+ def uptime
277
+ send_message!(Protocol::Device::GetInfo.new,
278
+ wait_for: Protocol::Device::StateInfo) do |payload|
279
+ payload.uptime.to_f / NSEC_IN_SEC
280
+ end
281
+ end
282
+
283
+ # Return device last downtime
284
+ # @api private
285
+ # @return [Float] Device's last downtime in secodns
286
+ def last_downtime
287
+ send_message!(Protocol::Device::GetInfo.new,
288
+ wait_for: Protocol::Device::StateInfo) do |payload|
289
+ payload.downtime.to_f / NSEC_IN_SEC
290
+ end
291
+ end
292
+
293
+ # Returns the `site_id` the Light belongs to.
294
+ # @api private
295
+ # @return [String]
296
+ def site_id
297
+ if @site_id.nil?
298
+ # FIXME: This is ugly.
299
+ context.routing_manager.routing_table.site_id_for_device_id(id)
300
+ else
301
+ @site_id
302
+ end
303
+ end
304
+
305
+ # Returns the tags uint64 bitfield for protocol use.
306
+ # @api private
307
+ # @return [Integer]
308
+ def tags_field
309
+ try_until -> { @tags_field } do
310
+ send_message(Protocol::Device::GetTags.new)
311
+ end
312
+ @tags_field
313
+ end
314
+
315
+ # Add tag to the Light
316
+ # @param tag [String] The tag to add
317
+ # @return [Light] self
318
+ def add_tag(tag)
319
+ context.add_tag_to_device(tag: tag, device: self)
320
+ self
321
+ end
322
+
323
+ # Remove tag from the Light
324
+ # @param tag [String] The tag to remove
325
+ # @return [Light] self
326
+ def remove_tag(tag)
327
+ context.remove_tag_from_device(tag: tag, device: self)
328
+ self
329
+ end
330
+
331
+ # Returns the tags that are associated with the Light
332
+ # @return [Array<String>] tags
333
+ def tags
334
+ context.tags_for_device(self)
335
+ end
336
+
337
+ # Returns whether the light is a gateway
338
+ # @api private
339
+ def gateway?
340
+ context.transport_manager.gateways.include?(self)
341
+ end
342
+
343
+ # Returns a nice string representation of the Light
344
+ # @return [String]
345
+ def to_s
346
+ %Q{#<LIFX::LAN::Light id=#{id} label=#{label(fetch: false)} power=#{power(fetch: false)}>}.force_encoding('utf-8')
347
+ end
348
+ alias_method :inspect, :to_s
349
+
350
+ # Compare current Light to another light
351
+ # @param other [Light]
352
+ # @return [-1, 0, 1] Comparison value
353
+ def <=>(other)
354
+ raise ArgumentError.new("Comparison of #{self} with #{other} failed") unless other.is_a?(LIFX::Light)
355
+ [label, id, 0] <=> [other.label, other.id, 0]
356
+ end
357
+
358
+ # Queues a message to be sent the Light
359
+ # @param payload [Protocol::Payload] the payload to send
360
+ # @param acknowledge: [Boolean] whether the device should respond
361
+ # @param at_time: [Integer] Unix epoch in milliseconds to run the payload. Only applicable to certain payload types.
362
+ # @return [Light] returns self for chaining
363
+ def send_message(payload, acknowledge: true, at_time: nil)
364
+ context.send_message(target: Target.new(device_id: id), payload: payload, acknowledge: acknowledge, at_time: at_time)
365
+ end
366
+
367
+ # An exception for when synchronous messages take too long to receive a response
368
+ class MessageTimeout < Timeout::Error
369
+ attr_accessor :device
370
+ end
371
+
372
+ # Queues a message to be sent to the Light and waits for a response
373
+ # @param payload [Protocol::Payload] the payload to send
374
+ # @param wait_for: [Class] the payload class to wait for
375
+ # @param wait_timeout: [Numeric] wait timeout
376
+ # @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.
377
+ # @return [Object] the truthy result of `block` is returned.
378
+ # @raise [MessageTimeout] if the device doesn't respond in time
379
+ def send_message!(payload, wait_for: self.wait_for, wait_timeout: Config.message_wait_timeout, retry_interval: Config.message_retry_interval, &block)
380
+ if Thread.current[:sync_enabled]
381
+ raise "Cannot use synchronous methods inside a sync block"
382
+ end
383
+
384
+ result = nil
385
+ begin
386
+ block ||= Proc.new { |msg| true }
387
+ proc = -> (payload) {
388
+ result = block.call(payload)
389
+ }
390
+ add_hook(wait_for, proc)
391
+ try_until -> { result }, timeout: wait_timeout, timeout_exception: TimeoutError, action_interval: retry_interval, signal: @message_signal do
392
+ send_message(payload)
393
+ end
394
+ result
395
+ rescue TimeoutError
396
+ backtrace = caller_locations(2).map { |c| c.to_s }
397
+ caller_method = caller_locations(2, 1).first.label
398
+ ex = MessageTimeout.new("#{caller_method}: Timeout exceeded waiting for response from #{self}")
399
+ ex.device = self
400
+ ex.set_backtrace(backtrace)
401
+ raise ex
402
+ ensure
403
+ remove_hook(wait_for, proc)
404
+ end
405
+ end
406
+
407
+ protected
408
+
409
+ def add_hooks
410
+ add_hook(Protocol::Device::StateLabel) do |payload|
411
+ @label = payload.label.to_s.force_encoding('utf-8')
412
+ seen!
413
+ end
414
+
415
+ add_hook(Protocol::Light::State) do |payload|
416
+ @label = payload.label.snapshot.force_encoding('utf-8')
417
+ @color = Color.from_struct(payload.color.snapshot)
418
+ @power = payload.power.to_i
419
+ @tags_field = payload.tags
420
+ seen!
421
+ end
422
+
423
+ add_hook(Protocol::Device::StateTags) do |payload|
424
+ @tags_field = payload.tags
425
+ seen!
426
+ end
427
+
428
+ add_hook(Protocol::Device::StatePower) do |payload|
429
+ @power = payload.level.to_i
430
+ seen!
431
+ end
432
+
433
+ add_hook(Protocol::Device::StateWifiFirmware) do |payload|
434
+ @wifi_firmware = Firmware.new(payload)
435
+ seen!
436
+ end
437
+ end
438
+ end
439
+ end
440
+ end