Class: Aurora::ABCClient

Inherits:
Object
  • Object
show all
Defined in:
lib/aurora/abc_client.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(uri) ⇒ ABCClient

Returns a new instance of ABCClient.



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/aurora/abc_client.rb', line 169

def initialize(uri)
  @modbus_slave = self.class.open_modbus_slave(uri)
  @modbus_slave.read_retry_timeout = 15
  @modbus_slave.read_retries = 2
  raw_registers = @modbus_slave.holding_registers[2, 33, 88...110, 404, 412..413, 813, 1103, 1114]
  registers = Aurora.transform_registers(raw_registers.dup)
  @abc_version = registers[2]
  @program = registers[88]
  @model = registers[92]
  @serial_number = registers[105]
  @energy_monitor = raw_registers[412]

  @zones = if iz2? && iz2_version >= 2.0
             iz2_zone_count = @modbus_slave.holding_registers[483]
             (0...iz2_zone_count).map { |i| IZ2Zone.new(self, i + 1) }
           else
             [Thermostat.new(self)]
           end

  @abc_dipswitches = registers[33]
  @axb_dipswitches = registers[1103]
  @aux_heat = AuxHeat.new(self)
  @compressor = if %w[ABCVSP ABCVSPR ABCSPLVS].include?(@program)
                  Compressor::VSDrive.new(self)
                else
                  Compressor::GenericCompressor.new(self,
                                                    @abc_dipswitches[:compressor])
                end
  @blower = case raw_registers[404]
            when 1, 2 then Blower::ECM.new(self, registers[404])
            when 3 then Blower::FiveSpeed.new(self, registers[404])
            else; Blower::PSC.new(self, registers[404])
            end
  @pump = if (3..5).cover?(raw_registers[413])
            Pump::VSPump.new(self,
                             registers[413])
          elsif axb?
            Pump::GenericPump.new(self,
                                  registers[413])
          end
  @dhw = DHW.new(self) if axb? && (-999..999).cover?(registers[1114])
  @humidistat = Humidistat.new(self,
                               @abc_dipswitches[:accessory_relay] == :humidifier,
                               axb? && @axb_dipswitches[:accessory_relay2] == :dehumidifier)
  @humidistat = nil unless @humidistat.humidifier? || @humidistat.dehumidifier? || awl_communicating?
  @faults = []

  @entering_air_register = awl_axb? ? 740 : 567
  @registers_to_read = [6, 19..20, 25, 30..31, 112, 344, @entering_air_register]
  @registers_to_read << 1104 if axb?
  @registers_to_read.push(741..742) if awl_communicating?
  @registers_to_read << (1110..1111) if performance_monitoring?
  @registers_to_read.push(16, 1150..1153) if energy_monitoring?
  @registers_to_read << 900 if awl_axb?
  zones.each do |z|
    @registers_to_read.concat(z.registers_to_read)
  end
  @components = [aux_heat, compressor, blower, pump, dhw, humidistat].compact
  @components.each do |component|
    @registers_to_read.concat(component.registers_to_read)
  end
  # need dehumidify mode to calculate final current mode
  @registers_to_read.push(362) if compressor.is_a?(Compressor::VSDrive)
end

Instance Attribute Details

#abc_versionObject (readonly)

Returns the value of attribute abc_version.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def abc_version
  @abc_version
end

#air_coil_temperatureObject (readonly)

Returns the value of attribute air_coil_temperature.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def air_coil_temperature
  @air_coil_temperature
end

#aux_heatObject (readonly)

Returns the value of attribute aux_heat.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def aux_heat
  @aux_heat
end

#blowerObject (readonly)

Returns the value of attribute blower.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def blower
  @blower
end

#compressorObject (readonly)

Returns the value of attribute compressor.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def compressor
  @compressor
end

#current_faultObject (readonly)

Returns the value of attribute current_fault.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def current_fault
  @current_fault
end

#current_modeObject (readonly)

Returns the value of attribute current_mode.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def current_mode
  @current_mode
end

#deratedObject (readonly) Also known as: derated?

Returns the value of attribute derated.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def derated
  @derated
end

#dhwObject (readonly)

Returns the value of attribute dhw.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def dhw
  @dhw
end

#emergency_shutdownObject (readonly) Also known as: emergency_shutdown?

Returns the value of attribute emergency_shutdown.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def emergency_shutdown
  @emergency_shutdown
end

#entering_air_temperatureObject (readonly)

Returns the value of attribute entering_air_temperature.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def entering_air_temperature
  @entering_air_temperature
end

#entering_water_temperatureObject (readonly)

Returns the value of attribute entering_water_temperature.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def entering_water_temperature
  @entering_water_temperature
end

#faultsObject (readonly)

Returns the value of attribute faults.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def faults
  @faults
end

#high_pressure_switchObject (readonly)

Returns the value of attribute high_pressure_switch.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def high_pressure_switch
  @high_pressure_switch
end

#humidistatObject (readonly)

Returns the value of attribute humidistat.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def humidistat
  @humidistat
end

#leaving_air_temperatureObject (readonly)

Returns the value of attribute leaving_air_temperature.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def leaving_air_temperature
  @leaving_air_temperature
end

#leaving_water_temperatureObject (readonly)

Returns the value of attribute leaving_water_temperature.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def leaving_water_temperature
  @leaving_water_temperature
end

#line_voltageObject (readonly)

Returns the value of attribute line_voltage.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def line_voltage
  @line_voltage
end

#line_voltage_settingObject

Returns the value of attribute line_voltage_setting.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def line_voltage_setting
  @line_voltage_setting
end

#load_shedObject (readonly) Also known as: load_shed?

Returns the value of attribute load_shed.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def load_shed
  @load_shed
end

#locked_outObject (readonly) Also known as: locked_out?

Returns the value of attribute locked_out.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def locked_out
  @locked_out
end

#low_pressure_switchObject (readonly)

Returns the value of attribute low_pressure_switch.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def low_pressure_switch
  @low_pressure_switch
end

#modbus_slaveObject (readonly)

Returns the value of attribute modbus_slave.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def modbus_slave
  @modbus_slave
end

#modelObject (readonly)

Returns the value of attribute model.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def model
  @model
end

#outdoor_temperatureObject (readonly)

Returns the value of attribute outdoor_temperature.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def outdoor_temperature
  @outdoor_temperature
end

#pumpObject (readonly)

Returns the value of attribute pump.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def pump
  @pump
end

#safe_modeObject (readonly) Also known as: safe_mode?

Returns the value of attribute safe_mode.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def safe_mode
  @safe_mode
end

#serial_numberObject (readonly)

Returns the value of attribute serial_number.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def serial_number
  @serial_number
end

#wattsObject (readonly)

Returns the value of attribute watts.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def watts
  @watts
end

#zonesObject (readonly)

Returns the value of attribute zones.



132
133
134
# File 'lib/aurora/abc_client.rb', line 132

def zones
  @zones
end

Class Method Details

.open_modbus_slave(uri, ignore_missing_registers: false) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/aurora/abc_client.rb', line 18

def open_modbus_slave(uri, ignore_missing_registers: false)
  begin
    uri = URI.parse(uri)
  rescue URI::InvalidURIError
    raise unless File.exist?(uri)

    uri = URI::Generic.build(path: URI::RFC2396_PARSER.escape(uri))
  end

  io = case uri.scheme
       when "tcp"
         require "socket"
         TCPSocket.new(uri.host, uri.port)
       when "telnet", "rfc2217"
         require "net/telnet/rfc2217"
         Net::Telnet::RFC2217.new(uri.host,
                                  port: uri.port || 23,
                                  baud: 19_200,
                                  parity: :even)
       when "mqtt", "mqtts"
         require "aurora/mqtt_modbus"
         return Aurora::MQTTModBus.new(uri)
       when "esphome"
         begin
           gem "esphome", "~> 1.1"
           require "esphome/serial_proxy"
         rescue LoadError
           warn "Please `gem install esphome ~> 1.1` before using an ESPHome serial proxy"
           exit 1
         end
         ESPHome::SerialProxy.open(uri, baud: 19_200, parity: :even)
       else
         path = URI::RFC2396_PARSER.unescape(uri.path)
         if File.file?(path)
           return Aurora::MockABC.new(YAML.load_file(path),
                                      ignore_missing_registers:)
         end

         require "ccutrer-serialport"
         CCutrer::SerialPort.new(path, baud: 19_200, parity: :even)
       end

  client = ::ModBus::RTUClient.new(io)
  client.with_slave(1)
end

.query_registers(modbus_slave, query, try_individual: false) ⇒ Object



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/aurora/abc_client.rb', line 64

def query_registers(modbus_slave, query, try_individual: false)
  implicit = try_individual
  ranges = query.split(",").map do |addr|
    case addr
    when "known"
      implicit = true
      try_individual = true if try_individual.nil?
      Aurora::REGISTER_NAMES.keys
    when "valid"
      implicit = true
      try_individual = true if try_individual.nil?
      break Aurora::REGISTER_RANGES
    when "all"
      implicit = true
      try_individual = true if try_individual.nil?
      break 0..65_535
    when /^(\d+)(?:\.\.|-)(\d+)$/
      $1.to_i..$2.to_i
    else
      addr.to_i
    end
  end
  queries = Aurora.normalize_ranges(ranges)
  registers = {}
  last_log_time = nil
  queries.each do |subquery|
    last_log_time = log_query(last_log_time, subquery.inspect)
    registers.merge!(modbus_slave.read_multiple_holding_registers(*subquery))
  rescue ::ModBus::Errors::IllegalDataAddress, ::ModBus::Errors::IllegalFunction, ::ModBus::Errors::ModBusTimeout
    # maybe this unit doesn't respond to all the addresses we want?
    raise unless implicit

    # try each query individually
    subquery.each do |subsubquery|
      last_log_time = log_query(last_log_time, subsubquery.inspect)
      registers.merge!(modbus_slave.read_multiple_holding_registers(subsubquery))
    rescue ::ModBus::Errors::IllegalDataAddress,
           ::ModBus::Errors::IllegalFunction,
           ::ModBus::Errors::ModBusTimeout => e
      raise if e.is_a?(::ModBus::Errors::ModBusTimeout) && !try_individual
      next unless try_individual

      # seriously?? try each register individually
      Array(subsubquery).each do |i|
        last_log_time = log_query(last_log_time, i.to_s)
        registers[i] = modbus_slave.holding_registers[i]
      rescue ::ModBus::Errors::IllegalDataAddress, ::ModBus::Errors::IllegalFunction
        # don't catch ModBusTimeout here... it should have no problem responding to a single register request
        next
      end
    end
  end
  registers
end

Instance Method Details

#awl_axb?Boolean

is the AXB AWL compliant?

Returns:

  • (Boolean)


401
402
403
# File 'lib/aurora/abc_client.rb', line 401

def awl_axb?
  axb? && axb_version >= 2.0
end

#awl_communicating?Boolean

see https://www.waterfurnace.com/literature/symphony/ig2001ew.pdf is there a communicating system compliant with AWL?

Returns:

  • (Boolean)


386
387
388
# File 'lib/aurora/abc_client.rb', line 386

def awl_communicating?
  awl_thermostat? || awl_iz2?
end

#awl_iz2?Boolean

is the IZ2 AWL compliant?

Returns:

  • (Boolean)


396
397
398
# File 'lib/aurora/abc_client.rb', line 396

def awl_iz2?
  iz2? && iz2_version >= 2.0
end

#awl_thermostat?Boolean

is the thermostat AWL compliant?

Returns:

  • (Boolean)


391
392
393
# File 'lib/aurora/abc_client.rb', line 391

def awl_thermostat?
  thermostat? && thermostat_version >= 3.0
end

#clear_fault_historyObject



312
313
314
# File 'lib/aurora/abc_client.rb', line 312

def clear_fault_history
  @modbus_slave.holding_registers[47] = 0x5555
end

#cooling_airflow_adjustment=(value) ⇒ Object



297
298
299
300
# File 'lib/aurora/abc_client.rb', line 297

def cooling_airflow_adjustment=(value)
  value = 0x10000 + value if value.negative?
  @modbus_slave.holding_registers[346] = value
end

#energy_monitoring?Boolean

Returns:

  • (Boolean)


357
358
359
# File 'lib/aurora/abc_client.rb', line 357

def energy_monitoring?
  @energy_monitor == 2
end

#inspectObject



405
406
407
408
409
# File 'lib/aurora/abc_client.rb', line 405

def inspect
  "#<Aurora::ABCClient #{(instance_variables - [:@modbus_slave]).map do |iv|
                           "#{iv}=#{instance_variable_get(iv).inspect}"
                         end.join(", ")}>"
end

#loop_pressure_trip=(value) ⇒ Object



302
303
304
# File 'lib/aurora/abc_client.rb', line 302

def loop_pressure_trip=(value)
  @modbus_slave.holding_registers[419] = (value * 10).to_i
end

#manual_operation(mode: :off, compressor_speed: 0, blower_speed: :with_compressor, pump_speed: :with_compressor, aux_heat: false) ⇒ Object

Raises:

  • (ArgumentError)


316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/aurora/abc_client.rb', line 316

def manual_operation(mode: :off,
                     compressor_speed: 0,
                     blower_speed: :with_compressor,
                     pump_speed: :with_compressor,
                     aux_heat: false)
  raise ArgumentError, "mode must be :off, :heating, or :cooling" unless %i[off heating cooling].include?(mode)
  raise ArgumentError, "compressor speed must be between 0 and 12" unless (0..12).cover?(compressor_speed)

  unless blower_speed == :with_compressor || (0..12).cover?(blower_speed)
    raise ArgumentError,
          "blower speed must be :with_compressor or between 0 and 12"
  end
  unless pump_speed == :with_compressor || (0..100).cover?(pump_speed)
    raise ArgumentError,
          "pump speed must be :with_compressor or between 0 and 100"
  end

  value = 0
  value = 0x7fff if mode == :off
  value |= 0x100 if mode == :cooling
  value |= (blower_speed == :with_compressor) ? 0xf0 : (blower_speed << 4)
  value |= 0x200 if aux_heat

  @modbus_slave.holding_registers[3002] = value
  @modbus_slave.holding_registers[323] = (pump_speed == :with_compressor) ? 0x7fff : pump_speed
end

#performance_monitoring?Boolean

I'm not sure if this is correct. 5 Series documentation says that performance monitoring is an optional kit that requires an AXB, but AID Tool Sensor Kit Setup only lets you choose between None, Compressor Monitor, and Energy Monitor. So I'm assuming for now that any AXB has at least performance monitoring.

Returns:

  • (Boolean)


349
350
351
# File 'lib/aurora/abc_client.rb', line 349

def performance_monitoring?
  axb?
end

#query_registers(query) ⇒ Object



234
235
236
# File 'lib/aurora/abc_client.rb', line 234

def query_registers(query)
  self.class.query_registers(@modbus_slave, query)
end

#refreshObject



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/aurora/abc_client.rb', line 238

def refresh
  faults = @modbus_slave.read_multiple_holding_registers(601..699)
  @faults = Aurora.transform_registers(faults).values

  registers = @modbus_slave.holding_registers[*@registers_to_read]
  Aurora.transform_registers(registers)

  outputs = registers[30]

  @entering_air_temperature   = registers[@entering_air_register]
  @leaving_air_temperature    = registers[900] if awl_axb?
  if performance_monitoring?
    @leaving_water_temperature  = registers[1110]
    @entering_water_temperature = registers[1111]
  end
  @outdoor_temperature        = registers[742]
  @air_coil_temperature       = registers[20]
  @locked_out                 = registers[25].anybits?(0x8000)
  @current_fault              = registers[25] & 0x7fff
  @derated                    = (41..46).cover?(@current_fault)
  @safe_mode                  = [47, 48, 49, 72, 74].include?(@current_fault)
  @low_pressure_switch        = registers[31][:lps]
  @high_pressure_switch       = registers[31][:hps]
  @emergency_shutdown         = !!registers[31][:emergency_shutdown]
  @load_shed                  = !!registers[31][:load_shed]
  @line_voltage               = registers[16] if energy_monitoring?
  @line_voltage_setting       = registers[112]
  @watts                      = registers[1153]

  @current_mode = if outputs.include?(:lockout)
                    :lockout
                  elsif registers[362]
                    :dehumidify
                  elsif outputs.include?(:cc2) || outputs.include?(:cc)
                    if outputs.include?(:rv)
                      :cooling
                    elsif outputs.include?(:eh2) || outputs.include?(:eh1)
                      :heating_with_aux
                    else
                      :heating
                    end
                  elsif outputs.include?(:eh2) || outputs.include?(:eh1)
                    :emergency_heat
                  elsif outputs.include?(:blower)
                    :blower
                  elsif !registers[6].zero?
                    :waiting
                  else
                    :standby
                  end

  zones.each do |z|
    z.refresh(registers)
  end
  @components.each do |component|
    component.refresh(registers)
  end
end

#refrigeration_monitoring?Boolean

Returns:

  • (Boolean)


353
354
355
# File 'lib/aurora/abc_client.rb', line 353

def refrigeration_monitoring?
  @energy_monitor >= 1
end