huebot 1.1.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +16 -9
- data/bin/huebot +39 -19
- data/lib/huebot/bot.rb +56 -27
- 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 +73 -39
- data/lib/huebot/cli/runner.rb +34 -26
- data/lib/huebot/client.rb +1 -1
- data/lib/huebot/compiler/api_v1.rb +69 -14
- data/lib/huebot/{compiler.rb → compiler/compiler.rb} +1 -1
- 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/program.rb +4 -3
- data/lib/huebot/version.rb +1 -1
- data/lib/huebot.rb +3 -3
- metadata +9 -5
- data/lib/huebot/config.rb +0 -41
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 643b636258939c0eb021d13d9369d845be85a5854ca1f6c38fa66c6556551c68
|
4
|
+
data.tar.gz: e5e1e74fded7ed9d456f8a7ce0c65f8f787a08187eb019413bddaff7bddaca74
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '084232072462be01ec7867aadb43df7b476e4c0b6fe0fcc33b1d10cd23f568b8474d83ed450f0487123ce874c33a729d84bfa9e4cc683a59958e3b5fe7bd64e0'
|
7
|
+
data.tar.gz: 99c1ba63f4ff21161017fe42180ae76c5ee27721e32e7663eb5d09906953ef88886dd7e003f40e783e0ede9e3e5e123fab5635b5e0413085bf5882ebcb386bb8
|
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
|
|
@@ -34,13 +34,15 @@ serial:
|
|
34
34
|
state:
|
35
35
|
bri: 150
|
36
36
|
time: 10 # 10 second transition
|
37
|
-
pause:
|
37
|
+
pause:
|
38
|
+
after: 2 # 2 second pause before the next step
|
38
39
|
|
39
40
|
- transition:
|
40
41
|
state:
|
41
42
|
bri: 254
|
42
43
|
time: 10 # 10 second transition
|
43
|
-
pause:
|
44
|
+
pause:
|
45
|
+
after: 2 # 2 second pause before the next step
|
44
46
|
|
45
47
|
- transition:
|
46
48
|
state:
|
@@ -64,7 +66,8 @@ serial:
|
|
64
66
|
|
65
67
|
# Run these steps in parallel in an infinite loop
|
66
68
|
- parallel:
|
67
|
-
loop:
|
69
|
+
loop:
|
70
|
+
infinite: true
|
68
71
|
steps:
|
69
72
|
# Parallel branch 1: Fade inputs #1 and #3 up and down
|
70
73
|
- serial:
|
@@ -77,12 +80,14 @@ serial:
|
|
77
80
|
state:
|
78
81
|
bri: 254
|
79
82
|
time: 10 # transition over 10 seconds
|
80
|
-
pause:
|
83
|
+
pause:
|
84
|
+
after: 5 # pause an extra 5 sec after the transition
|
81
85
|
- transition:
|
82
86
|
state:
|
83
87
|
bri: 25
|
84
88
|
time: 10
|
85
|
-
pause:
|
89
|
+
pause:
|
90
|
+
after: 5
|
86
91
|
|
87
92
|
# Parallel branch 2: Fade inputs #2 and #4 down and up
|
88
93
|
- serial:
|
@@ -95,12 +100,14 @@ serial:
|
|
95
100
|
state:
|
96
101
|
bri: 25
|
97
102
|
time: 10
|
98
|
-
pause:
|
103
|
+
pause:
|
104
|
+
after: 5
|
99
105
|
- transition:
|
100
106
|
state:
|
101
107
|
bri: 254
|
102
108
|
time: 10
|
103
|
-
pause:
|
109
|
+
pause:
|
110
|
+
after: 5
|
104
111
|
```
|
105
112
|
|
106
113
|
[See the Wiki](https://github.com/jhollinger/huebot/wiki) for more documentation and examples.
|
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') if ENV["
|
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,56 +1,67 @@
|
|
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
|
15
19
|
|
16
20
|
def exec(node)
|
17
|
-
|
18
|
-
case i
|
21
|
+
case node.instruction
|
19
22
|
when Program::AST::Transition
|
20
|
-
transition
|
23
|
+
transition node.instruction
|
21
24
|
when Program::AST::SerialControl
|
22
|
-
serial node.children,
|
25
|
+
serial node.children, node.instruction
|
23
26
|
when Program::AST::ParallelControl
|
24
|
-
parallel node.children,
|
27
|
+
parallel node.children, node.instruction
|
25
28
|
else
|
26
|
-
raise Error, "Unexpected instruction '#{
|
29
|
+
raise Error, "Unexpected instruction '#{node.instruction.class.name}'"
|
27
30
|
end
|
28
31
|
end
|
29
32
|
|
30
|
-
def transition(
|
31
|
-
time = (state["transitiontime"] || 4).to_f / 10
|
32
|
-
devices = map_devices
|
33
|
+
def transition(i)
|
34
|
+
time = (i.state["transitiontime"] || 4).to_f / 10
|
35
|
+
devices = map_devices i.devices
|
36
|
+
@logger.log :transition, {devices: devices.map(&:name)}
|
37
|
+
|
38
|
+
wait i.pause.pre if i.pause&.pre
|
33
39
|
devices.map { |device|
|
34
40
|
Thread.new {
|
35
41
|
# TODO error handling
|
36
|
-
device.set_state state
|
37
|
-
|
42
|
+
_res = device.set_state i.state
|
43
|
+
@logger.log :set_state, {device: device.name, state: i.state, result: nil}
|
44
|
+
wait time if i.wait
|
38
45
|
}
|
39
46
|
}.map(&:join)
|
40
|
-
wait
|
47
|
+
wait i.pause.post if i.pause&.post
|
41
48
|
end
|
42
49
|
|
43
|
-
def serial(nodes,
|
44
|
-
|
45
|
-
|
50
|
+
def serial(nodes, i)
|
51
|
+
wait i.pause.pre if i.pause&.pre
|
52
|
+
control_loop(i.loop) { |loop_type|
|
53
|
+
@logger.log :serial, {loop: loop_type}
|
54
|
+
nodes.each { |node|
|
46
55
|
exec node
|
47
56
|
}
|
48
57
|
}
|
49
|
-
wait
|
58
|
+
wait i.pause.post if i.pause&.post
|
50
59
|
end
|
51
60
|
|
52
|
-
def parallel(nodes,
|
53
|
-
|
61
|
+
def parallel(nodes, i)
|
62
|
+
wait i.pause.pre if i.pause&.pre
|
63
|
+
control_loop(i.loop) { |loop_type|
|
64
|
+
@logger.log :parallel, {loop: loop_type}
|
54
65
|
nodes.map { |node|
|
55
66
|
Thread.new {
|
56
67
|
# TODO error handling
|
@@ -58,25 +69,37 @@ module Huebot
|
|
58
69
|
}
|
59
70
|
}.map(&:join)
|
60
71
|
}
|
61
|
-
wait
|
72
|
+
wait i.pause.post if i.pause&.post
|
62
73
|
end
|
63
74
|
|
64
75
|
def control_loop(lp)
|
65
76
|
case lp
|
66
77
|
when Program::AST::InfiniteLoop
|
67
|
-
loop {
|
78
|
+
loop {
|
79
|
+
wait lp.pause.pre if lp.pause&.pre
|
80
|
+
yield :infinite
|
81
|
+
wait lp.pause.post if lp.pause&.post
|
82
|
+
}
|
68
83
|
when Program::AST::CountedLoop
|
69
|
-
lp.n.times {
|
84
|
+
lp.n.times {
|
85
|
+
wait lp.pause.pre if lp.pause&.pre
|
86
|
+
yield :counted
|
87
|
+
wait lp.pause.post if lp.pause&.post
|
88
|
+
}
|
70
89
|
when Program::AST::DeadlineLoop
|
71
90
|
until Time.now >= lp.stop_time
|
72
|
-
|
91
|
+
wait lp.pause.pre if lp.pause&.pre
|
92
|
+
yield :deadline
|
93
|
+
wait lp.pause.post if lp.pause&.post
|
73
94
|
end
|
74
95
|
when Program::AST::TimerLoop
|
75
96
|
sec = ((lp.hours * 60) + lp.minutes) * 60
|
76
97
|
time = 0
|
77
98
|
until time >= sec
|
78
99
|
start = Time.now
|
79
|
-
|
100
|
+
wait lp.pause.pre if lp.pause&.pre
|
101
|
+
yield :timer
|
102
|
+
wait lp.pause.post if lp.pause&.post
|
80
103
|
time += (Time.now - start).round
|
81
104
|
end
|
82
105
|
else
|
@@ -102,8 +125,14 @@ module Huebot
|
|
102
125
|
end
|
103
126
|
|
104
127
|
def wait(seconds)
|
105
|
-
|
106
|
-
|
128
|
+
@logger.log :pause, {time: seconds}
|
129
|
+
@waiter.call seconds
|
130
|
+
end
|
131
|
+
|
132
|
+
module Waiter
|
133
|
+
def self.call(seconds)
|
134
|
+
sleep seconds
|
135
|
+
end
|
107
136
|
end
|
108
137
|
end
|
109
138
|
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,73 +1,91 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
require 'yaml'
|
3
|
+
require 'json'
|
3
4
|
|
4
5
|
module Huebot
|
5
6
|
module CLI
|
6
7
|
module Helpers
|
8
|
+
DEFAULT_API_VERSION = 1.1
|
9
|
+
|
7
10
|
#
|
8
11
|
# Returns the command given to huebot.
|
9
12
|
#
|
10
13
|
# @return [Symbol]
|
11
14
|
#
|
12
|
-
def self.get_cmd
|
13
|
-
|
15
|
+
def self.get_cmd(argv = ARGV)
|
16
|
+
argv[0].to_s.to_sym
|
14
17
|
end
|
15
18
|
|
16
|
-
def self.get_args(min: nil, max: nil, num: nil)
|
17
|
-
args =
|
19
|
+
def self.get_args!(argv = ARGV, min: nil, max: nil, num: nil)
|
20
|
+
args, error = get_args(argv, min: min, max: max, num: num)
|
21
|
+
if error
|
22
|
+
$stderr.puts error
|
23
|
+
exit 1
|
24
|
+
end
|
25
|
+
args
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.get_args(argv = ARGV, min: nil, max: nil, num: nil)
|
29
|
+
args = argv[1..]
|
18
30
|
if num
|
19
31
|
if num != args.size
|
20
|
-
|
21
|
-
exit 1
|
32
|
+
return nil, "Expected #{num} args, found #{args.size}"
|
22
33
|
end
|
23
34
|
elsif min and max
|
24
35
|
if args.size < min or args.size > max
|
25
|
-
|
36
|
+
return nil, "Expected #{min}-#{max} args, found #{args.size}"
|
26
37
|
end
|
27
38
|
elsif min
|
28
39
|
if args.size < min
|
29
|
-
|
30
|
-
exit 1
|
40
|
+
return nil, "Expected at least #{min} args, found #{args.size}"
|
31
41
|
end
|
32
42
|
elsif max
|
33
43
|
if args.size > max
|
34
|
-
|
35
|
-
exit 1
|
44
|
+
return nil, "Expected no more than #{max} args, found #{args.size}"
|
36
45
|
end
|
37
46
|
end
|
38
|
-
args
|
47
|
+
return args, nil
|
39
48
|
end
|
40
49
|
|
41
50
|
#
|
42
51
|
# Parses and returns input from the CLI. Serious errors might result in the program exiting.
|
43
52
|
#
|
44
|
-
# @
|
53
|
+
# @param opts [Huebot::CLI::Options] All given CLI options
|
45
54
|
# @return [Array<Huebot::Program::Src>] Array of given program sources
|
46
55
|
#
|
47
|
-
def self.get_input!
|
48
|
-
|
49
|
-
parser.parse!
|
50
|
-
|
51
|
-
files = ARGV[1..-1]
|
56
|
+
def self.get_input!(opts, argv = ARGV)
|
57
|
+
files = argv[1..-1]
|
52
58
|
if (bad_paths = files.select { |p| !File.exist? p }).any?
|
53
|
-
|
54
|
-
|
59
|
+
opts.stderr.puts "Cannot find #{bad_paths.join ', '}"
|
60
|
+
return []
|
55
61
|
end
|
56
62
|
|
57
63
|
sources = files.map { |path|
|
58
|
-
|
59
|
-
|
64
|
+
ext = File.extname path
|
65
|
+
src =
|
66
|
+
case ext
|
67
|
+
when ".yaml", ".yml"
|
68
|
+
YAML.safe_load(File.read path) || {}
|
69
|
+
when ".json"
|
70
|
+
JSON.load(File.read path) || {}
|
71
|
+
else
|
72
|
+
opts.stderr.puts "Unknown file extension '#{ext}'. Expected .yaml, .yml, or .json"
|
73
|
+
return []
|
74
|
+
end
|
75
|
+
version = (src.delete("version") || DEFAULT_API_VERSION).to_f
|
60
76
|
Program::Src.new(src, path, version)
|
61
77
|
}
|
62
78
|
|
63
|
-
if
|
64
|
-
puts "Please enter your YAML Huebot program below, followed by Ctrl+d:" if
|
65
|
-
|
66
|
-
|
67
|
-
|
79
|
+
if !opts.stdin.isatty or opts.read_stdin
|
80
|
+
opts.stdout.puts "Please enter your YAML or JSON Huebot program below, followed by Ctrl+d:" if opts.read_stdin
|
81
|
+
raw = opts.stdin.read.lstrip
|
82
|
+
src = raw[0] == "{" ? JSON.load(raw) : YAML.safe_load(raw)
|
83
|
+
|
84
|
+
opts.stdout.puts "Executing..." if opts.read_stdin
|
85
|
+
version = (src.delete("version") || DEFAULT_API_VERSION).to_f
|
68
86
|
sources << Program::Src.new(src, "STDIN", version)
|
69
87
|
end
|
70
|
-
|
88
|
+
sources
|
71
89
|
end
|
72
90
|
|
73
91
|
#
|
@@ -95,27 +113,35 @@ module Huebot
|
|
95
113
|
|
96
114
|
all_lights = programs.reduce([]) { |acc, p| acc + p.light_names }
|
97
115
|
if (missing_lights = device_mapper.missing_lights all_lights).any?
|
98
|
-
print_messages! io, "Unknown lights", missing_lights
|
116
|
+
print_messages! io, "Unknown lights", missing_lights unless quiet
|
99
117
|
end
|
100
118
|
|
101
119
|
all_groups = programs.reduce([]) { |acc, p| acc + p.group_names }
|
102
120
|
if (missing_groups = device_mapper.missing_groups all_groups).any?
|
103
|
-
print_messages! io, "Unknown groups", missing_groups
|
121
|
+
print_messages! io, "Unknown groups", missing_groups unless quiet
|
104
122
|
end
|
105
123
|
|
106
124
|
all_vars = programs.reduce([]) { |acc, p| acc + p.device_refs }
|
107
125
|
if (missing_vars = device_mapper.missing_vars all_vars).any?
|
108
|
-
print_messages! io, "Unknown device inputs", missing_vars.map { |d| "$#{d}" }
|
126
|
+
print_messages! io, "Unknown device inputs", missing_vars.map { |d| "$#{d}" } unless quiet
|
109
127
|
end
|
110
128
|
|
111
129
|
invalid_devices = missing_lights.size + missing_groups.size + missing_vars.size
|
112
130
|
return invalid_progs.any?, imperfect_progs.any?, invalid_devices > 0
|
113
131
|
end
|
114
132
|
|
133
|
+
def self.get_opts!
|
134
|
+
opts = default_options
|
135
|
+
parser = option_parser opts
|
136
|
+
parser.parse!
|
137
|
+
opts
|
138
|
+
end
|
139
|
+
|
115
140
|
# Print help and exit
|
116
141
|
def self.help!
|
117
|
-
|
118
|
-
|
142
|
+
opts = default_options
|
143
|
+
parser = option_parser opts
|
144
|
+
opts.stdout.puts parser.help
|
119
145
|
exit 1
|
120
146
|
end
|
121
147
|
|
@@ -135,15 +161,14 @@ module Huebot
|
|
135
161
|
}
|
136
162
|
end
|
137
163
|
|
138
|
-
def self.option_parser
|
139
|
-
|
140
|
-
parser = OptionParser.new { |opts|
|
164
|
+
def self.option_parser(options)
|
165
|
+
OptionParser.new { |opts|
|
141
166
|
opts.banner = %(
|
142
167
|
List all lights and groups:
|
143
168
|
huebot ls
|
144
169
|
|
145
170
|
Run program(s):
|
146
|
-
huebot run prog1.yaml [prog2.
|
171
|
+
huebot run prog1.yaml [prog2.yml [prog3.json ...]] [options]
|
147
172
|
|
148
173
|
Run program from STDIN:
|
149
174
|
cat prog1.yaml | huebot run [options]
|
@@ -168,9 +193,18 @@ module Huebot
|
|
168
193
|
opts.on("-lLIGHT", "--light=LIGHT", "Light ID or name") { |l| options.inputs << Light::Input.new(l) }
|
169
194
|
opts.on("-gGROUP", "--group=GROUP", "Group ID or name") { |g| options.inputs << Group::Input.new(g) }
|
170
195
|
opts.on("-i", "Read program from STDIN") { options.read_stdin = true }
|
171
|
-
opts.on("
|
196
|
+
opts.on("--debug", "Print debug info during run") { options.debug = true }
|
197
|
+
opts.on("--no-device-check", "Don't validate devices against the Bridge ('check' cmd only)") { options.no_device_check = true }
|
198
|
+
opts.on("-h", "--help", "Prints this help") { options.stdout.puts opts; exit }
|
172
199
|
}
|
173
|
-
|
200
|
+
end
|
201
|
+
|
202
|
+
def self.default_options
|
203
|
+
options = Options.new([], false)
|
204
|
+
options.stdin = $stdin
|
205
|
+
options.stdout = $stdout
|
206
|
+
options.stderr = $stderr
|
207
|
+
options
|
174
208
|
end
|
175
209
|
end
|
176
210
|
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
@@ -51,45 +51,61 @@ module Huebot
|
|
51
51
|
end
|
52
52
|
|
53
53
|
def build_transition(t, errors, warnings, inherited_devices = nil)
|
54
|
+
if t.nil?
|
55
|
+
errors << "'transition' may not be blank"
|
56
|
+
t = {}
|
57
|
+
end
|
58
|
+
|
54
59
|
state = build_state(t, errors, warnings)
|
55
60
|
devices = build_devices(t, errors, warnings, inherited_devices)
|
56
|
-
|
61
|
+
pause = build_pause(t, errors, warnings)
|
62
|
+
wait = @api_version >= 1.1 ? build_wait(t, errors, warnings) : true
|
57
63
|
|
58
64
|
errors << "'transition' requires devices" if devices.empty?
|
59
65
|
errors << "Unknown keys in 'transition': #{t.keys.join ", "}" if t.keys.any?
|
60
66
|
|
61
|
-
instruction = Program::AST::Transition.new(state, devices,
|
67
|
+
instruction = Program::AST::Transition.new(state, devices, wait, pause)
|
62
68
|
return instruction, []
|
63
69
|
end
|
64
70
|
|
65
71
|
def build_serial(t, errors, warnings, inherited_devices = nil)
|
72
|
+
if t.nil?
|
73
|
+
errors << "'serial' may not be blank"
|
74
|
+
t = {}
|
75
|
+
end
|
76
|
+
|
66
77
|
lp = build_loop(t, errors, warnings)
|
67
|
-
|
78
|
+
pause = build_pause(t, errors, warnings)
|
68
79
|
devices = build_devices(t, errors, warnings, inherited_devices)
|
69
80
|
children = build_steps(t, errors, warnings, devices)
|
70
81
|
|
71
82
|
errors << "'serial' requires steps" if children.empty?
|
72
83
|
errors << "Unknown keys in 'serial': #{t.keys.join ", "}" if t.keys.any?
|
73
84
|
|
74
|
-
instruction = Program::AST::SerialControl.new(lp,
|
85
|
+
instruction = Program::AST::SerialControl.new(lp, pause)
|
75
86
|
return instruction, children
|
76
87
|
end
|
77
88
|
|
78
89
|
def build_parallel(t, errors, warnings, inherited_devices = nil)
|
90
|
+
if t.nil?
|
91
|
+
errors << "'parallel' may not be blank"
|
92
|
+
t = {}
|
93
|
+
end
|
94
|
+
|
79
95
|
lp = build_loop(t, errors, warnings)
|
80
|
-
|
96
|
+
pause = build_pause(t, errors, warnings)
|
81
97
|
devices = build_devices(t, errors, warnings, inherited_devices)
|
82
98
|
children = build_steps(t, errors, warnings, devices)
|
83
99
|
|
84
100
|
errors << "'parallel' requires steps" if children.empty?
|
85
101
|
errors << "Unknown keys in 'parallel': #{t.keys.join ", "}" if t.keys.any?
|
86
102
|
|
87
|
-
instruction = Program::AST::ParallelControl.new(lp,
|
103
|
+
instruction = Program::AST::ParallelControl.new(lp, pause)
|
88
104
|
return instruction, children
|
89
105
|
end
|
90
106
|
|
91
107
|
def map_state_keys(state, errors, warnings)
|
92
|
-
# bugfix to YAML
|
108
|
+
# bugfix to YAML - it parses the "on" key as a Boolean
|
93
109
|
case state.delete true
|
94
110
|
when true
|
95
111
|
state["on"] = true
|
@@ -167,9 +183,7 @@ module Huebot
|
|
167
183
|
loop_val = t.delete "loop"
|
168
184
|
case loop_val
|
169
185
|
when Hash
|
170
|
-
pause = loop_val
|
171
|
-
errors << "'loop.pause' must be an integer. Found '#{pause.class.name}'" if pause and !pause.is_a? Integer
|
172
|
-
|
186
|
+
pause = build_pause(loop_val, errors, warnings)
|
173
187
|
lp =
|
174
188
|
case loop_val.keys
|
175
189
|
when INFINITE_KEYS
|
@@ -216,11 +230,19 @@ module Huebot
|
|
216
230
|
Program::AST::DeadlineLoop.new(stop_time)
|
217
231
|
end
|
218
232
|
|
219
|
-
def
|
220
|
-
|
221
|
-
|
233
|
+
def build_pause(t, errors, warnings)
|
234
|
+
case @api_version
|
235
|
+
when 1.0 then build_pause_1_0(t, errors, warnings)
|
236
|
+
when 1.1 then build_pause_1_1(t, errors, warnings)
|
237
|
+
else raise Error, "Unknown api version '#{@api_version}'"
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def build_pause_1_0(t, errors, warnings)
|
242
|
+
pause_val = t.delete "pause"
|
243
|
+
case pause_val
|
222
244
|
when Integer, Float
|
223
|
-
|
245
|
+
Program::AST::Pause.new(nil, pause_val)
|
224
246
|
when nil
|
225
247
|
nil
|
226
248
|
else
|
@@ -229,6 +251,39 @@ module Huebot
|
|
229
251
|
end
|
230
252
|
end
|
231
253
|
|
254
|
+
def build_pause_1_1(t, errors, warnings)
|
255
|
+
pause_val = t.delete "pause"
|
256
|
+
case pause_val
|
257
|
+
when Integer, Float
|
258
|
+
Program::AST::Pause.new(nil, pause_val)
|
259
|
+
when Hash
|
260
|
+
pre = pause_val.delete "before"
|
261
|
+
post = pause_val.delete "after"
|
262
|
+
errors << "'pause.before' must be an integer or float" unless pre.nil? or pre.is_a? Integer or pre.is_a? Float
|
263
|
+
errors << "'pause.after' must be an integer or float" unless post.nil? or post.is_a? Integer or post.is_a? Float
|
264
|
+
errors << "Unknown keys in 'pause': #{pause_val.keys.join ", "}" if pause_val.keys.any?
|
265
|
+
Program::AST::Pause.new(pre, post)
|
266
|
+
when nil
|
267
|
+
nil
|
268
|
+
else
|
269
|
+
errors << "'pause' must be an integer or float"
|
270
|
+
nil
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def build_wait(t, errors, warnings)
|
275
|
+
wait = t.delete "wait"
|
276
|
+
case wait
|
277
|
+
when true, false
|
278
|
+
wait
|
279
|
+
when nil
|
280
|
+
true
|
281
|
+
else
|
282
|
+
errors << "'transition.wait' must be true or false"
|
283
|
+
true
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
232
287
|
def build_devices(t, errors, warnings, inherited_devices = nil)
|
233
288
|
devices_ref = t.delete("devices") || {}
|
234
289
|
return inherited_devices if devices_ref.empty? and inherited_devices
|
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/program.rb
CHANGED
@@ -16,15 +16,16 @@ module Huebot
|
|
16
16
|
module AST
|
17
17
|
Node = Struct.new(:instruction, :children, :errors, :warnings)
|
18
18
|
|
19
|
-
Transition = Struct.new(:state, :devices, :
|
20
|
-
SerialControl = Struct.new(:loop, :
|
21
|
-
ParallelControl = Struct.new(:loop, :
|
19
|
+
Transition = Struct.new(:state, :devices, :wait, :pause)
|
20
|
+
SerialControl = Struct.new(:loop, :pause)
|
21
|
+
ParallelControl = Struct.new(:loop, :pause)
|
22
22
|
|
23
23
|
InfiniteLoop = Struct.new(:pause)
|
24
24
|
CountedLoop = Struct.new(:n, :pause)
|
25
25
|
TimerLoop = Struct.new(:hours, :minutes, :pause)
|
26
26
|
DeadlineLoop = Struct.new(:stop_time, :pause)
|
27
27
|
|
28
|
+
Pause = Struct.new(:pre, :post)
|
28
29
|
DeviceRef = Struct.new(:ref)
|
29
30
|
Light = Struct.new(:name)
|
30
31
|
Group = Struct.new(:name)
|
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.3.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-22 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
|