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,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
@@ -0,0 +1,5 @@
1
+ module LIFX
2
+ module LAN
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -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
@@ -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