├── spec ├── features │ ├── support │ │ └── setup.rb │ └── lights.feature ├── rule_spec.rb ├── scene_spec.rb ├── bridge_spec.rb ├── groupstate_spec.rb ├── bulb_spec.rb ├── group_spec.rb ├── datastore_spec.rb └── bulbstate_spec.rb ├── lib ├── lights │ ├── version.rb │ ├── loggerconfig.rb │ ├── hobject.rb │ ├── bulblist.rb │ ├── rulelist.rb │ ├── userlist.rb │ ├── grouplist.rb │ ├── scenelist.rb │ ├── sensorlist.rb │ ├── schedulelist.rb │ ├── list.rb │ ├── command.rb │ ├── bridge.rb │ ├── user.rb │ ├── group.rb │ ├── groupstate.rb │ ├── scene.rb │ ├── bulb.rb │ ├── schedule.rb │ ├── exception.rb │ ├── rule.rb │ ├── datastore.rb │ ├── sensor.rb │ ├── config.rb │ └── bulbstate.rb └── lights.rb ├── Gemfile ├── Rakefile ├── .gitignore ├── LICENSE.txt ├── README.md ├── lights.gemspec └── bin └── lights /spec/features/support/setup.rb: -------------------------------------------------------------------------------- 1 | require 'aruba/cucumber' 2 | -------------------------------------------------------------------------------- /lib/lights/version.rb: -------------------------------------------------------------------------------- 1 | module LightsConst 2 | VERSION = "1.0.2" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in lights.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/lights/loggerconfig.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module LoggerConfig 4 | LIGHTS_LEVEL = Logger::FATAL 5 | end 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | task :test do 4 | puts `bundle exec rspec spec/` 5 | puts `bundle exec cucumber spec/features/` 6 | end 7 | -------------------------------------------------------------------------------- /lib/lights/hobject.rb: -------------------------------------------------------------------------------- 1 | class HObject 2 | def initialize(*args) 3 | end 4 | 5 | def to_json(options={}) 6 | data.to_json 7 | end 8 | end 9 | 10 | -------------------------------------------------------------------------------- /lib/lights/bulblist.rb: -------------------------------------------------------------------------------- 1 | require 'lights/bulb' 2 | require 'lights/list' 3 | 4 | class BulbList < List 5 | def initialize(data = {}) 6 | super 7 | data.each{|id,value| @list << Bulb.new(id,value)} if data 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/lights/rulelist.rb: -------------------------------------------------------------------------------- 1 | require 'lights/rule' 2 | require 'lights/list' 3 | 4 | class RuleList < List 5 | def initialize(data = {}) 6 | super 7 | data.each{|id,value| @list << Rule.new(id,value)} if data 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/lights/userlist.rb: -------------------------------------------------------------------------------- 1 | require 'lights/user' 2 | require 'lights/list' 3 | 4 | class UserList < List 5 | def initialize(data = {}) 6 | super 7 | data.each{|id,value| @list << User.new(id,value)} if data 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/lights/grouplist.rb: -------------------------------------------------------------------------------- 1 | require 'lights/group' 2 | require 'lights/list' 3 | 4 | class GroupList < List 5 | def initialize(data = {}) 6 | super 7 | data.each{|id,value| @list << Group.new(id,value)} if data 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/lights/scenelist.rb: -------------------------------------------------------------------------------- 1 | require 'lights/scene' 2 | require 'lights/list' 3 | 4 | class SceneList < List 5 | def initialize(data = {}) 6 | super 7 | data.each{|id,value| @list << Scene.new(id,value)} if data 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/lights/sensorlist.rb: -------------------------------------------------------------------------------- 1 | require 'lights/sensor' 2 | require 'lights/list' 3 | 4 | class SensorList < List 5 | def initialize(data = {}) 6 | super 7 | data.each{|id,value| @list << Sensor.new(id,value)} if data 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/lights/schedulelist.rb: -------------------------------------------------------------------------------- 1 | require 'lights/schedule' 2 | require 'lights/list' 3 | 4 | class ScheduleList < List 5 | def initialize(data = {}) 6 | super 7 | data.each{|id,value| @list << Schedule.new(id,value)} if data 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # TESTING FILES 2 | lib/run.rb 3 | 4 | *.gem 5 | *.rbc 6 | .bundle 7 | .config 8 | Gemfile.lock 9 | coverage 10 | InstalledFiles 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | 19 | # YARD artifacts 20 | .yardoc 21 | _yardoc 22 | doc/ 23 | -------------------------------------------------------------------------------- /spec/rule_spec.rb: -------------------------------------------------------------------------------- 1 | require 'lights' 2 | 3 | describe Rule do 4 | it "properly reconstucts object hash" do 5 | data = { 6 | "name" => "test name", 7 | } 8 | rule = Rule.new(1,data) 9 | rule.id.should eql 1 10 | rule.name.should eql "test name" 11 | rule.data.should eql data 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/lights/list.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | require 'lights/hobject' 4 | 5 | class List < HObject 6 | include Enumerable 7 | extend Forwardable 8 | def_delegators :@list, :each, :<< , :size 9 | 10 | attr_reader :list 11 | def initialize(data = {}) 12 | @list = [] 13 | end 14 | 15 | def data 16 | data = {} 17 | @list.each {|b| data[b.id] = b.data} if @list 18 | data 19 | end 20 | end 21 | 22 | -------------------------------------------------------------------------------- /lib/lights/command.rb: -------------------------------------------------------------------------------- 1 | require 'lights/hobject' 2 | 3 | class Command < HObject 4 | attr_reader :address, :method, :body 5 | def initialize(data = {}) 6 | @address = data["address"] 7 | @body = data["body"] 8 | @method = data["method"] 9 | end 10 | 11 | def data 12 | data = {} 13 | data["address"] = @address if @address 14 | data["body"] = @body if @body 15 | data["method"] = @method if @method 16 | data 17 | end 18 | end 19 | 20 | -------------------------------------------------------------------------------- /lib/lights/bridge.rb: -------------------------------------------------------------------------------- 1 | require 'lights/hobject' 2 | 3 | class Bridge < HObject 4 | 5 | attr_reader :id, :ip, :name, :mac 6 | def initialize(data = {}) 7 | @id = data["id"] 8 | @ip = data["internalipaddress"] 9 | @mac = data["macaddress"] 10 | @name = data["name"] 11 | end 12 | 13 | def data 14 | data = {} 15 | data["id"] = @id if @id 16 | data["internalipaddress"] = @ip if @ip 17 | data["macaddress"] = @mac if @mac 18 | data["name"] = @name if @name 19 | data 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /lib/lights/user.rb: -------------------------------------------------------------------------------- 1 | require 'lights/hobject' 2 | 3 | class User < HObject 4 | attr_reader :id, :name, :create_date, :last_use_date 5 | def initialize( id, data = {} ) 6 | @id = id 7 | @name = data["name"] 8 | @create_date = data["create date"] 9 | @last_use_date = data["last use date"] 10 | end 11 | 12 | def data 13 | data = {} 14 | data["name"] = @name if @name 15 | data["create date"] = @create_date if @create_date 16 | data["last use date"] = @last_use_date if @last_use_date 17 | data 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/lights/group.rb: -------------------------------------------------------------------------------- 1 | require 'lights/hobject' 2 | 3 | class Group < HObject 4 | attr_reader :id, :data, :name, :lights, :action, :type 5 | attr_writer :name, :lights, :action 6 | def initialize( id = nil, data = {} ) 7 | @id = id 8 | @action = BulbState.new(data["action"]) 9 | @name = data["name"] 10 | @lights = data["lights"] 11 | @type = data["type"] 12 | end 13 | 14 | def data 15 | data = {} 16 | data["name"] = @name if @name 17 | data["lights"] = @lights if @lights 18 | data["type"] = @type if @type 19 | data["action"] = @action.data unless @action.data.empty? 20 | data 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/lights/groupstate.rb: -------------------------------------------------------------------------------- 1 | require 'lights/bulbstate' 2 | 3 | class GroupState < BulbState 4 | attr_reader :scene 5 | def initialize(data={}) 6 | super(data) 7 | set_scene data["scene"] if data["scene"] 8 | end 9 | 10 | def scene=(value) set_scene(value) end 11 | def set_scene(value) 12 | if value.class == String 13 | @scene = value 14 | else 15 | raise BulbStateValueTypeException, "Scene value has incorrect type. Requires String, got #{value.class}. Was #{value.inspect}" 16 | end 17 | end 18 | 19 | def data 20 | data = BulbState.instance_method(:data).bind(self).call 21 | data["scene"] = @scene if @scene 22 | data 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/scene_spec.rb: -------------------------------------------------------------------------------- 1 | require 'lights' 2 | 3 | describe Scene do 4 | it "properly parse input parameters" do 5 | data = { "name" => "test name", 6 | "lights" => ["1","2","3"], 7 | "active" => true } 8 | scene = Scene.new(1,data) 9 | scene.name.should eql "test name" 10 | scene.id.should eql 1 11 | scene.lights.should eql ["1","2","3"] 12 | scene.active.should eql true 13 | end 14 | 15 | it "properly reconstucts object hash" do 16 | data = { "name" => "test name", 17 | "lights" => ["1","2","3"], 18 | "active" => true } 19 | scene = Scene.new(1,data) 20 | scene.data.should eql data 21 | scene.id.should eql 1 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/lights/scene.rb: -------------------------------------------------------------------------------- 1 | require 'lights/hobject' 2 | 3 | class Scene < HObject 4 | attr_accessor :id, :name, :active, :lights, :recycle, :transition_time 5 | def initialize(id = nil,data = {}) 6 | @id = id 7 | @name = data["name"] 8 | @active = data["active"] 9 | @lights = data["lights"] 10 | @recycle = data["recycle"] 11 | @transition_time = data["transitiontime"] 12 | end 13 | 14 | def data 15 | data = {} 16 | data["name"] = @name if @name 17 | data["active"] = @active unless @active.nil? 18 | data["lights"] = @lights if @lights 19 | data["recycle"] = @recycle unless @recycle.nil? 20 | data["transitiontime"] = @transition_time if @transition_time 21 | data 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /spec/bridge_spec.rb: -------------------------------------------------------------------------------- 1 | require 'lights' 2 | 3 | describe Bridge do 4 | it "properly parse input parameters" do 5 | data = { 6 | "id" => "test id", 7 | "internalipaddress" => "192.168.1.27", 8 | "macaddress" => "01:23:45:67:89:AB", 9 | "name" => "test name", 10 | } 11 | bridge = Bridge.new(data) 12 | bridge.id.should eql "test id" 13 | bridge.ip.should eql "192.168.1.27" 14 | bridge.mac.should eql "01:23:45:67:89:AB" 15 | bridge.name.should eql "test name" 16 | end 17 | 18 | it "properly reconstructs object hash" do 19 | data = { 20 | "id" => "test id", 21 | "internalipaddress" => "192.168.1.27", 22 | "macaddress" => "01:23:45:67:89:AB", 23 | "name" => "test name", 24 | } 25 | bridge = Bridge.new(data) 26 | bridge.data.should eql data 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/groupstate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'lights' 2 | 3 | describe GroupState do 4 | 5 | it "properly reconstructs object hash" do 6 | data = { "scene" => "22978db88-on-0" } 7 | s = GroupState.new(data) 8 | s.data.should eql data 9 | end 10 | 11 | # SCENE 12 | it "should properly set scene value in constructor" do 13 | data = { "scene" => "22978db88-on-0" } 14 | s = GroupState.new(data) 15 | s.scene.should eql "22978db88-on-0" 16 | s.data["scene"].should eql "22978db88-on-0" 17 | end 18 | it "should properly set scene value" do 19 | s = GroupState.new 20 | s.scene = "22978db88-on-0" 21 | s.scene.should eq "22978db88-on-0" 22 | s.data["scene"].should eq "22978db88-on-0" 23 | end 24 | it "should raise exception when scene has invalid type" do 25 | s = GroupState.new 26 | expect { s.scene = 124 }.to raise_error 27 | end 28 | end 29 | 30 | -------------------------------------------------------------------------------- /lib/lights/bulb.rb: -------------------------------------------------------------------------------- 1 | require 'lights/bulbstate' 2 | require 'lights/hobject' 3 | 4 | class Bulb < HObject 5 | attr_reader :id, :name, :type, :sw_version, :state, 6 | :point_symbol, :model_id, :unique_id 7 | attr_writer :name, :state 8 | def initialize(id,data = {}) 9 | @id = id 10 | @name = data["name"] 11 | @type = data["type"] 12 | @sw_version = data["swversion"] 13 | @point_symbol = data["pointsymbol"] 14 | @model_id = data["modelid"] 15 | @unique_id = data["uniqueid"] 16 | @state = BulbState.new data["state"] 17 | end 18 | 19 | def data 20 | data = {} 21 | data["name"] = @name if @name 22 | data["type"] = @type if @type 23 | data["swversion"] = @sw_version if @sw_version 24 | data["state"] = @state.data unless @state.data.empty? 25 | data["pointsymbol"] = @point_symbol if @point_symbol 26 | data["modelid"] = @model_id if @model_id 27 | data["uniqueid"] = @unique_id if @unique_id 28 | data 29 | end 30 | end 31 | 32 | -------------------------------------------------------------------------------- /spec/bulb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'lights' 2 | 3 | describe Bulb do 4 | it "properly parse input parameters" do 5 | data = { "name" => "test name" } 6 | bulb = Bulb.new(1,data) 7 | bulb.name.should eql "test name" 8 | end 9 | 10 | it "properly set name parameter" do 11 | data = { "name" => "test name" } 12 | bulb = Bulb.new(1,data) 13 | bulb.name = "new test name" 14 | bulb.name.should eql "new test name" 15 | end 16 | 17 | it "properly creates state object" do 18 | data = { 19 | "name" => "test name", 20 | "state" => { 21 | "on" => true 22 | } 23 | } 24 | bulb = Bulb.new(1,data) 25 | bulb.state.on.should eql true 26 | bulb.state.data.should eql data["state"] 27 | end 28 | 29 | it "properly reconstucts object hash" do 30 | data = { 31 | "name" => "test name", 32 | "state" => { 33 | "on" => true 34 | } 35 | } 36 | bulb = Bulb.new(1,data) 37 | bulb.data.should eql data 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/lights/schedule.rb: -------------------------------------------------------------------------------- 1 | require 'lights/command' 2 | require 'lights/hobject' 3 | 4 | class Schedule < HObject 5 | attr_reader :id, :name, :time, :status, 6 | :description, :local_time, 7 | :created, :command 8 | def initialize(id,data = {}) 9 | @id = id 10 | @name = data["name"] 11 | @time = data["time"] 12 | @status = data["status"] 13 | @description = data["description"] 14 | @local_time = data["localtime"] 15 | @created = data["created"] 16 | @command = Command.new(data["command"]) 17 | end 18 | 19 | def scene 20 | @command.body["scene"] 21 | end 22 | 23 | def data 24 | data = {} 25 | data["name"] = @name if @name 26 | data["time"] = @time if @time 27 | data["status"] = @status if @status 28 | data["description"] = @description if @description 29 | data["localtime"] = @local_time if @local_time 30 | data["created"] = @created if @created 31 | data["command"] = @command.data unless @command.data.empty? 32 | data 33 | end 34 | end 35 | 36 | -------------------------------------------------------------------------------- /lib/lights/exception.rb: -------------------------------------------------------------------------------- 1 | class BridgeConnectException < Exception 2 | def initialize(msg = "Press the button on the Hue bridge and try again.") 3 | super 4 | end 5 | end 6 | 7 | class UsernameException < Exception 8 | def initialize(msg = "Please register username and try again.") 9 | super 10 | end 11 | end 12 | 13 | class BulbStateValueOutOfRangeException < Exception 14 | def initalize(msg = "Value out of range.") 15 | super 16 | end 17 | end 18 | 19 | class BulbStateValueTypeException < Exception 20 | def initialize(msg = "Value is of incorrect type.") 21 | super 22 | end 23 | end 24 | 25 | class ResourceUnavailableException < Exception 26 | def initialize(msg = "ResourceUnavailable") 27 | super 28 | end 29 | end 30 | 31 | class ParameterUnavailableException < Exception 32 | def intialize(msg = "Parameter unavailable") 33 | super 34 | end 35 | end 36 | 37 | class SceneLockedException < Exception 38 | def initialize(msg = "Scene could not be removed, because it's locked.") 39 | super 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/lights/rule.rb: -------------------------------------------------------------------------------- 1 | require 'lights/hobject' 2 | 3 | class Rule < HObject 4 | attr_reader :id, :name, :owner, :created, 5 | :last_triggered, :times_triggered, 6 | :status, :conditions, :actions 7 | def initialize( id = nil, data = {} ) 8 | @id = id 9 | @name = data["name"] 10 | @owner = data["owner"] 11 | @created = data["created"] 12 | @last_triggered = data["lasttriggered"] 13 | @times_triggered = data["timestriggered"] 14 | @status = data["status"] 15 | @conditions = data["conditions"] 16 | @actions = data["actions"] 17 | end 18 | 19 | def data 20 | data = {} 21 | data["name"] = @name if @name 22 | data["owner"] = @owner if @owner 23 | data["created"] = @created if @created 24 | data["lasttriggered"] = @last_triggered if @last_triggered 25 | data["timestriggered"] = @times_triggered if @times_triggered 26 | data["status"] = @status if @status 27 | data["conditions"] = @conditions if @conditions 28 | data["actions"] = @actions if @actions 29 | data 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Brady Turner 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lights 2 | ======== 3 | A Ruby library & CLI for interacting with Philips Hue. 4 | 5 | * Philips Hue API Documentation: http://www.developers.meethue.com/philips-hue-api 6 | * lights on RubyGems: https://rubygems.org/gems/lights 7 | 8 | Installation 9 | ---- 10 | ``` 11 | gem install lights 12 | ``` 13 | 14 | Basic Usage 15 | ----- 16 | ```ruby 17 | require 'lights' 18 | client = Lights.new( '192.168.x.x', 'username' ) 19 | client.register 20 | client.request_bulb_list 21 | ``` 22 | See [lights-examples](https://github.com/turnerba/lights-examples) for more usage examples. 23 | 24 | CLI Quick Setup 25 | ---- 26 | 27 | ``` 28 | lights discover -s 29 | lights register 30 | lights list 31 | lights on 32 | lights off 33 | ``` 34 | 35 | See [Sample Usage (Implemented)](https://github.com/turnerba/lights/wiki/Sample-Usage-(Implemented)) for more usage examples. 36 | 37 | Development 38 | ----- 39 | #### Test: 40 | ``` 41 | bundle exec rspec spec/ 42 | bundle exec cucumber spec/features/ 43 | ``` 44 | or 45 | ``` 46 | rake test 47 | ``` 48 | 49 | #### Build: 50 | ``` 51 | rake build 52 | ``` 53 | 54 | #### Install: 55 | ``` 56 | rake install 57 | ``` 58 | -------------------------------------------------------------------------------- /lights.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'lights/version' 5 | require 'date' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = 'lights' 9 | s.version = LightsConst::VERSION 10 | s.authors = ["Brady Turner"] 11 | s.email = 'bradyaturner@gmail.com' 12 | s.description = "Client library and CLI for controlling Philips Hue lights." 13 | s.summary = "lights" 14 | s.homepage = 'http://rubygems.org/gems/lights' 15 | s.license = 'MIT' 16 | s.date = Date.today.to_s 17 | 18 | s.files = `git ls-files`.split($/) 19 | s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } 20 | s.test_files = s.files.grep(%r{^(test|spec|features)/}) 21 | s.require_paths = ["lib"] 22 | 23 | s.add_runtime_dependency "simpletable", "~> 0.3.2" 24 | 25 | s.add_development_dependency "bundler", "~> 1.3" 26 | s.add_development_dependency "rake" 27 | s.add_development_dependency "rspec", "~> 2.6" 28 | s.add_development_dependency "cucumber" 29 | s.add_development_dependency "aruba" 30 | end 31 | -------------------------------------------------------------------------------- /spec/features/lights.feature: -------------------------------------------------------------------------------- 1 | Feature: Lights 2 | 3 | Scenario: No command 4 | When I run `lights` 5 | Then the output should contain "Must specify a command." 6 | 7 | Scenario: List Default 8 | When I run `lights list` 9 | Then the output should contain "ID" 10 | 11 | Scenario: List Hue lights 12 | When I run `lights list lights` 13 | Then the output should contain "ID" 14 | 15 | Scenario: List Hue groups 16 | When I run `lights list groups` 17 | Then the output should contain "ID" 18 | 19 | Scenario: List Hue sensors 20 | When I run `lights list sensors` 21 | Then the output should contain "ID" 22 | 23 | Scenario: List Hue scenes 24 | When I run `lights list scenes` 25 | Then the output should contain "ID" 26 | 27 | Scenario: List registered users 28 | When I run `lights list users` 29 | Then the output should contain "ID" 30 | 31 | Scenario: List rules 32 | When I run `lights list rules` 33 | Then the output should contain "ID" 34 | # This test is failing because OptionParser is casting the string as an integer (0) 35 | # Scenario: Incorrect parameters to set 36 | # When I run `lights set -l all -h test` 37 | # Then the output should contain "incorrect" 38 | 39 | -------------------------------------------------------------------------------- /lib/lights/datastore.rb: -------------------------------------------------------------------------------- 1 | require 'lights/config' 2 | require 'lights/bulblist' 3 | require 'lights/grouplist' 4 | require 'lights/scenelist' 5 | require 'lights/rulelist' 6 | require 'lights/schedulelist' 7 | require 'lights/sensorlist' 8 | require 'lights/hobject' 9 | 10 | class Datastore < HObject 11 | attr_reader :lights, :groups, :config, :rules, 12 | :scenes, :schedules, :sensors 13 | def initialize(data = {}) 14 | @lights = BulbList.new(data["lights"]) 15 | @groups = GroupList.new(data["groups"]) 16 | @config = HueConfig.new(data["config"]) 17 | @schedules = ScheduleList.new(data["schedules"]) 18 | @scenes = SceneList.new(data["scenes"]) 19 | @rules = RuleList.new(data["rules"]) 20 | @sensors = SensorList.new(data["sensors"]) 21 | end 22 | 23 | def list 24 | @lights.list + \ 25 | @groups.list + \ 26 | [@config] + \ 27 | @schedules.list + \ 28 | @scenes.list + \ 29 | @rules.list + \ 30 | @sensors.list 31 | end 32 | 33 | def data 34 | data = {} 35 | data["lights"] = @lights.data if !@lights.data.empty? 36 | data["groups"] = @groups.data if !@groups.data.empty? 37 | data["config"] = @config.data if !@config.data.empty? 38 | data["schedules"] = @schedules.data if !@schedules.data.empty? 39 | data["scenes"] = @scenes.data if !@scenes.data.empty? 40 | data["rules"] = @rules.data if !@rules.data.empty? 41 | data["sensors"] = @sensors.data if !@sensors.data.empty? 42 | data 43 | end 44 | end 45 | 46 | -------------------------------------------------------------------------------- /lib/lights/sensor.rb: -------------------------------------------------------------------------------- 1 | class SensorState < HObject 2 | attr_reader :last_updated 3 | def initialize(data) 4 | @last_updated = data["lastupdated"] 5 | @daylight = data["daylight"] 6 | @button_event = data["buttonevent"] 7 | end 8 | 9 | def data 10 | data = {} 11 | data["lastupdated"] = @last_updated if @last_updated 12 | data["daylight"] = @daylight unless @daylight.nil? 13 | data["buttonevent"] = @button_event if @button_event 14 | data 15 | end 16 | end 17 | 18 | class Sensor < HObject 19 | attr_reader :id, :data, :name, :type, :model_id, 20 | :manufacturer_name, :unique_id, 21 | :sw_version, :state 22 | def initialize( id, data = {} ) 23 | @id = id 24 | @name = data["name"] 25 | @type = data["type"] 26 | @model_id = data["modelid"] 27 | @manufacturer_name = data["manufacturername"] 28 | @unique_id = data["uniqueid"] 29 | @sw_version = data["swversion"] 30 | @config = data["config"] 31 | @state = SensorState.new(data["state"]) 32 | end 33 | 34 | def data 35 | data = {} 36 | data["name"] = @name if @name 37 | data["type"] = @type if @type 38 | data["modelid"] = @model_id if @model_id 39 | data["manufacturername"] = @manufacturer_name if @manufacturer_name 40 | data["uniqueid"] = @unique_id if @unique_id 41 | data["swversion"] = @sw_version if @sw_version 42 | data["config"] = @config if @config 43 | data["state"] = @state.data unless @state.data.empty? 44 | data 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/lights/config.rb: -------------------------------------------------------------------------------- 1 | require 'lights/userlist' 2 | require 'lights/hobject' 3 | 4 | class HueConfig < HObject 5 | attr_reader :name, :zigbee_channel, :mac, :dhcp, 6 | :ip_address, :netmask, :gateway, 7 | :proxy_address, :proxy_port, :utc, 8 | :local_time, :time_zone, :whitelist, 9 | :swversion, :api_version, :sw_update, 10 | :link_button, :portal_services, 11 | :portal_connection, :portal_state 12 | def initialize(data = {}) 13 | @name = data["name"] 14 | @zigbee_channel = data["zigbeechannel"] 15 | @mac = data["mac"] 16 | @dhcp = data["dhcp"] 17 | @ip_address = data["ipaddress"] 18 | @netmask = data["netmask"] 19 | @gateway = data["gateway"] 20 | @proxy_address = data["proxyaddress"] 21 | @proxy_port = data["proxyport"] 22 | @utc = data["UTC"] 23 | @local_time = data["localtime"] 24 | @time_zone = data["timezone"] 25 | @whitelist = UserList.new(data["whitelist"]) 26 | @sw_version = data["swversion"] 27 | @api_version = data["apiversion"] 28 | @sw_update = data["swupdate"] 29 | @link_button = data["linkbutton"] 30 | @portal_services = data["portalservices"] 31 | @portal_connection = data["portalconnection"] 32 | @portal_state = data["portalstate"] 33 | end 34 | 35 | def data 36 | data = {} 37 | data["name"] = @name if @name 38 | data["zigbeechannel"] = @zigbee_channel if @zigbee_channel 39 | data["mac"] = @mac if @mac 40 | data["dhcp"] = @dhcp unless @dhcp.nil? 41 | data["ipaddress"] = @ip_address if @ip_address 42 | data["netmask"] = @netmask if @netmask 43 | data["gateway"] = @gateway if @gateway 44 | data["proxyaddress"] = @proxy_address if @proxy_address 45 | data["proxyport"] = @proxy_port if @proxy_port 46 | data["UTC"] = @utc if @utc 47 | data["localtime"] = @local_time if @local_time 48 | data["timezone"] = @time_zone if @time_zone 49 | data["whitelist"] = @whitelist.data unless @whitelist.data.empty? 50 | data["swversion"] = @sw_version if @sw_version 51 | data["apiversion"] = @api_version if @api_version 52 | data["swupdate"] = @sw_update unless @sw_update.nil? 53 | data["linkbutton"] = @link_button unless @link_button.nil? 54 | data["portalservices"] = @portal_services unless @portal_services.nil? 55 | data["portalconnection"] = @portal_connection if @portal_connection 56 | data["portalstate"] = @portal_state unless @portal_state.nil? 57 | data 58 | end 59 | end 60 | 61 | -------------------------------------------------------------------------------- /spec/group_spec.rb: -------------------------------------------------------------------------------- 1 | require 'lights' 2 | 3 | describe Group do 4 | it "properly parses input parameters" do 5 | data = JSON.parse(GROUPS_JSON)["1"] 6 | group = Group.new(1,data) 7 | group.id.should eql 1 8 | group.name.should eql "Ceiling light" 9 | group.type.should eql "LightGroup" 10 | group.lights.should eql ["1","2","3","4"] 11 | end 12 | 13 | it "properly reconstucts object hash" do 14 | data = JSON.parse(GROUPS_JSON)["1"] 15 | group = Group.new(1,data) 16 | group.id.should eql 1 17 | group.data.should eql data 18 | end 19 | end 20 | 21 | GROUPS_JSON = %Q{ 22 | { 23 | "1": { 24 | "name": "Ceiling light", 25 | "lights": [ 26 | "1", 27 | "2", 28 | "3", 29 | "4" 30 | ], 31 | "type": "LightGroup", 32 | "action": { 33 | "on": true, 34 | "bri": 254, 35 | "hue": 56100, 36 | "sat": 232, 37 | "effect": "none", 38 | "xy": [ 39 | 0.4119, 40 | 0.1949 41 | ], 42 | "ct": 293, 43 | "colormode": "xy" 44 | } 45 | }, 46 | "2": { 47 | "name": "Lamp", 48 | "lights": [ 49 | "5" 50 | ], 51 | "type": "LightGroup", 52 | "action": { 53 | "on": true, 54 | "bri": 254, 55 | "hue": 56100, 56 | "sat": 232, 57 | "effect": "none", 58 | "xy": [ 59 | 0.4119, 60 | 0.1949 61 | ], 62 | "ct": 293, 63 | "colormode": "xy" 64 | } 65 | }, 66 | "3": { 67 | "name": "HueCraft_Ambiance1", 68 | "lights": [ 69 | "1", 70 | "2" 71 | ], 72 | "type": "LightGroup", 73 | "action": { 74 | "on": true, 75 | "bri": 254, 76 | "hue": 56100, 77 | "sat": 232, 78 | "effect": "none", 79 | "xy": [ 80 | 0.4119, 81 | 0.1949 82 | ], 83 | "ct": 293, 84 | "colormode": "xy" 85 | } 86 | }, 87 | "4": { 88 | "name": "HueCraft_Ambiance2", 89 | "lights": [ 90 | "3", 91 | "4" 92 | ], 93 | "type": "LightGroup", 94 | "action": { 95 | "on": true, 96 | "bri": 254, 97 | "hue": 56100, 98 | "sat": 232, 99 | "effect": "none", 100 | "xy": [ 101 | 0.4119, 102 | 0.1949 103 | ], 104 | "ct": 293, 105 | "colormode": "xy" 106 | } 107 | }, 108 | "5": { 109 | "name": "HueCraft_Effect1", 110 | "lights": [ 111 | "5" 112 | ], 113 | "type": "LightGroup", 114 | "action": { 115 | "on": true, 116 | "bri": 254, 117 | "hue": 56100, 118 | "sat": 232, 119 | "effect": "none", 120 | "xy": [ 121 | 0.4119, 122 | 0.1949 123 | ], 124 | "ct": 293, 125 | "colormode": "xy" 126 | } 127 | }, 128 | "6": { 129 | "name": "HueCraft_HueCraft", 130 | "lights": [ 131 | "1", 132 | "2", 133 | "3", 134 | "4", 135 | "5" 136 | ], 137 | "type": "LightGroup", 138 | "action": { 139 | "on": true, 140 | "bri": 254, 141 | "hue": 56100, 142 | "sat": 232, 143 | "effect": "none", 144 | "xy": [ 145 | 0.4119, 146 | 0.1949 147 | ], 148 | "ct": 293, 149 | "colormode": "xy" 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/lights.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'net/http' 4 | require 'uri' 5 | require 'json' 6 | 7 | require 'lights/bridge' 8 | require 'lights/exception' 9 | require 'lights/datastore' 10 | require 'lights/groupstate' 11 | require 'lights/loggerconfig' 12 | 13 | class Lights 14 | 15 | attr_reader :bulbs, :username 16 | def initialize(ip,username=nil) 17 | @ip = ip 18 | @username = username 19 | @http = Net::HTTP.new(ip,80) 20 | @bulbs = [] 21 | @groups = [] 22 | @bridges = [] 23 | @logger = Logger.new(STDERR) 24 | @logger.level = LoggerConfig::LIGHTS_LEVEL 25 | end 26 | 27 | def discover_hubs 28 | http = Net::HTTP.new("discovery.meethue.com",443) 29 | http.use_ssl = true 30 | request = Net::HTTP::Get.new( "/" ) 31 | response = http.request request 32 | 33 | case response.code.to_i 34 | when 200 35 | result = JSON.parse( response.body ) 36 | result.each { |b| @bridges << Bridge.new(b) } 37 | else 38 | @logger.fatal "Could not discover bridge. HTTP error #{response.code}" 39 | @logger.fatal "Response: #{response.body}" 40 | raise "Unknown error" 41 | end 42 | @bridges 43 | end 44 | 45 | def register 46 | data = { "devicetype"=>"lights" } 47 | response = @http.post "/api", data.to_json 48 | result = JSON.parse(response.body).first 49 | if result.has_key? "error" 50 | process_error result 51 | elsif result["success"] 52 | @username = result["success"]["username"] 53 | end 54 | result 55 | end 56 | 57 | # backwards compatibility 58 | alias_method :register_username, :register 59 | 60 | def request_config 61 | get "config" 62 | end 63 | 64 | def add_bulb(id,bulb_data) 65 | @bulbs << Bulb.new( id, bulb_data ) 66 | end 67 | 68 | def search_new 69 | post "lights" 70 | end 71 | 72 | def request_bulb_list 73 | get "lights" 74 | end 75 | 76 | def request_new_bulb_list 77 | get "lights/new" 78 | end 79 | 80 | def request_new_sensor_list 81 | get "sensors/new" 82 | end 83 | 84 | def request_bulb_info( id ) 85 | response = get "lights/#{id}" 86 | Bulb.new(id,response) 87 | end 88 | 89 | def request_group_info( id ) 90 | response = get "groups/#{id}" 91 | Group.new(id,response) 92 | end 93 | 94 | def request_sensor_info( id ) 95 | response = get "sensors/#{id}" 96 | Sensor.new(id,response) 97 | end 98 | 99 | def request_sensor_list 100 | get "sensors" 101 | end 102 | 103 | def request_group_list 104 | get "groups" 105 | end 106 | 107 | def request_schedule_list 108 | get "schedules" 109 | end 110 | 111 | def request_scene_list 112 | get "scenes" 113 | end 114 | 115 | def request_config 116 | get "config" 117 | end 118 | 119 | def request_rules 120 | get "rules" 121 | end 122 | 123 | def request_schedules 124 | get "schedules" 125 | end 126 | 127 | def request_datastore 128 | get "" 129 | end 130 | 131 | def set_bulb_state( id, state ) 132 | put "lights/#{id}/state", state 133 | end 134 | 135 | def set_group_state( id, state ) 136 | put "groups/#{id}/action", state 137 | end 138 | 139 | def create_group( group ) 140 | post "groups", group 141 | end 142 | 143 | def create_scene( scene ) 144 | post "scenes/#{scene.id}", scene 145 | end 146 | 147 | def delete_scene( id ) 148 | delete "scenes/#{id}" 149 | end 150 | 151 | def delete_group( id ) 152 | delete "groups/#{id}" 153 | end 154 | 155 | def edit_bulb( bulb ) 156 | put "lights/#{bulb.id}", bulb 157 | end 158 | 159 | def edit_group( group ) 160 | put "groups/#{group.id}", group 161 | end 162 | 163 | def delete_user( username ) 164 | delete "config/whitelist/#{username}" 165 | end 166 | 167 | private 168 | def process_error(result) 169 | type = result["error"]["type"] 170 | case type 171 | when 1 172 | raise UsernameException 173 | when 3 174 | raise ResourceUnavailableException, result["error"]["description"] 175 | when 6 176 | raise ParameterUnavailableException, result["error"]["description"] 177 | when 101 178 | raise BridgeConnectException 179 | when 403 180 | raise SceneLockedException, result["error"]["description"] 181 | else 182 | raise "Unknown Error: #{result["error"]["description"]}" 183 | end 184 | end 185 | 186 | def get( path ) 187 | @logger.debug "==> GET: #{path}" 188 | raise UsernameException unless @username 189 | request = Net::HTTP::Get.new( "/api/#{@username}/#{path}" ) 190 | response = @http.request request 191 | result = JSON.parse( response.body ) 192 | @logger.debug "<== #{response.code}" 193 | @logger.debug response.body 194 | if result.first.kind_of?(Hash) && result.first.has_key?("error") 195 | process_error result.first 196 | end 197 | result 198 | end 199 | 200 | def put( path, data={} ) 201 | @logger.debug "==> PUT: #{path}" 202 | raise UsernameException unless @username 203 | @logger.debug data.to_json 204 | response = @http.put( "/api/#{@username}/#{path}", data.to_json ) 205 | result = JSON.parse( response.body ) 206 | @logger.debug "<== #{response.code}" 207 | @logger.debug response.body 208 | if result.first.kind_of?(Hash) && result.first.has_key?("error") 209 | process_error result.first 210 | end 211 | result 212 | end 213 | 214 | def post( path, data={} ) 215 | @logger.debug "==> POST: #{path}" 216 | raise UsernameException unless @username 217 | @logger.debug data.to_json 218 | response = @http.post( "/api/#{@username}/#{path}", data.to_json ) 219 | result = JSON.parse( response.body ) 220 | @logger.debug "<== #{response.code}" 221 | @logger.debug response.body 222 | if result.first.kind_of?(Hash) && result.first.has_key?("error") 223 | process_error result.first 224 | end 225 | result 226 | end 227 | 228 | def delete( path ) 229 | @logger.debug "==> DELETE: #{path}" 230 | raise UsernameException unless @username 231 | request = Net::HTTP::Delete.new( "/api/#{@username}/#{path}" ) 232 | response = @http.request request 233 | result = JSON.parse( response.body ) 234 | @logger.debug "<== #{response.code}" 235 | @logger.debug response.body 236 | if result.first.kind_of?(Hash) && result.first.has_key?("error") 237 | process_error result.first 238 | end 239 | result 240 | end 241 | 242 | end 243 | 244 | -------------------------------------------------------------------------------- /lib/lights/bulbstate.rb: -------------------------------------------------------------------------------- 1 | require 'lights/hobject' 2 | 3 | class BulbState < HObject 4 | 5 | MAX_CT = 500 6 | MIN_CT = 153 7 | MAX_BRI = 255 8 | MIN_BRI = 0 9 | MAX_SAT = 255 10 | MIN_SAT = 0 11 | MAX_HUE = 65535 12 | MIN_HUE = 0 13 | MIN_TRANSITION_TIME = 0 14 | MAX_XY = 1.0 15 | MIN_XY = 0.0 16 | 17 | module Effect 18 | NONE = "none" 19 | COLORLOOP = "colorloop" 20 | end 21 | 22 | module Alert 23 | NONE = "none" 24 | SELECT = "select" 25 | LSELECT = "lselect" 26 | end 27 | 28 | module ColorMode 29 | HS = "hs" 30 | XY = "xy" 31 | CT = "ct" 32 | end 33 | 34 | module Hue 35 | YELLOW = 12750 36 | LGREEN = 22500 37 | GREEN = 25500 38 | BLUE = 46920 39 | PURPLE = 56100 40 | RED = 65535 41 | end 42 | 43 | attr_reader :on, :bri, :hue, :sat, :xy, :ct, 44 | :alert, :effect, :color_mode, 45 | :reachable, :transition_time 46 | def initialize( data = {} ) 47 | data = {} if data == nil 48 | @reachable = data["reachable"] 49 | 50 | # bridge returns invaild values for state variables when reachable is false 51 | unless @reachable == false 52 | @on = data["on"] 53 | set_bri data["bri"] 54 | set_hue data["hue"] 55 | set_sat data["sat"] 56 | set_xy data["xy"] 57 | set_ct data["ct"] 58 | set_alert data["alert"] 59 | set_effect data["effect"] 60 | set_color_mode data["colormode"] 61 | set_transition_time data["transitiontime"] 62 | end 63 | end 64 | 65 | def color_mode=(value) set_color_mode(value) end 66 | def set_color_mode(value) 67 | if value.nil? || value == ColorMode::XY \ 68 | || value == ColorMode::HS \ 69 | || value == ColorMode::CT 70 | @color_mode = value 71 | else 72 | raise BulbStateValueTypeException, "Color mode value has incorrect type. Requires 'hs', 'xy', or 'ct'. Was #{value.inspect}" 73 | end 74 | end 75 | 76 | def alert=(value) set_alert(value) end 77 | def set_alert(value) 78 | if value.nil? || value == Alert::NONE \ 79 | || value == Alert::SELECT \ 80 | || value == Alert::LSELECT 81 | @alert = value 82 | else 83 | raise BulbStateValueTypeException, "Alert value has incorrect type. Requires 'none', 'select', or 'lselect'. Was #{value.inspect}" 84 | end 85 | end 86 | 87 | def effect=(value) set_effect(value) end 88 | def set_effect(value) 89 | if value.nil? || value == Effect::NONE || value == Effect::COLORLOOP 90 | @effect = value 91 | else 92 | raise BulbStateValueTypeException, "Effect value has incorrect type. Requires 'none' or 'colorloop'. Was #{value.inspect}" 93 | end 94 | end 95 | 96 | def on=(value) set_on(value) end 97 | def set_on(value) 98 | # Tests if value is boolean 99 | if !!value == value 100 | @on = value 101 | else 102 | raise BulbStateValueTypeException, "On value has incorrect type. Requires boolean, got #{value.class}. Was #{value.inspect}" 103 | end 104 | end 105 | 106 | def bri=(value); set_bri(value) end 107 | def set_bri(value) 108 | if value.nil? || value.between?(MIN_BRI,MAX_BRI) 109 | @bri = value 110 | else 111 | raise BulbStateValueOutOfRangeException, "Brightness value out of range. Must be [#{MIN_BRI},#{MAX_BRI}]. Was #{value.inspect}" 112 | end 113 | end 114 | 115 | def ct=(value); set_ct(value) end 116 | def set_ct(value) 117 | if !value.nil? && (!value.is_a? Integer) 118 | raise BulbStateValueTypeException, "Color temperature value has incorrect type. Requires integer, got #{value.class}" 119 | elsif value.nil? || value.between?(MIN_CT,MAX_CT) 120 | @ct = value 121 | else 122 | raise BulbStateValueOutOfRangeException, "Color temperature value out of range. Must be [#{MIN_CT},#{MAX_CT}]. Was #{value.inspect}" 123 | end 124 | end 125 | 126 | def sat=(value); set_sat(value) end 127 | def set_sat(value) 128 | if !value.nil? && (!value.is_a? Integer) 129 | raise BulbStateValueTypeException, "Saturation value has incorrect type. Requires integer, got #{value.class}" 130 | elsif value.nil? || value.between?(MIN_SAT,MAX_SAT) 131 | @sat = value 132 | else 133 | raise BulbStateValueOutOfRangeException, "Saturation alue out of range. Must be [#{MIN_SAT},#{MAX_SAT}]. Was #{value.inspect}" 134 | end 135 | end 136 | 137 | def hue=(value); set_hue(value) end 138 | def set_hue(value) 139 | if !value.nil? && (!value.is_a? Integer) 140 | raise BulbStateValueTypeException, "Hue value has incorrect type. Requires integer, got #{value.class}" 141 | elsif value.nil? || value.between?(MIN_HUE,MAX_HUE) 142 | @hue = value 143 | else 144 | raise BulbStateValueOutOfRangeException, "Hue value out of range. Must be [#{MIN_HUE},#{MAX_HUE}]. Was #{value.inspect}" 145 | end 146 | end 147 | 148 | def transition_time=(value); set_transition_time(value) end 149 | def set_transition_time(value) 150 | if !value.nil? && (!value.is_a? Numeric) 151 | raise BulbStateValueTypeException, "Transition time value has incorrect type. Requires decimal, got #{value.class}" 152 | elsif value.nil? || value >= MIN_TRANSITION_TIME 153 | @transition_time = value 154 | else 155 | raise BulbStateValueOutOfRangeException, "Transition time value out of range. Must be > #{MIN_TRANSITION_TIME}. Was #{value.inspect}" 156 | end 157 | end 158 | 159 | def xy=(value); set_xy(value) end 160 | def set_xy(value) 161 | if !value.nil? && (!value.is_a? Array) 162 | raise BulbStateValueTypeException, "XY value has incorrect type. Requires array, got #{value.class}" 163 | elsif value.nil? 164 | return 165 | elsif value.length == 2 && value[0].is_a?(Numeric) \ 166 | && value[1].is_a?(Numeric) && value[0].to_f >= MIN_XY \ 167 | && value[0].to_f <= MAX_XY && value[1].to_f >= MIN_XY \ 168 | && value[1].to_f <= MAX_XY 169 | @xy = [] 170 | @xy[0] = value[0].to_f 171 | @xy[1] = value[1].to_f 172 | else 173 | raise BulbStateValueOutOfRangeException, "XY value out of range. Must be [#{MIN_XY},#{MAX_XY}]. Was #{value.inspect}" 174 | end 175 | end 176 | 177 | def data 178 | data = {} 179 | data["on"] = @on unless @on.nil? 180 | data["bri"] = @bri if @bri 181 | data["hue"] = @hue if @hue 182 | data["sat"] = @sat if @sat 183 | data["xy"] = @xy if @xy 184 | data["ct"] = @ct if @ct 185 | data["alert"] = @alert if @alert 186 | data["effect"] = @effect if @effect 187 | data["colormode"] = @color_mode if @color_mode 188 | data["reachable"] = @reachable unless @reachable.nil? 189 | data["transitiontime"] = @transition_time if @transition_time 190 | data 191 | end 192 | end 193 | 194 | -------------------------------------------------------------------------------- /spec/datastore_spec.rb: -------------------------------------------------------------------------------- 1 | require 'lights' 2 | 3 | describe Datastore do 4 | it "properly reconstructs sub objects" do 5 | data = JSON.parse(DATASTORE_JSON) 6 | ds = Datastore.new data 7 | ds.data.should eql data 8 | end 9 | 10 | it "properly reconstructs light objects" do 11 | data = JSON.parse(DATASTORE_JSON) 12 | ds = Datastore.new data 13 | ds.lights.data.should eql data["lights"] 14 | end 15 | it "properly reconstructs group objects" do 16 | data = JSON.parse(DATASTORE_JSON) 17 | ds = Datastore.new data 18 | ds.groups.data.should eql data["groups"] 19 | end 20 | it "properly reconstructs rule objects" do 21 | data = JSON.parse(DATASTORE_JSON) 22 | ds = Datastore.new data 23 | ds.rules.data.should eql data["rules"] 24 | end 25 | it "properly reconstructs scene objects" do 26 | data = JSON.parse(DATASTORE_JSON) 27 | ds = Datastore.new data 28 | ds.scenes.data.should eql data["scenes"] 29 | end 30 | it "properly reconstructs schedule objects" do 31 | data = JSON.parse(DATASTORE_JSON) 32 | ds = Datastore.new data 33 | ds.schedules.data.should eql data["schedules"] 34 | end 35 | it "properly reconstructs sensor objects" do 36 | data = JSON.parse(DATASTORE_JSON) 37 | ds = Datastore.new data 38 | ds.sensors.data.should eql data["sensors"] 39 | end 40 | it "properly reconstructs config object" do 41 | data = JSON.parse(DATASTORE_JSON) 42 | ds = Datastore.new data 43 | ds.config.data.should eql data["config"] 44 | end 45 | end 46 | 47 | DATASTORE_JSON = %Q{ 48 | { 49 | "lights": { 50 | "1": { 51 | "state": { 52 | "on": false, 53 | "bri": 254, 54 | "hue": 34495, 55 | "sat": 232, 56 | "effect": "none", 57 | "xy": [ 58 | 0.3151, 59 | 0.3251 60 | ], 61 | "ct": 155, 62 | "alert": "none", 63 | "colormode": "xy", 64 | "reachable": true 65 | }, 66 | "type": "Extended color light", 67 | "name": "Hue Lamp 1", 68 | "modelid": "LCT001", 69 | "uniqueid": "00:17:88:01:00:aa:bb:cb-0b", 70 | "swversion": "66013452", 71 | "pointsymbol": { 72 | "1": "0a00f1f01f1f1001f1ff100000000001f2000000", 73 | "2": "none", 74 | "3": "none", 75 | "4": "none", 76 | "5": "none", 77 | "6": "none", 78 | "7": "none", 79 | "8": "none" 80 | } 81 | }, 82 | "2": { 83 | "state": { 84 | "on": true, 85 | "bri": 254, 86 | "hue": 34495, 87 | "sat": 232, 88 | "effect": "none", 89 | "xy": [ 90 | 0.3151, 91 | 0.3251 92 | ], 93 | "ct": 155, 94 | "alert": "none", 95 | "colormode": "xy", 96 | "reachable": true 97 | }, 98 | "type": "Extended color light", 99 | "name": "Hue Lamp 2", 100 | "modelid": "LCT001", 101 | "uniqueid": "00:17:88:01:00:aa:bb:ca-0b", 102 | "swversion": "66013452", 103 | "pointsymbol": { 104 | "1": "0a00f1f01f1f1001f1ff100000000001f2000000", 105 | "2": "none", 106 | "3": "none", 107 | "4": "none", 108 | "5": "none", 109 | "6": "none", 110 | "7": "none", 111 | "8": "none" 112 | } 113 | }, 114 | "3": { 115 | "state": { 116 | "on": true, 117 | "bri": 254, 118 | "hue": 34495, 119 | "sat": 232, 120 | "effect": "none", 121 | "xy": [ 122 | 0.3151, 123 | 0.3251 124 | ], 125 | "ct": 155, 126 | "alert": "none", 127 | "colormode": "xy", 128 | "reachable": true 129 | }, 130 | "type": "Extended color light", 131 | "name": "Hue Lamp 3", 132 | "modelid": "LCT001", 133 | "uniqueid": "00:17:88:01:00:aa:bb:c9-0b", 134 | "swversion": "66013452", 135 | "pointsymbol": { 136 | "1": "0a00f1f01f1f1001f1ff100000000001f2000000", 137 | "2": "none", 138 | "3": "none", 139 | "4": "none", 140 | "5": "none", 141 | "6": "none", 142 | "7": "none", 143 | "8": "none" 144 | } 145 | }, 146 | "4": { 147 | "state": { 148 | "on": true, 149 | "bri": 254, 150 | "hue": 34495, 151 | "sat": 232, 152 | "effect": "none", 153 | "xy": [ 154 | 0.3151, 155 | 0.3251 156 | ], 157 | "ct": 155, 158 | "alert": "none", 159 | "colormode": "xy", 160 | "reachable": true 161 | }, 162 | "type": "Extended color light", 163 | "name": "Hue Lamp 4", 164 | "modelid": "LCT001", 165 | "uniqueid": "00:17:88:01:00:aa:bb:c8-0b", 166 | "swversion": "66013452", 167 | "pointsymbol": { 168 | "1": "none", 169 | "2": "none", 170 | "3": "none", 171 | "4": "none", 172 | "5": "none", 173 | "6": "none", 174 | "7": "none", 175 | "8": "none" 176 | } 177 | }, 178 | "5": { 179 | "state": { 180 | "on": true, 181 | "bri": 254, 182 | "hue": 34495, 183 | "sat": 232, 184 | "effect": "none", 185 | "xy": [ 186 | 0.3151, 187 | 0.3251 188 | ], 189 | "ct": 155, 190 | "alert": "select", 191 | "colormode": "xy", 192 | "reachable": true 193 | }, 194 | "type": "Extended color light", 195 | "name": "BT Lamp", 196 | "modelid": "LCT001", 197 | "uniqueid": "00:17:88:01:00:aa:bb:c7-0b", 198 | "swversion": "66013452", 199 | "pointsymbol": { 200 | "1": "none", 201 | "2": "none", 202 | "3": "none", 203 | "4": "none", 204 | "5": "none", 205 | "6": "none", 206 | "7": "none", 207 | "8": "none" 208 | } 209 | } 210 | }, 211 | "groups": { 212 | "1": { 213 | "name": "Ceiling light", 214 | "lights": [ 215 | "1", 216 | "2", 217 | "3", 218 | "4" 219 | ], 220 | "type": "LightGroup", 221 | "action": { 222 | "on": true, 223 | "bri": 254, 224 | "hue": 34495, 225 | "sat": 232, 226 | "effect": "none", 227 | "xy": [ 228 | 0.3151, 229 | 0.3251 230 | ], 231 | "ct": 155, 232 | "colormode": "xy" 233 | } 234 | }, 235 | "2": { 236 | "name": "Lamp", 237 | "lights": [ 238 | "5" 239 | ], 240 | "type": "LightGroup", 241 | "action": { 242 | "on": true, 243 | "bri": 254, 244 | "hue": 34495, 245 | "sat": 232, 246 | "effect": "none", 247 | "xy": [ 248 | 0.3151, 249 | 0.3251 250 | ], 251 | "ct": 155, 252 | "colormode": "xy" 253 | } 254 | } 255 | }, 256 | "config": { 257 | "name": "Philips hue", 258 | "zigbeechannel": 25, 259 | "mac": "00:11:22:33:44:55", 260 | "dhcp": false, 261 | "ipaddress": "192.168.1.27", 262 | "netmask": "255.255.255.0", 263 | "gateway": "192.168.1.1", 264 | "proxyaddress": "none", 265 | "proxyport": 0, 266 | "UTC": "2015-01-01T00:11:24", 267 | "localtime": "2014-12-31T19:11:24", 268 | "timezone": "America/New_York", 269 | "whitelist": { 270 | "testuser1": { 271 | "last use date": "2015-01-01T00:11:24", 272 | "create date": "2014-12-30T20:05:07", 273 | "name": "lights" 274 | }, 275 | "test user 2": { 276 | "last use date": "2014-05-26T14:20:54", 277 | "create date": "2013-08-03T17:05:51", 278 | "name": "unofficial hue app for osx" 279 | }, 280 | "test user 3": { 281 | "last use date": "2014-12-31T02:19:57", 282 | "create date": "2013-08-03T18:48:19", 283 | "name": "test user" 284 | } 285 | }, 286 | "swversion": "01018228", 287 | "apiversion": "1.5.0", 288 | "swupdate": { 289 | "updatestate": 0, 290 | "checkforupdate": false, 291 | "devicetypes": { 292 | "bridge": false, 293 | "lights": [ 294 | 295 | ] 296 | }, 297 | "url": "", 298 | "text": "", 299 | "notify": false 300 | }, 301 | "linkbutton": false, 302 | "portalservices": false, 303 | "portalconnection": "connected", 304 | "portalstate": { 305 | "signedon": true, 306 | "incoming": true, 307 | "outgoing": true, 308 | "communication": "disconnected" 309 | } 310 | }, 311 | "schedules": { 312 | "6033895564917352": { 313 | "name": "Alarm", 314 | "description": "Energize", 315 | "command": { 316 | "address": "/api/8Wc85V5IrG0XTHfB/groups/0/action", 317 | "body": { 318 | "scene": "d942060b6-on-5" 319 | }, 320 | "method": "PUT" 321 | }, 322 | "localtime": "W124/T06:40:00", 323 | "time": "W124/T11:40:00", 324 | "created": "2014-09-18T02:54:16", 325 | "status": "disabled" 326 | }, 327 | "0326522980291031": { 328 | "name": "Alarm", 329 | "description": "", 330 | "command": { 331 | "address": "/api/8Wc85V5IrG0XTHfB/groups/0/action", 332 | "body": { 333 | "scene": "19a493ad2-off-0" 334 | }, 335 | "method": "PUT" 336 | }, 337 | "localtime": "W124/T07:45:00", 338 | "time": "W124/T12:45:00", 339 | "created": "2014-09-09T01:13:57", 340 | "status": "enabled" 341 | } 342 | }, 343 | "scenes": { 344 | "fd0ceaedb-on-0": { 345 | "name": "Purple and Green", 346 | "lights": [ 347 | "1", 348 | "2", 349 | "3", 350 | "4", 351 | "5" 352 | ], 353 | "active": false 354 | }, 355 | "da01b1eaf-on-0": { 356 | "name": "Purple on 0", 357 | "lights": [ 358 | "1", 359 | "2", 360 | "3", 361 | "4", 362 | "5" 363 | ], 364 | "active": true 365 | }, 366 | "182a404bb-on-0": { 367 | "name": "Deck flower on 0", 368 | "lights": [ 369 | "1", 370 | "2", 371 | "3", 372 | "4", 373 | "5" 374 | ], 375 | "active": true 376 | }, 377 | "8fc639a85f-on-0": { 378 | "name": "Reading on 0", 379 | "lights": [ 380 | "5" 381 | ], 382 | "active": true 383 | }, 384 | "fedd8e5db-on-0": { 385 | "name": "Dark red on 0", 386 | "lights": [ 387 | "1", 388 | "2", 389 | "3", 390 | "4", 391 | "5" 392 | ], 393 | "active": true 394 | } 395 | }, 396 | "rules": { 397 | "1": { 398 | "name": "Tap 2.3 Reading", 399 | "owner": "8Wc85V5IrG0XTHfB", 400 | "created": "2014-09-17T23:34:25", 401 | "lasttriggered": "2014-12-31T03:29:37", 402 | "timestriggered": 10, 403 | "status": "enabled", 404 | "conditions": [ 405 | { 406 | "address": "/sensors/2/state/buttonevent", 407 | "operator": "eq", 408 | "value": "17" 409 | }, 410 | { 411 | "address": "/sensors/2/state/lastupdated", 412 | "operator": "dx" 413 | } 414 | ], 415 | "actions": [ 416 | { 417 | "address": "/groups/0/action", 418 | "method": "PUT", 419 | "body": { 420 | "scene": "dc8a6e2b5-on-0" 421 | } 422 | } 423 | ] 424 | }, 425 | "2": { 426 | "name": "Tap 2.1 All lights off", 427 | "owner": "8Wc85V5IrG0XTHfB", 428 | "created": "2014-07-16T20:43:43", 429 | "lasttriggered": "2014-12-31T06:29:46", 430 | "timestriggered": 12, 431 | "status": "enabled", 432 | "conditions": [ 433 | { 434 | "address": "/sensors/2/state/buttonevent", 435 | "operator": "eq", 436 | "value": "34" 437 | }, 438 | { 439 | "address": "/sensors/2/state/lastupdated", 440 | "operator": "dx" 441 | } 442 | ], 443 | "actions": [ 444 | { 445 | "address": "/groups/0/action", 446 | "method": "PUT", 447 | "body": { 448 | "scene": "19a493ad2-off-0" 449 | } 450 | } 451 | ] 452 | }, 453 | "3": { 454 | "name": "Tap 2.4 Purple", 455 | "owner": "8Wc85V5IrG0XTHfB", 456 | "created": "2014-07-19T16:32:31", 457 | "lasttriggered": "none", 458 | "timestriggered": 0, 459 | "status": "enabled", 460 | "conditions": [ 461 | { 462 | "address": "/sensors/2/state/buttonevent", 463 | "operator": "eq", 464 | "value": "18" 465 | }, 466 | { 467 | "address": "/sensors/2/state/lastupdated", 468 | "operator": "dx" 469 | } 470 | ], 471 | "actions": [ 472 | { 473 | "address": "/groups/0/action", 474 | "method": "PUT", 475 | "body": { 476 | "scene": "da01b1eaf-on-0" 477 | } 478 | } 479 | ] 480 | }, 481 | "4": { 482 | "name": "Tap 2.2 Energize", 483 | "owner": "8Wc85V5IrG0XTHfB", 484 | "created": "2014-07-16T20:43:43", 485 | "lasttriggered": "2014-12-31T21:42:04", 486 | "timestriggered": 9, 487 | "status": "enabled", 488 | "conditions": [ 489 | { 490 | "address": "/sensors/2/state/buttonevent", 491 | "operator": "eq", 492 | "value": "16" 493 | }, 494 | { 495 | "address": "/sensors/2/state/lastupdated", 496 | "operator": "dx" 497 | } 498 | ], 499 | "actions": [ 500 | { 501 | "address": "/groups/0/action", 502 | "method": "PUT", 503 | "body": { 504 | "scene": "d942060b6-on-0" 505 | } 506 | } 507 | ] 508 | } 509 | }, 510 | "sensors": { 511 | "1": { 512 | "state": { 513 | "daylight": false, 514 | "lastupdated": "none" 515 | }, 516 | "config": { 517 | "on": true, 518 | "long": "none", 519 | "lat": "none", 520 | "sunriseoffset": 30, 521 | "sunsetoffset": -30 522 | }, 523 | "name": "Daylight", 524 | "type": "Daylight", 525 | "modelid": "PHDL00", 526 | "manufacturername": "Philips", 527 | "swversion": "1.0" 528 | }, 529 | "2": { 530 | "state": { 531 | "buttonevent": 16, 532 | "lastupdated": "2014-12-31T21:42:04" 533 | }, 534 | "config": { 535 | "on": true 536 | }, 537 | "name": "Hue Tap 1", 538 | "type": "ZGPSwitch", 539 | "modelid": "ZGPSWITCH", 540 | "manufacturername": "Philips", 541 | "uniqueid": "00:00:00:00:00:aa:bb:cc-f2" 542 | } 543 | } 544 | } 545 | } 546 | -------------------------------------------------------------------------------- /bin/lights: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'lights' 4 | require 'optparse' 5 | require 'simpletable' 6 | 7 | LIGHTS_CONFIG_PATH = "#{ENV["HOME"]}/.lightsconfig" 8 | 9 | class LightsCli 10 | 11 | def initialize 12 | @config = {} 13 | if File.exists? LIGHTS_CONFIG_PATH 14 | @config = JSON.parse( IO.read( LIGHTS_CONFIG_PATH ) ) 15 | end 16 | end 17 | 18 | def configured? 19 | @config["username"] && @config["bridge_ip"] 20 | end 21 | 22 | def ip_configured? 23 | @config["bridge_ip"] 24 | end 25 | 26 | def config 27 | options = {} 28 | OptionParser.new do |opts| 29 | opts.on("-i", "--ip ", String, "Bridge ip address"){|ip| options[:ip]=ip} 30 | opts.on("-f", "--force", "Force write to config file"){|f| options[:force]=f} 31 | opts.on("-l", "--list", "List saved configuration settings") {|l| options[:list]=l} 32 | end.parse! 33 | 34 | unless options[:ip] || options[:list] 35 | puts "usage:" 36 | puts " #{File.basename(__FILE__)} config (--ip | --list)" 37 | exit 1 38 | end 39 | 40 | if options[:list] 41 | @config.each { |k,v| puts "#{k}: #{v}" } 42 | else 43 | if !options[:force] && File.exists?(LIGHTS_CONFIG_PATH) 44 | overwrite = "" 45 | while overwrite[0] != "y" \ 46 | && overwrite[0] != "Y" \ 47 | && overwrite[0] != "n" \ 48 | && overwrite[0] != "N" \ 49 | && overwrite[0] != "\n" 50 | print "Lights config already exists. Overwrite? [Y/n]: " 51 | overwrite = STDIN.gets 52 | end 53 | overwrite.upcase! 54 | if overwrite[0] == "N" 55 | exit 56 | end 57 | end 58 | 59 | @config["bridge_ip"] = options[:ip] || @config["bridge_ip"] 60 | 61 | write_config 62 | puts "Configuration settings saved." 63 | end 64 | end 65 | 66 | def create 67 | type = ARGV.shift 68 | hue = Lights.new @config["bridge_ip"], @config["username"] 69 | options = {} 70 | 71 | case type 72 | when "scene" 73 | OptionParser.new do |opts| 74 | opts.on("-l", "--lights 1,2,...N", Array, "Which lights are in the scene"){|l| options[:lights]=l} 75 | opts.on("-n", "--name ", String, "Set scene name"){|n| options[:name]=n} 76 | opts.on("-r", "--recycle", "Can bridge recycle scene"){|r| options[:recycle]=r} 77 | opts.on("-d", "--duration seconds", OptionParser::DecimalInteger, "Transition duration in seconds"){|d| options[:duration]=d} 78 | end.parse! 79 | 80 | if !options[:lights] 81 | puts "Must specify which lights are in the scene." 82 | exit 1 83 | end 84 | if !options[:name] 85 | puts "Must specify scene name." 86 | exit 1 87 | end 88 | 89 | s = Scene.new 90 | s.name = options[:name] 91 | s.lights = options[:lights] 92 | s.recycle = options[:recycle] || false 93 | s.transition_time = options[:duration] 94 | hue.create_scene s 95 | when "group" 96 | OptionParser.new do |opts| 97 | opts.on("-l", "--lights 1,2,...N", Array, "Which lights to put in group"){|l| options[:lights]=l} 98 | opts.on("-n", "--name ", String, "Set group name"){|n| options[:name]=n} 99 | end.parse! 100 | 101 | if !options[:lights] 102 | puts "Must specify which lights to group." 103 | exit 1 104 | end 105 | if !options[:name] 106 | puts "Must specify group name." 107 | exit 1 108 | end 109 | 110 | group = Group.new 111 | group.name = options[:name] 112 | group.lights = options[:lights] 113 | hue.create_group(group) 114 | when nil 115 | STDERR.puts "Must specify a type to create." 116 | else 117 | STDERR.puts "Don't know how to create type \"#{type}\"." 118 | end 119 | end 120 | 121 | def edit 122 | type = ARGV.shift 123 | hue = Lights.new @config["bridge_ip"], @config["username"] 124 | options = {} 125 | 126 | case type 127 | when "light" 128 | id = ARGV.shift 129 | if !id 130 | puts "Must specify light id." 131 | exit 1 132 | end 133 | 134 | OptionParser.new do |opts| 135 | opts.on("--name", "--name ", String, "Set light name"){|n| options[:name]=n} 136 | end.parse! 137 | if !options[:name] 138 | puts "Must specify new light name." 139 | exit 1 140 | end 141 | 142 | light = Bulb.new(id,{"name"=>options[:name]}) 143 | hue.edit_bulb light 144 | when "group" 145 | id = ARGV.shift 146 | if !id 147 | puts "Must specify group id." 148 | exit 1 149 | end 150 | 151 | OptionParser.new do |opts| 152 | opts.on("--name", "--name ", String, "Set light name"){|n| options[:name]=n} 153 | opts.on("-l", "--lights 1,2,...N", Array, "Which lights to put in group"){|l| options[:lights]=l} 154 | end.parse! 155 | if !options[:name] && !options[:lights] 156 | puts "Must specify a value to edit." 157 | exit 1 158 | end 159 | group = Group.new(id) 160 | group.name = options[:name] if options[:name] 161 | group.lights = options[:lights] if options[:lights] 162 | hue.edit_group group 163 | when nil 164 | STDERR.puts "Must specify a type to edit." 165 | else 166 | STDERR.puts "Don't know how to edit type \"#{type}\"." 167 | end 168 | end 169 | 170 | def delete 171 | type = ARGV.shift 172 | hue = Lights.new @config["bridge_ip"], @config["username"] 173 | 174 | case type 175 | when "group" 176 | id = ARGV.shift 177 | 178 | if !id 179 | puts "Must specify group id." 180 | exit 1 181 | end 182 | 183 | hue.delete_group id 184 | when "user" 185 | id = ARGV.shift 186 | 187 | if !id 188 | puts "Must specify user id." 189 | exit 1 190 | end 191 | 192 | hue.delete_user id 193 | when "scene" 194 | id = ARGV.shift 195 | 196 | if !id 197 | puts "Must specify scene id." 198 | exit 1 199 | end 200 | 201 | hue.delete_scene id 202 | when nil 203 | STDERR.puts "Must specify a type to delete." 204 | else 205 | STDERR.puts "Don't know how to delete type \"#{type}\"." 206 | end 207 | end 208 | 209 | def list 210 | hue = Lights.new @config["bridge_ip"], @config["username"] 211 | titles = [] 212 | methods = [] 213 | 214 | options = {} 215 | OptionParser.new do |opts| 216 | opts.on("-j", "--json", "Print JSON response"){|j| options[:json] = j} 217 | opts.on("-n", "--new", "Only list new"){|n| options[:new] = n} 218 | end.parse! 219 | 220 | type = ARGV.shift 221 | case type 222 | when nil, "","lights" 223 | response = options[:new] ? hue.request_new_bulb_list : hue.request_bulb_list 224 | objects = BulbList.new(response) 225 | titles = ["ID","NAME","REACHABLE?"] 226 | methods = [:id,:name,[:state,:reachable]] 227 | when "sensors" 228 | response = options[:new] ? hue.request_new_sensor_list : hue.request_sensor_list 229 | objects = SensorList.new(response) 230 | titles = ["ID","NAME"] 231 | methods = [:id,:name] 232 | when "groups" 233 | response = hue.request_group_list 234 | objects = GroupList.new(response) 235 | titles = ["ID","NAME","LIGHTS"] 236 | methods = [:id,:name,:lights] 237 | when "scenes" 238 | response = hue.request_scene_list 239 | objects = SceneList.new(response) 240 | titles = ["ID","NAME","LIGHTS","RECYCLE"] 241 | methods = [:id,:name,:lights,:recycle] 242 | when "users" 243 | response = hue.request_config 244 | objects = UserList.new(response["whitelist"]) 245 | titles = ["ID","NAME","CREATE DATE","LAST USE DATE"] 246 | methods = [:id,:name,:create_date,:last_use_date] 247 | when "rules" 248 | response = hue.request_rules 249 | objects = RuleList.new(response) 250 | titles = ["ID","NAME"] 251 | methods = [:id,:name] 252 | when "schedules" 253 | response = hue.request_schedules 254 | objects = ScheduleList.new(response) 255 | titles = ["ID","NAME","TIME","SCENE","STATUS"] 256 | methods = [:id,:name,:time,:scene,:status] 257 | when "datastore" 258 | response = hue.request_datastore 259 | objects = Datastore.new(response) 260 | titles = ["TYPE","ID","NAME"] 261 | methods = [:class,:id,:name] 262 | else 263 | puts "Don't know how to list type \"#{type}\"." 264 | return 265 | end 266 | if options[:json] 267 | puts JSON.pretty_generate objects.data 268 | else 269 | puts SimpleTable.new.from_objects(objects.list,titles,methods).text 270 | end 271 | end 272 | 273 | def register 274 | lights = Lights.new @config["bridge_ip"], @config["username"] 275 | response = lights.register 276 | @config["username"] = lights.username 277 | write_config 278 | end 279 | 280 | def discover 281 | options = {} 282 | OptionParser.new do |opts| 283 | opts.on("-s", "--save", "Save discovered bridge to configuration file"){|s| options[:save]=s} 284 | end.parse! 285 | 286 | lights = Lights.new @config["bridge_ip"], @config["username"] 287 | bridges = lights.discover_hubs 288 | bridges.each_with_index { |b,i| puts "[#{i+1}] #{b.name}: #{b.ip}" } 289 | 290 | if options[:save] && bridges.length >= 1 291 | if bridges.length > 1 292 | which_bridge = -1 293 | while !(which_bridge >=0 && which_bridge <= bridges.length) 294 | print "Which bridge would you like to save? (0 for none): " 295 | which_bridge = Integer( gets ) rescue -1 296 | end 297 | else 298 | which_bridge = 1 299 | end 300 | if which_bridge != 0 301 | @config["bridge_ip"] = bridges[which_bridge.to_i-1].ip 302 | write_config 303 | puts "Discovered bridge IP saved: #{bridges[which_bridge-1].ip}" 304 | end 305 | elsif bridges.length == 0 306 | puts "Did not discover any bridges." 307 | end 308 | end 309 | 310 | def on 311 | on_off true 312 | end 313 | 314 | def off 315 | on_off false 316 | end 317 | 318 | def set 319 | options = {} 320 | OptionParser.new do |opts| 321 | opts.on("-o", "--on", "Turn lights on"){|o| options[:on]=o} 322 | opts.on("-f", "--off", "Turn lights off"){|f| options[:off]=f} 323 | opts.on("-c", "--color ", String, "Set color"){|c| options[:color]=c} 324 | opts.on("-t", "--ct color_temp", OptionParser::DecimalInteger, "Set color temperature"){|c| options[:ct]=c} 325 | opts.on("-b", "--brightness brightness", OptionParser::DecimalInteger, "Set brightness"){|b| options[:brightness]=b} 326 | opts.on("-s", "--saturation saturation", OptionParser::DecimalInteger, "Set saturation"){|s| options[:saturation]=s} 327 | opts.on("-h", "--hue hue", OptionParser::DecimalInteger, "Set hue"){|h| options[:hue]=h} 328 | opts.on("-e", "--effect none|colorloop", String, "Set effect"){|e| options[:effect]=e} 329 | opts.on("-a", "--alert none|select|lselect", String, "Set alert"){|a| options[:alert]=a} 330 | opts.on("-z", "--xy x,y", Array, "Set xy"){|z| options[:xy]=z} 331 | opts.on("-l", "--lights 1,2,...N", Array, "Which lights to control"){|l| options[:lights]=l} 332 | opts.on("-g", "--groups 1,2,...N", Array, "Which groups to control"){|g| options[:groups]=g} 333 | opts.on("-S", "--scene ", String, "Which scene to recall"){|s| options[:scene]=s} 334 | opts.on("-d", "--duration seconds", OptionParser::DecimalInteger, "Transition duration in seconds"){|d| options[:duration]=d} 335 | end.parse! 336 | 337 | bad_args = false 338 | if !options[:on] && !options[:off] \ 339 | && !options[:ct] && !options[:brightness] \ 340 | && !options[:hue] && !options[:saturation] \ 341 | && !options[:effect] && !options[:alert] \ 342 | && !options[:xy] && !options[:color] \ 343 | && !options[:scene] 344 | puts "Must specify a state to set." 345 | bad_args = true 346 | end 347 | if (options[:hue] || options[:saturation]) \ 348 | && options[:ct] 349 | puts "Cannot set both color temperature and hue/saturation." 350 | bad_args = true 351 | end 352 | exit 1 if bad_args 353 | 354 | s = GroupState.new 355 | if options[:on] 356 | s.on = true 357 | elsif options[:off] 358 | s.on = false 359 | end 360 | if options[:color] 361 | begin 362 | s.hue = BulbState::Hue.const_get options[:color].upcase 363 | rescue NameError 364 | puts "Unrecognized color: #{options[:color]}" 365 | exit 1 366 | end 367 | end 368 | if options[:ct] 369 | s.ct = options[:ct] 370 | end 371 | if options[:brightness] 372 | s.bri = options[:brightness] 373 | end 374 | if options[:saturation] 375 | s.sat = options[:saturation] 376 | end 377 | if options[:hue] 378 | s.hue = options[:hue] 379 | end 380 | if options[:effect] 381 | s.effect = options[:effect] 382 | end 383 | if options[:duration] 384 | s.transition_time = options[:duration] * 10 385 | end 386 | if options[:alert] 387 | s.alert = options[:alert] 388 | end 389 | if options[:xy] 390 | s.xy = options[:xy] 391 | end 392 | if options[:scene] 393 | s.scene = options[:scene] 394 | end 395 | 396 | if options[:lights] 397 | set_bulb_state(s,options[:lights]) 398 | elsif options[:groups] 399 | set_group_state(s,options[:groups]) 400 | else 401 | set_group_state(s,[0]) 402 | end 403 | end 404 | 405 | def search 406 | lights = Lights.new @config["bridge_ip"], @config["username"] 407 | response = lights.search_new 408 | if response.first["success"] 409 | puts "Started search." 410 | else 411 | puts "Unknown error. Did not begin search." 412 | end 413 | end 414 | 415 | private 416 | def set_bulb_state(state,bulbs) 417 | lights = Lights.new @config["bridge_ip"], @config["username"] 418 | if bulbs.first == "all" 419 | lights.set_group_state 0,state 420 | else 421 | bulbs.each { |l| lights.set_bulb_state(l,state) } 422 | end 423 | end 424 | 425 | def set_group_state(state,groups) 426 | hue = Lights.new @config["bridge_ip"], @config["username"] 427 | groups.each { |g| hue.set_group_state(g,state) } 428 | end 429 | 430 | def write_config 431 | File.open(LIGHTS_CONFIG_PATH,"w+") { |file| file.write(@config.to_json) } 432 | end 433 | 434 | def on_off(is_on) 435 | options = {} 436 | OptionParser.new do |opts| 437 | opts.on("-l", "--lights 1,2,...N", Array, "Which lights to control"){|l| options[:lights]=l} 438 | end.parse! 439 | 440 | options[:lights] = ["all"] if !options[:lights] 441 | 442 | s = BulbState.new 443 | s.on = is_on 444 | set_bulb_state s, options[:lights] 445 | end 446 | end 447 | 448 | 449 | if !ARGV[0] 450 | STDERR.puts "Must specify a command. (config, list, register, discover, search, on, off, set, create, edit, delete)" 451 | exit 1 452 | end 453 | 454 | begin 455 | cli = LightsCli.new 456 | command = ARGV.shift 457 | if command == "config" 458 | cli.config 459 | elsif command == "discover" 460 | cli.discover 461 | elsif !cli.ip_configured? 462 | puts "Please run 'lights discover -s' or 'lights config --ip ' before using." 463 | elsif command == "register" 464 | cli.register 465 | elsif !cli.configured? 466 | puts "Please run 'lights register' before using." 467 | elsif command == "list" 468 | cli.list 469 | elsif command == "on" 470 | cli.on 471 | elsif command == "off" 472 | cli.off 473 | elsif command == "set" 474 | cli.set 475 | elsif command == "create" 476 | cli.create 477 | elsif command == "delete" 478 | cli.delete 479 | elsif command == "edit" 480 | cli.edit 481 | elsif command == "search" 482 | cli.search 483 | else 484 | puts "Cannot find command #{command}." 485 | end 486 | rescue BridgeConnectException, 487 | UsernameException, 488 | ResourceUnavailableException, 489 | ParameterUnavailableException, 490 | BulbStateValueOutOfRangeException, 491 | BulbStateValueTypeException => e 492 | puts e.message 493 | rescue Errno::ENETUNREACH, Errno::ENETDOWN 494 | puts "Please check your internet connection and try again." 495 | rescue Interrupt 496 | puts "" 497 | end 498 | 499 | 500 | -------------------------------------------------------------------------------- /spec/bulbstate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'lights' 2 | 3 | describe BulbState do 4 | 5 | it "properly reconstructs object hash" do 6 | data = { "on" => true } 7 | state = BulbState.new(data) 8 | state.data.should eql data 9 | end 10 | 11 | it "properly reconstructs object hash" do 12 | data = { "on" => false } 13 | state = BulbState.new(data) 14 | state.data.should eql data 15 | end 16 | 17 | # ON 18 | it "should properly set on value in constructor" do 19 | data = { "on" => true } 20 | bulb = BulbState.new(data) 21 | bulb.on.should eql true 22 | bulb.data["on"].should eql true 23 | end 24 | it "should properly set on value" do 25 | b = BulbState.new 26 | b.on = true 27 | b.on.should eq true 28 | b.data["on"].should eq true 29 | end 30 | it "should raise exception when on has invalid type" do 31 | b = BulbState.new 32 | expect { b.on = "test state" }.to raise_error 33 | end 34 | 35 | # BRI 36 | it "should properly set brightness value in constructor" do 37 | data = { "bri" => BulbState::MAX_BRI } 38 | b = BulbState.new(data) 39 | b.bri.should eq BulbState::MAX_BRI 40 | b.data["bri"].should eq BulbState::MAX_BRI 41 | end 42 | 43 | it "should properly set brightness value in constructor" do 44 | data = { "bri" => BulbState::MIN_BRI } 45 | b = BulbState.new(data) 46 | b.bri.should eq BulbState::MIN_BRI 47 | b.data["bri"].should eq BulbState::MIN_BRI 48 | end 49 | 50 | it "should properly set brightness value" do 51 | b = BulbState.new 52 | b.bri = BulbState::MAX_BRI 53 | b.bri.should eq BulbState::MAX_BRI 54 | b.data["bri"].should eq BulbState::MAX_BRI 55 | end 56 | 57 | it "should properly set brightness value" do 58 | b = BulbState.new 59 | b.bri = BulbState::MIN_BRI 60 | b.bri.should eq BulbState::MIN_BRI 61 | b.data["bri"].should eq BulbState::MIN_BRI 62 | end 63 | 64 | it "should raise exception when brightness value is not an integer" do 65 | data = { "bri" => "test value" } 66 | expect { BulbState.new(data) }.to raise_error 67 | end 68 | 69 | it "should raise exception when initial brightness is out of range (high)" do 70 | data = { "bri" => BulbState::MAX_BRI + 1 } 71 | expect { BulbState.new(data) }.to raise_error 72 | end 73 | 74 | it "should raise exception when initial brightness is out of range (low)" do 75 | data = { "bri" => BulbState::MIN_BRI - 1 } 76 | expect { BulbState.new(data) }.to raise_error 77 | end 78 | 79 | it "should raise exception when set brightness is out of range (high)" do 80 | b = BulbState.new() 81 | expect { b.bri = BulbState::MAX_BRI + 1 }.to raise_error 82 | end 83 | 84 | it "should raise exception when set brightness is out of range (LOW)" do 85 | b = BulbState.new() 86 | expect { b.bri = BulbState::MIN_BRI - 1 }.to raise_error 87 | end 88 | 89 | # SAT 90 | it "should properly set saturation value in constructor" do 91 | data = { "sat" => BulbState::MAX_SAT } 92 | b = BulbState.new(data) 93 | b.sat.should eq BulbState::MAX_SAT 94 | b.data["sat"].should eq BulbState::MAX_SAT 95 | end 96 | 97 | it "should properly set saturation value in constructor" do 98 | data = { "sat" => BulbState::MIN_SAT } 99 | b = BulbState.new(data) 100 | b.sat.should eq BulbState::MIN_SAT 101 | b.data["sat"].should eq BulbState::MIN_SAT 102 | end 103 | 104 | it "should properly set saturation value" do 105 | b = BulbState.new 106 | b.sat = BulbState::MAX_SAT 107 | b.sat.should eq BulbState::MAX_SAT 108 | b.data["sat"].should eq BulbState::MAX_SAT 109 | end 110 | 111 | it "should properly set saturation value" do 112 | b = BulbState.new 113 | b.sat = BulbState::MIN_SAT 114 | b.sat.should eq BulbState::MIN_SAT 115 | b.data["sat"].should eq BulbState::MIN_SAT 116 | end 117 | 118 | it "should raise exception when sat value is not an integer" do 119 | data = { "sat" => "test value" } 120 | expect { BulbState.new(data) }.to raise_error 121 | end 122 | 123 | it "should raise exception when initial saturation is out of range (high)" do 124 | data = { "sat" => BulbState::MAX_SAT + 1 } 125 | expect { BulbState.new(data) }.to raise_error 126 | end 127 | 128 | it "should raise exception when initial saturation is out of range (low)" do 129 | data = { "sat" => BulbState::MIN_SAT - 1 } 130 | expect { BulbState.new(data) }.to raise_error 131 | end 132 | 133 | it "should raise exception when set saturation is out of range (high)" do 134 | b = BulbState.new() 135 | expect { b.sat = BulbState::MAX_SAT + 1 }.to raise_error 136 | end 137 | 138 | it "should raise exception when set saturation is out of range (LOW)" do 139 | b = BulbState.new() 140 | expect { b.sat = BulbState::MIN_SAT - 1 }.to raise_error 141 | end 142 | 143 | # HUE 144 | it "should properly set hue value in constructor" do 145 | data = { "hue" => BulbState::MAX_HUE } 146 | b = BulbState.new(data) 147 | b.hue.should eq BulbState::MAX_HUE 148 | b.data["hue"].should eq BulbState::MAX_HUE 149 | end 150 | 151 | it "should properly set hue value in constructor" do 152 | data = { "hue" => BulbState::MIN_HUE } 153 | b = BulbState.new(data) 154 | b.hue.should eq BulbState::MIN_HUE 155 | b.data["hue"].should eq BulbState::MIN_HUE 156 | end 157 | 158 | it "should properly set hue value" do 159 | b = BulbState.new 160 | b.hue = BulbState::MAX_HUE 161 | b.hue.should eq BulbState::MAX_HUE 162 | b.data["hue"].should eq BulbState::MAX_HUE 163 | end 164 | 165 | it "should properly set hue value" do 166 | b = BulbState.new 167 | b.hue = BulbState::MIN_HUE 168 | b.hue.should eq BulbState::MIN_HUE 169 | b.data["hue"].should eq BulbState::MIN_HUE 170 | end 171 | 172 | it "should raise exception when hue value is not an integer" do 173 | data = { "hue" => "test value" } 174 | expect { BulbState.new(data) }.to raise_error 175 | end 176 | 177 | it "should raise exception when initial hue is out of range (high)" do 178 | data = { "hue" => BulbState::MAX_HUE + 1 } 179 | expect { BulbState.new(data) }.to raise_error 180 | end 181 | 182 | it "should raise exception when initial hue is out of range (low)" do 183 | data = { "hue" => BulbState::MIN_HUE - 1 } 184 | expect { BulbState.new(data) }.to raise_error 185 | end 186 | 187 | it "should raise exception when set hue is out of range (high)" do 188 | b = BulbState.new() 189 | expect { b.hue = BulbState::MAX_HUE + 1 }.to raise_error 190 | end 191 | 192 | it "should raise exception when set hue is out of range (LOW)" do 193 | b = BulbState.new() 194 | expect { b.hue = BulbState::MIN_HUE - 1 }.to raise_error 195 | end 196 | 197 | # CT 198 | it "should properly set color temperature value in constructor" do 199 | data = { "ct" => BulbState::MAX_CT } 200 | b = BulbState.new(data) 201 | b.ct.should eq BulbState::MAX_CT 202 | b.data["ct"].should eq BulbState::MAX_CT 203 | end 204 | 205 | it "should properly set color temperature value in constructor" do 206 | data = { "ct" => BulbState::MIN_CT } 207 | b = BulbState.new(data) 208 | b.ct.should eq BulbState::MIN_CT 209 | b.data["ct"].should eq BulbState::MIN_CT 210 | end 211 | 212 | it "should properly set color temperature value" do 213 | b = BulbState.new 214 | b.ct = BulbState::MAX_CT 215 | b.ct.should eq BulbState::MAX_CT 216 | b.data["ct"].should eq BulbState::MAX_CT 217 | end 218 | 219 | it "should properly set color temperature value" do 220 | b = BulbState.new 221 | b.ct = BulbState::MIN_CT 222 | b.ct.should eq BulbState::MIN_CT 223 | b.data["ct"].should eq BulbState::MIN_CT 224 | end 225 | 226 | it "should raise exception when color temperature value is not an integer" do 227 | data = { "ct" => "test value" } 228 | expect { BulbState.new(data) }.to raise_error 229 | end 230 | 231 | it "should raise exception when initial color temperature is out of range (high)" do 232 | data = { "ct" => BulbState::MAX_CT + 1 } 233 | expect { BulbState.new(data) }.to raise_error 234 | end 235 | 236 | it "should raise exception when initial color temperature is out of range (low)" do 237 | data = { "ct" => BulbState::MIN_CT - 1 } 238 | expect { BulbState.new(data) }.to raise_error 239 | end 240 | 241 | it "should raise exception when set color temperature is out of range (high)" do 242 | b = BulbState.new() 243 | expect { b.ct = BulbState::MAX_CT + 1 }.to raise_error 244 | end 245 | 246 | it "should raise exception when set color temperature is out of range (LOW)" do 247 | b = BulbState.new() 248 | expect { b.ct = BulbState::MIN_CT - 1 }.to raise_error 249 | end 250 | 251 | it "should ignore out of range color temperature when 'reachable'= false" do 252 | state = { 253 | "reachable" => false, 254 | "ct" => 0 255 | } 256 | b = BulbState.new state 257 | b.data["ct"].should eq nil 258 | b.data["reachable"].should eq false 259 | end 260 | 261 | # EFFECT 262 | it "should properly set effect value in constructor" do 263 | data = { "effect" => BulbState::Effect::COLORLOOP } 264 | b = BulbState.new(data) 265 | b.effect.should eq BulbState::Effect::COLORLOOP 266 | b.data["effect"].should eq BulbState::Effect::COLORLOOP 267 | end 268 | 269 | it "should properly set effect value in constructor" do 270 | data = { "effect" => BulbState::Effect::COLORLOOP } 271 | b = BulbState.new(data) 272 | b.effect.should eq BulbState::Effect::COLORLOOP 273 | b.data["effect"].should eq BulbState::Effect::COLORLOOP 274 | end 275 | 276 | it "should properly set effect value" do 277 | b = BulbState.new 278 | b.effect = BulbState::Effect::COLORLOOP 279 | b.effect.should eq BulbState::Effect::COLORLOOP 280 | b.data["effect"].should eq BulbState::Effect::COLORLOOP 281 | end 282 | 283 | it "should properly set effect value" do 284 | b = BulbState.new 285 | b.effect = BulbState::Effect::NONE 286 | b.effect.should eq BulbState::Effect::NONE 287 | b.data["effect"].should eq BulbState::Effect::NONE 288 | end 289 | 290 | it "should raise exception when effect value is invalid" do 291 | data = { "effect" => "test value" } 292 | expect { BulbState.new(data) }.to raise_error 293 | end 294 | 295 | # TRANSITION TIME 296 | it "should properly set transition time value in constructor" do 297 | data = { "transitiontime" => 0.1 } 298 | b = BulbState.new(data) 299 | b.transition_time.should eq 0.1 300 | b.data["transitiontime"].should eq 0.1 301 | end 302 | 303 | it "should properly set transition time value" do 304 | b = BulbState.new 305 | b.transition_time = BulbState::MIN_TRANSITION_TIME 306 | b.transition_time.should eq BulbState::MIN_TRANSITION_TIME 307 | b.data["transitiontime"].should eq BulbState::MIN_TRANSITION_TIME 308 | end 309 | 310 | it "should raise exception when transition time value is invalid" do 311 | data = { "transitiontime" => "test value" } 312 | expect { BulbState.new(data) }.to raise_error 313 | end 314 | 315 | it "should raise exception when transition time is out of range (LOW)" do 316 | data = { "transitiontime" => -1 } 317 | expect { BulbState.new(data) }.to raise_error 318 | end 319 | 320 | # ALERT 321 | it "should properly set alert value in constructor" do 322 | data = { "alert" => BulbState::Alert::SELECT } 323 | b = BulbState.new(data) 324 | b.alert.should eq BulbState::Alert::SELECT 325 | b.data["alert"].should eq BulbState::Alert::SELECT 326 | end 327 | 328 | it "should properly set alert value" do 329 | b = BulbState.new 330 | b.alert = BulbState::Alert::LSELECT 331 | b.alert.should eq BulbState::Alert::LSELECT 332 | b.data["alert"].should eq BulbState::Alert::LSELECT 333 | end 334 | 335 | it "should properly set alert value" do 336 | b = BulbState.new 337 | b.alert = BulbState::Alert::NONE 338 | b.alert.should eq BulbState::Alert::NONE 339 | b.data["alert"].should eq BulbState::Alert::NONE 340 | end 341 | 342 | it "should raise exception when alert value is invalid" do 343 | data = { "alert" => "test value" } 344 | expect { BulbState.new(data) }.to raise_error 345 | end 346 | 347 | # COLORMODE 348 | it "should properly set color mode value in constructor" do 349 | data = { "colormode" => BulbState::ColorMode::HS } 350 | b = BulbState.new(data) 351 | b.color_mode.should eq BulbState::ColorMode::HS 352 | b.data["colormode"].should eq BulbState::ColorMode::HS 353 | end 354 | 355 | it "should properly set color mode value" do 356 | b = BulbState.new 357 | b.color_mode = BulbState::ColorMode::XY 358 | b.color_mode.should eq BulbState::ColorMode::XY 359 | b.data["colormode"].should eq BulbState::ColorMode::XY 360 | end 361 | 362 | it "should properly set color mode value" do 363 | b = BulbState.new 364 | b.color_mode = BulbState::ColorMode::CT 365 | b.color_mode.should eq BulbState::ColorMode::CT 366 | b.data["colormode"].should eq BulbState::ColorMode::CT 367 | end 368 | 369 | it "should raise exception when alert value is invalid" do 370 | data = { "colormode" => "test value" } 371 | expect { BulbState.new(data) }.to raise_error 372 | end 373 | 374 | # XY 375 | it "should properly set xy value in constructor" do 376 | xy = [BulbState::MAX_XY,BulbState::MAX_XY] 377 | data = { "xy" => xy } 378 | b = BulbState.new(data) 379 | b.xy.should eq xy 380 | b.data["xy"].should eq xy 381 | end 382 | 383 | it "should properly set xy value in constructor" do 384 | xy = [BulbState::MIN_XY,BulbState::MIN_XY] 385 | data = { "xy" => xy } 386 | b = BulbState.new(data) 387 | b.xy.should eq xy 388 | b.data["xy"].should eq xy 389 | end 390 | 391 | it "should properly set xy value" do 392 | b = BulbState.new 393 | xy = [BulbState::MAX_XY,BulbState::MAX_XY] 394 | b.xy = xy 395 | b.xy.should eq xy 396 | b.data["xy"].should eq xy 397 | end 398 | 399 | it "should properly set xy value" do 400 | b = BulbState.new 401 | xy = [BulbState::MIN_XY,BulbState::MIN_XY] 402 | b.xy = xy 403 | b.xy.should eq xy 404 | b.data["xy"].should eq xy 405 | end 406 | 407 | it "should raise exception when xy value is not an array" do 408 | xy = {} 409 | data = { "xy" => xy } 410 | expect { BulbState.new(data) }.to raise_error 411 | end 412 | 413 | # XY - X 414 | it "should raise exception when x value is not a number" do 415 | xy = ["test value",BulbState::MIN_XY] 416 | data = { "xy" => xy } 417 | expect { BulbState.new(data) }.to raise_error 418 | end 419 | 420 | it "should raise exception when initial xy is out of range (X,HIGH)" do 421 | xy = [BulbState::MAX_XY+1,BulbState::MIN_XY] 422 | data = { "xy" => xy } 423 | expect { BulbState.new(data) }.to raise_error 424 | end 425 | 426 | it "should raise exception when initial xy is out of range (X,LOW)" do 427 | xy = [BulbState::MIN_XY-1,BulbState::MIN_XY] 428 | data = { "xy" => xy } 429 | expect { BulbState.new(data) }.to raise_error 430 | end 431 | 432 | it "should raise exception when set xy is out of range (X,HIGH)" do 433 | xy = [BulbState::MAX_XY+1,BulbState::MIN_XY] 434 | b = BulbState.new() 435 | expect { b.xy = xy }.to raise_error 436 | end 437 | 438 | it "should raise exception when set xy is out of range (X,LOW)" do 439 | xy = [BulbState::MIN_XY-1,BulbState::MIN_XY] 440 | b = BulbState.new() 441 | expect { b.xy = xy }.to raise_error 442 | end 443 | 444 | # XY - Y 445 | it "should raise exception when y value is not a number" do 446 | xy = [BulbState::MIN_XY,"test value"] 447 | data = { "xy" => xy } 448 | expect { BulbState.new(data) }.to raise_error 449 | end 450 | 451 | it "should raise exception when initial xy is out of range (Y,HIGH)" do 452 | xy = [BulbState::MIN_XY,BulbState::MAX_XY+1] 453 | data = { "xy" => xy } 454 | expect { BulbState.new(data) }.to raise_error 455 | end 456 | 457 | it "should raise exception when initial xy is out of range (Y,LOW)" do 458 | xy = [BulbState::MIN_XY,BulbState::MIN_XY-1] 459 | data = { "xy" => xy } 460 | expect { BulbState.new(data) }.to raise_error 461 | end 462 | 463 | it "should raise exception when set xy is out of range (Y,HIGH)" do 464 | xy = [BulbState::MAX_XY,BulbState::MAX_XY+1] 465 | b = BulbState.new() 466 | expect { b.xy = xy }.to raise_error 467 | end 468 | 469 | it "should raise exception when set xy is out of range (Y,LOW)" do 470 | xy = [BulbState::MIN_XY,BulbState::MIN_XY-1] 471 | b = BulbState.new() 472 | expect { b.xy = xy }.to raise_error 473 | end 474 | 475 | end 476 | 477 | --------------------------------------------------------------------------------