yeelight-client 1.0.2
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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +43 -0
- data/README.md +80 -0
- data/Rakefile +6 -0
- data/bin/bump +29 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/yeelight-client.rb +1 -0
- data/lib/yeelight_client/broadcast/broadcaster.rb +89 -0
- data/lib/yeelight_client/broadcast/response.rb +18 -0
- data/lib/yeelight_client/broadcast.rb +20 -0
- data/lib/yeelight_client/connection.rb +68 -0
- data/lib/yeelight_client/handler.rb +17 -0
- data/lib/yeelight_client/requests.rb +33 -0
- data/lib/yeelight_client/response/exception.rb +40 -0
- data/lib/yeelight_client/response/result.rb +17 -0
- data/lib/yeelight_client/response.rb +38 -0
- data/lib/yeelight_client/version.rb +3 -0
- data/lib/yeelight_client.rb +42 -0
- data/yeelight-client.gemspec +43 -0
- metadata +136 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: eb98a134503f504391f1761d58cb09eb9679daf9ac1f7a9b70289ee00f05ab76
         | 
| 4 | 
            +
              data.tar.gz: 0a0fee6c5ea0404fef06d81e1df180e42debdc1c5eab37b1abb30588655ae27f
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: aa273c4520f489e82948d812e1cd3de64f9240463a818baa079ec1e982ff2f041fda57261911a7b234bba33c7402464f653948848aa0de4fb8495f65fa37213b
         | 
| 7 | 
            +
              data.tar.gz: 3b631a17ca51965a6efc6386763e46c16d5d8e0cb36e5bc73fe3bc6e8a3efe1f6c56f1b5e68c6313ef32553789f1cdbba951f131050134ad966556cf9e779805
         | 
    
        data/.gitignore
    ADDED
    
    
    
        data/.rspec
    ADDED
    
    
    
        data/.travis.yml
    ADDED
    
    
    
        data/Gemfile
    ADDED
    
    
    
        data/Gemfile.lock
    ADDED
    
    | @@ -0,0 +1,43 @@ | |
| 1 | 
            +
            PATH
         | 
| 2 | 
            +
              remote: .
         | 
| 3 | 
            +
              specs:
         | 
| 4 | 
            +
                yeelight-client (1.0.2)
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            GEM
         | 
| 7 | 
            +
              remote: https://rubygems.org/
         | 
| 8 | 
            +
              specs:
         | 
| 9 | 
            +
                bump (0.8.0)
         | 
| 10 | 
            +
                coderay (1.1.2)
         | 
| 11 | 
            +
                diff-lcs (1.3)
         | 
| 12 | 
            +
                method_source (0.9.2)
         | 
| 13 | 
            +
                pry (0.12.2)
         | 
| 14 | 
            +
                  coderay (~> 1.1.0)
         | 
| 15 | 
            +
                  method_source (~> 0.9.0)
         | 
| 16 | 
            +
                rake (10.5.0)
         | 
| 17 | 
            +
                rspec (3.9.0)
         | 
| 18 | 
            +
                  rspec-core (~> 3.9.0)
         | 
| 19 | 
            +
                  rspec-expectations (~> 3.9.0)
         | 
| 20 | 
            +
                  rspec-mocks (~> 3.9.0)
         | 
| 21 | 
            +
                rspec-core (3.9.0)
         | 
| 22 | 
            +
                  rspec-support (~> 3.9.0)
         | 
| 23 | 
            +
                rspec-expectations (3.9.0)
         | 
| 24 | 
            +
                  diff-lcs (>= 1.2.0, < 2.0)
         | 
| 25 | 
            +
                  rspec-support (~> 3.9.0)
         | 
| 26 | 
            +
                rspec-mocks (3.9.0)
         | 
| 27 | 
            +
                  diff-lcs (>= 1.2.0, < 2.0)
         | 
| 28 | 
            +
                  rspec-support (~> 3.9.0)
         | 
| 29 | 
            +
                rspec-support (3.9.0)
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            PLATFORMS
         | 
| 32 | 
            +
              ruby
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            DEPENDENCIES
         | 
| 35 | 
            +
              bump (~> 0.8.0)
         | 
| 36 | 
            +
              bundler (~> 1.17)
         | 
| 37 | 
            +
              pry (~> 0.12.2)
         | 
| 38 | 
            +
              rake (~> 10.0)
         | 
| 39 | 
            +
              rspec (~> 3.0)
         | 
| 40 | 
            +
              yeelight-client!
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            BUNDLED WITH
         | 
| 43 | 
            +
               1.17.3
         | 
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,80 @@ | |
| 1 | 
            +
            # YeelightClient
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Official implementation of Yeelight Operation Spec
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            https://www.yeelight.com/en_US/developer
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            ## Installation
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            Add this line to your application's Gemfile:
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            ```ruby
         | 
| 12 | 
            +
            gem 'yeelight-client'
         | 
| 13 | 
            +
            ```
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            And then execute:
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                $ bundle
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            Or install it yourself as:
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                $ gem install yeelight-client
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            ## Usage
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            This gem split into two parts. The first is to how to discover all Yeelight lights in your local network.
         | 
| 26 | 
            +
            ```ruby
         | 
| 27 | 
            +
            results = YeelightClient::Broadcast.new(logger: Logger.new(STDOUT)).discover
         | 
| 28 | 
            +
            results.first.addr
         | 
| 29 | 
            +
            ```
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            You will see something like this if your have many lights:
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            ```
         | 
| 34 | 
            +
            I, [2019-11-02T23:42:56.142927 #3071]  INFO -- : Receive #<UDPSocket:fd 9, AF_INET, 0.0.0.0, 56343>
         | 
| 35 | 
            +
            I, [2019-11-02T23:42:56.143275 #3071]  INFO -- : Receive #<UDPSocket:fd 9, AF_INET, 0.0.0.0, 56343>
         | 
| 36 | 
            +
            I, [2019-11-02T23:42:56.143811 #3071]  INFO -- : Receive #<UDPSocket:fd 9, AF_INET, 0.0.0.0, 56343>
         | 
| 37 | 
            +
            I, [2019-11-02T23:42:56.144785 #3071]  INFO -- : Receive #<UDPSocket:fd 9, AF_INET, 0.0.0.0, 56343>
         | 
| 38 | 
            +
            I, [2019-11-02T23:42:56.144911 #3071]  INFO -- : Receive #<UDPSocket:fd 9, AF_INET, 0.0.0.0, 56343>
         | 
| 39 | 
            +
            I, [2019-11-02T23:42:56.145422 #3071]  INFO -- : Receive #<UDPSocket:fd 9, AF_INET, 0.0.0.0, 56343>
         | 
| 40 | 
            +
            ```
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            Get an addr from results:
         | 
| 43 | 
            +
            ```
         | 
| 44 | 
            +
            irb(main):002:0> results.first.addr
         | 
| 45 | 
            +
            => #<Addrinfo: 192.168.31.64:55443 TCP>
         | 
| 46 | 
            +
            irb(main):003:0> addr.ip_address
         | 
| 47 | 
            +
            => "192.168.31.16"
         | 
| 48 | 
            +
            irb(main):004:0> addr.ip_port
         | 
| 49 | 
            +
            => 55443
         | 
| 50 | 
            +
            ```
         | 
| 51 | 
            +
             | 
| 52 | 
            +
             | 
| 53 | 
            +
            For now you have every Yeelight host and port. We have to go next part of this gem Yeelight Client.
         | 
| 54 | 
            +
             | 
| 55 | 
            +
            Initialize client with received addr or with host and port
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            ```ruby
         | 
| 58 | 
            +
            client = YeelightClient.new(addrs: addr, logger: Logger.new(STDOUT))
         | 
| 59 | 
            +
            ```
         | 
| 60 | 
            +
             | 
| 61 | 
            +
            ```ruby
         | 
| 62 | 
            +
            client = YeelightClient.new(capabilities: { host: "192.168.31.16", port: 55443 }, logger: Logger.new(STDOUT))
         | 
| 63 | 
            +
            ```
         | 
| 64 | 
            +
             | 
| 65 | 
            +
            Client supports only two methods: set_bright and set_rgb
         | 
| 66 | 
            +
             | 
| 67 | 
            +
            ```ruby
         | 
| 68 | 
            +
            client.set_bright(100)
         | 
| 69 | 
            +
            client.set_rgb(0xff0000)
         | 
| 70 | 
            +
            ```
         | 
| 71 | 
            +
             | 
| 72 | 
            +
            ## Development
         | 
| 73 | 
            +
             | 
| 74 | 
            +
            After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
         | 
| 75 | 
            +
             | 
| 76 | 
            +
            To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
         | 
| 77 | 
            +
             | 
| 78 | 
            +
            ## Contributing
         | 
| 79 | 
            +
             | 
| 80 | 
            +
            Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/yeelight-client.
         | 
    
        data/Rakefile
    ADDED
    
    
    
        data/bin/bump
    ADDED
    
    | @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
            # frozen_string_literal: true
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            #
         | 
| 5 | 
            +
            # This file was generated by Bundler.
         | 
| 6 | 
            +
            #
         | 
| 7 | 
            +
            # The application 'bump' is installed as part of a gem, and
         | 
| 8 | 
            +
            # this file is here to facilitate running it.
         | 
| 9 | 
            +
            #
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            require "pathname"
         | 
| 12 | 
            +
            ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
         | 
| 13 | 
            +
              Pathname.new(__FILE__).realpath)
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            bundle_binstub = File.expand_path("../bundle", __FILE__)
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            if File.file?(bundle_binstub)
         | 
| 18 | 
            +
              if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
         | 
| 19 | 
            +
                load(bundle_binstub)
         | 
| 20 | 
            +
              else
         | 
| 21 | 
            +
                abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
         | 
| 22 | 
            +
            Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            require "rubygems"
         | 
| 27 | 
            +
            require "bundler/setup"
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            load Gem.bin_path("bump", "bump")
         | 
    
        data/bin/console
    ADDED
    
    | @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "bundler/setup"
         | 
| 4 | 
            +
            require "logger"
         | 
| 5 | 
            +
            require "yeelight_client"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            # You can add fixtures and/or initialization code here to make experimenting
         | 
| 8 | 
            +
            # with your gem easier. You can also use a different console, if you like.
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            # (If you use this, don't forget to add pry to your Gemfile!)
         | 
| 11 | 
            +
            # require "pry"
         | 
| 12 | 
            +
            # Pry.start
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            require "irb"
         | 
| 15 | 
            +
            IRB.start(__FILE__)
         | 
    
        data/bin/setup
    ADDED
    
    
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            require "yeelight_client"
         | 
| @@ -0,0 +1,89 @@ | |
| 1 | 
            +
            class YeelightClient
         | 
| 2 | 
            +
              class Broadcast
         | 
| 3 | 
            +
                class Broadcaster
         | 
| 4 | 
            +
                  DEFAULT_TIMEOUT = 3
         | 
| 5 | 
            +
                  DEFAULT_MAX_PACK = 65_535
         | 
| 6 | 
            +
                  HEADER_MATCH = /^([^:]+):\s*(.+)$/
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  SSDP_HOST = "239.255.255.250".freeze
         | 
| 9 | 
            +
                  SSDP_PORT = 1982
         | 
| 10 | 
            +
                  SSDP_MX = 100
         | 
| 11 | 
            +
                  SSDP_ST = "wifi_bulb".freeze
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def initialize(logger: nil)
         | 
| 14 | 
            +
                    @socket = create_socket
         | 
| 15 | 
            +
                    @logger = logger
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def broadcast
         | 
| 19 | 
            +
                    query = build_query
         | 
| 20 | 
            +
                    @socket.send(query, 0, SSDP_HOST, SSDP_PORT)
         | 
| 21 | 
            +
                    packages = receive
         | 
| 22 | 
            +
                    packages.map { |message, producer| process_packet(message, producer) }
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  private
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def receive
         | 
| 28 | 
            +
                    remaining = DEFAULT_TIMEOUT
         | 
| 29 | 
            +
                    responses = []
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    while remaining > 0
         | 
| 32 | 
            +
                      start_time = Time.now
         | 
| 33 | 
            +
                      ready = IO.select([@socket], nil, nil, remaining)
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                      @logger&.info { "Receive #{ready.dig(0, 0).inspect}" } if ready
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                      if ready
         | 
| 38 | 
            +
                        message, producer = @socket.recvfrom DEFAULT_MAX_PACK
         | 
| 39 | 
            +
                        responses << [message, producer]
         | 
| 40 | 
            +
                      end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                      remaining -= (Time.now - start_time).to_i
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    responses
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  def process_packet(message, producer)
         | 
| 49 | 
            +
                    message = parse_message(message)
         | 
| 50 | 
            +
                    { address: producer[3], port: producer[1], message: message }
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def parse_message(message)
         | 
| 54 | 
            +
                    message.gsub! "\r\n", "\n"
         | 
| 55 | 
            +
                    header, body = message.split "\n\n"
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                    header = header.split "\n"
         | 
| 58 | 
            +
                    status = header.shift
         | 
| 59 | 
            +
                    params = {}
         | 
| 60 | 
            +
                    header.each do |line|
         | 
| 61 | 
            +
                      match = HEADER_MATCH.match line
         | 
| 62 | 
            +
                      next if match.nil?
         | 
| 63 | 
            +
                      value = match[2]
         | 
| 64 | 
            +
                      value = (value[1, value.length - 2] || '') if value.start_with?('"') && value.end_with?('"')
         | 
| 65 | 
            +
                      params[match[1]] = value
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    { status: status, headers: params, body: body }
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  def build_query
         | 
| 72 | 
            +
                    query = "M-SEARCH * HTTP/1.1\r\n" \
         | 
| 73 | 
            +
                          "HOST: #{SSDP_HOST}:#{SSDP_PORT}\r\n" \
         | 
| 74 | 
            +
                          "MAN: \"ssdp:discover\"\r\n" \
         | 
| 75 | 
            +
                          "MX: #{SSDP_MX}\r\n" \
         | 
| 76 | 
            +
                          "ST: #{SSDP_ST}\r\n" \
         | 
| 77 | 
            +
                          "\r\n"
         | 
| 78 | 
            +
                    query
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def create_socket
         | 
| 82 | 
            +
                    broadcaster = UDPSocket.new
         | 
| 83 | 
            +
                    broadcaster.setsockopt Socket::SOL_SOCKET, Socket::SO_BROADCAST, true
         | 
| 84 | 
            +
                    broadcaster.setsockopt Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 1
         | 
| 85 | 
            +
                    broadcaster
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
              end
         | 
| 89 | 
            +
            end
         | 
| @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            class YeelightClient
         | 
| 2 | 
            +
              class Broadcast
         | 
| 3 | 
            +
                class Response
         | 
| 4 | 
            +
                  attr_reader :data
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  def initialize(data)
         | 
| 7 | 
            +
                    @data = data
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def addr
         | 
| 11 | 
            +
                    location = @data.dig(:message, :headers, "Location")
         | 
| 12 | 
            +
                    return unless location
         | 
| 13 | 
            +
                    uri = URI.parse(location)
         | 
| 14 | 
            +
                    Addrinfo.tcp(uri.host, uri.port)
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            require "socket"
         | 
| 2 | 
            +
            require "yeelight_client/broadcast/broadcaster"
         | 
| 3 | 
            +
            require "yeelight_client/broadcast/response"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            class YeelightClient
         | 
| 6 | 
            +
              class Broadcast
         | 
| 7 | 
            +
                def initialize(logger: nil)
         | 
| 8 | 
            +
                  @broadcaster = Broadcaster.new(logger: logger)
         | 
| 9 | 
            +
                  @logger = logger
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def discover
         | 
| 13 | 
            +
                  responses = @broadcaster.broadcast
         | 
| 14 | 
            +
                  responses.map do |response|
         | 
| 15 | 
            +
                    Broadcast::Response.new(response)
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| 20 | 
            +
             | 
| @@ -0,0 +1,68 @@ | |
| 1 | 
            +
            require "socket"
         | 
| 2 | 
            +
            require "json"
         | 
| 3 | 
            +
            require "timeout"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            class YeelightClient
         | 
| 6 | 
            +
              class Connection
         | 
| 7 | 
            +
                class Group
         | 
| 8 | 
            +
                  def initialize(connections:)
         | 
| 9 | 
            +
                    @connections = Array(connections)
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def request(query)
         | 
| 13 | 
            +
                    @connections.map { |connection| connection.request(query) }
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                DEFAULT_TIMEOUT = 60
         | 
| 18 | 
            +
                DEFAULT_REQUEST_ID = 0
         | 
| 19 | 
            +
                DEFAULT_PORT = 5544
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def initialize(host:, port: nil, logger: nil)
         | 
| 22 | 
            +
                  @host = host
         | 
| 23 | 
            +
                  @port = port || DEFAULT_PORT
         | 
| 24 | 
            +
                  @logger = logger
         | 
| 25 | 
            +
                  # TODO: shared counter
         | 
| 26 | 
            +
                  @request_id = rand(10) * 1000 + DEFAULT_REQUEST_ID
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def request(query)
         | 
| 30 | 
            +
                  request_id = build_request_id
         | 
| 31 | 
            +
                  query = { id: request_id }.merge(query)
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  @logger&.info { "Query #{query.to_json} to yeelight #{@host}:#{@port}" }
         | 
| 34 | 
            +
                  text = dump_query(query)
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  socket = TCPSocket.new(@host, @port)
         | 
| 37 | 
            +
                  socket.write(text)
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  result = Timeout.timeout(DEFAULT_TIMEOUT) do
         | 
| 40 | 
            +
                    loop do
         | 
| 41 | 
            +
                      data = socket.readline
         | 
| 42 | 
            +
                      result = parse_data(data)
         | 
| 43 | 
            +
                      break result if result["id"].eql?(@request_id)
         | 
| 44 | 
            +
                    end
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  @logger&.info { "Result #{result.to_json}" }
         | 
| 48 | 
            +
                  result
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                ensure
         | 
| 51 | 
            +
                  socket&.close
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                private
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def dump_query(query)
         | 
| 57 | 
            +
                  "#{query.to_json}\r\n"
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                def parse_data(data)
         | 
| 61 | 
            +
                  JSON.parse(data)
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                def build_request_id
         | 
| 65 | 
            +
                  @request_id += 1
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
              end
         | 
| 68 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            class YeelightClient
         | 
| 2 | 
            +
              module Handler
         | 
| 3 | 
            +
                def wrap(method)
         | 
| 4 | 
            +
                  prev = "_#{method}".to_sym
         | 
| 5 | 
            +
                  alias_method prev, method
         | 
| 6 | 
            +
                  define_method(method) do |*args|
         | 
| 7 | 
            +
                    begin
         | 
| 8 | 
            +
                      responses = send(prev, *args)
         | 
| 9 | 
            +
                      return responses if responses.size > 1
         | 
| 10 | 
            +
                      responses.first
         | 
| 11 | 
            +
                    rescue => exception
         | 
| 12 | 
            +
                      Response::Exception.new(exception)
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,33 @@ | |
| 1 | 
            +
            class YeelightClient
         | 
| 2 | 
            +
              module Requests
         | 
| 3 | 
            +
                extend Handler
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def set_bright(value, params: {})
         | 
| 6 | 
            +
                  params = prep_params(params)
         | 
| 7 | 
            +
                    .slice(:effect, :duration)
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  query = {
         | 
| 10 | 
            +
                    method: "set_bright",
         | 
| 11 | 
            +
                    params: [value, *params.values]
         | 
| 12 | 
            +
                  }
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  responses = @connection.request(query)
         | 
| 15 | 
            +
                  responses.map { |response| Response::Result.new(response) }
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def set_rgb(value, params: {})
         | 
| 19 | 
            +
                  params = prep_params(params)
         | 
| 20 | 
            +
                    .slice(:effect, :duration)
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  query = {
         | 
| 23 | 
            +
                    method: "set_rgb",
         | 
| 24 | 
            +
                    params: [value, *params.values]
         | 
| 25 | 
            +
                  }
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  responses = @connection.request(query)
         | 
| 28 | 
            +
                  responses.map { |response| Response::Result.new(response) }
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                instance_methods.each { |method| wrap(method) }
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
            end
         | 
| @@ -0,0 +1,40 @@ | |
| 1 | 
            +
            class YeelightClient
         | 
| 2 | 
            +
              class Response
         | 
| 3 | 
            +
                class Exception < YeelightClient::Response
         | 
| 4 | 
            +
                  attr_reader :exception
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  def initialize(exception)
         | 
| 7 | 
            +
                    @exception = exception
         | 
| 8 | 
            +
                    detect_errors
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def success?
         | 
| 12 | 
            +
                    false
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def data
         | 
| 16 | 
            +
                    {}
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def details
         | 
| 20 | 
            +
                    @exception.message
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  private
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def detect_errors
         | 
| 26 | 
            +
                    # EOFError - when lamp is busy
         | 
| 27 | 
            +
                    case @exception
         | 
| 28 | 
            +
                    when EOFError
         | 
| 29 | 
            +
                      assign_errors(:device_busy, :device_error)
         | 
| 30 | 
            +
                    when Timeout::Error, Errno::ETIMEDOUT
         | 
| 31 | 
            +
                      assign_errors(:http_timeout, :network_error)
         | 
| 32 | 
            +
                    when Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EINVAL
         | 
| 33 | 
            +
                      assign_errors(:socket_error, :network_error)
         | 
| 34 | 
            +
                    else
         | 
| 35 | 
            +
                      assign_errors(:unknown, :exception)
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            class YeelightClient
         | 
| 2 | 
            +
              class Response
         | 
| 3 | 
            +
                class Result < YeelightClient::Response
         | 
| 4 | 
            +
                  def data
         | 
| 5 | 
            +
                    return unless success?
         | 
| 6 | 
            +
                    super["result"]
         | 
| 7 | 
            +
                  end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  # {"id"=>4, "error"=>{"code"=>-5000, "message"=>"general error"}}
         | 
| 10 | 
            +
                  # Result {"id":8232,"error":{"code":-1,"message":"client quota exceeded"}}
         | 
| 11 | 
            +
                  def detect_errors
         | 
| 12 | 
            +
                    return unless success?
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            class YeelightClient
         | 
| 2 | 
            +
              class Response
         | 
| 3 | 
            +
                attr_reader :error, :error_type
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(data)
         | 
| 6 | 
            +
                  @data = data
         | 
| 7 | 
            +
                  detect_errors
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def success?
         | 
| 11 | 
            +
                  @data&.has_key?("result")
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def fail?
         | 
| 15 | 
            +
                  !success?
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def data
         | 
| 19 | 
            +
                  @data || {}
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def details
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                private
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def detect_errors
         | 
| 29 | 
            +
                  return if success?
         | 
| 30 | 
            +
                  assign_errors(:unknown, :unknown)
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def assign_errors(error, type)
         | 
| 34 | 
            +
                  @error = error
         | 
| 35 | 
            +
                  @error_type = type
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
| @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            require "yeelight_client/handler"
         | 
| 2 | 
            +
            require "yeelight_client/connection"
         | 
| 3 | 
            +
            require "yeelight_client/requests"
         | 
| 4 | 
            +
            require "yeelight_client/response"
         | 
| 5 | 
            +
            require "yeelight_client/response/result"
         | 
| 6 | 
            +
            require "yeelight_client/response/exception"
         | 
| 7 | 
            +
            require "yeelight_client/broadcast"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            class YeelightClient
         | 
| 10 | 
            +
              include Requests
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              DEFAULT_PARAMS = {
         | 
| 13 | 
            +
                effect: "sudden".freeze,
         | 
| 14 | 
            +
                duration: 0
         | 
| 15 | 
            +
              }
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              def initialize(addrs: nil, capabilities: {}, logger: nil)
         | 
| 18 | 
            +
                @logger = logger
         | 
| 19 | 
            +
                @connection = build_connection(addrs, capabilities)
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              private
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              def build_connection(addrs, capabilities)
         | 
| 25 | 
            +
                connections = if addrs
         | 
| 26 | 
            +
                  addrs = Array(addrs).uniq { |addr| "#{addr.ip_address}:#{addr.ip_port}".hash }
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  addrs.map do |addr|
         | 
| 29 | 
            +
                    Connection.new(host: addr.ip_address, port: addr.ip_port, logger: @logger)
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                else
         | 
| 32 | 
            +
                  raise "Please specify capabilities host and port" unless capabilities[:host] && capabilities[:port]
         | 
| 33 | 
            +
                  Connection.new(host: capabilities[:host], port: capabilities[:port], logger: @logger)
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                Connection::Group.new(connections: connections)
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
              def prep_params(params)
         | 
| 40 | 
            +
                DEFAULT_PARAMS.merge(params)
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
            end
         | 
| @@ -0,0 +1,43 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            lib = File.expand_path("../lib", __FILE__)
         | 
| 3 | 
            +
            $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
         | 
| 4 | 
            +
            require "yeelight_client/version"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            Gem::Specification.new do |spec|
         | 
| 7 | 
            +
              spec.name          = "yeelight-client"
         | 
| 8 | 
            +
              spec.version       = YeelightClient::VERSION
         | 
| 9 | 
            +
              spec.authors       = ["Andrew Ostroumov"]
         | 
| 10 | 
            +
              spec.email         = ["andrew.ostroumov@gmail.com"]
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              spec.summary       = %q{Yeelight Client}
         | 
| 13 | 
            +
              spec.description   = %q{Official implementation of Yeelight Operation Spec}
         | 
| 14 | 
            +
              spec.homepage      = "https://github.com/andrewostroumov/yeelight-client"
         | 
| 15 | 
            +
             | 
| 16 | 
            +
              # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
         | 
| 17 | 
            +
              # to allow pushing to a single host or delete this section to allow pushing to any host.
         | 
| 18 | 
            +
              # if spec.respond_to?(:metadata)
         | 
| 19 | 
            +
              #   spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
         | 
| 20 | 
            +
              #
         | 
| 21 | 
            +
              #   spec.metadata["homepage_uri"] = spec.homepage
         | 
| 22 | 
            +
              #   spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
         | 
| 23 | 
            +
              #   spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
         | 
| 24 | 
            +
              # else
         | 
| 25 | 
            +
              #   raise "RubyGems 2.0 or newer is required to protect against " \
         | 
| 26 | 
            +
              #     "public gem pushes."
         | 
| 27 | 
            +
              # end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              # Specify which files should be added to the gem when it is released.
         | 
| 30 | 
            +
              # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
         | 
| 31 | 
            +
              spec.files         = Dir.chdir(File.expand_path('..', __FILE__)) do
         | 
| 32 | 
            +
                `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
              spec.bindir        = "exe"
         | 
| 35 | 
            +
              spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
         | 
| 36 | 
            +
              spec.require_paths = ["lib"]
         | 
| 37 | 
            +
             | 
| 38 | 
            +
              spec.add_development_dependency "bundler", "~> 1.17"
         | 
| 39 | 
            +
              spec.add_development_dependency "rake", "~> 10.0"
         | 
| 40 | 
            +
              spec.add_development_dependency "rspec", "~> 3.0"
         | 
| 41 | 
            +
              spec.add_development_dependency "pry", "~> 0.12.2"
         | 
| 42 | 
            +
              spec.add_development_dependency "bump", "~> 0.8.0"
         | 
| 43 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,136 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: yeelight-client
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 1.0.2
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Andrew Ostroumov
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: exe
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2019-11-02 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: bundler
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - "~>"
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: '1.17'
         | 
| 20 | 
            +
              type: :development
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - "~>"
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: '1.17'
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: rake
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - "~>"
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: '10.0'
         | 
| 34 | 
            +
              type: :development
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - "~>"
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: '10.0'
         | 
| 41 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            +
              name: rspec
         | 
| 43 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            +
                requirements:
         | 
| 45 | 
            +
                - - "~>"
         | 
| 46 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            +
                    version: '3.0'
         | 
| 48 | 
            +
              type: :development
         | 
| 49 | 
            +
              prerelease: false
         | 
| 50 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - "~>"
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: '3.0'
         | 
| 55 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 56 | 
            +
              name: pry
         | 
| 57 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 | 
            +
                requirements:
         | 
| 59 | 
            +
                - - "~>"
         | 
| 60 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            +
                    version: 0.12.2
         | 
| 62 | 
            +
              type: :development
         | 
| 63 | 
            +
              prerelease: false
         | 
| 64 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 | 
            +
                requirements:
         | 
| 66 | 
            +
                - - "~>"
         | 
| 67 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            +
                    version: 0.12.2
         | 
| 69 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 70 | 
            +
              name: bump
         | 
| 71 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 72 | 
            +
                requirements:
         | 
| 73 | 
            +
                - - "~>"
         | 
| 74 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 75 | 
            +
                    version: 0.8.0
         | 
| 76 | 
            +
              type: :development
         | 
| 77 | 
            +
              prerelease: false
         | 
| 78 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 79 | 
            +
                requirements:
         | 
| 80 | 
            +
                - - "~>"
         | 
| 81 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 82 | 
            +
                    version: 0.8.0
         | 
| 83 | 
            +
            description: Official implementation of Yeelight Operation Spec
         | 
| 84 | 
            +
            email:
         | 
| 85 | 
            +
            - andrew.ostroumov@gmail.com
         | 
| 86 | 
            +
            executables: []
         | 
| 87 | 
            +
            extensions: []
         | 
| 88 | 
            +
            extra_rdoc_files: []
         | 
| 89 | 
            +
            files:
         | 
| 90 | 
            +
            - ".gitignore"
         | 
| 91 | 
            +
            - ".rspec"
         | 
| 92 | 
            +
            - ".travis.yml"
         | 
| 93 | 
            +
            - Gemfile
         | 
| 94 | 
            +
            - Gemfile.lock
         | 
| 95 | 
            +
            - README.md
         | 
| 96 | 
            +
            - Rakefile
         | 
| 97 | 
            +
            - bin/bump
         | 
| 98 | 
            +
            - bin/console
         | 
| 99 | 
            +
            - bin/setup
         | 
| 100 | 
            +
            - lib/yeelight-client.rb
         | 
| 101 | 
            +
            - lib/yeelight_client.rb
         | 
| 102 | 
            +
            - lib/yeelight_client/broadcast.rb
         | 
| 103 | 
            +
            - lib/yeelight_client/broadcast/broadcaster.rb
         | 
| 104 | 
            +
            - lib/yeelight_client/broadcast/response.rb
         | 
| 105 | 
            +
            - lib/yeelight_client/connection.rb
         | 
| 106 | 
            +
            - lib/yeelight_client/handler.rb
         | 
| 107 | 
            +
            - lib/yeelight_client/requests.rb
         | 
| 108 | 
            +
            - lib/yeelight_client/response.rb
         | 
| 109 | 
            +
            - lib/yeelight_client/response/exception.rb
         | 
| 110 | 
            +
            - lib/yeelight_client/response/result.rb
         | 
| 111 | 
            +
            - lib/yeelight_client/version.rb
         | 
| 112 | 
            +
            - yeelight-client.gemspec
         | 
| 113 | 
            +
            homepage: https://github.com/andrewostroumov/yeelight-client
         | 
| 114 | 
            +
            licenses: []
         | 
| 115 | 
            +
            metadata: {}
         | 
| 116 | 
            +
            post_install_message: 
         | 
| 117 | 
            +
            rdoc_options: []
         | 
| 118 | 
            +
            require_paths:
         | 
| 119 | 
            +
            - lib
         | 
| 120 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 121 | 
            +
              requirements:
         | 
| 122 | 
            +
              - - ">="
         | 
| 123 | 
            +
                - !ruby/object:Gem::Version
         | 
| 124 | 
            +
                  version: '0'
         | 
| 125 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 126 | 
            +
              requirements:
         | 
| 127 | 
            +
              - - ">="
         | 
| 128 | 
            +
                - !ruby/object:Gem::Version
         | 
| 129 | 
            +
                  version: '0'
         | 
| 130 | 
            +
            requirements: []
         | 
| 131 | 
            +
            rubyforge_project: 
         | 
| 132 | 
            +
            rubygems_version: 2.7.6
         | 
| 133 | 
            +
            signing_key: 
         | 
| 134 | 
            +
            specification_version: 4
         | 
| 135 | 
            +
            summary: Yeelight Client
         | 
| 136 | 
            +
            test_files: []
         |