11"""The Midea Heat Pump Water Heater integration."""
22from __future__ import annotations
33
4+ import json
45import logging
6+ from datetime import datetime
7+ from pathlib import Path
58
69from homeassistant .config_entries import ConfigEntry
710from 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
1015from .const import DOMAIN
1116from .coordinator import MideaModbusCoordinator
17+ from .profile_manager import ProfileManager
1218
1319_LOGGER = logging .getLogger (__name__ )
1420
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
2444async 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
6594async 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+ )
0 commit comments