thermostat 0.0.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ === 1.0.0 2010-03-09
2
+
3
+ * major enhancements:
4
+ * Conversion to newgem package for easier future maintenance
5
+ * Make a more sane from end interface
@@ -0,0 +1,29 @@
1
+ History.txt
2
+ Manifest.txt
3
+ PostInstall.txt
4
+ README.rdoc
5
+ Rakefile
6
+ account.yaml.example
7
+ config/website.yml
8
+ config/website.yml.sample
9
+ docs/PDP_API_R1_11.pdf
10
+ examples/test-fan-off.rb
11
+ examples/test-fan-on.rb
12
+ examples/test-run.rb
13
+ examples/test-set-heat.rb
14
+ lib/pdp/constants.rb
15
+ lib/pdp/network.rb
16
+ lib/pdp/oid.rb
17
+ lib/thermostat.rb
18
+ script/console
19
+ script/destroy
20
+ script/generate
21
+ script/txt2html
22
+ test/test_helper.rb
23
+ test/test_oid.rb
24
+ test/test_thermostat.rb
25
+ website/index.html
26
+ website/index.txt
27
+ website/javascripts/rounded_corners_lite.inc.js
28
+ website/stylesheets/screen.css
29
+ website/template.html.erb
@@ -0,0 +1,5 @@
1
+
2
+ For more information on thermostat.rb, see http://sdaguegems.rubyforge.org/thermostat
3
+
4
+
5
+
@@ -0,0 +1,66 @@
1
+ = thermostat
2
+
3
+ * http://github.com/sdague/thermostat.rb
4
+
5
+ == DESCRIPTION:
6
+
7
+ Thermostat.rb is an attempt to build an easy to use ruby api around
8
+ the web services provided by the Proliphix line of network
9
+ thermostats. Their API is documented at
10
+ http://www.proliphix.com/Documentation.aspx.
11
+
12
+ The code is tested on the NT20e unit, which is what I own. It should
13
+ react that same across all their network accessible units.
14
+
15
+ == FEATURES/PROBLEMS:
16
+
17
+ Note: It wrote the base module over a year ago, and am now starting to more actively use it in a new project, so interfaces may change to make the api easier to use.
18
+
19
+ Important notices from the manual:
20
+
21
+ **Limitations on the Frequency of Operations**
22
+
23
+ While the device can handle numerous back to back requests, it is
24
+ advisable to not sustain an operation frequency higher than 1 request
25
+ per 60 second period for a substantial amount of time. In other words,
26
+ sustained polling of the device should not exceed a few requests per
27
+ minute. A prolonged burst of requests higher than 1 request per 60
28
+ second period may degrade the main function of the thermostat. Also,
29
+ if one is making a PIB set and wishes to observe a reaction from the
30
+ device, it is advisable to wait for at least 1 second between the set
31
+ request and subsequent get request.
32
+
33
+ == SYNOPSIS:
34
+
35
+ FIX (code sample of usage)
36
+
37
+ == REQUIREMENTS:
38
+
39
+ == INSTALL:
40
+
41
+ sudo gem install thermostat
42
+
43
+ == LICENSE:
44
+
45
+ (The MIT License)
46
+
47
+ Copyright (c) 2010 Sean Dague
48
+
49
+ Permission is hereby granted, free of charge, to any person obtaining
50
+ a copy of this software and associated documentation files (the
51
+ 'Software'), to deal in the Software without restriction, including
52
+ without limitation the rights to use, copy, modify, merge, publish,
53
+ distribute, sublicense, and/or sell copies of the Software, and to
54
+ permit persons to whom the Software is furnished to do so, subject to
55
+ the following conditions:
56
+
57
+ The above copyright notice and this permission notice shall be
58
+ included in all copies or substantial portions of the Software.
59
+
60
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
61
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
62
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
63
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
64
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
65
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
66
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+ gem 'hoe', '>= 2.1.0'
3
+ require 'hoe'
4
+ require 'fileutils'
5
+ require './lib/thermostat'
6
+
7
+ ENV['VERSION'] = Thermostat::VERSION
8
+
9
+ Hoe.plugin :newgem
10
+ Hoe.plugin :website
11
+ # Hoe.plugin :cucumberfeatures
12
+
13
+ # Generate all the Rake tasks
14
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
15
+ $hoe = Hoe.spec 'thermostat' do
16
+ self.developer 'Sean Dague', 'sean@dague.net'
17
+ self.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
18
+ self.rubyforge_name = 'sdaguegems'
19
+ self.extra_deps = [['temperature', '>= 1.0.0']]
20
+ # self.extra_deps = [['activesupport','>= 2.0.2']]
21
+ end
22
+
23
+ require 'newgem/tasks'
24
+ Dir['tasks/**/*.rake'].each { |t| load t }
25
+
26
+ # TODO - want other tests/tasks run by default? Add them to the list
27
+ # remove_task :default
28
+ # task :default => [:spec, :features]
@@ -0,0 +1,4 @@
1
+ user: admin
2
+ passwd: passwd
3
+ ip: 0.0.0.0
4
+
@@ -0,0 +1,2 @@
1
+ host: sdague@rubyforge.org
2
+ remote_dir: /var/www/gforge-projects/sdaguegems/thermostat
@@ -0,0 +1,2 @@
1
+ host: unknown@rubyforge.org
2
+ remote_dir: /var/www/gforge-projects/thermostat
Binary file
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/ruby
2
+
3
+ $LOAD_PATH.unshift "../lib"
4
+
5
+ require "yaml"
6
+ require 'thermostat'
7
+ include Proliphix
8
+
9
+ c = YAML.load_file("../account.yaml")
10
+
11
+ t = Thermostat.new(c["ip"], c["user"], c["passwd"])
12
+
13
+ t.set_senors(ThermHvacMode, ThermHvacState, ThermFanState, ThermFanMode, ThermAverageTemp, ThermHeat1Usage)
14
+ t.fetch_data
15
+
16
+ puts t
17
+
18
+ t.set_data(ThermFanMode, 1)
19
+
20
+ puts t
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/ruby
2
+
3
+ $LOAD_PATH.unshift "../lib"
4
+
5
+ require "yaml"
6
+ require 'thermostat'
7
+
8
+ include Proliphix
9
+
10
+ c = YAML.load_file("../account.yaml")
11
+
12
+ t = Thermostat.new(c["ip"], c["user"], c["passwd"])
13
+
14
+ t.set_senors(ThermHvacMode, ThermHvacState, ThermFanState, ThermFanMode, ThermAverageTemp, ThermHeat1Usage)
15
+ t.fetch_data
16
+
17
+ puts t
18
+
19
+ t.set_data(ThermFanMode, 2)
20
+
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/ruby
2
+
3
+ $LOAD_PATH.unshift "../lib"
4
+
5
+ require "yaml"
6
+ require 'thermostat'
7
+
8
+ include Proliphix
9
+
10
+ c = YAML.load_file("../account.yaml")
11
+
12
+ t = Thermostat.new(c["ip"], c["user"], c["passwd"])
13
+
14
+ t.set_senors(ThermHvacMode, ThermHvacState, ThermFanState, ThermFanMode, ThermAverageTemp, ThermHeat1Usage, ThermSetbackHeat)
15
+
16
+ t.fetch_data
17
+
18
+ puts t
19
+
20
+
21
+
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/ruby
2
+
3
+ $LOAD_PATH.unshift "../lib"
4
+
5
+ require "yaml"
6
+ require 'thermostat'
7
+ include Proliphix
8
+
9
+ c = YAML.load_file("../account.yaml")
10
+
11
+ t = Thermostat.new(c["ip"], c["user"], c["passwd"])
12
+
13
+ t.set_senors(ThermHvacMode, ThermHvacState, ThermFanState, ThermFanMode, ThermAverageTemp, ThermHeat1Usage, ThermSetbackHeat)
14
+ t.fetch_data
15
+
16
+ puts t
17
+
18
+ # Sets the heat to 68.0 F
19
+ t.set_data(ThermSetbackHeat, 680)
20
+
@@ -0,0 +1,123 @@
1
+ module Proliphix
2
+ # This is the core networking module for getting data off the
3
+ # thermostat. It looks sort of funny because I am reusing
4
+ # constants as hash keys which are actually objects.
5
+ #
6
+ require "open-uri"
7
+
8
+ class Network
9
+ def initialize(ip, user, passwd)
10
+ @ip = ip
11
+ @user = user
12
+ @passwd = passwd
13
+ @last = Time.now.to_i
14
+ @fetched = {}
15
+ @sensors = [
16
+ ThermHvacMode,
17
+ ThermHvacState,
18
+ ThermFanMode,
19
+ ThermFanState,
20
+ ThermSetbackHeat,
21
+ ThermSetbackCool,
22
+ ThermHeat1Usage,
23
+ ThermCool1Usage,
24
+ ThermFanUsage,
25
+ ThermAverageTemp
26
+ ]
27
+ end
28
+
29
+
30
+ def [](reading)
31
+ return @fetched[reading]
32
+ end
33
+
34
+ def get(name)
35
+ if self[name] and up_to_date
36
+ return self[name]
37
+ else
38
+ fetch_data
39
+ # TODO: what if we don't manage to return values
40
+ return self[name]
41
+ end
42
+ end
43
+
44
+ def set(name, value)
45
+ set_data(name, value)
46
+ sleep 1
47
+ fetch_data
48
+ end
49
+
50
+ private
51
+
52
+ # Are we currently up to date with our data, if so don't worry
53
+ # about getting any other values.
54
+ def up_to_date
55
+ return (@last + 60) > Time.now.to_i
56
+ end
57
+
58
+ def url
59
+ return "http://#{@ip}"
60
+ end
61
+
62
+ def get_query_url
63
+ query = ""
64
+ for item in @sensors
65
+ if query != ""
66
+ query += "&"
67
+ end
68
+ query += "OID#{item.oid}="
69
+ end
70
+ query = url + "/get?" + query
71
+ end
72
+
73
+ def set_query_url(mod, value)
74
+ query = "OID#{mod.oid}=#{value}"
75
+ query = url + "/pdp?" + query + "&sumbit=Submit"
76
+ end
77
+
78
+ # Because of the timing issues with the thermostat we want to
79
+ # get all the data we can in one packet.
80
+ def fetch_data()
81
+ open(get_query_url, :http_basic_authentication => [@user, @passwd]) do |line|
82
+ parse_data(line.read)
83
+ end
84
+ return self
85
+ end
86
+
87
+ def set_data(mod, value)
88
+ unless mod.ro?
89
+ open(set_query_url(mod, value), :http_basic_authentication => [@user, @passwd]) do |line|
90
+ parse_data(line.read)
91
+ end
92
+ end
93
+ return self
94
+ end
95
+
96
+ # The return for the data from Proliphix is kind of silly. It
97
+ # includes oids = values in what looks like a uri string. So
98
+ # split twice to get values.
99
+ def parse_data(raw_data)
100
+ results = raw_data.split('&')
101
+ results.each do |r|
102
+ (oid, value) = r.split('=')
103
+ mod = lookup_oid(oid)
104
+ if mod
105
+ @fetched[mod] = mod[value]
106
+ end
107
+ end
108
+ @last = Time.now.to_i
109
+ end
110
+
111
+ def lookup_oid(oid)
112
+ newid = oid.sub(/^OID/, '')
113
+ @sensors.each do |s|
114
+ if s.oid == newid
115
+ return s
116
+ end
117
+ end
118
+ return nil
119
+ end
120
+
121
+
122
+ end
123
+ end
@@ -1,4 +1,6 @@
1
1
  module Proliphix
2
+ # This is the basic encapsulation class for OIDs used by the Proliphix thermostat.
3
+
2
4
  class OID
3
5
  def initialize(name, oid, readonly, valuemap = nil, scale = true)
4
6
  @name = name
@@ -8,22 +10,29 @@ module Proliphix
8
10
  @scale = scale
9
11
  end
10
12
 
13
+ # return the oid of the item that is used in the proliphix web
14
+ # service calls. This is typically of the format '4.1.12' or
15
+ # '4.5.2'
11
16
  def oid
12
17
  return @oid
13
18
  end
14
19
 
20
+ # return the name which you've provided. This should be a basic string.
15
21
  def name
16
22
  return @name
17
23
  end
18
24
 
25
+ # is the data from the oid read only.
19
26
  def ro?
20
27
  return @readonly
21
28
  end
22
29
 
23
- # a little tricky, but not too bad. Basically the access
24
- # gives us either the value off the value map, or the degrees
25
- # reading (as we always get degrees as 10x ints)
26
-
30
+ # Make the OIDs act as their own translator for the values
31
+ # that come out of the webservices interface. This is done
32
+ # via the array opperator because it's doable.
33
+ #
34
+ # We more or less get the raw value or the value divided by
35
+ # 10, as degrees come back as a 10x integer.
27
36
  def [](value)
28
37
  if @valuemap
29
38
  return @valuemap[value.to_i]
@@ -1,95 +1,112 @@
1
- require 'net/http'
2
- require 'uri'
3
- require 'open-uri'
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
4
3
 
5
- # order here is important
6
- require 'pdp/oid'
7
- require 'pdp/constants'
8
-
9
- # Some docs
4
+ # This is the main class for controlling the Proliphix Thermostat.
5
+ # You must first initialize it with the control point credentials
10
6
  class Thermostat
11
-
7
+ VERSION = '1.0.0'
8
+
9
+ require 'pdp/oid'
10
+ require 'pdp/constants'
11
+ require 'pdp/network'
12
+
13
+ include Proliphix
14
+
15
+ # create a new thermostat object. It needs
16
+ # * ip - the ip address of the thermostat, probably 192.168.1.2
17
+ # * user - the admin user for the thermostat
18
+ # * passwd - the admin passwd for the thermostat
12
19
  def initialize(ip, user, passwd)
13
- @ip = ip
14
- @user = user
15
- @passwd = passwd
16
- @fetched = {}
17
- @sensors = []
20
+ @network = Proliphix::Network.new(ip, user, passwd)
18
21
  end
19
-
22
+
23
+ # Get a reading for a constant, returns raw values
20
24
  def [](reading)
21
- return @fetched[reading]
25
+ return @network.get(reading)
22
26
  end
23
-
24
- def url
25
- return "http://#{@ip}"
27
+
28
+ # Set a reading for a constant.
29
+ def []=(reading, value)
30
+ return @network.set(reading, value)
26
31
  end
27
-
28
- def set_senors(*array)
29
- @sensors = array
30
- return self
32
+
33
+ # Get the thermostat mode. This will come back as one of "Off",
34
+ # "Heat", "Cool", or "Auto".
35
+ def mode
36
+ self[ThermHvacMode]
31
37
  end
32
-
33
- def get_query_url
34
- query = ""
35
- for item in @sensors
36
- if query != ""
37
- query += "&"
38
- end
39
- query += "OID#{item.oid}="
38
+
39
+ # Return what temperature the thermostat is attempting to heat to.
40
+ def heat_to
41
+ case mode
42
+ when "Heat" then return self[ThermSetbackHeat]
43
+ when "Auto" then return self[ThermSetbackHeat]
40
44
  end
41
- query = url + "/get?" + query
45
+ return nil
42
46
  end
43
-
44
- def set_query_url(mod, value)
45
- query = "OID#{mod.oid}=#{value}"
46
- query = url + "/pdp?" + query + "&sumbit=Submit"
47
+
48
+ # Return what temperature the thermostat is attempting to cool to.
49
+ def cool_to
50
+ case mode
51
+ when "Cool" then return self[ThermSetbackCool]
52
+ when "Auto" then return self[ThermSetbackCool]
53
+ end
54
+ return nil
55
+ end
56
+
57
+ # Return the current temperature reading on the thermostat
58
+ def temp
59
+ self[ThermAverageTemp]
47
60
  end
48
61
 
49
- # Because of the timing issues with the thermostat we want to get all the data we can in one packet.
50
- def fetch_data()
51
- open(get_query_url, :http_basic_authentication => [@user, @passwd]) do |line|
52
- parse_data(line.read)
62
+ # Set the target heat-to temperature. The effects in non heating
63
+ # mode aren't well defined.
64
+ def heat_to=(value)
65
+ value *= 10
66
+ if (value > 500) and (value < 800)
67
+ self[ThermSetbackHeat] = value.to_i
53
68
  end
54
- return self
69
+ heat_to
55
70
  end
56
71
 
57
- def set_data(mod, value)
58
- unless mod.ro?
59
- open(set_query_url(mod, value), :http_basic_authentication => [@user, @passwd]) do |line|
60
- parse_data(line.read)
61
- end
72
+ # Set the target cool-to temperature. The effects in non cooling
73
+ # mode aren't well defined.
74
+ def cool_to=(value)
75
+ value *= 10
76
+ if (value > 600) and (value < 900)
77
+ self[ThermSetbackCool] = value.to_i
62
78
  end
63
- return self
79
+ cool_to
80
+ end
81
+
82
+ # Are we currently running the heat, returns a boolean
83
+ def heating?
84
+ self[ThermHvacState] == "Heat"
64
85
  end
65
86
 
87
+ # Are we currently running the AC, returns a boolean
88
+ def cooling?
89
+ self[ThermHvacState] == "Cool"
90
+ end
66
91
 
67
- def parse_data(raw_data)
68
- results = raw_data.split('&')
69
- results.each do |r|
70
- (oid, value) = r.split('=')
71
- mod = lookup_oid(oid)
72
- if mod
73
- @fetched[mod] = mod[value]
74
- end
75
- end
92
+ # Is the fan currently on. It doesn't matter why it's on, just
93
+ # that it's on.
94
+ def fan_on?
95
+ self[ThermFanState] == "On"
76
96
  end
77
97
 
78
- def lookup_oid(oid)
79
- newid = oid.sub(/^OID/, '')
80
- @sensors.each do |s|
81
- if s.oid == newid
82
- return s
83
- end
84
- end
85
- return nil
98
+ # Turn the fan on
99
+ def fan_on!
100
+ self[ThermFanMode] = 2
101
+ self[ThermFanState]
86
102
  end
87
103
 
88
- def to_s
89
- s = ""
90
- @fetched.each do |k, v|
91
- s += "#{k.name}: #{v}\n"
92
- end
93
- return s
104
+ # Turn the fan off
105
+ def fan_off!
106
+ self[ThermFanMode] = 1
107
+ self[ThermFanState]
94
108
  end
95
109
  end
110
+
111
+
112
+