waterfurnace_aurora 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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)