lifx-lan 0.1.0

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