tools

various tools I have been using throughout the years
Log | Files | Refs | README | LICENSE

hue.rb (5400B)


      1 # frozen_string_literal: true
      2 
      3 require 'net/http'
      4 require 'json'
      5 require 'securerandom'
      6 require 'logger'
      7 require 'fileutils'
      8 require 'socket'
      9 require 'timeout'
     10 
     11 module Hue
     12   DEVICE_TYPE = 'hue-cli'
     13   DEFAULT_UDP_TIMEOUT = 5
     14   ERROR_DEFAULT_EXISTS = 'Default application already registered.'
     15   ERROR_NO_BRIDGE_FOUND = 'No bridge found.'
     16 
     17   def self.device_type
     18     DEVICE_TYPE
     19   end
     20 
     21   def self.one_time_uuid
     22     SecureRandom.hex(16)
     23   end
     24 
     25   def self.register_default
     26     raise Hue::Error, ERROR_DEFAULT_EXISTS if begin
     27       Hue::Config::Application.default
     28     rescue StandardError
     29       nil
     30     end
     31 
     32     bridge_config = register_bridges.values.first # Assuming one bridge for now
     33     puts 'Registering new app...'
     34     instance = Hue::Bridge.register(bridge_config.uri)
     35     app_config = Hue::Config::Application.new(bridge_config.id, instance.application_id)
     36     app_config.write
     37     instance
     38   end
     39 
     40   def self.application
     41     application_config = Hue::Config::Application.default
     42     bridge_id = application_config.bridge_id
     43     bridge_config = Hue::Config::Bridge.find(bridge_id) || register_bridges[bridge_id]
     44 
     45     raise Error, "Unable to find bridge: #{bridge_id}" unless bridge_config
     46 
     47     Hue::Bridge.new(application_config.id, bridge_config.uri)
     48   end
     49 
     50   def self.remove_default
     51     instance = application
     52     instance.unregister
     53     Hue::Config::Application.default.delete
     54     true
     55   end
     56 
     57   def self.discover
     58     bridges = {}
     59     udp_discover(bridges)
     60     nupnp_discover(bridges)
     61     bridges
     62   end
     63 
     64   UDP_SSDP_PAYLOAD = <<~PAYLOAD
     65     M-SEARCH * HTTP/1.1
     66     ST: ssdp:all
     67     MX: 10
     68     MAN: ssdp:discover
     69     HOST: 239.255.255.250:1900
     70   PAYLOAD
     71   UDP_SSDP_ADDRESS = '239.255.255.250'
     72   UDP_SSDP_PORT = 1900
     73 
     74   def self.udp_discover(bridges)
     75     log = Hue.logger
     76     log.info('Bridge UDP Discovery')
     77     socket = UDPSocket.new(Socket::AF_INET)
     78     socket.send(UDP_SSDP_PAYLOAD, 0, UDP_SSDP_ADDRESS, UDP_SSDP_PORT)
     79     Timeout.timeout(DEFAULT_UDP_TIMEOUT, Hue::Error) { listen_for_bridges(socket, bridges, log) }
     80   rescue Hue::Error => e
     81     log.warn(e)
     82     log.info('UDPSocket timed out.')
     83   end
     84 
     85   def self.listen_for_bridges(socket, bridges, log)
     86     loop do
     87       message, (_, port, hostname, ip_add) = socket.recvfrom(1024)
     88       if message =~ /IpBridge/ && /LOCATION: (.*)$/.match(message)
     89         uuid_match = /uuid:(.{36})/.match(message)
     90         next unless uuid_match
     91 
     92         uuid = uuid_match.captures.first
     93         log.info("Found bridge (#{hostname}:#{port}) with uuid: #{uuid}") unless bridges[uuid]
     94         bridges[uuid] = "http://#{ip_add}/api"
     95       else
     96         log.debug("Found #{hostname}:#{port}: #{message}")
     97       end
     98     end
     99   end
    100 
    101   def self.nupnp_discover(bridges)
    102     return unless bridges.empty?
    103 
    104     Hue.logger.info('Bridge NUPNP Discovery')
    105     response = Net::HTTP.get_response(URI('https://www.meethue.com/api/nupnp'))
    106     json = begin
    107       JSON.parse(response.body)
    108     rescue StandardError
    109       nil
    110     end
    111     return unless json
    112 
    113     json.each do |bridge|
    114       uuid = bridge['id']
    115       ip_add = bridge['internalipaddress']
    116       bridges[uuid] = "http://#{ip_add}/api" if uuid && ip_add
    117     end
    118   end
    119 
    120   def self.register_bridges
    121     bridges = discover
    122     raise Error, ERROR_NO_BRIDGE_FOUND if bridges.empty?
    123 
    124     bridges.each_with_object({}) do |(id, ip), hash|
    125       config = Hue::Config::Bridge.new(id, ip)
    126       config.write
    127       hash[id] = config
    128     end
    129   end
    130 
    131   class Error < StandardError
    132     attr_accessor :original_error
    133 
    134     def initialize(message, original_error = nil)
    135       super(message)
    136       @original_error = original_error
    137     end
    138 
    139     def to_s
    140       @original_error ? "#{super}\nCause: #{@original_error}" : super
    141     end
    142   end
    143 
    144   module API
    145     class Error < ::Hue::Error
    146       def initialize(api_error)
    147         @type = api_error['type']
    148         @address = api_error['address']
    149         super(api_error['description'])
    150       end
    151     end
    152   end
    153 
    154   def self.logger
    155     @logger ||= begin
    156       log_dir_path = ensure_log_dir
    157       log_file_path = File.join(log_dir_path, 'hue.log')
    158       log_file = File.new(log_file_path, File::WRONLY | File::APPEND | File::CREAT)
    159       logger = Logger.new(log_file)
    160       logger.level = Logger::INFO
    161       logger
    162     end
    163   end
    164 
    165   def self.ensure_log_dir
    166     home = Dir.home
    167     [File.join(home, '.hue'), File.join(home, ".#{device_type}")].each do |path|
    168       FileUtils.mkdir_p(path)
    169       return path
    170     rescue Errno::EACCES
    171       next
    172     end
    173     raise Error, 'Unable to create log directory'
    174   end
    175 
    176   def self.percent_to_unit_interval(value)
    177     if (percent = /(\d+)%/.match(value.to_s))
    178       percent.captures.first.to_i / 100.0
    179     end
    180   end
    181 end
    182 
    183 # Core library components
    184 require_relative 'hue/config/abstract'
    185 require_relative 'hue/config/application'
    186 require_relative 'hue/config/bridge'
    187 require_relative 'hue/bridge'
    188 require_relative 'hue/colors'
    189 require_relative 'hue/bulb'
    190 
    191 # CLI components (utilities must come before extensions)
    192 require_relative 'hue/utilities/indentation'
    193 require_relative 'hue/cli'
    194 require_relative 'hue/cli/command'
    195 require_relative 'hue/cli/processors/light_state_alias'
    196 require_relative 'hue/cli/commands/register'
    197 require_relative 'hue/cli/commands/delete'
    198 require_relative 'hue/cli/commands/light'
    199 require_relative 'hue/cli/commands/lights'
    200 require_relative 'hue/extensions/hue'
    201 require_relative 'hue/extensions/bridge'
    202 require_relative 'hue/extensions/bulb'