Hey, i finally moved in my new home and had time messing with this. I am happy to report that i managed to get (almost) everything working, Here’s a rundown of what i did and the needed config.
oh boy was I in for a ride…
First of all: you can’t connect both the original heatpump remote control and the rs485 interface together,m since modbus is a one-master protocol, so you will need to either:
- remove the maxa remote panel and only connect your 485 bridge
- buy a multi-master 485 hub.
I tried getting an hub to retain all the original functionality but was not too lucky the first time.
The hub i bought (the cheapest i found on aliexpress, duh) was a YR-8103 module: After days and days of trying to understand how it works, trying every possible comibation of cabling and software configuration, and trying to deal with the seller (which was a fucking mess) I concluded that it’s not compatible with the maxa modbus, as far as i was able to gather that’s because it doesn’t support configuration of stop bit.
DO NOT buy the YR-8103 rs485 multi master module. it sucks, the software is not available in english, and it has an half assed implementation of the protocol.
The second time (i went with the second most cheaper this time, 5head) i bought a GC-1201s module, and this time i was fortunate enough, the module is able to relay messages from both the original panel and the 485 bridge to the heatpump.
Again, the sellers of this module are not too helpful, but the configuration software (which is detected as a virus by windows, but i think that’s just bad programming) at least has an “english” button which gets you an english interface. The manual is also not available in english, but i’ll attach a machine translated version of it.
The config is pretty single anyways, you just have to set:
- baud: 9600
- data bits: 8
- parity: even
- stop bits: 1
for all the ports on the module (2 masters + 1 slave)
Now, for everything else.
i used an elfin EW11 to read data from the heatpump, configured like this:
i then used the modbus integration in homeassistant to create sensors and controls for almost everything listed in the spec in the manual, what i tested works, but i’m sure it can be made better, i’m happy to try a revised version if anyone tries!
modbus:
- name: maxa_rtu_gateway
type: tcp
host: 192.168.22.6 # Elfin gateway IP
port: 502
timeout: 5
delay: 1
message_wait_milliseconds: 200
##############################################################
# SENSORS – READ-ONLY VALUES #
##############################################################
sensors:
# firmware registers cause errors, not essential anyways
# # Firmware / identification
# - name: "Firmware Version"
# slave: 1
# address: 1
# input_type: holding
# data_type: uint16
# - name: "Firmware Release"
# slave: 1
# address: 2
# input_type: holding
# data_type: uint16
# - name: "Firmware Sub-Release"
# slave: 1
# address: 3
# input_type: holding
# data_type: uint16
# Serial number (ASCII, 18 × 2-byte holding = 36 chars)
- name: "Serial Number"
slave: 1
address: 80
input_type: holding
data_type: string
count: 18
# Flow-meter
- name: "Water Flow Rate"
slave: 1
address: 444
input_type: holding
data_type: uint16
unit_of_measurement: "L/min"
# icon: mdi:water
# Temperatures (°C, scale 0.1)
- name: "Water Inlet Temp"
slave: 1
address: 400
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Water Outlet Temp"
slave: 1
address: 401
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "DHW Temp"
slave: 1
address: 405
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Comp Inhalation Temp"
slave: 1
address: 422
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Outdoor Temp"
slave: 1
address: 428
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Comp Discharge 1 Temp"
slave: 1
address: 433
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Comp Discharge 2 Temp"
slave: 1
address: 434
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Comp Discharge 3 Temp"
slave: 1
address: 435
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Solar Collector Temp"
slave: 1
address: 437
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Solar Accumulation Temp"
slave: 1
address: 438
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Plant Remote Temp"
slave: 1
address: 440
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Radiant Panels Mix Delivery Temp"
slave: 1
address: 443
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "DHW Preparer Recirc Temp"
slave: 1
address: 447
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Evaporation Temp"
slave: 1
address: 253
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Condensation Temp"
slave: 1
address: 254
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Evaporation Temp-C2"
slave: 1
address: 626
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Condensation Temp-C2"
slave: 1
address: 627
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Comp Inhalation Temp-C2"
slave: 1
address: 20422
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Comp1 Discharge Temp-C2"
slave: 1
address: 20433
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Comp2 Discharge Temp-C2"
slave: 1
address: 20434
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
- name: "Comp3 Discharge Temp-C2"
slave: 1
address: 20435
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "°C"
# Pressures (bar, scale 0.01)
- name: "High Pressure"
slave: 1
address: 406
input_type: holding
data_type: int16
scale: 0.01
precision: 2
unit_of_measurement: "bar"
- name: "Low Pressure"
slave: 1
address: 414
input_type: holding
data_type: int16
scale: 0.01
precision: 2
unit_of_measurement: "bar"
- name: "High Pressure-C2"
slave: 1
address: 20406
input_type: holding
data_type: int16
scale: 0.01
precision: 2
unit_of_measurement: "bar"
- name: "Low Pressure-C2"
slave: 1
address: 20414
input_type: holding
data_type: int16
scale: 0.01
precision: 2
unit_of_measurement: "bar"
# Analogue outputs (% , scale 0.1)
- name: "Condensation Fan %"
slave: 1
address: 7000
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "%"
- name: "Circulating Pump %"
slave: 1
address: 7001
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "%"
- name: "Condensation Fan %-C2"
slave: 1
address: 628
input_type: holding
data_type: int16
scale: 0.1
precision: 1
unit_of_measurement: "%"
# Operating hours (h)
- name: "Compressor 1 Hours"
slave: 1
address: 305
input_type: holding
data_type: int16
unit_of_measurement: "h"
- name: "Compressor 2 Hours"
slave: 1
address: 307
input_type: holding
data_type: int16
unit_of_measurement: "h"
- name: "Compressor 3 Hours"
slave: 1
address: 309
input_type: holding
data_type: int16
unit_of_measurement: "h"
- name: "Compressor 1-C2 Hours"
slave: 1
address: 313
input_type: holding
data_type: int16
unit_of_measurement: "h"
- name: "Compressor 2-C2 Hours"
slave: 1
address: 315
input_type: holding
data_type: int16
unit_of_measurement: "h"
- name: "Compressor 3-C2 Hours"
slave: 1
address: 317
input_type: holding
data_type: int16
unit_of_measurement: "h"
# Machine status register (200)
- name: "Machine Status Raw"
slave: 1
address: 200
input_type: holding
data_type: uint16
- name: "Alarm Register 950"
slave: 1
address: 950
input_type: holding
data_type: uint16
- name: "Alarm Register 951"
slave: 1
address: 951
input_type: holding
data_type: uint16
- name: "Alarm Register 952"
slave: 1
address: 952
input_type: holding
data_type: uint16
- name: "Alarm Register 953"
slave: 1
address: 953
input_type: holding
data_type: uint16
- name: "Alarm Register 954"
slave: 1
address: 954
input_type: holding
data_type: uint16
- name: "Alarm Register 955"
slave: 1
address: 955
input_type: holding
data_type: uint16
- name: "Alarm Register 956"
slave: 1
address: 956
input_type: holding
data_type: uint16
##############################################################
# CLIMATES – SETPOINTS (READ/WRITE) #
##############################################################
climates:
# Each climate entity exposes a single setpoint register as a thermostat.
- name: "Cooling Setpoint"
slave: 1
address: 7203
input_type: holding
data_type: int16
scale: 0.1
precision: 1
min_temp: 5.0
max_temp: 23.0
temp_step: 0.1
target_temp_register: 7203
temperature_unit: C
- name: "Heating Setpoint"
slave: 1
address: 7204
input_type: holding
data_type: int16
scale: 0.1
precision: 1
min_temp: 25.0
max_temp: 55.0
temp_step: 0.1
target_temp_register: 7204
temperature_unit: C
- name: "Sanitary Setpoint"
slave: 1
address: 7205
input_type: holding
data_type: int16
scale: 0.1
precision: 1
min_temp: 25.0
max_temp: 55.0
temp_step: 0.1
target_temp_register: 7205
temperature_unit: C
- name: "Second Cooling Setpoint"
slave: 1
address: 7206
input_type: holding
data_type: int16
scale: 0.1
precision: 1
min_temp: 5.0
max_temp: 23.0
temp_step: 0.1
target_temp_register: 7206
temperature_unit: C
- name: "Second Heating Setpoint"
slave: 1
address: 7207
input_type: holding
data_type: int16
scale: 0.1
precision: 1
min_temp: 25.0
max_temp: 55.0
temp_step: 0.1
target_temp_register: 7207
temperature_unit: C
- name: "DHW Preparer Setpoint"
slave: 1
address: 7208
input_type: holding
data_type: int16
scale: 0.1
precision: 1
min_temp: 0.0
max_temp: 80.0
temp_step: 0.1
target_temp_register: 7208
temperature_unit: C
# MACHINE MODE via climate entity (register 7200)
- name: "Heat Pump Mode"
slave: 1
address: 400 # water inlet temp as current temp feedback
input_type: holding
data_type: int16
scale: 0.1
precision: 1
temperature_unit: C
hvac_mode_register:
address: 7200
values:
state_off: 0 # Standby
state_cool: 1 # Cooling
state_heat: 2 # Heating
target_temp_register: 7203 # primary cooling setpoint for UI control
min_temp: 5.0
max_temp: 55.0
temp_step: 0.1
##############################################################
# SWITCHES – REMOTE ENABLE / SPECIAL REQUESTS #
##############################################################
switches:
- name: "Remote State Write Enable"
slave: 1
address: 7201
write_type: holding
command_on: 1 # bit 0 => value 1
command_off: 0
verify:
input_type: holding
delay: 1
- name: "Remote Setpoint Write Enable"
slave: 1
address: 7201
write_type: holding
command_on: 2 # bit 1 => value 2
command_off: 0
verify:
input_type: holding
delay: 1
# 7201 – Enable bits
- name: "Second Setpoint Enable"
slave: 1
address: 7201
write_type: holding
command_on: 4 # bit 2 => value 4
command_off: 0
verify:
input_type: holding
delay: 1
- name: "Room Temp Call Enable"
slave: 1
address: 7201
write_type: holding
command_on: 8 # bit 3 => value 8
command_off: 0
verify:
input_type: holding
delay: 1
- name: "Sanitary Call Enable"
slave: 1
address: 7201
write_type: holding
command_on: 16 # bit 4 => value 16
command_off: 0
verify:
input_type: holding
delay: 1
- name: "Anti-Legionella Enable"
slave: 1
address: 7201
write_type: holding
command_on: 32 # bit 5 => value 32
command_off: 0
verify:
input_type: holding
delay: 1
# 7202 – Remote commands / requests
- name: "Second Setpoint Active"
slave: 1
address: 7202
write_type: holding
command_on: 1 # bit 0
command_off: 0
verify:
input_type: holding
delay: 1
- name: "Room Temp Call"
slave: 1
address: 7202
write_type: holding
command_on: 2 # bit 1
command_off: 0
verify:
input_type: holding
delay: 1
- name: "Sanitary Call"
slave: 1
address: 7202
write_type: holding
command_on: 4 # bit 2
command_off: 0
verify:
input_type: holding
delay: 1
- name: "Anti-Legionella Cycle Request"
slave: 1
address: 7202
write_type: holding
command_on: 8 # bit 3
command_off: 0
verify:
input_type: holding
delay: 1
- name: "Plant Air-Vent"
slave: 1
address: 7202
write_type: holding
command_on: 32 # bit 5
command_off: 0
verify:
input_type: holding
delay: 1
- name: "Sanitary Disabling"
slave: 1
address: 7202
write_type: holding
command_on: 64 # bit 6
command_off: 0
verify:
input_type: holding
delay: 1
- name: "Forced Defrosting"
slave: 1
address: 7202
write_type: holding
command_on: 128 # bit 7
command_off: 0
verify:
input_type: holding
delay: 1
##############################################################
# CLIMATE (OPTIONAL) #
##############################################################
# Home Assistant’s generic_modbus climate platform can wrap
# the above setpoint numbers and a mode switch. This is left
# out to keep the YAML concise – create a climate entity in
# UI using the Helper → Generic Thermostat if needed.
# -------------------------------------------------------------------
# TEMPLATE SENSORS
# -------------------------------------------------------------------
template:
- sensor:
- name: "Current Alarm"
state: >
{% set regs = [
states('sensor.alarm_register_950')|int,
states('sensor.alarm_register_951')|int,
states('sensor.alarm_register_952')|int,
states('sensor.alarm_register_953')|int,
states('sensor.alarm_register_954')|int,
states('sensor.alarm_register_955')|int,
states('sensor.alarm_register_956')|int
] %}
{% set alarm_map = [
(0, 1, "High Pressure"),
(0, 2, "Low Pressure"),
(0, 4, "Compressor Thermal"),
(0, 8, "Fan Thermal"),
(0, 16, "Frost"),
(0, 32, "Lack of Flow"),
(0, 64, "DHW Low Temperature"),
(0, 128, "Lack of Lubrication"),
(0, 256, "High Discharge Temp Cp1"),
(0, 512, "Solar Collector High Temp"),
(0, 4096, "Compressor 2 Thermal"),
(0, 8192, "Fan 2 Thermal"),
(0, 32768, "Pump Thermal"),
(1, 2, "High Temperature"),
(1, 4, "High Discharge Temp Cp2"),
(1, 8, "Inverted Pressure Transducers"),
(1, 64, "Compressor 3 Thermal"),
(1, 128, "Fan 3 Thermal"),
(1, 512, "Pump 2 Thermal"),
(1, 2048, "Incongruent Temperatures"),
(1, 4096, "Poor Heat Exchange DHW"),
(1, 8192, "DHW Accumulation Tank High Temp"),
(1, 16384, "I/O Module 1 Disconnected"),
(1, 32768, "I/O Module 2 Disconnected"),
(2, 1, "Probe 1 Error"),
(2, 2, "Probe 2 Error"),
(2, 4, "Probe 3 Error"),
(2, 8, "Probe 4 Error"),
(2, 16, "Probe 5 Error"),
(2, 32, "Probe 6 Error"),
(2, 64, "Probe 7 Error"),
(2, 128, "Probe 8 Error"),
(2, 256, "Probe 9 Error"),
(2, 512, "Probe 10 Error"),
(2, 1024, "Probe 11 Error"),
(2, 2048, "Module 1 Probe 1 Error"),
(2, 4096, "Module 1 Probe 2 Error"),
(2, 8192, "Module 1 Probe 3 Error"),
(2, 16384, "Module 1 Probe 4 Error"),
(2, 32768, "Module 1 Probe 5 Error"),
(3, 1, "Module 1 Probe 6 Error"),
(3, 2, "Module 1 Probe 7 Error"),
(3, 4, "Module 1 Probe 8 Error"),
(3, 8, "Module 1 Probe 9 Error"),
(3, 16, "Module 1 Probe 10 Error"),
(3, 32, "Module 1 Probe 11 Error"),
(3, 64, "Module 2 Probe 1 Error"),
(3, 128, "Module 2 Probe 2 Error"),
(3, 256, "Module 2 Probe 3 Error"),
(3, 512, "Module 2 Probe 4 Error"),
(3, 1024, "Module 2 Probe 5 Error"),
(3, 2048, "Module 2 Probe 6 Error"),
(3, 4096, "Module 2 Probe 7 Error"),
(3, 8192, "Module 2 Probe 8 Error"),
(3, 16384, "Module 2 Probe 9 Error"),
(3, 32768, "Module 2 Probe 10 Error"),
(4, 1, "Module 2 Probe 11 Error"),
(4, 2, "Link Inverter 1"),
(4, 4, "Link Inverter 2"),
(4, 8, "Link Inverter 3"),
(4, 16, "Hardware Fault Inverter 1"),
(4, 32, "Hardware Fault Inverter 2"),
(4, 64, "Hardware Fault Inverter 3"),
(4, 128, "Overcurrent Inverter 1"),
(4, 256, "Overcurrent Inverter 2"),
(4, 512, "Overcurrent Inverter 3"),
(4, 1024, "High Temp Inverter 1"),
(4, 2048, "High Temp Inverter 2"),
(4, 4096, "High Temp Inverter 3"),
(4, 8192, "Bad Voltage Inverter 1"),
(4, 16384, "Bad Voltage Inverter 2"),
(4, 32768, "Bad Voltage Inverter 3"),
(5, 1, "Phase Sequence Inverter 1"),
(5, 2, "Phase Sequence Inverter 2"),
(5, 4, "Phase Sequence Inverter 3"),
(5, 8, "Model Error Inverter 1"),
(5, 16, "Model Error Inverter 2"),
(5, 32, "Model Error Inverter 3"),
(5, 64, "Overload Inverter 1"),
(5, 128, "Overload Inverter 2"),
(5, 256, "Overload Inverter 3"),
(5, 512, "Overcurrent PFC Inverter 1"),
(5, 1024, "Overcurrent PFC Inverter 2"),
(5, 2048, "Overcurrent PFC Inverter 3"),
(5, 4096, "Internal Comm Error Inverter 1"),
(5, 8192, "Internal Comm Error Inverter 2"),
(5, 16384, "Internal Comm Error Inverter 3"),
(5, 32768, "Fault PFC Inverter 1"),
(6, 1, "Fault PFC Inverter 2"),
(6, 2, "Fault PFC Inverter 3"),
(6, 4, "Probe Error Inverter 1"),
(6, 8, "Probe Error Inverter 2"),
(6, 16, "Probe Error Inverter 3"),
(6, 32, "Abnormal Condition Inverter 1"),
(6, 64, "Abnormal Condition Inverter 2"),
(6, 128, "Abnormal Condition Inverter 3"),
(6, 256, "EEPROM Error Inverter 1"),
(6, 512, "EEPROM Error Inverter 2"),
(6, 1024, "EEPROM Error Inverter 3"),
(6, 2048, "High Discharge Temp Cp3"),
(6, 4096, "Anti-Legionella Success"),
(6, 8192, "Anti-Legionella Failed/Stopped"),
] %}
{% set found = namespace(error="OK") %}
{% for reg_idx, bitmask, name in alarm_map %}
{% if regs[reg_idx] | int | bitwise_and(bitmask) %}
{% set found.error = name %}
{% break %}
{% endif %}
{% endfor %}
{{ found.error }}
- name: "Machine Status"
state: >-
{% set v = states('sensor.machine_status_raw') | int %}
{% set map = {
0: 'Standby',
1: 'Cooling',
2: 'Heating',
4: 'Sanitary Only',
5: 'Cooling + Sanitary',
6: 'Heating + Sanitary'
} %}
{{ map.get(v, 'Unknown') }}
icon: mdi:heat-pump
- select:
- name: "Machine Mode"
state: "{{ states('sensor.machine_status') }}"
options: "{{ ['Standby', 'Cooling', 'Heating', 'Sanitary Only', 'Cooling + Sanitary', 'Heating + Sanitary']}}"
select_option:
service: modbus.write_register
data:
hub: maxa_rtu_gateway
unit: 1
address: 7200
value: >-
{% set map = {
'Standby': 0,
'Cooling': 1,
'Heating': 2,
'Sanitary Only': 4,
'Cooling + Sanitary': 5,
'Heating + Sanitary': 6
} %}
{{ map[option] }}
icon: mdi:heat-pump
this config adds a whole bunch of entities to the modbus integration, and also some template sensor to decode the current operating mode (Machine Status) the set mode (Machine Mode) and the active error code description if present (Current Alarm),
To use thiss you should only change the ip address of the elfin module to the actual ip in your network and paste all of this configuration in your configuration.yaml, or add into a file and then import it with something like:
homeassistant:
packages:
maxa: !include maxa_modbus.yaml
here’s a peek of it working
If anyone else tries this, please do let me know!, I’m also open to any suggestion for improving the config (and will probably do a follow up if i do some improvements aswell).
Here’s also some useful resources to get into this rabbit hole:
MAXA i-32 controller manual (inlcudes modbus specification)
GC-1201s manual english translated
GC-1201s configuration software NOTE: this WILL be detected as a virus. i’m pretty sure it’s a case of chronically bad chinese programming, but i took precaution when using it and you should too, use at your own risk. Because of virus detection i had to encrypt it to a 7z file for it not to get flagged. the password for the file is 1234