Skip to content

Latest commit

 

History

History
206 lines (168 loc) · 19.8 KB

hanchu-ble-local-protocol-mapping.md

File metadata and controls

206 lines (168 loc) · 19.8 KB

Hanchu iESS Local BLE Protocol — Parameter Mapping Reference

Status as of September 2026. Compiled from: the hanchu_ess_ble fork's live BLE reads/writes (HA Green + M5Stack Atom Lite BLE proxy), the 1ulk Hanchu BLE Controller (hanchu-params.js / hanchu-controller.js), and cross-referencing against the Hanchu cloud app and a working cloud-based HA integration.

Scope note: this document maps every code investigated, including many not exposed as entities by default. The integration's built entities mirror the common cloud integration entity list. Everything else here is a reference for anyone wanting to expose additional codes using the same async_read_values() / async_write_value() pattern.


Protocol Summary

  • Transport: BLE GATT. Service UUID 0000ffff-0000-1000-8000-00805f9b34fb (fallback 0000ff00-...). Notify characteristic 0000ff01-..., write characteristic 0000ff02-....
  • Encryption: AES-128-CFB8 (segment_size=8), no padding. Static codec basis + state vector embedded in protocol.py. Session key derived per-connection from a random 6-character handshake token via derive_session_key().
  • Handshake: byte 0x05 + 6-char ASCII token, sent unencrypted to the write characteristic. Device acks with a 0x05 0x00-prefixed packet.
  • Read command: {"act":"1","cmd":"local","data":[{"k":key}...],"tid":"10001"}, AES-encrypted, written to the write characteristic. Response arrives via notifications, possibly split across multiple packets, reassembled and decrypted.
  • Write command: {"act":"3","cmd":"local","data":[{"k":key,"v":value}],"tid":"10001"}. Values must be plain integers, not strings. Successful write replies with {"<key>": 0} (0 = success acknowledgement).
  • Per-device addressing: the inverter and each individual battery pack are separate BLE peripherals, each with their own advertised name and address — not sub-nodes reached through a single connection. Device type is identifiable directly from the advertised name: inverter loggers begin HC:L110, battery pack loggers begin HC:L101. The same handshake/encryption/read protocol works identically against either — connect to the specific pack's own address and the same {"act":"1",...} read command structure applies, just requesting B-codes instead of P/L-codes.

Important Implementation Notes

These were discovered during live testing and are critical for correct operation:

  • Integer values for writes — values sent via async_write_value() must be plain Python integers, not strings. Sending a string (e.g. "1" instead of 1) causes the device to respond with a plaintext error notification that the packet parser won't recognise, taking down the whole operation.
  • CoordinatorEntity for write entities — select/number/time entities must inherit CoordinatorEntity so their displayed state stays live across coordinator poll cycles. Without this, values get stuck at "unknown" after an integration reload.
  • asyncio.Lock for BLE connection — HanchuBleClient holds a connection lock acquired by both reads and writes. Without this, the coordinator's scheduled poll can collide with a manual write, causing 20s+ connect timeouts and failed operations.
  • Hardened packet parsing — parse_packet() logs-and-skips rather than raising on unrecognised notification types, so unexpected device responses (e.g. plaintext error messages) don't crash the active operation.
  • Epoch registers need explicit datetime conversion — a register returning a raw Unix epoch integer (e.g. L094) will display as a meaningless, constantly-different number if exposed as a plain sensor. It must be given device_class=SensorDeviceClass.TIMESTAMP and have its value converted via datetime.fromtimestamp(value, tz=timezone.utc) before being returned from native_value — HA does not auto-convert raw integers for the timestamp device class.
  • Full BLE operation needs one outer timeout, not just connection setup — establish_connection() isn't the only call that can hang under BLE proxy contention; start_notify(), write_gatt_char(), and especially disconnect() (called in a finally block) have no timeout of their own and can block indefinitely on some Bleak/BlueZ backends. Wrapping only the connection step is not enough — wrap the entire connect → notify → handshake → write → disconnect sequence in a single asyncio.wait_for(..., timeout=60.0) so a hang anywhere in the chain surfaces as a clean, catchable TimeoutError instead of freezing the coordinator's update cycle permanently with no exception and no failure logged.
  • DataUpdateCoordinator created inside another coordinator's async_setup() may not reliably reschedule itself — HanchuBatteryCoordinator instances are created dynamically inside the parent inverter coordinator's async_setup(), rather than through HA's normal top-level async_setup_entry() flow. In testing, the base class's built-in periodic rescheduling ran the first refresh correctly but then never re-armed — entities went stale indefinitely with zero errors logged and failure counters frozen at 0, while a manual refresh (homeassistant.update_entity) always worked perfectly, confirming the read pipeline itself was healthy. Fixed by passing update_interval=None to disable the base class's own scheduling entirely, and instead driving refreshes explicitly via homeassistant.helpers.event.async_track_time_interval calling coordinator.async_request_refresh(). Don't do both (i.e. don't set a real update_interval and add an explicit timer) — the two schedulers can run concurrently, causing far more frequent, overlapping refresh attempts than intended and genuine race-condition errors from concurrent access to coordinator state.
  • A multi-entry write payload (multiple {"k":...,"v":...} objects in one data array) reports false success — the read side is confirmed to accept a multi-entry data array (used for batched reads across FAST_POLL_KEYS/SLOW_POLL_KEYS), which suggested by analogy that a multi-key write might work the same way. Bench-testing this directly on real hardware (Sept 2026) found the opposite: the device replied with a 0 (success) status for every key in the batch, and an immediate read-back even echoed the new values — but the underlying registers were not actually changed on the device or reflected in the vendor app. Do not rely on a multi-entry write payload or its reply/read-back as confirmation that values were committed. The reliable pattern for applying several writes together is to open one BLE connection and send each as its own single-key act:"3" request in sequence before disconnecting (see async_write_values() in ble_client.py) — this avoids per-write connection overhead without depending on unverified multi-key write semantics.
  • A register can report a commanded value before the physical change has actually completed — confirmed specifically for P500 (see below), where the register flips to the new value well before the inverter has finished a state transition that visibly takes much longer. Any consumer treating "register now reads the new value" as equivalent to "the physical change is finished" should verify that assumption per-register rather than assuming it generally, especially for registers tied to hardware transitions (relays, mode changes) rather than simple settings.

Confirmed Sensor / Read-Only Codes

Code Name Description Unit Notes
P000 Phase Mode AC phase configuration — 0 = single, 3 = three Live value 1 on some single-phase units
P002 Inverter Serial Number
P003 Inverter Model e.g. "HYB-5K"
P005 Inverter Power Limit Rated power limit of the inverter W Used as dynamic max for charge/discharge power limit entities
P006 Inverter Main Firmware Version e.g. V610-02003-25
P007 Inverter Safety Firmware Version e.g. V610-10008-11
P008 Inverter Brand e.g. "HANCHU"
P011 Parameter P011 Static ~50.00 across reads Likely a rated/reference frequency value
P024 PV String 1 Voltage DC-coupled PV input V ~0 on AC-coupled systems
P025 PV String 1 Current DC-coupled PV input A ~0 on AC-coupled systems
P026 PV String 2 Voltage DC-coupled PV input V ~0 on AC-coupled systems
P027 PV String 2 Current DC-coupled PV input A ~0 on AC-coupled systems
P028 PV String 3 Voltage DC-coupled PV input, 3-MPPT inverters only V Confirmed against real 3-MPPT hardware by @techjlb — follows the same +2 register offset per string as PV1/PV2. ~0 on 2-MPPT or AC-coupled systems
P029 PV String 3 Current DC-coupled PV input, 3-MPPT inverters only A Confirmed against real 3-MPPT hardware by @techjlb — follows the same +2 register offset per string as PV1/PV2. ~0 on 2-MPPT or AC-coupled systems
P044 Grid Voltage L1 V
P045 Grid Current L1 A
P046 Grid Parameter (P046) Returns None — unsupported on tested hardware/firmware
P048 Grid Parameter (P048) Returns None — unsupported on tested hardware/firmware
P053 Grid Frequency Hz ~50.00 on UK grid
P055 Active Power Net active power at inverter terminals W Reads ~0 during AC-coupled solar export — not equivalent to total grid power
P056 Grid Reactive Power Var
P057 Grid Power Factor 0.00–1.00 Correlates strongly with grid import/export direction on tested systems
P060 Total PV Power DC-coupled combined PV output W ~0 on AC-coupled systems
P061 PV Energy Today DC-coupled kWh ~0 on AC-coupled systems
P062 PV Energy Accumulated DC-coupled kWh ~0 on AC-coupled systems
P063 Battery Comms Status 0 = disconnected, 1 = connected
P064 Battery State 0 = disconnected, 1 = normal, 2 = charging, 3 = discharging
P067 Battery Voltage (alt) Inverter-side reading V
P068 Battery Current (alt) Inverter-side reading A
P069 Battery Power Positive = discharging, negative = charging — note: the hanchu-params.js registry has this backwards W The integration applies ×-1 scale factor to match cloud convention (positive = charging)
P070 Battery Temperature °C
P071 Battery SOC (decimal) e.g. 0.30 = 67% % Integration applies ×100 scale factor to convert to percentage
P075 Battery Charge Today kWh
P076 Battery Discharge Today kWh
P079 EPS Voltage EPS output during grid outage V
P080 EPS Current A
P081 EPS Frequency Hz
P082 EPS Active Power W
P083 EPS Reactive Power Var
P084 EPS Energy Today kWh
P085 EPS Energy Accumulated kWh
P088 Battery Capacity Rated capacity Ah Convert to kWh via Ah × 51.2 / 1000 (integration applies ×0.0512 scale factor)
P139 ARM Firmware Version
P142 Battery Pack Count
P237 AC Coupled PV Power Live AC-coupled solar power W Sign convention varies between hardware/firmware versions — some return positive when generating, others negative. The integration uses abs() in the Load Power calculation to handle both conventions correctly.
P644 Grid Power Live grid import/export W Confirmed live. Positive = importing, negative = exporting
L023 DTU Firmware Version e.g. 1.9.0
L034 Meter Type / CT Meter Version 0 = no meter, 3 = specific supported meter
L020 Timezone IANA timezone code reported by the DTU Confirmed readable — e.g. "Europe/London". Earlier notes assumed write-only; live testing (v1.0.10) confirmed it also returns correctly on read
L094 RTC Unix Epoch Timestamp Current Unix epoch time as reported by the DTU's onboard RTC s (epoch) Confirmed matches real-world time within normal poll-cycle drift. Exposed as SensorDeviceClass.TIMESTAMP via datetime.fromtimestamp(value, tz=timezone.utc) — raw integer alone displays as a meaningless incrementing number if not converted
L096 RTC Daylight Saving Offset DST offset as reported by the DTU min Correction to original hypothesis — earlier notes assumed hours; live testing (v1.0.10) confirmed the value is in minutes (60 during BST, expected 0 during GMT), consistent with a standard one-hour DST shift

Confirmed Battery Pack (B-code) Registers

Unlike P/L-codes, which are read from the inverter's own BLE connection, B-codes are read directly from an individual battery pack's own BLE address — see the per-device addressing note above. Confirmed working against real hardware in v1.1.0; identified from the 1ulk hanchu-params.js registry, which documents these but never wires them up (the reference implementation only ever polls the inverter).

Code Name Description Unit Notes
B002 Battery Serial Number
B034 Battery SoC State of charge, direct from the pack's own BMS % Matches the official app's per-pack SoC reading directly — no scale factor needed, unlike the inverter's P071
B035 Battery Pack Voltage V
B038 Environmental Temperature Ambient temperature at the pack °C
B039 Battery Temperature Cell temperature °C
B040 PCBA Temperature Control board temperature °C
B043 Battery Current Charge/discharge current for that pack A Sign convention: negative observed while discharging in testing
B145 Hardware Version
B146 Battery Model
B148 Firmware Version

Also documented in the registry but not currently exposed as entities: B005 (Battery Brand), B153 (485A Protocol), B157 (CAN2 Protocol) — recorded for future reference, not yet confirmed against live hardware.


Confirmed Control / Write Codes

All writes use act: "3" with a plain integer value.

Code Name Description Unit
P651 Work Mode 1=Self-Consumption, 2=Backup, 3=User-Defined, 4=Off-Grid (mirrors L019)
L017 Charge Power Limit Maximum battery charge power W
L018 Discharge Power Limit Maximum battery discharge power W
P647 Maximum Charge SOC Upper SOC limit — battery charges to this level then stops %
P648 On-Grid Battery Discharge Minimum Hard floor — battery will not discharge below this level while on-grid %
P772 Off-Grid Battery Discharge Minimum Hard floor for off-grid/EPS mode — distinct from P648 %
L074 Max SOC Limit Upper SOC limit for grid-to-battery charging %
L005 Charge Period 1 Start Seconds from midnight s
L006 Charge Period 1 End Seconds from midnight s
L007 Charge Period 2 Start Seconds from midnight s
L008 Charge Period 2 End Seconds from midnight s
L009 Charge Period 3 Start Seconds from midnight; 0 = disabled s
L010 Charge Period 3 End Seconds from midnight; 0 = disabled s
L011 Discharge Period 1 Start Seconds from midnight; 0 = disabled s
L012 Discharge Period 1 End Seconds from midnight; 0 = disabled s
L013 Discharge Period 2 Start Seconds from midnight; 0 = disabled s
L014 Discharge Period 2 End Seconds from midnight; 0 = disabled s
L015 Discharge Period 3 Start Seconds from midnight; 0 = disabled s
L016 Discharge Period 3 End Seconds from midnight; 0 = disabled s
P236 Meter PV Enable 0 = off, 1 = on
P245 Meter PV Direction 0 = import, 1 = export
P500 Grid Relay / Inverter Power State 1 = On, 0 = Off — controls the grid relay and overall inverter power state; setting Off stops solar, battery charge/discharge, and EPS output together, equivalent to the physical AC isolator switch. Confirmed the register reports the commanded value well before the physical transition finishes: Off completes in ~7-10s and the register matches quickly, but On was observed reading back as "on" ~20s in during HA testing, despite the inverter still visibly restarting for another ~30s (~50s total, several relay clicks, per PaulDGAL's hardware testing). Treat "register matches" and "physically complete" as separate things for this register, not interchangeable.

Documented But Not Implemented

Codes identified in the 1ulk hanchu-params.js registry but not currently exposed as entities. Recorded here for future reference.

Code Name Description Notes
L021 Clear WiFi Password Write trigger to clear stored WiFi credentials Write only — not suitable as a polled sensor

Known Gaps

Feature Status
Daily Grid Import (Power Purchased Today) P638 is documented in the registry but never returned in BLE replies on tested hardware — tested in isolation, batched, and across neighbouring code clusters. No alternative key found. Cloud-only or requires packet capture of official app to identify.
Daily Grid Export (Power Sold Today) Same as P638 — P639 also absent from all BLE replies.
Daily Load Energy No dedicated code found. Can be derived via HA's Riemann sum integral helper on the Load Power sensor.
AC-Coupled Daily PV Energy P237 gives live AC-coupled PV power but no daily total equivalent was found.
Fast Charge / Discharge Does not appear in the local inverter app settings — likely cloud-only via a separate endpoint.

Unmapped / Unknown Codes

Tested via live BLE reads but purpose unconfirmed:

Code Value(s) seen Behaviour Hypothesis
P011 50.00 Static Rated/reference frequency
P046 None Unsupported Not present on tested hardware
P048 None Unsupported Not present on tested hardware
P240 ~2015.xx Slow drift Possibly a calibration or batch code
P241 ~11.9x Slow drift Possibly a calibration constant
P498 ~1.7–2.1 Live, varies Unconfirmed measurement
P499 ~240–241 Live, varies Likely a secondary voltage reading
P640 ~6000+ Cumulative Energy counter — increments daily
P641 ~2700+ Cumulative Energy counter — increments daily
P642 ~5900+ Cumulative Energy counter — increments daily
P643 ~2700+ Cumulative Energy counter — increments daily
P644 Varies, sign-changing Live Confirmed Grid Power
P685–P693 No data Absent Unsupported/reserved on tested hardware

Derived Sensors

Sensor Formula Notes
Load Power P644 + abs(P237) + P069 (raw values) abs() applied to P237 to handle sign convention variation between hardware/firmware versions
Battery Capacity (kWh) P088 × 0.0512 P088 in Ah × 51.2V nominal / 1000
Battery SOC (%) P071 × 100 P071 is a decimal fraction e.g. 0.67 = 67%
Battery Power (cloud convention) P069 × -1 Inverts to positive=charging to match cloud convention

Credits

  • Blustery7752 — original hanchu-ess-ble integration providing the BLE connection, encryption, and read foundation
  • 1ulk — Hanchu BLE Controller web app whose hanchu-params.js parameter registry and hanchu-controller.js write implementation were essential references
  • PaulDGAL — real-world testing identifying BLE load and sign convention issues, proposing the tiered polling approach, discovering/mapping the RTC and firmware diagnostic registers (L020, L094, L096) later implemented in v1.0.10, and identifying the P500 register plus hands-on hardware testing of its full transition behaviour and timing (v1.3.0)
  • Dean (upton68) — bench-testing that identified the multi-entry write false-success behaviour, and real-hardware testing that identified P500's early-confirmation timing discrepancy on power-on