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,42 @@
1
+ require 'spec_helper'
2
+
3
+ module LIFX
4
+ module LAN
5
+ describe 'tags', integration: true do
6
+ let(:color) { Color.hsb(rand(360), 0.3, 0.3) }
7
+
8
+ specify 'Clearing, setting and using tags' do
9
+ light.add_tag('Foo')
10
+ expect(light.tags).to include('Foo')
11
+
12
+ test_tag = lights.with_tag('Foo')
13
+ test_tag.turn_on
14
+ test_tag.set_color(color, duration: 0)
15
+ flush
16
+ sleep 1 # Set messages are scheduled 250ms if no at_time is set
17
+ # It also returns the current light state rather than the
18
+ # final state
19
+ light.refresh
20
+ wait { expect(light.color).to be_similar_to(color) }
21
+
22
+ light.remove_tag('Foo')
23
+ wait { expect(light.tags).not_to include('Foo') }
24
+ end
25
+
26
+ it 'deletes tags when no longer assigned to a light' do
27
+ light.add_tag('TempTag')
28
+ light.remove_tag('TempTag')
29
+ expect(lifx.unused_tags).to include('TempTag')
30
+ lifx.purge_unused_tags!
31
+ expect(lifx.unused_tags).to be_empty
32
+ end
33
+
34
+ it 'handles non-ascii tags' do
35
+ light.add_tag('_tést')
36
+ expect(light.tags).to include('_tést')
37
+ light.remove_tag('_tést')
38
+ lifx.purge_unused_tags!
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ module LIFX
4
+ module LAN
5
+ describe LightCollection do
6
+ subject(:collection) { LightCollection.new(context: double) }
7
+
8
+ describe '#with_id' do
9
+ let(:light) { double(Light, id: 'id') }
10
+ before { allow(collection).to receive(:lights).and_return([light]) }
11
+
12
+ it 'returns a Light with matching id' do
13
+ expect(collection.with_id('id')).to eq light
14
+ end
15
+
16
+ it 'returns nil when none matches' do
17
+ ret = collection.with_id('wrong id')
18
+ expect(ret).to eq nil
19
+ end
20
+ end
21
+
22
+ describe '#with_label' do
23
+ let(:light) { double(Light, label: 'label') }
24
+ before { allow(collection).to receive(:lights).and_return([light]) }
25
+
26
+ it 'returns a Light with matching label' do
27
+ expect(collection.with_label('label')).to eq light
28
+ end
29
+
30
+ it 'returns nil' do
31
+ ret = collection.with_label('wrong label')
32
+ expect(ret).to eq nil
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,183 @@
1
+ require 'spec_helper'
2
+
3
+ describe LIFX::LAN::Message do
4
+ context 'unpacking' do
5
+ let(:data) do
6
+ "\x39\x00\x00\x34\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x31" \
7
+ "\x6c\x69\x66\x78\x31\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x67\x00" \
8
+ "\x00\x00\x00\x01\x00\x00\xff\xff\xff\xff\xac\x0d\xc8\x00\x00\x00\x00" \
9
+ "\x00\x80\x3f\x00\x00\x00".b
10
+ end
11
+ let(:msg) { LIFX::LAN::Message.unpack(data) }
12
+
13
+ it 'unpacks without errors' do
14
+ expect(msg).not_to be_nil
15
+ end
16
+
17
+ it 'returns the correct frame data' do
18
+ expect(msg.msg_size).to eq 57
19
+ expect(msg.protocol).to eq 1024
20
+ expect(msg).to be_addressable
21
+ end
22
+
23
+ it 'returns the correct address data' do
24
+ expect(msg.raw_site).to eq '1lifx1'
25
+ expect(msg.raw_target).to eq "\x00" * 8
26
+ end
27
+
28
+ it 'has correct ProtocolPath data' do
29
+ expect(msg.path).to be_a(LIFX::LAN::ProtocolPath)
30
+ expect(msg.path.site_id).to eq '316c69667831'
31
+ expect(msg.path.tag_ids).to eq []
32
+ expect(msg.path.device_id).to be_nil
33
+ end
34
+
35
+ it 'returns the correct metadata' do
36
+ expect(msg.at_time).to eq 0
37
+ expect(msg.type_).to eq 103
38
+ end
39
+
40
+ let(:payload) { msg.payload }
41
+ it 'returns the payload' do
42
+ expect(payload.class).to eq LIFX::LAN::Protocol::Light::SetWaveform
43
+ expect(payload.stream).to eq 0
44
+ expect(payload.transient).to eq(true)
45
+ expect(payload.color.hue).to eq 0
46
+ expect(payload.color.saturation).to eq 65_535
47
+ expect(payload.color.brightness).to eq 65_535
48
+ expect(payload.color.kelvin).to eq 3_500
49
+ expect(payload.period).to eq 200
50
+ expect(payload.cycles).to eq 1.0
51
+ expect(payload.skew_ratio).to eq 0
52
+ expect(payload.waveform).to eq 0
53
+ end
54
+
55
+ it 'repacks to the same data' do
56
+ expect(msg.pack).to eq data
57
+ end
58
+ end
59
+
60
+ context 'packing' do
61
+ context 'no attributes' do
62
+ let(:msg) { LIFX::LAN::Message.new }
63
+
64
+ it 'throws an exception' do
65
+ expect { msg.pack }.to raise_error(LIFX::LAN::Message::NoPayload)
66
+ end
67
+ end
68
+
69
+ context 'no path' do
70
+ let(:msg) { LIFX::LAN::Message.new(payload: LIFX::LAN::Protocol::Device::SetPower.new) }
71
+
72
+ it 'defaults to null site and target' do
73
+ unpacked = LIFX::LAN::Message.unpack(msg.pack)
74
+ expect(unpacked.path.site_id).to eq('000000000000')
75
+ expect(unpacked.path.device_id).to eq('000000000000')
76
+ end
77
+ end
78
+
79
+ context 'passed in via hash' do
80
+ let(:msg) do
81
+ LIFX::LAN::Message.new({
82
+ path: LIFX::LAN::ProtocolPath.new(tagged: false, raw_target: 'abcdefgh'),
83
+ at_time: 9001,
84
+ payload: LIFX::LAN::Protocol::Wifi::SetAccessPoint.new(
85
+ interface: 1,
86
+ ssid: 'who let the dogs out',
87
+ pass: 'woof, woof, woof woof!',
88
+ security: 1
89
+ )
90
+ })
91
+ end
92
+ # let(:unpacked) { LIFX::Message.unpack(msg.pack) }
93
+
94
+ it 'sets the size' do
95
+ expect(msg.msg_size).to eq 134
96
+ end
97
+
98
+ it 'packs correctly' do
99
+ expect(msg.pack).to eq "\x86\x00\x00\x14\x00\x00\x00\x00abcdefgh\x00" \
100
+ "\x00\x00\x00\x00\x00\x00\x00)#\x00\x00\x00" \
101
+ "\x00\x00\x001\x01\x00\x00\x01who let the " \
102
+ "dogs out\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
103
+ "\x00\x00\x00woof, woof, woof woof!\x00\x00" \
104
+ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
105
+ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
106
+ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
107
+ "\x00\x00\x00\x00\x00\x00\x00\x01".b
108
+ expect(msg.protocol).to eq 1024
109
+ expect(msg.path).not_to be_tagged
110
+ expect(msg).to be_addressable
111
+ expect(msg.path.raw_target).to eq 'abcdefgh'
112
+ expect(msg.at_time).to eq 9001
113
+ expect(msg.type_).to eq 305
114
+ expect(msg.payload.class).to eq LIFX::LAN::Protocol::Wifi::SetAccessPoint
115
+ expect(msg.payload.interface).to eq 1
116
+ expect(msg.payload.ssid).to eq 'who let the dogs out'
117
+ expect(msg.payload.pass).to eq 'woof, woof, woof woof!'
118
+ expect(msg.payload.security).to eq 1
119
+ end
120
+ end
121
+
122
+ context 'packing with tags' do
123
+ let(:msg) do
124
+ LIFX::LAN::Message.new({
125
+ path: LIFX::LAN::ProtocolPath.new(tag_ids: [0, 1]),
126
+ at_time: 9001,
127
+ payload: LIFX::LAN::Protocol::Device::GetTime.new
128
+ })
129
+ end
130
+
131
+ let(:unpacked) { LIFX::LAN::Message.unpack(msg.pack) }
132
+
133
+ it 'packs the tag correctly' do
134
+ expect(msg.pack).to eq "$\x00\x004\x00\x00\x00\x00\x03\x00\x00\x00" \
135
+ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
136
+ "\x00)#\x00\x00\x00\x00\x00\x00\x04\x00\x00" \
137
+ "\x00".b
138
+ end
139
+
140
+ it 'sets tagged' do
141
+ expect(unpacked.path).to be_tagged
142
+ end
143
+
144
+ it 'sets tags' do
145
+ expect(unpacked.path.tag_ids).to eq [0, 1]
146
+ end
147
+
148
+ it 'device should be nil' do
149
+ expect(unpacked.path.device_id).to be_nil
150
+ end
151
+ end
152
+
153
+ context 'packing with device' do
154
+ let(:msg) do
155
+ LIFX::LAN::Message.new({
156
+ path: LIFX::LAN::ProtocolPath.new(device_id: '0123456789ab', site_id: '0' * 12),
157
+ at_time: 9001,
158
+ payload: LIFX::LAN::Protocol::Device::GetTime.new
159
+ })
160
+ end
161
+
162
+ let(:unpacked) { LIFX::LAN::Message.unpack(msg.pack) }
163
+
164
+ it 'packs the tag correctly' do
165
+ expect(msg.pack).to eq "$\x00\x00\x14\x00\x00\x00\x00\x01#Eg\x89\xAB" \
166
+ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)#" \
167
+ "\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00".b
168
+ end
169
+
170
+ it 'sets tagged to false' do
171
+ expect(unpacked.path).not_to be_tagged
172
+ end
173
+
174
+ it 'sets device' do
175
+ expect(unpacked.path.device_id).to eq '0123456789ab'
176
+ end
177
+
178
+ it 'tags should be nil' do
179
+ expect(unpacked.path.tag_ids).to be_nil
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ module LIFX
4
+ module LAN
5
+ describe ProtocolPath do
6
+ describe 'initializing from raw data' do
7
+ subject do
8
+ ProtocolPath.new(raw_site: '1lifx1', raw_target: target, tagged: tagged)
9
+ end
10
+
11
+ context 'device target' do
12
+ let(:target) { "\xAB\xCD\xEF\x12\x34\x56\x00\x00" }
13
+ let(:tagged) { false }
14
+
15
+ it 'returns site_id in hex' do
16
+ expect(subject.site_id).to eq '316c69667831'
17
+ end
18
+
19
+ it 'returns device_id in hex' do
20
+ expect(subject.device_id).to eq 'abcdef123456'
21
+ end
22
+
23
+ it 'returns tagged as false' do
24
+ expect(subject).not_to be_tagged
25
+ end
26
+
27
+ it 'returns nil for tag_ids' do
28
+ expect(subject.tag_ids).to be_nil
29
+ end
30
+ end
31
+
32
+ context 'tagged target' do
33
+ let(:target) { "\x03\x00\x00\x00\x00\x00\x00\x00" }
34
+ let(:tagged) { true }
35
+
36
+ it 'returns site_id in hex' do
37
+ expect(subject.site_id).to eq '316c69667831'
38
+ end
39
+
40
+ it 'returns device_id as nil' do
41
+ expect(subject.device_id).to be_nil
42
+ end
43
+
44
+ it 'returns tagged as true' do
45
+ expect(subject).to be_tagged
46
+ end
47
+
48
+ it 'returns the tag_ids' do
49
+ expect(subject.tag_ids).to eq [0, 1]
50
+ end
51
+ end
52
+ end
53
+
54
+ describe 'initializing from strings' do
55
+ context 'device target' do
56
+ subject do
57
+ ProtocolPath.new(site_id: '316c69667831', device_id: 'abcdef123456')
58
+ end
59
+
60
+ it 'sets raw_site correctly' do
61
+ expect(subject.raw_site).to eq '1lifx1'
62
+ end
63
+
64
+ it 'sets raw_target correctly' do
65
+ expect(subject.raw_target).to eq "\xAB\xCD\xEF\x12\x34\x56\x00\x00".b
66
+ end
67
+
68
+ it 'sets tagged to false' do
69
+ expect(subject).to_not be_tagged
70
+ end
71
+ end
72
+
73
+ context 'tagged target' do
74
+ subject do
75
+ ProtocolPath.new(site_id: '316c69667831', tag_ids: [0, 1])
76
+ end
77
+
78
+ it 'sets raw_site properly' do
79
+ expect(subject.raw_site).to eq '1lifx1'
80
+ end
81
+
82
+ it 'sets raw_target correctly' do
83
+ expect(subject.raw_target).to eq "\x03\x00\x00\x00\x00\x00\x00\x00".b
84
+ end
85
+
86
+ it 'returns tagged as true' do
87
+ expect(subject).to be_tagged
88
+ end
89
+ end
90
+
91
+ context 'tagged target with no site' do
92
+ subject { ProtocolPath.new(tagged: true) }
93
+
94
+ it 'raw_site should be null string' do
95
+ expect(subject.raw_site).to eq "\x00\x00\x00\x00\x00\x00".b
96
+ end
97
+
98
+ it 'sets raw_target correctly' do
99
+ expect(subject.raw_target).to eq "\x00\x00\x00\x00\x00\x00\x00\x00".b
100
+ end
101
+
102
+ it 'returns tagged as true' do
103
+ expect(subject).to be_tagged
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ module LIFX
4
+ module LAN
5
+ describe RoutingManager do
6
+ describe '#tags_for_device_id' do
7
+ subject(:manager) { RoutingManager.new(context: double(timers: double(every: double))) }
8
+
9
+ before do
10
+ ['Some label', 'Another label', 'Much label'].each_with_index do |lbl, i|
11
+ manager.tag_table.update_table(device_id: 'device', tag_id: i, label: lbl)
12
+ end
13
+
14
+ manager.routing_table
15
+ .update_table(site_id: 'site', device_id: 'device', tag_ids: [0, 2])
16
+ end
17
+
18
+ it 'resolves tags' do
19
+ tags = manager.tags_for_device_id('device')
20
+ expect(tags).to eq ['Some label', 'Much label']
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ module LIFX
4
+ module LAN
5
+ describe RoutingTable do
6
+ describe '#clear_stale_entries' do
7
+ subject(:table) { RoutingTable.new }
8
+
9
+ before do
10
+ table.update_table(site_id: 'site', device_id: 'stale device', last_seen: Time.now - 305)
11
+ table.update_table(site_id: 'site', device_id: 'recent device', last_seen: Time.now)
12
+ end
13
+
14
+ it 'clears only entries older than 5 minutes' do
15
+ expect(table.entries.count).to eq(2)
16
+ table.clear_stale_entries
17
+ expect(table.entries.count).to eq(1)
18
+ expect(table.entries.first.device_id).to eq('recent device')
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,56 @@
1
+ require 'bundler'
2
+ Bundler.require
3
+ begin
4
+ require 'pry'
5
+ rescue LoadError
6
+ end
7
+
8
+ require 'lifx-lan'
9
+ require 'lifx/lan/utilities'
10
+
11
+ shared_context 'integration', integration: true do
12
+ def lifx
13
+ $lifx ||= begin
14
+ c = LIFX::LAN::Client.lan
15
+ begin
16
+ c.discover! do
17
+ c.tags.include?('_Test') && c.lights.with_tag('_Test').count > 0
18
+ end
19
+ rescue LIFX::Client::DiscoveryTimeout
20
+ raise "Could not find any lights with tag _Test in #{c.lights.inspect}"
21
+ end
22
+ c
23
+ end
24
+ end
25
+
26
+ def flush
27
+ lifx.flush
28
+ end
29
+
30
+ let(:lights) { lifx.lights.with_tag('_Test') }
31
+ let(:light) { lights.first }
32
+ end
33
+
34
+ module SpecHelpers
35
+ def wait(timeout: 5, retry_wait: 0.1, &block)
36
+ Timeout.timeout(timeout) do
37
+ begin
38
+ block.call
39
+ rescue RSpec::Expectations::ExpectationNotMetError
40
+ lights.refresh
41
+ sleep(retry_wait)
42
+ retry
43
+ end
44
+ end
45
+ rescue Timeout::Error
46
+ block.call
47
+ end
48
+ end
49
+
50
+ LIFX::LAN::Config.logger = Yell.new(STDERR) if ENV['DEBUG']
51
+
52
+ RSpec.configure do |config|
53
+ config.include(SpecHelpers)
54
+ config.formatter = 'documentation'
55
+ config.color = true
56
+ end