This page explains how to run Predbat against a Hanchu iESS inverter using this local Bluetooth integration instead of the Hanchu cloud. It has no dependency on the Hanchu cloud and keeps working if cloud connectivity is down. If you also run the cloud-based Predbat setup, you can run both side by side (for example cloud read-only for monitoring, BLE live for control) by giving each Predbat instance its own prefix.
Status: this is a community setup, not part of the official Predbat documentation, and is not supported by Hanchu or the Predbat project. This page is the maintained reference. Test carefully and keep Predbat in read-only mode (
switch.predbat_set_read_only) until you have verified each step.
TCT / TDT fields are not used here. The cloud integration's
TCT_START_1/TDT_START_1fields need raw seconds. This integration writes slot times through ordinarytime.set_valuecalls, which takeHH:MM:SSdirectly, so no seconds conversion is needed.
Control uses Predbat's generic Service API. BLE has no single-call equivalent to the cloud's iotSet / device_control, so Predbat's four service hooks (charge_start, charge_stop, discharge_start, discharge_stop) point at a bridge script. The script stages the charge/discharge slot 1 time entities with time.set_value, then calls hanchu_ess_ble.confirm_write to flush them to the device in a single BLE connection, with a retry and a notification on failure.
Install this integration via HACS and configure it — this requires a Bluetooth proxy within range of the inverter (see the README for hardware recommendations; an ESPHome-based proxy such as the M5Stack Atom Lite has been confirmed reliable). Confirm that inverter sensors are appearing in Home Assistant, and that you can control the inverter's number/select/time entities manually via the Confirm Write button, before proceeding.
Create the following helpers in Home Assistant (Settings → Devices & Services → Helpers):
Toggle helpers (toggle type):
| Entity ID | Name |
|---|---|
input_boolean.predbat_ble_charge_start |
Predbat BLE Charge Start |
input_boolean.predbat_ble_discharge_start |
Predbat BLE Discharge Start |
Text helper (text type):
| Entity ID | Name |
|---|---|
input_text.hanchu_ble_last_mode_action |
Hanchu BLE Last Mode Action |
input_text.hanchu_ble_last_mode_action tracks the last mode successfully applied so the bridge script can skip a redundant write when Predbat reasserts a state that is already active.
All four of Predbat's service hooks call the same script, script.hanchu_ble_set_state_queued, passing a mode_action field to indicate which state to apply. The script runs with mode: queued, so if Predbat fires two calls close together, Home Assistant queues the second behind the first rather than letting two BLE writes race each other.
Create a new script (Settings → Automations & Scenes → Scripts → Add Script → Edit in YAML) and paste the following, replacing YOURDEVICE with your inverter's entity slug as it appears in your HA entity IDs (e.g. hc_l110ym56g0915), and replacing notify.notify with your own mobile notification service:
alias: Hanchu BLE Set State Queued
mode: queued
fields:
mode_action:
required: true
selector:
select:
options:
- charge_start
- charge_stop
- discharge_start
- discharge_stop
sequence:
- variables:
act: >-
{% if mode_action is defined %}{{ mode_action }}
{% elif data is defined and data.mode_action is defined %}{{ data.mode_action }}
{% else %}unknown{% endif %}
- if:
- condition: template
value_template: "{{ act == states('input_text.hanchu_ble_last_mode_action') }}"
then:
- stop: "No change — same action already applied, skipping BLE write"
- variables:
# Predbat's sensors already report HH:MM:SS, and time.set_value takes
# HH:MM:SS directly — no seconds conversion needed, unlike the cloud
# integration's TCT/TDT fields which require raw seconds.
l005_charge_start: >-
{{ states('sensor.predbat_ble_hc_0_charge_start_time') if act ==
'charge_start' else '00:00:00' }}
l006_charge_end: >-
{{ states('sensor.predbat_ble_hc_0_charge_end_time') if act ==
'charge_start' else '00:00:00' }}
l011_discharge_start: >-
{{ states('sensor.predbat_ble_hc_0_discharge_start_time') if act ==
'discharge_start' else '00:00:00' }}
l012_discharge_end: >-
{{ states('sensor.predbat_ble_hc_0_discharge_end_time') if act ==
'discharge_start' else '00:00:00' }}
- action: time.set_value
target:
entity_id: time.YOURDEVICE_charge_slot_1_start
data:
time: "{{ l005_charge_start }}"
- action: time.set_value
target:
entity_id: time.YOURDEVICE_charge_slot_1_end
data:
time: "{{ l006_charge_end }}"
- action: time.set_value
target:
entity_id: time.YOURDEVICE_discharge_slot_1_start
data:
time: "{{ l011_discharge_start }}"
- action: time.set_value
target:
entity_id: time.YOURDEVICE_discharge_slot_1_end
data:
time: "{{ l012_discharge_end }}"
- action: hanchu_ess_ble.confirm_write
response_variable: result
- if:
- condition: template
value_template: "{{ not result.success }}"
then:
- delay:
seconds: 5
- action: hanchu_ess_ble.confirm_write
response_variable: result2
- if:
- condition: template
value_template: "{{ not result2.success }}"
then:
- action: notify.notify # Replace with your own notification service
data:
title: "⚠️ Hanchu BLE {{ act }} FAILED"
message: >-
{{ act }} write failed after retry ({{ result2.message }})
— check manually.
- stop: "Both attempts failed — leaving last_mode_action unchanged for retry"
- action: input_text.set_value
target:
entity_id: input_text.hanchu_ble_last_mode_action
data:
value: "{{ act }}"
- choose:
- conditions: "{{ act == 'charge_start' }}"
sequence:
- action: input_boolean.turn_on
entity_id: input_boolean.predbat_ble_charge_start
- conditions: "{{ act == 'charge_stop' }}"
sequence:
- action: input_boolean.turn_off
entity_id: input_boolean.predbat_ble_charge_start
- conditions: "{{ act == 'discharge_start' }}"
sequence:
- action: input_boolean.turn_on
entity_id: input_boolean.predbat_ble_discharge_start
- conditions: "{{ act == 'discharge_stop' }}"
sequence:
- action: input_boolean.turn_off
entity_id: input_boolean.predbat_ble_discharge_startThe script always stages all four charge/discharge slot-1 time entities on every call, zeroing whichever pair is not the active mode. This keeps charge and discharge mutually exclusive on the device without relying on separate stop/start calls landing in the right order, and mirrors the logic used by the cloud integration's Predbat bridge.
Note on register naming: unlike the cloud integration, the BLE integration writes directly to raw Hanchu register codes (L005/L006 for Charge Slot 1 Start/End, L011/L012 for Discharge Slot 1 Start/End) rather than the cloud API's TCT_START_1/TDT_START_1-style field names. The variable names in the script above (l005_charge_start etc.) reflect this — if you're comparing against the cloud script, map by register/entity, not by field name.
As with the cloud integration, Predbat may revise its planned charge or discharge end time mid-window without issuing a new charge_start/discharge_start service call. Without this automation, the inverter would continue using the original end time written at the start of the window.
Create a new automation (Settings → Automations & Scenes → Automations → Add Automation → Edit in YAML) and paste the following, replacing YOURDEVICE with your inverter's entity slug:
alias: Predbat BLE - Update Hanchu Charge/Discharge Window Times
description: >
Watches Predbat's BLE charge and discharge end time sensors and updates the
Hanchu BLE time slots when they change mid-window during an active charge or
discharge session.
triggers:
- trigger: state
entity_id: sensor.predbat_ble_hc_0_charge_end_time
id: charge_end_changed
- trigger: state
entity_id: sensor.predbat_ble_hc_0_discharge_end_time
id: discharge_end_changed
conditions:
- condition: template
value_template: >-
{{ trigger.to_state.state not in ['unknown', 'unavailable', '00:00:00'] }}
actions:
- choose:
- conditions:
- condition: trigger
id: charge_end_changed
- condition: template
value_template: "{{ is_state('input_boolean.predbat_ble_charge_start', 'on') }}"
sequence:
- variables:
charge_start_time: "{{ states('sensor.predbat_ble_hc_0_charge_start_time') }}"
charge_end_time: "{{ trigger.to_state.state }}"
- action: time.set_value
target:
entity_id: time.YOURDEVICE_charge_slot_1_start
data:
time: "{{ charge_start_time }}"
- action: time.set_value
target:
entity_id: time.YOURDEVICE_charge_slot_1_end
data:
time: "{{ charge_end_time }}"
- action: time.set_value
target:
entity_id: time.YOURDEVICE_discharge_slot_1_start
data:
time: "00:00:00"
- action: time.set_value
target:
entity_id: time.YOURDEVICE_discharge_slot_1_end
data:
time: "00:00:00"
- action: hanchu_ess_ble.confirm_write
response_variable: result
- if:
- condition: template
value_template: "{{ not result.success }}"
then:
- delay:
seconds: 5
- action: hanchu_ess_ble.confirm_write
response_variable: result2
- if:
- condition: template
value_template: "{{ not result2.success }}"
then:
- action: notify.notify # Replace with your own notification service
data:
title: "⚠️ Hanchu BLE charge end-time update FAILED"
message: >-
Write failed after retry ({{ result2.message }}) —
check manually.
- conditions:
- condition: trigger
id: discharge_end_changed
- condition: template
value_template: "{{ is_state('input_boolean.predbat_ble_discharge_start', 'on') }}"
sequence:
- variables:
discharge_start_time: "{{ states('sensor.predbat_ble_hc_0_discharge_start_time') }}"
discharge_end_time: "{{ trigger.to_state.state }}"
- action: time.set_value
target:
entity_id: time.YOURDEVICE_charge_slot_1_start
data:
time: "00:00:00"
- action: time.set_value
target:
entity_id: time.YOURDEVICE_charge_slot_1_end
data:
time: "00:00:00"
- action: time.set_value
target:
entity_id: time.YOURDEVICE_discharge_slot_1_start
data:
time: "{{ discharge_start_time }}"
- action: time.set_value
target:
entity_id: time.YOURDEVICE_discharge_slot_1_end
data:
time: "{{ discharge_end_time }}"
- action: hanchu_ess_ble.confirm_write
response_variable: result
- if:
- condition: template
value_template: "{{ not result.success }}"
then:
- delay:
seconds: 5
- action: hanchu_ess_ble.confirm_write
response_variable: result2
- if:
- condition: template
value_template: "{{ not result2.success }}"
then:
- action: notify.notify # Replace with your own notification service
data:
title: "⚠️ Hanchu BLE discharge end-time update FAILED"
message: >-
Write failed after retry ({{ result2.message }}) —
check manually.
mode: queuedUnlike the cloud integration, the BLE integration does not expose daily load, PV, grid import, or grid export energy totals natively — only live instantaneous power. Predbat needs all four as daily-resetting kWh totals, so each is derived using two standard Home Assistant helpers chained together: an Integral (Riemann sum) sensor that accumulates a live power sensor into kWh, feeding a Utility Meter that resets it to zero daily.
Worked example for grid import/export (replicate the same pattern for load and PV, pointed at your load/PV power sensors instead):
-
Create two template sensors in
configuration.yamlto split the bidirectional grid power sensor into import-only and export-only wattage (replaceYOURDEVICEwith your entity slug):template: - sensor: - name: "Hanchu BLE Grid Import Power" unique_id: YOURDEVICE_grid_import_power unit_of_measurement: "W" state_class: measurement state: > {% set p = states('sensor.YOURDEVICE_p644') | float(0) %} {{ [0, p] | max }} - name: "Hanchu BLE Grid Export Power" unique_id: YOURDEVICE_grid_export_power unit_of_measurement: "W" state_class: measurement state: > {% set p = states('sensor.YOURDEVICE_p644') | float(0) %} {{ [0, -p] | max }}
-
Create an Integral sensor helper (Settings → Devices & Services → Helpers → Add Helper → Integral sensor) for each: Input sensor = the corresponding power sensor above, Integration method = Trapezoidal rule, Metric prefix = kilo, Time unit = Hours. Name them e.g. "Hanchu BLE Grid Import Total" / "Hanchu BLE Grid Export Total".
-
Create a Utility Meter helper for each Integral sensor: Input sensor = the Integral sensor from step 2, Meter reset cycle = Daily, with both "Periodically resetting" and "Sensor always available" enabled. Name them e.g. "Hanchu BLE Grid Import Today" / "Hanchu BLE Grid Export Today" — these are the entities referenced as
import_today/export_todayinapps.yaml.
Repeat steps 1-3 for Load Power and PV Power (referencing sensor.YOURDEVICE_load_power and the appropriate PV power sensor for your setup — AC-coupled systems should use sensor.YOURDEVICE_p237) to produce load_today and pv_today, with no need for the import/export-style sign-splitting template sensor since both are already one-directional.
- Replace
YOURDEVICEthroughout the template with your device's entity slug as it appears in your HA entity IDs - Start from
hanchu_ble.yamlin this folder and save it as yourapps.yaml - Set
import_today/export_today/load_today/pv_todayto the Utility Meter entities created in Step 4 - Adjust
inverter_limit,inverter_limit_charge,inverter_limit_discharge,inverter_limit_exportandbattery_rate_maxto match your inverter and battery rated capacity in watts - If running this as a standalone Predbat instance (e.g. in its own Docker container) rather than a Home Assistant add-on, uncomment and set
ha_url/ha_keyto your HA instance's address and a Long-Lived Access Token — verify the token works with a directcurlrequest to your HA instance's/api/endpoint before saving it intoapps.yaml, since Predbat will crash-loop on a bad or malformed token with only a generic "HA interface not found" error to go on - Give this instance its own unique
prefix(e.g.predbat_ble) if you're running it alongside an existing cloud-based Predbat instance, so entity names for the two don't collide - Delete the
template: Trueline to allow Predbat to start - Configure your energy rates — see Energy Rates
- No soc_kw template sensor needed: unlike the cloud integration, Predbat derives this automatically from
soc_percent×soc_max— no separate template sensor is required for BLE. - Skipping redundant calls: as with the cloud integration,
input_text.hanchu_ble_last_mode_actionskips the write entirely when the requested mode is already the last one successfully applied, tracked only after a confirmed successful write. - No charge/discharge enable toggle: as with the cloud integration, Hanchu has no explicit enable/disable for charge or discharge — the slot zeroing mechanism (setting both start and end to
00:00:00) is the disable method. - Mid-window time updates: handled by the automation in Step 3, the same purpose as the equivalent cloud automation.
- Not device-level atomic: each staged register is still written as its own request within the single BLE connection
confirm_writeopens, one after another — see the README for the full detail on this and other known limitations.