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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +8 -0
- data/.yardopts +3 -0
- data/CHANGES.md +45 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +23 -0
- data/README.md +15 -0
- data/Rakefile +20 -0
- data/bin/lifx-snoop +50 -0
- data/examples/auto-off/auto-off.rb +34 -0
- data/examples/blink/blink.rb +19 -0
- data/examples/identify/identify.rb +69 -0
- data/examples/travis-build-light/build-light.rb +57 -0
- data/lib/bindata_ext/bool.rb +30 -0
- data/lib/bindata_ext/record.rb +11 -0
- data/lib/lifx-lan.rb +27 -0
- data/lib/lifx/lan/client.rb +149 -0
- data/lib/lifx/lan/color.rb +199 -0
- data/lib/lifx/lan/config.rb +17 -0
- data/lib/lifx/lan/firmware.rb +60 -0
- data/lib/lifx/lan/gateway_connection.rb +185 -0
- data/lib/lifx/lan/light.rb +440 -0
- data/lib/lifx/lan/light_collection.rb +111 -0
- data/lib/lifx/lan/light_target.rb +185 -0
- data/lib/lifx/lan/logging.rb +14 -0
- data/lib/lifx/lan/message.rb +168 -0
- data/lib/lifx/lan/network_context.rb +188 -0
- data/lib/lifx/lan/observable.rb +66 -0
- data/lib/lifx/lan/protocol/address.rb +25 -0
- data/lib/lifx/lan/protocol/device.rb +387 -0
- data/lib/lifx/lan/protocol/header.rb +24 -0
- data/lib/lifx/lan/protocol/light.rb +142 -0
- data/lib/lifx/lan/protocol/message.rb +19 -0
- data/lib/lifx/lan/protocol/metadata.rb +23 -0
- data/lib/lifx/lan/protocol/payload.rb +12 -0
- data/lib/lifx/lan/protocol/sensor.rb +31 -0
- data/lib/lifx/lan/protocol/type.rb +204 -0
- data/lib/lifx/lan/protocol/wan.rb +51 -0
- data/lib/lifx/lan/protocol/wifi.rb +102 -0
- data/lib/lifx/lan/protocol_path.rb +85 -0
- data/lib/lifx/lan/required_keyword_arguments.rb +12 -0
- data/lib/lifx/lan/routing_manager.rb +114 -0
- data/lib/lifx/lan/routing_table.rb +48 -0
- data/lib/lifx/lan/seen.rb +25 -0
- data/lib/lifx/lan/site.rb +97 -0
- data/lib/lifx/lan/tag_manager.rb +111 -0
- data/lib/lifx/lan/tag_table.rb +49 -0
- data/lib/lifx/lan/target.rb +24 -0
- data/lib/lifx/lan/thread.rb +13 -0
- data/lib/lifx/lan/timers.rb +29 -0
- data/lib/lifx/lan/transport.rb +46 -0
- data/lib/lifx/lan/transport/tcp.rb +91 -0
- data/lib/lifx/lan/transport/udp.rb +87 -0
- data/lib/lifx/lan/transport_manager.rb +43 -0
- data/lib/lifx/lan/transport_manager/lan.rb +169 -0
- data/lib/lifx/lan/utilities.rb +36 -0
- data/lib/lifx/lan/version.rb +5 -0
- data/lifx-lan.gemspec +26 -0
- data/spec/color_spec.rb +43 -0
- data/spec/gateway_connection_spec.rb +30 -0
- data/spec/integration/client_spec.rb +42 -0
- data/spec/integration/light_spec.rb +56 -0
- data/spec/integration/tags_spec.rb +42 -0
- data/spec/light_collection_spec.rb +37 -0
- data/spec/message_spec.rb +183 -0
- data/spec/protocol_path_spec.rb +109 -0
- data/spec/routing_manager_spec.rb +25 -0
- data/spec/routing_table_spec.rb +23 -0
- data/spec/spec_helper.rb +56 -0
- data/spec/transport/udp_spec.rb +44 -0
- data/spec/transport_spec.rb +14 -0
- 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
|