Skip to content

Commit c1529ea

Browse files
committed
v0.2.3 fixes #16, #18
1 parent 56f9a4c commit c1529ea

File tree

9 files changed

+454
-151
lines changed

9 files changed

+454
-151
lines changed

README.md

Lines changed: 167 additions & 111 deletions
Large diffs are not rendered by default.

custom_components/midea_heatpump_hws/__init__.py

Lines changed: 197 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
"""The Midea Heat Pump Water Heater integration."""
22
from __future__ import annotations
33

4+
import json
45
import logging
6+
from datetime import datetime
7+
from pathlib import Path
58

69
from homeassistant.config_entries import ConfigEntry
710
from homeassistant.const import Platform
8-
from homeassistant.core import HomeAssistant
11+
from homeassistant.core import HomeAssistant, ServiceCall
12+
from homeassistant.helpers import config_validation as cv
13+
import voluptuous as vol
914

1015
from .const import DOMAIN
1116
from .coordinator import MideaModbusCoordinator
17+
from .profile_manager import ProfileManager
1218

1319
_LOGGER = logging.getLogger(__name__)
1420

@@ -20,6 +26,20 @@
2026
Platform.SELECT,
2127
]
2228

29+
# Service schemas
30+
SERVICE_EXPORT_PROFILE = "export_profile"
31+
SERVICE_IMPORT_PROFILE = "import_profile"
32+
33+
EXPORT_PROFILE_SCHEMA = vol.Schema({
34+
vol.Optional("entry_id"): cv.string,
35+
vol.Optional("name", default="My Profile"): cv.string,
36+
vol.Optional("model", default="Custom"): cv.string,
37+
})
38+
39+
IMPORT_PROFILE_SCHEMA = vol.Schema({
40+
vol.Required("profile_json"): cv.string,
41+
})
42+
2343

2444
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
2545
"""Set up Midea Heat Pump Water Heater from a config entry."""
@@ -43,6 +63,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
4363
# Setup update listener for options flow
4464
entry.async_on_unload(entry.add_update_listener(update_listener))
4565

66+
# Register services (only once, not per entry)
67+
if not hass.services.has_service(DOMAIN, SERVICE_EXPORT_PROFILE):
68+
await _register_services(hass)
69+
4670
return True
4771

4872

@@ -58,11 +82,182 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
5882

5983
# Remove from data
6084
hass.data[DOMAIN].pop(entry.entry_id)
85+
86+
# Remove services if no more entries
87+
if not hass.data[DOMAIN]:
88+
hass.services.async_remove(DOMAIN, SERVICE_EXPORT_PROFILE)
89+
hass.services.async_remove(DOMAIN, SERVICE_IMPORT_PROFILE)
6190

6291
return unload_ok
6392

6493

6594
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
6695
"""Handle options update."""
6796
# Reload the integration
68-
await hass.config_entries.async_reload(entry.entry_id)
97+
await hass.config_entries.async_reload(entry.entry_id)
98+
99+
100+
async def _register_services(hass: HomeAssistant) -> None:
101+
"""Register integration services."""
102+
103+
async def handle_export_profile(call: ServiceCall) -> None:
104+
"""Handle profile export service call."""
105+
# Get the entry to export
106+
entry_id = call.data.get("entry_id")
107+
if entry_id:
108+
if entry_id not in hass.data[DOMAIN]:
109+
_LOGGER.error("Invalid entry_id: %s", entry_id)
110+
return
111+
else:
112+
# Use first entry if not specified
113+
if not hass.data[DOMAIN]:
114+
_LOGGER.error("No configured entries to export")
115+
return
116+
entry_id = list(hass.data[DOMAIN].keys())[0]
117+
118+
config = hass.data[DOMAIN][entry_id]["config"]
119+
120+
# Create profile
121+
profile_manager = ProfileManager(hass)
122+
profile_data = {
123+
"name": call.data.get("name", config.get("name", "My Profile")),
124+
"model": call.data.get("model", "Custom"),
125+
"exported": datetime.now().isoformat(),
126+
"version": "0.2.2",
127+
"integration": "midea_heatpump_hws",
128+
"connection": {
129+
"port": config.get("port", 502),
130+
"modbus_unit": config.get("modbus_unit", 1),
131+
"scan_interval": config.get("scan_interval", 60)
132+
},
133+
"registers": {
134+
"power": config.get("power_register", 0),
135+
"mode": config.get("mode_register", 1),
136+
"current_temp": config.get("temp_register", 102),
137+
"target_temp": config.get("target_temp_register", 2),
138+
"tank_top_temp": config.get("tank_top_temp_register", 101),
139+
"tank_bottom_temp": config.get("tank_bottom_temp_register", 102),
140+
"condensor_temp": config.get("condensor_temp_register", 103),
141+
"outdoor_temp": config.get("outdoor_temp_register", 104),
142+
"exhaust_temp": config.get("exhaust_temp_register", 105),
143+
"suction_temp": config.get("suction_temp_register", 106)
144+
},
145+
"mode_values": {
146+
"eco": config.get("eco_mode_value", 1),
147+
"performance": config.get("performance_mode_value", 2),
148+
"electric": config.get("electric_mode_value", 4)
149+
},
150+
"scaling": {
151+
"current_temp": {
152+
"offset": config.get("temp_offset", -15.0),
153+
"scale": config.get("temp_scale", 0.5)
154+
},
155+
"target_temp": {
156+
"offset": config.get("target_temp_offset", 0.0),
157+
"scale": config.get("target_temp_scale", 1.0)
158+
},
159+
"sensors": {
160+
"offset": config.get("sensors_temp_offset", -15.0),
161+
"scale": config.get("sensors_temp_scale", 0.5)
162+
}
163+
},
164+
"temp_limits": {
165+
"eco": {
166+
"min": config.get("eco_min_temp", 60),
167+
"max": config.get("eco_max_temp", 65)
168+
},
169+
"performance": {
170+
"min": config.get("performance_min_temp", 60),
171+
"max": config.get("performance_max_temp", 70)
172+
},
173+
"electric": {
174+
"min": config.get("electric_min_temp", 60),
175+
"max": config.get("electric_max_temp", 70)
176+
}
177+
},
178+
"defaults": {
179+
"target_temperature": config.get("target_temperature", 65),
180+
"enable_additional_sensors": config.get("enable_additional_sensors", True)
181+
}
182+
}
183+
184+
# Save to www folder for download
185+
www_path = hass.config.path("www")
186+
if not Path(www_path).exists():
187+
Path(www_path).mkdir()
188+
189+
# Create filename
190+
safe_name = call.data.get("name", "profile").lower().replace(" ", "_")
191+
safe_name = "".join(c for c in safe_name if c.isalnum() or c == "_")
192+
filename = f"midea_profile_{safe_name}_{datetime.now():%Y%m%d_%H%M%S}.json"
193+
file_path = Path(www_path) / filename
194+
195+
# Write file
196+
with open(file_path, 'w') as f:
197+
json.dump(profile_data, f, indent=2)
198+
199+
# Create persistent notification with download link
200+
await hass.services.async_call(
201+
"persistent_notification",
202+
"create",
203+
{
204+
"title": "🎉 Profile Exported Successfully!",
205+
"message": (
206+
f"Your water heater configuration has been exported.\n\n"
207+
f"**[📥 Download Profile](/local/{filename})**\n\n"
208+
f"**→ Right-click the link above and select 'Save Link As...' to download**\n\n"
209+
f"Share this profile with others using the same model, "
210+
f"or keep it as a backup of your configuration."
211+
),
212+
"notification_id": f"midea_profile_export_{datetime.now():%Y%m%d%H%M%S}"
213+
}
214+
)
215+
216+
_LOGGER.info("Profile exported to %s", file_path)
217+
218+
async def handle_import_profile(call: ServiceCall) -> None:
219+
"""Handle profile import service call."""
220+
try:
221+
profile_json = call.data.get("profile_json")
222+
profile_data = json.loads(profile_json)
223+
224+
# Save as custom profile
225+
profile_manager = ProfileManager(hass)
226+
saved_path = profile_manager.import_profile(profile_data)
227+
228+
if saved_path:
229+
await hass.services.async_call(
230+
"persistent_notification",
231+
"create",
232+
{
233+
"title": "✅ Profile Imported",
234+
"message": (
235+
f"Profile '{profile_data.get('name', 'Imported')}' has been imported successfully.\n\n"
236+
f"You can now use it when adding a new water heater."
237+
),
238+
"notification_id": f"midea_profile_import_{datetime.now():%Y%m%d%H%M%S}"
239+
}
240+
)
241+
_LOGGER.info("Profile imported successfully: %s", saved_path)
242+
else:
243+
_LOGGER.error("Failed to import profile")
244+
245+
except json.JSONDecodeError as e:
246+
_LOGGER.error("Invalid JSON in profile import: %s", e)
247+
except Exception as e:
248+
_LOGGER.error("Error importing profile: %s", e)
249+
250+
# Register services
251+
hass.services.async_register(
252+
DOMAIN,
253+
SERVICE_EXPORT_PROFILE,
254+
handle_export_profile,
255+
schema=EXPORT_PROFILE_SCHEMA,
256+
)
257+
258+
hass.services.async_register(
259+
DOMAIN,
260+
SERVICE_IMPORT_PROFILE,
261+
handle_import_profile,
262+
schema=IMPORT_PROFILE_SCHEMA,
263+
)

custom_components/midea_heatpump_hws/config_flow.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,22 +221,26 @@ async def async_step_load_profile(
221221
# No profiles available, go to manual setup
222222
return await self.async_step_connection()
223223

224-
# Create display names for profiles
224+
# Create display names for profiles - SIMPLE VERSION
225225
profile_options = {}
226226
for profile_id, profile_info in profiles.items():
227227
display_name = f"{profile_info['name']} ({profile_info['model']}) - {profile_info['type']}"
228228
profile_options[profile_id] = display_name
229229

230230
return self.async_show_form(
231231
step_id="load_profile",
232-
data_schema=STEP_PROFILE_SELECT_SCHEMA(profile_options),
232+
data_schema=vol.Schema({
233+
vol.Required("profile"): vol.In(profile_options),
234+
vol.Required(CONF_HOST): str,
235+
vol.Optional(CONF_NAME, default="Hot Water System"): str,
236+
}),
233237
description_placeholders={
234238
"title": "Select Profile",
235239
"description": "Choose a profile and enter your connection details"
236240
},
237241
)
238242

239-
# Load the selected profile
243+
# Load the selected profile - SIMPLE VERSION
240244
profile_data = self.profile_manager.load_profile(user_input["profile"])
241245
if profile_data:
242246
# Apply profile to configuration

custom_components/midea_heatpump_hws/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
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.2"
11+
"version": "0.2.3"
1212
}

custom_components/midea_heatpump_hws/select.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ async def async_setup_entry(
2222
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
2323
config = hass.data[DOMAIN][config_entry.entry_id]["config"]
2424

25-
entities = [MideaModeSelect(coordinator, config)]
25+
# Include host in entity name to make it unique
26+
host_suffix = f" ({config['host']})"
27+
28+
entities = [MideaModeSelect(coordinator, config, host_suffix)]
2629
async_add_entities(entities)
2730

2831

@@ -32,14 +35,15 @@ class MideaModeSelect(CoordinatorEntity, SelectEntity):
3235
def __init__(
3336
self,
3437
coordinator: MideaModbusCoordinator,
35-
config: dict
38+
config: dict,
39+
host_suffix: str
3640
):
3741
"""Initialize the mode selector."""
3842
super().__init__(coordinator)
3943
self._config = config
4044

41-
# Entity attributes
42-
self._attr_name = "Operation Mode"
45+
# Entity attributes - include host for uniqueness
46+
self._attr_name = f"Operation Mode{host_suffix}"
4347
self._attr_unique_id = f"midea_{config['host']}_{config[CONF_MODBUS_UNIT]}_mode_select"
4448
self._attr_icon = "mdi:cog"
4549
self._attr_options = ["eco", "performance", "electric"]
@@ -50,16 +54,16 @@ def device_info(self):
5054
return {
5155
"identifiers": {(DOMAIN, f"{self._config['host']}_{self._config[CONF_MODBUS_UNIT]}")},
5256
"name": f"Midea Heat Pump ({self._config['host']})",
53-
"manufacturer": "0xAHA",
57+
"manufacturer": "Midea",
5458
"model": "Heat Pump Water Heater",
5559
}
5660

5761
@property
5862
def current_option(self):
5963
"""Return the selected entity option."""
6064
if self.coordinator.data:
61-
return self.coordinator.data.get("mode", "Eco")
62-
return "Eco"
65+
return self.coordinator.data.get("mode", "eco")
66+
return "eco"
6367

6468
@property
6569
def available(self) -> bool:

custom_components/midea_heatpump_hws/sensor.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,16 @@ async def async_setup_entry(
4242

4343
entities = []
4444

45+
# Include host in entity names to make them unique
46+
host_suffix = f" ({config['host']})"
47+
4548
# Main temperature sensor
4649
entities.append(
4750
MideaTemperatureSensor(
4851
coordinator,
4952
config,
5053
"current_temp",
51-
"Current Temperature",
54+
f"Current Temperature{host_suffix}",
5255
config[CONF_TEMP_REGISTER],
5356
use_scaling=True
5457
)
@@ -60,7 +63,7 @@ async def async_setup_entry(
6063
coordinator,
6164
config,
6265
"target_temp",
63-
"Target Temperature",
66+
f"Target Temperature{host_suffix}",
6467
config[CONF_TARGET_TEMP_REGISTER],
6568
use_scaling=False
6669
)
@@ -69,12 +72,12 @@ async def async_setup_entry(
6972
# Additional temperature sensors if enabled
7073
if config.get(CONF_ENABLE_ADDITIONAL_SENSORS, True):
7174
sensor_configs = [
72-
("tank_top_temp", "Tank Top Temperature", config.get(CONF_TANK_TOP_TEMP_REGISTER), True),
73-
("tank_bottom_temp", "Tank Bottom Temperature", config.get(CONF_TANK_BOTTOM_TEMP_REGISTER), True),
74-
("condensor_temp", "Condensor Temperature", config.get(CONF_CONDENSOR_TEMP_REGISTER), False),
75-
("outdoor_temp", "Outdoor Temperature", config.get(CONF_OUTDOOR_TEMP_REGISTER), False),
76-
("exhaust_temp", "Exhaust Gas Temperature", config.get(CONF_EXHAUST_TEMP_REGISTER), False),
77-
("suction_temp", "Suction Temperature", config.get(CONF_SUCTION_TEMP_REGISTER), False),
75+
("tank_top_temp", f"Tank Top Temperature{host_suffix}", config.get(CONF_TANK_TOP_TEMP_REGISTER), True),
76+
("tank_bottom_temp", f"Tank Bottom Temperature{host_suffix}", config.get(CONF_TANK_BOTTOM_TEMP_REGISTER), True),
77+
("condensor_temp", f"Condensor Temperature{host_suffix}", config.get(CONF_CONDENSOR_TEMP_REGISTER), False),
78+
("outdoor_temp", f"Outdoor Temperature{host_suffix}", config.get(CONF_OUTDOOR_TEMP_REGISTER), False),
79+
("exhaust_temp", f"Exhaust Gas Temperature{host_suffix}", config.get(CONF_EXHAUST_TEMP_REGISTER), False),
80+
("suction_temp", f"Suction Temperature{host_suffix}", config.get(CONF_SUCTION_TEMP_REGISTER), False),
7881
]
7982

8083
for sensor_id, name, register, use_scaling in sensor_configs:
@@ -112,7 +115,7 @@ def __init__(
112115
self._register = register
113116
self._use_scaling = use_scaling
114117

115-
# Entity attributes
118+
# Entity attributes - name already includes host for uniqueness
116119
self._attr_name = name
117120
self._attr_unique_id = f"midea_{config['host']}_{config[CONF_MODBUS_UNIT]}_{sensor_id}"
118121
self._attr_device_class = SensorDeviceClass.TEMPERATURE
@@ -125,7 +128,7 @@ def device_info(self):
125128
return {
126129
"identifiers": {(DOMAIN, f"{self._config['host']}_{self._config[CONF_MODBUS_UNIT]}")},
127130
"name": f"Midea Heat Pump ({self._config['host']})",
128-
"manufacturer": "0xAHA",
131+
"manufacturer": "Midea",
129132
"model": "Heat Pump Water Heater",
130133
}
131134

0 commit comments

Comments
 (0)