lifx-lan 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +8 -0
  4. data/.yardopts +3 -0
  5. data/CHANGES.md +45 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +23 -0
  8. data/README.md +15 -0
  9. data/Rakefile +20 -0
  10. data/bin/lifx-snoop +50 -0
  11. data/examples/auto-off/auto-off.rb +34 -0
  12. data/examples/blink/blink.rb +19 -0
  13. data/examples/identify/identify.rb +69 -0
  14. data/examples/travis-build-light/build-light.rb +57 -0
  15. data/lib/bindata_ext/bool.rb +30 -0
  16. data/lib/bindata_ext/record.rb +11 -0
  17. data/lib/lifx-lan.rb +27 -0
  18. data/lib/lifx/lan/client.rb +149 -0
  19. data/lib/lifx/lan/color.rb +199 -0
  20. data/lib/lifx/lan/config.rb +17 -0
  21. data/lib/lifx/lan/firmware.rb +60 -0
  22. data/lib/lifx/lan/gateway_connection.rb +185 -0
  23. data/lib/lifx/lan/light.rb +440 -0
  24. data/lib/lifx/lan/light_collection.rb +111 -0
  25. data/lib/lifx/lan/light_target.rb +185 -0
  26. data/lib/lifx/lan/logging.rb +14 -0
  27. data/lib/lifx/lan/message.rb +168 -0
  28. data/lib/lifx/lan/network_context.rb +188 -0
  29. data/lib/lifx/lan/observable.rb +66 -0
  30. data/lib/lifx/lan/protocol/address.rb +25 -0
  31. data/lib/lifx/lan/protocol/device.rb +387 -0
  32. data/lib/lifx/lan/protocol/header.rb +24 -0
  33. data/lib/lifx/lan/protocol/light.rb +142 -0
  34. data/lib/lifx/lan/protocol/message.rb +19 -0
  35. data/lib/lifx/lan/protocol/metadata.rb +23 -0
  36. data/lib/lifx/lan/protocol/payload.rb +12 -0
  37. data/lib/lifx/lan/protocol/sensor.rb +31 -0
  38. data/lib/lifx/lan/protocol/type.rb +204 -0
  39. data/lib/lifx/lan/protocol/wan.rb +51 -0
  40. data/lib/lifx/lan/protocol/wifi.rb +102 -0
  41. data/lib/lifx/lan/protocol_path.rb +85 -0
  42. data/lib/lifx/lan/required_keyword_arguments.rb +12 -0
  43. data/lib/lifx/lan/routing_manager.rb +114 -0
  44. data/lib/lifx/lan/routing_table.rb +48 -0
  45. data/lib/lifx/lan/seen.rb +25 -0
  46. data/lib/lifx/lan/site.rb +97 -0
  47. data/lib/lifx/lan/tag_manager.rb +111 -0
  48. data/lib/lifx/lan/tag_table.rb +49 -0
  49. data/lib/lifx/lan/target.rb +24 -0
  50. data/lib/lifx/lan/thread.rb +13 -0
  51. data/lib/lifx/lan/timers.rb +29 -0
  52. data/lib/lifx/lan/transport.rb +46 -0
  53. data/lib/lifx/lan/transport/tcp.rb +91 -0
  54. data/lib/lifx/lan/transport/udp.rb +87 -0
  55. data/lib/lifx/lan/transport_manager.rb +43 -0
  56. data/lib/lifx/lan/transport_manager/lan.rb +169 -0
  57. data/lib/lifx/lan/utilities.rb +36 -0
  58. data/lib/lifx/lan/version.rb +5 -0
  59. data/lifx-lan.gemspec +26 -0
  60. data/spec/color_spec.rb +43 -0
  61. data/spec/gateway_connection_spec.rb +30 -0
  62. data/spec/integration/client_spec.rb +42 -0
  63. data/spec/integration/light_spec.rb +56 -0
  64. data/spec/integration/tags_spec.rb +42 -0
  65. data/spec/light_collection_spec.rb +37 -0
  66. data/spec/message_spec.rb +183 -0
  67. data/spec/protocol_path_spec.rb +109 -0
  68. data/spec/routing_manager_spec.rb +25 -0
  69. data/spec/routing_table_spec.rb +23 -0
  70. data/spec/spec_helper.rb +56 -0
  71. data/spec/transport/udp_spec.rb +44 -0
  72. data/spec/transport_spec.rb +14 -0
  73. metadata +187 -0
@@ -0,0 +1,51 @@
1
+ module LIFX
2
+ module LAN
3
+ module Protocol
4
+ # @api private
5
+ module Wan
6
+ class ConnectPlain < Payload
7
+ endian :little
8
+
9
+ string :user, length: 32, trim_padding: true
10
+ string :pass, length: 32, trim_padding: true
11
+ end
12
+
13
+ class ConnectKey < Payload
14
+ endian :little
15
+
16
+ string :auth_key, length: 32
17
+ end
18
+
19
+ class StateConnect < Payload
20
+ endian :little
21
+
22
+ string :auth_key, length: 32
23
+ end
24
+
25
+ class Sub < Payload
26
+ endian :little
27
+
28
+ string :target, length: 8
29
+ string :site, length: 6
30
+ bool :device # 0 - Targets a device. 1 - Targets a tag.
31
+ end
32
+
33
+ class Unsub < Payload
34
+ endian :little
35
+
36
+ string :target, length: 8
37
+ string :site, length: 6
38
+ bool :device # 0 - Targets a device. 1 - Targets a tag.
39
+ end
40
+
41
+ class StateSub < Payload
42
+ endian :little
43
+
44
+ string :target, length: 8
45
+ string :site, length: 6
46
+ bool :device # 0 - Targets a device. 1 - Targets a tag.
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,102 @@
1
+ module LIFX
2
+ module LAN
3
+ module Protocol
4
+ # @api private
5
+ module Wifi
6
+ module Interface
7
+ SOFT_AP = 1
8
+ STATION = 2
9
+ end
10
+
11
+ module Security
12
+ UNKNOWN = 0
13
+ OPEN = 1
14
+ WEP_PSK = 2
15
+ WPA_TKIP_PSK = 3
16
+ WPA_AES_PSK = 4
17
+ WPA2_AES_PSK = 5
18
+ WPA2_TKIP_PSK = 6
19
+ WPA2_MIXED_PSK = 7
20
+ end
21
+
22
+ module Status
23
+ CONNECTING = 0
24
+ CONNECTED = 1
25
+ FAILED = 2
26
+ OFF = 3
27
+ end
28
+
29
+ # Gets interface status
30
+ class Get < Payload
31
+ endian :little
32
+
33
+ uint8 :interface # Interface to introspect
34
+ end
35
+
36
+ # Switch interface on/off
37
+ class Set < Payload
38
+ endian :little
39
+
40
+ uint8 :interface # Interface to introspect
41
+ bool :active # Turns off interface if active is false
42
+ end
43
+
44
+ # Describes interface state
45
+ class State < Payload
46
+ endian :little
47
+
48
+ uint8 :interface # Interface to introspect
49
+ uint8 :status # Interface status
50
+ uint32 :ipv4 # IPv4 address if interface is active
51
+ string :ipv6, length: 16 # IPv6 address if interface is active and is IPv6 addressable
52
+ end
53
+
54
+ # Scan for WiFi access points. Returns StateAccessPoints
55
+ class GetAccessPoints < Payload
56
+ endian :little
57
+
58
+ end
59
+
60
+ # The AP scan results for an interface
61
+ class StateAccessPoints < Payload
62
+ endian :little
63
+
64
+ uint8 :interface # Interface to introspect
65
+ string :ssid, length: 32, trim_padding: true # SSID of Access Point
66
+ uint8 :security # Security mode
67
+ int16 :strength # Signal strength in dB
68
+ uint16 :channel # Frequency channel
69
+ end
70
+
71
+ # Get WiFi access point information. Returns StateAccessPoint
72
+ class GetAccessPoint < Payload
73
+ endian :little
74
+
75
+ uint8 :interface # Interface to introspect
76
+ end
77
+
78
+ # Configure interface, should return StateAccessPoint
79
+ class SetAccessPoint < Payload
80
+ endian :little
81
+
82
+ uint8 :interface # Interface to introspect
83
+ string :ssid, length: 32, trim_padding: true # SSID of Access Point
84
+ string :pass, length: 64, trim_padding: true # Passphrase used to authenticate
85
+ uint8 :security # Security mode
86
+ end
87
+
88
+ # Interface configuration
89
+ class StateAccessPoint < Payload
90
+ endian :little
91
+
92
+ uint8 :interface # Interface to introspect
93
+ string :ssid, length: 32, trim_padding: true # SSID of Access Point
94
+ uint8 :security # Security mode
95
+ int16 :strength # Signal strength in dB
96
+ uint16 :channel # Frequency channel
97
+ end
98
+
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,85 @@
1
+ require 'lifx/lan/utilities'
2
+
3
+ module LIFX
4
+ module LAN
5
+ # @private
6
+ class ProtocolPath
7
+ # ProtocolPath contains all the addressable information that is required
8
+ # for the protocol.
9
+ # It handles the conversion between raw binary and hex strings
10
+ # as well as raw tags_field to tags array
11
+ include Utilities
12
+
13
+ attr_accessor :raw_site, :raw_target, :tagged
14
+
15
+ def initialize(raw_site: "\x00".b * 6, raw_target: "\x00".b * 8, tagged: false,
16
+ site_id: nil, device_id: nil, tag_ids: nil)
17
+ self.raw_site = raw_site
18
+ self.raw_target = raw_target
19
+ self.tagged = tagged
20
+
21
+ self.site_id = site_id if site_id
22
+ self.device_id = device_id if device_id
23
+ self.tag_ids = tag_ids if tag_ids
24
+ end
25
+
26
+ def site_id
27
+ raw_site.unpack('H*').join
28
+ end
29
+
30
+ def site_id=(value)
31
+ self.raw_site = [value].pack('H12').b
32
+ end
33
+
34
+ def device_id
35
+ if !tagged?
36
+ raw_target[0...6].unpack('H*').join
37
+ else
38
+ nil
39
+ end
40
+ end
41
+
42
+ def device_id=(value)
43
+ self.raw_target = [value].pack('H16').b
44
+ self.tagged = false
45
+ end
46
+
47
+ def tag_ids
48
+ if tagged?
49
+ tag_ids_from_field(tags_field)
50
+ else
51
+ nil
52
+ end
53
+ end
54
+
55
+ def tag_ids=(values)
56
+ self.tags_field = values.reduce(0) do |value, tag_id|
57
+ value |= 2 ** tag_id
58
+ end
59
+ end
60
+
61
+ def tagged?
62
+ !!tagged
63
+ end
64
+
65
+ def all_sites?
66
+ site_id == "000000000000"
67
+ end
68
+
69
+ def all_tags?
70
+ tagged? && tag_ids.empty?
71
+ end
72
+
73
+ protected
74
+
75
+ def tags_field
76
+ raw_target.unpack('Q').first
77
+ end
78
+
79
+ def tags_field=(value)
80
+ self.raw_target = [value].pack('Q').b
81
+ self.tagged = true
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,12 @@
1
+ module LIFX
2
+ module LAN
3
+ module RequiredKeywordArguments
4
+ def required!(name)
5
+ backtrace = caller_locations(1).map { |c| c.to_s }
6
+ ex = ArgumentError.new("Missing required keyword argument '#{name}'")
7
+ ex.set_backtrace(backtrace)
8
+ raise ex
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,114 @@
1
+ require 'lifx/lan/routing_table'
2
+ require 'lifx/lan/tag_table'
3
+ require 'lifx/lan/utilities'
4
+ require 'weakref'
5
+
6
+ module LIFX
7
+ module LAN
8
+ # @private
9
+ class RoutingManager
10
+ include Logging
11
+ include Utilities
12
+ include RequiredKeywordArguments
13
+
14
+ # RoutingManager manages a routing table of site <-> device
15
+ # It can resolve a target to ProtocolPaths and manages the TagTable
16
+
17
+ attr_reader :context, :tag_table, :routing_table
18
+
19
+ STALE_ROUTING_TABLE_PURGE_INTERVAL = 60
20
+
21
+ def initialize(context: required!(:context))
22
+ @context = WeakRef.new(context)
23
+ @routing_table = RoutingTable.new
24
+ @tag_table = TagTable.new
25
+ @last_refresh_seen = {}
26
+ @context.timers.every(STALE_ROUTING_TABLE_PURGE_INTERVAL) do
27
+ routing_table.clear_stale_entries
28
+ end
29
+ end
30
+
31
+ def resolve_target(target)
32
+ if target.tag?
33
+ @tag_table.entries_with(label: target.tag).map do |entry|
34
+ ProtocolPath.new(site_id: entry.site_id, tag_ids: [entry.tag_id])
35
+ end
36
+ elsif target.broadcast?
37
+ [ProtocolPath.new(site_id: "\x00".b * 6, tag_ids: [])]
38
+ elsif target.site_id && target.device_id.nil?
39
+ [ProtocolPath.new(site_id: target.site_id, tag_ids: [])]
40
+ else
41
+ [ProtocolPath.new(site_id: target.site_id, device_id: target.device_id)]
42
+ end
43
+ end
44
+
45
+ def tags_for_device_id(device_id)
46
+ entry = @routing_table.entry_for_device_id(device_id)
47
+ return [] if entry.nil?
48
+ entry.tag_ids.map do |tag_id|
49
+ tag = @tag_table.entry_with(device_id: entry.device_id, tag_id: tag_id)
50
+ tag && tag.label
51
+ end.compact
52
+ end
53
+
54
+ def update_from_message(message)
55
+ return if message.site_id == NULL_SITE_ID
56
+ if message.tagged?
57
+ case message.payload
58
+ when Protocol::Light::Get
59
+ if message.path.all_tags?
60
+ @last_refresh_seen[message.device_id] = Time.now
61
+ end
62
+ end
63
+ return
64
+ end
65
+
66
+ payload = message.payload
67
+ if !@routing_table.device_ids.include?(message.device_id)
68
+ # New device detected, fire refresh events
69
+ refresh_site(message.site_id, message.device_id)
70
+ end
71
+ case payload
72
+ when Protocol::Device::StateTagLabels
73
+ tag_ids = tag_ids_from_field(payload.tags)
74
+ if payload.label.empty?
75
+ tag_ids.each do |tag_id|
76
+ @tag_table.delete_entries_with(device_id: message.device_id, tag_id: tag_id)
77
+ end
78
+ else
79
+ @tag_table.update_table(device_id: message.device_id, tag_id: tag_ids.first, label: payload.label.to_s.force_encoding('utf-8'))
80
+ end
81
+ when Protocol::Device::StateTags, Protocol::Light::State
82
+ @routing_table.update_table(site_id: message.site_id,
83
+ device_id: message.device_id,
84
+ tag_ids: tag_ids_from_field(message.payload.tags))
85
+ when Protocol::Device::StateService, Protocol::Device::StatePower
86
+ @routing_table.update_table(site_id: message.site_id, device_id: message.device_id)
87
+ end
88
+ end
89
+
90
+ MINIMUM_REFRESH_INTERVAL = 1
91
+ def refresh(force: false)
92
+ @routing_table.device_ids.each do |device_id|
93
+ next if (seen = @last_refresh_seen[device_id]) && Time.now - seen < MINIMUM_REFRESH_INTERVAL && !force
94
+ site_id = @routing_table.site_id_for_device_id(device_id)
95
+ refresh_site(site_id, device_id)
96
+ end
97
+ end
98
+
99
+ def refresh_site(site_id, device_id)
100
+ get_lights(site_id, device_id)
101
+ get_tag_labels(site_id, device_id)
102
+ end
103
+
104
+ def get_lights(site_id, device_id)
105
+ context.send_message(target: Target.new(site_id: site_id, device_id: device_id), payload: Protocol::Light::Get.new)
106
+ end
107
+
108
+ UINT64_MAX = 2 ** 64 - 1
109
+ def get_tag_labels(site_id, device_id)
110
+ context.send_message(target: Target.new(site_id: site_id, device_id: device_id), payload: Protocol::Device::GetTagLabels.new(tags: UINT64_MAX))
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,48 @@
1
+ module LIFX
2
+ module LAN
3
+ # @private
4
+ class RoutingTable
5
+ include Logging
6
+
7
+ class Entry < Struct.new(:site_id, :device_id, :tag_ids, :last_seen); end
8
+ # RoutingTable stores the device <-> site mapping
9
+ def initialize(entries: {})
10
+ @device_site_mapping = entries
11
+ end
12
+
13
+ def update_table(site_id: self.site_id, device_id: self.device_id, tag_ids: nil, last_seen: Time.now)
14
+ device_mapping = @device_site_mapping[device_id] ||= Entry.new(site_id, device_id, [])
15
+ device_mapping.site_id = site_id
16
+ device_mapping.last_seen = last_seen
17
+ device_mapping.tag_ids = tag_ids if tag_ids
18
+ end
19
+
20
+ def entry_for_device_id(device_id)
21
+ @device_site_mapping[device_id]
22
+ end
23
+
24
+ def site_id_for_device_id(device_id)
25
+ entry = entry_for_device_id(device_id)
26
+ entry ? entry.site_id : nil
27
+ end
28
+
29
+ def site_ids
30
+ entries.map(&:site_id).uniq
31
+ end
32
+
33
+ def device_ids
34
+ entries.map(&:device_id).uniq
35
+ end
36
+
37
+ def entries
38
+ @device_site_mapping.values
39
+ end
40
+
41
+ def clear_stale_entries(threshold: 60 * 5)
42
+ @device_site_mapping.reject! do |device_id, entry|
43
+ entry.last_seen < Time.now - threshold
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ module LIFX
2
+ module LAN
3
+ module Seen
4
+ # Returns the time when the device was last seen.
5
+ # @return [Time]
6
+ def last_seen
7
+ @last_seen
8
+ end
9
+
10
+ # Returns the number of seconds since the device was last seen.
11
+ # If the device hasn't been seen yet, it will use Unix epoch as
12
+ # the time it was seen.
13
+ # @return [Float]
14
+ def seconds_since_seen
15
+ Time.now - (last_seen || Time.at(0))
16
+ end
17
+
18
+ # Marks the device as being seen.
19
+ # @private
20
+ def seen!
21
+ @last_seen = Time.now
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,97 @@
1
+ require 'lifx/lan/seen'
2
+ require 'lifx/lan/timers'
3
+ require 'lifx/lan/observable'
4
+ require 'lifx/lan/gateway_connection'
5
+
6
+ module LIFX
7
+ module LAN
8
+ # @api private
9
+ class Site
10
+ include Seen
11
+ include Timers
12
+ include Logging
13
+ include Observable
14
+ include RequiredKeywordArguments
15
+
16
+ attr_reader :id, :gateways, :tag_manager
17
+
18
+ def initialize(id: required!(:id))
19
+ @id = id
20
+ @gateways = {}
21
+ @gateways_mutex = Mutex.new
22
+ @threads = []
23
+ @threads << initialize_timer_thread
24
+ initialize_stale_gateway_check
25
+ end
26
+
27
+ def write(message)
28
+ message.path.site_id = id
29
+ @gateways.values.each do |gateway|
30
+ gateway.write(message)
31
+ end
32
+ end
33
+
34
+ def handle_message(message, ip, transport)
35
+ logger.debug("<- #{self} #{transport}: #{message}")
36
+ payload = message.payload
37
+ case payload
38
+ when Protocol::Device::StateService
39
+ @gateways_mutex.synchronize do
40
+ @gateways[message.device_id] ||= GatewayConnection.new
41
+ @gateways[message.device_id].handle_message(message, ip, transport)
42
+ @gateways[message.device_id].add_observer(self, :message_received) do |**args|
43
+ notify_observers(:message_received, **args)
44
+ end
45
+ end
46
+ end
47
+ seen!
48
+ end
49
+
50
+ def flush(**options)
51
+ @gateways.values.map do |gateway|
52
+ Thread.start do
53
+ gateway.flush(**options)
54
+ end
55
+ end.each(&:join)
56
+ end
57
+
58
+ def to_s
59
+ %Q{#<LIFX::LAN::Site id=#{id}>}
60
+ end
61
+ alias_method :inspect, :to_s
62
+
63
+ def stop
64
+ @threads.each do |thread|
65
+ thread.abort
66
+ end
67
+ @gateways.values.each do |gateway|
68
+ gateway.close
69
+ end
70
+ end
71
+
72
+ def observer_callback_definition
73
+ {
74
+ message_received: -> (message: nil, ip: nil, transport: nil) {}
75
+ }
76
+ end
77
+
78
+
79
+ protected
80
+
81
+ STALE_GATEWAY_CHECK_INTERVAL = 10
82
+ def initialize_stale_gateway_check
83
+ timers.every(STALE_GATEWAY_CHECK_INTERVAL) do
84
+ @gateways_mutex.synchronize do
85
+ stale_gateways = @gateways.select do |k, v|
86
+ !v.connected?
87
+ end
88
+ stale_gateways.each do |id, _|
89
+ logger.info("#{self}: Dropping stale gateway id #{id}")
90
+ @gateways.delete(id)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end