waterfurnace_aurora 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d968e612fad4716e6bebfa9190d31fd40124ae5f5d793d61defe131929aee135
4
+ data.tar.gz: c8309747cbbb06b7649620a0c065015de5ac53237e460bb8ff9138669e6315f3
5
+ SHA512:
6
+ metadata.gz: 01c73b395a6f32e9d30db37a6d9bedcb82a0f2059163dd511e356c0bccd68ed2037ef6a7fcc880a4e030a6f355af835ceab67f9aa77fbb69258800c86c985730
7
+ data.tar.gz: d8dd44d7963e4d79dd81f8329435fe2fbb51e4e4b8e98008a6e014da8d9a97ef4910c776bd34e7d821af244555f62e60d625b8d14101c4501d39edfd30fea4fa
data/bin/aurora_fetch ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'aurora'
4
+ require 'ccutrer-serialport'
5
+ require 'uri'
6
+
7
+ uri = URI.parse(ARGV[0])
8
+
9
+ args = if uri.scheme == "telnet" || uri.scheme == "rfc2217"
10
+ require 'net/telnet/rfc2217'
11
+ [Net::Telnet::RFC2217.new('Host' => uri.host,
12
+ 'Port' => uri.port || 23,
13
+ 'baud' => 19200,
14
+ 'parity' => Net::Telnet::RFC2217::EVEN)]
15
+ else
16
+ [CCutrer::SerialPort.new(uri.path, baud: 19200, parity: :even)]
17
+ end
18
+
19
+ client = ModBus::RTUClient.new(*args)
20
+ client.debug = true
21
+ slave = client.with_slave(1)
22
+
23
+ registers = slave.holding_registers[ARGV[1].to_i]
24
+
25
+ puts registers.inspect
data/bin/aurora_mock ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'aurora'
4
+ require 'ccutrer-serialport'
5
+ require 'uri'
6
+ require 'yaml'
7
+
8
+ uri = URI.parse(ARGV[0])
9
+
10
+ args = if uri.scheme == "telnet" || uri.scheme == "rfc2217"
11
+ require 'net/telnet/rfc2217'
12
+ [Net::Telnet::RFC2217.new('Host' => uri.host,
13
+ 'Port' => uri.port || 23,
14
+ 'baud' => 19200,
15
+ 'parity' => Net::Telnet::RFC2217::EVEN)]
16
+ else
17
+ [CCutrer::SerialPort.new(uri.path, baud: 19200, parity: :even)]
18
+ end
19
+
20
+ port = ARGV[1]&.to_i || 502
21
+
22
+ server1 = ModBus::RTUServer.new(*args)
23
+ server1.debug = true
24
+ # AID Tool queries slave 1, AWL queries slave 2; just use both
25
+ slave1 = server1.with_slave(1)
26
+ slave2 = server1.with_slave(2)
27
+ server2 = ModBus::TCPServer.new(port)
28
+ slave255 = server2.with_slave(255)
29
+
30
+ r = slave1.holding_registers = slave2.holding_registers = slave255.holding_registers = Array.new(31473, 0)
31
+
32
+ # prepopulate some data
33
+ registers = YAML.load(File.read(File.expand_path('../registers.yml', __FILE__)))
34
+ registers.each { |(k, v)| r[k] = v }
35
+
36
+ server1.request_callback = ->(uid, func, req) do
37
+ if func == 68
38
+ puts "===== no idea to #{uid}: #{req.inspect}"
39
+ elsif func == 67
40
+ puts "===== write discontiguous registers to #{uid}:"
41
+ registers = req.map { |p| [p[:addr], p[:val]] }.to_h
42
+ puts Aurora.print_registers(registers)
43
+ elsif func == 16
44
+ puts "===== write multiple registers to #{uid}:"
45
+ registers = Range.new(req[:addr], req[:addr] + req[:quant] - 1).zip(req[:val]).to_h
46
+ puts Aurora.print_registers(registers)
47
+ elsif [3, 65, 66].include?(func)
48
+ else
49
+ puts "**** new func #{func}"
50
+ end
51
+ end
52
+
53
+ server1.response_callback = ->(uid, func, res, req) do
54
+ if func == 3 && res.is_a?(Array) && req
55
+ unless req[:quant] == res.length
56
+ puts "wrong number of results"
57
+ next
58
+ end
59
+ puts "===== read registers from #{uid}"
60
+ registers = Range.new(req[:addr], req[:addr] + req[:quant], true).to_a.zip(res).to_h
61
+ puts Aurora.print_registers(registers)
62
+ elsif func == 65 && res.is_a?(Array) && req
63
+ register_list = []
64
+ req.each { |params| register_list.concat(Range.new(params[:addr], params[:addr] + params[:quant], true).to_a) }
65
+ unless register_list.length == res.length
66
+ puts "wrong number of results"
67
+ next
68
+ end
69
+ puts "===== read multiple register ranges from #{uid}"
70
+ result = register_list.zip(res).to_h
71
+ puts Aurora.print_registers(result)
72
+ elsif func == 66 && res.is_a?(Array) && req
73
+ unless req.length == res.length
74
+ puts "wrong number of results"
75
+ next
76
+ end
77
+ puts "===== read discontiguous registers from #{uid}"
78
+ registers = req.zip(res).to_h
79
+ puts Aurora.print_registers(registers)
80
+ elsif [16, 67, 68].include?(func)
81
+ else
82
+ puts "**** new func #{func}"
83
+ end
84
+ end
85
+
86
+ server2.start
87
+ server1.send(:serve, server1.instance_variable_get(:@sp))
88
+ #server1.start
89
+ loop {}
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'aurora'
4
+ require 'ccutrer-serialport'
5
+ require 'socket'
6
+ require 'uri'
7
+
8
+ uri = URI.parse(ARGV[0])
9
+
10
+ args = if uri.scheme == "telnet" || uri.scheme == "rfc2217"
11
+ require 'net/telnet/rfc2217'
12
+ [Net::Telnet::RFC2217.new('Host' => uri.host,
13
+ 'Port' => uri.port || 23,
14
+ 'baud' => 19200,
15
+ 'parity' => Net::Telnet::RFC2217::EVEN)]
16
+ else
17
+ [CCutrer::SerialPort.new(uri.path, baud: 19200, parity: :even)]
18
+ end
19
+
20
+ server = ModBus::RTUServer.new(*args)
21
+ server.promiscuous = true
22
+ server.debug = true
23
+
24
+ server.request_callback = ->(uid, func, req) do
25
+ if func == 68
26
+ puts "===== no idea to #{uid}: #{req.inspect}"
27
+ elsif func == 67
28
+ puts "===== write discontiguous registers to #{uid}:"
29
+ registers = req.map { |p| [p[:addr], p[:val]] }.to_h
30
+ puts Aurora.print_registers(registers)
31
+ elsif func == 16
32
+ puts "===== write multiple registers to #{uid}:"
33
+ registers = Range.new(req[:addr], req[:addr] + req[:quant] - 1).zip(req[:val]).to_h
34
+ puts Aurora.print_registers(registers)
35
+ elsif [3, 65, 66].include?(func)
36
+ else
37
+ puts "**** new func #{func}"
38
+ end
39
+ end
40
+
41
+ server.response_callback = ->(uid, func, res, req) do
42
+ if func == 3 && res.is_a?(Array) && req
43
+ unless req[:quant] == res.length
44
+ puts "wrong number of results"
45
+ next
46
+ end
47
+ puts "===== read registers from #{uid}"
48
+ registers = Range.new(req[:addr], req[:addr] + req[:quant], true).to_a.zip(res).to_h
49
+ puts Aurora.print_registers(registers)
50
+ elsif func == 65 && res.is_a?(Array) && req
51
+ register_list = []
52
+ req.each { |params| register_list.concat(Range.new(params[:addr], params[:addr] + params[:quant], true).to_a) }
53
+ unless register_list.length == res.length
54
+ puts "wrong number of results"
55
+ next
56
+ end
57
+ puts "===== read multiple register ranges from #{uid}"
58
+ result = register_list.zip(res).to_h
59
+ puts Aurora.print_registers(result)
60
+ elsif func == 66 && res.is_a?(Array) && req
61
+ unless req.length == res.length
62
+ puts "wrong number of results"
63
+ next
64
+ end
65
+ puts "===== read discontiguous registers from #{uid}"
66
+ registers = req.zip(res).to_h
67
+ puts Aurora.print_registers(registers)
68
+ elsif [16, 67, 68].include?(func)
69
+ else
70
+ puts "**** new func #{func}"
71
+ end
72
+ end
73
+
74
+ require 'byebug'
75
+ server.send(:serve, server.instance_variable_get(:@sp))
76
+ loop {}
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'aurora'
4
+ require 'mqtt'
5
+ require 'ccutrer-serialport'
6
+ require 'uri'
7
+
8
+ uri = URI.parse(ARGV[0])
9
+ mqtt_uri = URI.parse(ARGV[1])
10
+
11
+ args = if uri.scheme == "telnet" || uri.scheme == "rfc2217"
12
+ require 'net/telnet/rfc2217'
13
+ [Net::Telnet::RFC2217.new('Host' => uri.host,
14
+ 'Port' => uri.port || 23,
15
+ 'baud' => 19200,
16
+ 'parity' => Net::Telnet::RFC2217::EVEN)]
17
+ else
18
+ [CCutrer::SerialPort.new(uri.path, baud: 19200, parity: :even)]
19
+ end
20
+
21
+ slave = ModBus::RTUClient.new(*args).with_slave(1)
22
+ abc = Aurora::ABCClient.new(slave)
23
+
24
+ class MQTTBridge
25
+ def initialize(abc, mqtt_uri)
26
+ @base_topic = "homie/aurora"
27
+ @abc = abc
28
+ @mqtt = MQTT::Client.new(mqtt_uri)
29
+ @mqtt.set_will("#{@base_topic}/$state", "lost", true, 1)
30
+ @mqtt.connect
31
+ @attributes = {}
32
+ @mutex = Mutex.new
33
+
34
+ publish_basic_attributes
35
+
36
+ abc_thread = Thread.new do
37
+ loop do
38
+ begin
39
+ @mutex.synchronize do
40
+ @abc.refresh
41
+ publish_attribute("abc/compressor-speed", @abc.compressor_speed)
42
+ publish_attribute("abc/current-mode", @abc.current_mode)
43
+ publish_attribute("abc/dhw-water-temperature", @abc.dhw_water_temperature)
44
+ publish_attribute("abc/entering-air-temperature", @abc.entering_air_temperature)
45
+ publish_attribute("abc/entering-water-temperature", @abc.entering_water_temperature)
46
+ publish_attribute("abc/fan-speed", @abc.fan_speed)
47
+ publish_attribute("abc/leaving-air-temperature", @abc.leaving_air_temperature)
48
+ publish_attribute("abc/leaving-water-temperature", @abc.leaving_water_temperature)
49
+ publish_attribute("abc/outdoor-temperature", @abc.outdoor_temperature)
50
+ publish_attribute("abc/relative-humidity", @abc.relative_humidity)
51
+ publish_attribute("abc/waterflow", @abc.waterflow)
52
+ publish_attribute("abc/fp1", @abc.fp1)
53
+ publish_attribute("abc/fp2", @abc.fp2)
54
+
55
+ @abc.iz2_zones.each do |z|
56
+ i = z.zone_number
57
+ publish_attribute("zone#{i}/target-mode", z.target_mode)
58
+ publish_attribute("zone#{i}/current-mode", z.current_mode)
59
+ publish_attribute("zone#{i}/target-fan-mode", z.target_fan_mode)
60
+ publish_attribute("zone#{i}/current-fan-mode", z.current_fan_mode)
61
+ publish_attribute("zone#{i}/fan-intermittent-on", z.fan_intermittent_on)
62
+ publish_attribute("zone#{i}/fan-intermittent-off", z.fan_intermittent_off)
63
+ publish_attribute("zone#{i}/priority", z.priority)
64
+ publish_attribute("zone#{i}/size", z.size)
65
+ publish_attribute("zone#{i}/normalized-size", z.normalized_size)
66
+ publish_attribute("zone#{i}/ambient-temperature", z.ambient_temperature)
67
+ publish_attribute("zone#{i}/heating-target-temperature", z.heating_target_temperature)
68
+ publish_attribute("zone#{i}/cooling-target-temperature", z.cooling_target_temperature)
69
+ end
70
+ end
71
+ rescue => e
72
+ puts "got garbage: #{e}; #{e.backtrace}"
73
+ exit 1
74
+ end
75
+ sleep(5)
76
+ end
77
+ end
78
+
79
+ @mqtt.get do |topic, value|
80
+ puts "got #{value.inspect} at #{topic}"
81
+ zone = topic =~ %r{zone([1-6])/} && @abc.iz2_zones[$1.to_i - 1]
82
+
83
+ begin
84
+ @mutex.synchronize do
85
+ case topic
86
+ when %r{zone([1-6])/target-mode/set$}
87
+ publish_attribute("zone#{$1}/target-mode", zone.target_mode = value.to_sym)
88
+ when %r{zone([1-6])/target-fan-mode/set$}
89
+ publish_attribute("zone#{$1}/target-fan-mode", zone.target_fan_mode = value.to_sym)
90
+ when %r{zone([1-6])/fan-intermittent-(on|off)/set$}
91
+ publish_attribute("zone#{$1}/fan-intermittent-#{$2}", zone.send("fan_intermittent_#{$2}=", value.to_i))
92
+ when %r{zone([1-6])/(heating|cooling)-target-temperature}
93
+ publish_attribute("zone#{$1}/#{$2}-target-temperature", zone.send("#{$2}_target_temperature=", value.to_f))
94
+ when /\$modbus$/
95
+ query = value.split(',').map do |addr|
96
+ if addr == "known"
97
+ Aurora::REGISTER_NAMES.keys
98
+ elsif addr =~ /^(\d+)\.\.(\d+)$/
99
+ $1.to_i..$2.to_i
100
+ else
101
+ addr.to_i
102
+ end
103
+ end
104
+ queries = Aurora.normalize_ranges(query)
105
+ registers = {}
106
+ queries.each do |query|
107
+ registers.merge!(@abc.modbus_slave.read_multiple_holding_registers(*query))
108
+ end
109
+ result = Aurora.print_registers(registers)
110
+ @mqtt.publish("#{@base_topic}/$modbus/response", result, false, 1)
111
+ when %r{\$modbus/(\d+)$}
112
+ register = $1.to_i
113
+ value = case value
114
+ when /\d+/
115
+ value.to_i
116
+ when /0x(\d+)/
117
+ $1.to_i(16)
118
+ end
119
+ @abc.modbus_slave.holding_registers[register] = value if value
120
+ end
121
+ end
122
+ rescue => e
123
+ puts "failed processing message: #{e}\n#{e.backtrace}"
124
+ end
125
+ end
126
+ end
127
+
128
+ def publish(topic, value)
129
+ @mqtt.publish("#{@base_topic}/#{topic}", value, true, 1)
130
+ end
131
+
132
+ def publish_attribute(attr, value)
133
+ if !@attributes.key?(attr) || @attributes[attr] != value
134
+ publish(attr, value.to_s)
135
+ @attributes[attr] = value
136
+ end
137
+ end
138
+
139
+ def subscribe(topic)
140
+ @mqtt.subscribe("#{@base_topic}/#{topic}")
141
+ end
142
+
143
+ def publish_basic_attributes
144
+ publish("$homie", "4.0.0")
145
+ publish("$name", "Aurora MQTT Bridge")
146
+ publish("$state", "init")
147
+ nodes = ["abc"]
148
+
149
+ publish("abc/$name", "Aurora Basic Control")
150
+ publish("abc/$datatype", "ABC")
151
+ publish("abc/$properties", "compressor-speed,current-mode,dhw-water-temperature,entering-air-temperature,entering-water-temperature,fan-speed,leaving-air-temperature,leaving-water-temperature,outdoor-temperature,relative-humidity,waterflow,fp1,fp2")
152
+
153
+ publish("abc/compressor-speed/$name", "Compressor Speed")
154
+ publish("abc/compressor-speed/$datatype", "integer")
155
+ publish("abc/compressor-speed/$format", "0:12")
156
+
157
+ publish("abc/current-mode/$name", "Current Heating/Cooling Mode")
158
+ publish("abc/current-mode/$datatype", "enum")
159
+ publish("abc/current-mode/$format", "lockout,standby,blower,h1,h2,c1,c2,eh1,eh2")
160
+
161
+ publish("abc/dhw-water-temperature/$name", "DHW Water Temperature")
162
+ publish("abc/dhw-water-temperature/$datatype", "float")
163
+ publish("abc/dhw-water-temperature/$unit", "ºF")
164
+
165
+ publish("abc/entering-air-temperature/$name", "Entering Air Temperature")
166
+ publish("abc/entering-air-temperature/$datatype", "float")
167
+ publish("abc/entering-air-temperature/$unit", "ºF")
168
+
169
+ publish("abc/entering-water-temperature/$name", "Entering Water Temperature")
170
+ publish("abc/entering-water-temperature/$datatype", "float")
171
+ publish("abc/entering-water-temperature/$unit", "ºF")
172
+
173
+ publish("abc/fan-speed/$name", "Fan Speed")
174
+ publish("abc/fan-speed/$datatype", "integer")
175
+ publish("abc/fan-speed/$format", "0:11")
176
+
177
+ publish("abc/leaving-air-temperature/$name", "Leaving Air Temperature")
178
+ publish("abc/leaving-air-temperature/$datatype", "float")
179
+ publish("abc/leaving-air-temperature/$unit", "ºF")
180
+
181
+ publish("abc/leaving-water-temperature/$name", "Leaving Water Temperature")
182
+ publish("abc/leaving-water-temperature/$datatype", "float")
183
+ publish("abc/leaving-water-temperature/$unit", "ºF")
184
+
185
+ publish("abc/outdoor-temperature/$name", "Outdoor Temperature")
186
+ publish("abc/outdoor-temperature/$datatype", "float")
187
+ publish("abc/outdoor-temperature/$unit", "ºF")
188
+
189
+ publish("abc/relative-humidity/$name", "Relative Humidity")
190
+ publish("abc/relative-humidity/$datatype", "integer")
191
+ publish("abc/relative-humidity/$unit", "%")
192
+ publish("abc/relative-humidity/$format", "0:100")
193
+
194
+ publish("abc/waterflow/$name", "Waterflow")
195
+ publish("abc/waterflow/$datatype", "float")
196
+ publish("abc/waterflow/$unit", "gpm")
197
+
198
+ publish("abc/fp1/$name", "FP1 Sensor")
199
+ publish("abc/fp1/$datatype", "float")
200
+ publish("abc/fp1/$unit", "ºF")
201
+
202
+ publish("abc/fp2/$name", "FP1 Sensor")
203
+ publish("abc/fp2/$datatype", "float")
204
+ publish("abc/fp2/$unit", "ºF")
205
+
206
+ (1..@abc.iz2_zones.length).each do |i|
207
+ nodes << "zone#{i}"
208
+ publish("zone#{i}/$name", "Zone #{i}")
209
+ publish("zone#{i}/$datatype", "IntelliZone 2 Zone")
210
+ publish("zone#{i}/$properties", "target-mode,current-mode,target-fan-mode,current-fan-mode,fan-intermittent-on,fan-intermittent-off,priority,size,normalized-size,ambient-temperature,heating-target-temperature,cooling-target-temperature")
211
+
212
+ publish("zone#{i}/target-mode/$name", "Target Heating/Cooling Mode")
213
+ publish("zone#{i}/target-mode/$datatype", "enum")
214
+ if i == 1
215
+ publish("zone#{i}/target-mode/$format", "off,auto,cool,heat,eheat")
216
+ else
217
+ publish("zone#{i}/target-mode/$format", "off,auto,cool,heat")
218
+ end
219
+ publish("zone#{i}/target-mode/$settable", "true")
220
+ subscribe("zone#{i}/target-mode/set")
221
+
222
+ publish("zone#{i}/current-mode/$name", "Current Heating/Cooling Mode Requested")
223
+ publish("zone#{i}/current-mode/$datatype", "enum")
224
+ publish("zone#{i}/current-mode/$format", "standby,h1,h2,h3,c1,c2")
225
+
226
+ publish("zone#{i}/target-fan-mode/$name", "Target Fan Mode")
227
+ publish("zone#{i}/target-fan-mode/$datatype", "enum")
228
+ publish("zone#{i}/target-fan-mode/$format", "auto,continuous,intermittent")
229
+ publish("zone#{i}/target-fan-mode/$settable", "true")
230
+ subscribe("zone#{i}/target-fan-mode/set")
231
+
232
+ publish("zone#{i}/current-fan-mode/$name", "Current Fan Status")
233
+ publish("zone#{i}/current-fan-mode/$datatype", "boolean")
234
+
235
+ publish("zone#{i}/fan-intermittent-on/$name", "Fan Intermittent Mode On Duration")
236
+ publish("zone#{i}/fan-intermittent-on/$datatype", "enum")
237
+ publish("zone#{i}/fan-intermittent-on/$format", "0,5,10,15,20")
238
+ publish("zone#{i}/fan-intermittent-on/$unit", "M")
239
+ publish("zone#{i}/fan-intermittent-on/$settable", "true")
240
+ subscribe("zone#{i}/fan-intermittent-on/set")
241
+
242
+ publish("zone#{i}/fan-intermittent-off/$name", "Fan Intermittent Mode Off Duration")
243
+ publish("zone#{i}/fan-intermittent-off/$datatype", "enum")
244
+ publish("zone#{i}/fan-intermittent-off/$format", "0,5,10,15,20,25,30,35,40")
245
+ publish("zone#{i}/fan-intermittent-off/$unit", "M")
246
+ publish("zone#{i}/fan-intermittent-off/$settable", "true")
247
+ subscribe("zone#{i}/fan-intermittent-off/set")
248
+
249
+ publish("zone#{i}/priority/$name", "Zone Priority")
250
+ publish("zone#{i}/priority/$datatype", "enum")
251
+ publish("zone#{i}/priority/$format", "economy,comfort")
252
+
253
+ publish("zone#{i}/size/$name", "Size")
254
+ publish("zone#{i}/size/$datatype", "enum")
255
+ publish("zone#{i}/size/$format", "0,25,45,70")
256
+
257
+ publish("zone#{i}/normalized-size/$name", "Normalized Size")
258
+ publish("zone#{i}/normalized-size/$datatype", "integer")
259
+ publish("zone#{i}/normalized-size/$format", "0:100")
260
+ publish("zone#{i}/normalized-size/$unit", "%")
261
+
262
+ publish("zone#{i}/ambient-temperature/$name", "Ambient Temperature")
263
+ publish("zone#{i}/ambient-temperature/$datatype", "float")
264
+ publish("zone#{i}/ambient-temperature/$unit", "ºF")
265
+
266
+ publish("zone#{i}/heating-target-temperature/$name", "Heating Target Temperature")
267
+ publish("zone#{i}/heating-target-temperature/$datatype", "integer")
268
+ publish("zone#{i}/heating-target-temperature/$unit", "ºF")
269
+ publish("zone#{i}/heating-target-temperature/$format", "40:90")
270
+ publish("zone#{i}/heating-target-temperature/$settable", "true")
271
+ subscribe("zone#{i}/heating-target-temperature/set")
272
+
273
+ publish("zone#{i}/cooling-target-temperature/$name", "Cooling Target Temperature")
274
+ publish("zone#{i}/cooling-target-temperature/$datatype", "integer")
275
+ publish("zone#{i}/cooling-target-temperature/$unit", "ºF")
276
+ publish("zone#{i}/cooling-target-temperature/$format", "54:99")
277
+ publish("zone#{i}/cooling-target-temperature/$settable", "true")
278
+ subscribe("zone#{i}/cooling-target-temperature/set")
279
+ end
280
+ publish("$nodes", nodes.join(','))
281
+
282
+ # direct access to modbus registers for debugging purposes
283
+ subscribe("$modbus")
284
+ subscribe("$modbus/+")
285
+ publish("$state", 'ready')
286
+ end
287
+ end
288
+
289
+ MQTTBridge.new(abc, mqtt_uri)