lifx 0.0.1 → 0.4.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 +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