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