waterfurnace_aurora 0.2.0 → 0.3.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.
@@ -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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Aurora
2
- VERSION = '0.2.0'
4
+ VERSION = "0.3.1"
3
5
  end
data/lib/aurora.rb CHANGED
@@ -1,10 +1,13 @@
1
- require 'rmodbus'
1
+ # frozen_string_literal: true
2
2
 
3
- require 'aurora/abc_client'
4
- require 'aurora/iz2_zone'
5
- require 'aurora/modbus/server'
6
- require 'aurora/modbus/slave'
7
- require 'aurora/registers'
3
+ require "rmodbus"
4
+
5
+ require "aurora/abc_client"
6
+ require "aurora/iz2_zone"
7
+ require "aurora/thermostat"
8
+ require "aurora/modbus/server"
9
+ require "aurora/modbus/slave"
10
+ require "aurora/registers"
8
11
 
9
12
  # extend ModBus for WaterFurnace's custom functions
10
13
  ModBus::RTUServer.include(Aurora::ModBus::Server)
@@ -1 +1,3 @@
1
- require 'aurora'
1
+ # frozen_string_literal: true
2
+
3
+ require "aurora"
metadata CHANGED
@@ -1,57 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: waterfurnace_aurora
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2021-08-23 00:00:00.000000000 Z
11
+ date: 2021-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: homie-mqtt
14
+ name: ccutrer-serialport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 1.4.1
19
+ version: 1.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 1.4.1
26
+ version: 1.0.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: net-telnet-rfc2217
28
+ name: homie-mqtt
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.0.4
33
+ version: 1.4.2
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: 0.0.4
40
+ version: 1.4.2
41
41
  - !ruby/object:Gem::Dependency
42
- name: ccutrer-serialport
42
+ name: net-telnet-rfc2217
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 1.0.0
47
+ version: 0.0.4
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 1.0.0
54
+ version: 0.0.4
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rmodbus-ccutrer
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -81,51 +81,56 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '9.0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: rake
84
+ name: gserver
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '13.0'
89
+ version: 0.0.1
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '13.0'
96
+ version: 0.0.1
97
97
  - !ruby/object:Gem::Dependency
98
- name: gserver
98
+ name: rubocop
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 0.0.1
103
+ version: '1.19'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 0.0.1
110
+ version: '1.19'
111
111
  description:
112
112
  email: cody@cutrer.com'
113
113
  executables:
114
+ - aurora_monitor
114
115
  - aurora_mqtt_bridge
116
+ - aurora_mock
117
+ - aurora_fetch
115
118
  extensions: []
116
119
  extra_rdoc_files: []
117
120
  files:
118
- - bin/aurora_fetch
119
- - bin/aurora_mock
120
- - bin/aurora_monitor
121
- - bin/aurora_mqtt_bridge
122
- - bin/registers.yml
121
+ - exe/aurora_fetch
122
+ - exe/aurora_mock
123
+ - exe/aurora_monitor
124
+ - exe/aurora_mqtt_bridge
125
+ - exe/registers.yml
123
126
  - lib/aurora.rb
124
127
  - lib/aurora/abc_client.rb
128
+ - lib/aurora/core_ext/string.rb
125
129
  - lib/aurora/iz2_zone.rb
126
130
  - lib/aurora/modbus/server.rb
127
131
  - lib/aurora/modbus/slave.rb
128
132
  - lib/aurora/registers.rb
133
+ - lib/aurora/thermostat.rb
129
134
  - lib/aurora/version.rb
130
135
  - lib/waterfurnace_aurora.rb
131
136
  homepage: https://github.com/ccutrer/waterfurnace
@@ -140,7 +145,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
140
145
  requirements:
141
146
  - - ">="
142
147
  - !ruby/object:Gem::Version
143
- version: '0'
148
+ version: '2.6'
144
149
  required_rubygems_version: !ruby/object:Gem::Requirement
145
150
  requirements:
146
151
  - - ">="
data/bin/aurora_fetch DELETED
@@ -1,25 +0,0 @@
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_monitor DELETED
@@ -1,76 +0,0 @@
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 {}
@@ -1,179 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'aurora'
4
- require 'homie-mqtt'
5
- require 'ccutrer-serialport'
6
- require 'uri'
7
-
8
- uri = URI.parse(ARGV[0])
9
- mqtt_uri = 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, homie)
26
- @abc = abc
27
- @homie = homie
28
- @mutex = Mutex.new
29
-
30
- @homie.instance_variable_set(:@block, ->(topic, value) do
31
- @mutex.synchronize do
32
- case topic
33
- when /\$modbus$/
34
- query = value.split(',').map do |addr|
35
- if addr == 'known'
36
- Aurora::REGISTER_NAMES.keys
37
- elsif addr =~ /^(\d+)\.\.(\d+)$/
38
- Regexp.last_match(1).to_i..Regexp.last_match(2).to_i
39
- else
40
- addr.to_i
41
- end
42
- end
43
- queries = Aurora.normalize_ranges(query)
44
- registers = {}
45
- queries.each do |query|
46
- registers.merge!(@abc.modbus_slave.read_multiple_holding_registers(*query))
47
- end
48
- result = Aurora.print_registers(registers)
49
- @homie.mqtt.publish("#{@home.topic}/$modbus/response", result, retain: false, qos: 1)
50
- when %r{\$modbus/(\d+)$}
51
- register = Regexp.last_match(1).to_i
52
- value = case value
53
- when /\d+/
54
- value.to_i
55
- when /0x(\d+)/
56
- Regexp.last_match(1).to_i(16)
57
- end
58
- @abc.modbus_slave.holding_registers[register] = value if value
59
- end
60
- end
61
- rescue StandardError => e
62
- puts "failed processing message: #{e}\n#{e.backtrace}"
63
- end)
64
-
65
- publish_basic_attributes
66
-
67
- loop do
68
- begin
69
- @mutex.synchronize do
70
- @abc.refresh
71
- @homie_abc["compressor-speed"].value = @abc.compressor_speed
72
- @homie_abc["current-mode"].value = @abc.current_mode
73
- @homie_abc["dhw-water-temperature"].value = @abc.dhw_water_temperature
74
- @homie_abc["entering-air-temperature"].value = @abc.entering_air_temperature
75
- @homie_abc["entering-water-temperature"].value = @abc.entering_water_temperature
76
- @homie_abc["fan-speed"].value = @abc.fan_speed
77
- @homie_abc["leaving-air-temperature"].value = @abc.leaving_air_temperature
78
- @homie_abc["leaving-water-temperature"].value = @abc.leaving_water_temperature
79
- @homie_abc["outdoor-temperature"].value = @abc.outdoor_temperature
80
- @homie_abc["relative-humidity"].value = @abc.relative_humidity
81
- @homie_abc["waterflow"].value = @abc.waterflow
82
- @homie_abc["fp1"].value = @abc.fp1
83
- @homie_abc["fp2"].value = @abc.fp2
84
-
85
- @abc.iz2_zones.each do |z|
86
- homie_zone = @homie["zone#{z.zone_number}"]
87
- homie_zone["target-mode"].value = z.target_mode
88
- homie_zone["current-mode"].value = z.current_mode
89
- homie_zone["target-fan-mode"].value = z.target_fan_mode
90
- homie_zone["current-fan-mode"].value = z.current_fan_mode
91
- homie_zone["fan-intermittent-on"].value = z.fan_intermittent_on
92
- homie_zone["fan-intermittent-off"].value = z.fan_intermittent_off
93
- homie_zone["priority"].value = z.priority
94
- homie_zone["size"].value = z.size
95
- homie_zone["normalized-size"].value = z.normalized_size
96
- homie_zone["ambient-temperature"].value = z.ambient_temperature
97
- homie_zone["heating-target-temperature"].value = z.heating_target_temperature
98
- homie_zone["cooling-target-temperature"].value = z.cooling_target_temperature
99
- end
100
- end
101
- rescue => e
102
- puts "got garbage: #{e}; #{e.backtrace}"
103
- exit 1
104
- end
105
- sleep(5)
106
- end
107
- end
108
-
109
- def publish(topic, value)
110
- @mqtt.publish("#{@base_topic}/#{topic}", value, true, 1)
111
- end
112
-
113
- def publish_attribute(attr, value)
114
- if !@attributes.key?(attr) || @attributes[attr] != value
115
- publish(attr, value.to_s)
116
- @attributes[attr] = value
117
- end
118
- end
119
-
120
- def subscribe(topic)
121
- @mqtt.subscribe("#{@base_topic}/#{topic}")
122
- end
123
-
124
- def publish_basic_attributes
125
- @homie_abc = @homie.node("abc", "Aurora Basic Control", "ABC") do |node|
126
- node.property("compressor-speed", "Compressor Speed", :integer, @abc.compressor_speed, format: 0..1)
127
- node.property("current-mode", "Current Heating/Cooling Mode", :enum, @abc.current_mode, format: %w[lockout standby blower h1 h2 c1 c2 eh1 eh2])
128
- node.property("dhw-water-temperature", "DHW Water Temperature", :float, @abc.dhw_water_temperature, unit: "ºF")
129
- node.property("entering-air-temperature", "Entering Air Temperature", :float, @abc.entering_air_temperature, unit: "ºF")
130
- node.property("entering-water-temperature", "Entering Water Temperature", :float, @abc.entering_water_temperature, unit: "ºF")
131
- node.property("fan-speed", "Fan Speed", :integer, @abc.fan_speed, format: 0..11)
132
- node.property("leaving-air-temperature", "Leaving Air Temperature", :float, @abc.leaving_air_temperature, unit: "ºF")
133
- node.property("leaving-water-temperature", "Leaving Water Temperature", :float, @abc.leaving_water_temperature, unit: "ºF")
134
- node.property("outdoor-temperature", "Outdoor Temperature", :float, @abc.outdoor_temperature, unit: "ºF")
135
- node.property("relative-humidity", "Relative Humidity", :integer, @abc.relative_humidity, unit: "%", format: 0..100)
136
- node.property("waterflow", "Waterflow", :float, unit: "gpm")
137
- node.property("fp1", "FP1 Sensor", :float, @abc.fp1, unit: "ºF")
138
- node.property("fp2", "FP2 Sensor", :float, @abc.fp2, unit: "ºF")
139
- end
140
-
141
- @abc.iz2_zones.each_with_index do |zone|
142
- @homie.node("zone#{zone.zone_number}", "Zone #{zone.zone_number}", "IntelliZone 2 Zone") do |node|
143
- allowed_modes = %w[off auto cool heat]
144
- allowed_modes << "eheat" if zone.zone_number == 1
145
- node.property("target-mode", "Target Heating/Cooling Mode", :enum, zone.target_mode, format: allowed_modes) do |value, property|
146
- @mutex.synchronize { property.value = zone.target_mode = value.to_sym }
147
- end
148
- node.property("current-mode", "Current Heating/Cooling Mode Requested", :enum, zone.current_mode, format: %w[standby h1 h2 h3 c1 c2])
149
- node.property("target-fan-mode", "Target Fan Mode", :enum, zone.target_fan_mode, format: %w[auto continuous intermittent]) do |value, property|
150
- @mutex.synchronize { property.value = zone.target_fan_mode = value.to_sym }
151
- end
152
- node.property("current-fan-mode", "Current Fan Status", :boolean, zone.current_fan_mode)
153
- node.property("fan-intermittent-on", "Fan Intermittent Mode On Duration", :enum, zone.fan_intermittent_on, unit: "M", format: %w[0 5 10 15 20]) do |value, property|
154
- @mutex.synchronize { property.value = zone.fan_intermittent_on = value.to_i }
155
- end
156
- node.property("fan-intermittent-off", "Fan Intermittent Mode Off Duration", :enum, zone.fan_intermittent_on, unit: "M", format: %w[0 5 10 15 20 25 30 35 40]) do |value, property|
157
- @mutex.synchronize { property.value = zone.fan_intermittent_on = value.to_i }
158
- end
159
- node.property("priority", "Zone Priority", :enum, zone.priority, format: %w[economy comfort])
160
- node.property("size", "Size", :enum, zone.size, format: %w[0 25 45 70])
161
- node.property("normalized-size", "Normalized Size", :integer, zone.normalized_size, unit: "%", format: 0..100)
162
- node.property("ambient-temperature", "Ambient Temperature", :float, zone.ambient_temperature, unit: "ºF")
163
- node.property("heating-target-temperature", "Heating Target Temperature", :integer, zone.heating_target_temperature, unit: "ºF", format: 40..90) do |value, property|
164
- @mutex.synchronize { property.value = zone.heating_target_temperature = value }
165
- end
166
- node.property("cooling-target-temperature", "Cooling Target Temperature", :integer, zone.cooling_target_temperature, unit: "ºF", format: 54..99) do |value, property|
167
- @mutex.synchronize { property.value = zone.cooling_target_temperature = value }
168
- end
169
- end
170
- end
171
-
172
- # direct access to modbus registers for debugging purposes
173
- @homie.mqtt.subscribe("#{@homie.topic}/$modbus")
174
- @homie.mqtt.subscribe("#{@homie.topic}/$modbus/+")
175
- @homie.publish
176
- end
177
- end
178
-
179
- MQTTBridge.new(abc, MQTT::Homie::Device.new("aurora", "Aurora MQTT Bridge", mqtt: mqtt_uri))