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/color.rb ADDED
@@ -0,0 +1,190 @@
1
+ module LIFX
2
+ module Colors
3
+ DEFAULT_KELVIN = 3500
4
+
5
+ {
6
+ red: 0,
7
+ orange: 36,
8
+ yellow: 60,
9
+ green: 120,
10
+ cyan: 195,
11
+ blue: 250,
12
+ purple: 280,
13
+ pink: 325
14
+ }.each do |color, hue|
15
+ define_method(color) do |saturation: 1.0, brightness: 1.0, kelvin: DEFAULT_KELVIN|
16
+ Color.new(hue, saturation, brightness, kelvin)
17
+ end
18
+ end
19
+
20
+ # Helper to create a white {Color}
21
+ # @param brightness: [Float] Valid range: `0..1`
22
+ # @param kelvin: [Integer] Valid range: `2500..10000`
23
+ # @return [Color]
24
+ def white(brightness: 1.0, kelvin: DEFAULT_KELVIN)
25
+ Color.new(0, 0, brightness, kelvin)
26
+ end
27
+
28
+ # Helper to create a random {Color}
29
+ def random_color(hue: rand(360), saturation: rand, brightness: rand, kelvin: DEFAULT_KELVIN)
30
+ Color.new(hue, saturation, brightness, kelvin)
31
+ end
32
+ end
33
+
34
+ # LIFX::Color represents a color intervally by HSBK (Hue, Saturation, Brightness/Value, Kelvin).
35
+ # It has methods to construct a LIFX::Color instance from various color representations.
36
+ class Color < Struct.new(:hue, :saturation, :brightness, :kelvin)
37
+ extend Colors
38
+ UINT16_MAX = 65535
39
+ KELVIN_MIN = 2500
40
+ KELVIN_MAX = 10000
41
+
42
+ class << self
43
+ # Helper method to create from HSB/HSV
44
+ # @param hue [Float] Valid range: `0..360`
45
+ # @param saturation [Float] Valid range: `0..1`
46
+ # @param brightness [Float] Valid range: `0..1`
47
+ # @return [Color]
48
+ def hsb(hue, saturation, brightness)
49
+ new(hue, saturation, brightness, DEFAULT_KELVIN)
50
+ end
51
+ alias_method :hsv, :hsb
52
+
53
+ # Helper method to create from HSBK/HSVK
54
+ # @param hue [Float] Valid range: `0..360`
55
+ # @param saturation [Float] Valid range: `0..1`
56
+ # @param brightness [Float] Valid range: `0..1`
57
+ # @param kelvin [Integer] Valid range: `2500..10000`
58
+ # @return [Color]
59
+ def hsbk(hue, saturation, brightness, kelvin)
60
+ new(hue, saturation, brightness, kelvin)
61
+ end
62
+
63
+ # Helper method to create from HSL
64
+ # @param hue [Float] Valid range: `0..360`
65
+ # @param saturation [Float] Valid range: `0..1`
66
+ # @param luminance [Float] Valid range: `0..1`
67
+ # @return [Color]
68
+ def hsl(hue, saturation, luminance)
69
+ # From: http://ariya.blogspot.com.au/2008/07/converting-between-hsl-and-hsv.html
70
+ l = luminance * 2
71
+ saturation *= (l <= 1) ? l : 2 - l
72
+ brightness = (l + saturation) / 2
73
+ saturation = (2 * saturation) / (l + saturation)
74
+ new(hue, saturation, brightness, DEFAULT_KELVIN)
75
+ end
76
+
77
+ # Helper method to create from RGB.
78
+ # @note RGB is not the recommended way to create colors
79
+ # @param r [Integer] Red. Valid range: `0..255`
80
+ # @param g [Integer] Green. Valid range: `0..255`
81
+ # @param b [Integer] Blue. Valid range: `0..255`
82
+ # @return [Color]
83
+ def rgb(r, g, b)
84
+ r = r / 255.0
85
+ g = g / 255.0
86
+ b = b / 255.0
87
+
88
+ max = [r, g, b].max
89
+ min = [r, g, b].min
90
+
91
+ h = s = v = max
92
+ d = max - min
93
+ s = max.zero? ? 0 : d / max
94
+
95
+ if max == min
96
+ h = 0
97
+ else
98
+ case max
99
+ when r
100
+ h = (g - b) / d + (g < b ? 6 : 0)
101
+ when g
102
+ h = (b - r) / d + 2
103
+ when b
104
+ h = (r - g) / d + 4
105
+ end
106
+ h = h * 60
107
+ end
108
+
109
+ new(h, s, v, DEFAULT_KELVIN)
110
+ end
111
+
112
+ # Creates an instance from a {Protocol::Light::Hsbk} struct
113
+ # @api private
114
+ # @param hsbk [Protocol::Light::Hsbk]
115
+ # @return [Color]
116
+ def from_struct(hsbk)
117
+ new(
118
+ (hsbk.hue.to_f / UINT16_MAX) * 360,
119
+ (hsbk.saturation.to_f / UINT16_MAX),
120
+ (hsbk.brightness.to_f / UINT16_MAX),
121
+ hsbk.kelvin
122
+ )
123
+ end
124
+ end
125
+
126
+ def initialize(hue, saturation, brightness, kelvin)
127
+ hue = hue % 360
128
+ super(hue, saturation, brightness, kelvin)
129
+ end
130
+
131
+ # Returns a new Color with the hue changed while keeping other attributes
132
+ # @param hue [Float] Hue in degrees. `0..360`
133
+ # @return [Color]
134
+ def with_hue(hue)
135
+ Color.new(hue, saturation, brightness, kelvin)
136
+ end
137
+
138
+ # Returns a new Color with the saturaiton changed while keeping other attributes
139
+ # @param saturaiton [Float] Saturation as float. `0..1`
140
+ # @return [Color]
141
+ def with_saturation(saturation)
142
+ Color.new(hue, saturation, brightness, kelvin)
143
+ end
144
+
145
+ # Returns a new Color with the brightness changed while keeping other attributes
146
+ # @param brightness [Float] Brightness as float. `0..1`
147
+ # @return [Color]
148
+ def with_brightness(brightness)
149
+ Color.new(hue, saturation, brightness, kelvin)
150
+ end
151
+
152
+ # Returns a new Color with the kelvin changed while keeping other attributes
153
+ # @param kelvin [Integer] Kelvin. `2500..10000`
154
+ # @return [Color]
155
+ def with_kelvin(kelvin)
156
+ Color.new(hue, saturation, brightness, kelvin)
157
+ end
158
+
159
+ # Returns a struct for use by the protocol
160
+ # @api private
161
+ # @return [Protocol::Light::Hsbk]
162
+ def to_hsbk
163
+ Protocol::Light::Hsbk.new(
164
+ hue: (hue / 360.0 * UINT16_MAX).to_i,
165
+ saturation: (saturation * UINT16_MAX).to_i,
166
+ brightness: (brightness * UINT16_MAX).to_i,
167
+ kelvin: [KELVIN_MIN, kelvin.to_i, KELVIN_MAX].sort[1]
168
+ )
169
+ end
170
+
171
+ # Returns hue, saturation, brightness and kelvin in an array
172
+ # @return [Array<Float, Float, Float, Integer>]
173
+ def to_a
174
+ [hue, saturation, brightness, kelvin]
175
+ end
176
+
177
+ EQUALITY_THRESHOLD = 0.001 # 0.1% variance
178
+ # Checks if colours are equal to 0.1% variance
179
+ # @param other [Color] Color to compare to
180
+ # @return [Boolean]
181
+ def ==(other)
182
+ return false unless other.is_a?(Color)
183
+ conditions = []
184
+ conditions << ((hue - other.hue).abs < (EQUALITY_THRESHOLD * 360))
185
+ conditions << ((saturation - other.saturation).abs < EQUALITY_THRESHOLD)
186
+ conditions << ((brightness - other.brightness).abs < EQUALITY_THRESHOLD)
187
+ conditions.all?
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,12 @@
1
+ require 'configatron/core'
2
+ require 'yell'
3
+ module LIFX
4
+ Config = Configatron::Store.new
5
+
6
+ Config.default_duration = 1
7
+ Config.allowed_transports = [:udp, :tcp]
8
+ Config.logger = Yell.new do |logger|
9
+ logger.level = 'gte.warn'
10
+ logger.adapter STDERR, format: '%d [%5L] %p/%t : %m'
11
+ end
12
+ end
@@ -0,0 +1,55 @@
1
+ module LIFX
2
+ # LIFX::Firmware handles decoding firmware payloads
3
+ class Firmware < Struct.new(:build_time, :major, :minor)
4
+ include Comparable
5
+
6
+ def initialize(payload)
7
+ self.build_time = decode_time(payload.build)
8
+ self.major = (payload.version >> 0x10)
9
+ self.minor = (payload.version & 0xFF)
10
+ end
11
+
12
+ def to_s
13
+ "#<Firmware version=#{self.major}.#{self.minor}>"
14
+ end
15
+ alias_method :inspect, :to_s
16
+
17
+ def <=>(obj)
18
+ case obj
19
+ when String
20
+ major, minor = obj.split('.', 2).map(&:to_i)
21
+ [self.major, self.minor] <=> [major, minor]
22
+ when Firmware
23
+ [self.major, self.minor] <=> [obj.major, obj.minor]
24
+ else
25
+ nil
26
+ end
27
+ end
28
+
29
+
30
+ protected
31
+
32
+ def decode_time(int)
33
+ if int < 1300000000000000000
34
+ year = byte(int, 56) + 2000
35
+ month = bytes(int, 48, 40, 32).map(&:chr).join
36
+ day = byte(int, 24)
37
+ hour = byte(int, 16)
38
+ min = byte(int, 8)
39
+ sec = byte(int, 0)
40
+ # Don't want to pull in DateTime just for DateTime.new
41
+ Time.parse("%s %d %04d, %02d:%02d:%02d" % [month, day, year, hour, min, sec])
42
+ else
43
+ Time.at(int / 1000000000)
44
+ end
45
+ end
46
+
47
+ def byte(n, pos)
48
+ 0xFF & (n >> pos)
49
+ end
50
+
51
+ def bytes(n, *range)
52
+ range.map {|r| byte(n, r)}
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,177 @@
1
+ require 'lifx/observable'
2
+ require 'lifx/timers'
3
+
4
+ module LIFX
5
+ # @api private
6
+ class GatewayConnection
7
+ # GatewayConnection handles the UDP and TCP connections to the gateway
8
+ # A GatewayConnection is created when a new device sends a StatePanGateway
9
+ include Timers
10
+ include Logging
11
+ include Observable
12
+
13
+ MAX_TCP_ATTEMPTS = 3
14
+ def initialize
15
+ @threads = []
16
+ @tcp_attempts = 0
17
+ @threads << initialize_write_queue
18
+ end
19
+
20
+ def handle_message(message, ip, transport)
21
+ payload = message.payload
22
+ case payload
23
+ when Protocol::Device::StatePanGateway
24
+ if use_udp? && !udp_connected? && payload.service == Protocol::Device::Service::UDP
25
+ # UDP transport here is only for sending directly to bulb
26
+ # We receive responses via UDP transport listening to broadcast in Network
27
+ connect_udp(ip, payload.port.to_i)
28
+ elsif use_tcp? && !tcp_connected? && payload.service == Protocol::Device::Service::TCP && (port = payload.port.snapshot) > 0
29
+ connect_tcp(ip, port)
30
+ end
31
+ else
32
+ logger.error("#{self}: Unhandled message: #{message}")
33
+ end
34
+ end
35
+
36
+ def use_udp?
37
+ Config.allowed_transports.include?(:udp)
38
+ end
39
+
40
+ def use_tcp?
41
+ Config.allowed_transports.include?(:tcp)
42
+ end
43
+
44
+ def udp_connected?
45
+ @udp_transport && @udp_transport.connected?
46
+ end
47
+
48
+ def tcp_connected?
49
+ @tcp_transport && @tcp_transport.connected?
50
+ end
51
+
52
+ def connected?
53
+ udp_connected? || tcp_connected?
54
+ end
55
+
56
+ def connect_udp(ip, port)
57
+ @udp_transport = Transport::UDP.new(ip, port)
58
+ end
59
+
60
+ def connect_tcp(ip, port)
61
+ if @tcp_attempts > MAX_TCP_ATTEMPTS
62
+ logger.info("#{self}: Ignoring TCP service of #{ip}:#{port} due to too many failed attempts.")
63
+ return
64
+ end
65
+ @tcp_attempts += 1
66
+ logger.info("#{self}: Establishing connection to #{ip}:#{port}")
67
+ @tcp_transport = Transport::TCP.new(ip, port)
68
+ @tcp_transport.add_observer(self) do |message:, ip:, transport:|
69
+ notify_observers(message: message, ip: ip, transport: @tcp_transport)
70
+ end
71
+ @tcp_transport.listen
72
+ at_exit do
73
+ @tcp_transport.close if @tcp_transport
74
+ end
75
+ end
76
+
77
+ def write(message)
78
+ @queue.push(message)
79
+ end
80
+
81
+ def close
82
+ @threads.each { |thr| Thread.kill(thr) }
83
+ [@tcp_transport, @udp_transport].compact.each(&:close)
84
+ end
85
+
86
+ def flush(timeout: nil)
87
+ proc = lambda do
88
+ while !@queue.empty?
89
+ sleep 0.05
90
+ end
91
+ end
92
+ if timeout
93
+ Timeout.timeout(timeout) do
94
+ proc.call
95
+ end
96
+ else
97
+ proc.call
98
+ end
99
+ end
100
+
101
+ def to_s
102
+ "#<LIFX::GatewayConnection tcp=#{@tcp_transport} tcp_attempts=#{@tcp_attempts} udp=#{@udp_transport}>"
103
+ end
104
+ alias_method :inspect, :to_s
105
+
106
+ def set_message_rate(rate)
107
+ @message_rate = rate
108
+ end
109
+ protected
110
+
111
+ MAXIMUM_QUEUE_LENGTH = 10
112
+ DEFAULT_MESSAGE_RATE = 5
113
+ def message_rate
114
+ @message_rate || DEFAULT_MESSAGE_RATE
115
+ end
116
+
117
+ def initialize_write_queue
118
+ @queue = SizedQueue.new(MAXIMUM_QUEUE_LENGTH)
119
+ @last_write = Time.now
120
+ Thread.abort_on_exception = true
121
+ Thread.new do
122
+ loop do
123
+ if !connected?
124
+ sleep 0.1
125
+ next
126
+ end
127
+ delay = [(1.0 / message_rate) - (Time.now - @last_write), 0].max
128
+ logger.debug("#{self}: Sleeping for #{delay}")
129
+ sleep(delay)
130
+ message = @queue.pop
131
+ if !message.is_a?(Message)
132
+ raise ArgumentError.new("Unexpected object in message queue: #{message.inspect}")
133
+ end
134
+ if !actually_write(message)
135
+ logger.error("#{self}: Couldn't write, pushing back onto queue.")
136
+ @queue << message
137
+ end
138
+ @last_write = Time.now
139
+ end
140
+ end
141
+ end
142
+
143
+ def check_connections
144
+ if @tcp_transport && !tcp_connected?
145
+ @tcp_transport = nil
146
+ logger.info("#{self}: TCP connection dropped, clearing.")
147
+ end
148
+
149
+ if @udp_transport && !udp_connected?
150
+ @udp_transport = nil
151
+ logger.info("#{self}: UDP connection dropped, clearing.")
152
+ end
153
+ end
154
+
155
+ def actually_write(message)
156
+ check_connections
157
+
158
+ # TODO: Support force sending over UDP
159
+ if tcp_connected?
160
+ if @tcp_transport.write(message)
161
+ logger.debug("-> #{self} #{@tcp_transport}: #{message}")
162
+ return true
163
+ end
164
+ end
165
+
166
+ if udp_connected?
167
+ if @udp_transport.write(message)
168
+ logger.debug("-> #{self} #{@tcp_transport}: #{message}")
169
+ return true
170
+ end
171
+ end
172
+
173
+ false
174
+ end
175
+
176
+ end
177
+ end