Connecting to the New Deye Logger to Home Assistant using ESPHome

Good day!

I just want to share how I managed to pull data from our Deye 8K Hybrid Inverter (SUN-8KSG05LP1-EU) into HomeAssistant.

There exists a myriad of ways to do it but I was having issues establishing a connection with the inverter using an RS485 module and ESP32Dev-C Kit. I was also a bit hesitant on routing another wire into the conduits that our installer has made. There was also connective wirelessly but upon checking, Deye’s latest version of its data logger gas blocked port 8899 from being accessed preventing Solarman-based wireless integration from working (please do correct me if I missed some details on to use it correctly). This got me curious on how else to pull data.

The Deye Cloud app has a “local mode” that connects to the logger via BLE. With this, I was able to monitor the app’s and the logger’s communication and made a YAML file that pulls several values from the inverter that the logger is connected to. I tried to map the values with the corresponding UI fields by comparing them with the screenshot during the data acquisition. I came up with the following values:

Production

Sensor Unit Register
Total Production kWh 0x0060
Daily Production kWh 0x006C

PV

Sensor Unit Register
PV1 Voltage V 0x006D
PV1 Current A 0x006E
PV1 Power W computed (V × I)
PV2 Voltage V 0x006F
PV2 Current A 0x0070
PV2 Power W computed (V × I)
Total PV Power W computed (PV1 + PV2)
Max Solar Power W 0x0035

Grid

Sensor Unit Register
Grid Voltage 1 V 0x0096
Grid Voltage 2 V 0x0097
Grid Power W 0x00A9

Load

Sensor Unit Register
Daily Load Consumption kWh 0x0054
Total Load Consumption kWh 0x0055
Load L1 Voltage V 0x009D
Load L1 Current A 0x00A4
Load L1 Power W 0x00B2
Total Load Power W 0x00B0

Inverter

Sensor Unit Register
Total Power W 0x00AD
Inverter L1 Power W 0x00AF
DC Temperature °C 0x005A
AC Temperature °C 0x005B

Battery

Sensor Unit Register
Battery Voltage V 0x00B7
Battery Current A 0x00BF
Battery Power W 0x00BE
Battery SOC % 0x00B8

Battery Settings (diagnostic)

Sensor Unit Register
Battery Capacity Ah 0x00CC
Battery Empty Voltage V 0x00CD
Max Charge Current A 0x00D2
Max Discharge Current A 0x00D3
Battery Shutdown % % 0x00D9
Battery Restart % % 0x00DA
Battery Low % % 0x00DB

Selling Mode (diagnostic)

Sensor Unit Register
Selling Mode Time 1–6 HH:MM 0x00FA–0x00FF
Selling Mode Power 1–6 W 0x0100–0x0105
Selling Mode SOC 1–6 % 0x010C–0x0111
Selling Mode Charge Flag 1–6 — 0x0112–0x0117

Please note that these registers are the only ones I could map. If you have a known list of registers, perhaps we could expand these further. If you also find something not correct, please do share.

esphome_deye.yaml

Hi,
I discovered on my 5k-SG05LP1;

245 = Export Power Limit.

244:
Sell to grid= 0
Zero export load = 1
Zero export ct =2

My goal is to create an automation where the inverter output follows the grid power using battery energy. :slight_smile:

Thank you so much—it's working perfectly for me. Could you please share the latest YAML code with any new properties you've identified? I also have one more question: can the same YAML be used for a three‑phase inverter? If you have the details, please share them. I really appreciate your efforts, and I’ll share this information with the community.

Hi @dijin123 !

Glad it's working on your end. I've updated the YAML file to fix some incorrect readings. From what I am seeing, the values are spot-on when comparing to the ones I see in the Deye Cloud website.

About he YAML's usage on the three-phase model, from what I've read, some registers might be different. You can check out kbialek/deye-inverter-mqtt to see if your three-phase inverter is included in the list. I also crossed-reference some registers from kbialek so credits go to them.

Hi @drkdoesha,

Thank you for your quick response. I've successfully integrated my Deye inverter with Home Assistant via BLE using ESPHome to fetch all the data. However, the Wi-Fi logger and the BLE connection aren't working simultaneously. The Deye Cloud sync through the Logger is stop working after we started the code in ESPHome. Is there a configuration option or workaround in the ESPHome code to fix this issue?

Hello, @dijin123 !

Unfortunately, that seems to be a limit on the logger. Once we connect to via BLE, the logger won't be connecting to the Deye Cloud servers.

As a workaround, I did put a reconnect interval. When set to 0, it will not disconnect. Other values would set the reconnect countdown from the time it disconnected. So if, for example, you set the BLE Reconnect Interval to 30s, after the ESP32 reads the values, it will disconnect from the logger then attempt to reconnect after 30 seconds. This allows the logger to also get to report to the Deye Cloud server.

image

I hope this workaround fits your requirement.

You may be coming up against the design limitation of BLE of only one session permitted. This is part of the BlueTooth standard, not something unique to this app. If ESPHome is grabbing the device, then the Deye Cloud might not be getting a word in edgewise.
I haven't looked at the code algorithm - if the Deye device is emitting passive BLE packets, you should be able to sniff them from multiple sources. If a session needs to be established, you will come across this limitation.

You're correct, @IOT7712. I had to establish a session with the logger to be able to capture their communication and retrieve the values.

You have to choose between local, cloud, or both. Maybe the integration can collect the data locally, and spoof the cloud session to stuff the data into the Deye Cloud for the best of both worlds.

RS485 beckons... Dump the BLE traffic altogether - you may find it intermittent and unreliable. You may find the following Protocol Wizard app of use to help you on your journey:

Hi @drkdoesha Thanks. Let me try testing it. Is the BLE Reconnect Interval code that you just shared available?

Oh yes, it does. Give it a try!

Hi @drkdoesha,
It works fine based on my testing. Thank you for sharing the code. My friend's inverter is 3‑phase, and he asked me to help him set up something similar.

Hi @drkdoesha
I ran the code for about 10 hours, but this morning the Deye cloud sync failed again and wasn't working properly. So I restarted the ESP32 device, and then it started working again and adjusted the BLE Reconnect option to 40s. I'm not sure what caused the issue.

I would increase the BLE Reconnect Interval, just like what you've done. I have not explored any other workaround aside from this. Like what @IOT7712 has described, it's a hardware limitation to which I have no plan on investigating further.

IOT7712 is right about the BLE limits. The cloud logger will be constantly fighting for that one session. The only way to get stable HA data with the cloud sync still running is to hardwire to the RS485 port.

Since your friend is on a 3-phase inverter, just a quick heads-up: the standard 3-phase Modbus registers and RJ45 pinouts are completely different from the single-phase models. Also, make sure your ESPHome yaml parses two's complement (S16) correctly for bidirectional data (like battery/grid current), otherwise HA will throw massive wattage spikes during export.

I did this last week obtaining local data via modbus port to RS485 to a flashed ESP32 connected via USB to Home Assistant running on a Macmini (be sure to use a metre and half ethernet cable existing the inverter, an extension cable for any USB WiFi Dongle from Mac/PC, and separate the Mac/PC a little distance to the Deye metal to avoid interference).

However the Deye data logger is the master device if it remains connected to the inverter and will overwrite any controls set in Home Assistant from your modbus exports within about 60 seconds. They fight each other for control of your inverter. Until i recreate the Deye cloud app data in HA i will keep it connected however so i can view data as i optimise setup. Despite best efforts of various AI to map the settings, until I obtained the 46 page Modbus protocol PDF from Deye, in Chinese, from their official support via email, specific to my model/firmware (as they are not universal) I could not accurately expose all the entities correctly in HA. It took days of fiddling to nail it down until I had that doc.

I understand their is another hardware wave device that will allow the two systems to coexist but I am minded to create an optimised HA data dashboard and controls and will eventually disconnect the factory Deye data logger.

For me the Solarman setup is i understand deprecated and the Deye API method into HA does not satisfy my needs either as I want sub 60 second reactive Home Assistant automations bypassing the Cloud, based on PV/battery system behaviours.

Hi @drkdoesha — huge thanks for this! :folded_hands:

I used your ESPHome BLE config as the base to bring my Deye SUN-8K-SG05LP1-EU-AM2-P (single-phase 8 kW, DL1000B-WIFI logger, BLE service 0x0922) into Home Assistant, and it works great. Your reverse-engineered register map for the SG05LP1 was spot-on for the AM2-P variant too. Sharing back the tweaks I made in case they help others with the same model. Full config attached.

Changes I made:

  1. Battery power/current sign was inverted on my unit. On the AM2-P, reg_s(8) (reg 0x00BE) reads positive while discharging, so I negate it to get the usual convention (positive = charging, negative = discharging). Same for battery current. Confirmed against the Deye app — it showed “Discharging” while my raw value was positive — and the numbers check out to the watt (Battery V × I = Battery Power):
    id(battery_power).publish_state((float)(-reg_s(8)));
    id(battery_current).publish_state(-reg_s(9) * 0.01f);

  2. Removed the PWM fan controller. I don’t have the cooling fans, so I stripped all the fan/PWM/tach/output/number/switch blocks. Keeps it a clean read-only monitor.

  3. Short BLE cycle to keep Deye Cloud alive. The logger only allows one BLE central at a time and stops uploading to Deye Cloud while HA is connected. So instead of staying connected, I poll, disconnect, and reconnect every 110 s (exposed as a “BLE Reconnect Interval” number in HA). That leaves the logger free ~90% of the time. I’m still evaluating whether the cloud sync holds up long-term with this — will report back.

  4. Commented out the Selling/TOU blocks (Selling Power/SOC/Voltage/Time/Charge-flag + Time-of-Use). I don’t use time-of-use scheduling, so disabling them drops 30 entities and shaves ~2 s off each poll. Left them commented so they’re trivial to re-enable.

Verification: every live value matched the Deye Cloud app at the same instant — daily/total production, PV1/PV2, battery V/SOC/power, grid voltage/power, load, temperatures, frequency. Rock solid.

To use: set ble_mac to your logger’s MAC and fill in your secrets. Thanks again for doing the hard part — this turned into a copy-paste job for me. :raising_hands:

1/2

################################################################################
# Deye SUN-8KSG05LP1-EU-AM2-P — Lectura BLE nativa en ESPHome (SOLO LECTURA)
#
# Basado en la config del foro de HA (mapa reverseado para SG05LP1), PODADA:
#   - QUITADO todo el control de ventiladores PWM.
#   - CORREGIDO el signo de Potencia de Batería (this unit: +=descarga → se niega).
#   - Ciclo CORTO para no acaparar el BLE: conecta, lee ~11s, desconecta, espera 110s.
#     Así el pincho tiene ~90% del tiempo libre para sincronizar con Deye Cloud.
#   - Métricas cada ~2 min (ajustable en vivo desde HA con "BLE Reconnect Interval").
#
# ANTES DE FLASHEAR — crea secrets.yaml junto a este fichero con:
#   wifi_ssid: "TU_WIFI"
#   wifi_password: "..."
#   api_key: "..."              (genera una nueva, base64 de 32 bytes)
#   ota_password: "..."
#   fallback_password: "..."
# 1ª vez por USB (web.esphome.io o dashboard), luego OTA.
#
# NOTA cloud: si tras probar ves que la nube igual se degrada, sube el
# "BLE Reconnect Interval" (menos frecuencia = más margen para la nube).
################################################################################

substitutions:
  devicename: 'deye-inversor'
  friendly_name: 'Deye Inversor'
  ble_mac: 'XX:XX:XX:XX:XX:XX'          # MAC BLE
  service_uuid: '00000922-0000-1000-8000-00805f9b34fb'
  read_uuid:    '0000fed8-0000-1000-8000-00805f9b34fb'
  write_uuid:   '0000fec7-0000-1000-8000-00805f9b34fb'

  # ── Frames Modbus (SIN CRC — se calcula en runtime) ─
  # Formato: 01 03 <start_hi><start_lo> <count_hi><count_lo>
  cmd_block_a: '010300540003' # Daily/Total Load Consumption
  cmd_block_b: '0103005A0002' # DC/AC Temperature
  cmd_block_c: '0103006C0005' # Daily Production, PV1/PV2 V & I
  cmd_block_d: '010300960002' # Grid Voltage 1 & 2
  cmd_block_e: '0103009D0002' # Load L1 Voltage
  cmd_block_f: '010300A4000C' # Current L1, Total/Inverter Power
  cmd_block_g: '010300B00003' # Total Load Power, Load L1 Power
  cmd_block_h: '010300B6000B' # Battery Temp, Voltage, SOC, Power, Current, Grid Freq
  cmd_block_i: '010300C8000E' # Battery Capacity, Empty V, Max Charge/Discharge
  cmd_block_j: '010300D90006' # Battery Shutdown/Restart/Low %
  cmd_block_k: '010300F80008' # Selling Mode Times 1-6
  cmd_block_l: '010301000006' # Selling Mode Power 1-6 (W)
  cmd_block_m: '0103010C0006' # Selling Mode SOC % 1-6
  cmd_block_n: '010301120006' # Selling Mode Charge flags 1-6
  cmd_block_o: '01030046000D' # Native energy counters (daily+total)
  cmd_block_p: '010301060006' # Selling Mode Voltages 1-6
  cmd_block_q: '010300BA0002' # PV1/PV2 Power (W)
  cmd_block_r: '010301380008' # BMS Data
  cmd_single_total_production: '010300600002' # Total Production (kWh)
  cmd_single_max_solar: '010300350001'        # Max Solar Power (W)
  cmd_single_total_pv_power: '010300730001'    # Total PV Power (W) — devuelve 0, se calcula
  cmd_single_grid_power: '010300A90001'        # Grid Power (W)
  cmd_single_active_power_reg: '010300280001'  # Active Power Regulation (%)

esphome:
  name: '${devicename}'
  friendly_name: '${friendly_name}'
  min_version: 2025.11.0
  name_add_mac_suffix: false

esp32:
  variant: esp32
  framework:
    type: esp-idf

# INFO para ver conexiones. Sube a DEBUG para ver tramas BLE crudas; baja a ERROR cuando vaya fino.
logger:
  level: INFO

api:
  encryption:
    key: !secret api_key
ota:
  platform: esphome
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: ${devicename}
    password: !secret fallback_password
  fast_connect: true
  power_save_mode: none

################################################################################
# BLE
################################################################################

esp32_ble_tracker:
  scan_parameters:
    interval: 1100ms
    window: 1100ms
    active: true

ble_client:
  - mac_address: '${ble_mac}'
    id: deye_ble
    on_connect:
      then:
        - lambda: 'ESP_LOGI("deye", "Conectado al inversor"); id(last_successful_poll) = millis();'
        - delay: 2000ms
        - script.execute: poll_all_blocks
    on_disconnect:
      then:
        - lambda: 'ESP_LOGI("deye", "BLE desconectado");'

################################################################################
# Globals
################################################################################

globals:
  - id: current_block
    type: int
    restore_value: no
    initial_value: '0'
  - id: pending_cmd
    type: std::string
    restore_value: no
    initial_value: '""'
  - id: ble_reconnect_counter
    type: int
    restore_value: no
    initial_value: '0'
  - id: last_successful_poll
    type: uint32_t
    restore_value: no
    initial_value: '0'

################################################################################
# Intervalos: poll (modo permanente), countdown de reconexión, watchdog
################################################################################

interval:
  # Poll cada 30s SOLO si estás en modo permanente (reconnect interval == 0).
  # En modo ciclo-corto (reconnect > 0) este no dispara; el poll lo lanza on_connect.
  - interval: 30s
    then:
      - if:
          condition:
            lambda: 'return id(deye_ble).connected() && id(ble_reconnect_interval).state == 0;'
          then:
            - script.execute: poll_all_blocks

  # Countdown de reconexión: cuando reconnect_interval > 0, reconecta al llegar a 0.
  - interval: 1s
    then:
      - if:
          condition:
            lambda: |-
              return id(ble_reconnect_interval).state > 0 &&
                     !id(deye_ble).connected() &&
                     !id(poll_all_blocks).is_running() &&
                     id(ble_reconnect_counter) > 0 &&
                     --id(ble_reconnect_counter) == 0;
          then:
            - lambda: 'ESP_LOGI("deye", "Reconectando al inversor...");'
            - switch.turn_on: inverter_ble_switch

  # Watchdog (solo en modo permanente): si no llega respuesta en 90s, fuerza reconexión.
  - interval: 10s
    then:
      - if:
          condition:
            lambda: |-
              return id(ble_reconnect_interval).state == 0 &&
                     id(deye_ble).connected() &&
                     id(last_successful_poll) != 0 &&
                     (millis() - id(last_successful_poll)) > 90000;
          then:
            - lambda: 'ESP_LOGW("deye", "Watchdog: sin respuesta en 90s — reconectando");'
            - switch.turn_off: inverter_ble_switch
            - delay: 2s
            - switch.turn_on: inverter_ble_switch

################################################################################
# Scripts
################################################################################

script:
  # Construye el comando AT con CRC16-Modbus y lo escribe por BLE.
  - id: send_modbus_cmd
    mode: queued
    parameters:
      frame: string
    then:
      - lambda: |-
          auto h = [](char c) -> uint8_t {
            if (c >= '0' && c <= '9') return c - '0';
            if (c >= 'A' && c <= 'F') return c - 'A' + 10;
            if (c >= 'a' && c <= 'f') return c - 'a' + 10;
            return 0;
          };
          std::vector<uint8_t> bytes;
          for (size_t i = 0; i + 1 < frame.size(); i += 2)
            bytes.push_back((h(frame[i]) << 4) | h(frame[i+1]));
          uint16_t crc = 0xFFFF;
          for (auto b : bytes) {
            crc ^= b;
            for (int i = 0; i < 8; i++)
              crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
          }
          char cbuf[5];
          snprintf(cbuf, sizeof(cbuf), "%02X%02X", crc & 0xFF, (crc >> 8) & 0xFF);
          id(pending_cmd) = "AT+INVDATA=8," + frame + cbuf + "\r\n";
          ESP_LOGD("deye", "TX: %s", id(pending_cmd).c_str());
      - ble_client.ble_write:
          id: deye_ble
          service_uuid: '${service_uuid}'
          characteristic_uuid: '${write_uuid}'
          value: !lambda |-
            return std::vector<uint8_t>(id(pending_cmd).begin(), id(pending_cmd).end());

  # Sondea TODOS los bloques en secuencia (versión simple, como la config probada del foro).
  # 400ms entre cada uno para dar tiempo al pincho a responder.
  # (La optimización fast/slow se estudiará DESPUÉS de confirmar que esto funciona.)
  - id: poll_all_blocks
    mode: single
    then:
      - lambda: 'id(current_block) = 1;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_c}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 2;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_q}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 3;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_single_total_pv_power}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 4;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_d}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 5;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_single_grid_power}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 6;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_e}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 7;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_f}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 8;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_g}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 9;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_h}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 10;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_b}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 11;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_a}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 12;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_single_total_production}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 13;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_single_max_solar}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 14;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_o}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 15;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_i}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 16;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_j}'}
      - delay: 400ms
      # ── SELLING/TOU DESACTIVADO (descomentar estas líneas para reactivar) ──
      # - lambda: 'id(current_block) = 17;'
      # - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_k}'}
      # - delay: 400ms
      # - lambda: 'id(current_block) = 18;'
      # - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_l}'}
      # - delay: 400ms
      # - lambda: 'id(current_block) = 19;'
      # - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_m}'}
      # - delay: 400ms
      # - lambda: 'id(current_block) = 20;'
      # - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_n}'}
      # - delay: 400ms
      # - lambda: 'id(current_block) = 21;'
      # - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_p}'}
      # - delay: 400ms
      - lambda: 'id(current_block) = 22;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_block_r}'}
      - delay: 400ms
      - lambda: 'id(current_block) = 23;'
      - script.execute: {id: send_modbus_cmd, frame: '${cmd_single_active_power_reg}'}
      - delay: 400ms
      # Modo ciclo-corto: al terminar el poll, desconecta y programa reconexión.
      - if:
          condition:
            lambda: 'return id(ble_reconnect_interval).state > 0;'
          then:
            - lambda: |-
                id(ble_reconnect_counter) = (int)id(ble_reconnect_interval).state;
                ESP_LOGI("deye", "Poll hecho — desconecto, reconecto en %.0fs", id(ble_reconnect_interval).state);
            - switch.turn_off: inverter_ble_switch

################################################################################
# Parser de notificaciones BLE — un handler, despacha según current_block
# Respuesta: "+ok=<hex modbus incl CRC>". reg(n)=unsigned, reg_s(n)=signed.
################################################################################


2/2

sensor:
  - platform: ble_client
    type: characteristic
    ble_client_id: deye_ble
    id: ble_raw_notify
    name: 'BLE Raw Notify'
    service_uuid: '${service_uuid}'
    characteristic_uuid: '${read_uuid}'
    notify: true
    internal: true
    update_interval: never
    lambda: |-
      std::string raw_str(x.begin(), x.end());
      ESP_LOGD("deye", "RX block=%d: %s", id(current_block), raw_str.c_str());
      if (x.size() < 5 || x[0] != '+' || x[1] != 'o' || x[2] != 'k' || x[3] != '=') {
        ESP_LOGW("deye", "Respuesta inesperada: %s", raw_str.c_str());
        return NAN;
      }
      std::string hex_str;
      for (size_t i = 4; i < x.size(); i++) {
        if (x[i] == '\r' || x[i] == '\n') break;
        hex_str += (char)x[i];
      }
      if (hex_str.size() % 2 != 0) { ESP_LOGW("deye", "Hex impar"); return NAN; }
      auto h = [](char c) -> uint8_t {
        if (c >= '0' && c <= '9') return c - '0';
        if (c >= 'A' && c <= 'F') return c - 'A' + 10;
        if (c >= 'a' && c <= 'f') return c - 'a' + 10;
        return 0;
      };
      std::vector<uint8_t> buf;
      buf.reserve(hex_str.size() / 2);
      for (size_t i = 0; i < hex_str.size(); i += 2)
        buf.push_back((h(hex_str[i]) << 4) | h(hex_str[i+1]));
      if (buf.size() < 3 || buf[0] != 0x01 || buf[1] != 0x03) { ESP_LOGW("deye", "No es Modbus"); return NAN; }
      uint8_t byte_count = buf[2];
      if (buf.size() < (size_t)(3 + byte_count + 2)) { ESP_LOGW("deye", "Frame incompleto"); return NAN; }
      auto reg = [&](int n) -> uint16_t { size_t p = 3 + n * 2; return ((uint16_t)buf[p] << 8) | buf[p + 1]; };
      auto reg_s = [&](int n) -> int16_t { return (int16_t)reg(n); };

      switch (id(current_block)) {
        case 1:  // Block C 0x006C
          id(daily_production).publish_state(reg(0) * 0.1f);
          id(pv1_voltage).publish_state(reg(1) * 0.1f);
          id(pv1_current).publish_state(reg(2) * 0.1f);
          id(pv2_voltage).publish_state(reg(3) * 0.1f);
          id(pv2_current).publish_state(reg(4) * 0.1f);
          break;
        case 2:  // Block Q 0x00BA — PV power
          id(pv1_power).publish_state((float)reg(0));
          id(pv2_power).publish_state((float)reg(1));
          break;
        case 3:  // 0x0073 devuelve 0 → PV total = PV1+PV2
          if (id(pv1_power).has_state() && id(pv2_power).has_state())
            id(total_pv_power).publish_state(id(pv1_power).state + id(pv2_power).state);
          break;
        case 4:  // Block D 0x0096
          id(grid_voltage_1).publish_state(reg(0) * 0.1f);
          id(grid_voltage_2).publish_state(reg(1) * 0.1f);
          break;
        case 5:  // 0x00A9 Grid Power (signed; +export/-import)
          id(grid_power).publish_state((float)reg_s(0));
          break;
        case 6:  // Block E 0x009D
          id(load_l1_voltage).publish_state(reg(0) * 0.1f);
          break;
        case 7:  // Block F 0x00A4
          id(current_l1).publish_state(reg_s(0) * 0.01f);
          id(internal_ct_l1_power).publish_state((float)reg_s(3));
          id(external_ct_l1_power).publish_state((float)reg_s(6));
          id(inverter_l1_power).publish_state((float)reg_s(11));
          break;
        case 8:  // Block G 0x00B0
          id(total_load_power).publish_state((float)reg_s(0));
          id(load_l1_power).publish_state((float)reg_s(2));
          break;
        case 9:  // Block H 0x00B6
          id(battery_temperature).publish_state((reg(0) - 1000) * 0.1f);
          id(battery_voltage).publish_state(reg(1) * 0.01f);
          id(battery_soc).publish_state((float)reg(2));
          // SIGNO CORREGIDO para esta unidad (AM2-P): raw +=descarga, así que negamos
          // para que la convención sea +=carga / -=descarga.
          id(battery_power).publish_state((float)(-reg_s(8)));
          id(battery_current).publish_state(-reg_s(9) * 0.01f);
          id(load_frequency).publish_state(reg(10) * 0.01f);
          break;
        case 10: // Block B 0x005A
          id(dc_temperature).publish_state((reg(0) - 1000) * 0.1f);
          id(ac_temperature).publish_state((reg(1) - 1000) * 0.1f);
          break;
        case 11: // Block A 0x0054
          id(daily_load_consumption).publish_state(reg(0) * 0.1f);
          id(total_load_consumption).publish_state(((uint32_t)reg(2) << 16 | reg(1)) * 0.1f);
          break;
        case 12: // 0x0060 Total Production DWORD
          id(total_production).publish_state(((uint32_t)reg(1) << 16 | reg(0)) * 0.1f);
          break;
        case 13: // 0x0035 Max Solar Power
          id(max_solar_power).publish_state((float)reg(0));
          break;
        case 14: // Block O 0x0046 (contadores nativos)
          id(daily_battery_charge).publish_state(reg(0) * 0.1f);
          id(daily_battery_discharge).publish_state(reg(1) * 0.1f);
          id(total_battery_charge).publish_state(((uint32_t)reg(3) << 16 | reg(2)) * 0.1f);
          id(total_battery_discharge).publish_state(((uint32_t)reg(5) << 16 | reg(4)) * 0.1f);
          id(daily_energy_bought).publish_state(reg(6) * 0.1f);
          id(daily_energy_sold).publish_state(reg(7) * 0.1f);
          id(total_energy_bought).publish_state(reg(8) * 0.1f);
          id(total_energy_sold).publish_state(((uint32_t)reg(12) << 16 | reg(11)) * 0.1f);
          break;
        case 15: // Block I 0x00C8
          id(battery_equalization_voltage).publish_state(reg(0) * 0.01f);
          id(battery_absorption_voltage).publish_state(reg(1) * 0.01f);
          id(battery_float_voltage).publish_state(reg(2) * 0.01f);
          id(battery_capacity).publish_state((float)reg(4));
          id(battery_empty_voltage).publish_state(reg(5) * 0.01f);
          id(max_charge_current).publish_state((float)reg(10));
          id(max_discharge_current).publish_state((float)reg(11));
          break;
        case 16: // Block J 0x00D9
          id(battery_shutdown_pct).publish_state((float)reg(0));
          id(battery_restart_pct).publish_state((float)reg(1));
          id(battery_low_pct).publish_state((float)reg(2));
          id(battery_shutdown_voltage).publish_state(reg(3) * 0.01f);
          id(battery_restart_voltage).publish_state(reg(4) * 0.01f);
          id(battery_low_voltage).publish_state(reg(5) * 0.01f);
          break;
        /* ── CASOS SELLING/TOU DESACTIVADOS — quita esta línea y la de cierre para reactivar ──
        case 17: { // Block K 0x00F8
          id(tou_enabled).publish_state((float)reg(0));
          auto fmt_time = [](uint16_t raw) -> std::string {
            char b[6]; snprintf(b, sizeof(b), "%d:%02d", raw / 100, raw % 100); return std::string(b);
          };
          id(selling_time_1).publish_state(fmt_time(reg(2)));
          id(selling_time_2).publish_state(fmt_time(reg(3)));
          id(selling_time_3).publish_state(fmt_time(reg(4)));
          id(selling_time_4).publish_state(fmt_time(reg(5)));
          id(selling_time_5).publish_state(fmt_time(reg(6)));
          id(selling_time_6).publish_state(fmt_time(reg(7)));
          break;
        }
        case 18: // Block L 0x0100
          id(selling_power_1).publish_state((float)reg(0));
          id(selling_power_2).publish_state((float)reg(1));
          id(selling_power_3).publish_state((float)reg(2));
          id(selling_power_4).publish_state((float)reg(3));
          id(selling_power_5).publish_state((float)reg(4));
          id(selling_power_6).publish_state((float)reg(5));
          break;
        case 19: // Block M 0x010C
          id(selling_soc_1).publish_state((float)reg(0));
          id(selling_soc_2).publish_state((float)reg(1));
          id(selling_soc_3).publish_state((float)reg(2));
          id(selling_soc_4).publish_state((float)reg(3));
          id(selling_soc_5).publish_state((float)reg(4));
          id(selling_soc_6).publish_state((float)reg(5));
          break;
        case 20: // Block N 0x0112
          id(selling_charge_flag_1).publish_state((float)reg(0));
          id(selling_charge_flag_2).publish_state((float)reg(1));
          id(selling_charge_flag_3).publish_state((float)reg(2));
          id(selling_charge_flag_4).publish_state((float)reg(3));
          id(selling_charge_flag_5).publish_state((float)reg(4));
          id(selling_charge_flag_6).publish_state((float)reg(5));
          break;
        case 21: // Block P 0x0106
          id(selling_voltage_1).publish_state(reg(0) * 0.01f);
          id(selling_voltage_2).publish_state(reg(1) * 0.01f);
          id(selling_voltage_3).publish_state(reg(2) * 0.01f);
          id(selling_voltage_4).publish_state(reg(3) * 0.01f);
          id(selling_voltage_5).publish_state(reg(4) * 0.01f);
          id(selling_voltage_6).publish_state(reg(5) * 0.01f);
          break;
        ── fin casos desactivados ── */
        case 22: // Block R 0x0138 BMS
          id(bms_charging_voltage).publish_state(reg(0) * 0.01f);
          id(bms_discharge_voltage).publish_state(reg(1) * 0.01f);
          id(bms_charge_current_limit).publish_state((float)reg(2));
          id(bms_discharge_current_limit).publish_state((float)reg(3));
          id(bms_soc).publish_state((float)reg(4));
          id(bms_voltage).publish_state(reg(5) * 0.01f);
          id(bms_current).publish_state((float)reg_s(6));
          id(bms_temperature).publish_state((reg(7) - 1000) * 0.1f);
          break;
        case 23: // 0x0028 Active Power Regulation
          id(active_power_regulation).publish_state(reg(0) * 0.1f);
          break;
      }
      id(last_successful_poll) = millis();
      return NAN;

  # ── Producción / PV ──
  - {platform: template, name: 'Total Production', id: total_production, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 1}
  - {platform: template, name: 'Daily Production', id: daily_production, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 1}
  - {platform: template, name: 'PV1 Voltage', id: pv1_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 1}
  - {platform: template, name: 'PV1 Current', id: pv1_current, unit_of_measurement: 'A', device_class: current, state_class: measurement, accuracy_decimals: 1}
  - {platform: template, name: 'PV1 Power', id: pv1_power, unit_of_measurement: 'W', device_class: power, state_class: measurement, accuracy_decimals: 0}
  - {platform: template, name: 'PV2 Voltage', id: pv2_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 1}
  - {platform: template, name: 'PV2 Current', id: pv2_current, unit_of_measurement: 'A', device_class: current, state_class: measurement, accuracy_decimals: 1}
  - {platform: template, name: 'PV2 Power', id: pv2_power, unit_of_measurement: 'W', device_class: power, state_class: measurement, accuracy_decimals: 0}
  - {platform: template, name: 'Total PV Power', id: total_pv_power, unit_of_measurement: 'W', device_class: power, state_class: measurement, accuracy_decimals: 0}
  - {platform: template, name: 'Max Solar Power', id: max_solar_power, unit_of_measurement: 'W', device_class: power, state_class: measurement, accuracy_decimals: 0, entity_category: diagnostic}

  # ── Red ──
  - {platform: template, name: 'Grid Voltage 1', id: grid_voltage_1, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 1}
  - {platform: template, name: 'Grid Voltage 2', id: grid_voltage_2, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 1}
  - {platform: template, name: 'Grid Power', id: grid_power, unit_of_measurement: 'W', device_class: power, state_class: measurement, accuracy_decimals: 0}
  - {platform: template, name: 'Internal CT L1 Power', id: internal_ct_l1_power, unit_of_measurement: 'W', device_class: power, state_class: measurement, accuracy_decimals: 0}
  - {platform: template, name: 'External CT L1 Power', id: external_ct_l1_power, unit_of_measurement: 'W', device_class: power, state_class: measurement, accuracy_decimals: 0}

  # ── Carga ──
  - {platform: template, name: 'Daily Load Consumption', id: daily_load_consumption, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 1}
  - {platform: template, name: 'Total Load Consumption', id: total_load_consumption, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 1}
  - {platform: template, name: 'Load L1 Voltage', id: load_l1_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 1}
  - {platform: template, name: 'Load L1 Current', id: current_l1, unit_of_measurement: 'A', device_class: current, state_class: measurement, accuracy_decimals: 2}
  - {platform: template, name: 'Total Load Power', id: total_load_power, unit_of_measurement: 'W', device_class: power, state_class: measurement, accuracy_decimals: 0}
  - {platform: template, name: 'Load L1 Power', id: load_l1_power, unit_of_measurement: 'W', device_class: power, state_class: measurement, accuracy_decimals: 0}
  - {platform: template, name: 'Inverter Output Power', id: inverter_l1_power, unit_of_measurement: 'W', device_class: power, state_class: measurement, accuracy_decimals: 0}

  # ── Temperaturas / frecuencia ──
  - {platform: template, name: 'DC Temperature', id: dc_temperature, unit_of_measurement: '°C', device_class: temperature, state_class: measurement, accuracy_decimals: 1}
  - {platform: template, name: 'AC Temperature', id: ac_temperature, unit_of_measurement: '°C', device_class: temperature, state_class: measurement, accuracy_decimals: 1}
  - {platform: template, name: 'Load Frequency', id: load_frequency, unit_of_measurement: 'Hz', device_class: frequency, state_class: measurement, accuracy_decimals: 2}

  # ── Batería ──
  - {platform: template, name: 'Battery Temperature', id: battery_temperature, unit_of_measurement: '°C', device_class: temperature, state_class: measurement, accuracy_decimals: 1}
  - {platform: template, name: 'Battery Voltage', id: battery_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 2}
  - {platform: template, name: 'Battery SOC', id: battery_soc, unit_of_measurement: '%', device_class: battery, state_class: measurement, accuracy_decimals: 0}
  - {platform: template, name: 'Battery Power', id: battery_power, unit_of_measurement: 'W', device_class: power, state_class: measurement, accuracy_decimals: 0}
  - {platform: template, name: 'Battery Current', id: battery_current, unit_of_measurement: 'A', device_class: current, state_class: measurement, accuracy_decimals: 2}
  - {platform: template, name: 'Battery Capacity', id: battery_capacity, unit_of_measurement: 'Ah', state_class: measurement, accuracy_decimals: 0, entity_category: diagnostic}
  - {platform: template, name: 'Battery Empty Voltage', id: battery_empty_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 2, entity_category: diagnostic}
  - {platform: template, name: 'Max Charge Current', id: max_charge_current, unit_of_measurement: 'A', device_class: current, state_class: measurement, accuracy_decimals: 0, entity_category: diagnostic}
  - {platform: template, name: 'Max Discharge Current', id: max_discharge_current, unit_of_measurement: 'A', device_class: current, state_class: measurement, accuracy_decimals: 0, entity_category: diagnostic}
  - {platform: template, name: 'Battery Equalization Voltage', id: battery_equalization_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 2, entity_category: diagnostic}
  - {platform: template, name: 'Battery Absorption Voltage', id: battery_absorption_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 2, entity_category: diagnostic}
  - {platform: template, name: 'Battery Float Voltage', id: battery_float_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 2, entity_category: diagnostic}
  - {platform: template, name: 'Battery Shutdown %', id: battery_shutdown_pct, unit_of_measurement: '%', state_class: measurement, accuracy_decimals: 0, entity_category: diagnostic}
  - {platform: template, name: 'Battery Restart %', id: battery_restart_pct, unit_of_measurement: '%', state_class: measurement, accuracy_decimals: 0, entity_category: diagnostic}
  - {platform: template, name: 'Battery Low %', id: battery_low_pct, unit_of_measurement: '%', state_class: measurement, accuracy_decimals: 0, entity_category: diagnostic}
  - {platform: template, name: 'Battery Shutdown Voltage', id: battery_shutdown_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 2, entity_category: diagnostic}
  - {platform: template, name: 'Battery Restart Voltage', id: battery_restart_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 2, entity_category: diagnostic}
  - {platform: template, name: 'Battery Low Voltage', id: battery_low_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 2, entity_category: diagnostic}

  # ── Contadores de energía nativos ──
  - {platform: template, name: 'Daily Battery Charge', id: daily_battery_charge, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 1}
  - {platform: template, name: 'Daily Battery Discharge', id: daily_battery_discharge, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 1}
  - {platform: template, name: 'Total Battery Charge', id: total_battery_charge, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 1}
  - {platform: template, name: 'Total Battery Discharge', id: total_battery_discharge, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 1}
  - {platform: template, name: 'Daily Energy Bought', id: daily_energy_bought, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 1}
  - {platform: template, name: 'Daily Energy Sold', id: daily_energy_sold, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 1}
  - {platform: template, name: 'Total Energy Bought', id: total_energy_bought, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 1}
  - {platform: template, name: 'Total Energy Sold', id: total_energy_sold, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 1}

  # ── BMS ──
  - {platform: template, name: 'BMS Charging Voltage', id: bms_charging_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 2, entity_category: diagnostic}
  - {platform: template, name: 'BMS Discharge Voltage', id: bms_discharge_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 2, entity_category: diagnostic}
  - {platform: template, name: 'BMS Charge Current Limit', id: bms_charge_current_limit, unit_of_measurement: 'A', device_class: current, state_class: measurement, accuracy_decimals: 0, entity_category: diagnostic}
  - {platform: template, name: 'BMS Discharge Current Limit', id: bms_discharge_current_limit, unit_of_measurement: 'A', device_class: current, state_class: measurement, accuracy_decimals: 0, entity_category: diagnostic}
  - {platform: template, name: 'BMS SOC', id: bms_soc, unit_of_measurement: '%', device_class: battery, state_class: measurement, accuracy_decimals: 0, entity_category: diagnostic}
  - {platform: template, name: 'BMS Voltage', id: bms_voltage, unit_of_measurement: 'V', device_class: voltage, state_class: measurement, accuracy_decimals: 2, entity_category: diagnostic}
  - {platform: template, name: 'BMS Current', id: bms_current, unit_of_measurement: 'A', device_class: current, state_class: measurement, accuracy_decimals: 0, entity_category: diagnostic}
  - {platform: template, name: 'BMS Temperature', id: bms_temperature, unit_of_measurement: '°C', device_class: temperature, state_class: measurement, accuracy_decimals: 1, entity_category: diagnostic}
  - {platform: template, name: 'Active Power Regulation', id: active_power_regulation, unit_of_measurement: '%', state_class: measurement, accuracy_decimals: 1, entity_category: diagnostic}
  # ── SENSORES SELLING/TOU DESACTIVADOS (descomentar para reactivar) ──
  # - {platform: template, name: 'Time of Use Enabled', id: tou_enabled, state_class: measurement, accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Power 1', id: selling_power_1, unit_of_measurement: 'W', device_class: power, accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Power 2', id: selling_power_2, unit_of_measurement: 'W', device_class: power, accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Power 3', id: selling_power_3, unit_of_measurement: 'W', device_class: power, accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Power 4', id: selling_power_4, unit_of_measurement: 'W', device_class: power, accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Power 5', id: selling_power_5, unit_of_measurement: 'W', device_class: power, accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Power 6', id: selling_power_6, unit_of_measurement: 'W', device_class: power, accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling SOC 1', id: selling_soc_1, unit_of_measurement: '%', accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling SOC 2', id: selling_soc_2, unit_of_measurement: '%', accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling SOC 3', id: selling_soc_3, unit_of_measurement: '%', accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling SOC 4', id: selling_soc_4, unit_of_measurement: '%', accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling SOC 5', id: selling_soc_5, unit_of_measurement: '%', accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling SOC 6', id: selling_soc_6, unit_of_measurement: '%', accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Charge Flag 1', id: selling_charge_flag_1, accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Charge Flag 2', id: selling_charge_flag_2, accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Charge Flag 3', id: selling_charge_flag_3, accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Charge Flag 4', id: selling_charge_flag_4, accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Charge Flag 5', id: selling_charge_flag_5, accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Charge Flag 6', id: selling_charge_flag_6, accuracy_decimals: 0, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Voltage 1', id: selling_voltage_1, unit_of_measurement: 'V', device_class: voltage, accuracy_decimals: 2, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Voltage 2', id: selling_voltage_2, unit_of_measurement: 'V', device_class: voltage, accuracy_decimals: 2, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Voltage 3', id: selling_voltage_3, unit_of_measurement: 'V', device_class: voltage, accuracy_decimals: 2, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Voltage 4', id: selling_voltage_4, unit_of_measurement: 'V', device_class: voltage, accuracy_decimals: 2, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Voltage 5', id: selling_voltage_5, unit_of_measurement: 'V', device_class: voltage, accuracy_decimals: 2, entity_category: diagnostic}
  # - {platform: template, name: 'Selling Voltage 6', id: selling_voltage_6, unit_of_measurement: 'V', device_class: voltage, accuracy_decimals: 2, entity_category: diagnostic}

  # ── Energía integrada (Riemann) para el Energy Dashboard de HA ──
  # Fuentes internas pre-filtradas (no negativas). battery_power: +=carga / -=descarga.
  - platform: template
    name: 'Grid Export Power'
    id: grid_export_power
    internal: true
    unit_of_measurement: 'W'
    device_class: power
    accuracy_decimals: 0
    update_interval: 30s
    lambda: 'if (id(grid_power).has_state()) return id(grid_power).state > 0 ? id(grid_power).state : 0.0f; return NAN;'
  - platform: template
    name: 'Grid Import Power'
    id: grid_import_power
    internal: true
    unit_of_measurement: 'W'
    device_class: power
    accuracy_decimals: 0
    update_interval: 30s
    lambda: 'if (id(grid_power).has_state()) return id(grid_power).state < 0 ? -id(grid_power).state : 0.0f; return NAN;'
  - platform: template
    name: 'Battery Charge Power'
    id: battery_charge_power
    internal: true
    unit_of_measurement: 'W'
    device_class: power
    accuracy_decimals: 0
    update_interval: 30s
    lambda: 'if (id(battery_power).has_state()) return id(battery_power).state > 0 ? id(battery_power).state : 0.0f; return NAN;'
  - platform: template
    name: 'Battery Discharge Power'
    id: battery_discharge_power
    internal: true
    unit_of_measurement: 'W'
    device_class: power
    accuracy_decimals: 0
    update_interval: 30s
    lambda: 'if (id(battery_power).has_state()) return id(battery_power).state < 0 ? -id(battery_power).state : 0.0f; return NAN;'
  - {platform: integration, name: 'Grid Export Energy', id: grid_export_energy, sensor: grid_export_power, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 3, time_unit: h, filters: [{multiply: 0.001}]}
  - {platform: integration, name: 'Grid Import Energy', id: grid_import_energy, sensor: grid_import_power, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 3, time_unit: h, filters: [{multiply: 0.001}]}
  - {platform: integration, name: 'Battery Charge Energy', id: battery_charge_energy, sensor: battery_charge_power, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 3, time_unit: h, filters: [{multiply: 0.001}]}
  - {platform: integration, name: 'Battery Discharge Energy', id: battery_discharge_energy, sensor: battery_discharge_power, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 3, time_unit: h, filters: [{multiply: 0.001}]}
  - {platform: integration, name: 'PV1 Energy', id: pv1_energy, sensor: pv1_power, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 3, time_unit: h, filters: [{multiply: 0.001}]}
  - {platform: integration, name: 'PV2 Energy', id: pv2_energy, sensor: pv2_power, unit_of_measurement: 'kWh', device_class: energy, state_class: total_increasing, accuracy_decimals: 3, time_unit: h, filters: [{multiply: 0.001}]}

################################################################################
# Tiempos de venta (texto HH:MM) — DESACTIVADO (descomentar para reactivar)
################################################################################
# text_sensor:
#   - {platform: template, name: 'Selling Time 1', id: selling_time_1, entity_category: diagnostic}
#   - {platform: template, name: 'Selling Time 2', id: selling_time_2, entity_category: diagnostic}
#   - {platform: template, name: 'Selling Time 3', id: selling_time_3, entity_category: diagnostic}
#   - {platform: template, name: 'Selling Time 4', id: selling_time_4, entity_category: diagnostic}
#   - {platform: template, name: 'Selling Time 5', id: selling_time_5, entity_category: diagnostic}
#   - {platform: template, name: 'Selling Time 6', id: selling_time_6, entity_category: diagnostic}

################################################################################
# Control mínimo del ciclo BLE
################################################################################
number:
  # Segundos que el ESP32 permanece DESCONECTADO entre polls.
  # 0 = conexión permanente (acapara el BLE, la nube NO sincroniza).
  # 110 (por defecto) = poll cada ~2 min, BLE ocupado ~10% → nube tiene margen.
  # Baja a ~50 para poll cada ~1 min. Ajustable en vivo desde HA.
  - platform: template
    name: 'BLE Reconnect Interval'
    id: ble_reconnect_interval
    unit_of_measurement: 's'
    icon: mdi:bluetooth-connect
    min_value: 0
    max_value: 600
    step: 10
    restore_value: true
    initial_value: 110
    optimistic: true
    on_value:
      then:
        - if:
            condition:
              lambda: 'return x == 0 && !id(deye_ble).connected();'
            then:
              - lambda: 'id(ble_reconnect_counter) = 0;'
              - switch.turn_on: inverter_ble_switch

switch:
  - platform: ble_client
    ble_client_id: deye_ble
    name: 'Inverter BLE Connection'
    id: inverter_ble_switch
    icon: mdi:bluetooth-connect
    restore_mode: RESTORE_DEFAULT_ON
    internal: true

For anyone looking for a different solution and ending up in this thread:

I emailed Deye support and they remotely upgraded my logger to 1.7.2. After that, I enabled Modbus TCP and configured Solarman in HA.