hacky_hal 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/.gitignore +3 -0
  2. data/Gemfile +12 -0
  3. data/LICENSE +20 -0
  4. data/README.markdown +57 -0
  5. data/hacky_hal.gemspec +20 -0
  6. data/lib/hacky_hal/device_controllers/base.rb +14 -0
  7. data/lib/hacky_hal/device_controllers/epson_projector.rb +180 -0
  8. data/lib/hacky_hal/device_controllers/generic_serial_port.rb +108 -0
  9. data/lib/hacky_hal/device_controllers/generic_ssh.rb +74 -0
  10. data/lib/hacky_hal/device_controllers/io_gear_avior_hdmi_switch.rb +58 -0
  11. data/lib/hacky_hal/device_controllers/linux_computer.rb +36 -0
  12. data/lib/hacky_hal/device_controllers/osx_computer.rb +19 -0
  13. data/lib/hacky_hal/device_controllers/roku.rb +121 -0
  14. data/lib/hacky_hal/device_controllers/yamaha_av_receiver.rb +202 -0
  15. data/lib/hacky_hal/device_resolvers/base.rb +9 -0
  16. data/lib/hacky_hal/device_resolvers/ssdp.rb +36 -0
  17. data/lib/hacky_hal/device_resolvers/static_uri.rb +17 -0
  18. data/lib/hacky_hal/log.rb +33 -0
  19. data/lib/hacky_hal/options.rb +21 -0
  20. data/lib/hacky_hal/registry.rb +22 -0
  21. data/lib/hacky_hal/util.rb +23 -0
  22. data/lib/hacky_hal.rb +17 -0
  23. data/spec/hacky_hal/device_controllers/base_spec.rb +16 -0
  24. data/spec/hacky_hal/device_controllers/generic_serial_port_spec.rb +167 -0
  25. data/spec/hacky_hal/device_controllers/generic_ssh_spec.rb +141 -0
  26. data/spec/hacky_hal/device_controllers/roku_spec.rb +30 -0
  27. data/spec/hacky_hal/device_controllers/yamaha_av_receiver_spec.rb +29 -0
  28. data/spec/hacky_hal/device_resolvers/base_spec.rb +8 -0
  29. data/spec/hacky_hal/device_resolvers/ssdp_spec.rb +49 -0
  30. data/spec/hacky_hal/device_resolvers/static_uri_spec.rb +16 -0
  31. data/spec/hacky_hal/log_spec.rb +26 -0
  32. data/spec/hacky_hal/options_spec.rb +22 -0
  33. data/spec/hacky_hal/registry_spec.rb +30 -0
  34. data/spec/hacky_hal/util_spec.rb +27 -0
  35. data/spec/spec_helper.rb +11 -0
  36. data/support/hal-9000-small.png +0 -0
  37. data/support/hal-9000.png +0 -0
  38. metadata +144 -0
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ /Gemfile.lock
2
+ .DS_Store
3
+ **.orig
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "upnp-nickewing"
6
+ gem "net-ssh"
7
+ gem "serialport"
8
+
9
+ group :test do
10
+ gem "rspec"
11
+ gem "rspec-mocks"
12
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (C) 2013 by Nick Ewing
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
20
+
data/README.markdown ADDED
@@ -0,0 +1,57 @@
1
+ ![HackyHal](https://raw.github.com/nickewing/hacky_hal/master/support/hal-9000-small.png) HackyHAL
2
+ ========
3
+
4
+ What is it?
5
+ -----------
6
+
7
+ HackyHAL (Hacky Home Automation Library) is in its current form is a small
8
+ Ruby library meant to control devices through the network or serial ports.
9
+ The number of supported devices is currently very limited, however hopefully
10
+ the library will grow to support more devices over time.
11
+
12
+ Who is this for?
13
+ ----------------
14
+ This project is for anyone wishing to write their own custom home automation
15
+ software/scripts. It is not user friendly and does not have any form of
16
+ built-in UI.
17
+
18
+ What devices are supported?
19
+ ---------------------------
20
+ Supported functionality varies greatly with each device.
21
+
22
+ * **Epson Projector** (via serial port. Tested on HC8350, though likely also
23
+ works with other models)
24
+ * **Yamaha AV Receiver** (via network. Tested on RX-A1020. Should work with
25
+ RX-A2020 and RX-A3020 as well)
26
+ * **Roku** (via network)
27
+ * **Iogear AVIOR HDMI Switch** (via serial port. 8x1 GHSW8181 or 4x1 GHSW8141)
28
+ * **SSH accessible computer**
29
+
30
+ How can I use it?
31
+ -------------------
32
+ HackyHAL is simply a library to be used however you want.
33
+
34
+ You'll likely want to run a server utilizing HackyHAL on a networked computer (a
35
+ Raspberry Pi works great).
36
+ You'd then attach any serial port devices to that computer. You could then
37
+ create a mobile app to control the server. See the examples directory.
38
+
39
+ Can I contribue?
40
+ ----------------
41
+ Please do! It would be great to see this library grow to support many more
42
+ devices. Just fork, make your changes, and send a pull request. Please be sure
43
+ to write tests for contributions.
44
+
45
+ Contributors
46
+ ------------
47
+ * Nick Ewing
48
+
49
+ Special thanks to [Mischa McLachlan](https://www.iconfinder.com/icons/27626/9000_hal_light_red_space_icon) for the HAL 9000 icon.
50
+
51
+ License and Copyright
52
+ ---------------------
53
+
54
+ HackyHAL is distributed under the MIT License. See LICENSE.
55
+
56
+ Copyright © 2013 Nick Ewing
57
+
data/hacky_hal.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = "hacky_hal"
5
+ gem.version = "0.2.0"
6
+
7
+ gem.authors = ["Nick Ewing"]
8
+ gem.email = ["nick@nickewing.net"]
9
+ gem.description = "HackyHAL - Hacky Home Automation Library"
10
+ gem.summary = gem.description
11
+ gem.homepage = ""
12
+
13
+ gem.add_dependency("upnp-nickewing", "~> 0.1.0")
14
+ gem.add_dependency("serialport", "~> 1.1.0")
15
+ gem.add_dependency("net-ssh", "~> 2.6.0")
16
+
17
+ gem.files = `git ls-files`.split($\)
18
+ gem.test_files = gem.files.grep(/^spec/)
19
+ gem.require_paths = ["lib"]
20
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "../log"
2
+ require_relative "../options"
3
+
4
+ module HackyHAL
5
+ module DeviceControllers
6
+ class Base
7
+ include Options
8
+
9
+ def log(message, level = :info)
10
+ Log.instance.send(level, "#{options[:name]}: #{message}")
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,180 @@
1
+ require_relative "generic_serial_port"
2
+
3
+ module HackyHAL
4
+ module DeviceControllers
5
+ class EpsonProjector < GenericSerialPort
6
+
7
+ SERIAL_PORT_OPTIONS = {
8
+ baud_rate: 9600,
9
+ data_bits: 8,
10
+ stop_bits: 1,
11
+ parity: SerialPort::NONE,
12
+ flow_control: SerialPort::NONE
13
+ }
14
+
15
+ SOURCE_COMMAND_TIMEOUT = 5
16
+
17
+ POWER_STATUS_TO_SYMBOL = {
18
+ "00" => :standby, # standby, network off
19
+ "01" => :on,
20
+ "02" => :warming_up,
21
+ "03" => :cooling_down,
22
+ "04" => :standby, # standby, network on
23
+ "05" => :standby # standby, abnormal
24
+ }
25
+
26
+ DEVICE_SOURCE_ID_TO_NAME = {
27
+ "HC8350" => {
28
+ "30" => "HDMI1",
29
+ "A0" => "HDMI2",
30
+ "14" => "Component (YCbCr)",
31
+ "15" => "Component (YPbPr)",
32
+ "21" => "PC",
33
+ "41" => "Video (RCA)",
34
+ "42" => "S-Video"
35
+ }
36
+ }
37
+
38
+ DEVICE_CMODE_ID_TO_NAME = {
39
+ "HC8350" => {
40
+ "06" => "Dynamic",
41
+ "0C" => "Living Room",
42
+ "07" => "Natural",
43
+ "15" => "Cinema",
44
+ "0B" => "x.v.Color"
45
+ }
46
+ }
47
+
48
+ DEVICE_ASPECT_RATIO_ID_TO_NAME = {
49
+ "HC8350" => {
50
+ "00" => "Normal"
51
+ }
52
+ }
53
+
54
+ ERROR_CODE_TO_MESSAGE = {
55
+ "00" => "There is no error or the error is recovered",
56
+ "01" => "Fan error",
57
+ "03" => "Lamp failure at power on",
58
+ "04" => "High internal temperature error",
59
+ "06" => "Lamp error",
60
+ "07" => "Open Lamp cover door error",
61
+ "08" => "Cinema filter error",
62
+ "09" => "Electric dual-layered capacitor is disconnected",
63
+ "0A" => "Auto iris error",
64
+ "0B" => "Subsystem Error",
65
+ "0C" => "Low air flow error",
66
+ "0D" => "Air filter air flow sensor error",
67
+ "0E" => "Power supply unit error (Ballast)",
68
+ "0F" => "Shutter error",
69
+ "10" => "Cooling system error (peltiert element)",
70
+ "11" => "Cooling system error (Pump)"
71
+ }
72
+
73
+ attr_reader :model
74
+
75
+ def initialize(options = {})
76
+ options[:serial_options] = SERIAL_PORT_OPTIONS
77
+ super(options)
78
+ @model = options[:model]
79
+ end
80
+
81
+ def on
82
+ power_status == :on
83
+ end
84
+
85
+ def on=(value)
86
+ value = value ? "ON" : "OFF"
87
+ write_command("PWR #{value}")
88
+ read_command(1)
89
+ end
90
+
91
+ def power_status
92
+ write_command("PWR?")
93
+ status_code = get_command_output(read_command)
94
+ POWER_STATUS_TO_SYMBOL[status_code] || :unknown
95
+ end
96
+
97
+ def lamp_hours
98
+ write_command("LAMP?")
99
+ get_command_output(read_command).to_i
100
+ end
101
+
102
+ def source
103
+ write_command("SOURCE?")
104
+ source_id = get_command_output(read_command)
105
+
106
+ source_name_hash = DEVICE_SOURCE_ID_TO_NAME[model]
107
+ source_name = source_name_hash[source_id] || "Unknown"
108
+
109
+ {id: source_id, name: source_name}
110
+ end
111
+
112
+ def source=(source_id)
113
+ write_command("SOURCE #{source_id}")
114
+ read_command(SOURCE_COMMAND_TIMEOUT)
115
+ end
116
+
117
+ def color_mode
118
+ write_command("CMODE?")
119
+ color_mode_id = get_command_output(read_command)
120
+
121
+ color_mode_hash = DEVICE_CMODE_ID_TO_NAME[model]
122
+ color_mode_name = color_mode_hash[color_mode_id] || "Unknown"
123
+
124
+ {id: color_mode_id, name: color_mode_name}
125
+ end
126
+
127
+ def color_mode=(color_mode_id)
128
+ write_command("CMODE #{color_mode_id}")
129
+ read_command
130
+ end
131
+
132
+ def aspect_ratio
133
+ write_command("ASPECT?")
134
+ aspect_ratio_id = get_command_output(read_command)
135
+
136
+ aspect_ratio_hash = DEVICE_ASPECT_RATIO_ID_TO_NAME[model]
137
+ aspect_ratio_name = aspect_ratio_hash[aspect_ratio_id] || "Unknown"
138
+
139
+ {id: aspect_ratio_id, name: aspect_ratio_name}
140
+ end
141
+
142
+ def aspect_ratio=(aspect_ratio_id)
143
+ write_command("ASPECT #{aspect_ratio_id}")
144
+ read_command
145
+ end
146
+
147
+ def error
148
+ write_command("ERR?")
149
+ set_read_timeout(DEFAULT_COMMAND_TIMEOUT)
150
+ error_code = get_command_output(read_line)
151
+ if error_code
152
+ error_message = ERROR_CODE_TO_MESSAGE[error_code] || "Unknown"
153
+ {code: error_code, message: error_message}
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ # overrides GenericSerialPort#error_response?
160
+ def error_response?(response)
161
+ response =~ /^:?ERR/
162
+ end
163
+
164
+ def handle_error
165
+ error_details = error
166
+
167
+ if error_details
168
+ raise CommandError, "Projector returned error code #{error_details[:code]}: #{error_details[:message]}."
169
+ else
170
+ raise CommandError, "Projector returned an error."
171
+ end
172
+ end
173
+
174
+ def get_command_output(output)
175
+ output =~ /^:?\w+=(.+)\r:$/
176
+ $1 ? $1.chomp : nil
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,108 @@
1
+ require "serialport"
2
+ require_relative "base"
3
+
4
+ module HackyHAL
5
+ module DeviceControllers
6
+ class GenericSerialPort < Base
7
+ MAX_READ_WRITE_RETRIES = 1
8
+
9
+ DEFAULT_COMMAND_TIMEOUT = 3
10
+
11
+ DEFAULT_OPTIONS = {
12
+ baud_rate: 9600,
13
+ data_bits: 8,
14
+ stop_bits: 1,
15
+ parity: SerialPort::NONE,
16
+ flow_control: SerialPort::NONE
17
+ }
18
+
19
+ class CommandError < Exception; end
20
+
21
+ attr_reader :serial_device_path, :serial_options
22
+
23
+ def initialize(options = {})
24
+ super(options)
25
+ ensure_option(:serial_device_path)
26
+
27
+ @serial_device_path = options[:serial_device_path]
28
+ @serial_options = DEFAULT_OPTIONS.merge(options[:serial_options] || {})
29
+ end
30
+
31
+ def write_command(command)
32
+ read_write_retry do
33
+ serial_port.flush
34
+ command = "#{command}\r\n"
35
+ log("Wrote: #{command.inspect}", :debug)
36
+ serial_port.write(command)
37
+ true
38
+ end
39
+ end
40
+
41
+ def read_command(timeout = DEFAULT_COMMAND_TIMEOUT)
42
+ set_read_timeout(timeout) if timeout
43
+
44
+ begin
45
+ output = read_line
46
+ log("Read: #{output.inspect}", :debug)
47
+ handle_error if error_response?(output)
48
+ output
49
+ rescue EOFError
50
+ log("Read EOFError", :warn)
51
+ nil
52
+ end
53
+ end
54
+
55
+ def disconnect
56
+ @serial_port.close if @serial_port
57
+ @serial_port = nil
58
+ end
59
+
60
+ def serial_port
61
+ @serial_port ||= SerialPort.new(serial_device_path).tap do |s|
62
+ s.baud = serial_options[:baud_rate]
63
+ s.data_bits = serial_options[:data_bits]
64
+ s.stop_bits = serial_options[:stop_bits]
65
+ s.parity = serial_options[:parity]
66
+ s.flow_control = serial_options[:flow_control]
67
+ end
68
+ end
69
+
70
+ protected
71
+
72
+ def read_write_retry
73
+ retries = 0
74
+
75
+ begin
76
+ yield
77
+ rescue Errno::EIO => e
78
+ if retries < MAX_READ_WRITE_RETRIES
79
+ retries += 1
80
+ disconnect
81
+ retry
82
+ else
83
+ raise e
84
+ end
85
+ end
86
+ end
87
+
88
+ def handle_error
89
+ raise CommandError, "Serial device returned an error."
90
+ end
91
+
92
+ def set_read_timeout(timeout)
93
+ serial_port.read_timeout = timeout * 1000
94
+ end
95
+
96
+ def read_line
97
+ read_write_retry do
98
+ serial_port.readline
99
+ end
100
+ end
101
+
102
+ def error_response?(response)
103
+ false
104
+ end
105
+
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,74 @@
1
+ require 'net/ssh'
2
+ require_relative "base"
3
+
4
+ module HackyHAL
5
+ module DeviceControllers
6
+ class GenericSsh < Base
7
+ MAX_COMMAND_RETRIES = 1
8
+
9
+ attr_reader :host, :user, :ssh_options
10
+
11
+ def initialize(options)
12
+ super(options)
13
+
14
+ ensure_option(:host)
15
+ ensure_option(:user)
16
+
17
+ @host = options[:host]
18
+ @user = options[:user]
19
+ @ssh_options = options[:ssh_options] || {}
20
+ end
21
+
22
+ def exec(command)
23
+ out = nil
24
+ retries = 0
25
+
26
+ begin
27
+ connect unless connected?
28
+ log("Command: #{command.inspect}", :debug)
29
+ out = ssh_exec(command)
30
+ log("Output: #{out.inspect}", :debug)
31
+ rescue Net::SSH::Disconnect, EOFError => e
32
+ log("Command failed: #{e.class.name} - #{e.message}", :warn)
33
+ disconnect
34
+
35
+ if retries < MAX_COMMAND_RETRIES
36
+ log("Retrying last command", :warn)
37
+ retries += 1
38
+ retry
39
+ end
40
+ end
41
+
42
+ out
43
+ end
44
+
45
+ def connect
46
+ disconnect if @ssh
47
+ @ssh = Net::SSH.start(host, user, ssh_options)
48
+ rescue SocketError, Net::SSH::Exception, Errno::EHOSTUNREACH => e
49
+ log("Failed to connect: #{e.class.name} - #{e.message}", :warn)
50
+ end
51
+
52
+ def connected?
53
+ @ssh && !@ssh.closed?
54
+ end
55
+
56
+ def disconnect
57
+ @ssh.close if connected?
58
+ rescue Net::SSH::Disconnect
59
+ ensure
60
+ @ssh = nil
61
+ end
62
+
63
+ private
64
+
65
+ def ssh_exec(command)
66
+ out = []
67
+ @ssh.exec!(command) do |channel, stream, data|
68
+ out << data
69
+ end
70
+ out.join("")
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,58 @@
1
+ require_relative "generic_serial_port"
2
+
3
+ module HackyHAL
4
+ module DeviceControllers
5
+ class IoGearAviorHdmiSwitch < GenericSerialPort
6
+
7
+ SERIAL_PORT_OPTIONS = {
8
+ baud_rate: 19200,
9
+ data_bits: 8,
10
+ stop_bits: 1,
11
+ parity: SerialPort::NONE,
12
+ flow_control: SerialPort::NONE
13
+ }
14
+
15
+ def initialize(options = {})
16
+ options[:serial_options] = SERIAL_PORT_OPTIONS
17
+ super(options)
18
+ end
19
+
20
+ def input=(value)
21
+ unless value.is_a?(Fixnum)
22
+ raise ArgumentError, "Input value must be an integer."
23
+ end
24
+
25
+ unless value > 0
26
+ raise ArgumentError, "Input value must be positive."
27
+ end
28
+
29
+ value = value.to_s.rjust(2, "0")
30
+ write_command("sw i#{value}")
31
+ read_command
32
+ end
33
+
34
+ def switch_to_next_input
35
+ write_command("sw +")
36
+ read_command
37
+ end
38
+
39
+ def switch_to_previous_input
40
+ write_command("sw -")
41
+ read_command
42
+ end
43
+
44
+ def power_on_detection=(value)
45
+ value = value ? "on" : "off"
46
+ write_command("pod #{value}")
47
+ read_command
48
+ end
49
+
50
+ private
51
+
52
+ # overrides GenericSerialPort#error_response?
53
+ def error_response?(response)
54
+ response =~ /Command incorrect/
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,36 @@
1
+ require_relative "generic_ssh"
2
+
3
+ module HackyHAL
4
+ module DeviceControllers
5
+ class LinuxComputer < GenericSsh
6
+ def mirror_screens(source_screen, dest_screen)
7
+ xrandr_command("--output #{dest_screen} --same-as #{source_screen}")
8
+ end
9
+
10
+ def set_screen_position(screen_1, screen_2, position)
11
+ xrandr_command("--output #{screen_1} --#{position}-of #{screen_2}")
12
+ end
13
+
14
+ def reset_display_settings(screen)
15
+ xrandr_command("--output #{screen} --auto")
16
+ end
17
+
18
+ private
19
+
20
+ def xrandr_command(options)
21
+ x_env_variables = [
22
+ "CONSOLE=$(sudo fgconsole)",
23
+ "SESSION=$(who -s | grep tty$CONSOLE | tr '()' ' ')",
24
+ "XUSER=$(echo $SESSION | awk '{print $1}')",
25
+ "DISPLAY=$(echo $SESSION | awk '{print $5}')",
26
+ "XAUTHORITY=/home/$XUSER/.Xauthority",
27
+ "export DISPLAY",
28
+ "export XAUTHORITY"
29
+ ].join("; ")
30
+
31
+ command = "#{x_env_variables}; xrandr -d $DISPLAY #{options}"
32
+ exec(command)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "generic_ssh"
2
+
3
+ module HackyHAL
4
+ module DeviceControllers
5
+ class OsxComputer < GenericSsh
6
+ def mirror_screens
7
+ exec("mirror -on")
8
+ end
9
+
10
+ def unmirror_screens
11
+ exec("mirror -off")
12
+ end
13
+
14
+ def set_audio_output_device(name)
15
+ exec("audiodevice output '#{name}'")
16
+ end
17
+ end
18
+ end
19
+ end