lifx 0.0.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +1 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +1 -1
- data/README.md +71 -13
- data/Rakefile +12 -0
- data/bin/lifx-console +15 -0
- data/bin/lifx-snoop +50 -0
- data/examples/auto-off/Gemfile +3 -0
- data/examples/auto-off/auto-off.rb +35 -0
- data/examples/identify/Gemfile +3 -0
- data/examples/identify/identify.rb +70 -0
- data/examples/travis-build-light/Gemfile +4 -0
- data/examples/travis-build-light/build-light.rb +57 -0
- data/lib/bindata_ext/bool.rb +29 -0
- data/lib/bindata_ext/record.rb +11 -0
- data/lib/lifx/client.rb +136 -0
- data/lib/lifx/color.rb +190 -0
- data/lib/lifx/config.rb +12 -0
- data/lib/lifx/firmware.rb +55 -0
- data/lib/lifx/gateway_connection.rb +177 -0
- data/lib/lifx/light.rb +406 -0
- data/lib/lifx/light_collection.rb +105 -0
- data/lib/lifx/light_target.rb +189 -0
- data/lib/lifx/logging.rb +11 -0
- data/lib/lifx/message.rb +166 -0
- data/lib/lifx/network_context.rb +200 -0
- data/lib/lifx/observable.rb +46 -0
- data/lib/lifx/protocol/address.rb +21 -0
- data/lib/lifx/protocol/device.rb +225 -0
- data/lib/lifx/protocol/header.rb +24 -0
- data/lib/lifx/protocol/light.rb +110 -0
- data/lib/lifx/protocol/message.rb +17 -0
- data/lib/lifx/protocol/metadata.rb +21 -0
- data/lib/lifx/protocol/payload.rb +7 -0
- data/lib/lifx/protocol/sensor.rb +29 -0
- data/lib/lifx/protocol/type.rb +134 -0
- data/lib/lifx/protocol/wan.rb +50 -0
- data/lib/lifx/protocol/wifi.rb +76 -0
- data/lib/lifx/protocol_path.rb +84 -0
- data/lib/lifx/routing_manager.rb +110 -0
- data/lib/lifx/routing_table.rb +33 -0
- data/lib/lifx/seen.rb +15 -0
- data/lib/lifx/site.rb +89 -0
- data/lib/lifx/tag_manager.rb +105 -0
- data/lib/lifx/tag_table.rb +47 -0
- data/lib/lifx/target.rb +23 -0
- data/lib/lifx/timers.rb +18 -0
- data/lib/lifx/transport/tcp.rb +81 -0
- data/lib/lifx/transport/udp.rb +67 -0
- data/lib/lifx/transport.rb +41 -0
- data/lib/lifx/transport_manager/lan.rb +140 -0
- data/lib/lifx/transport_manager.rb +34 -0
- data/lib/lifx/utilities.rb +33 -0
- data/lib/lifx/version.rb +1 -1
- data/lib/lifx.rb +15 -1
- data/lifx.gemspec +11 -7
- data/spec/color_spec.rb +45 -0
- data/spec/gateway_connection_spec.rb +32 -0
- data/spec/integration/client_spec.rb +40 -0
- data/spec/integration/light_spec.rb +43 -0
- data/spec/integration/tags_spec.rb +31 -0
- data/spec/message_spec.rb +163 -0
- data/spec/protocol_path_spec.rb +109 -0
- data/spec/routing_manager_spec.rb +22 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/transport/udp_spec.rb +38 -0
- data/spec/transport_spec.rb +14 -0
- metadata +143 -26
data/lib/lifx/site.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'lifx/seen'
|
2
|
+
require 'lifx/timers'
|
3
|
+
require 'lifx/observable'
|
4
|
+
require 'lifx/gateway_connection'
|
5
|
+
|
6
|
+
module LIFX
|
7
|
+
# @api private
|
8
|
+
class Site
|
9
|
+
include Seen
|
10
|
+
include Timers
|
11
|
+
include Logging
|
12
|
+
include Observable
|
13
|
+
|
14
|
+
attr_reader :id, :gateways, :tag_manager
|
15
|
+
|
16
|
+
def initialize(id:)
|
17
|
+
@id = id
|
18
|
+
@gateways = {}
|
19
|
+
@gateways_mutex = Mutex.new
|
20
|
+
@threads = []
|
21
|
+
@threads << initialize_timer_thread
|
22
|
+
initialize_stale_gateway_check
|
23
|
+
end
|
24
|
+
|
25
|
+
def write(message)
|
26
|
+
message.path.site_id = id
|
27
|
+
@gateways.values.each do |gateway|
|
28
|
+
gateway.write(message)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def handle_message(message, ip, transport)
|
33
|
+
logger.debug("<- #{self} #{transport}: #{message}")
|
34
|
+
payload = message.payload
|
35
|
+
case payload
|
36
|
+
when Protocol::Device::StatePanGateway
|
37
|
+
@gateways_mutex.synchronize do
|
38
|
+
@gateways[message.device_id] ||= GatewayConnection.new
|
39
|
+
@gateways[message.device_id].handle_message(message, ip, transport)
|
40
|
+
@gateways[message.device_id].add_observer(self) do |**args|
|
41
|
+
notify_observers(**args)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
seen!
|
46
|
+
end
|
47
|
+
|
48
|
+
def flush(**options)
|
49
|
+
@gateways.values.map do |gateway|
|
50
|
+
Thread.new do
|
51
|
+
gateway.flush(**options)
|
52
|
+
end
|
53
|
+
end.each(&:join)
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
%Q{#<LIFX::Site id=#{id}>}
|
58
|
+
end
|
59
|
+
alias_method :inspect, :to_s
|
60
|
+
|
61
|
+
def stop
|
62
|
+
@threads.each do |thread|
|
63
|
+
Thread.kill(thread)
|
64
|
+
end
|
65
|
+
@gateways.values.each do |gateway|
|
66
|
+
gateway.close
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
STALE_GATEWAY_CHECK_INTERVAL = 10
|
74
|
+
def initialize_stale_gateway_check
|
75
|
+
timers.every(STALE_GATEWAY_CHECK_INTERVAL) do
|
76
|
+
@gateways_mutex.synchronize do
|
77
|
+
stale_gateways = @gateways.select do |k, v|
|
78
|
+
!v.connected?
|
79
|
+
end
|
80
|
+
stale_gateways.each do |id, _|
|
81
|
+
logger.info("#{self}: Dropping stale gateway id #{id}")
|
82
|
+
@gateways.delete(id)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module LIFX
|
2
|
+
# @api private
|
3
|
+
class TagManager
|
4
|
+
# TagManager handles discovery of tags, resolving tags to [site_id, tags_field] pairs,
|
5
|
+
# creating, setting and removing tags.
|
6
|
+
|
7
|
+
# Stores site <-> [tag_name, tag_id]
|
8
|
+
include Utilities
|
9
|
+
include Logging
|
10
|
+
|
11
|
+
attr_reader :context
|
12
|
+
|
13
|
+
class TagLimitReached < StandardError; end
|
14
|
+
|
15
|
+
def initialize(context:, tag_table:)
|
16
|
+
@context = context
|
17
|
+
@tag_table = tag_table
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_tag(label:, site_id:)
|
21
|
+
id = next_unused_id_on_site_id(site_id)
|
22
|
+
raise TagLimitReached if id.nil?
|
23
|
+
# Add the entry for the tag we're about to create to prevent a case where
|
24
|
+
# we don't receive a StateTagLabels before another tag gets created
|
25
|
+
@tag_table.update_table(tag_id: id, label: label, site_id: site_id)
|
26
|
+
context.send_message(target: Target.new(site_id: site_id),
|
27
|
+
payload: Protocol::Device::SetTagLabels.new(tags: id_to_tags_field(id), label: label))
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_tag_to_device(tag:, device:)
|
31
|
+
tag_entry = entry_with(label: tag, site_id: device.site_id)
|
32
|
+
if !tag_entry
|
33
|
+
create_tag(label: tag, site_id: device.site_id)
|
34
|
+
tag_entry = entry_with(label: tag, site_id: device.site_id)
|
35
|
+
end
|
36
|
+
|
37
|
+
device_tags_field = device.tags_field
|
38
|
+
device_tags_field |= id_to_tags_field(tag_entry.tag_id)
|
39
|
+
device.send_message!(Protocol::Device::SetTags.new(tags: device_tags_field), wait_for: Protocol::Device::StateTags) do
|
40
|
+
device.tags.include?(tag)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def remove_tag_from_device(tag:, device:)
|
45
|
+
tag_entry = entry_with(label: tag, site_id: device.site_id)
|
46
|
+
return if !tag_entry
|
47
|
+
|
48
|
+
device_tags_field = device.tags_field
|
49
|
+
device_tags_field &= ~id_to_tags_field(tag_entry.tag_id)
|
50
|
+
device.send_message!(Protocol::Device::SetTags.new(tags: device_tags_field), wait_for: Protocol::Device::StateTags) do
|
51
|
+
!device.tags.include?(tag)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def tags
|
56
|
+
@tag_table.tags
|
57
|
+
end
|
58
|
+
|
59
|
+
def unused_tags
|
60
|
+
@tag_table.tags.select do |tag|
|
61
|
+
context.lights.with_tag(tag).empty?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# This will clear out tags that currently do not resolve to any devices.
|
66
|
+
# If used when devices that are tagged with a tag that is not attached to an
|
67
|
+
# active device, it will effectively untag them when they're back on.
|
68
|
+
def purge_unused_tags!
|
69
|
+
unused_tags.each do |tag|
|
70
|
+
logger.info("Purging tag '#{tag}'")
|
71
|
+
entries_with(label: tag).each do |entry|
|
72
|
+
payload = Protocol::Device::SetTagLabels.new(tags: id_to_tags_field(entry.tag_id), label: '')
|
73
|
+
context.send_message(target: Target.new(site_id: entry.site_id),
|
74
|
+
payload: payload,
|
75
|
+
acknowledge: true)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
Timeout.timeout(5) do
|
79
|
+
while !unused_tags.empty?
|
80
|
+
sleep 0.1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
VALID_TAG_IDS = (0...64).to_a.freeze
|
88
|
+
|
89
|
+
def entry_with(**args)
|
90
|
+
entries_with(**args).first
|
91
|
+
end
|
92
|
+
|
93
|
+
def entries_with(**args)
|
94
|
+
@tag_table.entries_with(**args)
|
95
|
+
end
|
96
|
+
|
97
|
+
def id_to_tags_field(id)
|
98
|
+
2 ** id
|
99
|
+
end
|
100
|
+
|
101
|
+
def next_unused_id_on_site_id(site_id)
|
102
|
+
(VALID_TAG_IDS - entries_with(site_id: site_id).map(&:tag_id)).first
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module LIFX
|
2
|
+
# @api private
|
3
|
+
class TagTable
|
4
|
+
class Entry < Struct.new(:tag_id, :label, :site_id); end
|
5
|
+
|
6
|
+
def initialize(entries: {})
|
7
|
+
@entries = Hash.new { |h, k| h[k] = {} }
|
8
|
+
entries.each do |k, v|
|
9
|
+
@entries[k] = v
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def entries_with(tag_id: nil, site_id: nil, label: nil)
|
14
|
+
entries.select do |entry|
|
15
|
+
ret = []
|
16
|
+
ret << (entry.tag_id == tag_id) if tag_id
|
17
|
+
ret << (entry.site_id == site_id) if site_id
|
18
|
+
ret << (entry.label == label) if label
|
19
|
+
ret.all?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def entry_with(**args)
|
24
|
+
entries_with(**args).first
|
25
|
+
end
|
26
|
+
|
27
|
+
def update_table(tag_id:, label:, site_id:)
|
28
|
+
entry = @entries[site_id][tag_id] ||= Entry.new(tag_id, label, site_id)
|
29
|
+
entry.label = label
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete_entries_with(tag_id: nil, site_id: nil, label: nil)
|
33
|
+
matching_entries = entries_with(tag_id: tag_id, site_id: site_id, label: label)
|
34
|
+
matching_entries.each do |entry|
|
35
|
+
@entries[entry.site_id].delete(entry.tag_id)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def tags
|
40
|
+
entries.map(&:label).uniq
|
41
|
+
end
|
42
|
+
|
43
|
+
def entries
|
44
|
+
@entries.values.map(&:values).flatten
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/lifx/target.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module LIFX
|
2
|
+
# @api private
|
3
|
+
|
4
|
+
class Target
|
5
|
+
# Target is a high-level abstraction for the target of a Message
|
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
|
data/lib/lifx/timers.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'timers'
|
2
|
+
module LIFX
|
3
|
+
module Timers
|
4
|
+
protected
|
5
|
+
def initialize_timer_thread
|
6
|
+
timers.after(1) {} # Just so timers.wait doesn't complain when there's no timer
|
7
|
+
Thread.new do
|
8
|
+
loop do
|
9
|
+
timers.wait
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def timers
|
15
|
+
@timers ||= ::Timers.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
class Transport
|
5
|
+
# @api private
|
6
|
+
class TCP < Transport
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
def initialize(*args)
|
10
|
+
super
|
11
|
+
connect
|
12
|
+
end
|
13
|
+
|
14
|
+
def connected?
|
15
|
+
@socket && !@socket.closed?
|
16
|
+
end
|
17
|
+
|
18
|
+
CONNECT_TIMEOUT = 3
|
19
|
+
def connect
|
20
|
+
Timeout.timeout(CONNECT_TIMEOUT) do
|
21
|
+
@socket = TCPSocket.new(host, port) # Performs the connection
|
22
|
+
end
|
23
|
+
@socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, 1024)
|
24
|
+
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
25
|
+
logger.info("#{self}: Connected.")
|
26
|
+
rescue => ex
|
27
|
+
logger.warn("#{self}: Exception occured in #connect - #{ex}")
|
28
|
+
logger.debug("#{self}: Backtrace: #{ex.backtrace.join("\n")}")
|
29
|
+
@socket = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def close
|
33
|
+
return if !@socket
|
34
|
+
Thread.kill(@listener) if @listener
|
35
|
+
@listener = nil
|
36
|
+
@socket.close if !@socket.closed?
|
37
|
+
@socket = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
HEADER_SIZE = 8
|
41
|
+
def listen
|
42
|
+
return if @listener
|
43
|
+
@listener = Thread.new do
|
44
|
+
while @socket do
|
45
|
+
begin
|
46
|
+
header_data = @socket.recv(HEADER_SIZE, Socket::MSG_PEEK)
|
47
|
+
header = Protocol::Header.read(header_data)
|
48
|
+
size = header.msg_size
|
49
|
+
data = @socket.recv(size)
|
50
|
+
message = Message.unpack(data)
|
51
|
+
|
52
|
+
notify_observers(message: message, ip: host, transport: self)
|
53
|
+
rescue Message::UnpackError
|
54
|
+
if !@ignore_unpackable_messages
|
55
|
+
logger.warn("#{self}: Exception occured - #{ex}")
|
56
|
+
end
|
57
|
+
rescue => ex
|
58
|
+
logger.warn("#{self}: Exception occured in #listen - #{ex}")
|
59
|
+
logger.debug("#{self}: Backtrace: #{ex.backtrace.join("\n")}")
|
60
|
+
close
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
SEND_TIMEOUT = 2
|
67
|
+
def write(message)
|
68
|
+
data = message.pack
|
69
|
+
Timeout.timeout(SEND_TIMEOUT) do
|
70
|
+
@socket.write(data)
|
71
|
+
end
|
72
|
+
true
|
73
|
+
rescue => ex
|
74
|
+
logger.warn("#{self}: Exception in #write: #{ex}")
|
75
|
+
logger.debug("#{self}: Backtrace: #{ex.backtrace.join("\n")}")
|
76
|
+
close
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'socket'
|
2
|
+
module LIFX
|
3
|
+
class Transport
|
4
|
+
# @api private
|
5
|
+
class UDP < Transport
|
6
|
+
def initialize(*args)
|
7
|
+
super
|
8
|
+
@socket = create_socket
|
9
|
+
end
|
10
|
+
|
11
|
+
def connected?
|
12
|
+
!!@socket
|
13
|
+
end
|
14
|
+
|
15
|
+
def write(message)
|
16
|
+
data = message.pack
|
17
|
+
@socket.send(data, 0, host, port)
|
18
|
+
true
|
19
|
+
rescue => ex
|
20
|
+
logger.warn("#{self}: Error on #write: #{ex}")
|
21
|
+
logger.debug("#{self}: Backtrace: #{ex.backtrace.join("\n")}")
|
22
|
+
close
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def listen(ip: self.host, port: self.port)
|
27
|
+
if @listener
|
28
|
+
raise "Socket already being listened to"
|
29
|
+
end
|
30
|
+
|
31
|
+
Thread.abort_on_exception = true
|
32
|
+
@listener = Thread.new do
|
33
|
+
reader = UDPSocket.new
|
34
|
+
reader.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
35
|
+
reader.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true)
|
36
|
+
reader.bind(ip, port)
|
37
|
+
loop do
|
38
|
+
begin
|
39
|
+
bytes, (_, _, ip, _) = reader.recvfrom(128)
|
40
|
+
message = Message.unpack(bytes)
|
41
|
+
notify_observers(message: message, ip: ip, transport: self)
|
42
|
+
rescue Message::UnpackError
|
43
|
+
if !@ignore_unpackable_messages
|
44
|
+
logger.warn("#{self}: Unrecognised bytes: #{bytes.bytes.map { |b| '%02x ' % b }.join}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def close
|
52
|
+
Thread.kill(@listener) if @listener
|
53
|
+
return if !@socket
|
54
|
+
@socket.close
|
55
|
+
@socket = nil
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
def create_socket
|
61
|
+
UDPSocket.new.tap do |socket|
|
62
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'lifx/observable'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
# @api private
|
5
|
+
class Transport
|
6
|
+
include Logging
|
7
|
+
include Observable
|
8
|
+
|
9
|
+
attr_reader :host, :port
|
10
|
+
|
11
|
+
def initialize(host, port, ignore_unpackable_messages: true)
|
12
|
+
@host = host
|
13
|
+
@port = port
|
14
|
+
@ignore_unpackable_messages = ignore_unpackable_messages
|
15
|
+
end
|
16
|
+
|
17
|
+
def listen
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
def write(message)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def close
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
%Q{#<#{self.class.name} #{host}:#{port}>}
|
31
|
+
end
|
32
|
+
alias_method :inspect, :to_s
|
33
|
+
|
34
|
+
def observer_callback_definition
|
35
|
+
-> (message:, ip: nil, transport: nil) {}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
require 'lifx/transport/udp'
|
41
|
+
require 'lifx/transport/tcp'
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'lifx/site'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
module TransportManager
|
5
|
+
class LAN < Base
|
6
|
+
def initialize(bind_ip: '0.0.0.0', send_ip: '255.255.255.255', port: 56700, peer_port: 56750)
|
7
|
+
super
|
8
|
+
@bind_ip = bind_ip
|
9
|
+
@send_ip = send_ip
|
10
|
+
@port = port
|
11
|
+
@peer_port = peer_port
|
12
|
+
|
13
|
+
@sites = {}
|
14
|
+
initialize_transports
|
15
|
+
end
|
16
|
+
|
17
|
+
def flush(**options)
|
18
|
+
@sites.values.map do |site|
|
19
|
+
Thread.new do
|
20
|
+
site.flush(**options)
|
21
|
+
end
|
22
|
+
end.each(&:join)
|
23
|
+
end
|
24
|
+
|
25
|
+
DISCOVERY_INTERVAL_WHEN_NO_SITES_FOUND = 1 # seconds
|
26
|
+
DISCOVERY_INTERVAL = 15 # seconds
|
27
|
+
def discover
|
28
|
+
stop_discovery
|
29
|
+
Thread.abort_on_exception = true
|
30
|
+
@discovery_thread = Thread.new do
|
31
|
+
@last_request_seen = Time.at(0)
|
32
|
+
message = Message.new(path: ProtocolPath.new(tagged: true), payload: Protocol::Device::GetPanGateway.new)
|
33
|
+
logger.info("Discovering gateways on #{@bind_ip}:#{@port}")
|
34
|
+
loop do
|
35
|
+
interval = @sites.empty? ?
|
36
|
+
DISCOVERY_INTERVAL_WHEN_NO_SITES_FOUND :
|
37
|
+
DISCOVERY_INTERVAL
|
38
|
+
if Time.now - @last_request_seen > interval
|
39
|
+
write(message)
|
40
|
+
end
|
41
|
+
sleep(interval / 2.0)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def stop_discovery
|
47
|
+
Thread.kill(@discovery_thread) if @discovery_thread
|
48
|
+
end
|
49
|
+
|
50
|
+
def stop
|
51
|
+
stop_discovery
|
52
|
+
@transport.close
|
53
|
+
@sites.values.each do |site|
|
54
|
+
site.stop
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def write(message)
|
59
|
+
return unless on_network?
|
60
|
+
if message.path.all_sites?
|
61
|
+
broadcast(message)
|
62
|
+
else
|
63
|
+
site = @sites[message.path.site_id]
|
64
|
+
if site
|
65
|
+
site.write(message)
|
66
|
+
else
|
67
|
+
broadcast(message)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
broadcast_to_peers(message)
|
71
|
+
end
|
72
|
+
|
73
|
+
def on_network?
|
74
|
+
Socket.getifaddrs.any? { |ifaddr| ifaddr.broadaddr }
|
75
|
+
end
|
76
|
+
|
77
|
+
def broadcast(message)
|
78
|
+
if !@transport.connected?
|
79
|
+
create_broadcast_transport
|
80
|
+
end
|
81
|
+
@transport.write(message)
|
82
|
+
end
|
83
|
+
|
84
|
+
def broadcast_to_peers(message)
|
85
|
+
if !@peer_transport.connected?
|
86
|
+
create_peer_transport
|
87
|
+
end
|
88
|
+
@peer_transport.write(message)
|
89
|
+
end
|
90
|
+
|
91
|
+
def sites
|
92
|
+
@sites.dup
|
93
|
+
end
|
94
|
+
|
95
|
+
def gateways
|
96
|
+
@sites.values.map(&:gateways)
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def initialize_transports
|
102
|
+
create_broadcast_transport
|
103
|
+
create_peer_transport
|
104
|
+
end
|
105
|
+
|
106
|
+
def create_broadcast_transport
|
107
|
+
@transport = Transport::UDP.new(@send_ip, @port)
|
108
|
+
@transport.add_observer(self) do |message:, ip:, transport:|
|
109
|
+
handle_broadcast_message(message, ip, @transport)
|
110
|
+
notify_observers(message: message, ip: ip, transport: transport)
|
111
|
+
end
|
112
|
+
@transport.listen(ip: @bind_ip)
|
113
|
+
end
|
114
|
+
|
115
|
+
def create_peer_transport
|
116
|
+
@peer_transport = Transport::UDP.new('255.255.255.255', @peer_port)
|
117
|
+
@peer_transport.add_observer(self) do |message:, ip:, transport:|
|
118
|
+
notify_observers(message: message, ip: ip, transport: transport)
|
119
|
+
end
|
120
|
+
@peer_transport.listen(ip: @bind_ip)
|
121
|
+
end
|
122
|
+
|
123
|
+
def handle_broadcast_message(message, ip, transport)
|
124
|
+
payload = message.payload
|
125
|
+
case payload
|
126
|
+
when Protocol::Device::StatePanGateway
|
127
|
+
if !@sites.has_key?(message.path.site_id)
|
128
|
+
@sites[message.path.site_id] = Site.new(id: message.path.site_id)
|
129
|
+
@sites[message.path.site_id].add_observer(self) do |**args|
|
130
|
+
notify_observers(**args)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
@sites[message.path.site_id].handle_message(message, ip, transport)
|
134
|
+
when Protocol::Device::GetPanGateway
|
135
|
+
@last_request_seen = Time.now
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'lifx/observable'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
# @api private
|
5
|
+
module TransportManager
|
6
|
+
class Base
|
7
|
+
include Logging
|
8
|
+
include Observable
|
9
|
+
|
10
|
+
def initialize(**args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def discover
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def write(message)
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
def flush(**options)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
require 'lifx/transport_manager/lan'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module LIFX
|
2
|
+
module Utilities
|
3
|
+
def try_until(condition_proc, timeout_exception: Timeout::Error,
|
4
|
+
timeout: 3,
|
5
|
+
condition_interval: 0.1,
|
6
|
+
action_interval: 0.5,
|
7
|
+
signal: nil, &action_block)
|
8
|
+
Timeout.timeout(timeout) do
|
9
|
+
m = Mutex.new
|
10
|
+
time = 0
|
11
|
+
while !condition_proc.call
|
12
|
+
if Time.now.to_f - time > action_interval
|
13
|
+
time = Time.now.to_f
|
14
|
+
action_block.call
|
15
|
+
end
|
16
|
+
if signal
|
17
|
+
m.synchronize do
|
18
|
+
signal.wait(m, condition_interval)
|
19
|
+
end
|
20
|
+
else
|
21
|
+
sleep(condition_interval)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
rescue Timeout::Error
|
26
|
+
raise timeout_exception if timeout_exception
|
27
|
+
end
|
28
|
+
|
29
|
+
def tag_ids_from_field(tags_field)
|
30
|
+
(0...64).to_a.select { |t| (tags_field & (2 ** t)) > 0 }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/lifx/version.rb
CHANGED