We recently upgraded one of our properties to 3 phase, and it has an existing single phase PV system with a Solax X1 Hybrid (Gen 2+ inverter.

Hybrid inverters use a power meter installed in the switchboard to allow them to offset the household consumption, drawing power from the batteries/solar as required.

The single phase inverter shipped with a Chint DDSU666 single phase modbus meter, which worked well for the single phase installation.

When we upgraded to 3 phase, I install the Chint DTSU666 3 phase modbus meter, hoping it would be supported by the inverter, but unfortunately it wasn't.

My next port of call was to use the PowerScraper software that I wrote to micromanage the power, but I got into a weird situation where the inverter would not spill the excess solar into the grid when the battery was full - it was as if the battery control registered controlled grid power rather than the battery power.

I finally settled on utilising an ESP32 to talk to the 3 phase meter, and emulate the single phase meter. I used an ESP32 development board, and a couple of TTL to RS485 modules to interface to the meter and inverter. On the meter side, I have a physical 120Ohm resistor terminating the RS485 line. On each of the modules, I have shorted the empty R0 pads with solder to enable the onboard terminating resistor. The interfaces were glued to the back of the ESP32 module, and enamel wire jumpers were added to provide power and data.

An ESP32 development board with RS485 modules glued onto the back

 

The 2 RS485 modules

 

Looking at the requests coming from the inverter, it was hammering register 0x0B (meter type), and not any of the power registers. Passing the value 10 (0x000a) that I got from the DTSU666 didn't work.

I couldn't find any documentation online for what value is returned for the DDSU666, so I ended up adding code to increment the value each time it was accessed, and to save that value once a power register was accessed. This gave me the value 166 (0x00A6) as the first value that the inverter would accept. I later verified this with a spare DDSU666 I had at home.

Giving that value to the inverter made it happy, and it then continued on to request power from the power register (0x2004).

Here is the ESPHome snippet I used to proxy the DTSU666 3 phase data data and emulate the DDSU666 single phase meter:

# Define globals: one for the meter type that auto-increments on read,
# and one to store the meter type when any of the other registers are read.
globals:
 - id: emulated_meter_type
   type: uint16_t
   restore_value: no
   initial_value: '166'
 # - id: last_emulated_meter_type
 #   type: uint16_t
 #   restore_value: no
 #   initial_value: '0'

# Expose both globals to Home Assistant
# number:
#   - platform: template
#     name: "Emulated Meter Type"
#     id: emulated_meter_type_number
#     min_value: 0
#     max_value: 65535
#     step: 1
#     lambda: |-
#       return id(emulated_meter_type);
#     set_action:
#       - lambda: |-
#           id(emulated_meter_type) = x;
 # - platform: template
 #   name: "Last Emulated Meter Type"
 #   id: last_emulated_meter_type_number
 #   min_value: 0
 #   max_value: 65535
 #   step: 1
 #   lambda: |-
 #     return id(last_emulated_meter_type);
 #   set_action:
 #     - lambda: |-
 #         id(last_emulated_meter_type) = x;

# --- UART for DTSU666 three‑phase meter (client) ---
uart:
 - id: uart_dtsu666
   rx_pin: GPIO19
   tx_pin: GPIO18
   baud_rate: 9600
   parity: EVEN
   stop_bits: 1

# --- UART for DDSU666 single‑phase emulation (server) ---
 - id: uart_ddsu666
   rx_pin: GPIO23
   tx_pin: GPIO22
   baud_rate: 9600
   parity: NONE
   stop_bits: 1

# --- Modbus bus definitions ---
modbus:
 - id: modbus_dtsu666
   uart_id: uart_dtsu666
 - id: modbus_ddsu666
   uart_id: uart_ddsu666
   role: server

# --- Modbus controller for DTSU666 (client mode) ---
modbus_controller:
 - id: dtsu666
   modbus_id: modbus_dtsu666
   address: 1
   update_interval: 0.5s

# =====================================================
# DDSU666 Single‑Phase Meter Emulation via Modbus Server
# =====================================================
# This allows an external Modbus client to query the ESPHome node as if it were a DDSU666 meter.
 - id: ddsu666
   modbus_id: modbus_ddsu666
   address: 1   # Set the emulated DDSU meter's Modbus address
   server_registers:
     - address: 0x0B
       value_type: U_WORD
       read_lambda: |-
         // Increment the emulated meter type each time this register is read.
         uint16_t current = id(emulated_meter_type);
         // id(emulated_meter_type) = current + 1;
         return current;
     - address: 0x2000
       value_type: FP32
       read_lambda: |-
         // Update the last_meter_type global when this register is read.
         // id(last_emulated_meter_type) = id(emulated_meter_type);
         // Emulated Voltage from aggregated sensor (already scaled)
         return id(emulated_voltage).state;
     - address: 0x2002
       value_type: FP32
       read_lambda: |-
         // id(last_emulated_meter_type) = id(emulated_meter_type);
         // Emulated Current from aggregated sensor
         return id(emulated_current).state;
     - address: 0x2004
       value_type: FP32
       read_lambda: |-
         // id(last_emulated_meter_type) = id(emulated_meter_type);
         // Emulated Active Power from aggregated sensor
         return id(emulated_active_power).state / 1000.0;
     - address: 0x2006
       value_type: FP32
       read_lambda: |-
         // id(last_emulated_meter_type) = id(emulated_meter_type);
         // Emulated Reactive Power from aggregated sensor
         return id(emulated_reactive_power).state;
     - address: 0x200A
       value_type: FP32
       read_lambda: |-
         // id(last_emulated_meter_type) = id(emulated_meter_type);
         // Emulated Power Factor from aggregated sensor
         return id(emulated_pf).state;
     - address: 0x4000
       value_type: FP32
       read_lambda: |-
         // id(last_emulated_meter_type) = id(emulated_meter_type);
         // Emulated Net Forward Energy from aggregated sensor
         return id(emulated_forward_energy).state;
     - address: 0x400A
       value_type: FP32
       read_lambda: |-
         // id(last_emulated_meter_type) = id(emulated_meter_type);
         // Emulated Net Reverse Energy from aggregated sensor
         return id(emulated_reverse_energy).state;

# =====================================================
# DTSU666 Three‑Phase Meter Sensors
# =====================================================

sensor:
 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Meter Type"
   id: meter_type
   register_type: holding
   address: 0x0B
   register_count: 1
   value_type: U_WORD

 ## Voltages (Electrical Parameters)
 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Line Voltage Uab"
   register_type: holding
   address: 0x2000
   register_count: 2
   value_type: FP32
   unit_of_measurement: "V"
   filters:
     - multiply: 0.1

 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Line Voltage Ubc"
   register_type: holding
   address: 0x2002
   register_count: 2
   value_type: FP32
   unit_of_measurement: "V"
   filters:
     - multiply: 0.1

 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Line Voltage Uca"
   register_type: holding
   address: 0x2004
   register_count: 2
   value_type: FP32
   unit_of_measurement: "V"
   filters:
     - multiply: 0.1

 # Phase voltages (used for aggregation)
 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Phase Voltage Ua"
   id: phase_voltage_a
   register_type: holding
   address: 0x2006
   register_count: 2
   value_type: FP32
   unit_of_measurement: "V"
   filters:
     - multiply: 0.1

 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Phase Voltage Ub"
   id: phase_voltage_b
   register_type: holding
   address: 0x2008
   register_count: 2
   value_type: FP32
   unit_of_measurement: "V"
   filters:
     - multiply: 0.1

 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Phase Voltage Uc"
   id: phase_voltage_c
   register_type: holding
   address: 0x200A
   register_count: 2
   value_type: FP32
   unit_of_measurement: "V"
   filters:
     - multiply: 0.1

 ## Currents (Electrical Parameters)
 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Phase Current Ia"
   id: phase_current_a
   register_type: holding
   address: 0x200C
   register_count: 2
   value_type: FP32
   unit_of_measurement: "A"
   filters:
     - multiply: 0.001

 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Phase Current Ib"
   id: phase_current_b
   register_type: holding
   address: 0x200E
   register_count: 2
   value_type: FP32
   unit_of_measurement: "A"
   filters:
     - multiply: 0.001

 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Phase Current Ic"
   id: phase_current_c
   register_type: holding
   address: 0x2010
   register_count: 2
   value_type: FP32
   unit_of_measurement: "A"
   filters:
     - multiply: 0.001

 ## Active Power and Reactive Power
 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Combined Active Power Pt"
   id: combined_active_power
   register_type: holding
   address: 0x2012
   register_count: 2
   value_type: FP32
   unit_of_measurement: "W"
   filters:
     - multiply: 0.1

 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Combined Reactive Power Qt"
   id: combined_reactive_power
   register_type: holding
   address: 0x201A
   register_count: 2
   value_type: FP32
   unit_of_measurement: "var"
   filters:
     - multiply: 0.1

 ## Power Factor
 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Combined Power Factor PFt"
   id: combined_pf
   register_type: holding
   address: 0x202A
   register_count: 2
   value_type: FP32
   filters:
     - multiply: 0.001

 ## Frequency
 - platform: modbus_controller
   modbus_controller_id: dtsu666
   id: frequency
   name: "Frequency"
   register_type: holding
   address: 0x2044
   register_count: 2
   value_type: FP32
   unit_of_measurement: "Hz"
   filters:
     - multiply: 0.01

 ## Energy Data: Forward Active Energy
 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Net Forward Active Energy"
   id: net_forward_energy
   register_type: holding
   address: 0x1026
   register_count: 2
   value_type: FP32
   unit_of_measurement: "kWh"

 ## Energy Data: Reverse Active Energy
 - platform: modbus_controller
   modbus_controller_id: dtsu666
   name: "Net Reverse Active Energy"
   id: net_reverse_energy
   register_type: holding
   address: 0x1030
   register_count: 2
   value_type: FP32
   unit_of_measurement: "kWh"

# =====================================================
# Template Sensors for Aggregated (Single‑Phase) Values
# These values are computed from the three‑phase readings.
# =====================================================
 - platform: template
   name: "Emulated Voltage"
   id: emulated_voltage
   unit_of_measurement: "V"
   lambda: |-
     // Average the three phase voltages
     return (id(phase_voltage_a).state + id(phase_voltage_b).state + id(phase_voltage_c).state) / 3.0;
   update_interval: 0.5s

 - platform: template
   name: "Emulated Current"
   id: emulated_current
   unit_of_measurement: "A"
   lambda: |-
     // Average the three phase currents
     return (id(phase_current_a).state + id(phase_current_b).state + id(phase_current_c).state) / 3.0;
   update_interval: 0.5s

 - platform: template
   name: "Emulated Active Power"
   id: emulated_active_power
   unit_of_measurement: "W"
   lambda: |-
     // Use the combined active power directly from the meter
     return id(combined_active_power).state;
   update_interval: 0.5s

 - platform: template
   name: "Emulated Reactive Power"
   id: emulated_reactive_power
   unit_of_measurement: "var"
   lambda: |-
     return id(combined_reactive_power).state;
   update_interval: 0.5s

 - platform: template
   name: "Emulated Power Factor"
   id: emulated_pf
   lambda: |-
     return id(combined_pf).state;
   update_interval: 0.5s

 - platform: template
   name: "Emulated Frequency"
   id: emulated_frequency
   unit_of_measurement: "Hz"
   lambda: |-
     return id(frequency).state;
   update_interval: 0.5s

 - platform: template
   name: "Emulated Forward Energy"
   id: emulated_forward_energy
   unit_of_measurement: "kWh"
   lambda: |-
     return id(net_forward_energy).state;
   update_interval: 0.5s

 - platform: template
   name: "Emulated Reverse Energy"
   id: emulated_reverse_energy
   unit_of_measurement: "kWh"
   lambda: |-
     return id(net_reverse_energy).state;
   update_interval: 0.5s
 

References

ESPHome Modbus Controller

Chint DDSU666 Single Phase Energy Meter Modbus Registers

Chint DTSU 3 Phase Energy Meter Modbus Registers