1515 APIVersion ,
1616 DeviceInfo as EsphomeDeviceInfo ,
1717 EncryptionPlaintextAPIError ,
18+ ExecuteServiceResponse ,
1819 HomeassistantServiceCall ,
1920 InvalidAuthAPIError ,
2021 InvalidEncryptionKeyAPIError ,
2122 LogLevel ,
2223 ReconnectLogic ,
2324 RequiresEncryptionAPIError ,
25+ SupportsResponseType ,
2426 UserService ,
2527 UserServiceArgType ,
2628 ZWaveProxyRequest ,
4446 EventStateChangedData ,
4547 HomeAssistant ,
4648 ServiceCall ,
49+ ServiceResponse ,
4750 State ,
51+ SupportsResponse ,
4852 callback ,
4953)
5054from homeassistant .exceptions import (
5862 device_registry as dr ,
5963 entity_registry as er ,
6064 issue_registry as ir ,
61- json ,
65+ json as json_helper ,
6266 template ,
6367)
6468from homeassistant .helpers .device_registry import format_mac
7074)
7175from homeassistant .helpers .service import async_set_service_schema
7276from homeassistant .helpers .template import Template
77+ from homeassistant .util .json import json_loads_object
7378
7479from .bluetooth import async_connect_scanner
7580from .const import (
9196
9297# Import config flow so that it's added to the registry
9398from .entry_data import ESPHomeConfigEntry , RuntimeEntryData
99+ from .enum_mapper import EsphomeEnumMapper
94100
95101DEVICE_CONFLICT_ISSUE_FORMAT = "device_conflict-{}"
96102UNPACK_UINT32_BE = struct .Struct (">I" ).unpack_from
@@ -367,7 +373,7 @@ async def _handle_service_call_with_response(
367373 response_dict = {"response" : action_response }
368374
369375 # JSON encode response data for ESPHome
370- response_data = json .json_bytes (response_dict )
376+ response_data = json_helper .json_bytes (response_dict )
371377
372378 except (
373379 ServiceNotFound ,
@@ -1150,13 +1156,52 @@ class ServiceMetadata(NamedTuple):
11501156}
11511157
11521158
1153- @callback
1154- def execute_service (
1155- entry_data : RuntimeEntryData , service : UserService , call : ServiceCall
1156- ) -> None :
1157- """Execute a service on a node."""
1159+ async def execute_service (
1160+ entry_data : RuntimeEntryData ,
1161+ service : UserService ,
1162+ call : ServiceCall ,
1163+ * ,
1164+ supports_response : SupportsResponseType ,
1165+ ) -> ServiceResponse :
1166+ """Execute a service on a node and optionally wait for response."""
1167+ # Determine if we should wait for a response
1168+ # NONE: fire and forget
1169+ # OPTIONAL/ONLY/STATUS: always wait for success/error confirmation
1170+ wait_for_response = supports_response != SupportsResponseType .NONE
1171+
1172+ if not wait_for_response :
1173+ # Fire and forget - no response expected
1174+ try :
1175+ await entry_data .client .execute_service (service , call .data )
1176+ except APIConnectionError as err :
1177+ raise HomeAssistantError (
1178+ translation_domain = DOMAIN ,
1179+ translation_key = "action_call_failed" ,
1180+ translation_placeholders = {
1181+ "call_name" : service .name ,
1182+ "device_name" : entry_data .name ,
1183+ "error" : str (err ),
1184+ },
1185+ ) from err
1186+ else :
1187+ return None
1188+
1189+ # Determine if we need response_data from ESPHome
1190+ # ONLY: always need response_data
1191+ # OPTIONAL: only if caller requested it
1192+ # STATUS: never need response_data (just success/error)
1193+ need_response_data = supports_response == SupportsResponseType .ONLY or (
1194+ supports_response == SupportsResponseType .OPTIONAL and call .return_response
1195+ )
1196+
11581197 try :
1159- entry_data .client .execute_service (service , call .data )
1198+ response : (
1199+ ExecuteServiceResponse | None
1200+ ) = await entry_data .client .execute_service (
1201+ service ,
1202+ call .data ,
1203+ return_response = need_response_data ,
1204+ )
11601205 except APIConnectionError as err :
11611206 raise HomeAssistantError (
11621207 translation_domain = DOMAIN ,
@@ -1167,13 +1212,64 @@ def execute_service(
11671212 "error" : str (err ),
11681213 },
11691214 ) from err
1215+ except TimeoutError as err :
1216+ raise HomeAssistantError (
1217+ translation_domain = DOMAIN ,
1218+ translation_key = "action_call_timeout" ,
1219+ translation_placeholders = {
1220+ "call_name" : service .name ,
1221+ "device_name" : entry_data .name ,
1222+ },
1223+ ) from err
1224+
1225+ assert response is not None
1226+
1227+ if not response .success :
1228+ raise HomeAssistantError (
1229+ translation_domain = DOMAIN ,
1230+ translation_key = "action_call_failed" ,
1231+ translation_placeholders = {
1232+ "call_name" : service .name ,
1233+ "device_name" : entry_data .name ,
1234+ "error" : response .error_message ,
1235+ },
1236+ )
1237+
1238+ # Parse and return response data as JSON if we requested it
1239+ if need_response_data and response .response_data :
1240+ try :
1241+ return json_loads_object (response .response_data )
1242+ except ValueError as err :
1243+ raise HomeAssistantError (
1244+ translation_domain = DOMAIN ,
1245+ translation_key = "action_call_failed" ,
1246+ translation_placeholders = {
1247+ "call_name" : service .name ,
1248+ "device_name" : entry_data .name ,
1249+ "error" : f"Invalid JSON response: { err } " ,
1250+ },
1251+ ) from err
1252+ return None
11701253
11711254
11721255def build_service_name (device_info : EsphomeDeviceInfo , service : UserService ) -> str :
11731256 """Build a service name for a node."""
11741257 return f"{ device_info .name .replace ('-' , '_' )} _{ service .name } "
11751258
11761259
1260+ # Map ESPHome SupportsResponseType to Home Assistant SupportsResponse
1261+ # STATUS (100) is ESPHome-specific: waits for success/error internally but
1262+ # doesn't return data to HA, so it maps to NONE from HA's perspective
1263+ _RESPONSE_TYPE_MAPPER = EsphomeEnumMapper [SupportsResponseType , SupportsResponse ](
1264+ {
1265+ SupportsResponseType .NONE : SupportsResponse .NONE ,
1266+ SupportsResponseType .OPTIONAL : SupportsResponse .OPTIONAL ,
1267+ SupportsResponseType .ONLY : SupportsResponse .ONLY ,
1268+ SupportsResponseType .STATUS : SupportsResponse .NONE ,
1269+ }
1270+ )
1271+
1272+
11771273@callback
11781274def _async_register_service (
11791275 hass : HomeAssistant ,
@@ -1205,11 +1301,21 @@ def _async_register_service(
12051301 "selector" : metadata .selector ,
12061302 }
12071303
1304+ # Get the supports_response from the service, defaulting to NONE
1305+ esphome_supports_response = service .supports_response or SupportsResponseType .NONE
1306+ ha_supports_response = _RESPONSE_TYPE_MAPPER .from_esphome (esphome_supports_response )
1307+
12081308 hass .services .async_register (
12091309 DOMAIN ,
12101310 service_name ,
1211- partial (execute_service , entry_data , service ),
1311+ partial (
1312+ execute_service ,
1313+ entry_data ,
1314+ service ,
1315+ supports_response = esphome_supports_response ,
1316+ ),
12121317 vol .Schema (schema ),
1318+ supports_response = ha_supports_response ,
12131319 )
12141320 async_set_service_schema (
12151321 hass ,
0 commit comments