Ha 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|