waterfurnace_aurora 0.2.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1bbf110fc6004aa69327e04b6a778fb2b59db4357cbd4c9f519805b581a2d505
4
- data.tar.gz: dc95dcbaa26625f59b3822adae5c1e1e0dc38483df90d8ba10b5f417824d845f
3
+ metadata.gz: 47ba3e76677f00093692ed3f75daac6629fc77c71102a200efe88a846d271045
4
+ data.tar.gz: 6183cec3c273fafa6eb529e3e9fb0c6769941a51bd22d808528ffbd0f75b3fad
5
5
  SHA512:
6
- metadata.gz: f82a6d294a7643c0cfcc504a7e78f3eced539579d950d70f2ceb66f144891af71269c05c2cdffa2c505ae03175220317cd0b3cfffcedc092e1b3d014ede2a847
7
- data.tar.gz: 486d293ef3021d2c0342cfaa4a7286eef0e7e0854cbaa79be1d453c274d492f94c2bb78ae920bf6442ccc7c90b4b4cae416b67d0ccb9ca3d9c539fcd8c890497
6
+ metadata.gz: 6a961b2e49137c51cb888f0bf3a551c530be8eed02cc9fc978c38ab0ef3b00c582b90c1d44466f126f90bed0934632b22fe976166b3c59ded871b61b2ee6202c
7
+ data.tar.gz: f275fe6dce4542e0212abd90b7346c7e6970e23af7b657bc4c05ed3dd9682b36ad87e68abcbf03644e5fff62af89d2149edc0aa16605f33021734a52c81ca263
data/exe/aurora_monitor CHANGED
@@ -3,11 +3,38 @@
3
3
 
4
4
  require "aurora"
5
5
  require "ccutrer-serialport"
6
+ require "optparse"
6
7
  require "socket"
7
8
  require "uri"
8
9
 
10
+ diff_only = debug_modbus = ignore_awl_heartbeat = ignore_sensors = false
11
+
12
+ OptionParser.new do |opts|
13
+ opts.banner = "Usage: aurora_monitor /path/to/serial/port [options]"
14
+
15
+ opts.on("-q", "--quiet",
16
+ "Enables quiet mode (--diff-only, --ignore-awl-heartbeat, --ignore-sensors) to ease in deciphering new registers") do # rubocop:disable Layout/LineLength
17
+ diff_only = true
18
+ ignore_awl_heartbeat = true
19
+ ignore_sensors = true
20
+ end
21
+ opts.on("--diff-only", "Only show registers if they've changed from their previous value") { diff_only = true }
22
+ opts.on("--debug-modbus", "Print actual protocol bytes") { debug_modbus = true }
23
+ opts.on("--ignore-awl-heartbeat", "Don't print AWL heartbeat requests") { ignore_awl_heartbeat = true }
24
+ opts.on("--ignore-sensors", "Don't print sensor registers (i.e. because they change a lot)") { ignore_sensors = true }
25
+ opts.on("-h", "--help", "Prints this help") do
26
+ puts opts
27
+ exit
28
+ end
29
+ end.parse!
30
+
9
31
  uri = URI.parse(ARGV[0])
10
32
 
33
+ last_registers = {}
34
+
35
+ SENSOR_REGISTERS = [16, 19, 20, 740, 900, 1109, 1105, 1106, 1107, 1108, 1110, 1111, 1114, 1117, 1134, 1147, 1149, 1151,
36
+ 1153, 1165].freeze
37
+
11
38
  args = if uri.scheme == "telnet" || uri.scheme == "rfc2217"
12
39
  require "net/telnet/rfc2217"
13
40
  [Net::Telnet::RFC2217.new("Host" => uri.host,
@@ -20,23 +47,38 @@ args = if uri.scheme == "telnet" || uri.scheme == "rfc2217"
20
47
 
21
48
  server = ModBus::RTUServer.new(*args)
22
49
  server.promiscuous = true
23
- server.debug = true
50
+ server.debug = debug_modbus
51
+
52
+ diff_and_print = lambda do |registers|
53
+ registers = registers.slice(*(registers.keys - SENSOR_REGISTERS)) if ignore_sensors
54
+ next puts Aurora.print_registers(registers) unless diff_only
55
+
56
+ new_registers = last_registers.merge(registers)
57
+ diff = Aurora.diff_registers(last_registers, new_registers)
58
+ unless diff.empty?
59
+ puts "#{Time.now} ===== read"
60
+ puts Aurora.print_registers(diff)
61
+ end
62
+ last_registers = new_registers
63
+ end
24
64
 
25
65
  server.request_callback = lambda { |uid, func, req|
26
66
  if func == 68
27
- puts "===== no idea to #{uid}: #{req.inspect}"
67
+ puts "#{Time.now} ===== no idea to #{uid}: #{req.inspect}" unless diff_only
28
68
  elsif func == 67
29
- puts "===== write discontiguous registers to #{uid}:"
69
+ puts "#{Time.now} ===== write discontiguous registers to #{uid}:"
30
70
  registers = req.map { |p| [p[:addr], p[:val]] }.to_h
31
71
  puts Aurora.print_registers(registers)
32
72
  elsif func == 16
33
- puts "===== write multiple registers to #{uid}:"
34
73
  registers = Range.new(req[:addr], req[:addr] + req[:quant] - 1).zip(req[:val]).to_h
74
+ next if ignore_awl_heartbeat && registers == { 460 => 102, 461 => 0, 462 => 5 }
75
+
76
+ puts "#{Time.now} ===== write multiple registers to #{uid}:"
35
77
  puts Aurora.print_registers(registers)
36
78
  elsif [3, 65, 66].include?(func)
37
79
  # no output
38
80
  else
39
- puts "**** new func #{func}"
81
+ puts "#{Time.now} **** new func #{func}"
40
82
  end
41
83
  }
42
84
 
@@ -46,9 +88,9 @@ server.response_callback = lambda { |uid, func, res, req|
46
88
  puts "wrong number of results"
47
89
  next
48
90
  end
49
- puts "===== read registers from #{uid}"
91
+ puts "#{Time.now} ===== read registers from #{uid}" unless diff_only
50
92
  registers = Range.new(req[:addr], req[:addr] + req[:quant], true).to_a.zip(res).to_h
51
- puts Aurora.print_registers(registers)
93
+ diff_and_print.call(registers)
52
94
  elsif func == 65 && res.is_a?(Array) && req
53
95
  register_list = []
54
96
  req.each { |params| register_list.concat(Range.new(params[:addr], params[:addr] + params[:quant], true).to_a) }
@@ -56,24 +98,23 @@ server.response_callback = lambda { |uid, func, res, req|
56
98
  puts "wrong number of results"
57
99
  next
58
100
  end
59
- puts "===== read multiple register ranges from #{uid}"
101
+ puts "#{Time.now} ===== read multiple register ranges from #{uid}" unless diff_only
60
102
  result = register_list.zip(res).to_h
61
- puts Aurora.print_registers(result)
103
+ diff_and_print.call(result)
62
104
  elsif func == 66 && res.is_a?(Array) && req
63
105
  unless req.length == res.length
64
106
  puts "wrong number of results"
65
107
  next
66
108
  end
67
- puts "===== read discontiguous registers from #{uid}"
109
+ puts "#{Time.now} ===== read discontiguous registers from #{uid}" unless diff_only
68
110
  registers = req.zip(res).to_h
69
- puts Aurora.print_registers(registers)
111
+ diff_and_print.call(registers)
70
112
  elsif [16, 67, 68].include?(func)
71
113
  # no output
72
114
  else
73
- puts "**** new func #{func}"
115
+ puts "#{Time.now} **** new func #{func}"
74
116
  end
75
117
  }
76
118
 
77
- require "byebug"
78
119
  server.send(:serve, server.instance_variable_get(:@sp))
79
120
  loop { nil }
@@ -5,6 +5,7 @@ require "aurora"
5
5
  require "homie-mqtt"
6
6
  require "ccutrer-serialport"
7
7
  require "uri"
8
+ require "aurora/core_ext/string"
8
9
 
9
10
  uri = URI.parse(ARGV[0])
10
11
  mqtt_uri = ARGV[1]
@@ -28,7 +29,7 @@ class MQTTBridge
28
29
  @homie = homie
29
30
  @mutex = Mutex.new
30
31
 
31
- @homie.instance_variable_set(:@block, lambda do |topic, value|
32
+ @homie.out_of_band_topic_proc = lambda do |topic, value|
32
33
  @mutex.synchronize do
33
34
  case topic
34
35
  when /\$modbus$/
@@ -48,7 +49,7 @@ class MQTTBridge
48
49
  registers.merge!(@abc.modbus_slave.read_multiple_holding_registers(*subquery))
49
50
  end
50
51
  result = Aurora.print_registers(registers)
51
- @homie.mqtt.publish("#{@home.topic}/$modbus/response", result, retain: false, qos: 1)
52
+ @homie.mqtt.publish("#{@homie.topic}/$modbus/response", result, retain: false, qos: 1)
52
53
  when %r{\$modbus/(\d+)$}
53
54
  register = Regexp.last_match(1).to_i
54
55
  value = case value
@@ -61,47 +62,46 @@ class MQTTBridge
61
62
  end
62
63
  end
63
64
  rescue StandardError => e
64
- puts "failed processing message: #{e}\n#{e.backtrace}"
65
- end)
65
+ logger.error("failed processing message: #{e}\n#{e.backtrace}")
66
+ end
66
67
 
68
+ @abc.refresh
67
69
  publish_basic_attributes
68
70
 
69
71
  loop do
70
72
  begin
71
73
  @mutex.synchronize do
72
74
  @abc.refresh
73
- @homie_abc["compressor-speed"].value = @abc.compressor_speed
74
- @homie_abc["current-mode"].value = @abc.current_mode
75
- @homie_abc["dhw-water-temperature"].value = @abc.dhw_water_temperature
76
- @homie_abc["entering-air-temperature"].value = @abc.entering_air_temperature
77
- @homie_abc["entering-water-temperature"].value = @abc.entering_water_temperature
78
- @homie_abc["fan-speed"].value = @abc.fan_speed
79
- @homie_abc["leaving-air-temperature"].value = @abc.leaving_air_temperature
80
- @homie_abc["leaving-water-temperature"].value = @abc.leaving_water_temperature
81
- @homie_abc["outdoor-temperature"].value = @abc.outdoor_temperature
82
- @homie_abc["relative-humidity"].value = @abc.relative_humidity
83
- @homie_abc["waterflow"].value = @abc.waterflow
84
- @homie_abc["fp1"].value = @abc.fp1
85
- @homie_abc["fp2"].value = @abc.fp2
75
+ %i[compressor_speed
76
+ current_mode
77
+ dhw_water_temperature
78
+ entering_air_temperature
79
+ entering_water_temperature
80
+ fan_speed
81
+ leaving_air_temperature
82
+ leaving_water_temperature
83
+ outdoor_temperature
84
+ relative_humidity
85
+ waterflow
86
+ fp1
87
+ fp2
88
+ compressor_watts
89
+ blower_watts
90
+ aux_heat_watts
91
+ loop_pump_watts
92
+ total_watts].each do |property|
93
+ @homie_abc[property.to_s.tr("_", "-")].value = @abc.public_send(property)
94
+ end
86
95
 
87
- @abc.iz2_zones.each do |z|
88
- homie_zone = @homie["zone#{z.zone_number}"]
89
- homie_zone["target-mode"].value = z.target_mode
90
- homie_zone["current-mode"].value = z.current_mode
91
- homie_zone["target-fan-mode"].value = z.target_fan_mode
92
- homie_zone["current-fan-mode"].value = z.current_fan_mode
93
- homie_zone["fan-intermittent-on"].value = z.fan_intermittent_on
94
- homie_zone["fan-intermittent-off"].value = z.fan_intermittent_off
95
- homie_zone["priority"].value = z.priority
96
- homie_zone["size"].value = z.size
97
- homie_zone["normalized-size"].value = z.normalized_size
98
- homie_zone["ambient-temperature"].value = z.ambient_temperature
99
- homie_zone["heating-target-temperature"].value = z.heating_target_temperature
100
- homie_zone["cooling-target-temperature"].value = z.cooling_target_temperature
96
+ @abc.zones.each_with_index do |z, idx|
97
+ homie_zone = @homie["zone#{idx + 1}"]
98
+ homie_zone.each do |property|
99
+ property.value = z.public_send(property.id.tr("-", "_"))
100
+ end
101
101
  end
102
102
  end
103
103
  rescue => e
104
- puts "got garbage: #{e}; #{e.backtrace}"
104
+ logger.error("got garbage: #{e}; #{e.backtrace}")
105
105
  exit 1
106
106
  end
107
107
  sleep(5)
@@ -129,34 +129,46 @@ class MQTTBridge
129
129
  node.property("waterflow", "Waterflow", :float, unit: "gpm")
130
130
  node.property("fp1", "FP1 Sensor", :float, @abc.fp1, unit: "ºF")
131
131
  node.property("fp2", "FP2 Sensor", :float, @abc.fp2, unit: "ºF")
132
+ %i[compressor blower aux_heat loop_pump total].each do |component|
133
+ component = "#{component}_watts"
134
+ node.property(component.tr("_", "-"), component.tr("_", " ").titleize, :integer,
135
+ @abc.public_send(component), unit: "W")
136
+ end
132
137
  end
133
138
 
134
- @abc.iz2_zones.each do |zone|
135
- @homie.node("zone#{zone.zone_number}", "Zone #{zone.zone_number}", "IntelliZone 2 Zone") do |node|
139
+ @abc.zones.each_with_index do |zone, i|
140
+ type = zone.is_a?(Aurora::IZ2Zone) ? "IntelliZone 2 Zone" : "Thermostat"
141
+ @homie.node("zone#{i + 1}", "Zone #{i + 1}", type) do |node|
136
142
  allowed_modes = %w[off auto cool heat]
137
- allowed_modes << "eheat" if zone.zone_number == 1
143
+ allowed_modes << "eheat" if i.zero?
138
144
  node.property("target-mode", "Target Heating/Cooling Mode", :enum, zone.target_mode,
139
145
  format: allowed_modes) do |value, property|
140
146
  @mutex.synchronize { property.value = zone.target_mode = value.to_sym }
141
147
  end
142
- node.property("current-mode", "Current Heating/Cooling Mode Requested", :enum, zone.current_mode,
143
- format: %w[standby h1 h2 h3 c1 c2])
144
- node.property("target-fan-mode", "Target Fan Mode", :enum, zone.target_fan_mode,
148
+ if zone.respond_to?(:current_mode) # TODO: implement for non-IZ2
149
+ node.property("current-mode", "Current Heating/Cooling Mode Requested", :enum, zone.current_mode,
150
+ format: %w[standby h1 h2 h3 c1 c2])
151
+ end
152
+ node.property("target-fan-mode", "Target Fan Mode", :enum, zone.target_fan_mode,
145
153
  format: %w[auto continuous intermittent]) do |value, property|
146
154
  @mutex.synchronize { property.value = zone.target_fan_mode = value.to_sym }
147
155
  end
148
- node.property("current-fan-mode", "Current Fan Status", :boolean, zone.current_fan_mode)
149
- node.property("fan-intermittent-on", "Fan Intermittent Mode On Duration", :enum, zone.fan_intermittent_on,
150
- unit: "M", format: %w[0 5 10 15 20]) do |value, property|
151
- @mutex.synchronize { property.value = zone.fan_intermittent_on = value.to_i }
156
+ if zone.respond_to?(:current_fan_mode) # TODO: implement for non-IZ2
157
+ node.property("current-fan-mode", "Current Fan Status", :boolean, zone.current_fan_mode)
158
+ node.property("fan-intermittent-on", "Fan Intermittent Mode On Duration", :enum, zone.fan_intermittent_on,
159
+ unit: "M", format: %w[0 5 10 15 20]) do |value, property|
160
+ @mutex.synchronize { property.value = zone.fan_intermittent_on = value.to_i }
161
+ end
162
+ node.property("fan-intermittent-off", "Fan Intermittent Mode Off Duration", :enum, zone.fan_intermittent_on,
163
+ unit: "M", format: %w[0 5 10 15 20 25 30 35 40]) do |value, property|
164
+ @mutex.synchronize { property.value = zone.fan_intermittent_on = value.to_i }
165
+ end
152
166
  end
153
- node.property("fan-intermittent-off", "Fan Intermittent Mode Off Duration", :enum, zone.fan_intermittent_on,
154
- unit: "M", format: %w[0 5 10 15 20 25 30 35 40]) do |value, property|
155
- @mutex.synchronize { property.value = zone.fan_intermittent_on = value.to_i }
167
+ if zone.is_a?(Aurora::IZ2Zone)
168
+ node.property("priority", "Zone Priority", :enum, zone.priority, format: %w[economy comfort])
169
+ node.property("size", "Size", :enum, zone.size, format: %w[0 25 45 70])
170
+ node.property("normalized-size", "Normalized Size", :integer, zone.normalized_size, unit: "%", format: 0..100)
156
171
  end
157
- node.property("priority", "Zone Priority", :enum, zone.priority, format: %w[economy comfort])
158
- node.property("size", "Size", :enum, zone.size, format: %w[0 25 45 70])
159
- node.property("normalized-size", "Normalized Size", :integer, zone.normalized_size, unit: "%", format: 0..100)
160
172
  node.property("ambient-temperature", "Ambient Temperature", :float, zone.ambient_temperature, unit: "ºF")
161
173
  node.property("heating-target-temperature", "Heating Target Temperature", :integer,
162
174
  zone.heating_target_temperature, unit: "ºF", format: 40..90) do |value, property|
@@ -176,4 +188,13 @@ class MQTTBridge
176
188
  end
177
189
  end
178
190
 
179
- MQTTBridge.new(abc, MQTT::Homie::Device.new("aurora", "Aurora MQTT Bridge", mqtt: mqtt_uri))
191
+ log_level = ARGV.include?("--debug") ? :debug : :warn
192
+ logger = Logger.new($stdout)
193
+ logger.level = log_level
194
+ slave.logger = logger
195
+
196
+ device = "aurora-#{abc.serial_number}"
197
+ homie = MQTT::Homie::Device.new(device, "Aurora MQTT Bridge", mqtt: mqtt_uri)
198
+ homie.logger = logger
199
+
200
+ MQTTBridge.new(abc, homie)
@@ -3,7 +3,8 @@
3
3
  module Aurora
4
4
  class ABCClient
5
5
  attr_reader :modbus_slave,
6
- :iz2_zones,
6
+ :serial_number,
7
+ :zones,
7
8
  :current_mode,
8
9
  :fan_speed,
9
10
  :entering_air_temperature,
@@ -16,26 +17,42 @@ module Aurora
16
17
  :compressor_speed,
17
18
  :outdoor_temperature,
18
19
  :fp1,
19
- :fp2
20
+ :fp2,
21
+ :compressor_watts,
22
+ :blower_watts,
23
+ :aux_heat_watts,
24
+ :loop_pump_watts,
25
+ :total_watts
20
26
 
21
27
  def initialize(modbus_slave)
22
28
  @modbus_slave = modbus_slave
23
29
  @modbus_slave.read_retry_timeout = 15
24
30
  @modbus_slave.read_retries = 2
31
+ registers_array = @modbus_slave.holding_registers[105...110]
32
+ registers = registers_array.each_with_index.map { |r, i| [i + 105, r] }.to_h
33
+ @serial_number = Aurora.transform_registers(registers)[105]
25
34
  iz2_zone_count = @modbus_slave.holding_registers[483]
26
- @iz2_zones = (0...iz2_zone_count).map { |i| IZ2Zone.new(self, i + 1) }
35
+ # TODO: better detect IZ2/Non-IZ2
36
+ @zones = if iz2_zone_count > 1
37
+ (0...iz2_zone_count).map { |i| IZ2Zone.new(self, i + 1) }
38
+ else
39
+ [Thermostat.new(self)]
40
+ end
27
41
  end
28
42
 
29
43
  def refresh
30
- registers_to_read = [19..20, 30, 344, 740..741, 900, 1110..1111, 1114, 1117, 3027, 31_003]
31
- # IZ2 zones
32
- iz2_zones.each_with_index do |_z, i|
33
- base1 = 21_203 + i * 9
34
- base2 = 31_007 + i * 3
35
- base3 = 31_200 + i * 3
36
- registers_to_read << (base1..(base1 + 1))
37
- registers_to_read << (base2..(base2 + 2))
38
- registers_to_read << base3
44
+ registers_to_read = [19..20, 30, 344, 740..741, 900, 1110..1111, 1114, 1117, 1147..1153, 1165, 3027, 31_003]
45
+ if zones.first.is_a?(IZ2Zone)
46
+ zones.each_with_index do |_z, i|
47
+ base1 = 21_203 + i * 9
48
+ base2 = 31_007 + i * 3
49
+ base3 = 31_200 + i * 3
50
+ registers_to_read << (base1..(base1 + 1))
51
+ registers_to_read << (base2..(base2 + 2))
52
+ registers_to_read << base3
53
+ end
54
+ else
55
+ registers_to_read << 745..747
39
56
  end
40
57
 
41
58
  registers = @modbus_slave.holding_registers[*registers_to_read]
@@ -54,6 +71,11 @@ module Aurora
54
71
  @fp1 = registers[19]
55
72
  @fp2 = registers[20]
56
73
  @locked_out = registers[1117]
74
+ @compressor_watts = registers[1147]
75
+ @blower_watts = registers[1149]
76
+ @aux_heat_watts = registers[1151]
77
+ @loop_pump_watts = registers[1165]
78
+ @total_watts = registers[1153]
57
79
 
58
80
  outputs = registers[30]
59
81
  @current_mode = if outputs.include?(:lockout)
@@ -72,7 +94,7 @@ module Aurora
72
94
  :standby
73
95
  end
74
96
 
75
- iz2_zones.each do |z|
97
+ zones.each do |z|
76
98
  z.refresh(registers)
77
99
  end
78
100
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aurora
4
+ module Inflector
5
+ def titleize
6
+ gsub(/\b(?<!\w['â`])[a-z]/, &:capitalize)
7
+ end
8
+ end
9
+ end
10
+ String.include(Aurora::Inflector)
@@ -1,22 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "aurora/thermostat"
4
+
3
5
  module Aurora
4
- class IZ2Zone
6
+ class IZ2Zone < Thermostat
5
7
  attr_reader :zone_number,
6
- :target_mode,
7
8
  :current_mode,
8
- :target_fan_mode,
9
9
  :current_fan_mode,
10
10
  :fan_intermittent_on,
11
11
  :fan_intermittent_off,
12
12
  :priority,
13
- :size, :normalized_size,
14
- :ambient_temperature,
15
- :cooling_target_temperature,
16
- :heating_target_temperature
13
+ :size, :normalized_size
17
14
 
18
15
  def initialize(abc, zone_number)
19
- @abc = abc
16
+ super(abc)
20
17
  @zone_number = zone_number
21
18
  end
22
19
 
@@ -42,68 +39,47 @@ module Aurora
42
39
  end
43
40
 
44
41
  def target_mode=(value)
45
- value = Aurora::HEATING_MODE.invert[value]
46
- return unless value
42
+ return unless (raw_value = Aurora::HEATING_MODE.invert[value])
47
43
 
48
- @abc.modbus_slave.holding_registers[21_202 + (zone_number - 1) * 9] = value
49
- @target_mode = Aurora::HEATING_MODE[@abc.modbus_slave.holding_registers[21_202 + (zone_number - 1) * 9]]
44
+ @abc.modbus_slave.holding_registers[21_202 + (zone_number - 1) * 9] = raw_value
45
+ @target_mode = value
50
46
  end
51
47
 
52
48
  def target_fan_mode=(value)
53
- value = Aurora::FAN_MODE.invert[value]
54
- return unless value
49
+ return unless (raw_value = Aurora::FAN_MODE.invert[value])
55
50
 
56
- @abc.modbus_slave.holding_registers[21_205 + (zone_number - 1) * 9] = value
57
- registers = @abc.modbus_slave.read_multiple_holding_registers(31_008 + (zone_number - 1) * 3)
58
- Aurora.transform_registers(registers)
59
- @target_fan_mode = registers.first.last[:fan]
51
+ @abc.modbus_slave.holding_registers[21_205 + (zone_number - 1) * 9] = raw_value
52
+ @target_fan_mode = value
60
53
  end
61
54
 
62
55
  def fan_intermittent_on=(value)
63
56
  return unless value >= 0 && value <= 25 && (value % 5).zero?
64
57
 
65
58
  @abc.modbus_slave.holding_registers[21_206 + (zone_number - 1) * 9] = value
66
- registers = @abc.modbus_slave.read_multiple_holding_registers(31_008 + (zone_number - 1) * 3)
67
- Aurora.transform_registers(registers)
68
- @fan_intermittent_on = registers.first.last[:on_time]
59
+ @fan_intermittent_on = value
69
60
  end
70
61
 
71
62
  def fan_intermittent_off=(value)
72
63
  return unless value >= 0 && value <= 40 && (value % 5).zero?
73
64
 
74
65
  @abc.modbus_slave.holding_registers[21_207 + (zone_number - 1) * 9] = value
75
- registers = @abc.modbus_slave.read_multiple_holding_registers(31_008 + (zone_number - 1) * 3)
76
- Aurora.transform_registers(registers)
77
- @fan_intermittent_on = registers.first.last[:off_time]
66
+ @fan_intermittent_off = value
78
67
  end
79
68
 
80
69
  def heating_target_temperature=(value)
81
70
  return unless value >= 40 && value <= 90
82
71
 
83
- value = (value * 10).to_i
84
- @abc.modbus_slave.holding_registers[21_203 + (zone_number - 1) * 9] = value
85
-
86
- base = 31_008 + (zone_number - 1) * 3
87
- registers = @abc.modbus_slave.read_multiple_holding_registers(base..(base + 1))
88
- Aurora.transform_registers(registers)
89
- registers[base + 1][:heating_target_temperature]
72
+ raw_value = (value * 10).to_i
73
+ @abc.modbus_slave.holding_registers[21_203 + (zone_number - 1) * 9] = raw_value
74
+ @heating_target_temperature = value
90
75
  end
91
76
 
92
77
  def cooling_target_temperature=(value)
93
78
  return unless value >= 54 && value <= 99
94
79
 
95
- value = (value * 10).to_i
80
+ raw_value = (value * 10).to_i
96
81
  @abc.modbus_slave.holding_registers[21_204 + (zone_number - 1) * 9] = value
97
-
98
- registers = @abc.modbus_slave.read_multiple_holding_registers(31_008 + (zone_number - 1) * 3)
99
- Aurora.transform_registers(registers)
100
- registers.first.last[:cooling_target_temperature]
101
- end
102
-
103
- def inspect
104
- "#<Aurora::IZ2Zone #{(instance_variables - [:@abc]).map do |iv|
105
- "#{iv}=#{instance_variable_get(iv).inspect}"
106
- end.join(', ')}>"
82
+ @cooling_target_temperature = raw_value
107
83
  end
108
84
  end
109
85
  end
@@ -17,7 +17,7 @@ module Aurora
17
17
  end
18
18
 
19
19
  def holding_registers
20
- WFProxy.new(self, :holding_register)
20
+ @holding_registers ||= WFProxy.new(self, :holding_register)
21
21
  end
22
22
  end
23
23
 
@@ -33,7 +33,6 @@ module Aurora
33
33
  def read_rtu_response(io)
34
34
  # Read the slave_id and function code
35
35
  msg = read(io, 2)
36
- log logging_bytes(msg)
37
36
 
38
37
  function_code = msg.getbyte(1)
39
38
  case function_code
@@ -52,7 +52,6 @@ module Aurora
52
52
  end
53
53
 
54
54
  def to_string(registers, idx, length)
55
- puts "converting #{idx} of length #{length}"
56
55
  (idx...(idx + length)).map do |i|
57
56
  (registers[i] >> 8).chr + (registers[i] & 0xff).chr
58
57
  end.join.sub(/[ \0]+$/, "")
@@ -323,7 +322,8 @@ module Aurora
323
322
  REGISTER_CONVERTERS = {
324
323
  TO_HUNDREDTHS => [2, 3, 807, 813, 816, 817, 819, 820, 825, 828],
325
324
  method(:dipswitch_settings) => [4, 33],
326
- TO_TENTHS => [19, 20, 401, 567, 740, 745, 746, 900, 1105, 1106, 1107, 1108, 1110, 1111, 1114, 1117, 1134, 1136,
325
+ TO_TENTHS => [19, 20, 401, 567, 740, 745, 746, 747, 900, 1105, 1106, 1107, 1108, 1110, 1111, 1114, 1117, 1134, 1136,
326
+ 12_619, 12_620,
327
327
  21_203, 21_204,
328
328
  21_212, 21_213,
329
329
  21_221, 21_222,
@@ -348,8 +348,8 @@ module Aurora
348
348
  ->(v) { from_bitmask(v, AXB_INPUTS) } => [1103],
349
349
  ->(v) { from_bitmask(v, AXB_OUTPUTS) } => [1104],
350
350
  ->(v) { TO_TENTHS.call(NEGATABLE.call(v)) } => [1136],
351
- ->(v) { HEATING_MODE[v] } => [21_202, 21_211, 21_220, 21_229, 21_238, 21_247],
352
- ->(v) { FAN_MODE[v] } => [21_205, 21_214, 21_223, 21_232, 21_241, 21_250],
351
+ ->(v) { HEATING_MODE[v] } => [12_602, 21_202, 21_211, 21_220, 21_229, 21_238, 21_247],
352
+ ->(v) { FAN_MODE[v] } => [12_621, 21_205, 21_214, 21_223, 21_232, 21_241, 21_250],
353
353
  ->(v) { from_bitmask(v, HUMIDIFIER_SETTINGS) } => [31_109],
354
354
  ->(v) { { humidification_target: v >> 8, dehumidification_target: v & 0xff } } => [31_110],
355
355
  method(:iz2_demand) => [31_005],
@@ -367,7 +367,8 @@ module Aurora
367
367
  REGISTER_FORMATS = {
368
368
  "%ds" => [1, 6, 9, 15, 84, 85],
369
369
  "%dV" => [16, 112],
370
- "%0.1fºF" => [19, 20, 401, 567, 740, 745, 746, 900, 1110, 1111, 1114, 1134, 1136,
370
+ "%0.1fºF" => [19, 20, 401, 567, 740, 745, 746, 747, 900, 1110, 1111, 1114, 1134, 1136,
371
+ 12_619, 12_620,
371
372
  21_203, 21_204,
372
373
  21_212, 21_213,
373
374
  21_221, 21_222,
@@ -398,7 +399,7 @@ module Aurora
398
399
  base2 = 31_007 + (i - 1) * 3
399
400
  base3 = 31_200 + (i - 1) * 3
400
401
  {
401
- base1 => "Zone #{i} Heating Mode",
402
+ base1 => "Zone #{i} Heating Mode (write)",
402
403
  (base1 + 1) => "Zone #{i} Heating Setpoint (write)",
403
404
  (base1 + 2) => "Zone #{i} Cooling Setpoint (write)",
404
405
  (base1 + 3) => "Zone #{i} Fan Mode (write)",
@@ -493,25 +494,6 @@ module Aurora
493
494
  61_000..61_009
494
495
  ].freeze
495
496
 
496
- def read_all_registers(modbus_slave)
497
- result = []
498
- REGISTER_RANGES.each do |range|
499
- # read at most 100 at a time
500
- range.each_slice(100) do |keys|
501
- result.concat(modbus_slave.holding_registers[keys.first..keys.last])
502
- end
503
- end
504
- REGISTER_RANGES.map(&:to_a).flatten.zip(result).to_h
505
- end
506
-
507
- def diff_registers(lhs, rhs)
508
- diff = {}
509
- lhs.each_key do |k|
510
- diff[k] = [lhs[k], rhs[k]] if lhs[k] != rhs[k]
511
- end
512
- diff
513
- end
514
-
515
497
  REGISTER_NAMES = {
516
498
  1 => "Random Start Delay",
517
499
  2 => "ABC Program Version",
@@ -579,6 +561,7 @@ module Aurora
579
561
  741 => "Relative Humidity",
580
562
  745 => "Heating Set Point",
581
563
  746 => "Cooling Set Point",
564
+ 747 => "Ambient Temperature",
582
565
  807 => "AXB Version",
583
566
  813 => "IZ2 Version?",
584
567
  816 => "AOC Version 1?",
@@ -607,6 +590,10 @@ module Aurora
607
590
  1153 => "Total Watts",
608
591
  1157 => "Ht of Rej",
609
592
  1165 => "VS Pump Watts",
593
+ 12_602 => "Heating Mode (write)",
594
+ 12_619 => "Heating Setpoint (write)",
595
+ 12_620 => "Cooling Setpoint (write)",
596
+ 12_621 => "Fan Mode (write)",
610
597
  3027 => "Compressor Speed",
611
598
  31_003 => "Outdoor Temp",
612
599
  31_005 => "IZ2 Demand",
@@ -641,6 +628,25 @@ module Aurora
641
628
  registers
642
629
  end
643
630
 
631
+ def read_all_registers(modbus_slave)
632
+ result = []
633
+ REGISTER_RANGES.each do |range|
634
+ # read at most 100 at a time
635
+ range.each_slice(100) do |keys|
636
+ result.concat(modbus_slave.holding_registers[keys.first..keys.last])
637
+ end
638
+ end
639
+ REGISTER_RANGES.map(&:to_a).flatten.zip(result).to_h
640
+ end
641
+
642
+ def diff_registers(lhs, rhs)
643
+ diff = {}
644
+ (lhs.keys | rhs.keys).each do |k|
645
+ diff[k] = rhs[k] if lhs[k] != rhs[k]
646
+ end
647
+ diff
648
+ end
649
+
644
650
  def print_registers(registers)
645
651
  result = []
646
652
  registers.each do |(k, value)|
@@ -648,15 +654,17 @@ module Aurora
648
654
  next if REGISTER_NAMES.key?(k) && REGISTER_NAMES[k].nil?
649
655
 
650
656
  name = REGISTER_NAMES[k]
657
+
651
658
  value_proc = REGISTER_CONVERTERS.find { |(_, z)| z.include?(k) }&.first || ->(v) { v }
652
659
  format = REGISTER_FORMATS.find { |(_, z)| z.include?(k) }&.first || "%s"
653
660
  format = "%1$d (0x%1$04x)" unless name
654
- name ||= "???"
655
661
 
656
662
  value = value_proc.arity == 2 ? value_proc.call(registers, k) : value_proc.call(value)
657
663
  value = value.join(", ") if value.is_a?(Array)
658
664
  value = format(format, value) if value
659
665
 
666
+ name ||= "???"
667
+
660
668
  result << "#{name} (#{k}): #{value}"
661
669
  end
662
670
  result.join("\n")
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aurora
4
+ class Thermostat
5
+ attr_reader :target_mode,
6
+ :target_fan_mode,
7
+ :ambient_temperature,
8
+ :cooling_target_temperature,
9
+ :heating_target_temperature
10
+
11
+ def initialize(abc)
12
+ @abc = abc
13
+ end
14
+
15
+ def refresh(registers)
16
+ @ambient_temperature = registers[747]
17
+ @heating_target_temperature = registers[746]
18
+ @cooling_target_temperature = registers[745]
19
+ end
20
+
21
+ def target_mode=(value)
22
+ return unless (raw_value = HEATING_MODE.invert[value])
23
+
24
+ @abc.modbus_slave.holding_registers[12_602] = raw_value
25
+ @target_mode = value
26
+ end
27
+
28
+ def target_fan_mode=(value)
29
+ return unless (raw_value = FAN_MODE.invert[value])
30
+
31
+ @abc.modbus_slave.holding_registers[12_621] = raw_value
32
+ @target_fan_mode = value
33
+ end
34
+
35
+ def heating_target_temperature=(value)
36
+ return unless value >= 40 && value <= 90
37
+
38
+ raw_value = (value * 10).to_i
39
+ @abc.modbus_slave.holding_registers[12_619] = raw_value
40
+ @heating_target_temperature = value
41
+ end
42
+
43
+ def cooling_target_temperature=(value)
44
+ return unless value >= 54 && value <= 99
45
+
46
+ raw_value = (value * 10).to_i
47
+ @abc.modbus_slave.holding_registers[12_620] = raw_value
48
+ @cooling_target_temperature = value
49
+ end
50
+
51
+ def inspect
52
+ "#<Aurora::#{self.class.name} #{(instance_variables - [:@abc]).map do |iv|
53
+ "#{iv}=#{instance_variable_get(iv).inspect}"
54
+ end.join(', ')}>"
55
+ end
56
+ end
57
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aurora
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.2"
5
5
  end
data/lib/aurora.rb CHANGED
@@ -4,6 +4,7 @@ require "rmodbus"
4
4
 
5
5
  require "aurora/abc_client"
6
6
  require "aurora/iz2_zone"
7
+ require "aurora/thermostat"
7
8
  require "aurora/modbus/server"
8
9
  require "aurora/modbus/slave"
9
10
  require "aurora/registers"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: waterfurnace_aurora
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-08-23 00:00:00.000000000 Z
11
+ date: 2021-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ccutrer-serialport
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 1.4.1
33
+ version: 1.4.4
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 1.4.1
40
+ version: 1.4.4
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: net-telnet-rfc2217
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '2.0'
61
+ version: '2.1'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '2.0'
68
+ version: '2.1'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: byebug
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -125,10 +125,12 @@ files:
125
125
  - exe/registers.yml
126
126
  - lib/aurora.rb
127
127
  - lib/aurora/abc_client.rb
128
+ - lib/aurora/core_ext/string.rb
128
129
  - lib/aurora/iz2_zone.rb
129
130
  - lib/aurora/modbus/server.rb
130
131
  - lib/aurora/modbus/slave.rb
131
132
  - lib/aurora/registers.rb
133
+ - lib/aurora/thermostat.rb
132
134
  - lib/aurora/version.rb
133
135
  - lib/waterfurnace_aurora.rb
134
136
  homepage: https://github.com/ccutrer/waterfurnace