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,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
|