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 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