huebot 1.0.0 → 1.2.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: 5da1b1dcd0f32ce09e44155b39d7bc6ed92aa285944fc3a67801cf02642737c0
4
- data.tar.gz: 9116d3a891e608276e64517f9252a42cddd615ca9952c55e816ca53196bdde15
3
+ metadata.gz: 7febcd2a42112b4c3636581d9419a58c490a1157637e143fd46e6e7d94aefba5
4
+ data.tar.gz: d4a9ab3178d381786595f14a14899e55af7789b4650cfa2da1928b83b5fe2135
5
5
  SHA512:
6
- metadata.gz: fc1ae1f775c685e635fe36d85ef33548a50163f80378f42301f73e9bdf868343fecb4b9a9fa64490466771dc44b51f95745587bc32423e65cdc306dae0e01fa9
7
- data.tar.gz: b5721fb115fc0f8c38e9389063a9012a51d7aa2fa0cf2f1b7a2d93812d916b2ffb1f27508358483a9656b4bdb09d7f78fc7bd9e4f65ff599214c9cdd2f0e8a93
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.yml --light="Office Desk"
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: true
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
- retval = Huebot::CLI::Runner.ls bridge
18
+
19
+ retval = Huebot::CLI::Runner.ls(bridge.lights, bridge.groups, opts)
17
20
  exit retval
18
21
 
19
22
  when :run
20
- opts, sources = cli.get_input!
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
- retval = Huebot::CLI::Runner.run(bridge, sources, opts)
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, sources = cli.get_input!
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
- bridge, error = Huebot::Bridge.connect
42
- if error
43
- $stderr.puts error
44
- exit 1
45
- end
46
- retval = Huebot::CLI::Runner.check(bridge, sources, opts)
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, _sources = cli.get_input!
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
- retval = Huebot::CLI::Runner.get_state(bridge, opts.inputs)
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
- ip = cli.get_args(num: 1).first
66
- retval = Huebot::CLI::Runner.set_ip ip
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.get_args(num: 0)
71
- retval = Huebot::CLI::Runner.unregister
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.get_args(num: 0)
76
- retval = Huebot::CLI::Runner.unregister
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
- #@client = device_mapper.bridge.client
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
- nodes.map { |node|
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 { yield }
76
+ loop {
77
+ yield :infinite
78
+ wait lp.pause if lp.pause
79
+ }
68
80
  when Program::AST::CountedLoop
69
- lp.n.times { yield }
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
- # TODO sleep in small bursts in a loop so can detect if an Interrupt was caught
106
- sleep seconds
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(config = Huebot::Config.new)
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
@@ -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
- ARGV[0].to_s.to_sym
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 = ARGV[1..]
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
- $stderr.puts "Expected #{num} args, found #{args.size}"
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
- $stderr.puts "Expected #{min}-#{max} args, found #{args.size}"
34
+ return nil, "Expected #{min}-#{max} args, found #{args.size}"
26
35
  end
27
36
  elsif min
28
37
  if args.size < min
29
- $stderr.puts "Expected at least #{num} args, found #{args.size}"
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
- $stderr.puts "Expected no more than #{num} args, found #{args.size}"
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
- # @return [Huebot::CLI::Options] All given CLI options
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
- options, parser = option_parser
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
- $stderr.puts "Cannot find #{bad_paths.join ', '}"
54
- exit 1
57
+ opts.stderr.puts "Cannot find #{bad_paths.join ', '}"
58
+ return []
55
59
  end
56
60
 
57
61
  sources = files.map { |path|
58
- src = YAML.load_file(path)
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 options.read_stdin
64
- src = YAML.load($stdin.read)
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
- return options, sources
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
- _, parser = option_parser
116
- puts parser.help
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
- options = Options.new([], false)
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 file1.yml [file2.yml [file3.yml ...]] [options]
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 file1.yml [file2.yml [file3.yml ...]] [options]
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("-h", "--help", "Prints this help") { puts opts; exit }
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
- return options, parser
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
@@ -1,76 +1,84 @@
1
1
  module Huebot
2
2
  module CLI
3
3
  module Runner
4
- def self.ls(bridge)
5
- puts "Lights\n" + bridge.lights.map { |l| " #{l.id}: #{l.name}" }.join("\n") + \
6
- "\nGroups\n" + bridge.groups.map { |g| " #{g.id}: #{g.name}" }.join("\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
- $stderr.puts "#{e.class.name}: #{e.message}"
9
+ opts.stderr.puts "#{e.class.name}: #{e.message}"
10
10
  return 1
11
11
  end
12
12
 
13
- def self.run(bridge, sources, opts)
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
- found_errors, _found_warnings, missing_devices = Helpers.check! programs, device_mapper, $stderr
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
- bot = Huebot::Bot.new(device_mapper)
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
- $stderr.puts "#{e.class.name}: #{e.message}"
26
+ opts.stderr.puts "#{e.class.name}: #{e.message}"
26
27
  return 1
27
28
  end
28
29
 
29
- def self.check(bridge, sources, opts)
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
- found_errors, found_warnings, missing_devices = Helpers.check! programs, device_mapper, $stdout
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
- $stderr.puts "#{e.class.name}: #{e.message}"
48
+ opts.stderr.puts "#{e.class.name}: #{e.message}"
38
49
  return 1
39
50
  end
40
51
 
41
- def self.get_state(bridge, inputs)
42
- device_mapper = Huebot::DeviceMapper.new(bridge, inputs)
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
- $stderr.puts "#{e.class.name}: #{e.message}"
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
- $stderr.puts "#{e.class.name}: #{e.message}"
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
- $stderr.puts "#{e.class.name}: #{e.message}"
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
@@ -10,7 +10,7 @@ module Huebot
10
10
 
11
11
  attr_reader :config
12
12
 
13
- def initialize(config = Huebot::Config.new)
13
+ def initialize(config = Huebot::CLI::Config.new)
14
14
  @config = config
15
15
  @ip = config["ip"] # NOTE will usually be null
16
16
  @username = nil
@@ -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 2000..6530
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 2700 and 6530"
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
@@ -2,20 +2,18 @@ module Huebot
2
2
  class DeviceMapper
3
3
  Unmapped = Class.new(Error)
4
4
 
5
- def initialize(bridge, inputs = [])
6
- all_lights, all_groups = bridge.lights, bridge.groups
7
-
8
- @lights_by_id = all_lights.reduce({}) { |a, l| a[l.id] = l; a }
9
- @lights_by_name = all_lights.reduce({}) { |a, l| a[l.name] = l; a }
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..-6].downcase} with id or name '#{x.val}'")
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
@@ -14,7 +14,6 @@ module Huebot
14
14
  @client = client
15
15
  @id = id.to_i
16
16
  @name = attrs.fetch("name")
17
- @attrs = attrs
18
17
  end
19
18
 
20
19
  private
data/lib/huebot/light.rb CHANGED
@@ -14,7 +14,6 @@ module Huebot
14
14
  @client = client
15
15
  @id = id.to_i
16
16
  @name = attrs.fetch("name")
17
- @attrs = attrs
18
17
  end
19
18
 
20
19
  private
@@ -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
@@ -0,0 +1,7 @@
1
+ module Huebot
2
+ module Logging
3
+ autoload :NullLogger, 'huebot/logging/null_logger'
4
+ autoload :CollectingLogger, 'huebot/logging/collecting_logger'
5
+ autoload :IOLogger, 'huebot/logging/io_logger'
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module Huebot
2
+ module Logging
3
+ class NullLogger
4
+ def log(_event_type, _data = {})
5
+ self
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,4 +1,4 @@
1
1
  module Huebot
2
2
  # Gem version
3
- VERSION = '1.0.0'
3
+ VERSION = '1.2.0'
4
4
  end
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.0.0
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-18 00:00:00.000000000 Z
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/config.rb
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