Skip to content

Commit bc4cae3

Browse files
authored
Merge pull request #26 from 0xAHA/claude/plan-issue-25-mOrfd
2 parents 846494f + 424b518 commit bc4cae3

10 files changed

Lines changed: 333 additions & 43 deletions

File tree

README.md

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*Transform your Midea/OEM heat pump hot water system into a smart, Home Assistant-controlled water heater entity!*
44

55
![HACS Badge](https://img.shields.io/badge/HACS-Custom-orange.svg)
6-
![Version](https://img.shields.io/badge/Version-0.2.4-blue.svg)
6+
![Version](https://img.shields.io/badge/Version-0.2.5-blue.svg)
77
[![GitHub Issues](https://img.shields.io/github/issues/0xAHA/Midea-Heat-Pump-HA.svg)](https://github.com/0xAHA/Midea-Heat-Pump-HA/issues)
88

99
Help keep this integration alive! Your support is much appreciated :)
@@ -19,6 +19,7 @@ This integration creates a fully functional **water heater entity** in Home Assi
1919
-**Profile System**: Load from pre-configured profiles or save your own for easy setup and sharing
2020
-**UI Configuration**: Configure entirely through the Home Assistant UI - no YAML required!
2121
-**Sanitize/Sterilize Mode**: Dedicated switch for hot water sanitization (heats to 65°C to kill bacteria)
22+
-**Heater Assist & Sanitize Status**: Binary sensors showing whether the resistance element is active and whether a sanitize cycle is running
2223
-**Mode-Specific Temperature Limits**: Enforces different min/max temperatures per operation mode
2324
-**Control operation modes**: Off, Eco, Performance (Hybrid), Electric (E-Heater)
2425
-**Set target temperature** via direct Modbus with automatic range enforcement
@@ -240,6 +241,8 @@ Exported profiles can be:
240241
| Outdoor Temp | 104 | T4 sensor | Configurable |
241242
| Exhaust Gas Temp | 105 | Tp sensor | No scaling |
242243
| Suction Temp | 106 | Th sensor | Configurable |
244+
| Heater Assist State | 108 | Resistance element substate (read-only) | None |
245+
| Sanitize Cycle State | 109 | Sanitize cycle substate (read-only) | None |
243246

244247
**Note**: Your heat pump model may use different registers. Use the configuration UI to adjust as needed, then save as a custom profile.
245248

@@ -378,25 +381,23 @@ automation:
378381

379382
---
380383

381-
## 🚀 What's New in v0.2.4
384+
## 🚀 What's New in v0.2.5
382385

383-
### Sanitize/Sterilize Mode Support (Optional Feature)
384-
- **Optional sanitize switch** for models that support hot water sanitization
385-
- **Hardware-controlled** - Integration only enables/disables, heat pump handles the rest
386-
- **Simple on/off control** via switch entity (register 3)
387-
- **Not enabled by default** - Only appears if you configure the sterilize register
388-
- **EcoSpring HP300 profile** added with sanitize support pre-configured
386+
### Heater Assist & Sanitize Cycle Binary Sensors (EcoSpring HP300)
387+
- **Heater Assist binary sensor** - shows `On` when the resistance heating element is actively supplementing the heat pump (register 108 ≠ 0)
388+
- **Sanitize Cycle Active binary sensor** - shows `On` when a sanitize cycle is in progress (register 109 = 32 or 33), distinct from the write-only sterilize switch
389+
- Both sensors use the `running` device class for clean On/Off display
390+
- Raw register values are logged at DEBUG level on every update for diagnostics
391+
- Sensors only appear when the profile includes `heater_assist_register` and `sanitize_state_register`
389392

390-
### Bug Fixes & Improvements
391-
- Added Python cache files to .gitignore
392-
- Improved register documentation in README
393-
- Enhanced modbus_test.py with sterilize register support
393+
### EcoSpring HP300 Profile Updated (v1.1)
394+
- Minimum temperature corrected from 60°C to **55°C** across all modes (community-validated)
395+
- Model updated to cover both 280L and 300L variants
396+
- Registers 108 and 109 added with documented substate values
394397

395-
### Previous Features (v0.2.3)
396-
- Profile system with pre-configured models
397-
- Multiple device support with unique entity naming
398-
- Export/import profiles for community sharing
399-
- Enhanced services for profile management
398+
### Previous Features (v0.2.4)
399+
- Sanitize/Sterilize Mode switch (register 3)
400+
- EcoSpring HP300 profile added
400401

401402
---
402403

@@ -459,7 +460,7 @@ logger:
459460
- [x] **Profile system** ✅ Completed in v0.2.3
460461
- [x] **Multiple device support** ✅ Completed in v0.2.3
461462
- [ ] **Community profile library** (shared configurations)
462-
- [ ] **Enhanced diagnostics** (connection status, detailed error reporting)
463+
- [x] **Enhanced diagnostics** ✅ Completed in v0.2.5 (heater assist & sanitize cycle binary sensors)
463464
- [ ] **Energy monitoring** (power consumption tracking)
464465
- [ ] **Advanced scheduling** (built-in time/temperature profiles)
465466

custom_components/midea_heatpump_hws/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
# Define platforms here directly to avoid import issues
2222
PLATFORMS: list[Platform] = [
23+
Platform.BINARY_SENSOR,
2324
Platform.WATER_HEATER,
2425
Platform.SENSOR,
2526
Platform.SWITCH,
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""Binary sensors for Midea Heat Pump diagnostic state registers."""
2+
import logging
3+
from typing import Any
4+
5+
from homeassistant.components.binary_sensor import (
6+
BinarySensorDeviceClass,
7+
BinarySensorEntity,
8+
)
9+
from homeassistant.config_entries import ConfigEntry
10+
from homeassistant.core import HomeAssistant, callback
11+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
12+
from homeassistant.helpers.update_coordinator import CoordinatorEntity
13+
14+
from .const import (
15+
DOMAIN,
16+
CONF_MODBUS_UNIT,
17+
CONF_HEATER_ASSIST_REGISTER,
18+
CONF_SANITIZE_STATE_REGISTER,
19+
)
20+
from .coordinator import MideaModbusCoordinator
21+
22+
_LOGGER = logging.getLogger(__name__)
23+
24+
# Register 109 values that indicate an active sanitize cycle
25+
_SANITIZE_ACTIVE_VALUES = {32, 33}
26+
27+
28+
async def async_setup_entry(
29+
hass: HomeAssistant,
30+
config_entry: ConfigEntry,
31+
async_add_entities: AddEntitiesCallback,
32+
) -> None:
33+
"""Set up Midea diagnostic binary sensors from config entry."""
34+
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
35+
config = hass.data[DOMAIN][config_entry.entry_id]["config"]
36+
37+
host_suffix = f" ({config['host']})"
38+
entities = []
39+
40+
if config.get(CONF_HEATER_ASSIST_REGISTER) is not None:
41+
entities.append(
42+
MideaBinarySensor(
43+
coordinator=coordinator,
44+
config=config,
45+
data_key="heater_assist_raw",
46+
name=f"Heater Assist{host_suffix}",
47+
register=config[CONF_HEATER_ASSIST_REGISTER],
48+
device_class=BinarySensorDeviceClass.RUNNING,
49+
is_on_fn=lambda v: v != 0,
50+
)
51+
)
52+
53+
if config.get(CONF_SANITIZE_STATE_REGISTER) is not None:
54+
entities.append(
55+
MideaBinarySensor(
56+
coordinator=coordinator,
57+
config=config,
58+
data_key="sanitize_state_raw",
59+
name=f"Sanitize Cycle Active{host_suffix}",
60+
register=config[CONF_SANITIZE_STATE_REGISTER],
61+
device_class=BinarySensorDeviceClass.RUNNING,
62+
is_on_fn=lambda v: v in _SANITIZE_ACTIVE_VALUES,
63+
)
64+
)
65+
66+
async_add_entities(entities)
67+
68+
69+
class MideaBinarySensor(CoordinatorEntity, BinarySensorEntity):
70+
"""Read-only binary sensor derived from a raw diagnostic register value."""
71+
72+
def __init__(
73+
self,
74+
coordinator: MideaModbusCoordinator,
75+
config: dict,
76+
data_key: str,
77+
name: str,
78+
register: int,
79+
device_class: BinarySensorDeviceClass,
80+
is_on_fn,
81+
) -> None:
82+
"""Initialize the binary sensor."""
83+
super().__init__(coordinator)
84+
self._config = config
85+
self._data_key = data_key
86+
self._register = register
87+
self._is_on_fn = is_on_fn
88+
89+
self._attr_name = name
90+
self._attr_unique_id = f"midea_{config['host']}_{config[CONF_MODBUS_UNIT]}_{data_key}"
91+
self._attr_device_class = device_class
92+
93+
@property
94+
def device_info(self) -> dict[str, Any]:
95+
"""Return device info to group this sensor with the main device."""
96+
return {
97+
"identifiers": {(DOMAIN, f"{self._config['host']}_{self._config[CONF_MODBUS_UNIT]}")},
98+
"name": f"Midea Heat Pump ({self._config['host']})",
99+
"manufacturer": "Midea",
100+
"model": "Heat Pump Water Heater",
101+
}
102+
103+
@property
104+
def is_on(self) -> bool | None:
105+
"""Return True when the register value indicates active state."""
106+
raw = (self.coordinator.data or {}).get(self._data_key)
107+
if raw is None:
108+
return None
109+
return self._is_on_fn(raw)
110+
111+
@property
112+
def available(self) -> bool:
113+
"""Return if entity is available."""
114+
return self.coordinator.last_update_success and self._data_key in (self.coordinator.data or {})
115+
116+
@callback
117+
def _handle_coordinator_update(self) -> None:
118+
"""Handle updated data from the coordinator, logging raw register value."""
119+
raw = (self.coordinator.data or {}).get(self._data_key)
120+
_LOGGER.debug(
121+
"%s: register %s raw=%s -> is_on=%s",
122+
self._attr_name,
123+
self._register,
124+
raw,
125+
self._is_on_fn(raw) if raw is not None else None,
126+
)
127+
self.async_write_ha_state()

custom_components/midea_heatpump_hws/const.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
DOMAIN = "midea_heatpump_hws"
55

6-
PLATFORMS: list[Platform] = [Platform.WATER_HEATER, Platform.SENSOR, Platform.SWITCH, Platform.SELECT]
6+
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.WATER_HEATER, Platform.SENSOR, Platform.SWITCH, Platform.SELECT]
77

88
# Default register addresses
99
DEFAULT_POWER_REGISTER = 0
@@ -71,4 +71,6 @@
7171
CONF_OUTDOOR_TEMP_REGISTER = "outdoor_temp_register"
7272
CONF_EXHAUST_TEMP_REGISTER = "exhaust_temp_register"
7373
CONF_SUCTION_TEMP_REGISTER = "suction_temp_register"
74-
CONF_ENABLE_ADDITIONAL_SENSORS = "enable_additional_sensors"
74+
CONF_ENABLE_ADDITIONAL_SENSORS = "enable_additional_sensors"
75+
CONF_HEATER_ASSIST_REGISTER = "heater_assist_register"
76+
CONF_SANITIZE_STATE_REGISTER = "sanitize_state_register"

custom_components/midea_heatpump_hws/coordinator.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
CONF_OUTDOOR_TEMP_REGISTER,
3636
CONF_EXHAUST_TEMP_REGISTER,
3737
CONF_SUCTION_TEMP_REGISTER,
38+
CONF_HEATER_ASSIST_REGISTER,
39+
CONF_SANITIZE_STATE_REGISTER,
3840
)
3941

4042
_LOGGER = logging.getLogger(__name__)
@@ -101,6 +103,10 @@ def __init__(
101103
"suction_temp": config.get(CONF_SUCTION_TEMP_REGISTER),
102104
}
103105

106+
# Diagnostic state registers (raw integer, no scaling)
107+
self.heater_assist_register = config.get(CONF_HEATER_ASSIST_REGISTER)
108+
self.sanitize_state_register = config.get(CONF_SANITIZE_STATE_REGISTER)
109+
104110
self._client: AsyncModbusTcpClient | None = None
105111
self._lock = asyncio.Lock()
106112
self._pending_writes: dict[str, Any] = {}
@@ -251,6 +257,27 @@ async def _async_update_data(self) -> dict[str, Any]:
251257
except Exception as ex:
252258
_LOGGER.exception("Error reading %s: %s", sensor_name, ex)
253259

260+
# Read diagnostic state registers (raw integer, no scaling)
261+
for reg_name, register in [
262+
("heater_assist_raw", self.heater_assist_register),
263+
("sanitize_state_raw", self.sanitize_state_register),
264+
]:
265+
if register is None:
266+
continue
267+
try:
268+
result = await self._client.read_holding_registers(
269+
address=register,
270+
count=1,
271+
device_id=self.modbus_unit
272+
)
273+
if not result.isError():
274+
data[reg_name] = result.registers[0]
275+
_LOGGER.debug("Read %s register %s -> %s", reg_name, register, data[reg_name])
276+
else:
277+
_LOGGER.debug("Failed to read %s register %s: %s", reg_name, register, result)
278+
except Exception as ex:
279+
_LOGGER.exception("Error reading %s: %s", reg_name, ex)
280+
254281
_LOGGER.debug("Modbus data updated: %s", data)
255282
return data
256283

custom_components/midea_heatpump_hws/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
"name": "Midea Heatpump HWS",
44
"codeowners": ["@0xAHA"],
55
"config_flow": true,
6-
"dependencies": ["sensor", "switch"],
6+
"dependencies": ["binary_sensor", "sensor", "switch"],
77
"documentation": "https://github.com/0xAHA/Midea-Heat-Pump-HA",
88
"iot_class": "local_push",
99
"issue_tracker": "https://github.com/0xAHA/Midea-Heat-Pump-HA/issues",
1010
"requirements": ["pymodbus>=3.11.0"],
11-
"version": "0.2.4"
11+
"version": "0.2.5"
1212
}

custom_components/midea_heatpump_hws/models/defaults/ecospring_hp300.json

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"name": "EcoSpring HP300",
3-
"model": "HP300",
3+
"model": "HP300 (280L / 300L)",
44
"manufacturer": "EcoSpring",
5-
"version": "1.0",
6-
"description": "Configuration for EcoSpring 300L heat pump water heater with sanitize mode support",
7-
"created": "2025-01-06T00:00:00",
5+
"version": "1.1",
6+
"description": "Configuration for EcoSpring HP300 heat pump water heater (280L/300L variants). Register map and temperature limits validated by community testing (issue #25). Registers 108 and 109 expose heater-assist substate and sanitize-cycle detection as read-only sensors.",
7+
"created": "2026-03-17T00:00:00",
88
"author": "Community Contribution",
99

1010
"connection": {
@@ -24,7 +24,9 @@
2424
"condensor_temp": 103,
2525
"outdoor_temp": 104,
2626
"exhaust_temp": 105,
27-
"suction_temp": 106
27+
"suction_temp": 106,
28+
"heater_assist_register": 108,
29+
"sanitize_state_register": 109
2830
},
2931

3032
"mode_values": {
@@ -50,21 +52,35 @@
5052

5153
"temp_limits": {
5254
"eco": {
53-
"min": 60,
55+
"min": 55,
5456
"max": 65
5557
},
5658
"performance": {
57-
"min": 60,
59+
"min": 55,
5860
"max": 70
5961
},
6062
"electric": {
61-
"min": 60,
63+
"min": 55,
6264
"max": 70
6365
}
6466
},
6567

6668
"defaults": {
67-
"target_temperature": 65,
69+
"target_temperature": 60,
6870
"enable_additional_sensors": true
71+
},
72+
73+
"notes": {
74+
"diagnostic_registers": {
75+
"108_heater_assist": "Raw substate register. Known values: 0=idle, 13=heater assist active, 2=sanitize cycle delayed start",
76+
"109_sanitize_state": "Raw substate register. Known values: 0=idle/heater assist, 32=sanitize cycle immediate, 33=sanitize cycle delayed",
77+
"combined_states": {
78+
"normal_idle": "108=0, 109=0",
79+
"heater_assist_active": "108=13, 109=2",
80+
"sanitize_immediate": "108=0, 109=32",
81+
"sanitize_delayed": "108=2, 109=33"
82+
},
83+
"source": "Community reverse-engineering, GitHub issue #25"
84+
}
6985
}
7086
}

custom_components/midea_heatpump_hws/profile_manager.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ def save_profile(self, name: str, config: dict[str, Any], model: str = "Custom")
106106
"condensor_temp": config.get("condensor_temp_register", 103),
107107
"outdoor_temp": config.get("outdoor_temp_register", 104),
108108
"exhaust_temp": config.get("exhaust_temp_register", 105),
109-
"suction_temp": config.get("suction_temp_register", 106)
109+
"suction_temp": config.get("suction_temp_register", 106),
110+
**({} if config.get("heater_assist_register") is None else {"heater_assist_register": config["heater_assist_register"]}),
111+
**({} if config.get("sanitize_state_register") is None else {"sanitize_state_register": config["sanitize_state_register"]}),
110112
},
111113

112114
"mode_values": {
@@ -239,7 +241,12 @@ def apply_profile_to_config(self, profile_data: dict[str, Any], user_input: dict
239241
config["outdoor_temp_register"] = registers.get("outdoor_temp", 104)
240242
config["exhaust_temp_register"] = registers.get("exhaust_temp", 105)
241243
config["suction_temp_register"] = registers.get("suction_temp", 106)
242-
244+
# Optional diagnostic registers (only set if present in profile)
245+
if "heater_assist_register" in registers:
246+
config["heater_assist_register"] = registers["heater_assist_register"]
247+
if "sanitize_state_register" in registers:
248+
config["sanitize_state_register"] = registers["sanitize_state_register"]
249+
243250
# Apply mode values
244251
mode_values = profile_data.get("mode_values", {})
245252
config["eco_mode_value"] = mode_values.get("eco", 1)

0 commit comments

Comments
 (0)