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