rzwaveway 0.0.4 → 0.0.6

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.
@@ -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