How to connect to IMMERGAS Magis Combo V2 / Magis Pro V2 heat pump via MODBUS into HA

Hi All,

I’m using ESP32 that I want to connect to the modbus terminals of the internal unit.

The same ESP32 uses 1 relay to open and close a EV that is connected to the terminals 40 and 40-1 (heat zone 1)

Step 1 - relay only and t+t- NOT connected
Using the relay I can successfully turn on and off the heat, confirmed also by the heat icon on the right side of the boiler display when the EV is open (SUCCESS)

Step 2 - NO relay and t+t- connected
USP32+MAX485 (Serial 9600:8:N:1, slave id 11) the connection works ok , I can see the communication icon on the top right corner of the boiler display(SUCCESS)

I’ve tried to play with parameter A 22 ( mine is set OFF):

  • OFF [default] - modbus communication works
  • 485 - no communication
  • UC - communication works, but an error appers after some time, E189 but I’m not sure (maybe this mode requires some kind of heartbeat on some registers ? )

Step 3 - relay and t+t- connected
Modbus communication still works ok.
After the EV opens via the realy the boiler doesn’t start heating (no heat icon on the right side of the boiler display) , so, heating is not working with the relay anymore.

I played with parameter A31 (as suggested on a previous comment):

  • RT [default] - terminals 40 and 40-1 starts the heat proccess IF there isn’t the communication
  • RP - terminals 40 and 40-1 starts the heat proccess
  • RPT - terminals 40 and 40-1 starts the heat proccess

I cannot understand the difference between RP and RPT, but both requires a remote panel connected to terminal d+d-, otherwise boiler will show and alarm after some time ( E 121 ).

If A31 is set to RP or RPT another icon appears on the top left corner of the display.

I played also with parameter A30 ( which enables dominus) leaving A31 = RT:

  • OFF [default]
  • ON - same result as A31 set to RP or RTP, but different error after some time ( E 142 )

While playing with parameter A30 and A31 I’ve also sniff the modbus bus on d+d-, and I can see that after you enable remote panel or dominus, boiler starts to poll the assigned slave id (eg 41 for remote panel on zone 1)

Holding registers

I have done some other experiment, checking other forums and hilding register map:

  • 512 seems read-only, always 0 in my case ( in other similar models, must set to 0x55 every 20-30sec)
  • 2010 seems read-write, always 0 in my case, any other value written has not affect in all my attempt ( hot water request, maybe? )

Conclusion

I would conclude that:

  • The boiler “ignores” terminals 40 and 40-1 as soon as a communication is enstablished on T+T- (if A31=RT and A30=OFF)
  • The boiler “checks” 40 and 40-1 even while communicating on T+T-, but you must connect also a remote panel or Dominus on D+D-
  • A31 set to RP or RTP requires a remote panel connected, or boiler will report an error
  • A30 set to ON requires a dominus panel connected, or boiler will report an error
  • DHW seems fine in all the attemps I’ve done

I’d prefer to connect my esp32+max485+relay to the boiler without any other remote device, but, seems that is not possible at this stage.

Hello,

  • RT [default] - mean Room thermostat
  • RP - mean Remote zone control panel
  • RPT - mean Remote zone control panel with thermostat

So the RPT mode means hybrid mode where you must have the zone 1 panel connected to d+ d- and the room thermostat to terminals 40-1 41, and in the zone 1 panel itself you must activate the option Room thermostat - YES.

If you would like to completely eliminate the zone 1 panel, then you operate on the RT Room Thermostat option and you must connect a potential-free control device to terminals 40-1 41.

Then the Immergas is only a heating tool and the brain is the master on-off control device.

Hi, I tried to connect using:

  • M5Stack Atom with RS485
  • BMS Modbus (T-/T+)
  • Boiler model is Magis Combo 9 V2 Hybrid.
  • ESPhome builder Interface.
    But don’t have a communication in the BMS Connector (A13).
    The Settings:
  • Serial 9600:8:N:1, slave id 11
  • The rotary switch SW1 has been set to 1
    Someone could me help to understand the reason? Thanks

Thanks for your reply. However, as far as I can tell, my boiler doesn’t react to terminals 40–1/41 when there is an active communication on T+T– (A31 = RT).

If I disconnect the wires from T+T–, terminals 40–1/41 immediately start triggering the heating process as soon as the communication icon disappears from the boiler display.

EDIT: It seems the boiler requires an ACK from the BMS (on T+T–) before accepting the heating request. Maybe an holding register needs to be set to 1… but this is just speculation.

Currently I’m using a Zone 1 panel on terminals D+D–, an ESP + MAX485 on T+T–, and an ESP32 + relay on 40–1/41, with A31 = RP, and everything works as expected.
My only complaint is the Zone 1 panel, which I don’t really use, so I’d prefer to remove it.

I’m considering removing the Zone 1 panel, connecting the ESP + MAX485 to D+D–, and “emulating” it (which in this case should act as a Modbus slave).

During my tests, communication on terminals T+ and T– was working fine from the beginning with the factory default parameters.

The only way to disable communication on T+ and T– was to set A22 = 485. In most cases the correct value should be OFF, so make sure that parameter isn’t set incorrectly.

Other than that, I suspect you may have a wiring issue or a configuration problem on your M5Stack Atom.

Hi Aberto,
Thanks for your replay.
My actual configuration is:
I connected the WeMos D1 Mini Pro V3.0 (ESP8266) and the TTL to RS485 to the F1 and F2 Connectors, using the Samsung Nasa Protocol.
This mode works fine even if I spend a lot of time to find the right code.
My intention is the extrapolate another sensors via Modbus for more controls.
Now with the oscilloscope don’t have the signal in the both pins (T-/T+) even if the setting in the boiler are:

  • A16 = RP
  • A21 = 11
  • A22 = OFF
  • A30 = OFF
  • A31 = RP

In the 40-1/41 pins I have a normal thermostat (Bticino Smart).
Another clue, I tried to connect the Atom M5Stack in the D+/D- but in the boiler i saw “E 189”. I tried to inverter the Rs485 Pins but I got the same error.
Only with the Waveshare Convert RS485 a ETH (C) RJ45 don’t have any error in the D+/D- pins.
That’s all at the moment.

D+ and D– are the terminals used for Dominus or Zone 1/2 remote panels. These devices act as Modbus slaves, so the boiler polls them continuously and the bus must remain active at all times.
I sniffed the traffic on these terminals and I can see messages addressed to slave ID 30 (and ID 41 when A31 = RP/RPT).

T+ and T– are used for a BMS (Building Management System ??), which acts as the Modbus master and polls the boiler. If no BMS device is connected, it’s normal for the bus to remain idle.

The E189 error refers to a communication timeout involving A22 (the internal module that links the boiler to the external unit A23). This suggests that your device is interfering with the Modbus bus and preventing proper communication.

This is just my interpretation based on personal experience and the manufacturer’s documentation.

Thanks Alberto,
I let you know any progress, because yesterday I had all sensors up and running using the ATOMS3 connected to T-/T+. Then when I restarted the ATOM, dont’ have any signals only “Stop waiting for response from 11”. I dont know exactly the reason.
Regards

Hi Alberto, now the modbus on the T-/T+ signals works fine. The issue has been relative to bias and the boiler don’t give the signals. The modbus was like dead.

The View is only an example.

Hi Andysate, would you please share your ESPHome builder configuration code ?

This is the Code for BMS Modbus (T-/T+) using the M5Stack AtomS3 Lite + 485

# 
=============================================================================
# IMMERGAS MAGIS COMBO V2 - ESPHome Modbus Configuration
# =============================================================================

substitutions:
  name: "immergas-magis-combo"
  friendly_name: "Immergas Magis Combo"
  comment: "Heat pump monitoring via BMS Modbus (T-/T+)"
esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  comment: ${comment}
  on_boot:
    priority: -10
    then:
    # White led during the boot
      - light.turn_on:
          id: led_atom
          red: 100%
          green: 100%
          blue: 100%
          brightness: 40%

esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  framework:
    type: arduino


light:
  - platform: neopixelbus
    id: led_atom
    type: GRB
    variant: WS2812
    pin: GPIO35
    num_leds: 1
    name: "LED Bias Status"

# =============================================================================
# NETWORK & API
# =============================================================================

wifi:
  ssid: "SSID" 
  password: "PWD"
  min_auth_mode: WPA

  ap:
    ssid: "Immergas-Fallback"
    password: "xxxxxxxx"

captive_portal:

api:
  encryption:
    key: "Enc Key" #api_encryption_key

ota:
  - platform: esphome
    password: "XXXX" #ota_password

web_server:
  port: 80
  version: 3
  local: true

logger:
  level: DEBUG

# =============================================================================
# UART / RS-485 CONFIGURATION
# =============================================================================
# M5Stack RS485 Unit (separate module with Grove connector):
# - TX: GPIO26
# - RX: GPIO32
# BMS Bus settings: 9600 baud, 8 data bits, No parity, 1 stop bit
# =============================================================================

uart:
  id: mod_bus
  tx_pin: GPIO6
  rx_pin: GPIO5
  baud_rate: 9600
  parity: NONE
  stop_bits: 1
  data_bits: 8
  setup_priority: -10
  # Debug
  debug:
    direction: BOTH
    dummy_receiver: false
    after:
      delimiter: "\n"
    sequence:
      - lambda: UARTDebug::log_hex(direction, bytes, ' ');

# =============================================================================
# MODBUS CONFIGURATION
# =============================================================================
# BMS T-/T+ bus uses slave address 11 (standard Immergas)
# =============================================================================

modbus:
  id: modbus1
  uart_id: mod_bus

modbus_controller:
  - id: immergas
    address: 11              # Standard BMS slave address Immergas   
    modbus_id: modbus1
    setup_priority: -10
    command_throttle: 300ms  # Delay commands to prevent overload
    update_interval: 60s

# =============================================================================
# SENSORS - SYSTEM STATUS
# =============================================================================

sensor:
  # System Mode (0=out, 1=warm water, 2=cooling, 3=heating)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "System Mode RAW"
    id: system_mode_raw
    register_type: holding
    address: 2000
    register_count: 1
    value_type: U_WORD
    internal: true

  # =============================================================================
  # SENSORS - TEMPERATURE (validated by community)
  # =============================================================================

  # Supply temperature (D20)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Supply temperature"
    id: flow_temp
    register_type: holding
    address: 3000
    value_type: U_WORD
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 1
    filters:
      - multiply: 0.1
    #   
  # This event is valid
    on_value:
      then:
        # Green Led = Modbus OK
        - light.turn_on:
            id: led_atom
            red: 0%
            green: 100%
            blue: 0%
            brightness: 40%

  # Return temperature (D08)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Return temperature"
    id: return_temp
    register_type: holding
    address: 3001
    value_type: U_WORD
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 1
    filters:
      - multiply: 0.1

  # Outside temperature (D06)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Outside temperature"
    id: outdoor_temp
    register_type: holding
    address: 3002
    value_type: U_WORD
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 1
    filters:
      - multiply: 0.1

  # Hot water temperature (D03)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Hot water temperature"
    id: dhw_temp
    register_type: holding
    address: 3016
    value_type: U_WORD
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 1
    filters:
      - multiply: 0.1

  # Indoor unit return temperature / refrigerant (D23)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Indoor unit return temperature"
    id: indoor_return_temp
    register_type: holding
    address: 3029
    value_type: U_WORD
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 1
    filters:
      - multiply: 0.1

  # Refrigerant liquid temperature (D24)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Refrigerant temperature"
    id: refrigerant_temp
    register_type: holding
    address: 3056
    value_type: U_WORD
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 1
    filters:
      - multiply: 0.1

  # =============================================================================
  # SENSORS - SETPOINTS
  # =============================================================================
  
  # Hot water setpoint (RW - reading and writing possible)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Hot water Setpoint"
    id: dhw_setpoint
    register_type: holding
    address: 2095
    value_type: U_WORD
    unit_of_measurement: "°C"
    device_class: temperature
    accuracy_decimals: 1
    filters:
      - multiply: 0.1

  # Zone 1 max CV temperature (R04)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Zone 1 Max CV Temperature"
    id: zone1_max_ch
    register_type: holding
    address: 4351
    value_type: U_WORD
    unit_of_measurement: "°C"
    device_class: temperature
    accuracy_decimals: 0

  # Zone 1 min CV temperature (R05)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Zone 1 Min CV Temperature"
    id: zone1_min_ch
    register_type: holding
    address: 4350
    value_type: U_WORD
    unit_of_measurement: "°C"
    device_class: temperature
    accuracy_decimals: 0

  # Zone 1 max cooling temperature (R13)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Zone 1 Max Cooling Temperature"
    id: zone1_max_cool
    register_type: holding
    address: 4356
    value_type: U_WORD
    unit_of_measurement: "°C"
    device_class: temperature
    accuracy_decimals: 0

  # Zone 1 min cooling temperature (R12)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Zone 1 Min Cooling Temperature"
    id: zone1_min_cool
    register_type: holding
    address: 4357
    value_type: U_WORD
    unit_of_measurement: "°C"
    device_class: temperature
    accuracy_decimals: 0

  # =============================================================================
  # SENSORS - PUMPS & FLOW
  # =============================================================================

  # Pump speed (D14) in l/h
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Pump speed"
    id: pump_flow_rate
    register_type: holding
    address: 3054
    value_type: U_WORD
    unit_of_measurement: "l/h"
    icon: mdi:pump
    state_class: measurement
    accuracy_decimals: 0

  # Pump min speed % (A03)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Pump min Speed"
    id: pump_min_speed
    register_type: holding
    address: 6010
    value_type: U_WORD
    unit_of_measurement: "%"
    icon: mdi:pump
    accuracy_decimals: 0

  # Pump max speed % (A04)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Pump MAX speed"
    id: pump_max_speed
    register_type: holding
    address: 6011
    value_type: U_WORD
    unit_of_measurement: "%"
    icon: mdi:pump
    accuracy_decimals: 0

  # =============================================================================
  # SENSORS - STATUS CODES
  # =============================================================================

  # Heat pump status
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Heat pump Status Code"
    id: heatpump_status
    register_type: holding
    address: 6500
    value_type: U_WORD
    icon: mdi:heat-pump

  # Boiler status
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Boiler Status Code"
    id: boiler_status
    register_type: holding
    address: 6501
    value_type: U_WORD
    icon: mdi:water-boiler

  # System status
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "System Status Code"
    id: system_status
    register_type: holding
    address: 6502
    value_type: U_WORD
    icon: mdi:information

  # EEV position (expansion valve)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "EEV Position"
    id: eev_position
    register_type: holding
    address: 4557
    value_type: U_WORD
    icon: mdi:valve
    accuracy_decimals: 0

  # Time between ignitions (T05) in Minutes
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Time Between Ignitions"
    id: ignition_interval
    register_type: holding
    address: 6000
    value_type: U_WORD
    unit_of_measurement: "min"
    icon: mdi:timer
    accuracy_decimals: 0

  # Outdoor unit model (A11)
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Outdoor Unit Model Code"
    id: outdoor_unit_model
    register_type: holding
    address: 4202
    value_type: U_WORD
    icon: mdi:information
    entity_category: diagnostic

  # =============================================================================
  # CALCULATED SENSORS
  # =============================================================================

  # Delta T (supply - return)
  - platform: template
    name: "Delta T"
    id: delta_t
    icon: mdi:thermometer-lines
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 1
    update_interval: 30s
    lambda: |-
      if (isnan(id(flow_temp).state) || isnan(id(return_temp).state)) {
        return NAN;
      }
      return id(flow_temp).state - id(return_temp).state;

  # Estimated thermal power (kW)
  - platform: template
    name: "Thermal Power"
    id: thermal_power
    icon: mdi:fire
    unit_of_measurement: "kW"
    device_class: power
    state_class: measurement
    accuracy_decimals: 2
    update_interval: 30s
    lambda: |-
      if (isnan(id(pump_flow_rate).state) || isnan(id(delta_t).state)) {
        return NAN;
      }
      // P = flow (l/h) * dT (°C) * 1.163 (Wh/l/K) / 1000
      return (id(pump_flow_rate).state * id(delta_t).state * 1.163) / 1000.0;

  # =============================================================================
  # ESP32 DIAGNOSTIC
  # =============================================================================

  - platform: wifi_signal
    name: "WiFi Signal"
    update_interval: 60s
    entity_category: diagnostic

  - platform: uptime
    name: "Uptime"
    entity_category: diagnostic

  - platform: internal_temperature
    name: "ESP32-S3 Temperature"
    entity_category: diagnostic

# =============================================================================
# TEXT SENSORS - READABLE STATUS
# =============================================================================

text_sensor:
  # System mode readable
  - platform: template
    name: "System Mode"
    id: system_mode
    icon: mdi:thermostat
    update_interval: 30s
    lambda: |-
      int mode = (int)id(system_mode_raw).state;
      switch(mode) {
        case 0: return std::string("Out");
        case 1: return std::string("Hot water only");
        case 2: return std::string("Cooling");
        case 3: return std::string("Heating");
        default: return std::string("Unknown (" + std::to_string(mode) + ")");
      }

  # System status readability (based on community observations)
  - platform: template
    name: "System Status"
    id: system_status_text
    icon: mdi:information
    update_interval: 30s
    lambda: |-
      int status = (int)id(system_status).state;
      switch(status) {
        case 0: return std::string("Stand-by");
        case 6: return std::string("Heating");
        case 8: return std::string("Heating (cycle)");
        case 41: return std::string("Out");
        case 82: return std::string("Stand-by");
        default: return std::string("Code: " + std::to_string(status));
      }

  # Heat pump status readable
  - platform: template
    name: "Heat Pump Status"
    id: heatpump_status_text
    icon: mdi:heat-pump
    update_interval: 30s
    lambda: |-
      int status = (int)id(heatpump_status).state;
      switch(status) {
        case 300: return std::string("Stand-by");
        case 390: return std::string("Heating");
        default: return std::string("Code: " + std::to_string(status));
      }

  - platform: wifi_info
    ip_address:
      name: "IP Address"
      entity_category: diagnostic
    ssid:
      name: "WiFi SSID"
      entity_category: diagnostic

  - platform: version
    name: "ESPHome Version"
    entity_category: diagnostic

# =============================================================================
# SELECT
# =============================================================================

select:
  - platform: modbus_controller
    modbus_controller_id: immergas
    name: "Operation Mode"
    id: op_mode
    address: 2000
    value_type: U_WORD
    optionsmap:
      "Off": 0
      "DHW Only": 1
      "Cooling": 2
      "Heating": 3

# =============================================================================
# BINARY SENSORS
# =============================================================================

binary_sensor:
  - platform: status
    name: "Status"
    entity_category: diagnostic

  # Heating active (derived from system mode)
  - platform: template
    name: "Heating active"
    id: heating_active
    icon: mdi:radiator
    device_class: heat
    lambda: |-
      return (int)id(system_mode_raw).state == 3;

  # Cooling active
  - platform: template
    name: "Cooling Active"
    id: cooling_active
    icon: mdi:snowflake
    device_class: cold
    lambda: |-
      return (int)id(system_mode_raw).state == 2;

  # Warm Water actief
  - platform: template
    name: "Warm Water Actief"
    id: dhw_active
    icon: mdi:water-boiler
    lambda: |-
      return (int)id(system_mode_raw).state == 1;
  
  # MODBUS OK / FAIL
  - platform: template
    id: modbus_ok
    name: "Modbus OK"
    device_class: connectivity
    lambda: |-
      if (!isnan(id(flow_temp).state)) {
        return true;     // valid value
      }
      return false;       // value not receive

    on_state:
      then:
        - if:
            condition:
              binary_sensor.is_off: modbus_ok
            then:
              # RED LED = Modbus NOT Response
              - light.turn_on:
                  id: led_atom
                  red: 100%
                  green: 0%
                  blue: 0%
                  brightness: 40%

# =============================================================================
# BUTTONS
# =============================================================================

button:
  - platform: restart
    name: "Restart ESP"
    entity_category: config

# =============================================================================
# TIJD
# =============================================================================

time:
  - platform: homeassistant
    id: homeassistant_time
    timezone: "Europe/Rome"

This is the Code for Nasa Protocol (F1/F2) using the WeMos D1 Mini Pro V3.0 + 485


external_components:
  - source: components
    components: [samsung_nasa]

esphome:
  name: samsung-nasa
  friendly_name: Samsung Nasa
  min_version: 2026.1.0
  comment: "Heat pump monitoring via Nasa Protocol (F1/F2)"
esp8266:
  board: d1_mini

# Enable logging
logger:
  level: DEBUG
  baud_rate: 115200
 # baud_rate: 0   # disattiva la log UART


# Enable Home Assistant API
api:

# Allow Over-The-Air updates
ota:
  - platform: web_server
  - platform: esphome

wifi:
  ssid: "SSID" 
  password: "PWD"
  min_auth_mode: WPA
  # Set up a wifi access point
  ap: {}

# captive_portal:

# This creates a HA style local web page 
web_server:
  port: 80
  version: 3
  log: False
  local: True

# Specify pins used by the board to communicate over RS485

uart:
  id: uart_bus
  tx_pin: D7        # D1 -> D7 GPIO13 (MOSI)
  rx_pin: D6        # RO -> D6 GPIO12 (MISO)
  baud_rate: 9600
  parity: EVEN
  stop_bits: 1
  rx_buffer_size: 256

## SAMSUNG NASA CONFIGURATION
samsung_nasa:
  debug_log_messages: false
  debug_log_undefined_messages: false
  nasa_client:
    flow_control_pin: D1   # RE+DE → D1 (GPIO5)(SCL)
  devices:
    # Indoor unit
   - address: 20.00.00
     id: nasa_device_1
    # Outdoor unit
   - address: 10.00.00
     id: nasa_device_2

select:

sensor:
  - platform: samsung_nasa
    message: 0x8204
    nasa_device_id: nasa_device_2
    name: "Outdoor Temperature"
    id: out_temp
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
  - platform: samsung_nasa
    message: 0x823D
    nasa_device_id: nasa_device_2
    name: "Outdoor Fan Speed"
  - platform: samsung_nasa
    message: 0x4205
    nasa_device_id: nasa_device_1
    name: "EVA return temperature"
    id: inlet_pressure
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 1
    filters:
      - multiply: 1
  - platform: samsung_nasa
    message: 0x8238
    nasa_device_id: nasa_device_2
    name: "Compressor Current Frequency"
    id: compressor_freq
    unit_of_measurement: "Hz"
  - platform: samsung_nasa
    message: 0x4205
    nasa_device_id: nasa_device_1
    name: "Indoor Eva In Temperature"
    id: outlet_pressure_kpa
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 1
    filters:
      - multiply: 1
  - platform: samsung_nasa
    message: 0x24FC
    nasa_device_id: nasa_device_2
    name: "Heat pump voltage"
    id: power_consumed_v
    unit_of_measurement: "V"
    device_class: voltage
    state_class: measurement
  - platform: samsung_nasa
    message: 0x8217
    nasa_device_id: nasa_device_2
    name: "Outdoor current (Amps)"
    id: power_consumed_a
    unit_of_measurement: "A"
    device_class: power
    state_class: measurement
  - platform: samsung_nasa
    message: 0x8413
    nasa_device_id: nasa_device_2
    name: "Power Consumed"
    id: power_consumed_w
    unit_of_measurement: "W"
    device_class: power
    state_class: measurement
  - platform: samsung_nasa
    message: 0x82E3
    nasa_device_id: nasa_device_2
    name: "Outdoor unit product option capacity"
    id: out_unit_prod_cap
    unit_of_measurement: "KW"
    filters:
      - multiply: 0.1
    device_class: power
    state_class: measurement
    
 # RSSI in dBm (nativo)
  - platform: wifi_signal
    name: "WiFi RSSI"
    id: wifi_rssi_dbm
    update_interval: 30s

 # Error Status
  - platform: samsung_nasa
    message: 0x8235
    nasa_device_id: nasa_device_2
    name: "Error Code"

binary_sensor:
  - platform: samsung_nasa
    message: 0x8010
    nasa_device_id: nasa_device_2
    name: "Compressor Status"
    id: compressor_status

  - platform: samsung_nasa
    message: 0x4089
    nasa_device_id: nasa_device_1
    name: "Primary Water Pump Status"
    id: PPump_status
  
  - platform: samsung_nasa
    message: 0x4124
    nasa_device_id: nasa_device_1
    name: "Smart Grid"
    id: smart_grid

  - platform: samsung_nasa
    message: 0x4123
    nasa_device_id: nasa_device_1
    name: "PV Control"
    id: pv_control
  
  - platform: samsung_nasa
    message: 0x4000
    nasa_device_id: nasa_device_1
    name: "Indoor unit power on-off"
    id: indoor_power

I not tested 100% but ATM works.

Biasing

In the first project using M5Stack, you must to activated the Modbus because was dead. For activated it needs to connect 2 resistance as below:

A (D+) ──[ 1kΩ ]── +5V
B (D–) ──[ 1kΩ ]── GND

Using a multimeter you should find:

From G (GND) to A (D+) = ~3 V
From G (GND) to B (D–) = 0 V

This means the bus is polarized correctly.

Only double check maybe the bus is already polarized correctly, and some devices have the 120 Ohm from A to B internally.

Best Regards

Thanks a lot. I have Dominus and Zone 1 remote panel, hopefully there will not be any interference.

No problem, The gol in this case is to find all modbus sensors and use the T1/T2 RS485 Modbus connection, but in this case, Immegas we not give the official table. When I starterd with this project I ask the table directly to Immergas Company. They sent me this:

I not sure the document is useful for us.

I stumbled upon this discussion and have a question.

Why connect the entire Immergas Magis Pro V2 Combo to Home Assistant? Wouldn’t it be better to use smart thermostats like the BTicino Smarther 2 and control it from there instead?

Is that possible?

Thank you

The BTicino Smarther 2 is only a relays to open and close the boiler. With Modbus we have a lot of signals for better understand the comportment and if there are some errors without use the little display on board. Another alternative is the Dominus but I never use it, and not have the integration. In the end if you use HA you can decide, I.E. close the boiler or set a range of temperatures, etc… remotely.

After playing with Atom+rs485, I returned back to hacking Dominus interface. Here is a simple python code for reading/writing single PDU. It connects to Dominus local TCP port 2000. Code is made with GPT based on decrypted Dominus app android code and network sniffer.

#!/usr/bin/env python3
"""
Immergas CLI tool
-----------------

This script can:
1. Generate authentication string from MAC + password
2. Connect to heater on TCP port 2000
3. Drain the echoed auth string sent back by the heater
4. Read a single PDU
5. Write a single PDU value

It is intentionally small and easy to study.

Examples
--------

Read zone 1 current temperature:
python3 read_immergas_pdu.py \
  --host YOUR_DOMINUS_IP_ADDRESS \
  --mac YOUR_DOMINUS_MAC_ADDRESS \
  --password YOUR_DOMINUS_PASSWORD \
  read --pdu 2011 --temp

Read boiler status:
python3 read_immergas_pdu.py \
  --host YOUR_DOMINUS_IP_ADDRESS \
  --mac YOUR_DOMINUS_MAC_ADDRESS \
  --password YOUR_DOMINUS_PASSWORD \
  read --pdu 2000

Write zone 1 target to 22.6 °C:
python3 read_immergas_pdu.py \
  --host YOUR_DOMINUS_IP_ADDRESS \
  --mac YOUR_DOMINUS_MAC_ADDRESS \
  --password YOUR_DOMINUS_PASSWORD \
  write --pdu 2015 --temp-value 22.6

Write boiler status to HEATING (3):
python3 read_immergas_pdu.py \
  --host YOUR_DOMINUS_IP_ADDRESS \
  --mac YOUR_DOMINUS_MAC_ADDRESS \
  --password YOUR_DOMINUS_PASSWORD \
  write --pdu 2000 --value 3
"""

import argparse
import hashlib
import socket
import sys
import time
from dataclasses import dataclass

# Character maps used by Immergas encoding
MAP1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 "
MAP2 = "0SFGLcTdjaxZPyeK9QhptB5v7zJH3Mq1VibDfCAN6EgYRwXlo8Wk4mun2rOsUI="


@dataclass
class Reply:
    """Decoded 7-byte reply frame"""
    reply_type: int
    pdu: int
    value: int
    raw_frame: bytes


def normalize_mac(mac: str) -> str:
    """Remove separators and lowercase MAC address"""
    return mac.replace(":", "").replace("-", "").lower()


def make_auth(mac: str, password: str) -> str:
    """
    Build Immergas auth string (#D...)

    Steps:
    1. MAC without separators
    2. MD5(password) -> first 12 hex chars
    3. Build plain text: "MAC md5"
    4. Encode using custom alphabet with shift=2
    5. Prefix with #D
    """
    mac12 = normalize_mac(mac)
    md5_12 = hashlib.md5(password.encode("utf-8")).hexdigest()[:12]
    plain = f"{mac12} {md5_12}"

    encoded = []
    n = len(MAP1)
    for ch in plain:
        idx = MAP1.find(ch)
        if idx == -1:
            encoded.append(ch)
        else:
            encoded.append(MAP2[(idx + 2) % n])

    return "#D" + "".join(encoded)


def calc_crc(body: bytes) -> int:
    """
    Custom CRC used by Immergas protocol

    Notes:
    - polynomial: 4129
    - initial value: 0xFFFF
    - processed bit-by-bit, LSB first
    """
    poly = 4129
    crc = 0xFFFF

    for c in body:
        for _ in range(8):
            if (crc & 1) == (c & 1):
                crc >>= 1
            else:
                crc = (crc >> 1) ^ poly
            c >>= 1

    return crc & 0xFFFF


def build_read_frame(pdu: int, req_type: int) -> bytes:
    """Build 7-byte read request, using req_type 0x00 or 0x80"""
    body = bytes([
        req_type,
        (pdu >> 8) & 0xFF,
        pdu & 0xFF,
        0x00,
        0x00,
    ])
    crc = calc_crc(body)
    return body + bytes([(crc >> 8) & 0xFF, crc & 0xFF])


def build_write_frame(pdu: int, value: int, req_type: int = 0x90) -> bytes:
    """
    Build 7-byte write request.

    We use 0x90 because it is confirmed by captures for writes.
    """
    body = bytes([
        req_type,
        (pdu >> 8) & 0xFF,
        pdu & 0xFF,
        (value >> 8) & 0xFF,
        value & 0xFF,
    ])
    crc = calc_crc(body)
    return body + bytes([(crc >> 8) & 0xFF, crc & 0xFF])


def recv_exact(sock: socket.socket, length: int) -> bytes:
    """Receive exactly N bytes or raise an error"""
    data = bytearray()
    while len(data) < length:
        chunk = sock.recv(length - len(data))
        if not chunk:
            raise ConnectionError("Socket closed")
        data.extend(chunk)
    return bytes(data)


def decode_reply(reply: bytes, expected_pdu: int) -> Reply:
    """Validate CRC and expected PDU, then decode the 7-byte reply"""
    if len(reply) != 7:
        raise ValueError(f"Reply must be 7 bytes, got {len(reply)}")

    crc_rx = (reply[5] << 8) | reply[6]
    crc_calc = calc_crc(reply[:5])
    if crc_rx != crc_calc:
        raise ValueError(f"CRC mismatch: rx=0x{crc_rx:04x}, calc=0x{crc_calc:04x}")

    pdu = (reply[1] << 8) | reply[2]
    if pdu != expected_pdu:
        raise ValueError(f"Wrong PDU in reply: got {pdu}, expected {expected_pdu}")

    value = (reply[3] << 8) | reply[4]
    return Reply(reply_type=reply[0], pdu=pdu, value=value, raw_frame=reply)


def drain_post_auth(sock: socket.socket, timeout: float, debug: bool) -> None:
    """
    After auth, this heater echoes the auth string back as plain ASCII.
    Drain that text before sending the binary request frame.
    """
    time.sleep(0.2)
    original_timeout = sock.gettimeout()
    sock.settimeout(0.2)

    drained = bytearray()
    try:
        while True:
            chunk = sock.recv(256)
            if not chunk:
                break
            drained.extend(chunk)
            if len(chunk) < 256:
                break
    except socket.timeout:
        pass
    finally:
        sock.settimeout(original_timeout if original_timeout is not None else timeout)

    if debug and drained:
        print("RX post-auth:", drained.hex(" "))
        try:
            print("RX post-auth ascii:", drained.decode("ascii", errors="replace"))
        except Exception:
            pass


def connect_and_auth(host: str, port: int, mac: str, password: str, timeout: float, debug: bool) -> socket.socket:
    """Open socket, send auth string, and drain any immediate echoed auth data"""
    auth = make_auth(mac, password)
    sock = socket.create_connection((host, port), timeout=timeout)
    sock.settimeout(timeout)
    sock.sendall(auth.encode("ascii"))
    drain_post_auth(sock, timeout=timeout, debug=debug)
    return sock


def read_pdu(host: str, port: int, mac: str, password: str, pdu: int, timeout: float, debug: bool) -> Reply:
    """Connect, authenticate, read one PDU, close connection"""
    last_error = None

    # Use a fresh connection for each request variant.
    for req_type in (0x00, 0x80):
        try:
            with connect_and_auth(host, port, mac, password, timeout, debug) as sock:
                frame = build_read_frame(pdu, req_type)

                if debug:
                    print("TX auth:", make_auth(mac, password))
                    print("TX frame:", frame.hex(" "))

                sock.sendall(frame)
                reply = recv_exact(sock, 7)

                if debug:
                    print("RX frame:", reply.hex(" "))

                return decode_reply(reply, expected_pdu=pdu)
        except Exception as exc:
            last_error = exc

    raise RuntimeError(f"Failed to read PDU {pdu}: {last_error}")


def write_pdu(host: str, port: int, mac: str, password: str, pdu: int, value: int, timeout: float, debug: bool) -> Reply:
    """Connect, authenticate, write one PDU value, close connection"""
    with connect_and_auth(host, port, mac, password, timeout, debug) as sock:
        frame = build_write_frame(pdu, value, req_type=0x90)

        if debug:
            print("TX auth:", make_auth(mac, password))
            print("TX frame:", frame.hex(" "))

        sock.sendall(frame)
        reply = recv_exact(sock, 7)

        if debug:
            print("RX frame:", reply.hex(" "))

        return decode_reply(reply, expected_pdu=pdu)


def print_reply(reply: Reply, show_temp: bool = False) -> None:
    """Human-readable output"""
    print("Reply type:", f"0x{reply.reply_type:02x}")
    print("PDU:", reply.pdu)
    print("Raw value:", reply.value)
    print("Raw value hex:", f"0x{reply.value:04x}")
    print("Frame:", reply.raw_frame.hex(" "))

    if show_temp:
        print("Temperature:", f"{reply.value / 10.0:.1f} °C")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Read or write a single Immergas PDU")

    # Common connection/auth settings
    parser.add_argument("--host", required=True, help="Heater IP address")
    parser.add_argument("--port", type=int, default=2000, help="Heater TCP port")
    parser.add_argument("--mac", required=True, help="Heater module MAC address")
    parser.add_argument("--password", required=True, help="Heater password")
    parser.add_argument("--timeout", type=float, default=2.0, help="Socket timeout in seconds")
    parser.add_argument("--debug", action="store_true", help="Print auth echo and raw TX/RX frames")

    sub = parser.add_subparsers(dest="command", required=True)

    # Read command
    read_cmd = sub.add_parser("read", help="Read a single PDU")
    read_cmd.add_argument("--pdu", type=int, required=True, help="PDU number, for example 2011")
    read_cmd.add_argument("--temp", action="store_true", help="Interpret value as tenths of degrees C")

    # Write command
    write_cmd = sub.add_parser("write", help="Write a single PDU")
    write_cmd.add_argument("--pdu", type=int, required=True, help="PDU number, for example 2015")

    value_group = write_cmd.add_mutually_exclusive_group(required=True)
    value_group.add_argument("--value", type=int, help="Raw 16-bit value to write")
    value_group.add_argument("--temp-value", type=float, help="Temperature to encode as value*10")

    write_cmd.add_argument("--temp", action="store_true", help="Also print echoed reply as temperature")

    return parser


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()

    try:
        if args.command == "read":
            reply = read_pdu(
                host=args.host,
                port=args.port,
                mac=args.mac,
                password=args.password,
                pdu=args.pdu,
                timeout=args.timeout,
                debug=args.debug,
            )
            print_reply(reply, show_temp=args.temp)
            return 0

        if args.command == "write":
            raw_value = args.value
            if raw_value is None:
                raw_value = int(round(args.temp_value * 10.0))

            reply = write_pdu(
                host=args.host,
                port=args.port,
                mac=args.mac,
                password=args.password,
                pdu=args.pdu,
                value=raw_value,
                timeout=args.timeout,
                debug=args.debug,
            )
            print_reply(reply, show_temp=args.temp or (args.temp_value is not None))
            return 0

        parser.error("Unknown command")
        return 2

    except Exception as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())

Updated Dominus config files for PDU mapping are on GitHub - harykx/dominus · GitHub

If anyone interested, there is a example of HomeAssistant Add-on on GitHub - harykx/dominus-ha · GitHub .
It is GPT generated code (I do not have that programming skills myself), but It works quite well on my Odroid n2+ running HA OS.
Please note, that Dominus app is not able connect when HA Add-on is running, becase Dominus device allows only 1 tcp connection at a time.

Thanks harykx.

Hello Andy!
To confirm, you have connected two devices to yours Immergas? One to T+/T- and second to F1 and F1?
Did I understand it correctly?
If not, did you try to eneble heating with just T+/T- and your code? Is it possible?

The case is, that I have only my Atom5 Lite and RS485 connected to T+/T-. I have also Tybox 137+ connected to 40-1 / 41, it was working before connecting my Atom to T+/T-, and now i’m not able to start heating… :frowning: and it’s really frustrating, because I can change my DHW temperature and have better usage of my PV energy, but can’t heating… :wink:

I’m not interested in buying Remote Panel to enable using Wifi so I’m looking for better solution.

I wonder, if @Bettapro try to emulating zone 1 panel by esp and 485 using D+/D- ?