lifx-lan 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,43 @@
|
|
1
|
+
require 'lifx/lan/observable'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
# @api private
|
6
|
+
module TransportManager
|
7
|
+
class Base
|
8
|
+
include Logging
|
9
|
+
include Observable
|
10
|
+
attr_accessor :context
|
11
|
+
|
12
|
+
def initialize(**args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def discover
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def write(message)
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def flush(**options)
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
def stop
|
28
|
+
@context = nil
|
29
|
+
remove_observers
|
30
|
+
end
|
31
|
+
|
32
|
+
def observer_callback_definition
|
33
|
+
{
|
34
|
+
message_received: -> (message: nil, ip: nil, transport: nil) {},
|
35
|
+
disconnected: -> {}
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
require 'lifx/lan/transport_manager/lan'
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'lifx/lan/site'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
module TransportManager
|
6
|
+
class LAN < Base
|
7
|
+
include Timers
|
8
|
+
def initialize(bind_ip: '0.0.0.0', send_ip: Config.broadcast_ip, port: 56700)
|
9
|
+
super
|
10
|
+
@bind_ip = bind_ip
|
11
|
+
@send_ip = send_ip
|
12
|
+
@port = port
|
13
|
+
|
14
|
+
@sites = {}
|
15
|
+
@threads = []
|
16
|
+
@threads << initialize_timer_thread
|
17
|
+
initialize_transport
|
18
|
+
initialize_periodic_refresh
|
19
|
+
initialize_message_rate_updater
|
20
|
+
end
|
21
|
+
|
22
|
+
def flush(**options)
|
23
|
+
@sites.values.map do |site|
|
24
|
+
Thread.start do
|
25
|
+
site.flush(**options)
|
26
|
+
end
|
27
|
+
end.each(&:join)
|
28
|
+
end
|
29
|
+
|
30
|
+
DISCOVERY_INTERVAL_WHEN_NO_SITES_FOUND = 1 # seconds
|
31
|
+
DISCOVERY_INTERVAL = 15 # seconds
|
32
|
+
def discover
|
33
|
+
stop_discovery
|
34
|
+
@discovery_thread = Thread.start do
|
35
|
+
@last_request_seen = Time.at(0)
|
36
|
+
message = Message.new(path: ProtocolPath.new(tagged: true), payload: Protocol::Device::GetService.new)
|
37
|
+
logger.info("Discovering gateways on #{@bind_ip}:#{@port}")
|
38
|
+
loop do
|
39
|
+
interval = @sites.empty? ?
|
40
|
+
DISCOVERY_INTERVAL_WHEN_NO_SITES_FOUND :
|
41
|
+
DISCOVERY_INTERVAL
|
42
|
+
if Time.now - @last_request_seen > interval
|
43
|
+
write(message)
|
44
|
+
end
|
45
|
+
sleep(interval / 2.0)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def stop_discovery
|
51
|
+
if @discovery_thread
|
52
|
+
@discovery_thread.abort
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def stop
|
57
|
+
super
|
58
|
+
stop_discovery
|
59
|
+
stop_timers
|
60
|
+
@threads.each do |thr|
|
61
|
+
thr.abort
|
62
|
+
end
|
63
|
+
@transport.close
|
64
|
+
@sites.values.each do |site|
|
65
|
+
site.stop
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def write(message)
|
70
|
+
return unless on_network?
|
71
|
+
if message.path.all_sites?
|
72
|
+
broadcast(message)
|
73
|
+
else
|
74
|
+
site = @sites[message.path.site_id]
|
75
|
+
if site
|
76
|
+
site.write(message)
|
77
|
+
else
|
78
|
+
broadcast(message)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
@message_rate_timer.reset
|
82
|
+
end
|
83
|
+
|
84
|
+
def on_network?
|
85
|
+
if Socket.respond_to?(:getifaddrs) # Ruby 2.1+
|
86
|
+
Socket.getifaddrs.any? { |ifaddr| ifaddr.broadaddr }
|
87
|
+
else # Ruby 2.0
|
88
|
+
Socket.ip_address_list.any? do |addrinfo|
|
89
|
+
# Not entirely sure how to check if on a LAN with IPv6
|
90
|
+
addrinfo.ipv4_private? || (addrinfo.respond_to?(:ipv6_unique_local?) && addrinfo.ipv6_unique_local?)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def broadcast(message)
|
96
|
+
if !@transport.connected?
|
97
|
+
create_broadcast_transport
|
98
|
+
end
|
99
|
+
@transport.write(message)
|
100
|
+
end
|
101
|
+
|
102
|
+
def sites
|
103
|
+
@sites.dup
|
104
|
+
end
|
105
|
+
|
106
|
+
def gateways
|
107
|
+
@sites.values.map(&:gateways).map(&:keys).flatten.uniq.map { |id| context.lights.with_id(id) }.compact
|
108
|
+
end
|
109
|
+
|
110
|
+
def gateway_connections
|
111
|
+
@sites.values.map(&:gateways).map(&:values).flatten
|
112
|
+
end
|
113
|
+
|
114
|
+
def message_rate
|
115
|
+
@message_rate || DEFAULT_MESSAGE_RATE
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
def initialize_periodic_refresh
|
121
|
+
timers.every(10) do
|
122
|
+
context.refresh(force: false)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
DEFAULT_MESSAGE_RATE = 5 # per second
|
127
|
+
MESSAGE_RATE_1_2 = 20
|
128
|
+
def initialize_message_rate_updater
|
129
|
+
@message_rate_timer = timers.every(2) do
|
130
|
+
@message_rate = MESSAGE_RATE_1_2
|
131
|
+
gateway_connections.each do |connection|
|
132
|
+
connection.set_message_rate(@message_rate)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def initialize_transport
|
138
|
+
create_broadcast_transport
|
139
|
+
end
|
140
|
+
|
141
|
+
def create_broadcast_transport
|
142
|
+
@transport = Transport::UDP.new(@send_ip, @port)
|
143
|
+
@transport.add_observer(self, :message_received) do |message: nil, ip: nil, transport: nil|
|
144
|
+
handle_broadcast_message(message, ip, @transport)
|
145
|
+
notify_observers(:message_received, message: message, ip: ip, transport: transport)
|
146
|
+
end
|
147
|
+
@transport.listen(ip: @bind_ip)
|
148
|
+
end
|
149
|
+
|
150
|
+
def handle_broadcast_message(message, ip, transport)
|
151
|
+
return if message.nil?
|
152
|
+
payload = message.payload
|
153
|
+
case payload
|
154
|
+
when Protocol::Device::StateService
|
155
|
+
if !@sites.has_key?(message.path.site_id)
|
156
|
+
@sites[message.path.site_id] = Site.new(id: message.path.site_id)
|
157
|
+
@sites[message.path.site_id].add_observer(self, :message_received) do |**args|
|
158
|
+
notify_observers(:message_received, **args)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
@sites[message.path.site_id].handle_message(message, ip, transport)
|
162
|
+
when Protocol::Device::GetService
|
163
|
+
@last_request_seen = Time.now
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module LIFX
|
2
|
+
module LAN
|
3
|
+
# @private
|
4
|
+
module Utilities
|
5
|
+
def try_until(condition_proc, timeout_exception: TimeoutError,
|
6
|
+
timeout: 3,
|
7
|
+
condition_interval: 0.1,
|
8
|
+
action_interval: 0.5,
|
9
|
+
signal: nil, &action_block)
|
10
|
+
Timeout.timeout(timeout) do
|
11
|
+
m = Mutex.new
|
12
|
+
time = 0
|
13
|
+
while !condition_proc.call
|
14
|
+
if Time.now.to_f - time > action_interval
|
15
|
+
time = Time.now.to_f
|
16
|
+
action_block.call
|
17
|
+
end
|
18
|
+
if signal
|
19
|
+
m.synchronize do
|
20
|
+
signal.wait(m, condition_interval)
|
21
|
+
end
|
22
|
+
else
|
23
|
+
sleep(condition_interval)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
rescue Timeout::Error
|
28
|
+
raise timeout_exception if timeout_exception
|
29
|
+
end
|
30
|
+
|
31
|
+
def tag_ids_from_field(tags_field)
|
32
|
+
(0...64).to_a.select { |t| (tags_field & (2 ** t)) > 0 }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lifx-lan.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'lifx/lan/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "lifx-lan"
|
8
|
+
spec.version = LIFX::LAN::VERSION
|
9
|
+
spec.authors = ["Jack Chen (chendo), Julian Cheal (juliancheal)"]
|
10
|
+
spec.email = ["julian.cheal+lifx-gem@lifx.co"]
|
11
|
+
spec.description = %q{A Ruby gem that allows easy interaction with LIFX devices.}
|
12
|
+
spec.summary = %q{A Ruby gem that allows easy interaction with LIFX devices. Handles discovery, rate limiting, tags, gateway connections and provides an object-based API for interacting with LIFX devices. }
|
13
|
+
spec.homepage = "https://github.com/juliancheal/lifx-lan"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/).reject { |f| f =~ /^script\// }
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
spec.required_ruby_version = ">= 2.0"
|
21
|
+
|
22
|
+
spec.add_dependency "bindata", "~> 2.0"
|
23
|
+
spec.add_dependency "timers", "~> 1.0"
|
24
|
+
spec.add_dependency "configatron", "~> 3.0"
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
26
|
+
end
|
data/spec/color_spec.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
describe Color do
|
6
|
+
let(:default_kelvin) { 3500 }
|
7
|
+
|
8
|
+
describe '.rgb' do
|
9
|
+
context 'translating from RGB' do
|
10
|
+
shared_examples 'translating color' do |name, rgb, expected|
|
11
|
+
it "translates #{name} correctly" do
|
12
|
+
translation = Color.rgb(*rgb).to_a
|
13
|
+
expect(translation).to eq [*expected, default_kelvin]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it_behaves_like 'translating color', 'red', [255, 0, 0], [0, 1, 1]
|
18
|
+
it_behaves_like 'translating color', 'yellow', [255, 255, 0], [60, 1, 1]
|
19
|
+
it_behaves_like 'translating color', 'green', [0, 255, 0], [120, 1, 1]
|
20
|
+
it_behaves_like 'translating color', 'cyan', [0, 255, 255], [180, 1, 1]
|
21
|
+
it_behaves_like 'translating color', 'blue', [0, 0, 255], [240, 1, 1]
|
22
|
+
it_behaves_like 'translating color', 'white', [255, 255, 255], [0, 0, 1]
|
23
|
+
it_behaves_like 'translating color', 'black', [0, 0, 0], [0, 0, 0]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#similar_to?' do
|
28
|
+
it 'matches reds on on either end of hue spectrums' do
|
29
|
+
expect(Color.hsb(359.9, 1, 1)).to be_similar_to(Color.hsb(0, 1, 1))
|
30
|
+
expect(Color.hsb(0, 1, 1)).to be_similar_to(Color.hsb(359.9, 1, 1))
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'does not match different colours' do
|
34
|
+
expect(Color.hsb(120, 1, 1)).to_not be_similar_to(Color.hsb(0, 1, 1))
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'matches similar colours' do
|
38
|
+
expect(Color.hsb(120, 1, 1)).to be_similar_to(Color.hsb(120.3, 1, 1))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
describe GatewayConnection do
|
6
|
+
subject(:gateway) { GatewayConnection.new }
|
7
|
+
|
8
|
+
let(:message) { double(Message, is_a?: true, pack: '') }
|
9
|
+
let(:ip) { '127.0.0.1' }
|
10
|
+
let(:port) { 35_003 }
|
11
|
+
|
12
|
+
after { gateway.close }
|
13
|
+
|
14
|
+
context 'write queue resiliency' do
|
15
|
+
it 'does not send if there is no available connection' do
|
16
|
+
expect(gateway).to_not receive(:actually_write)
|
17
|
+
gateway.write(message)
|
18
|
+
expect { gateway.flush(timeout: 0.5) }.to raise_error(TimeoutError)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'pushes message back into queue if unable to write' do
|
22
|
+
gateway.connect_udp(ip, port)
|
23
|
+
expect(gateway).to receive(:actually_write).and_return(false, true)
|
24
|
+
gateway.write(message)
|
25
|
+
gateway.flush
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
describe Client, integration: true do
|
6
|
+
describe '#sync' do
|
7
|
+
let(:minimum_lights) { 3 }
|
8
|
+
let(:udp) { Transport::UDP.new('0.0.0.0', 56_750) }
|
9
|
+
let(:white) { Color.white(brightness: 0.5) }
|
10
|
+
|
11
|
+
it 'schedules sending all messages to be executed at the same time' do
|
12
|
+
if lights.count < minimum_lights
|
13
|
+
pending 'This test requires 3 or more lights tagged under Test'
|
14
|
+
return
|
15
|
+
end
|
16
|
+
|
17
|
+
lifx.discover! { lights.count >= minimum_lights }
|
18
|
+
|
19
|
+
lights.set_color(white, duration: 0)
|
20
|
+
sleep 1
|
21
|
+
|
22
|
+
msgs = []
|
23
|
+
udp.add_observer(self, :message_received) do |message: nil, ip: nil, transport: nil|
|
24
|
+
msgs << message if message.payload.is_a?(Protocol::Light::SetWaveform)
|
25
|
+
end
|
26
|
+
udp.listen
|
27
|
+
|
28
|
+
lifx.sync do
|
29
|
+
lights.each do |light|
|
30
|
+
light.pulse(LIFX::LAN::Color.hsb(rand(360), 1, 1), period: 1)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
expect(msgs.count).to eq lights.count
|
35
|
+
expect(msgs.map(&:at_time).uniq.count).to eq 1
|
36
|
+
|
37
|
+
flush
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
describe Light, integration: true do
|
6
|
+
describe '#set_power' do
|
7
|
+
it 'sets the power of the light asynchronously' do
|
8
|
+
light.set_power(:off)
|
9
|
+
wait { expect(light).to be_off }
|
10
|
+
light.set_power(:on)
|
11
|
+
wait { expect(light).to be_on }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#set_power!' do
|
16
|
+
it 'sets the power of the light synchronously' do
|
17
|
+
light.set_power!(:off)
|
18
|
+
expect(light).to be_off
|
19
|
+
light.set_power!(:on)
|
20
|
+
expect(light).to be_on
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#set_color' do
|
25
|
+
let(:color) { Color.hsb(rand(360), rand, rand) }
|
26
|
+
|
27
|
+
it 'sets the color of the light asynchronously' do
|
28
|
+
light.set_color(color, duration: 0)
|
29
|
+
sleep 1
|
30
|
+
light.refresh
|
31
|
+
wait { expect(light.color).to be_similar_to(color) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#set_label' do
|
36
|
+
let!(:original_label) { light.label }
|
37
|
+
|
38
|
+
after do
|
39
|
+
light.set_label(original_label)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'sets the label of the light synchronously' do
|
43
|
+
label = light.label.sub(/\d+|$/, rand(100).to_s)
|
44
|
+
light.set_label(label)
|
45
|
+
expect(light.label).to eq(label)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'works with accented characters' do
|
49
|
+
label = 'tést'
|
50
|
+
light.set_label(label)
|
51
|
+
expect(light.label).to eq(label)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|