Great answer, with a bunch of information. I will try to reply in chunks, otherwise I guess it is too much.
What is the brand and in which region is it sold? Is there a commercial support service involved?
Sorry, I’m not familiar with this system, not sure if my post is useful.
We reached out to SPRSUN in China and bought them directly from there and got the shipped via train within 2 month, without bad surprises.
Unfortunately we needed some time for the different projects and so the first installations were ready only when the heating period had ended (end of March, begin of April).
Actually we used the original EW11 and an own EW11 resp EE11 in parallel, both attached to the cable for the original EW11. The original EW11 was kept parametrized as the App setup and the other used in its serial setup “Protocol Setup” set to Modbus instead of none.
With that setting you can run the HA and the App in parallel, even not exactly in same time.
The EW11 acts as Modbus master when used, but keep quite if they are not working.
To have the monitoring read done in the shortest time and don’t block the line for a longer period I decided to start with an external Python program which connects, reads the data via some Modbus TCP calls in blocks (reading multiple register blocks at the same time) and ship the results via MQTT to HA, where I wrote an integration package to create the corresponding entities.
That allows in parallel the use of the app, as the line is not always blocked.
But I realized that there seems to be a “always phone home” and a security of “MAC address only”.
So the heatpump if connected with the App seems to ship that data constantly to a cloud service in China, which does not sounds critical at first glance. But obviously the heat pump / EW11 setup seems to actively open the connection and also seems to be able to receive requests from China as well.
That this can - and most probaly will - be critical I realized in spring 2024. Suddenly my Sungrow battery, part of my PV home system, stopped working and was not startable anymore. Reason for that was an failed firmware update attempt from Sungrow, which they said was needed because of a major design change of their monitoring. But that new firmware was not well tested and a large numbers of Sungrow SBR Battery systems in Germany were affected.
At that point in time they had only a minor support team, totally overwhelmed with a flood a new support requests. The BMS part had to be exchanged, which needs a severe number of parallel exchange requests and it took month to get all the systems working again.
That however was an unwanted incident, not a hostile attack, but it shows that - even on a “friendly purpose” cloud connection Chinese verndors are capable to perform changes over the same connection and can generally also deactivate all these devices from remote.
The only protection is to isolate the devives and don’t let them call home.
Next I realized, that the transfer of heatpumps from one account to the next is based on an QR code, which the app generates for this purpose. That QR Code is just the MAC address of the EW11 stick. As these are vendor specific you can easily guess other EW11 Mac addresses and that would enable you with you account to work on other people machines.
So I decided to run the App just for tests and reverse engineering and do the rest with Modbus and Carel controller as they are purely running local.
I also found the forum at arturhome.pl with /t/pompa-ciepla-sprsun-select-cgk40v3l/12596/6 and that referenced a repo github.com repo PiterPiotr/Sprsun-PC from Piter / Pjotr (I guess he’s also active here). Meanwhile that us not existing anymore, but there are clones like GitHub - paddyponchero/Sprsun-PC: Integration of the Sprsun CGKxxxV3L-B heat pump with HA and I relized that he had more register in use then the official Modbus register description.
So I leared a way how to use the monitoring with Modbus in HA directly, but I didn’t really like the way using single requests per Entity, as we have over 500 registers to watch and that will lead to a constant reading on the wire, blocking the connection and also probably lead to trouble.
So I tried to use parallel reads and with 6 chunks with 100 register each I was able to read them with HA methods via modbus in bulk as well.
Here is my package I started a an early prototype, which reads in 6 calls the register from 0 to 599 und some of them are set as own entities.
Unfortunately the modbus implementation was recently changed and I have to work out new working version as that
But the the code I have.
I’m running out of time, rest will follow later today.
Regards from a winterly Cologne
Detlef
# SPRSUN Wärmepumpe - Modbus Integration (2025.11-kompatibel)
# ACHTUNG: Alte Entities (wp_h_0 bis wp_h_5) existieren nicht mehr!
# Neue Entity-IDs: sensor.wp_register_block_0 bis sensor.wp_register_block_5
modbus:
- name: "WP"
type: tcp
host: !secret wp_modbus_host_ip
port: !secret wp_modbus_port
delay: 5
timeout: 10
sensors:
- name: "WP Register Block 0"
slave: !secret wp_modbus_slave
address: 0
count: 100
data_type: custom
structure: "100H"
scan_interval: 29
unique_id: "wp_modbus_block_0"
- name: "WP Register Block 1"
slave: !secret wp_modbus_slave
address: 100
count: 100
data_type: custom
structure: "100H"
scan_interval: 31
unique_id: "wp_modbus_block_1"
- name: "WP Register Block 2"
slave: !secret wp_modbus_slave
address: 200
count: 100
data_type: custom
structure: "100H"
scan_interval: 31
unique_id: "wp_modbus_block_2"
- name: "WP Register Block 3"
slave: !secret wp_modbus_slave
address: 300
count: 100
data_type: custom
structure: "100H"
scan_interval: 41
unique_id: "wp_modbus_block_3"
- name: "WP Register Block 4"
slave: !secret wp_modbus_slave
address: 400
count: 100
data_type: custom
structure: "100H"
scan_interval: 29
unique_id: "wp_modbus_block_4"
- name: "WP Register Block 5"
slave: !secret wp_modbus_slave
address: 500
count: 100
data_type: custom
structure: "100H"
scan_interval: 37
unique_id: "wp_modbus_block_5"
template:
# Binary Sensoren aus Register 4 in Block 0 (Bit-Operationen)
- binary_sensor:
- name: "WP OS1 Compressor"
unique_id: wp_os1_compressor
state: >
{% set values = state_attr('sensor.wp_register_block_0', 'value') | default([]) %}
{{ (values[4] | default(0) | int) | bitwise_and(1) > 0 }}
device_class: running
- name: "WP OS1 Fan"
unique_id: wp_os1_fan
state: >
{% set values = state_attr('sensor.wp_register_block_0', 'value') | default([]) %}
{{ (values[4] | default(0) | int) | bitwise_and(32) > 0 }}
device_class: running
- name: "WP OS1 4-way valve"
unique_id: wp_os1_4_way_valve
state: >
{% set values = state_attr('sensor.wp_register_block_0', 'value') | default([]) %}
{{ (values[4] | default(0) | int) | bitwise_and(64) > 0 }}
device_class: opening
- name: "WP OS1 High or low fan speed"
unique_id: wp_os1_high_or_low_fan_speed
state: >
{% set values = state_attr('sensor.wp_register_block_0', 'value') | default([]) %}
{{ (values[4] | default(0) | int) | bitwise_and(128) > 0 }}
device_class: running
# Sensoren aus verschiedenen Registern
- sensor:
# Register 1 in Block 0
- name: "WP COP"
unique_id: wp_cop
state: >
{% set values = state_attr('sensor.wp_register_block_0', 'value') | default([]) %}
{{ (values[1] | default(0) | float) * 0.1 }}
unit_of_measurement: ""
state_class: measurement
# Register 23 in Block 0
- name: "WP AC Voltage Phase A"
unique_id: wp_ac_voltage_phase_a
state: >
{% set values = state_attr('sensor.wp_register_block_0', 'value') | default([]) %}
{{ values[23] | default(0) | float }}
unit_of_measurement: "V"
device_class: voltage
state_class: measurement
# Register 48 in Block 5 (Adresse 548)
- name: "WP AC Voltage Phase B"
unique_id: wp_ac_voltage_phase_b
state: >
{% set values = state_attr('sensor.wp_register_block_5', 'value') | default([]) %}
{{ values[48] | default(0) | float }}
unit_of_measurement: "V"
device_class: voltage
state_class: measurement
# Register 50 in Block 5 (Adresse 550)
- name: "WP AC Voltage Phase C"
unique_id: wp_ac_voltage_phase_c
state: >
{% set values = state_attr('sensor.wp_register_block_5', 'value') | default([]) %}
{{ values[50] | default(0) | float }}
unit_of_measurement: "V"
device_class: voltage
state_class: measurement
# Register 15 in Block 0
- name: "WP Outlet Temperature"
unique_id: wp_outlet_temperature
state: >
{% set values = state_attr('sensor.wp_register_block_0', 'value') | default([]) %}
{{ (values[15] | default(0) | float) * 0.1 }}
device_class: temperature
unit_of_measurement: "°C"
state_class: measurement
# Register 14 in Block 0
- name: "WP Inlet Temperature"
unique_id: wp_inlet_temperature
state: >
{% set values = state_attr('sensor.wp_register_block_0', 'value') | default([]) %}
{{ (values[14] | default(0) | float) * 0.1 }}
device_class: temperature
unit_of_measurement: "°C"
state_class: measurement
# Register 17 in Block 0
- name: "WP Ambient Temperature"
unique_id: wp_ambient_temperature
state: >
{% set values = state_attr('sensor.wp_register_block_0', 'value') | default([]) %}
{{ (values[17] | default(0) | float) * 0.5 }}
device_class: temperature
unit_of_measurement: "°C"
state_class: measurement
#### DC PUMP (Fnn Values) - Block 3, Register 90-96
- name: "F13 DC Pumpmodus 0 Manuell/1 Auto"
unique_id: wp_f13_dc_pumpmodus
state: >
{% set values = state_attr('sensor.wp_register_block_3', 'value') | default([]) %}
{{ values[90] | default(0) | int }}
unit_of_measurement: ""
state_class: measurement
- name: "F14 DC Pumpenzyklus"
unique_id: wp_f14_dc_pumpenzyklus
state: >
{% set values = state_attr('sensor.wp_register_block_3', 'value') | default([]) %}
{{ values[91] | default(0) | int }}
unit_of_measurement: "sec"
state_class: measurement
- name: "F15 DC PUMP FREQ SET"
unique_id: wp_f15_dc_pump_freq_set
state: >
{% set values = state_attr('sensor.wp_register_block_3', 'value') | default([]) %}
{{ values[92] | default(0) | int }}
unit_of_measurement: "%"
state_class: measurement
- name: "F16 DC PUMP MAX FREQ"
unique_id: wp_f16_dc_pump_max_freq
state: >
{% set values = state_attr('sensor.wp_register_block_3', 'value') | default([]) %}
{{ values[93] | default(0) | int }}
unit_of_measurement: "%"
state_class: measurement
- name: "F17 DC PUMP MIN FREQ"
unique_id: wp_f17_dc_pump_min_freq
state: >
{% set values = state_attr('sensor.wp_register_block_3', 'value') | default([]) %}
{{ values[94] | default(0) | int }}
unit_of_measurement: "%"
state_class: measurement
- name: "F18 DC Pumpskala Faktor"
unique_id: wp_f18_dc_pumpskala_faktor
state: >
{% set values = state_attr('sensor.wp_register_block_3', 'value') | default([]) %}
{{ values[95] | default(0) | int }}
unit_of_measurement: ""
state_class: measurement
- name: "F19 DC PUMP Diff."
unique_id: wp_f19_dc_pump_diff
state: >
{% set values = state_attr('sensor.wp_register_block_3', 'value') | default([]) %}
{{ values[96] | default(0) | int }}
unit_of_measurement: ""
state_class: measurement
#### Letzte Aktualisierungszeiten (vereinfachte Version)
- name: "WP Block 0 Last Update"
unique_id: wp_block_0_last_update
state: >
{% set e = 'sensor.wp_register_block_0' %}
{% if is_state_attr(e, 'timestamp', defined) %}
{{ as_datetime(state_attr(e, 'timestamp')) | as_local | strftime('%d.%m.%Y %H:%M:%S') }}
{% elif states(e) and states[e].last_updated %}
{{ as_datetime(states[e].last_updated) | as_local | strftime('%d.%m.%Y %H:%M:%S') }}
{% else %}
unknown
{% endif %}
attributes:
age_seconds: >
{% set e = 'sensor.wp_register_block_0' %}
{% if is_state_attr(e, 'timestamp', defined) %}
{{ (now().timestamp() - as_timestamp(state_attr(e, 'timestamp'))) | int }}
{% elif states(e) and states[e].last_updated %}
{{ (now().timestamp() - as_timestamp(states[e].last_updated)) | int }}
{% else %}
-1
{% endif %}
- name: "WP Block 1 Last Update"
unique_id: wp_block_1_last_update
state: >
{% set e = 'sensor.wp_register_block_1' %}
{% if is_state_attr(e, 'timestamp', defined) %}
{{ as_datetime(state_attr(e, 'timestamp')) | as_local | strftime('%d.%m.%Y %H:%M:%S') }}
{% elif states(e) and states[e].last_updated %}
{{ as_datetime(states[e].last_updated) | as_local | strftime('%d.%m.%Y %H:%M:%S') }}
{% else %}
unknown
{% endif %}
attributes:
age_seconds: >
{% set e = 'sensor.wp_register_block_1' %}
{% if is_state_attr(e, 'timestamp', defined) %}
{{ (now().timestamp() - as_timestamp(state_attr(e, 'timestamp'))) | int }}
{% elif states(e) and states[e].last_updated %}
{{ (now().timestamp() - as_timestamp(states[e].last_updated)) | int }}
{% else %}
-1
{% endif %}
- name: "WP Block 2 Last Update"
unique_id: wp_block_2_last_update
state: >
{% set e = 'sensor.wp_register_block_2' %}
{% if is_state_attr(e, 'timestamp', defined) %}
{{ as_datetime(state_attr(e, 'timestamp')) | as_local | strftime('%d.%m.%Y %H:%M:%S') }}
{% elif states(e) and states[e].last_updated %}
{{ as_datetime(states[e].last_updated) | as_local | strftime('%d.%m.%Y %H:%M:%S') }}
{% else %}
unknown
{% endif %}
attributes:
age_seconds: >
{% set e = 'sensor.wp_register_block_2' %}
{% if is_state_attr(e, 'timestamp', defined) %}
{{ (now().timestamp() - as_timestamp(state_attr(e, 'timestamp'))) | int }}
{% elif states(e) and states[e].last_updated %}
{{ (now().timestamp() - as_timestamp(states[e].last_updated)) | int }}
{% else %}
-1
{% endif %}
- name: "WP Block 3 Last Update"
unique_id: wp_block_3_last_update
state: >
{% set e = 'sensor.wp_register_block_3' %}
{% if is_state_attr(e, 'timestamp', defined) %}
{{ as_datetime(state_attr(e, 'timestamp')) | as_local | strftime('%d.%m.%Y %H:%M:%S') }}
{% elif states(e) and states[e].last_updated %}
{{ as_datetime(states[e].last_updated) | as_local | strftime('%d.%m.%Y %H:%M:%S') }}
{% else %}
unknown
{% endif %}
attributes:
age_seconds: >
{% set e = 'sensor.wp_register_block_3' %}
{% if is_state_attr(e, 'timestamp', defined) %}
{{ (now().timestamp() - as_timestamp(state_attr(e, 'timestamp'))) | int }}
{% elif states(e) and states[e].last_updated %}
{{ (now().timestamp() - as_timestamp(states[e].last_updated)) | int }}
{% else %}
-1
{% endif %}
- name: "WP Block 4 Last Update"
unique_id: wp_block_4_last_update
state: >
{% set e = 'sensor.wp_register_block_4' %}
{% if is_state_attr(e, 'timestamp', defined) %}
{{ as_datetime(state_attr(e, 'timestamp')) | as_local | strftime('%d.%m.%Y %H:%M:%S') }}
{% elif states(e) and states[e].last_updated %}
{{ as_datetime(states[e].last_updated) | as_local | strftime('%d.%m.%Y %H:%M:%S') }}
{% else %}
unknown
{% endif %}
attributes:
age_seconds: >
{% set e = 'sensor.wp_register_block_4' %}
{% if is_state_attr(e, 'timestamp', defined) %}
{{ (now().timestamp() - as_timestamp(state_attr(e, 'timestamp'))) | int }}
{% elif states(e) and states[e].last_updated %}
{{ (now().timestamp() - as_timestamp(states[e].last_updated)) | int }}
{% else %}
-1
{% endif %}
- name: "WP Block 5 Last Update"
unique_id: wp_block_5_last_update
state: >
{% set e = 'sensor.wp_register_block_5' %}
{% if is_state_attr(e, 'timestamp', defined) %}
{{ as_datetime(state_attr(e, 'timestamp')) | as_local | strftime('%d.%m.%Y %H:%M:%S') }}
{% elif states(e) and states[e].last_updated %}
{{ as_datetime(states[e].last_updated) | as_local | strftime('%d.%m.%Y %H:%M:%S') }}
{% else %}
unknown
{% endif %}
attributes:
age_seconds: >
{% set e = 'sensor.wp_register_block_5' %}
{% if is_state_attr(e, 'timestamp', defined) %}
{{ (now().timestamp() - as_timestamp(state_attr(e, 'timestamp'))) | int }}
{% elif states(e) and states[e].last_updated %}
{{ (now().timestamp() - as_timestamp(states[e].last_updated)) | int }}
{% else %}
-1
{% endif %}