waterfurnace_aurora 0.2.2 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/exe/aurora_fetch +1 -1
- data/exe/aurora_mock +1 -1
- data/exe/aurora_monitor +56 -12
- data/exe/aurora_mqtt_bridge +70 -49
- data/lib/aurora/abc_client.rb +35 -13
- data/lib/aurora/core_ext/string.rb +10 -0
- data/lib/aurora/iz2_zone.rb +18 -42
- data/lib/aurora/modbus/slave.rb +1 -2
- data/lib/aurora/registers.rb +34 -26
- data/lib/aurora/thermostat.rb +57 -0
- data/lib/aurora/version.rb +1 -1
- data/lib/aurora.rb +1 -0
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3127c610617c1008b2de42b4b03fc89867d12233548fc0baf6ea0ebb5ea08e8
|
4
|
+
data.tar.gz: '0190fccfece31159a9c5acdd14b648521c32e7b64f7cb596daeb3548b882393a'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
|
data/exe/aurora_mqtt_bridge
CHANGED
@@ -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.
|
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("#{@
|
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
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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.
|
88
|
-
homie_zone = @homie["zone#{
|
89
|
-
homie_zone
|
90
|
-
|
91
|
-
|
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
|
-
|
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.
|
135
|
-
|
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
|
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
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
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)
|
data/lib/aurora/abc_client.rb
CHANGED
@@ -3,7 +3,8 @@
|
|
3
3
|
module Aurora
|
4
4
|
class ABCClient
|
5
5
|
attr_reader :modbus_slave,
|
6
|
-
:
|
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
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
97
|
+
zones.each do |z|
|
76
98
|
z.refresh(registers)
|
77
99
|
end
|
78
100
|
end
|
data/lib/aurora/iz2_zone.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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] =
|
49
|
-
@target_mode =
|
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
|
-
|
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] =
|
57
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
84
|
-
@abc.modbus_slave.holding_registers[21_203 + (zone_number - 1) * 9] =
|
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
|
-
|
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
|
data/lib/aurora/modbus/slave.rb
CHANGED
@@ -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
|
data/lib/aurora/registers.rb
CHANGED
@@ -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
|
data/lib/aurora/version.rb
CHANGED
data/lib/aurora.rb
CHANGED
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.
|
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-
|
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.
|
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.
|
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.
|
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.
|
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
|