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,111 @@
|
|
1
|
+
require 'weakref'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
# @api private
|
6
|
+
# @private
|
7
|
+
class TagManager
|
8
|
+
# TagManager handles discovery of tags, resolving tags to [site_id, tags_field] pairs,
|
9
|
+
# creating, setting and removing tags.
|
10
|
+
|
11
|
+
# Stores site <-> [tag_name, tag_id]
|
12
|
+
include Utilities
|
13
|
+
include Logging
|
14
|
+
include RequiredKeywordArguments
|
15
|
+
|
16
|
+
attr_reader :context
|
17
|
+
|
18
|
+
class TagLimitReached < StandardError; end
|
19
|
+
|
20
|
+
def initialize(context: required!(:context), tag_table: required!(:tag_table))
|
21
|
+
@context = WeakRef.new(context)
|
22
|
+
@tag_table = tag_table
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_tag(label: required!(:label), site_id: required!(:site_id))
|
26
|
+
id = next_unused_id_on_site_id(site_id)
|
27
|
+
raise TagLimitReached if id.nil?
|
28
|
+
# Add the entry for the tag we're about to create to prevent a case where
|
29
|
+
# we don't receive a StateTagLabels before another tag gets created
|
30
|
+
@tag_table.update_table(tag_id: id, label: label, site_id: site_id)
|
31
|
+
context.send_message(target: Target.new(site_id: site_id),
|
32
|
+
payload: Protocol::Device::SetTagLabels.new(tags: id_to_tags_field(id), label: label.encode('utf-8')))
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_tag_to_device(tag: required!(:tag), device: required!(:device))
|
36
|
+
tag_entry = entry_with(label: tag, site_id: device.site_id)
|
37
|
+
if !tag_entry
|
38
|
+
create_tag(label: tag, site_id: device.site_id)
|
39
|
+
tag_entry = entry_with(label: tag, site_id: device.site_id)
|
40
|
+
end
|
41
|
+
|
42
|
+
device_tags_field = device.tags_field
|
43
|
+
device_tags_field |= id_to_tags_field(tag_entry.tag_id)
|
44
|
+
device.send_message!(Protocol::Device::SetTags.new(tags: device_tags_field), wait_for: Protocol::Device::StateTags) do
|
45
|
+
device.tags.include?(tag)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def remove_tag_from_device(tag: required!(:tag), device: required!(:device))
|
50
|
+
tag_entry = entry_with(label: tag, site_id: device.site_id)
|
51
|
+
return if !tag_entry
|
52
|
+
|
53
|
+
device_tags_field = device.tags_field
|
54
|
+
device_tags_field &= ~id_to_tags_field(tag_entry.tag_id)
|
55
|
+
device.send_message!(Protocol::Device::SetTags.new(tags: device_tags_field), wait_for: Protocol::Device::StateTags) do
|
56
|
+
!device.tags.include?(tag)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def tags
|
61
|
+
@tag_table.tags
|
62
|
+
end
|
63
|
+
|
64
|
+
def unused_tags
|
65
|
+
@tag_table.tags.select do |tag|
|
66
|
+
context.lights.with_tag(tag).empty?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# This will clear out tags that currently do not resolve to any devices.
|
71
|
+
# If used when devices that are tagged with a tag that is not attached to an
|
72
|
+
# active device, it will effectively untag them when they're back on.
|
73
|
+
def purge_unused_tags!
|
74
|
+
unused_tags.each do |tag|
|
75
|
+
logger.info("Purging tag '#{tag}'")
|
76
|
+
entries_with(label: tag).each do |entry|
|
77
|
+
payload = Protocol::Device::SetTagLabels.new(tags: id_to_tags_field(entry.tag_id), label: '')
|
78
|
+
context.send_message(target: Target.new(site_id: entry.site_id),
|
79
|
+
payload: payload,
|
80
|
+
acknowledge: true)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
Timeout.timeout(5) do
|
84
|
+
while !unused_tags.empty?
|
85
|
+
sleep 0.1
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
VALID_TAG_IDS = (0...64).to_a.freeze
|
93
|
+
|
94
|
+
def entry_with(**args)
|
95
|
+
entries_with(**args).first
|
96
|
+
end
|
97
|
+
|
98
|
+
def entries_with(**args)
|
99
|
+
@tag_table.entries_with(**args)
|
100
|
+
end
|
101
|
+
|
102
|
+
def id_to_tags_field(id)
|
103
|
+
2 ** id
|
104
|
+
end
|
105
|
+
|
106
|
+
def next_unused_id_on_site_id(site_id)
|
107
|
+
(VALID_TAG_IDS - entries_with(site_id: site_id).map(&:tag_id)).first
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module LIFX
|
2
|
+
module LAN
|
3
|
+
# @api private
|
4
|
+
class TagTable
|
5
|
+
class Entry < Struct.new(:tag_id, :label, :device_id); end
|
6
|
+
|
7
|
+
def initialize(entries: {})
|
8
|
+
@entries = Hash.new { |h, k| h[k] = {} }
|
9
|
+
entries.each do |k, v|
|
10
|
+
@entries[k] = v
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def entries_with(tag_id: nil, device_id: nil, label: nil)
|
15
|
+
entries.select do |entry|
|
16
|
+
ret = []
|
17
|
+
ret << (entry.tag_id == tag_id) if tag_id
|
18
|
+
ret << (entry.device_id == device_id) if device_id
|
19
|
+
ret << (entry.label == label) if label
|
20
|
+
ret.all?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def entry_with(**args)
|
25
|
+
entries_with(**args).first
|
26
|
+
end
|
27
|
+
|
28
|
+
def update_table(tag_id: self.tag_id, label: self.label, device_id: self.device_id)
|
29
|
+
entry = @entries[device_id][tag_id] ||= Entry.new(tag_id, label, device_id)
|
30
|
+
entry.label = label
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete_entries_with(tag_id: nil, device_id: nil, label: nil)
|
34
|
+
matching_entries = entries_with(tag_id: tag_id, device_id: device_id, label: label)
|
35
|
+
matching_entries.each do |entry|
|
36
|
+
@entries[entry.device_id].delete(entry.tag_id)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def tags
|
41
|
+
entries.map(&:label).uniq
|
42
|
+
end
|
43
|
+
|
44
|
+
def entries
|
45
|
+
@entries.values.map(&:values).flatten
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module LIFX
|
2
|
+
module LAN
|
3
|
+
# Target is a high-level abstraction for the target of a Message
|
4
|
+
# @api private
|
5
|
+
class Target
|
6
|
+
|
7
|
+
attr_reader :site_id, :device_id, :tag, :broadcast
|
8
|
+
def initialize(device_id: nil, site_id: nil, tag: nil, broadcast: nil)
|
9
|
+
@site_id = site_id
|
10
|
+
@device_id = device_id
|
11
|
+
@tag = tag
|
12
|
+
@broadcast = broadcast
|
13
|
+
end
|
14
|
+
|
15
|
+
def broadcast?
|
16
|
+
!!broadcast
|
17
|
+
end
|
18
|
+
|
19
|
+
def tag?
|
20
|
+
!!tag
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'timers'
|
2
|
+
module LIFX
|
3
|
+
module LAN
|
4
|
+
# @private
|
5
|
+
module Timers
|
6
|
+
protected
|
7
|
+
def initialize_timer_thread
|
8
|
+
timers.after(1) {} # Just so timers.wait doesn't complain when there's no timer
|
9
|
+
@timer_thread = Thread.start do
|
10
|
+
loop do
|
11
|
+
timers.wait
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def stop_timers
|
17
|
+
timers.each(&:cancel)
|
18
|
+
if @timer_thread
|
19
|
+
@timer_thread.abort
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
public
|
24
|
+
def timers
|
25
|
+
@timers ||= ::Timers.new
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'lifx/lan/observable'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
# @api private
|
6
|
+
class Transport
|
7
|
+
include Logging
|
8
|
+
include Observable
|
9
|
+
|
10
|
+
attr_reader :host, :port
|
11
|
+
|
12
|
+
def initialize(host, port, ignore_unpackable_messages: true)
|
13
|
+
@host = host
|
14
|
+
@port = port
|
15
|
+
@ignore_unpackable_messages = ignore_unpackable_messages
|
16
|
+
end
|
17
|
+
|
18
|
+
def listen
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def write(message)
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
def close
|
27
|
+
remove_observers
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
%Q{#<#{self.class.name} #{host}:#{port}>}
|
32
|
+
end
|
33
|
+
alias_method :inspect, :to_s
|
34
|
+
|
35
|
+
def observer_callback_definition
|
36
|
+
{
|
37
|
+
message_received: -> (message: nil, ip: nil, transport: nil) {},
|
38
|
+
disconnected: -> {}
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
require 'lifx/lan/transport/udp'
|
46
|
+
require 'lifx/lan/transport/tcp'
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
class Transport
|
6
|
+
# @api private
|
7
|
+
# @private
|
8
|
+
class TCP < Transport
|
9
|
+
include Logging
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
super
|
13
|
+
connect
|
14
|
+
end
|
15
|
+
|
16
|
+
def connected?
|
17
|
+
!!(@socket && !@socket.closed?)
|
18
|
+
end
|
19
|
+
|
20
|
+
CONNECT_TIMEOUT = 3
|
21
|
+
def connect
|
22
|
+
Timeout.timeout(CONNECT_TIMEOUT) do
|
23
|
+
@socket = TCPSocket.new(host, port) # Performs the connection
|
24
|
+
end
|
25
|
+
@socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, 1024)
|
26
|
+
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
27
|
+
logger.info("#{self}: Connected.")
|
28
|
+
rescue => ex
|
29
|
+
logger.warn("#{self}: Exception occurred in #connect - #{ex}")
|
30
|
+
logger.debug("#{self}: Backtrace: #{ex.backtrace.join("\n")}")
|
31
|
+
@socket = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def close
|
35
|
+
super
|
36
|
+
return if !@socket
|
37
|
+
if !@socket.closed?
|
38
|
+
@socket.close
|
39
|
+
notify_observers(:disconnected)
|
40
|
+
end
|
41
|
+
@socket = nil
|
42
|
+
if @listener
|
43
|
+
@listener.abort
|
44
|
+
end
|
45
|
+
@listener = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
HEADER_SIZE = 8
|
49
|
+
def listen
|
50
|
+
return if @listener
|
51
|
+
@listener = Thread.start do
|
52
|
+
while @socket do
|
53
|
+
begin
|
54
|
+
header_data = @socket.recv(HEADER_SIZE, Socket::MSG_PEEK)
|
55
|
+
header = Protocol::Header.read(header_data)
|
56
|
+
size = header.msg_size
|
57
|
+
data = @socket.recv(size)
|
58
|
+
message = Message.unpack(data)
|
59
|
+
|
60
|
+
notify_observers(:message_received, {message: message, ip: host, transport: self})
|
61
|
+
rescue Message::UnpackError
|
62
|
+
if Config.log_invalid_messages
|
63
|
+
logger.info("#{self}: Exception occurred while decoding message - #{ex}")
|
64
|
+
logger.info("Data: #{data.inspect}")
|
65
|
+
end
|
66
|
+
rescue => ex
|
67
|
+
logger.warn("#{self}: Exception occurred in #listen - #{ex}")
|
68
|
+
logger.debug("#{self}: Backtrace: #{ex.backtrace.join("\n")}")
|
69
|
+
close
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
SEND_TIMEOUT = 2
|
76
|
+
def write(message)
|
77
|
+
data = message.pack
|
78
|
+
Timeout.timeout(SEND_TIMEOUT) do
|
79
|
+
@socket.write(data)
|
80
|
+
end
|
81
|
+
true
|
82
|
+
rescue => ex
|
83
|
+
logger.warn("#{self}: Exception in #write: #{ex}")
|
84
|
+
logger.debug("#{self}: Backtrace: #{ex.backtrace.join("\n")}")
|
85
|
+
close
|
86
|
+
false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'socket'
|
2
|
+
module LIFX
|
3
|
+
module LAN
|
4
|
+
class Transport
|
5
|
+
# @api private
|
6
|
+
# @private
|
7
|
+
class UDP < Transport
|
8
|
+
def initialize(*args)
|
9
|
+
super
|
10
|
+
@socket = create_socket
|
11
|
+
end
|
12
|
+
|
13
|
+
def connected?
|
14
|
+
!!@socket
|
15
|
+
end
|
16
|
+
|
17
|
+
def write_raw(data)
|
18
|
+
logger.debug("-> #{data}")
|
19
|
+
@socket.send(data.scan(/../).map { |x| x.hex.chr }.join, 0, host, port)
|
20
|
+
true
|
21
|
+
rescue => ex
|
22
|
+
logger.warn("#{self}: Error on #write: #{ex}")
|
23
|
+
logger.debug("#{self}: Backtrace: #{ex.backtrace.join("\n")}")
|
24
|
+
close
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
def write(message)
|
29
|
+
logger.debug("-> Broadcast UDP message: #{message}")
|
30
|
+
logger.debug("-> #{message.to_hex}")
|
31
|
+
data = message.pack
|
32
|
+
@socket.send(data, 0, host, port)
|
33
|
+
true
|
34
|
+
rescue => ex
|
35
|
+
logger.warn("#{self}: Error on #write: #{ex}")
|
36
|
+
logger.debug("#{self}: Backtrace: #{ex.backtrace.join("\n")}")
|
37
|
+
close
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
def listen(ip: self.host, port: self.port)
|
42
|
+
if @listener
|
43
|
+
raise "Socket already being listened to"
|
44
|
+
end
|
45
|
+
|
46
|
+
@listener = Thread.start do
|
47
|
+
reader = UDPSocket.new
|
48
|
+
reader.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
49
|
+
reader.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true) if Socket.const_defined?('SO_REUSEPORT')
|
50
|
+
reader.bind(ip, port)
|
51
|
+
loop do
|
52
|
+
begin
|
53
|
+
bytes, (_, _, ip, _) = reader.recvfrom(128)
|
54
|
+
logger.debug("<- #{bytes.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join}")
|
55
|
+
message = Message.unpack(bytes)
|
56
|
+
logger.debug("<- Incoming message: #{message}")
|
57
|
+
notify_observers(:message_received, {message: message, ip: ip, transport: self})
|
58
|
+
rescue Message::UnpackError
|
59
|
+
if Config.log_invalid_messages
|
60
|
+
logger.warn("#{self}: Unrecognised bytes: #{bytes.bytes.map { |b| '%02x ' % b }.join}")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def close
|
68
|
+
super
|
69
|
+
return if !@socket
|
70
|
+
@socket.close
|
71
|
+
@socket = nil
|
72
|
+
if @listener
|
73
|
+
@listener.abort
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def create_socket
|
80
|
+
UDPSocket.new.tap do |socket|
|
81
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|