waterfurnace_aurora 0.2.2 → 0.3.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19a356cb48b70e999a4dda4d697fe20f178f19c1e86de2fbc4c2d0eda6cc2b40
4
- data.tar.gz: 7f831138bcf91c456ed380e3a2661e52abfcdfed308fc5587618c202a7102c12
3
+ metadata.gz: c3127c610617c1008b2de42b4b03fc89867d12233548fc0baf6ea0ebb5ea08e8
4
+ data.tar.gz: '0190fccfece31159a9c5acdd14b648521c32e7b64f7cb596daeb3548b882393a'
5
5
  SHA512:
6
- metadata.gz: e15907a96a287316f1631ad43db6f510dc7353bd985d5343017078d8742fb830085b13eaf2f128db0c409525b7b533d83dcd19f70d4a830372b24c78b0285be2
7
- data.tar.gz: ed4b561646b9885005ad9c327f46dcaac5abd86cf20ceb1ac1f147bc19afb57c56f0344c23d88716fef53127e6a45c0c7a61f321bc843ade914eb23068f728ce
6
+ metadata.gz: 5d8ae25889bcea9d6f6e7adfe8e242da729bc5172102688d4790c20a594e094f2aa27c78dfc7104895109bf4dc1636e357e6a03f60cb20918085f95fb8059211
7
+ data.tar.gz: 72682b009c4f19ff335d1b19759ccd813130148e9d5cfabfb9c915c88a497dbcc3290b2a36e1ef173e513843fd9e19827080236c092fd311618f88ac128e3de4
data/exe/aurora_fetch CHANGED
@@ -18,7 +18,7 @@ args = if uri.scheme == "telnet" || uri.scheme == "rfc2217"
18
18
  end
19
19
 
20
20
  client = ModBus::RTUClient.new(*args)
21
- client.debug = true
21
+ client.logger = Logger.new($stdout, :debug)
22
22
  slave = client.with_slave(1)
23
23
 
24
24
  registers = slave.holding_registers[ARGV[1].to_i]
data/exe/aurora_mock CHANGED
@@ -21,7 +21,7 @@ args = if uri.scheme == "telnet" || uri.scheme == "rfc2217"
21
21
  port = ARGV[1]&.to_i || 502
22
22
 
23
23
  server1 = ModBus::RTUServer.new(*args)
24
- server1.debug = true
24
+ server1.logger = Logger.new($stdout, :debug)
25
25
  # AID Tool queries slave 1, AWL queries slave 2; just use both
26
26
  slave1 = server1.with_slave(1)
27
27
  slave2 = server1.with_slave(2)
data/exe/aurora_monitor CHANGED
@@ -3,11 +3,39 @@
3
3
 
4
4
  require "aurora"
5
5
  require "ccutrer-serialport"
6
+ require "logger"
7
+ require "optparse"
6
8
  require "socket"
7
9
  require "uri"
8
10
 
11
+ diff_only = debug_modbus = ignore_awl_heartbeat = ignore_sensors = false
12
+
13
+ OptionParser.new do |opts|
14
+ opts.banner = "Usage: aurora_monitor /path/to/serial/port [options]"
15
+
16
+ opts.on("-q", "--quiet",
17
+ "Enables quiet mode (--diff-only, --ignore-awl-heartbeat, --ignore-sensors) to ease in deciphering new registers") do # rubocop:disable Layout/LineLength
18
+ diff_only = true
19
+ ignore_awl_heartbeat = true
20
+ ignore_sensors = true
21
+ end
22
+ opts.on("--diff-only", "Only show registers if they've changed from their previous value") { diff_only = true }
23
+ opts.on("--debug-modbus", "Print actual protocol bytes") { debug_modbus = true }
24
+ opts.on("--ignore-awl-heartbeat", "Don't print AWL heartbeat requests") { ignore_awl_heartbeat = true }
25
+ opts.on("--ignore-sensors", "Don't print sensor registers (i.e. because they change a lot)") { ignore_sensors = true }
26
+ opts.on("-h", "--help", "Prints this help") do
27
+ puts opts
28
+ exit
29
+ end
30
+ end.parse!
31
+
9
32
  uri = URI.parse(ARGV[0])
10
33
 
34
+ last_registers = {}
35
+
36
+ SENSOR_REGISTERS = [16, 19, 20, 740, 900, 1109, 1105, 1106, 1107, 1108, 1110, 1111, 1114, 1117, 1134, 1147, 1149, 1151,
37
+ 1153, 1165].freeze
38
+
11
39
  args = if uri.scheme == "telnet" || uri.scheme == "rfc2217"
12
40
  require "net/telnet/rfc2217"
13
41
  [Net::Telnet::RFC2217.new("Host" => uri.host,
@@ -20,23 +48,39 @@ args = if uri.scheme == "telnet" || uri.scheme == "rfc2217"
20
48
 
21
49
  server = ModBus::RTUServer.new(*args)
22
50
  server.promiscuous = true
23
- server.debug = true
51
+ server.logger = Logger.new($stdout)
52
+ server.logger.level = :debug if debug_modbus
53
+
54
+ diff_and_print = lambda do |registers|
55
+ registers = registers.slice(*(registers.keys - SENSOR_REGISTERS)) if ignore_sensors
56
+ next puts Aurora.print_registers(registers) unless diff_only
57
+
58
+ new_registers = last_registers.merge(registers)
59
+ diff = Aurora.diff_registers(last_registers, new_registers)
60
+ unless diff.empty?
61
+ puts "#{Time.now} ===== read"
62
+ puts Aurora.print_registers(diff)
63
+ end
64
+ last_registers = new_registers
65
+ end
24
66
 
25
67
  server.request_callback = lambda { |uid, func, req|
26
68
  if func == 68
27
- puts "===== no idea to #{uid}: #{req.inspect}"
69
+ puts "#{Time.now} ===== no idea to #{uid}: #{req.inspect}" unless diff_only
28
70
  elsif func == 67
29
- puts "===== write discontiguous registers to #{uid}:"
71
+ puts "#{Time.now} ===== write discontiguous registers to #{uid}:"
30
72
  registers = req.map { |p| [p[:addr], p[:val]] }.to_h
31
73
  puts Aurora.print_registers(registers)
32
74
  elsif func == 16
33
- puts "===== write multiple registers to #{uid}:"
34
75
  registers = Range.new(req[:addr], req[:addr] + req[:quant] - 1).zip(req[:val]).to_h
76
+ next if ignore_awl_heartbeat && registers == { 460 => 102, 461 => 0, 462 => 5 }
77
+
78
+ puts "#{Time.now} ===== write multiple registers to #{uid}:"
35
79
  puts Aurora.print_registers(registers)
36
80
  elsif [3, 65, 66].include?(func)
37
81
  # no output
38
82
  else
39
- puts "**** new func #{func}"
83
+ puts "#{Time.now} **** new func #{func}"
40
84
  end
41
85
  }
42
86
 
@@ -46,9 +90,9 @@ server.response_callback = lambda { |uid, func, res, req|
46
90
  puts "wrong number of results"
47
91
  next
48
92
  end
49
- puts "===== read registers from #{uid}"
93
+ puts "#{Time.now} ===== read registers from #{uid}" unless diff_only
50
94
  registers = Range.new(req[:addr], req[:addr] + req[:quant], true).to_a.zip(res).to_h
51
- puts Aurora.print_registers(registers)
95
+ diff_and_print.call(registers)
52
96
  elsif func == 65 && res.is_a?(Array) && req
53
97
  register_list = []
54
98
  req.each { |params| register_list.concat(Range.new(params[:addr], params[:addr] + params[:quant], true).to_a) }
@@ -56,21 +100,21 @@ server.response_callback = lambda { |uid, func, res, req|
56
100
  puts "wrong number of results"
57
101
  next
58
102
  end
59
- puts "===== read multiple register ranges from #{uid}"
103
+ puts "#{Time.now} ===== read multiple register ranges from #{uid}" unless diff_only
60
104
  result = register_list.zip(res).to_h
61
- puts Aurora.print_registers(result)
105
+ diff_and_print.call(result)
62
106
  elsif func == 66 && res.is_a?(Array) && req
63
107
  unless req.length == res.length
64
108
  puts "wrong number of results"
65
109
  next
66
110
  end
67
- puts "===== read discontiguous registers from #{uid}"
111
+ puts "#{Time.now} ===== read discontiguous registers from #{uid}" unless diff_only
68
112
  registers = req.zip(res).to_h
69
- puts Aurora.print_registers(registers)
113
+ diff_and_print.call(registers)
70
114
  elsif [16, 67, 68].include?(func)
71
115
  # no output
72
116
  else
73
- puts "**** new func #{func}"
117
+ puts "#{Time.now} **** new func #{func}"
74
118
  end
75
119
  }
76
120
 
@@ -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.2"
4
+ VERSION = "0.3.3"
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.2
4
+ version: 0.3.3
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-25 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