huebot 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 267bb3cc14e143785af2250a762b365f513bb18a3630b68edf10d3df3748de2a
4
- data.tar.gz: 7afa69b3aa54a936deebbcea75b409f2411fa5482554b22cd29aff088ce351b6
3
+ metadata.gz: b919267fa7949b2e208ca3b62112c21f05aa1cb9e52a1cb175ab32a96e2b3411
4
+ data.tar.gz: 8ba14d33785521841fe31daff20786b6ead22592d15e3938b7b518202411b888
5
5
  SHA512:
6
- metadata.gz: 442d3fb05c7f1e6610437ff5fc2f92d25694e2e9342ff9b16217ba80b7982538d48f807304e487fd583385864e9f4cf86491e33d2c66ba663b22449867ba780c
7
- data.tar.gz: '01908c65fb9c472271904973e313bc080eb96d91b557ef2e8bb3902a5ea054f7fb71b3b37bf6f51f356bf742c5ab32a336e4c013059a0a537dc3a538955dfc97'
6
+ metadata.gz: 462cf2419a33689160a4471157b07302ab3fbbc48341903dde3d1c520f4cfc72a21fe6f1dc83262251270c692ad991aa04b666229f30b7218dc70d98f20ec398
7
+ data.tar.gz: 04c0e7d1fec4c384ceab06e022442ca90f9fefafbe0b15df9d6e3d8fd9db291ab16eca3af13e1db1edf0b8ea902e9d9e89bc36cdaa072e72fa956152db617c63
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Huebot
2
2
 
3
- Orchestration and automation for Philips Hue devices. Huebot can be used as a Ruby library or a command line utility. Huebot programs are declared as YAML files.
3
+ Program your Hue lights in YAML!
4
4
 
5
5
  $ huebot run dimmer.yml --light="Office Desk"
6
6
 
@@ -34,17 +34,18 @@ The variable `$all` refers to all lights and/or groups passed in on the command
34
34
 
35
35
  gem install huebot
36
36
 
37
- ## License
37
+ Having trouble with Hue Bridge auto discovery? Me too. If you know your bridge's IP (and ideally have assigned it a static one), you can set it manually:
38
38
 
39
- Huebot is licensed under the MIT license (see LICENSE file).
39
+ huebot set-ip <your bridge's IP>
40
40
 
41
- A patched version of the "hue" gem is bundled in huebot's codebase (to remove a dependency that's unnecessarily annoying to install). The license for it can be found at `lib/hue/LICENSE`.
41
+ Configuration is stored in `~/.config/huebot`.
42
42
 
43
- ## UNDER ACTIVE DEVELOPMENT
43
+ ## License
44
+
45
+ Huebot is licensed under the MIT license (see LICENSE file).
44
46
 
45
47
  **TODO**
46
48
 
47
49
  * Validate number of inputs against compiled programs
48
- * Brief explanation various features
49
- * Wiki entry with more examples
50
- * Link to official Hue docs
50
+ * More explanation various features in Wiki
51
+ * More examples in Wiki
data/bin/huebot CHANGED
@@ -9,15 +9,25 @@ require 'huebot/cli'
9
9
  Huebot::CLI.tap { |cli|
10
10
  case cli.get_cmd
11
11
  when :ls
12
- client = Hue::Client.new
13
- puts "Lights\n" + client.lights.map { |l| " #{l.id}: #{l.name}" }.join("\n") + \
14
- "\nGroups\n" + client.groups.map { |g| " #{g.id}: #{g.name}" }.join("\n")
12
+ bridge, error = Huebot::Bridge.connect
13
+ if error
14
+ $stderr.puts error
15
+ exit 1
16
+ end
17
+
18
+ puts "Lights\n" + bridge.lights.map { |l| " #{l.id}: #{l.name}" }.join("\n") + \
19
+ "\nGroups\n" + bridge.groups.map { |g| " #{g.id}: #{g.name}" }.join("\n")
15
20
 
16
21
  when :run
17
22
  opts, sources = cli.get_input!
18
23
 
19
- client = Hue::Client.new
20
- device_mapper = Huebot::DeviceMapper.new(client, opts.inputs)
24
+ bridge, error = Huebot::Bridge.connect
25
+ if error
26
+ $stderr.puts error
27
+ exit 1
28
+ end
29
+
30
+ device_mapper = Huebot::DeviceMapper.new(bridge, opts.inputs)
21
31
  compiler = Huebot::Compiler.new(device_mapper)
22
32
 
23
33
  programs = sources.map { |src|
@@ -26,14 +36,19 @@ Huebot::CLI.tap { |cli|
26
36
  found_errors, _found_warnings = cli.check! programs, $stderr
27
37
  exit 1 if found_errors
28
38
 
29
- bot = Huebot::Bot.new(client)
39
+ bot = Huebot::Bot.new(bridge)
30
40
  programs.each { |prog| bot.execute prog }
31
41
 
32
42
  when :check
33
43
  opts, sources = cli.get_input!
34
44
 
35
- client = Hue::Client.new
36
- device_mapper = Huebot::DeviceMapper.new(client, opts.inputs)
45
+ bridge, error = Huebot::Bridge.connect
46
+ if error
47
+ $stderr.puts error
48
+ exit 1
49
+ end
50
+
51
+ device_mapper = Huebot::DeviceMapper.new(bridge, opts.inputs)
37
52
  compiler = Huebot::Compiler.new(device_mapper)
38
53
 
39
54
  programs = sources.map { |src|
@@ -43,6 +58,21 @@ Huebot::CLI.tap { |cli|
43
58
  # TODO validate NUMBER of inputs against each program
44
59
  exit (found_errors || found_warnings) ? 1 : 0
45
60
 
61
+ when :"set-ip"
62
+ ip = cli.get_args(num: 1).first
63
+ config = Huebot::Config.new
64
+ config["ip"] = ip
65
+
66
+ when :"clear-ip"
67
+ cli.get_args(num: 0)
68
+ config = Huebot::Config.new
69
+ config["ip"] = nil
70
+
71
+ when :unregister
72
+ cli.get_args(num: 0)
73
+ config = Huebot::Config.new
74
+ config.clear
75
+
46
76
  else cli.help!
47
77
  end
48
78
  }
@@ -0,0 +1,24 @@
1
+ module Huebot
2
+ class Bridge
3
+ def self.connect(config = Huebot::Config.new)
4
+ client = Client.new(config)
5
+ error = client.connect
6
+ return nil, error if error
7
+ return new(client)
8
+ end
9
+
10
+ attr_reader :client
11
+
12
+ def initialize(client)
13
+ @client = client
14
+ end
15
+
16
+ def lights
17
+ client.get!("/lights").map { |(id, attrs)| Light.new(client, id, attrs) }
18
+ end
19
+
20
+ def groups
21
+ client.get!("/groups").map { |(id, attrs)| Group.new(client, id, attrs) }
22
+ end
23
+ end
24
+ end
data/lib/huebot/cli.rb CHANGED
@@ -23,6 +23,31 @@ module Huebot
23
23
  ARGV[0].to_s.to_sym
24
24
  end
25
25
 
26
+ def self.get_args(min: nil, max: nil, num: nil)
27
+ args = ARGV[1..]
28
+ if num
29
+ if num != args.size
30
+ $stderr.puts "Expected #{num} args, found #{args.size}"
31
+ exit 1
32
+ end
33
+ elsif min and max
34
+ if args.size < min or args.size > max
35
+ $stderr.puts "Expected #{min}-#{max} args, found #{args.size}"
36
+ end
37
+ elsif min
38
+ if args.size < min
39
+ $stderr.puts "Expected at least #{num} args, found #{args.size}"
40
+ exit 1
41
+ end
42
+ elsif max
43
+ if args.size > max
44
+ $stderr.puts "Expected no more than #{num} args, found #{args.size}"
45
+ exit 1
46
+ end
47
+ end
48
+ args
49
+ end
50
+
26
51
  #
27
52
  # Parses and returns input from the CLI. Serious errors might result in the program exiting.
28
53
  #
@@ -37,7 +62,7 @@ module Huebot
37
62
  if files.empty? and !options.read_stdin
38
63
  puts parser.help
39
64
  exit 1
40
- elsif (bad_paths = files.select { |p| !File.exists? p }).any?
65
+ elsif (bad_paths = files.select { |p| !File.exist? p }).any?
41
66
  $stderr.puts "Cannot find #{bad_paths.join ', '}"
42
67
  exit 1
43
68
  else
@@ -107,6 +132,13 @@ Run program(s):
107
132
  Validate programs and inputs:
108
133
  huebot check file1.yml [file2.yml [file3.yml ...]] [options]
109
134
 
135
+ Manually set/clear the IP for your Hue Bridge (useful when on a VPN):
136
+ huebot set-ip 192.168.1.20
137
+ huebot clear-ip
138
+
139
+ Clear all connection config:
140
+ huebot unregister
141
+
110
142
  Options:
111
143
  ).strip
112
144
  opts.on("-lLIGHT", "--light=LIGHT", "Light ID or name") { |l| options.inputs << LightInput.new(l) }
@@ -0,0 +1,131 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+
5
+ module Huebot
6
+ class Client
7
+ DISCOVERY_URI = URI(ENV["HUE_DISCOVERY_API"] || "https://discovery.meethue.com/")
8
+ Bridge = Struct.new(:id, :ip)
9
+
10
+ attr_reader :config
11
+
12
+ def initialize(config = Huebot::Config.new)
13
+ @config = config
14
+ @ip = config["ip"] # NOTE will usually be null
15
+ @username = nil
16
+ end
17
+
18
+ def connect
19
+ if config["ip"]
20
+ @ip = config["ip"]
21
+ elsif config["id"]
22
+ @ip = bridges.detect { |b| b.id == id }&.ip
23
+ return "Unable to find Hue Bridge '#{config["id"]}' on your network" if @ip.nil?
24
+ else
25
+ bridge = bridges.first
26
+ return "Unable to find a Hue Bridge on your network" if bridge.nil?
27
+ config["id"] = bridge.id
28
+ @ip = bridge.ip
29
+ end
30
+
31
+ if config["username"]
32
+ if valid_username? config["username"]
33
+ @username = config["username"]
34
+ else
35
+ return "Invalid Hue Bridge username '#{config["username"]}'"
36
+ end
37
+ else
38
+ username, error = register
39
+ return error if error
40
+ config["username"] = @username = username
41
+ end
42
+ nil
43
+ end
44
+
45
+ def get!(path)
46
+ resp, error = get path
47
+ raise error if error
48
+ resp
49
+ end
50
+
51
+ def get(path)
52
+ url = "http://#{@ip}/api"
53
+ url << "/#{@username}" if @username
54
+ url << path
55
+ req = Net::HTTP::Get.new(URI(url))
56
+ req_json req
57
+ end
58
+
59
+ def post!(path, body)
60
+ resp, error = post path, body
61
+ raise error if error
62
+ resp
63
+ end
64
+
65
+ def post(path, body)
66
+ url = "http://#{@ip}/api"
67
+ url << "/#{@username}" if @username
68
+ url << path
69
+ req = Net::HTTP::Post.new(URI(url))
70
+ req["Content-Type"] = "application/json"
71
+ req.body = body.to_json
72
+ req_json req
73
+ end
74
+
75
+ def put!(path, body)
76
+ resp, error = put path, body
77
+ raise error if error
78
+ resp
79
+ end
80
+
81
+ def put(path, body)
82
+ url = "http://#{@ip}/api"
83
+ url << "/#{@username}" if @username
84
+ url << path
85
+ req = Net::HTTP::Put.new(URI(url))
86
+ req["Content-Type"] = "application/json"
87
+ req.body = body.to_json
88
+ req_json req
89
+ end
90
+
91
+ def req_json(req)
92
+ resp = Net::HTTP.start req.uri.host, req.uri.port, {use_ssl: false} do |http|
93
+ http.request req
94
+ end
95
+ case resp.code.to_i
96
+ when 200..201
97
+ data = JSON.parse(resp.body)
98
+ if data[0] and (error = data[0]["error"])
99
+ return nil, error.fetch("description")
100
+ else
101
+ return data, nil
102
+ end
103
+ else
104
+ raise "Unexpected response from Bridge (#{resp.code}): #{resp.body}"
105
+ end
106
+ end
107
+
108
+ def bridges
109
+ req = Net::HTTP::Get.new(DISCOVERY_URI)
110
+ resp = Net::HTTP.start req.uri.host, req.uri.port, {use_ssl: true} do |http|
111
+ http.request req
112
+ end
113
+ JSON.parse(resp.body).map { |x|
114
+ Bridge.new(x.fetch("id"), x.fetch("internalipaddress"))
115
+ }
116
+ end
117
+
118
+ private
119
+
120
+ def valid_username?(username)
121
+ _resp, error = get("/#{username}")
122
+ !error
123
+ end
124
+
125
+ def register
126
+ resp, error = post "/", {"devicetype": "huebot"}
127
+ return nil, error if error
128
+ resp[0].fetch("success").fetch("username")
129
+ end
130
+ end
131
+ end
@@ -1,5 +1,7 @@
1
1
  module Huebot
2
2
  class Compiler
3
+ DEVICE_FIELDS = %i(light lights group groups device devices).freeze
4
+
3
5
  def initialize(device_mapper)
4
6
  @device_mapper = device_mapper
5
7
  end
@@ -130,9 +132,9 @@ module Huebot
130
132
  end
131
133
  state[:transitiontime] = t.delete("time") || t.delete(:time) || t.delete("transitiontime") || t.delete(:transitiontime) || 4
132
134
 
133
- transition.state = t.merge(state).reduce({}) { |a, (key, val)|
134
- a[key.to_sym] = val
135
- a
135
+ transition.state = t.merge(state).each_with_object({}) { |(key, val), obj|
136
+ key = key.to_sym
137
+ obj[key] = val unless DEVICE_FIELDS.include? key
136
138
  }
137
139
  return errors, warnings, transition
138
140
  end
@@ -0,0 +1,41 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+
4
+ module Huebot
5
+ class Config
6
+ def initialize(path = "~/.config/huebot")
7
+ @path = File.expand_path(path)
8
+ @dir = File.dirname(@path)
9
+ @dir_exists = File.exist? @dir
10
+ @config = File.exist?(@path) ? YAML.load_file(@path) : {}
11
+ end
12
+
13
+ def [](attr)
14
+ @config[attr.to_s]
15
+ end
16
+
17
+ def []=(attr, val)
18
+ if val.nil?
19
+ @config.delete(attr.to_s)
20
+ else
21
+ @config[attr.to_s] = val
22
+ end
23
+ write
24
+ end
25
+
26
+ def clear
27
+ @config.clear
28
+ write
29
+ end
30
+
31
+ private
32
+
33
+ def write
34
+ unless @dir_exists
35
+ FileUtils.mkdir_p @dir
36
+ @dir_exists = true
37
+ end
38
+ File.write(@path, YAML.dump(@config))
39
+ end
40
+ end
41
+ end
@@ -2,8 +2,8 @@ module Huebot
2
2
  class DeviceMapper
3
3
  Unmapped = Class.new(StandardError)
4
4
 
5
- def initialize(client, inputs = [])
6
- all_lights, all_groups = client.lights, client.groups
5
+ def initialize(bridge, inputs = [])
6
+ all_lights, all_groups = bridge.lights, bridge.groups
7
7
 
8
8
  @lights_by_id = all_lights.reduce({}) { |a, l| a[l.id] = l; a }
9
9
  @lights_by_name = all_lights.reduce({}) { |a, l| a[l.name] = l; a }
@@ -0,0 +1,7 @@
1
+ module Huebot
2
+ module DeviceState
3
+ def set_state(state)
4
+ client.put!(state_url, state)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ module Huebot
2
+ class Group
3
+ include DeviceState
4
+ attr_reader :client, :id, :name
5
+
6
+ def initialize(client, id, attrs)
7
+ @client = client
8
+ @id = id
9
+ @name = attrs.fetch("name")
10
+ @attrs = attrs
11
+ end
12
+
13
+ private
14
+
15
+ def state_url
16
+ url "/action"
17
+ end
18
+
19
+ def url(path)
20
+ "/groups/#{id}#{path}"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Huebot
2
+ class Light
3
+ include DeviceState
4
+ attr_reader :client, :id, :name
5
+
6
+ def initialize(client, id, attrs)
7
+ @client = client
8
+ @id = id
9
+ @name = attrs.fetch("name")
10
+ @attrs = attrs
11
+ end
12
+
13
+ private
14
+
15
+ def state_url
16
+ url "/state"
17
+ end
18
+
19
+ def url(path)
20
+ "/lights/#{id}#{path}"
21
+ end
22
+ end
23
+ end
@@ -1,4 +1,4 @@
1
1
  module Huebot
2
2
  # Gem version
3
- VERSION = '0.4.0'
3
+ VERSION = '0.5.0'
4
4
  end
data/lib/huebot.rb CHANGED
@@ -1,6 +1,10 @@
1
- require 'hue'
2
-
3
1
  module Huebot
2
+ autoload :Config, 'huebot/config'
3
+ autoload :Client, 'huebot/client'
4
+ autoload :Bridge, 'huebot/bridge'
5
+ autoload :DeviceState, 'huebot/device_state'
6
+ autoload :Light, 'huebot/light'
7
+ autoload :Group, 'huebot/group'
4
8
  autoload :DeviceMapper, 'huebot/device_mapper'
5
9
  autoload :Program, 'huebot/program'
6
10
  autoload :Compiler, 'huebot/compiler'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: huebot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Hollinger
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-17 00:00:00.000000000 Z
11
+ date: 2023-12-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Declare and run YAML programs for Philips Hue devices
14
14
  email: jordan.hollinger@gmail.com
@@ -19,29 +19,24 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - README.md
21
21
  - bin/huebot
22
- - lib/hue.rb
23
- - lib/hue/LICENSE
24
- - lib/hue/bridge.rb
25
- - lib/hue/client.rb
26
- - lib/hue/editable_state.rb
27
- - lib/hue/errors.rb
28
- - lib/hue/group.rb
29
- - lib/hue/light.rb
30
- - lib/hue/scene.rb
31
- - lib/hue/translate_keys.rb
32
- - lib/hue/version.rb
33
22
  - lib/huebot.rb
34
23
  - lib/huebot/bot.rb
24
+ - lib/huebot/bridge.rb
35
25
  - lib/huebot/cli.rb
26
+ - lib/huebot/client.rb
36
27
  - lib/huebot/compiler.rb
28
+ - lib/huebot/config.rb
37
29
  - lib/huebot/device_mapper.rb
30
+ - lib/huebot/device_state.rb
31
+ - lib/huebot/group.rb
32
+ - lib/huebot/light.rb
38
33
  - lib/huebot/program.rb
39
34
  - lib/huebot/version.rb
40
35
  homepage: https://github.com/jhollinger/huebot
41
36
  licenses:
42
37
  - MIT
43
38
  metadata: {}
44
- post_install_message:
39
+ post_install_message:
45
40
  rdoc_options: []
46
41
  require_paths:
47
42
  - lib
@@ -56,8 +51,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
56
51
  - !ruby/object:Gem::Version
57
52
  version: '0'
58
53
  requirements: []
59
- rubygems_version: 3.0.3.1
60
- signing_key:
54
+ rubygems_version: 3.4.1
55
+ signing_key:
61
56
  specification_version: 4
62
57
  summary: Orchestration for Hue devices
63
58
  test_files: []
data/lib/hue/LICENSE DELETED
@@ -1,22 +0,0 @@
1
- Copyright (c) 2013-2014 Sam Soffes, http://soff.es
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.