rzwaveway 0.0.11 → 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,73 +1,25 @@
1
- module RZWaveWay
2
- class AliveEvent
3
- attr_reader :device_id
4
- attr_reader :time
5
-
6
- def initialize device_id, time
7
- @device_id = device_id
8
- @time = time
9
- end
10
- end
11
-
12
- class NotAliveEvent
13
- attr_reader :device_id
14
- attr_reader :time_delay
15
- attr_reader :missed_count
16
-
17
- def initialize(device_id, time_delay, missed_count)
18
- @device_id = device_id
19
- @time_delay = time_delay
20
- @missed_count = missed_count
21
- end
22
- end
23
-
24
- class DeadEvent
25
- attr_reader :device_id
26
-
27
- def initialize(device_id)
28
- @device_id = device_id
29
- end
30
- end
1
+ require 'ostruct'
31
2
 
32
- class LevelEvent
33
- attr_reader :device_id
34
- attr_reader :time
35
- attr_reader :level
3
+ module RZWaveWay
4
+ class Event < OpenStruct
5
+ def initialize(hash)
6
+ raise ArgumentError, 'Hash can not be nil' unless hash
7
+ raise ArgumentError, 'Missing device_id' unless hash.has_key? :device_id
8
+ hash[:time] = Time.now.to_i unless hash.has_key? :time
36
9
 
37
- def initialize(device_id, time, level)
38
- @device_id = device_id
39
- @time = time
40
- @level = level
10
+ super(hash)
41
11
  end
42
12
  end
43
13
 
44
- class MultiLevelEvent < LevelEvent
45
- end
46
-
47
- class BatteryValueEvent
48
- attr_reader :device_id
49
- attr_reader :time
50
- attr_reader :value
14
+ class AlarmEvent < Event ; end
51
15
 
52
- def initialize(device_id, time, value)
53
- @device_id = device_id
54
- @time = time
55
- @value = value
56
- end
57
- end
16
+ class AliveDevice < Event ; end
17
+ class InactiveDevice < Event ; end
18
+ class DeadDevice < Event ; end
58
19
 
59
- class SmokeEvent
60
- attr_reader :device_id
61
- attr_reader :time
62
- end
20
+ class LevelEvent < Event ; end
63
21
 
64
- class HighTemperatureEvent
65
- attr_reader :device_id
66
- attr_reader :time
67
- end
22
+ class MultiLevelEvent < LevelEvent ; end
68
23
 
69
- class TamperingEvent
70
- attr_reader :device_id
71
- attr_reader :time
72
- end
24
+ class BatteryValueEvent < Event ; end
73
25
  end
@@ -34,11 +34,6 @@ module RZWaveWay
34
34
  end
35
35
  end
36
36
 
37
- def level!
38
- @device.SwitchMultiLevel.get
39
- nil
40
- end
41
-
42
37
  private
43
38
 
44
39
  DISABLED = 0
@@ -46,7 +41,7 @@ module RZWaveWay
46
41
  SIREN = 66
47
42
 
48
43
  def set level
49
- @device.SwitchMultiLevel.set level
44
+ @device.SwitchMultiLevel.level = level
50
45
  end
51
46
  end
52
47
  end
@@ -0,0 +1,38 @@
1
+ module RZWaveWay
2
+ module PropertiesCache
3
+ def save_properties
4
+ properties.values.each {|property| property.save}
5
+ end
6
+
7
+ def to_hash
8
+ properties.each_with_object({}) {|property, hash| hash[property[0]] = property[1].to_hash}
9
+ end
10
+
11
+ private
12
+
13
+ def define_property(property_name, key, read_only, data)
14
+ options = {
15
+ value: find("#{key}.value", data),
16
+ update_time: find("#{key}.updateTime", data),
17
+ read_only: read_only
18
+ }
19
+ property = Property.new(options)
20
+ properties[property_name] = property
21
+ (class << self; self end).send(:define_method, property_name) { property.value }
22
+ end
23
+
24
+ def find(name, data)
25
+ parts = name.split '.'
26
+ result = data
27
+ parts.each do | part |
28
+ raise "Could not find part '#{part}' in '#{name}'" unless result.has_key? part
29
+ result = result[part]
30
+ end
31
+ result
32
+ end
33
+
34
+ def properties
35
+ @properties ||= {}
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,47 @@
1
+ module RZWaveWay
2
+ class Property
3
+ attr_reader :value
4
+ attr_reader :update_time
5
+
6
+ def initialize(options)
7
+ @previous_value = @value = options[:value]
8
+ @previous_update_time = @update_time = options[:update_time]
9
+ @read_only = options[:read_only]
10
+ end
11
+
12
+ def changed?
13
+ @previous_value != @value
14
+ end
15
+
16
+ def read_only?
17
+ @read_only == true
18
+ end
19
+
20
+ def save
21
+ @previous_value = @value
22
+ @previous_update_time = @update_time
23
+ end
24
+
25
+ def to_hash
26
+ {
27
+ read_only: read_only?,
28
+ value: @value,
29
+ update_time: @update_time
30
+ }
31
+ end
32
+
33
+ def update(value, update_time)
34
+ if @update_time <= update_time
35
+ @update_time = update_time
36
+ if @value != value
37
+ @value = value
38
+ true
39
+ else
40
+ false
41
+ end
42
+ else
43
+ false
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,3 +1,3 @@
1
1
  module RZWaveWay
2
- VERSION = '0.0.11'
2
+ VERSION = '0.0.13'
3
3
  end
@@ -1,133 +1,120 @@
1
- require 'json'
2
-
3
1
  module RZWaveWay
4
2
  class ZWaveDevice
5
- include CommandClass
6
3
  include CommandClasses
7
4
  include Logger
5
+ include PropertiesCache
8
6
 
9
- attr_reader :name
10
7
  attr_reader :id
11
8
  attr_reader :last_contact_time
12
- attr_accessor :contact_frequency
9
+ attr_reader :status
13
10
 
14
11
  def initialize(id, data)
15
12
  @id = id
13
+ @command_classes = {}
16
14
  initialize_from data
17
- log.info "Created ZWaveDevice with name='#{name}' (id='#{id}')"
15
+ update_status
16
+ log.info "Created device with name='#{name}' status=#{status} (id='#{id}')"
17
+ end
18
+
19
+ def contact
20
+ RZWaveWay::ZWay.instance.run_zway_no_operation(id)
18
21
  end
19
22
 
20
23
  def contacts_controller_periodically?
21
24
  support_commandclass? CommandClass::WAKEUP
22
25
  end
23
26
 
24
- def next_contact_time
25
- @last_contact_time + (@contact_frequency * (1 + @missed_contact_count) * 1.1)
27
+ def inspect
28
+ output = [to_s]
29
+ output += @command_classes.collect {|id, command_class| "#{id} - #{command_class}"}
30
+ output.join "\n"
26
31
  end
27
32
 
28
33
  def support_commandclass?(command_class_id)
29
34
  @command_classes.has_key? command_class_id
30
35
  end
31
36
 
32
- def process updates
33
- events = []
37
+ def process(updates)
34
38
  updates_per_commandclass = group_per_commandclass updates
35
39
  updates_per_commandclass.each do |cc, values|
36
40
  if @command_classes.has_key? cc
37
- event = @command_classes[cc].process(values)
38
- events << event if event
41
+ @command_classes[cc].process(values) {|event| yield event}
39
42
  else
40
43
  log.warn "Could not find command class: '#{cc}'"
41
44
  end
42
45
  end
43
- process_device_data(updates, events)
44
- events
45
- end
46
-
47
- def process_alive_check
48
- return if @dead
49
- if @contact_frequency > 0
50
- current_time = Time.now.to_i
51
- delta = current_time - next_contact_time
52
- if delta > 0
53
- count = ((current_time - @last_contact_time) / @contact_frequency).to_i
54
- if count > MAXIMUM_MISSED_CONTACT
55
- @dead = true
56
- DeadEvent.new(@id)
57
- elsif count > @missed_contact_count
58
- @missed_contact_count = count
59
- NotAliveEvent.new(@id, delta, count)
60
- end
61
- end
62
- end
46
+ process_device_data(updates)
47
+ save_changes
63
48
  end
64
49
 
65
50
  def notify_contacted(time)
66
- if time.to_i > @last_contact_time
67
- @dead = false
68
- @last_contact_time = time.to_i
69
- @missed_contact_count = 0
70
- true
51
+ if time > @last_contact_time
52
+ @last_contact_time = time
71
53
  end
72
54
  end
73
55
 
74
- def add_property(property)
75
- name = property.delete(:name)
76
- property[:read_only] = true unless property.has_key? :read_only
77
- @properties[name] = property
56
+ def refresh
57
+ @command_classes.values.each do |command_class|
58
+ command_class.refresh if command_class.respond_to? :refresh
59
+ end
78
60
  end
79
61
 
80
- def get_property(name)
81
- [
82
- @properties[name][:value],
83
- @properties[name][:update_time]
84
- ]
62
+ def state
63
+ hash = to_hash
64
+ @command_classes.values.each_with_object(hash) {|cc, hash| hash.merge!(cc.to_hash)}
85
65
  end
86
66
 
87
- def properties
88
- @properties.reject { |_, property| property.has_key? :internal }
67
+ def to_s
68
+ "#{id} (#{name}) - #{status} (#{Time.at(last_contact_time)})"
89
69
  end
90
70
 
91
- def update_property(name, value, update_time)
92
- if @properties.has_key?(name)
93
- property = @properties[name]
94
- if property[:value] != value || property[:update_time] < update_time
95
- property[:value] = value
96
- property[:update_time] = update_time
97
- true
71
+ def update_status
72
+ @status = if contacts_controller_periodically?
73
+ if self.WakeUp.on_time?
74
+ :alive
75
+ elsif self.WakeUp.missed_contact_count < 10 # times
76
+ :inactive
77
+ else
78
+ :dead
79
+ end
80
+ else
81
+ if elapsed_minutes_since_last_contact > 60 # minutes
82
+ :dead
83
+ elsif elapsed_minutes_since_last_contact > 5 # minutes
84
+ :inactive
85
+ else
86
+ :alive
98
87
  end
99
88
  end
100
89
  end
101
90
 
102
91
  private
103
92
 
104
- MAXIMUM_MISSED_CONTACT = 10
105
-
106
93
  def create_commandclasses_from data
107
- cc_classes = {}
108
94
  data['instances']['0']['commandClasses'].each do |id, sub_tree|
109
95
  cc_id = id.to_i
110
- cc_class = CommandClasses::Factory.instance.instantiate(cc_id, sub_tree, self)
111
- cc_classes[cc_id] = cc_class
96
+ cc_class = CommandClasses::Factory.instance.instantiate(cc_id, self)
97
+ cc_class.build_from(sub_tree)
98
+ @command_classes[cc_id] = cc_class
112
99
  cc_class_name = cc_class.class.name.split('::').last
113
- (class << self; self end).send(:define_method, cc_class_name) { cc_class } unless cc_class_name == 'Dummy'
100
+ (class << self; self end).send(:define_method, cc_class_name) { cc_class } unless cc_class_name == 'Unsupported'
114
101
  end
115
- cc_classes
116
102
  end
117
103
 
118
- def initialize_from data
119
- @name = find('data.givenName.value', data)
120
- last_contact_times = [
121
- find('data.lastReceived.updateTime', data),
122
- find('data.lastSend.updateTime', data)
123
- ]
124
- @last_contact_time = last_contact_times.max
104
+ def elapsed_minutes_since_last_contact(time = Time.now)
105
+ (time.to_i - last_contact_time) / 60
106
+ end
125
107
 
126
- @dead = false
127
- @missed_contact_count = 0
128
- @contact_frequency = 0
129
- @properties = {}
130
- @command_classes = create_commandclasses_from data
108
+ def initialize_from data
109
+ define_property(:name, 'data.givenName', true, data)
110
+ define_property(:is_failed, 'data.isFailed', true, data)
111
+ define_property(:failure_count, 'data.failureCount', true, data)
112
+
113
+ @last_contact_time = find('data.lastReceived.updateTime', data)
114
+ notify_contacted(properties[:is_failed].update_time) unless is_failed
115
+
116
+ create_commandclasses_from data
117
+ save_changes
131
118
  end
132
119
 
133
120
  def group_per_commandclass updates
@@ -137,7 +124,7 @@ module RZWaveWay
137
124
  match_data = key.match(/\Ainstances.0.commandClasses.(\d+)./)
138
125
  if match_data
139
126
  command_class = match_data[1].to_i
140
- updates_per_commandclass[command_class] = {} unless updates_per_commandclass.has_key?(command_class)
127
+ updates_per_commandclass[command_class] ||= {}
141
128
  updates_per_commandclass[command_class][match_data.post_match] = value
142
129
  else
143
130
  other_updates[key] = value
@@ -148,15 +135,23 @@ module RZWaveWay
148
135
  updates_per_commandclass
149
136
  end
150
137
 
151
- def process_device_data(updates, events)
152
- times = []
153
- updates.each do | key, value |
154
- if key == 'data.lastReceived' || key == 'data.lastSend'
155
- times << value['updateTime']
138
+ def process_device_data(data)
139
+ data.each do | key, value |
140
+ case key
141
+ when /^(?:data.)?failureCount/
142
+ properties[:failure_count].update(value['value'], value['updateTime'])
143
+ when /^(?:data.)?isFailed/
144
+ properties[:is_failed].update(value['value'], value['updateTime'])
145
+ notify_contacted(value['updateTime']) unless is_failed
146
+ when /^(?:data.)?lastReceived/
147
+ notify_contacted(value['updateTime'])
156
148
  end
157
149
  end
158
- time = times.max
159
- events << AliveEvent.new(@id, time) if notify_contacted(time)
150
+ end
151
+
152
+ def save_changes
153
+ save_properties
154
+ @command_classes.values.each {|cc| cc.save_properties}
160
155
  end
161
156
  end
162
157
  end
@@ -13,7 +13,10 @@ module RZWaveWay
13
13
  attr_reader :log
14
14
 
15
15
  def initialize
16
+ @devices = {}
17
+ @event_handlers = {}
16
18
  @log = default_logger
19
+ @update_time = '0'
17
20
  end
18
21
 
19
22
  def execute(device_id, command_class, command_class_function, argument = nil)
@@ -30,19 +33,31 @@ module RZWaveWay
30
33
  clazz.new(device)
31
34
  end
32
35
 
36
+ def inspect
37
+ content = to_s
38
+ devices.values.each {|device| content << "\n#{device}"}
39
+ content
40
+ end
41
+
42
+ def run_zway_no_operation device_id
43
+ run_zway "devices[#{device_id}].SendNoOperation()"
44
+ end
45
+
33
46
  def setup(options, *adapter_params)
34
47
  hostname = options[:hostname] || '127.0.0.1'
35
48
  port = options[:port] || 8083
49
+ username = options[:username] || 'admin'
50
+ password = options[:password] || 'changeme'
36
51
  adapter_params = :httpclient if adapter_params.compact.empty?
37
52
  @base_uri="http://#{hostname}:#{port}"
38
- @connection = Faraday.new {|faraday| faraday.adapter *adapter_params}
53
+ @connection = Faraday.new do |connection|
54
+ connection.basic_auth username, password
55
+ connection.adapter *adapter_params
56
+ end
39
57
  @log = options[:logger] if options.has_key? :logger
40
58
  end
41
59
 
42
60
  def start
43
- @devices = {}
44
- @event_handlers = {}
45
- @update_time = '0'
46
61
  loop do
47
62
  results = get_zway_data_tree_updates
48
63
  if results.has_key?('devices')
@@ -59,43 +74,52 @@ module RZWaveWay
59
74
  @event_handlers[event] = listener
60
75
  end
61
76
 
62
- def process_events
63
- check_devices
77
+ def process
64
78
  updates = get_zway_data_tree_updates
65
- events = devices_process updates
66
- check_not_alive_devices(events)
67
- deliver_to_handlers(events)
68
- end
69
-
70
- private
79
+ updates_per_device = group_per_device updates
71
80
 
72
- DATA_TREE_BASE_PATH='/ZWaveAPI/Data/'
73
- RUN_BASE_PATH='/ZWaveAPI/Run/'
81
+ events = []
82
+ devices.each do |device_id, device|
83
+ previous_status = device.status
74
84
 
75
- def check_devices
76
- @devices.values.each do |device|
77
- unless device.contacts_controller_periodically?
78
- current_time = Time.now.to_i
79
- # TODO ensure last_contact_time is set in the device initializer
80
- if (current_time % 10 == 0) && (current_time > device.next_contact_time - 60)
81
- run_zway_no_operation device.id
85
+ if updates_per_device.has_key? device_id
86
+ device_updates = updates_per_device[device_id]
87
+ device.process(device_updates) do |event|
88
+ events << event
82
89
  end
83
90
  end
91
+
92
+ if previous_status != device.update_status
93
+ events << create_status_event_for(device)
94
+ end
84
95
  end
96
+ deliver_to_handlers(events)
85
97
  end
86
98
 
87
- def check_not_alive_devices(events)
88
- @devices.values.each do |device|
89
- event = device.process_alive_check
90
- events << event if event
91
- end
99
+ def to_s
100
+ "ZWay at '#{@base_uri}' with #{devices.count} device(s)"
92
101
  end
93
102
 
103
+ private
104
+
105
+ DATA_TREE_BASE_PATH='/ZWaveAPI/Data/'
106
+ RUN_BASE_PATH='/ZWaveAPI/Run/'
107
+
94
108
  def create_device(device_id, device_data_tree)
95
109
  if device_id > 1
96
- device = ZWaveDevice.new(device_id, device_data_tree)
97
- device.contact_frequency = 300 unless device.contacts_controller_periodically?
98
- @devices[device_id] = device
110
+ @devices[device_id] = ZWaveDevice.new(device_id, device_data_tree)
111
+ end
112
+ end
113
+
114
+ def create_status_event_for(device)
115
+ log.debug "Device '#{device.name}' is now #{device.status}"
116
+ case device.status
117
+ when :alive
118
+ AliveDevice.new(device_id: device.id, time: device.last_contact_time)
119
+ when :inactive
120
+ InactiveDevice.new(device_id: device.id)
121
+ when :dead
122
+ DeadDevice.new(device_id: device.id)
99
123
  end
100
124
  end
101
125
 
@@ -114,20 +138,6 @@ module RZWaveWay
114
138
  end
115
139
  end
116
140
 
117
- def devices_process updates
118
- events = []
119
- updates_per_device = group_per_device updates
120
- updates_per_device.each do | id, updates |
121
- if @devices[id]
122
- device_events = @devices[id].process updates
123
- events += device_events unless device_events.empty?
124
- else
125
- log.warn "Could not find device with id '#{id}'"
126
- end
127
- end
128
- events
129
- end
130
-
131
141
  def group_per_device updates
132
142
  updates_per_device = {}
133
143
  updates.each do | key, value |
@@ -152,7 +162,7 @@ module RZWaveWay
152
162
  results = JSON.parse response.body
153
163
  @update_time = results.delete('updateTime')
154
164
  else
155
- log.error(response.reason)
165
+ log.error("#{response.status} - #{response.reason_phrase}" )
156
166
  end
157
167
  rescue StandardError => e
158
168
  log.error("Failed to communicate with ZWay HTTP server: #{e}")
@@ -190,10 +200,6 @@ module RZWaveWay
190
200
  run_zway command_path
191
201
  end
192
202
 
193
- def run_zway_no_operation device_id
194
- run_zway "devices[#{device_id}].SendNoOperation()"
195
- end
196
-
197
203
  def run_zway command_path
198
204
  begin
199
205
  uri = URI.encode(@base_uri + RUN_BASE_PATH + command_path, '[]')