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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/Gemfile +10 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +71 -13
  6. data/Rakefile +12 -0
  7. data/bin/lifx-console +15 -0
  8. data/bin/lifx-snoop +50 -0
  9. data/examples/auto-off/Gemfile +3 -0
  10. data/examples/auto-off/auto-off.rb +35 -0
  11. data/examples/identify/Gemfile +3 -0
  12. data/examples/identify/identify.rb +70 -0
  13. data/examples/travis-build-light/Gemfile +4 -0
  14. data/examples/travis-build-light/build-light.rb +57 -0
  15. data/lib/bindata_ext/bool.rb +29 -0
  16. data/lib/bindata_ext/record.rb +11 -0
  17. data/lib/lifx/client.rb +136 -0
  18. data/lib/lifx/color.rb +190 -0
  19. data/lib/lifx/config.rb +12 -0
  20. data/lib/lifx/firmware.rb +55 -0
  21. data/lib/lifx/gateway_connection.rb +177 -0
  22. data/lib/lifx/light.rb +406 -0
  23. data/lib/lifx/light_collection.rb +105 -0
  24. data/lib/lifx/light_target.rb +189 -0
  25. data/lib/lifx/logging.rb +11 -0
  26. data/lib/lifx/message.rb +166 -0
  27. data/lib/lifx/network_context.rb +200 -0
  28. data/lib/lifx/observable.rb +46 -0
  29. data/lib/lifx/protocol/address.rb +21 -0
  30. data/lib/lifx/protocol/device.rb +225 -0
  31. data/lib/lifx/protocol/header.rb +24 -0
  32. data/lib/lifx/protocol/light.rb +110 -0
  33. data/lib/lifx/protocol/message.rb +17 -0
  34. data/lib/lifx/protocol/metadata.rb +21 -0
  35. data/lib/lifx/protocol/payload.rb +7 -0
  36. data/lib/lifx/protocol/sensor.rb +29 -0
  37. data/lib/lifx/protocol/type.rb +134 -0
  38. data/lib/lifx/protocol/wan.rb +50 -0
  39. data/lib/lifx/protocol/wifi.rb +76 -0
  40. data/lib/lifx/protocol_path.rb +84 -0
  41. data/lib/lifx/routing_manager.rb +110 -0
  42. data/lib/lifx/routing_table.rb +33 -0
  43. data/lib/lifx/seen.rb +15 -0
  44. data/lib/lifx/site.rb +89 -0
  45. data/lib/lifx/tag_manager.rb +105 -0
  46. data/lib/lifx/tag_table.rb +47 -0
  47. data/lib/lifx/target.rb +23 -0
  48. data/lib/lifx/timers.rb +18 -0
  49. data/lib/lifx/transport/tcp.rb +81 -0
  50. data/lib/lifx/transport/udp.rb +67 -0
  51. data/lib/lifx/transport.rb +41 -0
  52. data/lib/lifx/transport_manager/lan.rb +140 -0
  53. data/lib/lifx/transport_manager.rb +34 -0
  54. data/lib/lifx/utilities.rb +33 -0
  55. data/lib/lifx/version.rb +1 -1
  56. data/lib/lifx.rb +15 -1
  57. data/lifx.gemspec +11 -7
  58. data/spec/color_spec.rb +45 -0
  59. data/spec/gateway_connection_spec.rb +32 -0
  60. data/spec/integration/client_spec.rb +40 -0
  61. data/spec/integration/light_spec.rb +43 -0
  62. data/spec/integration/tags_spec.rb +31 -0
  63. data/spec/message_spec.rb +163 -0
  64. data/spec/protocol_path_spec.rb +109 -0
  65. data/spec/routing_manager_spec.rb +22 -0
  66. data/spec/spec_helper.rb +52 -0
  67. data/spec/transport/udp_spec.rb +38 -0
  68. data/spec/transport_spec.rb +14 -0
  69. 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
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module LIFX
2
- VERSION = "0.0.1"
2
+ VERSION = "0.4.0"
3
3
  end