huebot 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -3
- data/bin/huebot +39 -19
- data/lib/huebot/bot.rb +35 -12
- data/lib/huebot/bridge.rb +3 -6
- data/lib/huebot/{cli.rb → cli/cli.rb} +2 -1
- data/lib/huebot/cli/config.rb +47 -0
- data/lib/huebot/cli/helpers.rb +75 -36
- data/lib/huebot/cli/runner.rb +34 -26
- data/lib/huebot/client.rb +1 -1
- data/lib/huebot/compiler/api_v1.rb +22 -3
- data/lib/huebot/device_mapper.rb +6 -8
- data/lib/huebot/group.rb +0 -1
- data/lib/huebot/light.rb +0 -1
- data/lib/huebot/logging/collecting_logger.rb +20 -0
- data/lib/huebot/logging/io_logger.rb +19 -0
- data/lib/huebot/logging/logging.rb +7 -0
- data/lib/huebot/logging/null_logger.rb +9 -0
- data/lib/huebot/version.rb +1 -1
- data/lib/huebot.rb +3 -3
- metadata +9 -5
- data/lib/huebot/config.rb +0 -41
- /data/lib/huebot/{compiler.rb → compiler/compiler.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7febcd2a42112b4c3636581d9419a58c490a1157637e143fd46e6e7d94aefba5
|
4
|
+
data.tar.gz: d4a9ab3178d381786595f14a14899e55af7789b4650cfa2da1928b83b5fe2135
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 15150d4cd9652f7d5e2518bd714de8155f1b574054360f61c0ac383c30f6fe81e3421f0bf64f0da43f090dabd2677b71609ae0a1e4f3741524adcdd631bd1116
|
7
|
+
data.tar.gz: 548d5adc4af710d2b1c9719e54fad7aa5fe5ba18749845089d0cb66fb59b4fd5e8279acc4214e961757c9956bad0b79ed8dacf4e531e3f54097322ce44a6b4bb
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Huebot
|
2
2
|
|
3
|
-
Program your Hue lights!
|
3
|
+
Program your Hue lights using YAML or JSON!
|
4
4
|
|
5
|
-
$ huebot run dimmer.
|
5
|
+
$ huebot run dimmer.yaml --light="Office Desk"
|
6
6
|
|
7
7
|
A few examples are below. [See the Wiki](https://github.com/jhollinger/huebot/wiki) for full documentation.
|
8
8
|
|
@@ -64,7 +64,8 @@ serial:
|
|
64
64
|
|
65
65
|
# Run these steps in parallel in an infinite loop
|
66
66
|
- parallel:
|
67
|
-
loop:
|
67
|
+
loop:
|
68
|
+
infinite: true
|
68
69
|
steps:
|
69
70
|
# Parallel branch 1: Fade inputs #1 and #3 up and down
|
70
71
|
- serial:
|
data/bin/huebot
CHANGED
@@ -1,23 +1,27 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
# Used for local testing
|
4
|
-
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
4
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') if ENV["DEV"] == "1"
|
5
5
|
|
6
6
|
require 'huebot'
|
7
7
|
|
8
8
|
Huebot::CLI::Helpers.tap { |cli|
|
9
9
|
case cli.get_cmd
|
10
10
|
when :ls
|
11
|
+
cli.get_args!(num: 0)
|
12
|
+
opts = cli.get_opts!
|
11
13
|
bridge, error = Huebot::Bridge.connect
|
12
14
|
if error
|
13
15
|
$stderr.puts error
|
14
16
|
exit 1
|
15
17
|
end
|
16
|
-
|
18
|
+
|
19
|
+
retval = Huebot::CLI::Runner.ls(bridge.lights, bridge.groups, opts)
|
17
20
|
exit retval
|
18
21
|
|
19
22
|
when :run
|
20
|
-
opts
|
23
|
+
opts = cli.get_opts!
|
24
|
+
sources = cli.get_input! opts
|
21
25
|
if sources.empty?
|
22
26
|
cli.help!
|
23
27
|
exit 1
|
@@ -28,26 +32,35 @@ Huebot::CLI::Helpers.tap { |cli|
|
|
28
32
|
$stderr.puts error
|
29
33
|
exit 1
|
30
34
|
end
|
31
|
-
|
35
|
+
|
36
|
+
retval = Huebot::CLI::Runner.run(sources, bridge.lights, bridge.groups, opts)
|
32
37
|
exit retval
|
33
38
|
|
34
39
|
when :check
|
35
|
-
opts
|
40
|
+
opts = cli.get_opts!
|
41
|
+
sources = cli.get_input! opts
|
36
42
|
if sources.empty?
|
37
43
|
cli.help!
|
38
44
|
exit 1
|
39
45
|
end
|
40
46
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
+
lights, groups =
|
48
|
+
if opts.no_device_check
|
49
|
+
[[], []]
|
50
|
+
else
|
51
|
+
bridge, error = Huebot::Bridge.connect
|
52
|
+
if error
|
53
|
+
$stderr.puts error
|
54
|
+
exit 1
|
55
|
+
end
|
56
|
+
[bridge.lights, bridge.groups]
|
57
|
+
end
|
58
|
+
|
59
|
+
retval = Huebot::CLI::Runner.check(sources, lights, groups, opts)
|
47
60
|
exit retval
|
48
61
|
|
49
62
|
when :"get-state"
|
50
|
-
opts
|
63
|
+
opts = cli.get_opts!
|
51
64
|
if opts.inputs.empty?
|
52
65
|
cli.help!
|
53
66
|
exit 1
|
@@ -58,22 +71,29 @@ Huebot::CLI::Helpers.tap { |cli|
|
|
58
71
|
$stderr.puts error
|
59
72
|
exit 1
|
60
73
|
end
|
61
|
-
|
74
|
+
|
75
|
+
retval = Huebot::CLI::Runner.get_state(bridge.lights, bridge.groups, opts)
|
62
76
|
exit retval
|
63
77
|
|
64
78
|
when :"set-ip"
|
65
|
-
|
66
|
-
|
79
|
+
opts = cli.get_opts!
|
80
|
+
ip = cli.get_args!(num: 1).first
|
81
|
+
config = Huebot::CLI::Config.new
|
82
|
+
retval = Huebot::CLI::Runner.set_ip config, ip, opts
|
67
83
|
exit retval
|
68
84
|
|
69
85
|
when :"clear-ip"
|
70
|
-
cli.
|
71
|
-
|
86
|
+
opts = cli.get_opts!
|
87
|
+
cli.get_args!(num: 0)
|
88
|
+
config = Huebot::CLI::Config.new
|
89
|
+
retval = Huebot::CLI::Runner.clear_ip config, opts
|
72
90
|
exit retval
|
73
91
|
|
74
92
|
when :unregister
|
75
|
-
cli.
|
76
|
-
|
93
|
+
opts = cli.get_opts!
|
94
|
+
cli.get_args!(num: 0)
|
95
|
+
config = Huebot::CLI::Config.new
|
96
|
+
retval = Huebot::CLI::Runner.unregister config, opts
|
77
97
|
exit retval
|
78
98
|
|
79
99
|
else cli.help!
|
data/lib/huebot/bot.rb
CHANGED
@@ -1,14 +1,18 @@
|
|
1
1
|
module Huebot
|
2
|
+
# The Huebot runtime
|
2
3
|
class Bot
|
3
4
|
Error = Class.new(StandardError)
|
4
5
|
|
5
|
-
def initialize(device_mapper)
|
6
|
+
def initialize(device_mapper, waiter: nil, logger: nil)
|
6
7
|
@device_mapper = device_mapper
|
7
|
-
|
8
|
+
@logger = logger || Logging::NullLogger.new
|
9
|
+
@waiter = waiter || Waiter
|
8
10
|
end
|
9
11
|
|
10
12
|
def execute(program)
|
13
|
+
@logger.log :start, {program: program.name}
|
11
14
|
exec program.data
|
15
|
+
@logger.log :stop, {program: program.name}
|
12
16
|
end
|
13
17
|
|
14
18
|
private
|
@@ -30,10 +34,13 @@ module Huebot
|
|
30
34
|
def transition(state, device_refs, sleep_time = nil)
|
31
35
|
time = (state["transitiontime"] || 4).to_f / 10
|
32
36
|
devices = map_devices device_refs
|
37
|
+
@logger.log :transition, {devices: devices.map(&:name)}
|
38
|
+
|
33
39
|
devices.map { |device|
|
34
40
|
Thread.new {
|
35
41
|
# TODO error handling
|
36
|
-
device.set_state state
|
42
|
+
_res = device.set_state state
|
43
|
+
@logger.log :set_state, {device: device.name, state: state, result: nil}
|
37
44
|
wait time
|
38
45
|
}
|
39
46
|
}.map(&:join)
|
@@ -41,8 +48,9 @@ module Huebot
|
|
41
48
|
end
|
42
49
|
|
43
50
|
def serial(nodes, lp, sleep_time = nil)
|
44
|
-
control_loop(lp) {
|
45
|
-
|
51
|
+
control_loop(lp) { |loop_type|
|
52
|
+
@logger.log :serial, {loop: loop_type}
|
53
|
+
nodes.each { |node|
|
46
54
|
exec node
|
47
55
|
}
|
48
56
|
}
|
@@ -50,7 +58,8 @@ module Huebot
|
|
50
58
|
end
|
51
59
|
|
52
60
|
def parallel(nodes, lp, sleep_time = nil)
|
53
|
-
control_loop(lp) {
|
61
|
+
control_loop(lp) { |loop_type|
|
62
|
+
@logger.log :parallel, {loop: loop_type}
|
54
63
|
nodes.map { |node|
|
55
64
|
Thread.new {
|
56
65
|
# TODO error handling
|
@@ -64,19 +73,27 @@ module Huebot
|
|
64
73
|
def control_loop(lp)
|
65
74
|
case lp
|
66
75
|
when Program::AST::InfiniteLoop
|
67
|
-
loop {
|
76
|
+
loop {
|
77
|
+
yield :infinite
|
78
|
+
wait lp.pause if lp.pause
|
79
|
+
}
|
68
80
|
when Program::AST::CountedLoop
|
69
|
-
lp.n.times {
|
81
|
+
lp.n.times {
|
82
|
+
yield :counted
|
83
|
+
wait lp.pause if lp.pause
|
84
|
+
}
|
70
85
|
when Program::AST::DeadlineLoop
|
71
86
|
until Time.now >= lp.stop_time
|
72
|
-
yield
|
87
|
+
yield :deadline
|
88
|
+
wait lp.pause if lp.pause
|
73
89
|
end
|
74
90
|
when Program::AST::TimerLoop
|
75
91
|
sec = ((lp.hours * 60) + lp.minutes) * 60
|
76
92
|
time = 0
|
77
93
|
until time >= sec
|
78
94
|
start = Time.now
|
79
|
-
yield
|
95
|
+
yield :timer
|
96
|
+
wait lp.pause if lp.pause
|
80
97
|
time += (Time.now - start).round
|
81
98
|
end
|
82
99
|
else
|
@@ -102,8 +119,14 @@ module Huebot
|
|
102
119
|
end
|
103
120
|
|
104
121
|
def wait(seconds)
|
105
|
-
|
106
|
-
|
122
|
+
@logger.log :pause, {time: seconds}
|
123
|
+
@waiter.call seconds
|
124
|
+
end
|
125
|
+
|
126
|
+
module Waiter
|
127
|
+
def self.call(seconds)
|
128
|
+
sleep seconds
|
129
|
+
end
|
107
130
|
end
|
108
131
|
end
|
109
132
|
end
|
data/lib/huebot/bridge.rb
CHANGED
@@ -1,24 +1,21 @@
|
|
1
1
|
module Huebot
|
2
2
|
class Bridge
|
3
|
-
def self.connect(
|
4
|
-
client = Client.new(config)
|
3
|
+
def self.connect(client = Client.new)
|
5
4
|
error = client.connect
|
6
5
|
return nil, error if error
|
7
6
|
return new(client)
|
8
7
|
end
|
9
8
|
|
10
|
-
attr_reader :client
|
11
|
-
|
12
9
|
def initialize(client)
|
13
10
|
@client = client
|
14
11
|
end
|
15
12
|
|
16
13
|
def lights
|
17
|
-
client.get!("/lights").map { |(id, attrs)| Light.new(client, id, attrs) }
|
14
|
+
@client.get!("/lights").map { |(id, attrs)| Light.new(@client, id, attrs) }
|
18
15
|
end
|
19
16
|
|
20
17
|
def groups
|
21
|
-
client.get!("/groups").map { |(id, attrs)| Group.new(client, id, attrs) }
|
18
|
+
@client.get!("/groups").map { |(id, attrs)| Group.new(@client, id, attrs) }
|
22
19
|
end
|
23
20
|
end
|
24
21
|
end
|
@@ -8,8 +8,9 @@ module Huebot
|
|
8
8
|
#
|
9
9
|
# @attr inputs [Array<String>]
|
10
10
|
#
|
11
|
-
Options = Struct.new(:inputs, :read_stdin)
|
11
|
+
Options = Struct.new(:inputs, :read_stdin, :debug, :no_device_check, :stdin, :stdout, :stderr, :bot_waiter)
|
12
12
|
|
13
|
+
autoload :Config, 'huebot/cli/config'
|
13
14
|
autoload :Helpers, 'huebot/cli/helpers'
|
14
15
|
autoload :Runner, 'huebot/cli/runner'
|
15
16
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Huebot
|
5
|
+
module CLI
|
6
|
+
class Config
|
7
|
+
def initialize(path = "~/.config/huebot")
|
8
|
+
@path = File.expand_path(path)
|
9
|
+
@dir = File.dirname(@path)
|
10
|
+
@dir_exists = File.exist? @dir
|
11
|
+
reload
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](attr)
|
15
|
+
@config[attr.to_s]
|
16
|
+
end
|
17
|
+
|
18
|
+
def []=(attr, val)
|
19
|
+
if val.nil?
|
20
|
+
@config.delete(attr.to_s)
|
21
|
+
else
|
22
|
+
@config[attr.to_s] = val
|
23
|
+
end
|
24
|
+
write
|
25
|
+
end
|
26
|
+
|
27
|
+
def clear
|
28
|
+
@config.clear
|
29
|
+
write
|
30
|
+
end
|
31
|
+
|
32
|
+
def reload
|
33
|
+
@config = File.exist?(@path) ? YAML.load_file(@path) : {}
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def write
|
39
|
+
unless @dir_exists
|
40
|
+
FileUtils.mkdir_p @dir
|
41
|
+
@dir_exists = true
|
42
|
+
end
|
43
|
+
File.write(@path, YAML.dump(@config))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/huebot/cli/helpers.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
require 'yaml'
|
3
|
+
require 'json'
|
3
4
|
|
4
5
|
module Huebot
|
5
6
|
module CLI
|
@@ -9,63 +10,80 @@ module Huebot
|
|
9
10
|
#
|
10
11
|
# @return [Symbol]
|
11
12
|
#
|
12
|
-
def self.get_cmd
|
13
|
-
|
13
|
+
def self.get_cmd(argv = ARGV)
|
14
|
+
argv[0].to_s.to_sym
|
14
15
|
end
|
15
16
|
|
16
|
-
def self.get_args(min: nil, max: nil, num: nil)
|
17
|
-
args =
|
17
|
+
def self.get_args!(argv = ARGV, min: nil, max: nil, num: nil)
|
18
|
+
args, error = get_args(argv, min: min, max: max, num: num)
|
19
|
+
if error
|
20
|
+
$stderr.puts error
|
21
|
+
exit 1
|
22
|
+
end
|
23
|
+
args
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.get_args(argv = ARGV, min: nil, max: nil, num: nil)
|
27
|
+
args = argv[1..]
|
18
28
|
if num
|
19
29
|
if num != args.size
|
20
|
-
|
21
|
-
exit 1
|
30
|
+
return nil, "Expected #{num} args, found #{args.size}"
|
22
31
|
end
|
23
32
|
elsif min and max
|
24
33
|
if args.size < min or args.size > max
|
25
|
-
|
34
|
+
return nil, "Expected #{min}-#{max} args, found #{args.size}"
|
26
35
|
end
|
27
36
|
elsif min
|
28
37
|
if args.size < min
|
29
|
-
|
30
|
-
exit 1
|
38
|
+
return nil, "Expected at least #{min} args, found #{args.size}"
|
31
39
|
end
|
32
40
|
elsif max
|
33
41
|
if args.size > max
|
34
|
-
|
35
|
-
exit 1
|
42
|
+
return nil, "Expected no more than #{max} args, found #{args.size}"
|
36
43
|
end
|
37
44
|
end
|
38
|
-
args
|
45
|
+
return args, nil
|
39
46
|
end
|
40
47
|
|
41
48
|
#
|
42
49
|
# Parses and returns input from the CLI. Serious errors might result in the program exiting.
|
43
50
|
#
|
44
|
-
# @
|
51
|
+
# @param opts [Huebot::CLI::Options] All given CLI options
|
45
52
|
# @return [Array<Huebot::Program::Src>] Array of given program sources
|
46
53
|
#
|
47
|
-
def self.get_input!
|
48
|
-
|
49
|
-
parser.parse!
|
50
|
-
|
51
|
-
files = ARGV[1..-1]
|
54
|
+
def self.get_input!(opts, argv = ARGV)
|
55
|
+
files = argv[1..-1]
|
52
56
|
if (bad_paths = files.select { |p| !File.exist? p }).any?
|
53
|
-
|
54
|
-
|
57
|
+
opts.stderr.puts "Cannot find #{bad_paths.join ', '}"
|
58
|
+
return []
|
55
59
|
end
|
56
60
|
|
57
61
|
sources = files.map { |path|
|
58
|
-
|
62
|
+
ext = File.extname path
|
63
|
+
src =
|
64
|
+
case ext
|
65
|
+
when ".yaml", ".yml"
|
66
|
+
YAML.safe_load(File.read path) || {}
|
67
|
+
when ".json"
|
68
|
+
JSON.load(File.read path) || {}
|
69
|
+
else
|
70
|
+
opts.stderr.puts "Unknown file extension '#{ext}'. Expected .yaml, .yml, or .json"
|
71
|
+
return []
|
72
|
+
end
|
59
73
|
version = (src.delete("version") || 1.0).to_f
|
60
74
|
Program::Src.new(src, path, version)
|
61
75
|
}
|
62
76
|
|
63
|
-
if
|
64
|
-
|
77
|
+
if !opts.stdin.isatty or opts.read_stdin
|
78
|
+
opts.stdout.puts "Please enter your YAML or JSON Huebot program below, followed by Ctrl+d:" if opts.read_stdin
|
79
|
+
raw = opts.stdin.read.lstrip
|
80
|
+
src = raw[0] == "{" ? JSON.load(raw) : YAML.safe_load(raw)
|
81
|
+
|
82
|
+
opts.stdout.puts "Executing..." if opts.read_stdin
|
65
83
|
version = (src.delete("version") || 1.0).to_f
|
66
84
|
sources << Program::Src.new(src, "STDIN", version)
|
67
85
|
end
|
68
|
-
|
86
|
+
sources
|
69
87
|
end
|
70
88
|
|
71
89
|
#
|
@@ -93,27 +111,35 @@ module Huebot
|
|
93
111
|
|
94
112
|
all_lights = programs.reduce([]) { |acc, p| acc + p.light_names }
|
95
113
|
if (missing_lights = device_mapper.missing_lights all_lights).any?
|
96
|
-
print_messages! io, "Unknown lights", missing_lights
|
114
|
+
print_messages! io, "Unknown lights", missing_lights unless quiet
|
97
115
|
end
|
98
116
|
|
99
117
|
all_groups = programs.reduce([]) { |acc, p| acc + p.group_names }
|
100
118
|
if (missing_groups = device_mapper.missing_groups all_groups).any?
|
101
|
-
print_messages! io, "Unknown groups", missing_groups
|
119
|
+
print_messages! io, "Unknown groups", missing_groups unless quiet
|
102
120
|
end
|
103
121
|
|
104
122
|
all_vars = programs.reduce([]) { |acc, p| acc + p.device_refs }
|
105
123
|
if (missing_vars = device_mapper.missing_vars all_vars).any?
|
106
|
-
print_messages! io, "Unknown device inputs", missing_vars.map { |d| "$#{d}" }
|
124
|
+
print_messages! io, "Unknown device inputs", missing_vars.map { |d| "$#{d}" } unless quiet
|
107
125
|
end
|
108
126
|
|
109
127
|
invalid_devices = missing_lights.size + missing_groups.size + missing_vars.size
|
110
128
|
return invalid_progs.any?, imperfect_progs.any?, invalid_devices > 0
|
111
129
|
end
|
112
130
|
|
131
|
+
def self.get_opts!
|
132
|
+
opts = default_options
|
133
|
+
parser = option_parser opts
|
134
|
+
parser.parse!
|
135
|
+
opts
|
136
|
+
end
|
137
|
+
|
113
138
|
# Print help and exit
|
114
139
|
def self.help!
|
115
|
-
|
116
|
-
|
140
|
+
opts = default_options
|
141
|
+
parser = option_parser opts
|
142
|
+
opts.stdout.puts parser.help
|
117
143
|
exit 1
|
118
144
|
end
|
119
145
|
|
@@ -133,18 +159,22 @@ module Huebot
|
|
133
159
|
}
|
134
160
|
end
|
135
161
|
|
136
|
-
def self.option_parser
|
137
|
-
|
138
|
-
parser = OptionParser.new { |opts|
|
162
|
+
def self.option_parser(options)
|
163
|
+
OptionParser.new { |opts|
|
139
164
|
opts.banner = %(
|
140
165
|
List all lights and groups:
|
141
166
|
huebot ls
|
142
167
|
|
143
168
|
Run program(s):
|
144
|
-
huebot run
|
169
|
+
huebot run prog1.yaml [prog2.yml [prog3.json ...]] [options]
|
170
|
+
|
171
|
+
Run program from STDIN:
|
172
|
+
cat prog1.yaml | huebot run [options]
|
173
|
+
huebot run [options] < prog1.yaml
|
174
|
+
huebot run -i [options]
|
145
175
|
|
146
176
|
Validate programs and inputs:
|
147
|
-
huebot check
|
177
|
+
huebot check prog1.yaml [prog2.yaml [prog3.yaml ...]] [options]
|
148
178
|
|
149
179
|
Print the current state of the given lights and/or groups:
|
150
180
|
huebot get-state [options]
|
@@ -161,9 +191,18 @@ module Huebot
|
|
161
191
|
opts.on("-lLIGHT", "--light=LIGHT", "Light ID or name") { |l| options.inputs << Light::Input.new(l) }
|
162
192
|
opts.on("-gGROUP", "--group=GROUP", "Group ID or name") { |g| options.inputs << Group::Input.new(g) }
|
163
193
|
opts.on("-i", "Read program from STDIN") { options.read_stdin = true }
|
164
|
-
opts.on("
|
194
|
+
opts.on("--debug", "Print debug info during run") { options.debug = true }
|
195
|
+
opts.on("--no-device-check", "Don't validate devices against the Bridge ('check' cmd only)") { options.no_device_check = true }
|
196
|
+
opts.on("-h", "--help", "Prints this help") { options.stdout.puts opts; exit }
|
165
197
|
}
|
166
|
-
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.default_options
|
201
|
+
options = Options.new([], false)
|
202
|
+
options.stdin = $stdin
|
203
|
+
options.stdout = $stdout
|
204
|
+
options.stderr = $stderr
|
205
|
+
options
|
167
206
|
end
|
168
207
|
end
|
169
208
|
end
|
data/lib/huebot/cli/runner.rb
CHANGED
@@ -1,76 +1,84 @@
|
|
1
1
|
module Huebot
|
2
2
|
module CLI
|
3
3
|
module Runner
|
4
|
-
def self.ls(
|
5
|
-
puts "Lights\n" +
|
6
|
-
"\nGroups\n" +
|
4
|
+
def self.ls(lights, groups, opts)
|
5
|
+
opts.stdout.puts "Lights\n" + lights.map { |l| " #{l.id}: #{l.name}" }.join("\n") + \
|
6
|
+
"\nGroups\n" + groups.map { |g| " #{g.id}: #{g.name}" }.join("\n")
|
7
7
|
return 0
|
8
8
|
rescue ::Huebot::Error => e
|
9
|
-
|
9
|
+
opts.stderr.puts "#{e.class.name}: #{e.message}"
|
10
10
|
return 1
|
11
11
|
end
|
12
12
|
|
13
|
-
def self.run(
|
14
|
-
device_mapper = Huebot::DeviceMapper.new(bridge, opts.inputs)
|
13
|
+
def self.run(sources, lights, groups, opts)
|
15
14
|
programs = sources.map { |src|
|
16
15
|
Huebot::Compiler.build src
|
17
16
|
}
|
18
|
-
|
17
|
+
device_mapper = Huebot::DeviceMapper.new(lights: lights, groups: groups, inputs: opts.inputs)
|
18
|
+
found_errors, _found_warnings, missing_devices = Helpers.check! programs, device_mapper, opts.stderr
|
19
19
|
return 1 if found_errors || missing_devices
|
20
20
|
|
21
|
-
|
21
|
+
logger = opts.debug ? Logging::IOLogger.new(opts.stdout) : nil
|
22
|
+
bot = Huebot::Bot.new(device_mapper, logger: logger, waiter: opts.bot_waiter)
|
22
23
|
programs.each { |prog| bot.execute prog }
|
23
24
|
return 0
|
24
25
|
rescue ::Huebot::Error => e
|
25
|
-
|
26
|
+
opts.stderr.puts "#{e.class.name}: #{e.message}"
|
26
27
|
return 1
|
27
28
|
end
|
28
29
|
|
29
|
-
def self.check(
|
30
|
-
device_mapper = Huebot::DeviceMapper.new(bridge, opts.inputs)
|
30
|
+
def self.check(sources, lights, groups, opts)
|
31
31
|
programs = sources.map { |src|
|
32
32
|
Huebot::Compiler.build src
|
33
33
|
}
|
34
|
-
|
34
|
+
|
35
|
+
# Assume all devices and inputs are correct
|
36
|
+
if opts.no_device_check
|
37
|
+
light_input_names = opts.inputs.select { |i| i.is_a? Light::Input }.map(&:val)
|
38
|
+
lights = programs.reduce(light_input_names) { |acc, p| acc + p.light_names }.uniq.each_with_index.map { |name, i| Light.new(nil, i+1, {"name" => name}) }
|
39
|
+
|
40
|
+
group_input_names = opts.inputs.select { |i| i.is_a? Group::Input }.map(&:val)
|
41
|
+
groups = programs.reduce(group_input_names) { |acc, p| acc + p.group_names }.uniq.each_with_index.map { |name, i| Group.new(nil, i+1, {"name" => name}) }
|
42
|
+
end
|
43
|
+
|
44
|
+
device_mapper = Huebot::DeviceMapper.new(lights: lights, groups: groups, inputs: opts.inputs)
|
45
|
+
found_errors, found_warnings, missing_devices = Helpers.check! programs, device_mapper, opts.stderr
|
35
46
|
return (found_errors || found_warnings || missing_devices) ? 1 : 0
|
36
47
|
rescue ::Huebot::Error => e
|
37
|
-
|
48
|
+
opts.stderr.puts "#{e.class.name}: #{e.message}"
|
38
49
|
return 1
|
39
50
|
end
|
40
51
|
|
41
|
-
def self.get_state(
|
42
|
-
device_mapper = Huebot::DeviceMapper.new(
|
52
|
+
def self.get_state(lights, groups, opts)
|
53
|
+
device_mapper = Huebot::DeviceMapper.new(lights: lights, groups: groups, inputs: opts.inputs)
|
43
54
|
device_mapper.each do |device|
|
44
|
-
puts device.name
|
45
|
-
puts " #{device.get_state}"
|
55
|
+
opts.stdout.puts device.name
|
56
|
+
opts.stdout.puts " #{device.get_state}"
|
46
57
|
end
|
47
58
|
0
|
48
59
|
end
|
49
60
|
|
50
|
-
def self.set_ip
|
51
|
-
config = Huebot::Config.new
|
61
|
+
def self.set_ip(config, ip, opts)
|
52
62
|
config["ip"] = ip
|
53
63
|
0
|
54
64
|
rescue ::Huebot::Error => e
|
55
|
-
|
65
|
+
opts.stderr.puts "#{e.class.name}: #{e.message}"
|
56
66
|
return 1
|
57
67
|
end
|
58
68
|
|
59
|
-
def self.clear_ip
|
60
|
-
config = Huebot::Config.new
|
69
|
+
def self.clear_ip(config, opts)
|
61
70
|
config["ip"] = nil
|
62
71
|
0
|
63
72
|
rescue ::Huebot::Error => e
|
64
|
-
|
73
|
+
opts.stderr.puts "#{e.class.name}: #{e.message}"
|
65
74
|
return 1
|
66
75
|
end
|
67
76
|
|
68
|
-
def self.unregister
|
69
|
-
config = Huebot::Config.new
|
77
|
+
def self.unregister(config, opts)
|
70
78
|
config.clear
|
71
79
|
0
|
72
80
|
rescue ::Huebot::Error => e
|
73
|
-
|
81
|
+
opts.stderr.puts "#{e.class.name}: #{e.message}"
|
74
82
|
return 1
|
75
83
|
end
|
76
84
|
end
|
data/lib/huebot/client.rb
CHANGED
@@ -13,6 +13,10 @@ module Huebot
|
|
13
13
|
TIMER_KEYS = ["timer"].freeze
|
14
14
|
DEADLINE_KEYS = ["until"].freeze
|
15
15
|
HHMM = /\A[0-9]{2}:[0-9]{2}\Z/.freeze
|
16
|
+
PERCENT_CAPTURE = /\A([0-9]+)%\Z/.freeze
|
17
|
+
MIN_KELVIN = 2000
|
18
|
+
MAX_KELVIN = 6530
|
19
|
+
MAX_BRI = 254
|
16
20
|
|
17
21
|
def initialize(api_version)
|
18
22
|
@api_version = api_version
|
@@ -85,7 +89,7 @@ module Huebot
|
|
85
89
|
end
|
86
90
|
|
87
91
|
def map_state_keys(state, errors, warnings)
|
88
|
-
# bugfix to YAML
|
92
|
+
# bugfix to YAML - it parses the "on" key as a Boolean
|
89
93
|
case state.delete true
|
90
94
|
when true
|
91
95
|
state["on"] = true
|
@@ -105,12 +109,27 @@ module Huebot
|
|
105
109
|
|
106
110
|
ctk = state.delete "ctk"
|
107
111
|
case ctk
|
108
|
-
when
|
112
|
+
when MIN_KELVIN..MAX_KELVIN
|
109
113
|
state["ct"] = (1_000_000 / ctk).round # https://en.wikipedia.org/wiki/Mired
|
110
114
|
when nil
|
111
115
|
# pass
|
112
116
|
else
|
113
|
-
errors << "'transition.state.ctk' must be an integer between
|
117
|
+
errors << "'transition.state.ctk' must be an integer between #{MIN_KELVIN} and #{MAX_KELVIN}"
|
118
|
+
end
|
119
|
+
|
120
|
+
case state["bri"]
|
121
|
+
when Integer, nil
|
122
|
+
# pass
|
123
|
+
when PERCENT_CAPTURE
|
124
|
+
n = $1.to_i
|
125
|
+
if n >= 0 and n <= 100
|
126
|
+
percent = n * 0.01
|
127
|
+
state["bri"] = (MAX_BRI * percent).round
|
128
|
+
else
|
129
|
+
errors << "'transition.state.bri' must be an integer or a percent between 0% and 100%"
|
130
|
+
end
|
131
|
+
else
|
132
|
+
errors << "'transition.state.bri' must be an integer or a percent between 0% and 100%"
|
114
133
|
end
|
115
134
|
|
116
135
|
state
|
data/lib/huebot/device_mapper.rb
CHANGED
@@ -2,20 +2,18 @@ module Huebot
|
|
2
2
|
class DeviceMapper
|
3
3
|
Unmapped = Class.new(Error)
|
4
4
|
|
5
|
-
def initialize(
|
6
|
-
|
7
|
-
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@groups_by_id = all_groups.reduce({}) { |a, g| a[g.id] = g; a }
|
11
|
-
@groups_by_name = all_groups.reduce({}) { |a, g| a[g.name] = g; a }
|
5
|
+
def initialize(lights: [], groups:[], inputs: [])
|
6
|
+
@lights_by_id = lights.each_with_object({}) { |l, a| a[l.id] = l }
|
7
|
+
@lights_by_name = lights.each_with_object({}) { |l, a| a[l.name] = l }
|
8
|
+
@groups_by_id = groups.each_with_object({}) { |g, a| a[g.id] = g }
|
9
|
+
@groups_by_name = groups.each_with_object({}) { |g, a| a[g.name] = g }
|
12
10
|
@devices_by_var = inputs.each_with_index.each_with_object({}) { |(x, idx), obj|
|
13
11
|
obj[idx + 1] =
|
14
12
|
case x
|
15
13
|
when Light::Input then @lights_by_id[x.val.to_i] || @lights_by_name[x.val]
|
16
14
|
when Group::Input then @groups_by_id[x.val.to_i] || @groups_by_name[x.val]
|
17
15
|
else raise Error, "Invalid input: #{x}"
|
18
|
-
end || raise(Unmapped, "Could not find #{x.class.name[8..-
|
16
|
+
end || raise(Unmapped, "Could not find #{x.class.name[8..-8].downcase} with id or name '#{x.val}'")
|
19
17
|
}
|
20
18
|
@all = @devices_by_var.values
|
21
19
|
end
|
data/lib/huebot/group.rb
CHANGED
data/lib/huebot/light.rb
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Huebot
|
2
|
+
module Logging
|
3
|
+
class CollectingLogger
|
4
|
+
attr_reader :events
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@events = []
|
8
|
+
@mut = Mutex.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def log(event_type, data = {})
|
12
|
+
now = Time.now
|
13
|
+
@mut.synchronize {
|
14
|
+
@events << [now, event_type, data]
|
15
|
+
}
|
16
|
+
self
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Huebot
|
4
|
+
module Logging
|
5
|
+
class IOLogger
|
6
|
+
def initialize(io)
|
7
|
+
@io = io
|
8
|
+
@mut = Mutex.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def log(event_type, data = {})
|
12
|
+
ts = Time.now.iso8601
|
13
|
+
@mut.synchronize {
|
14
|
+
@io.puts "#{ts} #{event_type} #{data.to_json}"
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/huebot/version.rb
CHANGED
data/lib/huebot.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
module Huebot
|
2
2
|
Error = Class.new(StandardError)
|
3
3
|
|
4
|
-
autoload :Config, 'huebot/config'
|
5
4
|
autoload :Client, 'huebot/client'
|
6
|
-
autoload :CLI, 'huebot/cli'
|
5
|
+
autoload :CLI, 'huebot/cli/cli'
|
7
6
|
autoload :Bridge, 'huebot/bridge'
|
8
7
|
autoload :DeviceState, 'huebot/device_state'
|
9
8
|
autoload :Light, 'huebot/light'
|
10
9
|
autoload :Group, 'huebot/group'
|
11
10
|
autoload :DeviceMapper, 'huebot/device_mapper'
|
12
11
|
autoload :Program, 'huebot/program'
|
13
|
-
autoload :Compiler, 'huebot/compiler'
|
12
|
+
autoload :Compiler, 'huebot/compiler/compiler'
|
14
13
|
autoload :Bot, 'huebot/bot'
|
14
|
+
autoload :Logging, 'huebot/logging/logging'
|
15
15
|
autoload :VERSION, 'huebot/version'
|
16
16
|
end
|
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: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jordan Hollinger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-12-
|
11
|
+
date: 2023-12-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -50,17 +50,21 @@ files:
|
|
50
50
|
- lib/huebot.rb
|
51
51
|
- lib/huebot/bot.rb
|
52
52
|
- lib/huebot/bridge.rb
|
53
|
-
- lib/huebot/cli.rb
|
53
|
+
- lib/huebot/cli/cli.rb
|
54
|
+
- lib/huebot/cli/config.rb
|
54
55
|
- lib/huebot/cli/helpers.rb
|
55
56
|
- lib/huebot/cli/runner.rb
|
56
57
|
- lib/huebot/client.rb
|
57
|
-
- lib/huebot/compiler.rb
|
58
58
|
- lib/huebot/compiler/api_v1.rb
|
59
|
-
- lib/huebot/
|
59
|
+
- lib/huebot/compiler/compiler.rb
|
60
60
|
- lib/huebot/device_mapper.rb
|
61
61
|
- lib/huebot/device_state.rb
|
62
62
|
- lib/huebot/group.rb
|
63
63
|
- lib/huebot/light.rb
|
64
|
+
- lib/huebot/logging/collecting_logger.rb
|
65
|
+
- lib/huebot/logging/io_logger.rb
|
66
|
+
- lib/huebot/logging/logging.rb
|
67
|
+
- lib/huebot/logging/null_logger.rb
|
64
68
|
- lib/huebot/program.rb
|
65
69
|
- lib/huebot/version.rb
|
66
70
|
homepage: https://github.com/jhollinger/huebot
|
data/lib/huebot/config.rb
DELETED
@@ -1,41 +0,0 @@
|
|
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
|
File without changes
|