rzwaveway 0.0.4 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,59 +2,142 @@ require 'json'
2
2
 
3
3
  module RZWaveWay
4
4
  class ZWaveDevice
5
+ include CommandClass
5
6
  include CommandClasses
6
7
 
8
+ attr_reader :name
7
9
  attr_reader :id
10
+ attr_reader :last_contact_time
11
+ attr_accessor :contact_frequency
8
12
 
9
13
  def initialize(id, data)
10
14
  @id = id
11
- @command_classes = create_commandclasses_from data
12
- $log.info "Created ZWaveDevice with id='#{id}'"
15
+ initialize_from data
16
+ $log.info "Created ZWaveDevice with name='#{name}' (id='#{id}')"
13
17
  end
14
18
 
15
- def create_commandclasses_from data
16
- cc_classes = {}
17
- data['instances']['0']['commandClasses'].each do |cc_id, sub_tree|
18
- cc_classes[cc_id.to_i] = CommandClass.new(cc_id.to_i, sub_tree)
19
- end
20
- cc_classes
19
+ def contacts_controller_periodically?
20
+ support_commandclass? CommandClass::WAKEUP
21
21
  end
22
22
 
23
- def build_json
24
- properties = {'deviceId' => @id}
25
- @command_classes.each do |cc_id, cc|
26
- properties.merge!(cc.properties)
27
- end
28
- properties.to_json
23
+ def next_contact_time
24
+ @last_contact_time + (@contact_frequency * (1 + @missed_contact_count) * 1.1)
25
+ end
26
+
27
+ def to_json
28
+ attributes = {
29
+ 'name' => @name,
30
+ 'deviceId' => @id,
31
+ # TODO remove these obsolete attributes (kept for backward compatibility)
32
+ 'lastSleepTime' => @last_contact_time,
33
+ 'lastWakeUpTime' => @last_contact_time,
34
+ 'wakeUpInterval' => @contact_frequency
35
+ # ---
36
+ # 'lastContactTime' => @last_contact_time,
37
+ # 'contactFrequency' => @contact_frequency,
38
+ # 'properties' => @properties.to_json
39
+ }
40
+ attributes.to_json
29
41
  end
30
42
 
31
- def support_commandclass? command_class
32
- @command_classes.has_key? command_class
43
+ def support_commandclass?(command_class_id)
44
+ @command_classes.has_key? command_class_id
33
45
  end
34
46
 
35
47
  def process updates
36
48
  events = []
37
- updates_per_commandclass = group_per_commandclass updates
49
+ updates_per_commandclass = group_per_commandclass updates
38
50
  updates_per_commandclass.each do |cc, values|
39
51
  if @command_classes.has_key? cc
40
- event = @command_classes[cc].process(values, @id)
52
+ event = @command_classes[cc].process(values)
41
53
  events << event if event
42
54
  else
43
55
  $log.warn "Could not find command class: '#{cc}'"
44
56
  end
45
57
  end
58
+ process_device_data(updates, events)
46
59
  events
47
60
  end
48
61
 
49
62
  def process_alive_check
50
- if(support_commandclass? WAKEUP)
51
- return @command_classes[WAKEUP].process_alive_check(@id)
63
+ return if @dead
64
+ if @contact_frequency > 0
65
+ current_time = Time.now.to_i
66
+ delta = current_time - next_contact_time
67
+ if delta > 0
68
+ count = ((current_time - @last_contact_time) / @contact_frequency).to_i
69
+ if count > MAXIMUM_MISSED_CONTACT
70
+ @dead = true
71
+ DeadEvent.new(@id)
72
+ elsif count > @missed_contact_count
73
+ @missed_contact_count = count
74
+ NotAliveEvent.new(@id, delta, count)
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ def notify_contacted(time)
81
+ if time.to_i > @last_contact_time
82
+ @dead = false
83
+ @last_contact_time = time.to_i
84
+ @missed_contact_count = 0
85
+ true
86
+ end
87
+ end
88
+
89
+ def add_property(name, value, updateTime)
90
+ @properties[name] = [value, updateTime]
91
+ end
92
+
93
+ def get_property(name)
94
+ @properties[name].dup
95
+ end
96
+
97
+ def update_property(name, value, updateTime)
98
+ if @properties.has_key?(name)
99
+ property = @properties[name]
100
+ if property[0] != value || property[1] < updateTime
101
+ property[0] = value
102
+ property[1] = updateTime
103
+ true
104
+ end
52
105
  end
53
106
  end
54
107
 
55
108
  private
56
109
 
110
+ MAXIMUM_MISSED_CONTACT = 10
111
+
112
+ def create_commandclasses_from data
113
+ cc_classes = {}
114
+ data['instances']['0']['commandClasses'].each do |id, sub_tree|
115
+ cc_id = id.to_i
116
+ cc_class = CommandClasses::Factory.instance.instantiate(cc_id, sub_tree, self)
117
+ cc_classes[cc_id] = cc_class
118
+ cc_class_name = cc_class.class.name.split('::').last
119
+ (class << self; self end).send(:define_method, cc_class_name) { cc_class } unless cc_class_name == 'Dummy'
120
+ end
121
+ cc_classes
122
+ end
123
+
124
+ def initialize_from data
125
+ @name = find('data.givenName.value', data)
126
+ last_contact_times = [
127
+ find('data.lastReceived.updateTime', data),
128
+ find('data.lastSend.updateTime', data)
129
+ ]
130
+ @last_contact_time = last_contact_times.max
131
+
132
+ @dead = false
133
+ @missed_contact_count = 0
134
+ @contact_frequency = 0
135
+ @properties = {}
136
+ @command_classes = create_commandclasses_from data
137
+ end
138
+
57
139
  def group_per_commandclass updates
140
+ other_updates = {}
58
141
  updates_per_commandclass = {}
59
142
  updates.each do | key, value |
60
143
  match_data = key.match(/\Ainstances.0.commandClasses.(\d+)./)
@@ -63,10 +146,23 @@ module RZWaveWay
63
146
  updates_per_commandclass[command_class] = {} unless updates_per_commandclass.has_key?(command_class)
64
147
  updates_per_commandclass[command_class][match_data.post_match] = value
65
148
  else
66
- $log.warn "? #{key}" unless key.match(/\Adata./)
149
+ other_updates[key] = value
67
150
  end
68
151
  end
152
+ updates.clear
153
+ updates.merge!(other_updates)
69
154
  updates_per_commandclass
70
155
  end
156
+
157
+ def process_device_data(updates, events)
158
+ times = []
159
+ updates.each do | key, value |
160
+ if key == 'data.lastReceived' || key == 'data.lastSend'
161
+ times << value['updateTime']
162
+ end
163
+ end
164
+ time = times.max
165
+ events << AliveEvent.new(@id, time) if notify_contacted(time)
166
+ end
71
167
  end
72
- end
168
+ end
@@ -1,38 +1,121 @@
1
- require 'httpclient'
1
+ require 'singleton'
2
+
3
+ require 'faraday'
2
4
  require 'log4r'
3
5
  require 'json'
4
6
 
5
7
  module RZWaveWay
6
- module ZWay
7
- extend self
8
+ class ZWay
9
+ include Singleton
8
10
  include Log4r
9
11
 
10
- BASE_PATH='/ZWaveAPI/Data/'
12
+ attr_reader :devices
11
13
 
12
- def self.init hostname
14
+ def initialize
13
15
  $log = Logger.new 'RZWaveWay'
16
+ formatter = PatternFormatter.new(:pattern => "[%l] %d - %m")
14
17
  outputter = Outputter.stdout
15
- outputter.formatter = PatternFormatter.new(:pattern => "[%l] %d - %m")
16
- $log.outputters = Outputter.stdout
17
- @devices = {}
18
- @update_time = "0"
19
- @event_handlers = {}
20
- @http_client = HTTPClient.new
18
+ outputter.formatter = formatter
19
+ outputter.level = Log4r::INFO
20
+ file_outputter = RollingFileOutputter.new('file', filename: 'rzwaveway.log', maxsize: 1048576, trunc: 86400)
21
+ file_outputter.formatter = formatter
22
+ file_outputter.level = Log4r::DEBUG
23
+ $log.outputters = [Outputter.stdout, file_outputter]
24
+ end
25
+
26
+ def execute(device_id, command_class, command_class_function, argument = nil)
27
+ raise "No device with id '#{device_id}'" unless @devices.has_key?(device_id)
28
+ raise "Device with id '#{device_id}' does not support command class '#{command_class}'" unless @devices[device_id].support_commandclass?(command_class)
29
+ function_name = command_class_function.to_s
30
+ run_zway_function(device_id, command_class, function_name, argument)
31
+ end
32
+
33
+ def find_extension(name, device_id)
34
+ device = @devices[device_id.to_i]
35
+ raise ArgumentError, "No device with id '#{device_id}'" unless device
36
+ clazz = qualified_const_get "RZWaveWay::Extensions::#{name}"
37
+ clazz.new(device)
38
+ end
39
+
40
+ def setup(hostname, *adapter_params)
41
+ adapter_params = :httpclient if adapter_params.compact.empty?
21
42
  @base_uri="http://#{hostname}:8083"
43
+ @connection = Faraday.new {|faraday| faraday.adapter *adapter_params}
22
44
  end
23
45
 
24
- def get_devices
25
- results = http_post_request
26
- results["devices"].each do |device_id,device_data_tree|
27
- device_id = device_id.to_i
28
- @devices[device_id] = ZWaveDevice.new(device_id, device_data_tree) if device_id > 1
46
+ def start
47
+ @devices = {}
48
+ @event_handlers = {}
49
+ @update_time = '0'
50
+ loop do
51
+ results = get_zway_data_tree_updates
52
+ if results.has_key?('devices')
53
+ results['devices'].each {|device_id,device_data_tree| create_device(device_id.to_i, device_data_tree)}
54
+ break
55
+ else
56
+ sleep 1.0
57
+ $log.warn 'No devices found at start-up, retrying'
58
+ end
29
59
  end
30
- @devices
60
+ end
61
+
62
+ def on_event(event, &listener)
63
+ @event_handlers[event] = listener
31
64
  end
32
65
 
33
66
  def process_events
67
+ check_devices
68
+ updates = get_zway_data_tree_updates
69
+ events = devices_process updates
70
+ check_not_alive_devices(events)
71
+ deliver_to_handlers(events)
72
+ end
73
+
74
+ private
75
+
76
+ DATA_TREE_BASE_PATH='/ZWaveAPI/Data/'
77
+ RUN_BASE_PATH='/ZWaveAPI/Run/'
78
+
79
+ def check_devices
80
+ @devices.values.each do |device|
81
+ unless device.contacts_controller_periodically?
82
+ current_time = Time.now.to_i
83
+ # TODO ensure last_contact_time is set in the device initializer
84
+ if (current_time % 10 == 0) && (current_time > device.next_contact_time - 60)
85
+ run_zway_no_operation device.id
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def check_not_alive_devices(events)
92
+ @devices.values.each do |device|
93
+ event = device.process_alive_check
94
+ events << event if event
95
+ end
96
+ end
97
+
98
+ def create_device(device_id, device_data_tree)
99
+ if device_id > 1
100
+ device = ZWaveDevice.new(device_id, device_data_tree)
101
+ device.contact_frequency = 300 unless device.contacts_controller_periodically?
102
+ @devices[device_id] = device
103
+ end
104
+ end
105
+
106
+ def deliver_to_handlers events
107
+ events.each do |event|
108
+ handler = @event_handlers[event.class]
109
+ if handler
110
+ handler.call(event)
111
+ else
112
+ $log.warn "No event handler for #{event.class}"
113
+ end
114
+ end
115
+ end
116
+
117
+ def devices_process updates
34
118
  events = []
35
- updates = http_post_request
36
119
  updates_per_device = group_per_device updates
37
120
  updates_per_device.each do | id, updates |
38
121
  if @devices[id]
@@ -42,16 +125,6 @@ module RZWaveWay
42
125
  $log.warn "Could not find device with id '#{id}'"
43
126
  end
44
127
  end
45
- alive_events = check_not_alive_devices
46
- events += alive_events unless alive_events.empty?
47
- events.each do |event|
48
- handler = @event_handlers[event.class]
49
- if handler
50
- handler.call(event)
51
- else
52
- $log.warn "no handler for #{event.class}"
53
- end
54
- end
55
128
  events
56
129
  end
57
130
 
@@ -64,36 +137,75 @@ module RZWaveWay
64
137
  updates_per_device[device_id] = {} unless(updates_per_device.has_key?(device_id))
65
138
  updates_per_device[device_id][match_data.post_match] = value
66
139
  else
67
- $log.warn "? #{key}"
140
+ $log.debug "No device group match for key='#{key}'"
68
141
  end
69
142
  end
70
143
  updates_per_device
71
144
  end
72
145
 
73
- def check_not_alive_devices
74
- events = []
75
- @devices.values.each do |device|
76
- event = device.process_alive_check
77
- events << event if event
146
+ def get_zway_data_tree_updates
147
+ results = {}
148
+ url = @base_uri + DATA_TREE_BASE_PATH + "#{@update_time}"
149
+ begin
150
+ response = @connection.get(url)
151
+ if response.success?
152
+ results = JSON.parse response.body
153
+ @update_time = results.delete('updateTime')
154
+ else
155
+ $log.error(response.reason)
156
+ end
157
+ rescue StandardError => e
158
+ $log.error("Failed to communicate with ZWay HTTP server: #{e}")
78
159
  end
79
- events
160
+ results
80
161
  end
81
162
 
82
- def http_post_request
83
- results = {}
84
- url = @base_uri + BASE_PATH + "#{@update_time}"
85
- response = @http_client.post(url)
86
- if response.ok?
87
- results = JSON.parse response.body
88
- @update_time = results.delete("updateTime")
163
+ def qualified_const_get(str)
164
+ path = str.to_s.split('::')
165
+ from_root = path[0].empty?
166
+ if from_root
167
+ from_root = []
168
+ path = path[1..-1]
89
169
  else
90
- $log.error(response.reason)
170
+ start_ns = ((Class === self)||(Module === self)) ? self : self.class
171
+ from_root = start_ns.to_s.split('::')
91
172
  end
92
- results
173
+ until from_root.empty?
174
+ begin
175
+ return (from_root+path).inject(Object) { |ns,name| ns.const_get(name) }
176
+ rescue NameError
177
+ from_root.delete_at(-1)
178
+ end
179
+ end
180
+ path.inject(Object) { |ns,name| ns.const_get(name) }
93
181
  end
94
182
 
95
- def on_event (event, &listener)
96
- @event_handlers[event] = listener
183
+ def run_zway_function(device_id, command_class, function_name, argument)
184
+ command_path = "devices[#{device_id}].instances[0].commandClasses[#{command_class}]."
185
+ if argument
186
+ command_path += "#{function_name}(#{argument})"
187
+ else
188
+ command_path += "#{function_name}()"
189
+ end
190
+ run_zway command_path
191
+ end
192
+
193
+ def run_zway_no_operation device_id
194
+ run_zway "devices[#{device_id}].SendNoOperation()"
195
+ end
196
+
197
+ def run_zway command_path
198
+ begin
199
+ uri = URI.encode(@base_uri + RUN_BASE_PATH + command_path, '[]')
200
+ response = @connection.get(uri)
201
+ unless response.success?
202
+ $log.error(response.status)
203
+ $log.error(response.body)
204
+ end
205
+ rescue StandardError => e
206
+ $log.error("Failed to communicate with ZWay HTTP server: #{e}")
207
+ $log.error(e.backtrace)
208
+ end
97
209
  end
98
210
  end
99
- end
211
+ end
data/lib/rzwaveway.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require_relative 'rzwaveway/command_classes'
2
2
  require_relative 'rzwaveway/events'
3
3
  require_relative 'rzwaveway/zwave_device'
4
- require_relative 'rzwaveway/zway'
4
+ require_relative 'rzwaveway/zway'
5
+ require_relative 'rzwaveway/extensions'
data/rzwaveway.gemspec CHANGED
@@ -1,18 +1,24 @@
1
+ $LOAD_PATH.unshift 'lib'
2
+ require 'rzwaveway/version'
3
+
1
4
  Gem::Specification.new do |s|
2
- s.name = "rzwaveway"
3
- s.version = "0.0.4"
4
- s.authors = ["Vincent Touchard"]
5
+ s.name = 'rzwaveway'
6
+ s.version = RZWaveWay::VERSION
7
+ s.authors = ['Vincent Touchard']
5
8
  s.date = %q{2014-02-18}
6
9
  s.summary = 'ZWave API for ZWay'
7
10
  s.description = 'A Ruby API to use the Razberry ZWave ZWay interface'
8
- s.email = 'vincentoo.ignore@yahoo.com'
11
+ s.email = 'touchardv@yahoo.com'
9
12
  s.homepage = 'https://github.com/touchardv/rzwaveway'
10
13
  s.files = `git ls-files`.split("\n")
11
14
  s.has_rdoc = false
12
15
 
13
16
  dependencies = [
14
- [:runtime, "log4r", "~> 1.1.10"],
15
- [:runtime, "httpclient", "~> 2.3.4.1"]
17
+ [:runtime, 'log4r', '~> 1.1.10'],
18
+ [:runtime, 'faraday'],
19
+ [:runtime, 'httpclient'],
20
+ [:development, 'bundler', '~> 1.0'],
21
+ [:development, 'rspec', '~> 3.0.0']
16
22
  ]
17
23
 
18
24
  dependencies.each do |type, name, version|
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ module RZWaveWay
4
+ module CommandClasses
5
+ describe Battery do
6
+ let(:device) { ZWaveDevice.new(create_id, create_device_data) }
7
+ let(:command_class) do
8
+ Battery.new(
9
+ {'data' => { 'last' => {
10
+ 'value' => 60,
11
+ 'type' => 'int',
12
+ 'updateTime' => 1409681662
13
+ }}}, device)
14
+ end
15
+
16
+ describe '#new' do
17
+ it 'stores interesting properties' do
18
+ command_class
19
+ expect(device.get_property(:battery_level)).to eq [60, 1409681662]
20
+ end
21
+ end
22
+
23
+ describe '#process' do
24
+ it 'does nothing when it processes no updates' do
25
+ expect(command_class.process({})).to be_nil
26
+ end
27
+
28
+ it 'returns a battery event' do
29
+ updates = {
30
+ 'data.last' => {
31
+ 'value' => 50,
32
+ 'type' => 'int',
33
+ 'updateTime' => 1409681762
34
+ }}
35
+ event = command_class.process(updates)
36
+ expect(event.class).to be RZWaveWay::BatteryValueEvent
37
+ expect(event.value).to eq 50
38
+ expect(event.device_id).to eq device.id
39
+ expect(event.time).to eq 1409681762
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ module RZWaveWay
4
+ module CommandClasses
5
+ describe SensorBinary do
6
+ let(:device) { ZWaveDevice.new(create_id, create_device_data) }
7
+ let(:command_class) do
8
+ SensorBinary.new(
9
+ {'data' => { '1' => { 'level' => {
10
+ 'value' => false,
11
+ 'updateTime' => 1405102560
12
+ }}}}, device)
13
+ end
14
+
15
+ describe '#new' do
16
+ it 'stores interesting properties' do
17
+ command_class
18
+ expect(device.get_property(:level)).to eq [false, 1405102560]
19
+ end
20
+ end
21
+
22
+ describe '#process' do
23
+ it 'does nothing when it processes no updates' do
24
+ expect(command_class.process({})).to be_nil
25
+ end
26
+
27
+ it 'returns a level event' do
28
+ updates = {
29
+ 'data.1' => { 'level' => {
30
+ 'value' => true,
31
+ 'updateTime' => 1405102860
32
+ }}}
33
+ event = command_class.process(updates)
34
+ expect(event.class).to be RZWaveWay::LevelEvent
35
+ expect(event.level).to eq true
36
+ expect(event.device_id).to eq device.id
37
+ expect(event.time).to eq 1405102860
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ module RZWaveWay
4
+ module CommandClasses
5
+ describe SwitchBinary do
6
+ let(:device) { ZWaveDevice.new(create_id, create_device_data) }
7
+ let(:command_class) do
8
+ SwitchBinary.new(
9
+ {'data' => { 'level' => {
10
+ 'value' => false,
11
+ 'updateTime' => 1405102560
12
+ }}}, device)
13
+ end
14
+
15
+ describe '#it caches the level' do
16
+ it 'stores interesting properties' do
17
+ command_class
18
+ expect(device.get_property(:level)).to eq [false, 1405102560]
19
+ end
20
+ end
21
+
22
+ describe '#process' do
23
+ it 'does nothing when it processes no updates' do
24
+ expect(command_class.process({})).to be_nil
25
+ end
26
+
27
+ it 'returns a multi level event' do
28
+ updates = {
29
+ 'data.level' => {
30
+ 'value' => true,
31
+ 'updateTime' => 1405102860
32
+ }}
33
+ event = command_class.process(updates)
34
+ expect(event.class).to be RZWaveWay::LevelEvent
35
+ expect(event.level).to eq true
36
+ expect(event.device_id).to eq device.id
37
+ expect(event.time).to eq 1405102860
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ module RZWaveWay
4
+ module CommandClasses
5
+ describe SwitchMultiLevel do
6
+ let(:device) { ZWaveDevice.new(create_id, create_device_data) }
7
+ let(:command_class) do
8
+ SwitchMultiLevel.new(
9
+ {'data' => { 'level' => {
10
+ 'value' => 33,
11
+ 'updateTime' => 1405102560
12
+ }}}, device)
13
+ end
14
+
15
+ describe '#new' do
16
+ it 'stores interesting properties' do
17
+ command_class
18
+ expect(device.get_property(:level)).to eq [33, 1405102560]
19
+ end
20
+ end
21
+
22
+ describe '#process' do
23
+ it 'does nothing when it processes no updates' do
24
+ expect(command_class.process({})).to be_nil
25
+ end
26
+
27
+ it 'returns a multi level event' do
28
+ updates = {
29
+ 'data.level' => {
30
+ 'value' => 66,
31
+ 'updateTime' => 1405102860
32
+ }}
33
+ event = command_class.process(updates)
34
+ expect(event.class).to be RZWaveWay::MultiLevelEvent
35
+ expect(event.level).to eq 66
36
+ expect(event.device_id).to eq device.id
37
+ expect(event.time).to eq 1405102860
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end