Notes on using an ME3000SP inverter

If you are installing one of these then firstly read this set of notes, it's very useful. I'll confirm/add a few things here.

Firstly, upon install you absolutely MUST 'freeze' (lock) the direction of the main import/export CT clamp. If you don't do this, then it will look like it's all working ok, but when the sun comes out the inverter may detect the solar energy being exported as an import and attempt to match it, and which ends up basically discharging your battery into the grid at maximum power (plus the solar being exported on top).

As mentioned on the page above the UI is a little confusing though. If the screen says "FREEZE" under the CT list then the direction is frozen. If the screen says "UNFREEZE" then you haven't done it right, or if you are checking it later then it has somehow become unfrozen.

Secondly, connecting the PVsolar CT clamp appears to be optional (the inverter should be able to just charge until the export CT goes to zero), but I have just experienced a problem where the inverter will click it's relays, start a charge and then flick off and cycle though checking/charging (and clicking the relays). I fixed it by connecting the solar CT, which I had disconnected at some point over the winter while tidying the cabling. (Side note: I also had a buzzing from my solar consumer unit, I thought it was an RCBO but it was actually the CT 'ringing' because it wasn't connected!)

Thirdly, don't blindly update the firmware unless you need to. Usually it's a good idea to update software regularly, but I've read about people experiencing various problems with newer versions, when they had no reason to upgrade in the first place. If it ain't broken, don't fix it!

ESPHome Integration

I couldn't find a native ESPHome configuration for the ME3000SP, so I wrote this, based on work for other similar Sofar devices. Hopefully it might be useful to others! This is specifically intended for Passive (manual) work mode (which needs to be selected on the inverter). The mode will appear as a Select dropdown in Home Assistant, which can be manipulated either in the UI or through automations. The charge rate appears as a numeric field - set the default to whatever value you need.

substitutions:
  device_name: me3000sp
  name: ${device_name}
  device_verbose_name: "ME3000SP"
  ip: 192.168.1.110

esphome:
  name: ${device_name}
  platform: ESP32
  board: esp32dev

logger:
  level: DEBUG

web_server:
  port: 80

packages:
  wifi: !include includes/wifi.yaml
  config: !include includes/config.yaml
  ha: !include includes/ha.yaml

uart:

  - id: mod_bus
    tx_pin: GPIO26
    rx_pin: GPIO27
    baud_rate: 9600
    stop_bits: 1
    debug:
      direction: BOTH

modbus:
  id: modbus_me3000sp
  uart_id: mod_bus
  send_wait_time: 200ms

modbus_controller:

  - id: me3000sp
    address: 0x01
    modbus_id: modbus_me3000sp
    update_interval: 30s
    setup_priority: -10

text_sensor:

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Run state
    id: run_state
    register_type: holding
    address: 0x0200
    lambda: |-
      auto z = "Unknown";
      char d = data[item->offset+1];
      if (d == 0) z = "Wait";
      else if (d == 1) z = "Checking";
      else if (d == 2) z = "Charging";
      else if (d == 3) z = "Checking Discharge";
      else if (d == 4) z = "Discharging";
      else if (d == 5) z = "EPS State";
      else if (d == 6) z = "Fault";
      else if (d == 7) z = "Permanent Fault";
      return {z};

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Fault Message
    id: inverter_fault_message
    register_type: holding
    address: 0x0201
    response_size: 10
    lambda: |-
      std::string z = "";
      int idx = item->offset;
      //byte[0]
      if ((data[idx] & 0x1) != 0) z += "GridOVP,";
      if ((data[idx] & 0x2) != 0) z += "GridUVP,";
      if ((data[idx] & 0x4) != 0) z += "GridOFP,";
      if ((data[idx] & 0x8) != 0) z += "GridUFP,";
      if ((data[idx] & 0x10) != 0) z += "PVUVP,";
      if ((data[idx] & 0x20) != 0) z += "GridLVRT,";
      if ((data[idx] & 0x40) != 0) z += "reserve-ID7,";
      if ((data[idx] & 0x80) != 0) z += "reserve-ID8,";
      //byte[1]
      idx++;
      if ((data[idx] & 0x1) != 0) z += "PVOVP,";
      if ((data[idx] & 0x2) != 0) z += "IpvUnbalance,";
      if ((data[idx] & 0x4) != 0) z += "PvConfigSetWrong,";
      if ((data[idx] & 0x8) != 0) z += "GFCIFault,";
      if ((data[idx] & 0x10) != 0) z += "PhaseSequenceFault,";
      if ((data[idx] & 0x20) != 0) z += "HwBoostOCP,";
      if ((data[idx] & 0x40) != 0) z += "HwAcOCP,";
      if ((data[idx] & 0x80) != 0) z += "AcRmsOCP,";
      //byte[2]
      idx++;
      if ((data[idx] & 0x1) != 0) z += "HwADFaultIGrid,";
      if ((data[idx] & 0x2) != 0) z += "HwADFaultDCI,";
      if ((data[idx] & 0x4) != 0) z += "HwADFaultVGrid,";
      if ((data[idx] & 0x8) != 0) z += "GFCIDeviceFault,";
      if ((data[idx] & 0x10) != 0) z += "MChip_Fault,";
      if ((data[idx] & 0x20) != 0) z += "HwAuxPowerFault,";
      if ((data[idx] & 0x40) != 0) z += "BusVoltZeroFault,";
      if ((data[idx] & 0x80) != 0) z += "IacRmsUnbalance,";
      //byte[3]
      idx++;
      if ((data[idx] & 0x1) != 0) z += "BusUVP,";
      if ((data[idx] & 0x2) != 0) z += "BusOVP,";
      if ((data[idx] & 0x4) != 0) z += "VbusUnbalance,";
      if ((data[idx] & 0x8) != 0) z += "DciOCP,";
      if ((data[idx] & 0x10) != 0) z += "SwOCPInstant,";
      if ((data[idx] & 0x20) != 0) z += "SwBOCPInstant,";
      if ((data[idx] & 0x40) != 0) z += "reserved-ID31,";
      if ((data[idx] & 0x80) != 0) z += "reserved-ID32,";
      //byte[4]
      idx++;
      if (data[idx] != 0) z += "reserved-ID33~40,";
      //byte[5]
      idx++;
      if (data[idx] != 0) z += "reserved-ID41~48,";
      //byte[6]
      idx++;
      if ((data[idx] & 0x1) != 0) z += "ConsistentFault_VGrid,";
      if ((data[idx] & 0x2) != 0) z += "ConsistentFault_FGrid,";
      if ((data[idx] & 0x4) != 0) z += "ConsistentFault_DCI,";
      if ((data[idx] & 0x8) != 0) z += "ConsistentFault_GFCI,";
      if ((data[idx] & 0x10) != 0) z += "SpiCommLose,";
      if ((data[idx] & 0x20) != 0) z += "SciCommLose,";
      if ((data[idx] & 0x40) != 0) z += "RelayTestFail,";
      if ((data[idx] & 0x80) != 0) z += "PvIsoFault,";
      //byte[7]
      idx++;
      if ((data[idx] & 0x1) != 0) z += "OverTempFault_Inv,";
      if ((data[idx] & 0x2) != 0) z += "OverTempFault_Boost,";
      if ((data[idx] & 0x4) != 0) z += "OverTempFault_Env,";
      if ((data[idx] & 0x8) != 0) z += "PEConnectFault,";
      if ((data[idx] & 0x10) != 0) z += "reserved-ID61,";
      if ((data[idx] & 0x20) != 0) z += "reserved-ID62,";
      if ((data[idx] & 0x40) != 0) z += "reserved-ID63,";
      if ((data[idx] & 0x80) != 0) z += "reserved-ID64,";
      //byte[8]
      idx++;
      if ((data[idx] & 0x1) != 0) z += "unrecoverHwAcOCP,";
      if ((data[idx] & 0x2) != 0) z += "unrecoverBusOVP,";
      if ((data[idx] & 0x4) != 0) z += "unrecoverIacRmsUnbalance,";
      if ((data[idx] & 0x8) != 0) z += "unrecoverIpvUnbalance,";
      if ((data[idx] & 0x10) != 0) z += "unrecoverVbusUnbalance,";
      if ((data[idx] & 0x20) != 0) z += "unrecoverOCPInstant,";
      if ((data[idx] & 0x40) != 0) z += "unrecoverPvConfigSetWrong,";
      if ((data[idx] & 0x80) != 0) z += "reserved-ID72,";
      //byte[9]
      idx++;
      if ((data[idx] & 0x1) != 0) z += "reserved-ID73,";
      if ((data[idx] & 0x2) != 0) z += "unrecoverIPVInstant,";
      if ((data[idx] & 0x4) != 0) z += "unrecoverWRITEEEPROM,";
      if ((data[idx] & 0x8) != 0) z += "unrecoverREADEEPROM,";
      if ((data[idx] & 0x10) != 0) z += "unrecoverRelayFail,";
      if ((data[idx] & 0x20) != 0) z += "reserved-ID78,";
      if ((data[idx] & 0x40) != 0) z += "reserved-ID79,";
      if ((data[idx] & 0x80) != 0) z += "reserved-ID80,";
      if(z.length() > 0){
        z.pop_back();
      }
      return {z};

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Battery type
    id: battery_type
    register_type: read
    address: 0x10B0
    skip_updates: 360
    lambda: |-
      auto z = "Unknown";
      char d = data[item->offset+1];
      if (d == 0) z = "DARFON";
      else if (d == 1) z = "PYLON";
      else if (d == 2) z = "SOLTARO";
      else if (d == 3) z = "ALPHA.ESS";
      else if (d == 4) z = "GENERAL";
      else if (d == 100) z = "DEFAULT";
      return {z};

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Serial number 
    id: serial_number
    register_type: read
    address: 0x2001
    skip_updates: 360
    raw_encode: NONE
    register_count: 14
    response_size: 26
    offset: 2
    lambda: |-
      return x.substr(0,14);

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Software version 
    id: software_version
    register_type: read
    address: 0x2001
    skip_updates: 360
    raw_encode: NONE
    register_count: 14
    response_size: 26
    offset: 2
    lambda: |-
      return x.substr(12,4);

  - platform: homeassistant
    entity_id: input_select.me3000sp_mode
    id: mode
    on_value:
      - lambda: |-
          if (id(mode).state == "auto") 
          {
            ESP_LOGI("mode", "Mode set to auto");

            //id(led_auto).execute();

            modbus_controller::ModbusController *controller = id(me3000sp);
            std::vector<uint8_t> payload = {0x1, 0x42, 0x01, 0x03, 0x00, 0x00};
            modbus_controller::ModbusCommandItem command = modbus_controller::ModbusCommandItem::create_custom_command(controller, payload);
            controller->queue_command(command);
          } 
          else if (id(mode).state == "wait") 
          {
            ESP_LOGI("mode", "Mode set to wait");

            //id(led_wait).execute();

            modbus_controller::ModbusController *controller = id(me3000sp);
            std::vector<uint8_t> payload = {0x1, 0x42, 0x01, 0x00, 0x55, 0x55};
            modbus_controller::ModbusCommandItem command = modbus_controller::ModbusCommandItem::create_custom_command(controller, payload);
            controller->queue_command(command);
          } 
          else if (id(mode).state == "battery_save") 
          {
            ESP_LOGI("mode", "Mode set to battery_save");
          } 
          else if (id(mode).state == "charge") 
          {
            ESP_LOGI("mode", "Mode set to charge");
            int rate = min(3000, (int) id(charge_rate).state);

            //id(led_charge).execute();

            if (rate > 0)
            {
              ESP_LOGI("charging", "Forcing charge at %d W", rate);
              modbus_controller::ModbusController *controller = id(me3000sp);
              std::vector<uint8_t> payload = {0x1, 0x42, 0x01, 0x02, (rate >> 8) & 0xFF, rate & 0xFF};
              modbus_controller::ModbusCommandItem command = modbus_controller::ModbusCommandItem::create_custom_command(controller, payload);
              controller->queue_command(command);
            }
            else
            {
              ESP_LOGI("charging", "Alert! Mode is charge, but rate is set to %d - doing nothing", rate);
            }
          } 

sensor:

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Grid voltage
    id: grid_voltage
    unit_of_measurement: "V"
    device_class: voltage
    register_type: holding
    address: 0x0206
    value_type: U_WORD
    filters:
      - multiply: 0.1

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Grid current
    id: grid_current
    unit_of_measurement: "A"
    device_class: current
    register_type: holding
    address: 0x0207
    value_type: S_WORD
    filters:
      - multiply: 0.01

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Grid frequency
    id: grid_frequency
    unit_of_measurement: "Hz"
    device_class: frequency
    register_type: holding
    address: 0x020C
    value_type: U_WORD
    filters:
      - multiply: 0.01

  # read from the CT clamp?
  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Grid power
    id: grid_power
    register_type: holding
    address: 0x0212
    unit_of_measurement: "W"
    device_class: power
    value_type: S_WORD
    filters:
      - multiply: 10

      ####


  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Battery power
    id: battery_power
    unit_of_measurement: "W"
    device_class: power
    register_type: holding
    address: 0x020d
    value_type: S_WORD
    filters:
      - multiply: 10

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Battery charge today
    id: battery_charge_today
    unit_of_measurement: "Wh"
    device_class: energy
    register_type: holding
    address: 0x0224
    skip_updates: 6
    value_type: S_WORD
    filters:
      - multiply: 10

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Battery discharge today
    id: battery_discharge_today
    unit_of_measurement: "Wh"
    device_class: energy
    register_type: holding
    address: 0x0225
    skip_updates: 6
    value_type: S_WORD
    filters:
      - multiply: 10

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Internal temperature
    id: internal_temperature
    unit_of_measurement: "C"
    device_class: temperature
    register_type: holding
    address: 0x0238
    value_type: S_WORD
 
  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Heat shrink temperature
    id: heat_shrink_temperature
    unit_of_measurement: "C"
    device_class: temperature
    register_type: holding
    address: 0x0239
    value_type: S_WORD
 
  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Battery voltage
    id: battery_voltage
    unit_of_measurement: "V"
    device_class: voltage
    register_type: holding
    address: 0x020e
    value_type: U_WORD
    filters:
      - multiply: .01

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Battery current
    id: battery_current
    unit_of_measurement: "A"
    device_class: current
    register_type: holding
    address: 0x020f
    value_type: S_WORD
    filters:
      - multiply: .01

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Battery SOC
    id: battery_percent
    register_type: holding
    address: 0x0210
    device_class: battery
    unit_of_measurement: "%"
    value_type: U_WORD
    filters:
      - lambda: |-
          if (x > 0 && x <= 100) return x;
          else return {};

  # this is from the inverter's own temperature sensor port
  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Battery temperature
    id: battery_temperature
    register_type: holding
    address: 0x0211
    unit_of_measurement: "C"
    device_class: temperature
    value_type: U_WORD
    
  # Presuambly calculated from the CT clamp and internal consumption
  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} Load power
    id: load_power
    register_type: holding
    address: 0x0213
    unit_of_measurement: "W"
    device_class: power
    value_type: U_WORD
    filters:
      - multiply: 10

  # EPS
  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} EPS output power
    id: eps_output_power
    register_type: holding
    address: 0x0217
    unit_of_measurement: "W"
    device_class: power
    value_type: U_WORD
    filters:
      - multiply: 10

  - platform: modbus_controller
    modbus_controller_id: me3000sp
    name: ${device_verbose_name} EPS voltage
    id: eps_voltage
    register_type: holding
    address: 0x0216
    unit_of_measurement: "V"
    device_class: voltage
    value_type: U_WORD
    filters:
      - multiply: 0.1

switch:
  - platform: restart
    name: ${device_verbose_name} Restart


number:
  - platform: template
    name: ${device_verbose_name} Charge rate
    id: charge_rate
    icon: "mdi:lightning-bolt"
    unit_of_measurement: W
    device_class: power 
    optimistic: true
    min_value: 0
    max_value: 3000
    restore_value: True
    initial_value: 2750
    step: 1