rzwaveway 0.0.11 → 0.0.13

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.
@@ -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, '[]')