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/message.rb
    ADDED
    
    | @@ -0,0 +1,166 @@ | |
| 1 | 
            +
            require 'forwardable'
         | 
| 2 | 
            +
            require 'lifx/protocol_path'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module LIFX
         | 
| 5 | 
            +
              # @api private
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              class Message
         | 
| 8 | 
            +
                include Logging
         | 
| 9 | 
            +
                extend Forwardable
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                class MessageError < StandardError; end
         | 
| 12 | 
            +
                class UnpackError < MessageError; end
         | 
| 13 | 
            +
                class PackError < MessageError; end
         | 
| 14 | 
            +
                class NoPath < MessageError; end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                class InvalidFrame < UnpackError; end
         | 
| 17 | 
            +
                class UnsupportedProtocolVersion < UnpackError; end
         | 
| 18 | 
            +
                class NotAddressableFrame < UnpackError; end
         | 
| 19 | 
            +
                class NoPayload < PackError; end
         | 
| 20 | 
            +
                class UnmappedPayload < MessageError; end
         | 
| 21 | 
            +
                class InvalidFields < PackError; end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                PROTOCOL_VERSION = 1024
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                class << self
         | 
| 26 | 
            +
                  attr_accessor :log_invalid_messages
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def unpack(data)
         | 
| 29 | 
            +
                    raise InvalidFrame if data.length < 2
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    header = Protocol::Header.read(data)
         | 
| 32 | 
            +
                    raise UnsupportedProtocolVersion.new("Expected #{PROTOCOL_VERSION} but got #{header.protocol} instead") if header.protocol != PROTOCOL_VERSION
         | 
| 33 | 
            +
                    raise NotAddressableFrame if header.addressable == 0
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    message = Protocol::Message.read(data)
         | 
| 36 | 
            +
                    path = ProtocolPath.new(raw_site: message.raw_site, raw_target: message.raw_target, tagged: message.tagged)
         | 
| 37 | 
            +
                    payload_class = message_type_for_id(message.type.snapshot)
         | 
| 38 | 
            +
                    if payload_class.nil?
         | 
| 39 | 
            +
                      if self.log_invalid_messages
         | 
| 40 | 
            +
                        logger.error("Message.unpack: Unrecognised payload ID: #{message.type}")
         | 
| 41 | 
            +
                        logger.error("Message.unpack: Message: #{message}")
         | 
| 42 | 
            +
                      end
         | 
| 43 | 
            +
                      return nil # FIXME
         | 
| 44 | 
            +
                      raise UnmappedPayload.new("Unrecognised payload ID: #{message.type}")
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
                    begin
         | 
| 47 | 
            +
                      payload = payload_class.read(message.payload)
         | 
| 48 | 
            +
                    rescue => ex
         | 
| 49 | 
            +
                      if message.raw_site == "\x00" * 6
         | 
| 50 | 
            +
                        logger.info("Message.unpack: Ignoring malformed message from virgin bulb")
         | 
| 51 | 
            +
                      else
         | 
| 52 | 
            +
                        if self.log_invalid_messages
         | 
| 53 | 
            +
                          logger.error("Message.unpack: Exception while unpacking payload of type #{payload_class}: #{ex}")
         | 
| 54 | 
            +
                          logger.error("Message.unpack: Data: #{data.inspect}")
         | 
| 55 | 
            +
                        end
         | 
| 56 | 
            +
                      end
         | 
| 57 | 
            +
                    end
         | 
| 58 | 
            +
                    new(path, message, payload)
         | 
| 59 | 
            +
                  rescue => ex
         | 
| 60 | 
            +
                    if self.log_invalid_messages
         | 
| 61 | 
            +
                      logger.debug("Message.unpack: Exception while unpacking #{data.inspect}")
         | 
| 62 | 
            +
                      logger.debug("Message.unpack: #{ex} - #{ex.backtrace.join("\n")}")
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
                    raise ex
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  def message_type_for_id(type_id)
         | 
| 68 | 
            +
                    Protocol::TYPE_ID_TO_CLASS[type_id]
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  def type_id_for_message_class(klass)
         | 
| 72 | 
            +
                    Protocol::CLASS_TO_TYPE_ID[klass]
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  def valid_fields
         | 
| 76 | 
            +
                    @valid_fields ||= Protocol::Message.new.field_names.map(&:to_sym)
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                LIFX::Protocol::Message.fields.each do |field|
         | 
| 81 | 
            +
                  define_method(field.name) do
         | 
| 82 | 
            +
                    @message.send(field.name).snapshot
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  define_method("#{field.name}=") do |value|
         | 
| 86 | 
            +
                    @message.send("#{field.name}=", value)
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                alias_method :tagged?, :tagged
         | 
| 91 | 
            +
                alias_method :addressable?, :addressable
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                def_delegators :path, :device_id, :site_id, :tagged
         | 
| 94 | 
            +
                
         | 
| 95 | 
            +
                attr_accessor :path, :payload
         | 
| 96 | 
            +
                def initialize(*args)
         | 
| 97 | 
            +
                  if args.count == 3 
         | 
| 98 | 
            +
                    @path, @message, @payload = args
         | 
| 99 | 
            +
                  elsif (hash = args.first).is_a?(Hash)
         | 
| 100 | 
            +
                    path = hash.delete(:path)
         | 
| 101 | 
            +
                    payload = hash.delete(:payload)
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                    check_valid_fields!(hash)
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                    @message = Protocol::Message.new(hash)
         | 
| 106 | 
            +
                    self.payload = payload
         | 
| 107 | 
            +
                    self.path = path
         | 
| 108 | 
            +
                    @message.tagged = path.tagged? if path
         | 
| 109 | 
            +
                  else
         | 
| 110 | 
            +
                    @message = Protocol::Message.new
         | 
| 111 | 
            +
                  end
         | 
| 112 | 
            +
                  @message.msg_size = @message.num_bytes
         | 
| 113 | 
            +
                  @message.protocol = PROTOCOL_VERSION
         | 
| 114 | 
            +
                rescue => ex
         | 
| 115 | 
            +
                  raise MessageError.new("Unable to initialize message with args: #{args.inspect} - #{ex}")
         | 
| 116 | 
            +
                end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                def payload=(payload)
         | 
| 119 | 
            +
                  @payload = payload
         | 
| 120 | 
            +
                  type_id = self.class.type_id_for_message_class(payload.class)
         | 
| 121 | 
            +
                  if type_id.nil?
         | 
| 122 | 
            +
                    raise UnmappedPayload.new("Unmapped payload class #{payload.class}")
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
                  @message.type = type_id
         | 
| 125 | 
            +
                  @message.payload = payload.pack
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                def pack
         | 
| 129 | 
            +
                  raise NoPayload if !payload
         | 
| 130 | 
            +
                  raise NoPath if !path
         | 
| 131 | 
            +
                  @message.raw_site = path.raw_site
         | 
| 132 | 
            +
                  @message.raw_target = path.raw_target
         | 
| 133 | 
            +
                  @message.tagged = path.tagged?
         | 
| 134 | 
            +
                  @message.msg_size = @message.num_bytes
         | 
| 135 | 
            +
                  @message.pack
         | 
| 136 | 
            +
                end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                def to_s
         | 
| 139 | 
            +
                  hash = {site: path.site_id}
         | 
| 140 | 
            +
                  if path.tagged?
         | 
| 141 | 
            +
                    hash[:tags] = path.tag_ids
         | 
| 142 | 
            +
                    hash[:tags] = 'all' if hash[:tags].empty?
         | 
| 143 | 
            +
                  else
         | 
| 144 | 
            +
                    hash[:device] = path.device_id
         | 
| 145 | 
            +
                  end
         | 
| 146 | 
            +
                  hash[:type] = payload.class.to_s.sub('LIFX::Protocol::', '')
         | 
| 147 | 
            +
                  hash[:addressable] = addressable? ? 'true' : 'false'
         | 
| 148 | 
            +
                  hash[:tagged] = path.tagged? ? 'true' : 'false'
         | 
| 149 | 
            +
                  hash[:at_time] = @message.at_time if @message.at_time && @message.at_time > 0
         | 
| 150 | 
            +
                  hash[:protocol] = protocol
         | 
| 151 | 
            +
                  hash[:payload] = payload.snapshot if payload
         | 
| 152 | 
            +
                  attrs = hash.map { |k, v| "#{k}=#{v}" }.join(' ')
         | 
| 153 | 
            +
                  %Q{#<LIFX::Message #{attrs}>}
         | 
| 154 | 
            +
                end
         | 
| 155 | 
            +
                alias_method :inspect, :to_s
         | 
| 156 | 
            +
                
         | 
| 157 | 
            +
                protected
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                def check_valid_fields!(hash)
         | 
| 160 | 
            +
                  invalid_fields = hash.keys - self.class.valid_fields
         | 
| 161 | 
            +
                  if invalid_fields.count > 0
         | 
| 162 | 
            +
                    raise InvalidFields.new("Invalid fields for Message: #{invalid_fields.join(', ')}")
         | 
| 163 | 
            +
                  end
         | 
| 164 | 
            +
                end
         | 
| 165 | 
            +
              end
         | 
| 166 | 
            +
            end
         | 
| @@ -0,0 +1,200 @@ | |
| 1 | 
            +
            require 'lifx/timers'
         | 
| 2 | 
            +
            require 'lifx/transport_manager'
         | 
| 3 | 
            +
            require 'lifx/routing_manager'
         | 
| 4 | 
            +
            require 'lifx/tag_manager'
         | 
| 5 | 
            +
            require 'lifx/light'
         | 
| 6 | 
            +
            require 'lifx/protocol_path'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module LIFX
         | 
| 9 | 
            +
              class NetworkContext
         | 
| 10 | 
            +
                include Timers
         | 
| 11 | 
            +
                include Logging
         | 
| 12 | 
            +
                include Utilities
         | 
| 13 | 
            +
                extend Forwardable
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                # NetworkContext stores lights and ties together TransportManager, TagManager and RoutingManager
         | 
| 16 | 
            +
                attr_reader :transport_manager, :tag_manager, :routing_manager
         | 
| 17 | 
            +
                
         | 
| 18 | 
            +
                def initialize(transport: :lan)
         | 
| 19 | 
            +
                  @devices = {}
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  @transport_manager = case transport
         | 
| 22 | 
            +
                  when :lan
         | 
| 23 | 
            +
                    TransportManager::LAN.new
         | 
| 24 | 
            +
                  else
         | 
| 25 | 
            +
                    raise ArgumentError.new("Unknown transport method: #{transport}")
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                  @transport_manager.add_observer(self) do |message:, ip:, transport:|
         | 
| 28 | 
            +
                    handle_message(message, ip, transport)
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  reset!
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  @threads = []
         | 
| 34 | 
            +
                  @threads << initialize_timer_thread
         | 
| 35 | 
            +
                  initialize_periodic_refresh
         | 
| 36 | 
            +
                  initialize_message_rate_updater
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def discover
         | 
| 40 | 
            +
                  @transport_manager.discover
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def refresh
         | 
| 44 | 
            +
                  @routing_manager.refresh
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def reset!
         | 
| 48 | 
            +
                  @routing_manager = RoutingManager.new(context: self)
         | 
| 49 | 
            +
                  @tag_manager = TagManager.new(context: self, tag_table: @routing_manager.tag_table)
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def stop
         | 
| 53 | 
            +
                  @transport_manager.stop
         | 
| 54 | 
            +
                  @threads.each do |thread|
         | 
| 55 | 
            +
                    Thread.kill(thread)
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                # Sends a message to their destination(s)
         | 
| 60 | 
            +
                # @param target: [Target] Target of the message
         | 
| 61 | 
            +
                # @param payload: [Protocol::Payload] Message payload
         | 
| 62 | 
            +
                # @param acknowledge: [Boolean] If recipients must acknowledge with a response
         | 
| 63 | 
            +
                def send_message(target:, payload:, acknowledge: false)
         | 
| 64 | 
            +
                  paths = @routing_manager.resolve_target(target)
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  messages = paths.map do |path|
         | 
| 67 | 
            +
                    Message.new(path: path, payload: payload, acknowledge: acknowledge)
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  if within_sync?
         | 
| 71 | 
            +
                    Thread.current[:sync_messages].push(*messages)
         | 
| 72 | 
            +
                    return
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  messages.each do |message|
         | 
| 76 | 
            +
                    @transport_manager.write(message)
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                protected def within_sync?
         | 
| 81 | 
            +
                  !!Thread.current[:sync_enabled]
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                # Synchronize asynchronous set_color, set_waveform and set_power messages to multiple devices.
         | 
| 85 | 
            +
                # You cannot use synchronous methods in the block
         | 
| 86 | 
            +
                # @note This is alpha
         | 
| 87 | 
            +
                # @yield Block to synchronize commands in
         | 
| 88 | 
            +
                # @return [Float] Delay before messages are executed
         | 
| 89 | 
            +
                def sync(&block)
         | 
| 90 | 
            +
                  if within_sync?
         | 
| 91 | 
            +
                    raise "You cannot nest sync"
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
                  messages = Thread.new do
         | 
| 94 | 
            +
                    Thread.current[:sync_enabled] = true
         | 
| 95 | 
            +
                    Thread.current[:sync_messages] = messages = []
         | 
| 96 | 
            +
                    block.call
         | 
| 97 | 
            +
                    Thread.current[:sync_enabled] = false
         | 
| 98 | 
            +
                    messages
         | 
| 99 | 
            +
                  end.join.value
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  time = nil
         | 
| 102 | 
            +
                  try_until -> { time } do
         | 
| 103 | 
            +
                    light = gateways.sample
         | 
| 104 | 
            +
                    time = light && light.time
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                  delay = (messages.count + 1) * (1.0 / message_rate) 
         | 
| 108 | 
            +
                  at_time = ((time.to_f + delay) * 1_000_000_000).to_i
         | 
| 109 | 
            +
                  messages.each do |m|
         | 
| 110 | 
            +
                    m.at_time = at_time
         | 
| 111 | 
            +
                    @transport_manager.write(m)
         | 
| 112 | 
            +
                  end
         | 
| 113 | 
            +
                  flush
         | 
| 114 | 
            +
                  delay
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def flush(**options)
         | 
| 118 | 
            +
                  @transport_manager.flush(**options)
         | 
| 119 | 
            +
                end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                def register_device(device)
         | 
| 122 | 
            +
                  device_id = device.id
         | 
| 123 | 
            +
                  @devices[device_id] = device # What happens when there's already one registered?
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                def lights
         | 
| 127 | 
            +
                  LightCollection.new(context: self)
         | 
| 128 | 
            +
                end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                def all_lights
         | 
| 131 | 
            +
                  @devices.values
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                # Tags
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                def_delegators :@tag_manager, :tags,
         | 
| 137 | 
            +
                                              :unused_tags,
         | 
| 138 | 
            +
                                              :purge_unused_tags!,
         | 
| 139 | 
            +
                                              :add_tag_to_device,
         | 
| 140 | 
            +
                                              :remove_tag_from_device
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                def tags_for_device(device)
         | 
| 143 | 
            +
                  @routing_manager.tags_for_device_id(device.id)
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                def gateways
         | 
| 147 | 
            +
                  transport_manager.gateways.map(&:keys).flatten.map { |id| lights.with_id(id) }
         | 
| 148 | 
            +
                end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                def gateway_connections
         | 
| 151 | 
            +
                  transport_manager.gateways.map(&:values).flatten
         | 
| 152 | 
            +
                end
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                protected
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                def handle_message(message, ip, transport)
         | 
| 157 | 
            +
                  logger.debug("<- #{self} #{transport}: #{message}")
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                  @routing_manager.update_from_message(message)
         | 
| 160 | 
            +
                  if !message.tagged?
         | 
| 161 | 
            +
                    if @devices[message.device_id].nil?
         | 
| 162 | 
            +
                      device = Light.new(context: self, id: message.device_id, site_id: message.site_id)
         | 
| 163 | 
            +
                      register_device(device)
         | 
| 164 | 
            +
                    end
         | 
| 165 | 
            +
                    device = @devices[message.device_id]
         | 
| 166 | 
            +
                    device.handle_message(message, ip, transport)
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
                end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                def initialize_periodic_refresh
         | 
| 171 | 
            +
                  timers.every(10) do
         | 
| 172 | 
            +
                    refresh
         | 
| 173 | 
            +
                  end
         | 
| 174 | 
            +
                end
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                def initialize_message_rate_updater
         | 
| 177 | 
            +
                  timers.every(5) do
         | 
| 178 | 
            +
                    missing_mesh_firmware = lights.select { |l| l.mesh_firmware(fetch: false).nil? }
         | 
| 179 | 
            +
                    if missing_mesh_firmware.count > 10
         | 
| 180 | 
            +
                      send_message(target: Target.new(broadcast: true), payload: Protocol::Device::GetMeshFirmware.new)
         | 
| 181 | 
            +
                    elsif missing_mesh_firmware.count > 0
         | 
| 182 | 
            +
                      missing_mesh_firmware.each { |l| l.send_message(Protocol::Device::GetMeshFirmware.new) }
         | 
| 183 | 
            +
                    else
         | 
| 184 | 
            +
                      @message_rate = lights.all? do |light|
         | 
| 185 | 
            +
                        m = light.mesh_firmware(fetch: false)
         | 
| 186 | 
            +
                        m && m >= '1.2'
         | 
| 187 | 
            +
                      end ? 20 : 5
         | 
| 188 | 
            +
                      gateway_connections.each do |connection|
         | 
| 189 | 
            +
                        connection.set_message_rate(@message_rate)
         | 
| 190 | 
            +
                      end
         | 
| 191 | 
            +
                    end
         | 
| 192 | 
            +
                  end
         | 
| 193 | 
            +
                end
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                DEFAULT_MESSAGING_RATE = 5 # per second
         | 
| 196 | 
            +
                def message_rate
         | 
| 197 | 
            +
                  @message_rate || 5
         | 
| 198 | 
            +
                end
         | 
| 199 | 
            +
              end
         | 
| 200 | 
            +
            end
         | 
| @@ -0,0 +1,46 @@ | |
| 1 | 
            +
            module LIFX
         | 
| 2 | 
            +
              module Observable
         | 
| 3 | 
            +
                class ObserverCallbackMismatch < ArgumentError; end
         | 
| 4 | 
            +
                def add_observer(obj, &callback)
         | 
| 5 | 
            +
                  if !callback_has_required_keys?(callback)
         | 
| 6 | 
            +
                    raise ObserverCallbackMismatch.new
         | 
| 7 | 
            +
                  end
         | 
| 8 | 
            +
                  observers[obj] = callback
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def remove_observer(obj)
         | 
| 12 | 
            +
                  observers.delete(obj)
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def notify_observers(**args)
         | 
| 16 | 
            +
                  observers.each do |_, callback|
         | 
| 17 | 
            +
                    callback.call(**args)
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def callback_has_required_keys?(callback)
         | 
| 22 | 
            +
                  (required_keys_for_callback - required_keys_in_proc(callback)).empty?
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def observer_callback_definition
         | 
| 26 | 
            +
                  nil
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def required_keys_for_callback
         | 
| 30 | 
            +
                  @_required_keys_for_callback ||= begin
         | 
| 31 | 
            +
                    return [] if !observer_callback_definition
         | 
| 32 | 
            +
                    required_keys_in_proc(observer_callback_definition)
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def required_keys_in_proc(proc)
         | 
| 37 | 
            +
                  proc.parameters.select do |type, _|
         | 
| 38 | 
            +
                    type == :keyreq
         | 
| 39 | 
            +
                  end.map(&:last)
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def observers
         | 
| 43 | 
            +
                  @_observers ||= {}
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
            end
         | 
| @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            module LIFX
         | 
| 2 | 
            +
              module Protocol
         | 
| 3 | 
            +
                module AddressFields
         | 
| 4 | 
            +
                  def AddressFields.included(mod)
         | 
| 5 | 
            +
                    mod.instance_eval do
         | 
| 6 | 
            +
                      hide :_reserved2
         | 
| 7 | 
            +
                      string :raw_target, length: 8
         | 
| 8 | 
            +
                      string :raw_site, length: 6
         | 
| 9 | 
            +
                      bool_bit1 :acknowledge
         | 
| 10 | 
            +
                      bit15le :_reserved2
         | 
| 11 | 
            +
                    end
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                class Address < BinData::Record
         | 
| 16 | 
            +
                  endian :little
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  include AddressFields
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
            end
         | 
| @@ -0,0 +1,225 @@ | |
| 1 | 
            +
            # Generated code ahoy!
         | 
| 2 | 
            +
            module LIFX
         | 
| 3 | 
            +
              module Protocol
         | 
| 4 | 
            +
                module Device
         | 
| 5 | 
            +
                  module Service
         | 
| 6 | 
            +
                    UDP = 1
         | 
| 7 | 
            +
                    TCP = 2
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  class SetSite < Payload
         | 
| 11 | 
            +
                    endian :little
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    string :site, length: 6
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  class GetPanGateway < Payload
         | 
| 17 | 
            +
                    endian :little
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  class StatePanGateway < Payload
         | 
| 22 | 
            +
                    endian :little
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    uint8 :service
         | 
| 25 | 
            +
                    uint32 :port
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  class GetTime < Payload
         | 
| 29 | 
            +
                    endian :little
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  class SetTime < Payload
         | 
| 34 | 
            +
                    endian :little
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    uint64 :time # Nanoseconds since epoch.
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  class StateTime < Payload
         | 
| 40 | 
            +
                    endian :little
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    uint64 :time # Nanoseconds since epoch.
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  class GetResetSwitch < Payload
         | 
| 46 | 
            +
                    endian :little
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  class StateResetSwitch < Payload
         | 
| 51 | 
            +
                    endian :little
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    uint8 :position
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  class GetMeshInfo < Payload
         | 
| 57 | 
            +
                    endian :little
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  class StateMeshInfo < Payload
         | 
| 62 | 
            +
                    endian :little
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    float :signal # Milliwatts.
         | 
| 65 | 
            +
                    uint32 :tx # Bytes.
         | 
| 66 | 
            +
                    uint32 :rx # Bytes.
         | 
| 67 | 
            +
                    int16 :mcu_temperature # Deci-celsius. 25.45 celsius is 2545
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  class GetMeshFirmware < Payload
         | 
| 71 | 
            +
                    endian :little
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  class StateMeshFirmware < Payload
         | 
| 76 | 
            +
                    endian :little
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    uint64 :build # Firmware build nanoseconds since epoch.
         | 
| 79 | 
            +
                    uint64 :install # Firmware install nanoseconds since epoch.
         | 
| 80 | 
            +
                    uint32 :version # Firmware human readable version.
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  class GetWifiInfo < Payload
         | 
| 84 | 
            +
                    endian :little
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  class StateWifiInfo < Payload
         | 
| 89 | 
            +
                    endian :little
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    float :signal # Milliwatts.
         | 
| 92 | 
            +
                    uint32 :tx # Bytes.
         | 
| 93 | 
            +
                    uint32 :rx # Bytes.
         | 
| 94 | 
            +
                    int16 :mcu_temperature # Deci-celsius. 25.45 celsius is 2545
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  class GetWifiFirmware < Payload
         | 
| 98 | 
            +
                    endian :little
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                  class StateWifiFirmware < Payload
         | 
| 103 | 
            +
                    endian :little
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                    uint64 :build # Firmware build nanoseconds since epoch.
         | 
| 106 | 
            +
                    uint64 :install # Firmware install nanoseconds since epoch.
         | 
| 107 | 
            +
                    uint32 :version # Firmware human readable version.
         | 
| 108 | 
            +
                  end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                  class GetPower < Payload
         | 
| 111 | 
            +
                    endian :little
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  class SetPower < Payload
         | 
| 116 | 
            +
                    endian :little
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                    uint16 :level # 0 Standby. > 0 On.
         | 
| 119 | 
            +
                  end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                  class StatePower < Payload
         | 
| 122 | 
            +
                    endian :little
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                    uint16 :level # 0 Standby. > 0 On.
         | 
| 125 | 
            +
                  end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                  class GetLabel < Payload
         | 
| 128 | 
            +
                    endian :little
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                  end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                  class SetLabel < Payload
         | 
| 133 | 
            +
                    endian :little
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                    string :label, length: 32, trim_padding: true
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  class StateLabel < Payload
         | 
| 139 | 
            +
                    endian :little
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                    string :label, length: 32, trim_padding: true
         | 
| 142 | 
            +
                  end
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                  class GetTags < Payload
         | 
| 145 | 
            +
                    endian :little
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                  end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                  class SetTags < Payload
         | 
| 150 | 
            +
                    endian :little
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                    uint64 :tags
         | 
| 153 | 
            +
                  end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                  class StateTags < Payload
         | 
| 156 | 
            +
                    endian :little
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                    uint64 :tags
         | 
| 159 | 
            +
                  end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                  class GetTagLabels < Payload
         | 
| 162 | 
            +
                    endian :little
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                    uint64 :tags
         | 
| 165 | 
            +
                  end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                  class SetTagLabels < Payload
         | 
| 168 | 
            +
                    endian :little
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                    uint64 :tags
         | 
| 171 | 
            +
                    string :label, length: 32, trim_padding: true
         | 
| 172 | 
            +
                  end
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                  class StateTagLabels < Payload
         | 
| 175 | 
            +
                    endian :little
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                    uint64 :tags
         | 
| 178 | 
            +
                    string :label, length: 32, trim_padding: true
         | 
| 179 | 
            +
                  end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                  class GetVersion < Payload
         | 
| 182 | 
            +
                    endian :little
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                  end
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                  class StateVersion < Payload
         | 
| 187 | 
            +
                    endian :little
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                    uint32 :vendor
         | 
| 190 | 
            +
                    uint32 :product
         | 
| 191 | 
            +
                    uint32 :version
         | 
| 192 | 
            +
                  end
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                  class GetInfo < Payload
         | 
| 195 | 
            +
                    endian :little
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                  end
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                  class StateInfo < Payload
         | 
| 200 | 
            +
                    endian :little
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                    uint64 :time # Nanoseconds since epoch.
         | 
| 203 | 
            +
                    uint64 :uptime # Nanoseconds since boot.
         | 
| 204 | 
            +
                    uint64 :downtime # Nanoseconds off last power cycle.
         | 
| 205 | 
            +
                  end
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                  class GetMcuRailVoltage < Payload
         | 
| 208 | 
            +
                    endian :little
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                  end
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                  class StateMcuRailVoltage < Payload
         | 
| 213 | 
            +
                    endian :little
         | 
| 214 | 
            +
             | 
| 215 | 
            +
                    uint32 :voltage
         | 
| 216 | 
            +
                  end
         | 
| 217 | 
            +
             | 
| 218 | 
            +
                  class Reboot < Payload
         | 
| 219 | 
            +
                    endian :little
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                  end
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                end
         | 
| 224 | 
            +
              end
         | 
| 225 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            module LIFX
         | 
| 2 | 
            +
              module Protocol
         | 
| 3 | 
            +
                module HeaderFields
         | 
| 4 | 
            +
                  def HeaderFields.included(mod)
         | 
| 5 | 
            +
                    mod.instance_eval do
         | 
| 6 | 
            +
                      hide :_reserved, :_reserved1
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                      uint16 :msg_size
         | 
| 9 | 
            +
                      bit12le :protocol
         | 
| 10 | 
            +
                      bool_bit1 :addressable, value: true
         | 
| 11 | 
            +
                      bool_bit1 :tagged
         | 
| 12 | 
            +
                      bit2le :_reserved
         | 
| 13 | 
            +
                      uint32 :_reserved1
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
                
         | 
| 18 | 
            +
                class Header < BinData::Record
         | 
| 19 | 
            +
                  endian :little
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  include HeaderFields
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         |