Ha 0.3.1
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 +7 -0
- data/.DS_Store +0 -0
- data/.byebug_history +11 -0
- data/.gitignore +8 -0
- data/.vscode/launch.json +22 -0
- data/.vscode/settings.json +2 -0
- data/.vscode/tasks.json +44 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +40 -0
- data/Rakefile +10 -0
- data/Readme.md +58 -0
- data/bin/bundle +105 -0
- data/bin/byebug +29 -0
- data/bin/coderay +29 -0
- data/bin/gdb_wrapper +29 -0
- data/bin/ha +28 -0
- data/bin/pry +29 -0
- data/bin/rake +29 -0
- data/bin/rdebug-ide +29 -0
- data/bin/thor +29 -0
- data/exe/ha +4 -0
- data/ha.gemspec +30 -0
- data/lib/ha.rb +22 -0
- data/lib/ha/cli_table.rb +49 -0
- data/lib/ha/context.rb +20 -0
- data/lib/ha/deleteme.rb +12 -0
- data/lib/ha/group.rb +19 -0
- data/lib/ha/hue.rb +78 -0
- data/lib/ha/hue_command.rb +34 -0
- data/lib/ha/hue_resource.rb +13 -0
- data/lib/ha/light.rb +24 -0
- data/lib/ha/rule.rb +19 -0
- data/lib/ha/sensor.rb +33 -0
- data/lib/ha/version.rb +3 -0
- data/test/cli_table_test.rb +32 -0
- data/test/fixtures/state1.json +99 -0
- data/test/fixtures/state2.json +423 -0
- data/test/fixtures/state3.json +938 -0
- data/test/ha_test.rb +11 -0
- data/test/hue_test.rb +18 -0
- data/test/rule_test.rb +24 -0
- data/test/sensor_test.rb +22 -0
- data/test/test_helper.rb +8 -0
- metadata +183 -0
data/bin/pry
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'pry' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("pry", "pry")
|
data/bin/rake
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rake' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rake", "rake")
|
data/bin/rdebug-ide
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rdebug-ide' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("ruby-debug-ide", "rdebug-ide")
|
data/bin/thor
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'thor' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("thor", "thor")
|
data/exe/ha
ADDED
data/ha.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require "ha/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "Ha"
|
9
|
+
spec.version = Ha::VERSION
|
10
|
+
spec.authors = ["Pito Salas"]
|
11
|
+
spec.email = ["pitosalas@gmail.com"]
|
12
|
+
|
13
|
+
spec.summary = %q{Cli to control Home Automation}
|
14
|
+
spec.description = %q{A well behaved CLI for controlling Philips Hue and other home automation devices.}
|
15
|
+
spec.homepage = "http://www.salas.com"
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0")
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_development_dependency "pry-byebug"
|
24
|
+
spec.add_development_dependency "rake", '~> 0'
|
25
|
+
spec.add_development_dependency "minitest"
|
26
|
+
spec.add_development_dependency 'ruby-debug-ide', '~> 0'
|
27
|
+
|
28
|
+
spec.add_runtime_dependency "thor", '~> 0'
|
29
|
+
spec.add_runtime_dependency "faraday", '~> 0'
|
30
|
+
end
|
data/lib/ha.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "thor"
|
2
|
+
require 'pry-byebug'
|
3
|
+
require_relative "ha/version"
|
4
|
+
require_relative "ha/hue_command"
|
5
|
+
require_relative "ha/context"
|
6
|
+
|
7
|
+
module Ha
|
8
|
+
class CLI < Thor
|
9
|
+
desc "hello NAME", "say hello to NAME"
|
10
|
+
|
11
|
+
def hello(name)
|
12
|
+
puts "Hello #{name}"
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "hue SUBCOMMAND", "commands for working with Philips Hue"
|
16
|
+
subcommand "hue", HueCommand
|
17
|
+
|
18
|
+
def self.exit_on_failure?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/ha/cli_table.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
class CliTable
|
2
|
+
attr_accessor :headers, :rows, :column_widths
|
3
|
+
|
4
|
+
def add(headers, rows)
|
5
|
+
@headers = headers
|
6
|
+
@rows = rows
|
7
|
+
end
|
8
|
+
|
9
|
+
def rows_count
|
10
|
+
@rows.length
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
def render
|
15
|
+
result = header_render + "\n"
|
16
|
+
@rows.each do
|
17
|
+
|row|
|
18
|
+
result << row_render(row) + "\n"
|
19
|
+
end
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
def header_render
|
24
|
+
result = ""
|
25
|
+
@headers.each_index { |i| result << (" %#{column_widths[i]}s" % headers[i]) }
|
26
|
+
result
|
27
|
+
end
|
28
|
+
|
29
|
+
def row_render row
|
30
|
+
result = ""
|
31
|
+
@headers.each_index { |i| result << (" %#{column_widths[i]}s" % format_value(row[i])) }
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
def reset
|
36
|
+
@rows = []
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def format_value(val)
|
42
|
+
if [true, false].include? val
|
43
|
+
val ? "Yes" : "No"
|
44
|
+
elsif val.nil?
|
45
|
+
val = "nil"
|
46
|
+
end
|
47
|
+
val
|
48
|
+
end
|
49
|
+
end
|
data/lib/ha/context.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
class Context
|
4
|
+
FILE_SPEC = "#{Dir.home}/.ha"
|
5
|
+
attr_accessor :context
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
if File.exist? FILE_SPEC
|
9
|
+
myfile = File.open(FILE_SPEC, "r")
|
10
|
+
@context = YAML.load(myfile.read)
|
11
|
+
end
|
12
|
+
@context ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def save
|
16
|
+
puts "saving context"
|
17
|
+
myfile = File.open(FILE_SPEC, "w")
|
18
|
+
myfile.write(@context.to_yaml)
|
19
|
+
end
|
20
|
+
end
|
data/lib/ha/deleteme.rb
ADDED
data/lib/ha/group.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative "hue_resource"
|
2
|
+
class Group < HueResource
|
3
|
+
attr_reader :detail, :on, :name, :lights
|
4
|
+
def initialize(key, hashvalue)
|
5
|
+
super
|
6
|
+
@lights = hashvalue["lights"]
|
7
|
+
@lights_s = hashvalue["lights"].join(",")
|
8
|
+
gen_reskey("g")
|
9
|
+
@state.merge! ({"on" =>@lights_s, "name" => @name, "detail" => @detail})
|
10
|
+
end
|
11
|
+
|
12
|
+
def array(selectors)
|
13
|
+
selectors.map { |key| @state[key] }
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.owning(number, grouparray)
|
17
|
+
grouparray.select { |group| group.lights.include? number}
|
18
|
+
end
|
19
|
+
end
|
data/lib/ha/hue.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'faraday'
|
3
|
+
require_relative 'sensor'
|
4
|
+
require_relative 'light'
|
5
|
+
require_relative 'group'
|
6
|
+
require_relative 'rule'
|
7
|
+
|
8
|
+
BRIDGE_IP = "10.0.0.89"
|
9
|
+
USERNAME = "78UEGUotX3otmWxbhiucELCLiiKmaD9E2O5YW-d1"
|
10
|
+
|
11
|
+
|
12
|
+
class Hue
|
13
|
+
|
14
|
+
def initialize(context, bridge_state)
|
15
|
+
@context = context
|
16
|
+
@bridge_state = bridge_state
|
17
|
+
@groups = groups
|
18
|
+
@sensors = sensors
|
19
|
+
@lights = lights
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.bridge_state
|
23
|
+
unless defined? @bridge_state
|
24
|
+
conn = Faraday.new(url: "http://#{BRIDGE_IP}")
|
25
|
+
get = conn.get("/api/#{USERNAME}/")
|
26
|
+
@bridge_state = JSON.parse(get.body)
|
27
|
+
end
|
28
|
+
@bridge_state
|
29
|
+
end
|
30
|
+
|
31
|
+
def pair
|
32
|
+
@context[:useraccount] = "12345"
|
33
|
+
@context.save
|
34
|
+
end
|
35
|
+
|
36
|
+
def sensors
|
37
|
+
parsed = @bridge_state["sensors"]
|
38
|
+
parsed.to_a.map { |sensor_pair| Sensor.new(*sensor_pair)}
|
39
|
+
end
|
40
|
+
|
41
|
+
def lights
|
42
|
+
parsed = @bridge_state["lights"]
|
43
|
+
parsed.to_a.map { |light_pair| Light.new(*light_pair, @groups)}
|
44
|
+
end
|
45
|
+
|
46
|
+
def groups
|
47
|
+
parsed = @bridge_state["groups"]
|
48
|
+
parsed.to_a.map { |group_pair| Group.new(*group_pair)}
|
49
|
+
end
|
50
|
+
|
51
|
+
def rules
|
52
|
+
parsed = @bridge_state["rules"]
|
53
|
+
parsed.to_a.map { |rule_pair| Rule.new(*rule_pair)}
|
54
|
+
end
|
55
|
+
|
56
|
+
def all_a selector
|
57
|
+
items = sensors_a selector
|
58
|
+
items += lights_a selector
|
59
|
+
items += groups_a selector
|
60
|
+
items += rules_a selector
|
61
|
+
end
|
62
|
+
|
63
|
+
def sensors_a selector
|
64
|
+
sensors.map { |sensor| sensor.array(selector)}
|
65
|
+
end
|
66
|
+
|
67
|
+
def lights_a selector
|
68
|
+
lights.map { |light| light.array(selector)}
|
69
|
+
end
|
70
|
+
|
71
|
+
def groups_a selector
|
72
|
+
groups.map { |group| group.array(selector)}
|
73
|
+
end
|
74
|
+
|
75
|
+
def rules_a selector
|
76
|
+
rules.map { |rule| rule.array(selector)}
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative 'cli_table'
|
2
|
+
require_relative 'hue'
|
3
|
+
|
4
|
+
# The 'hue' commmands
|
5
|
+
class HueCommand < Thor
|
6
|
+
desc "list", "list Hue items"
|
7
|
+
def list
|
8
|
+
hue = Hue.new(context, Hue.bridge_state)
|
9
|
+
table = CliTable.new
|
10
|
+
table.headers = list_headers
|
11
|
+
table.rows = hue.all_a list_headers
|
12
|
+
table.column_widths = column_widths
|
13
|
+
puts table.render
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "pair", "run pairing process to allow you to access the hue bridge"
|
17
|
+
def pair
|
18
|
+
Hue.new(context).pair
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def context
|
24
|
+
Context.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def list_headers
|
28
|
+
["id", "name", "on", "detail"]
|
29
|
+
end
|
30
|
+
|
31
|
+
def column_widths
|
32
|
+
["5", "22", "12", "-30"]
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class HueResource
|
2
|
+
def initialize(key, hash_value)
|
3
|
+
@hash_value = hash_value
|
4
|
+
@lastupdated = @hash_value.dig("state", "lastupdated")
|
5
|
+
@detail = hash_value["type"]
|
6
|
+
@name = hash_value["name"]
|
7
|
+
@state = { "key" => key, "lastupdated" => @lastupdated, "detail" => @detail, "name" => @name}
|
8
|
+
end
|
9
|
+
|
10
|
+
def gen_reskey(detail)
|
11
|
+
@state.merge!({ "id" => detail + @state["key"]})
|
12
|
+
end
|
13
|
+
end
|
data/lib/ha/light.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative "hue_resource"
|
2
|
+
class Light < HueResource
|
3
|
+
|
4
|
+
def initialize(key, hashvalue, grouparray)
|
5
|
+
super(key, hashvalue)
|
6
|
+
@grouparray = grouparray
|
7
|
+
@onstate = hashvalue.dig("state", "on")
|
8
|
+
@brightstate = hashvalue.dig("state", "bri")
|
9
|
+
@combinedsate = "#{@onstate} (#{@brightstate})"
|
10
|
+
@number = key
|
11
|
+
@group = Group.owning(key, grouparray)
|
12
|
+
gen_reskey("l")
|
13
|
+
build_name
|
14
|
+
@state.merge! ({"on" => @combinedsate})
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_name
|
18
|
+
@state["detail"] += " in " + Group.owning(@number, @grouparray).first.name
|
19
|
+
end
|
20
|
+
|
21
|
+
def array(selectors)
|
22
|
+
selectors.map { |key| @state[key] }
|
23
|
+
end
|
24
|
+
end
|
data/lib/ha/rule.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative "hue_resource"
|
2
|
+
class Rule < HueResource
|
3
|
+
|
4
|
+
attr_reader :detail, :on, :name, :lights
|
5
|
+
def initialize(key, hashvalue)
|
6
|
+
super(key, hashvalue)
|
7
|
+
@detail = "#{hashvalue["conditions"].length} conds => #{hashvalue["actions"].length} acts"
|
8
|
+
@state.merge! ({"on" => @hash_value["status"], "name" => @name, "detail" => @detail})
|
9
|
+
gen_reskey("r")
|
10
|
+
end
|
11
|
+
|
12
|
+
def array(selectors)
|
13
|
+
selectors.map { |key| @state[key] }
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.owning(number, grouparray)
|
17
|
+
grouparray.select { |group| group.lights.include? number}
|
18
|
+
end
|
19
|
+
end
|