From 851778d256216826f47616aa1440d43b031e54b7 Mon Sep 17 00:00:00 2001 From: Alvaro Duarte <59612788+ad-ha@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:55:41 +0100 Subject: [PATCH] 0.7.0 --- custom_components/mg_saic/__init__.py | 9 + custom_components/mg_saic/api.py | 350 +++++++------ custom_components/mg_saic/binary_sensor.py | 2 + custom_components/mg_saic/button.py | 69 --- custom_components/mg_saic/climate.py | 6 +- custom_components/mg_saic/config_flow.py | 19 +- custom_components/mg_saic/const.py | 8 +- custom_components/mg_saic/coordinator.py | 76 ++- custom_components/mg_saic/lock.py | 65 ++- custom_components/mg_saic/manifest.json | 2 +- custom_components/mg_saic/sensor.py | 491 +++++++++++------- custom_components/mg_saic/services.py | 150 +++--- custom_components/mg_saic/services.yaml | 122 ++--- custom_components/mg_saic/switch.py | 268 +++++++--- .../mg_saic/translations/en.json | 213 ++++---- .../mg_saic/translations/es.json | 213 ++++---- 16 files changed, 1192 insertions(+), 871 deletions(-) diff --git a/custom_components/mg_saic/__init__.py b/custom_components/mg_saic/__init__.py index b19d119..c658364 100644 --- a/custom_components/mg_saic/__init__.py +++ b/custom_components/mg_saic/__init__.py @@ -47,6 +47,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN].setdefault("coordinators_by_vin", {}) hass.data[DOMAIN]["coordinators_by_vin"][vin] = coordinator + # Register an update listener to handle options updates + entry.async_on_unload(entry.add_update_listener(update_listener)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Register Services @@ -59,6 +62,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return False +async def update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + coordinator = hass.data[DOMAIN][f"{entry.entry_id}_coordinator"] + await coordinator.async_update_options(entry.options) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/custom_components/mg_saic/api.py b/custom_components/mg_saic/api.py index f712e95..e6e0fa3 100644 --- a/custom_components/mg_saic/api.py +++ b/custom_components/mg_saic/api.py @@ -1,7 +1,7 @@ import asyncio from saic_ismart_client_ng import SaicApi from saic_ismart_client_ng.model import SaicApiConfiguration -from .const import LOGGER, REGION_BASE_URIS, BatterySoc, VehicleWindowId +from .const import LOGGER, REGION_BASE_URIS, BatterySoc class SAICMGAPIClient: @@ -20,10 +20,46 @@ def __init__( self.saic_api = None self.username_is_email = username_is_email self.country_code = country_code + self._login_lock = asyncio.Lock() if region is None: LOGGER.debug("No region specified, defaulting to Europe.") self.region_name = region if region is not None else "Europe" + # GENERAL API HANDLING + async def _ensure_initialized(self): + """Ensure that the APIs are initialized and logged in.""" + if not self.saic_api or not self.saic_api.is_logged_in: + async with self._login_lock: + if not self.saic_api or not self.saic_api.is_logged_in: + await self.login() + + async def _make_api_call(self, api_call, *args, **kwargs): + """Wrap API calls to handle token expiration and re-login.""" + await self._ensure_initialized() + try: + return await api_call(*args, **kwargs) + except Exception as e: + error_message = str(e).lower() + if ( + "invalid session" in error_message + or "token expired" in error_message + or "not logged in" in error_message + ): + LOGGER.warning( + "Token expired or session invalid, attempting to re-login." + ) + async with self._login_lock: + if not self.saic_api.is_logged_in: + await self.login() + try: + return await api_call(*args, **kwargs) + except Exception as retry_e: + LOGGER.error(f"API call failed after re-login: {retry_e}") + raise + else: + LOGGER.error(f"API call failed: {e}") + raise + async def login(self): """Authenticate with the API.""" # Get the base_url for this region @@ -48,21 +84,30 @@ async def login(self): try: await self.saic_api.login() + if not self.saic_api.is_logged_in: + raise Exception("Login failed") LOGGER.debug("Login successful, initializing vehicle APIs.") except Exception as e: LOGGER.error("Failed to log in to MG SAIC API: %s", e) + self.saic_api = None raise - async def _ensure_initialized(self): - """Ensure that the APIs are initialized.""" - if not self.saic_api: - await self.login() + # GET VEHICLE DATA + async def get_charging_info(self): + """Retrieve charging information.""" + try: + charging_status = await self._make_api_call( + self.saic_api.get_vehicle_charging_management_data, self.vin + ) + return charging_status + except Exception as e: + LOGGER.error("Error retrieving charging information: %s", e) + return None async def get_vehicle_info(self): """Retrieve vehicle information.""" - await self._ensure_initialized() try: - vehicle_list_resp = await self.saic_api.vehicle_list() + vehicle_list_resp = await self._make_api_call(self.saic_api.vehicle_list) vehicles = vehicle_list_resp.vinList self.vin = vehicles[0].vin if vehicles else None return vehicles @@ -72,29 +117,77 @@ async def get_vehicle_info(self): async def get_vehicle_status(self): """Retrieve vehicle status.""" - await self._ensure_initialized() try: - vehicle_status = await self.saic_api.get_vehicle_status(self.vin) + vehicle_status = await self._make_api_call( + self.saic_api.get_vehicle_status, self.vin + ) return vehicle_status except Exception as e: LOGGER.error("Error retrieving vehicle status: %s", e) return None - async def get_charging_info(self): - """Retrieve charging information.""" - await self._ensure_initialized() + # ACTIONS + + # ALARM CONTROL + async def trigger_alarm( + self, vin: str, with_horn=True, with_lights=True, should_stop=False + ): + """Trigger or stop the alarm (Find My Car feature).""" try: - charging_status = await self.saic_api.get_vehicle_charging_management_data( - self.vin + await self._make_api_call( + self.saic_api.control_find_my_car, + vin=vin, + should_stop=should_stop, + with_horn=with_horn, + with_lights=with_lights, ) - return charging_status except Exception as e: - LOGGER.error("Error retrieving charging information: %s", e) - return None + LOGGER.error(f"Error triggering alarm for VIN {vin}: {e}") + raise + + # CHARGING CONTROL + async def send_vehicle_charging_control(self, vin, action): + """Send a charging control command to the vehicle.""" + try: + LOGGER.debug(f"Charging control - VIN: {vin}, action: {action}") + # Use the control_charging method from the saic-python-client-ng library + if action == "start": + await self._make_api_call( + self.saic_api.control_charging, vin=vin, stop_charging=False + ) + else: + await self._make_api_call( + self.saic_api.control_charging, vin=vin, stop_charging=True + ) + LOGGER.info(f"Charging {action} command sent successfully for VIN: {vin}") + except Exception as e: + LOGGER.error(f"Error sending charging {action} command for VIN {vin}: {e}") + raise + + async def send_vehicle_charging_ptc_heat(self, vin, action): + """Send a battery heating control command to the vehicle.""" + try: + LOGGER.debug(f"Battery heating control - VIN: {vin}, action: {action}") + # Use the control_battery_heating method from the saic-python-client-ng library + if action == "start": + await self._make_api_call( + self.saic_api.control_battery_heating, vin=vin, enable=False + ) + else: + await self._make_api_call( + self.saic_api.control_battery_heating, vin=vin, enable=True + ) + LOGGER.info( + f"Battery heating {action} command sent successfully for VIN: {vin}" + ) + except Exception as e: + LOGGER.error( + f"Error sending battery heating {action} command for VIN {vin}: {e}" + ) + raise async def set_target_soc(self, vin, target_soc_percentage): """Set the target SOC of the vehicle.""" - await self._ensure_initialized() try: # Map percentage to BatterySoc enum percentage_to_enum = { @@ -111,9 +204,10 @@ async def set_target_soc(self, vin, target_soc_percentage): raise ValueError( f"Invalid target SOC percentage: {target_soc_percentage}" ) - # Call the method with the enum value - await self.saic_api.set_target_battery_soc(vin, battery_soc) + await self._make_api_call( + self.saic_api.set_target_battery_soc, vin, battery_soc + ) LOGGER.info( "Set target SOC to %d%% for VIN: %s", target_soc_percentage, vin ) @@ -121,33 +215,55 @@ async def set_target_soc(self, vin, target_soc_percentage): LOGGER.error("Error setting target SOC for VIN %s: %s", vin, e) raise - # Climate control actions + # CLIMATE CONTROL + async def control_heated_seats(self, vin, action): + """Control the heated seats.""" + try: + await self._make_api_call(self.saic_api.control_heated_seats, vin, action) + LOGGER.info("Heated seats controlled successfully.") + except Exception as e: + LOGGER.error("Error controlling heated seats: %s", e) + + async def control_rear_window_heat(self, vin, action): + """Control the rear window heat.""" + try: + if action.lower() == "start": + enable = True + elif action.lower() == "stop": + enable = False + else: + raise ValueError( + f"Invalid action '{action}'. Expected 'start' or 'stop'." + ) + + await self._make_api_call( + self.saic_api.control_rear_window_heat, vin, enable=enable + ) + LOGGER.info("Rear window heat %sed successfully.", action) + except Exception as e: + LOGGER.error("Error controlling rear window heat: %s", e) + raise + async def start_ac(self, vin): """Start the vehicle AC.""" - await self._ensure_initialized() try: - await self.saic_api.start_ac(vin) + await self._make_api_call(self.saic_api.start_ac, vin) LOGGER.info("AC started successfully.") except Exception as e: LOGGER.error("Error starting AC: %s", e) - - async def stop_ac(self, vin): - """Stop the vehicle AC.""" - await self._ensure_initialized() - try: - await self.saic_api.stop_ac(vin) - LOGGER.info("AC stopped successfully.") - except Exception as e: - LOGGER.error("Error stopping AC: %s", e) + raise async def start_climate(self, vin, temperature, fan_speed): """Start the vehicle AC with temperature and fan speed settings.""" - await self._ensure_initialized() try: # Map temperature in Celsius to temperature_idx expected by the API temperature_idx = self._map_temperature_to_idx(temperature) - await self.saic_api.control_climate( - vin, fan_speed=fan_speed, ac_on=True, temperature_idx=temperature_idx + await self._make_api_call( + self.saic_api.control_climate, + vin, + fan_speed=fan_speed, + ac_on=True, + temperature_idx=temperature_idx, ) LOGGER.info( "AC started with temperature %s°C and fan speed %s for VIN: %s", @@ -159,6 +275,24 @@ async def start_climate(self, vin, temperature, fan_speed): LOGGER.error("Error starting AC with settings for VIN %s: %s", vin, e) raise + async def start_front_defrost(self, vin): + """Start the front defrost.""" + try: + await self._make_api_call(self.saic_api.start_front_defrost, vin) + LOGGER.info("Front defrost started successfully.") + except Exception as e: + LOGGER.error("Error starting front defrost: %s", e) + raise + + async def stop_ac(self, vin): + """Stop the vehicle AC.""" + try: + await self._make_api_call(self.saic_api.stop_ac, vin) + LOGGER.info("AC stopped successfully.") + except Exception as e: + LOGGER.error("Error stopping AC: %s", e) + raise + def _map_temperature_to_idx(self, temperature): """Map temperature in Celsius to temperature_idx expected by the API.""" temperature_to_idx = { @@ -183,147 +317,61 @@ def _map_temperature_to_idx(self, temperature): raise ValueError("Invalid temperature value. Must be between 16 and 30°C.") return idx - # Locks control actions + # LOCKS CONTROL + async def control_charging_port_lock(self, vin: str, unlock: bool): + """Control the charging port lock (lock/unlock).""" + try: + await self._make_api_call( + self.saic_api.control_charging_port_lock, vin=vin, unlock=unlock + ) + LOGGER.info( + "Charging port %s successfully for VIN: %s", + "unlocked" if unlock else "locked", + vin, + ) + except Exception as e: + LOGGER.error("Error controlling charging port lock for VIN %s: %s", vin, e) + raise + async def lock_vehicle(self, vin): """Lock the vehicle.""" - await self._ensure_initialized() try: - await self.saic_api.lock_vehicle(vin) + await self._make_api_call(self.saic_api.lock_vehicle, vin) LOGGER.info("Vehicle locked successfully.") except Exception as e: LOGGER.error("Error locking vehicle: %s", e) - async def unlock_vehicle(self, vin): - """Unlock the vehicle.""" - await self._ensure_initialized() - try: - await self.saic_api.unlock_vehicle(vin) - LOGGER.info("Vehicle unlocked successfully.") - except Exception as e: - LOGGER.error("Error unlocking vehicle: %s", e) - async def open_tailgate(self, vin): """Open the vehicle tailgate.""" - await self._ensure_initialized() try: - await self.saic_api.open_tailgate(vin) + await self._make_api_call(self.saic_api.open_tailgate, vin) LOGGER.info("Tailgate opened successfully.") except Exception as e: LOGGER.error("Error opening tailgate: %s", e) - # Alarm control actions - async def trigger_alarm( - self, vin: str, with_horn=True, with_lights=True, should_stop=False - ): - """Trigger or stop the alarm (Find My Car feature).""" - try: - await self.saic_api.control_find_my_car( - vin=vin, - should_stop=should_stop, - with_horn=with_horn, - with_lights=with_lights, - ) - except Exception as e: - LOGGER.error(f"Error triggering alarm for VIN {vin}: {e}") - raise - - # Charging control actions - async def send_vehicle_charging_control(self, vin, action): - """Send a charging control command to the vehicle.""" - await self._ensure_initialized() - try: - LOGGER.debug(f"Charging control - VIN: {vin}, action: {action}") - - # Use the control_charging method from the saic-python-client-ng library - if action == "start": - await self.saic_api.control_charging(vin=vin, stop_charging=False) - else: - await self.saic_api.control_charging(vin=vin, stop_charging=True) - - LOGGER.info(f"Charging {action} command sent successfully for VIN: {vin}") - except Exception as e: - LOGGER.error(f"Error sending charging {action} command for VIN {vin}: {e}") - raise - - async def send_vehicle_charging_ptc_heat(self, vin, action): - """Send a charging control command to the vehicle.""" - await self._ensure_initialized() - try: - LOGGER.debug(f"Battery heating control - VIN: {vin}, action: {action}") - - # Use the control_charging method from the saic-python-client-ng library - if action == "start": - await self.saic_api.control_battery_heating(vin=vin, enable=False) - else: - await self.saic_api.control_battery_heating(vin=vin, enable=True) - - LOGGER.info( - f"Battery heating {action} command sent successfully for VIN: {vin}" - ) - except Exception as e: - LOGGER.error( - f"Error sending battery heating {action} command for VIN {vin}: {e}" - ) - raise - - async def control_rear_window_heat(self, vin, action): - """Control the rear window heat.""" - await self._ensure_initialized() - try: - await self.saic_api.control_rear_window_heat(vin, action) - LOGGER.info("Rear window heat controlled successfully.") - except Exception as e: - LOGGER.error("Error controlling rear window heat: %s", e) - - async def control_heated_seats(self, vin, action): - """Control the heated seats.""" - await self._ensure_initialized() - try: - await self.saic_api.control_heated_seats(vin, action) - LOGGER.info("Heated seats controlled successfully.") - except Exception as e: - LOGGER.error("Error controlling heated seats: %s", e) - - async def start_front_defrost(self, vin): - """Start the front defrost.""" - await self._ensure_initialized() + async def unlock_vehicle(self, vin): + """Unlock the vehicle.""" try: - await self.saic_api.start_front_defrost(vin) - LOGGER.info("Front defrost started successfully.") + await self._make_api_call(self.saic_api.unlock_vehicle, vin) + LOGGER.info("Vehicle unlocked successfully.") except Exception as e: - LOGGER.error("Error starting front defrost: %s", e) + LOGGER.error("Error unlocking vehicle: %s", e) - # NEW FUNCTIONALITIES TO BE REORGANIZED + # WINDOWS CONTROL async def control_sunroof(self, vin, action): """Control the sunroof (open/close).""" - await self._ensure_initialized() try: LOGGER.debug(f"Sunroof control - VIN: {vin}, action: {action}") - if action == "open": - await self.saic_api.control_sunroof(vin=vin, should_open=True) - else: - await self.saic_api.control_sunroof(vin=vin, should_open=False) - LOGGER.info( - f"Sunroof {action} successfully successfully for VIN: {vin}" - ) - except Exception as e: - LOGGER.error("Error controlling sunroof for VIN %s: %s", vin, e) - raise - - async def control_charging_port_lock(self, vin: str, unlock: bool): - """Control the charging port lock (lock/unlock).""" - await self._ensure_initialized() - try: - await self.saic_api.control_charging_port_lock(vin=vin, unlock=unlock) - LOGGER.info( - "Charging port %s successfully for VIN: %s", - "unlocked" if unlock else "locked", - vin, + should_open = action == "open" + await self._make_api_call( + self.saic_api.control_sunroof, vin=vin, should_open=should_open ) + LOGGER.info(f"Sunroof {action} command sent successfully for VIN: {vin}") except Exception as e: - LOGGER.error("Error controlling charging port lock for VIN %s: %s", vin, e) + LOGGER.error("Error controlling sunroof for VIN %s: %s", vin, e) raise + # SESSION MANAGEMENT async def close(self): """Close the client session.""" try: diff --git a/custom_components/mg_saic/binary_sensor.py b/custom_components/mg_saic/binary_sensor.py index 557f516..3292295 100644 --- a/custom_components/mg_saic/binary_sensor.py +++ b/custom_components/mg_saic/binary_sensor.py @@ -205,6 +205,7 @@ async def async_setup_entry(hass, entry, async_add_entities): LOGGER.error("Error setting up MG SAIC binary sensors: %s", e) +# GENERAL VEHICLE BINARY SENSORS class SAICMGBinarySensor(CoordinatorEntity, BinarySensorEntity): """Representation of a MG SAIC binary sensor.""" @@ -277,6 +278,7 @@ def device_info(self): } +# CHARGING SENSORS class SAICMGChargingBinarySensor(CoordinatorEntity, BinarySensorEntity): """Representation of a MG SAIC charging binary sensor.""" diff --git a/custom_components/mg_saic/button.py b/custom_components/mg_saic/button.py index 4a08c50..623101c 100644 --- a/custom_components/mg_saic/button.py +++ b/custom_components/mg_saic/button.py @@ -17,10 +17,7 @@ async def async_setup_entry(hass, entry, async_add_entities): vin = vin_info.vin buttons = [ - SAICMGOpenTailgateButton(coordinator, client, vin_info, vin), SAICMGTriggerAlarmButton(coordinator, client, vin_info, vin), - SAICMGStartFrontDefrostButton(coordinator, client, vin_info, vin), - SAICMGControlRearWindowHeatButton(coordinator, client, vin_info, vin), SAICMGUpdateDataButton(coordinator, client, vin_info, vin), ] @@ -66,24 +63,6 @@ async def delayed_refresh(): LOGGER.warning("Coordinator not found for VIN %s", self._vin) -class SAICMGOpenTailgateButton(SAICMGButton): - """Button to open the tailgate.""" - - def __init__(self, coordinator, client, vin_info, vin): - super().__init__( - coordinator, client, vin_info, vin, "Open Tailgate", "mdi:car-back" - ) - - async def async_press(self): - """Handle the button press.""" - try: - await self._client.open_tailgate(self._vin) - LOGGER.info("Tailgate opened for VIN: %s", self._vin) - await self.schedule_data_refresh() - except Exception as e: - LOGGER.error("Error opening tailgate for VIN %s: %s", self._vin, e) - - class SAICMGTriggerAlarmButton(SAICMGButton): """Button to trigger the vehicle alarm.""" @@ -102,54 +81,6 @@ async def async_press(self): LOGGER.error("Error triggering alarm for VIN %s: %s", self._vin, e) -class SAICMGStartFrontDefrostButton(SAICMGButton): - """Button to start front defrost.""" - - def __init__(self, coordinator, client, vin_info, vin): - super().__init__( - coordinator, - client, - vin_info, - vin, - "Start Front Defrost", - "mdi:car-defrost-front", - ) - - async def async_press(self): - """Handle the button press.""" - try: - await self._client.start_front_defrost(self._vin) - LOGGER.info("Front defrost started for VIN: %s", self._vin) - await self.schedule_data_refresh() - except Exception as e: - LOGGER.error("Error starting front defrost for VIN %s: %s", self._vin, e) - - -class SAICMGControlRearWindowHeatButton(SAICMGButton): - """Button to activate rear window heat.""" - - def __init__(self, coordinator, client, vin_info, vin): - super().__init__( - coordinator, - client, - vin_info, - vin, - "Activate Rear Window Heat", - "mdi:car-defrost-rear", - ) - - async def async_press(self): - """Handle the button press.""" - try: - await self._client.control_rear_window_heat(self._vin, True) - LOGGER.info("Rear window heat activated for VIN: %s", self._vin) - await self.schedule_data_refresh() - except Exception as e: - LOGGER.error( - "Error activating rear window heat for VIN %s: %s", self._vin, e - ) - - class SAICMGUpdateDataButton(SAICMGButton): """Button to manually update vehicle data.""" diff --git a/custom_components/mg_saic/climate.py b/custom_components/mg_saic/climate.py index 816d1e3..77174ff 100644 --- a/custom_components/mg_saic/climate.py +++ b/custom_components/mg_saic/climate.py @@ -56,7 +56,7 @@ def __init__(self, coordinator, client, vin_info, vin): # Initialize with default values self._attr_current_temperature = None - self._attr_target_temperature = 22.0 # Default target temperature + self._attr_target_temperature = 22.0 self._attr_fan_mode = FAN_MEDIUM self._attr_hvac_mode = HVACMode.OFF @@ -80,7 +80,7 @@ def current_temperature(self): status.basicVehicleStatus, "interiorTemperature", None ) if interior_temp is not None and interior_temp != -128: - return interior_temp # Adjust if necessary + return interior_temp return None @property @@ -89,7 +89,7 @@ def hvac_mode(self): status = self.coordinator.data.get("status") if status: ac_status = getattr(status.basicVehicleStatus, "remoteClimateStatus", None) - if ac_status == 1: + if ac_status == 3: return HVACMode.COOL return HVACMode.OFF diff --git a/custom_components/mg_saic/config_flow.py b/custom_components/mg_saic/config_flow.py index d41141b..9f10d52 100644 --- a/custom_components/mg_saic/config_flow.py +++ b/custom_components/mg_saic/config_flow.py @@ -8,6 +8,7 @@ COUNTRY_CODES, UPDATE_INTERVAL, UPDATE_INTERVAL_CHARGING, + UPDATE_INTERVAL_POWERED, REGION_CHOICES, REGION_BASE_URIS, ) @@ -189,14 +190,13 @@ class SAICMGOptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry): """Initialize options flow.""" - self.config_entry = config_entry - async def async_step_init(self, data=None): + async def async_step_init(self, user_input=None): """Manage the options.""" - if data is not None: - return self.async_create_entry(title="", data=data) + if user_input is not None: + return self.async_create_entry(title="", data=user_input) - data = vol.Schema( + data_schema = vol.Schema( { vol.Optional( "scan_interval", @@ -211,7 +211,14 @@ async def async_step_init(self, data=None): int(UPDATE_INTERVAL_CHARGING.total_seconds()), ), ): vol.All(vol.Coerce(int), vol.Range(min=60)), + vol.Optional( + "powered_scan_interval", + default=self.config_entry.options.get( + "powered_scan_interval", + int(UPDATE_INTERVAL_POWERED.total_seconds()), + ), + ): vol.All(vol.Coerce(int), vol.Range(min=60)), } ) - return self.async_show_form(step_id="init", data_schema=data) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/custom_components/mg_saic/const.py b/custom_components/mg_saic/const.py index ec33487..204ffbf 100644 --- a/custom_components/mg_saic/const.py +++ b/custom_components/mg_saic/const.py @@ -97,11 +97,9 @@ # Update Settings -DEFAULT_SCAN_INTERVAL = timedelta(minutes=120) -DEFAULT_CHARGING_SCAN_INTERVAL = timedelta(minutes=10) - -UPDATE_INTERVAL = DEFAULT_SCAN_INTERVAL -UPDATE_INTERVAL_CHARGING = DEFAULT_CHARGING_SCAN_INTERVAL +UPDATE_INTERVAL = timedelta(minutes=120) +UPDATE_INTERVAL_CHARGING = timedelta(minutes=10) +UPDATE_INTERVAL_POWERED = timedelta(minutes=15) # Generic response tresholds GENERIC_RESPONSE_SOC_THRESHOLD = 1000 diff --git a/custom_components/mg_saic/coordinator.py b/custom_components/mg_saic/coordinator.py index 58b4a7c..11abe74 100644 --- a/custom_components/mg_saic/coordinator.py +++ b/custom_components/mg_saic/coordinator.py @@ -1,4 +1,4 @@ -from datetime import timedelta +from datetime import datetime, timedelta, timezone import asyncio from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .api import SAICMGAPIClient @@ -8,6 +8,7 @@ RETRY_LIMIT, UPDATE_INTERVAL, UPDATE_INTERVAL_CHARGING, + UPDATE_INTERVAL_POWERED, RETRY_BACKOFF_FACTOR, CHARGING_STATUS_CODES, GENERIC_RESPONSE_STATUS_THRESHOLD, @@ -20,6 +21,13 @@ class SAICMGDataUpdateCoordinator(DataUpdateCoordinator): def __init__(self, hass, client: SAICMGAPIClient, config_entry): """Initialize.""" + super().__init__( + hass, + LOGGER, + name="MG SAIC data update coordinator", + update_interval=None, + ) + self.client = client self.config_entry = config_entry self.is_charging = False @@ -36,17 +44,47 @@ def __init__(self, hass, client: SAICMGAPIClient, config_entry): "charging_scan_interval", UPDATE_INTERVAL_CHARGING.total_seconds() ) ) + self.update_interval_powered = timedelta( + seconds=config_entry.options.get( + "powered_scan_interval", UPDATE_INTERVAL_POWERED.total_seconds() + ) + ) + + LOGGER.debug( + f"Update intervals initialized: Default: {self.update_interval}, Charging: {self.update_interval_charging}, Powered: {self.update_interval_powered}" + ) # Use the vehicle type from the config entry self.vehicle_type = self.config_entry.data.get("vehicle_type") - super().__init__( - hass, - LOGGER, - name="MG SAIC data update coordinator", - update_interval=self.update_interval, + async def async_update_options(self, options): + """Update options and reschedule refresh.""" + # Update intervals from options + self.update_interval = timedelta( + seconds=options.get("scan_interval", self.update_interval.total_seconds()) + ) + self.update_interval_charging = timedelta( + seconds=options.get( + "charging_scan_interval", + self.update_interval_charging.total_seconds(), + ) + ) + self.update_interval_powered = timedelta( + seconds=options.get( + "powered_scan_interval", + self.update_interval_powered.total_seconds(), + ) ) + LOGGER.debug( + f"Update intervals updated via options: Normal: {self.update_interval}, Charging: {self.update_interval_charging}, Powered: {self.update_interval_powered}" + ) + + # Reschedule refresh + if self._unsub_refresh: + self._unsub_refresh() + self._schedule_refresh() + async def async_setup(self): """Set up the coordinator.""" self.is_initial_setup = True @@ -97,13 +135,26 @@ async def _async_update_data(self): bms_chrg_sts = getattr(chrg_data, "bmsChrgSts", None) self.is_charging = bms_chrg_sts in CHARGING_STATUS_CODES - # Adjust update interval based on charging status - new_interval = ( - self.update_interval_charging if self.is_charging else self.update_interval - ) + # Determine if the vehicle is powered on + status_data = data.get("status") + if status_data: + power_mode = getattr(status_data.basicVehicleStatus, "powerMode", None) + if power_mode in [2, 3]: + self.is_powered_on = True + else: + self.is_powered_on = False + + # Adjust Update Intervals + if self.is_powered_on: + new_interval = self.update_interval_powered + elif self.is_charging: + new_interval = self.update_interval_charging + else: + new_interval = self.update_interval + if self.update_interval != new_interval: self.update_interval = new_interval - LOGGER.debug("Update interval set to %s", self.update_interval) + LOGGER.debug(f"Update interval changed to {self.update_interval}") if self._unsub_refresh: self._unsub_refresh() self._schedule_refresh() @@ -114,6 +165,9 @@ async def _async_update_data(self): LOGGER.debug("Vehicle Status: %s", data.get("status")) LOGGER.debug("Vehicle Charging Data: %s", data.get("charging")) + # Set the last update time + self.last_update_time = datetime.now(timezone.utc) + return data async def _fetch_with_retries(self, fetch_func, is_generic_func, data_name): diff --git a/custom_components/mg_saic/lock.py b/custom_components/mg_saic/lock.py index 6db182e..13e0361 100644 --- a/custom_components/mg_saic/lock.py +++ b/custom_components/mg_saic/lock.py @@ -15,8 +15,12 @@ async def async_setup_entry(hass, entry, async_add_entities): vin_info = coordinator.data["info"][0] vin = vin_info.vin - lock_entity = SAICMGLockEntity(coordinator, client, vin_info, vin) - async_add_entities([lock_entity]) + lock_entities = [ + SAICMGLockEntity(coordinator, client, vin_info, vin), + SAICMGBootLockEntity(coordinator, client, vin_info, vin), + ] + + async_add_entities(lock_entities) class SAICMGLockEntity(CoordinatorEntity, LockEntity): @@ -77,3 +81,60 @@ async def async_unlock(self, **kwargs): await self.coordinator.async_request_refresh() except Exception as e: LOGGER.error("Error unlocking vehicle for VIN %s: %s", self._vin, e) + + +class SAICMGBootLockEntity(CoordinatorEntity, LockEntity): + """Representation of the vehicle's boot as a lock.""" + + def __init__(self, coordinator, client, vin_info, vin): + """Initialize the boot lock entity.""" + super().__init__(coordinator) + self._client = client + self._vin = vin + self._vin_info = vin_info + + self._attr_name = f"{vin_info.brandName} {vin_info.modelName} Boot" + self._attr_unique_id = f"{vin}_boot_lock" + self._attr_icon = "mdi:car-back" + + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._vin)}, + "name": f"{self._vin_info.brandName} {self._vin_info.modelName}", + "manufacturer": self._vin_info.brandName, + "model": self._vin_info.modelName, + "serial_number": self._vin, + } + + @property + def is_locked(self): + """Return true if the boot is closed (locked).""" + status = self.coordinator.data.get("status") + if status: + boot_status = getattr(status.basicVehicleStatus, "bootStatus", None) + return boot_status == 0 + return None + + @property + def available(self): + """Return True if the lock entity is available.""" + return ( + self.coordinator.last_update_success + and self.coordinator.data.get("status") is not None + ) + + async def async_lock(self, **kwargs): + """Lock (close) the boot.""" + # Since we can only open the boot, we log that closing is manual + LOGGER.warning("Closing the boot must be done manually.") + + async def async_unlock(self, **kwargs): + """Unlock (open) the boot.""" + try: + await self._client.open_tailgate(self._vin) + LOGGER.info("Boot opened for VIN: %s", self._vin) + await self.coordinator.async_request_refresh() + except Exception as e: + LOGGER.error("Error opening boot for VIN %s: %s", self._vin, e) diff --git a/custom_components/mg_saic/manifest.json b/custom_components/mg_saic/manifest.json index 749e63d..fdd5723 100644 --- a/custom_components/mg_saic/manifest.json +++ b/custom_components/mg_saic/manifest.json @@ -12,5 +12,5 @@ "pycryptodome", "saic-ismart-client-ng==0.5.2" ], - "version": "0.6.2" + "version": "0.7.0" } diff --git a/custom_components/mg_saic/sensor.py b/custom_components/mg_saic/sensor.py index 0758dbe..56e6b27 100644 --- a/custom_components/mg_saic/sensor.py +++ b/custom_components/mg_saic/sensor.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass, entry, async_add_entities): SAICMGVehicleSensor( coordinator, entry, - "Battery Voltage", + "Ancillary Battery Voltage", "batteryVoltage", "basicVehicleStatus", SensorDeviceClass.VOLTAGE, @@ -82,6 +82,29 @@ async def async_setup_entry(hass, entry, async_add_entities): 1.0, "status", ), + SAICMGVehicleSensor( + coordinator, + entry, + "Last Key Seen", + "lastKeySeen", + "basicVehicleStatus", + None, + None, + "mdi:key", + None, + 1.0, + "status", + ), + SAICMGLastUpdateSensor( + coordinator, + entry, + "Last Update Time", + SensorDeviceClass.TIMESTAMP, + None, + "mdi:update", + None, + None, + ), SAICMGMileageSensor( coordinator, entry, @@ -430,6 +453,101 @@ async def async_setup_entry(hass, entry, async_add_entities): LOGGER.error("Error setting up MG SAIC sensors: %s", e) +# GENERAL VEHICLE DETAIL SENSORS +class SAICMGMileageSensor(CoordinatorEntity, SensorEntity): + """Sensor for Mileage, uses data from both VehicleStatusResp and ChrgMgmtDataResp.""" + + def __init__( + self, + coordinator, + entry, + name, + field, + status_type, + charging_status_type, + device_class, + unit, + icon, + state_class, + factor, + data_type, + ): + """Initialize the sensor.""" + super().__init__(coordinator) + self._name = name + self._field = field + self._status_type = status_type + self._charging_status_type = charging_status_type + self._factor = factor + self._attr_device_class = device_class + self._attr_native_unit_of_measurement = unit + self._attr_icon = icon + self._attr_state_class = state_class + self._data_type = data_type + vin_info = self.coordinator.data["info"][0] + self._unique_id = f"{entry.entry_id}_{vin_info.vin}_{field}" + + @property + def unique_id(self): + return self._unique_id + + @property + def name(self): + vin_info = self.coordinator.data["info"][0] + return f"{vin_info.brandName} {vin_info.modelName} {self._name}" + + @property + def available(self): + """Return True if the entity is available.""" + # This sensor depends on both 'status' and 'charging' data + return ( + self.coordinator.last_update_success + and self.coordinator.data.get("status") is not None + and self.coordinator.data.get("charging") is not None + ) + + @property + def native_value(self): + # First, try to get mileage from VehicleStatusResp + data = self.coordinator.data.get("status") + mileage = None + if data: + status_data = getattr(data, self._status_type, None) + if status_data: + mileage = getattr(status_data, self._field, None) + if mileage == 0 or mileage is None: + mileage = None # Invalid or zero mileage + else: + mileage = mileage * self._factor + + # If mileage is None or zero, try to get from ChrgMgmtDataResp + if mileage is None: + charging_data = self.coordinator.data.get("charging") + if charging_data: + charging_status_data = getattr( + charging_data, self._charging_status_type, None + ) + if charging_status_data: + mileage = getattr(charging_status_data, self._field, None) + if mileage == 0 or mileage is None: + mileage = None + else: + mileage = mileage * self._factor + + return mileage + + @property + def device_info(self): + vin_info = self.coordinator.data["info"][0] + return { + "identifiers": {(DOMAIN, vin_info.vin)}, + "name": f"{vin_info.brandName} {vin_info.modelName}", + "manufacturer": vin_info.brandName, + "model": vin_info.modelName, + "serial_number": vin_info.vin, + } + + class SAICMGVehicleSensor(CoordinatorEntity, SensorEntity): """Representation of a MG SAIC vehicle sensor.""" @@ -530,6 +648,56 @@ def device_info(self): } +class SAICMGVehicleDetailSensor(CoordinatorEntity, SensorEntity): + """Representation of a sensor for MG SAIC vehicle details.""" + + def __init__(self, coordinator, entry, name, field, data_type): + """Initialize the sensor.""" + super().__init__(coordinator) + self._name = name + self._field = field + self._data_type = data_type + vin_info = self.coordinator.data["info"][0] + self._unique_id = f"{entry.entry_id}_{vin_info.vin}_{field}" + + @property + def unique_id(self): + return self._unique_id + + @property + def name(self): + vin_info = self.coordinator.data["info"][0] + return f"{vin_info.brandName} {vin_info.modelName} {self._name}" + + @property + def available(self): + """Return True if the entity is available.""" + required_data = self.coordinator.data.get(self._data_type) + return self.coordinator.last_update_success and required_data is not None + + @property + def native_value(self): + data = self.coordinator.data.get(self._data_type) + if data: + vin_info = data[0] + raw_value = getattr(vin_info, self._field, None) + if raw_value is not None: + return raw_value + return None + + @property + def device_info(self): + vin_info = self.coordinator.data["info"][0] + return { + "identifiers": {(DOMAIN, vin_info.vin)}, + "name": f"{vin_info.brandName} {vin_info.modelName}", + "manufacturer": vin_info.brandName, + "model": vin_info.modelName, + "serial_number": vin_info.vin, + } + + +# STATUS SENSORS class SAICMGElectricRangeSensor(CoordinatorEntity, SensorEntity): """Sensor for Electric Range, uses data from both RvsChargeStatus and VehicleStatusResp.""" @@ -706,17 +874,41 @@ def device_info(self): } -class SAICMGVehicleDetailSensor(CoordinatorEntity, SensorEntity): - """Representation of a sensor for MG SAIC vehicle details.""" +# CHARGING SENSORS +class SAICMGChargingCurrentSensor(CoordinatorEntity, SensorEntity): + """Representation of a MG SAIC charging current sensor""" - def __init__(self, coordinator, entry, name, field, data_type): + def __init__( + self, + coordinator, + entry, + name, + field, + device_class, + unit, + icon, + state_class, + factor=None, + data_source="chrgMgmtData", + data_type="charging", + ): """Initialize the sensor.""" super().__init__(coordinator) self._name = name self._field = field + self._device_class = device_class + self._unit = unit + self._state_class = state_class + self._icon = icon + self._factor = factor + self._data_source = data_source self._data_type = data_type + self._attr_device_class = device_class + self._attr_native_unit_of_measurement = unit + self._attr_icon = icon + self._attr_state_class = state_class vin_info = self.coordinator.data["info"][0] - self._unique_id = f"{entry.entry_id}_{vin_info.vin}_{field}" + self._unique_id = f"{entry.entry_id}_{vin_info.vin}_{field}_charge" @property def unique_id(self): @@ -735,13 +927,37 @@ def available(self): @property def native_value(self): - data = self.coordinator.data.get(self._data_type) - if data: - vin_info = data[0] - raw_value = getattr(vin_info, self._field, None) - if raw_value is not None: - return raw_value - return None + """Return the state of the sensor""" + try: + charging_data = getattr( + self.coordinator.data.get("charging"), self._data_source, None + ) + if charging_data: + charging_status = getattr(charging_data, "bmsChrgSts", None) + + if charging_status in [0, 5]: + return 0 + + raw_value = getattr(charging_data, self._field, None) + + # Calculate Current + if raw_value is not None and self._factor is not None: + calculated_value = 1000 - (raw_value * self._factor) + return round(calculated_value, 2) + else: + return None + else: + LOGGER.error("No charging data available for %s", self._name) + return None + + except Exception as e: + LOGGER.error( + "Error retrieving charging current sensor %s: %s", + self._name, + e, + exc_info=True, + ) + return None @property def device_info(self): @@ -755,38 +971,36 @@ def device_info(self): } -class SAICMGMileageSensor(CoordinatorEntity, SensorEntity): - """Sensor for Mileage, uses data from both VehicleStatusResp and ChrgMgmtDataResp.""" +class SAICMGChargingPowerSensor(CoordinatorEntity, SensorEntity): + """Sensor for Charging Power, calculated from voltage and current.""" def __init__( self, coordinator, entry, name, - field, - status_type, - charging_status_type, device_class, unit, icon, state_class, - factor, - data_type, + data_source="chrgMgmtData", + data_type="charging", ): """Initialize the sensor.""" super().__init__(coordinator) self._name = name - self._field = field - self._status_type = status_type - self._charging_status_type = charging_status_type - self._factor = factor + self._device_class = device_class + self._unit = unit + self._state_class = state_class + self._icon = icon + self._data_source = data_source self._attr_device_class = device_class self._attr_native_unit_of_measurement = unit self._attr_icon = icon self._attr_state_class = state_class self._data_type = data_type vin_info = self.coordinator.data["info"][0] - self._unique_id = f"{entry.entry_id}_{vin_info.vin}_{field}" + self._unique_id = f"{entry.entry_id}_{vin_info.vin}_charging_power" @property def unique_id(self): @@ -800,42 +1014,45 @@ def name(self): @property def available(self): """Return True if the entity is available.""" - # This sensor depends on both 'status' and 'charging' data - return ( - self.coordinator.last_update_success - and self.coordinator.data.get("status") is not None - and self.coordinator.data.get("charging") is not None - ) + required_data = self.coordinator.data.get(self._data_type) + return self.coordinator.last_update_success and required_data is not None @property def native_value(self): - # First, try to get mileage from VehicleStatusResp - data = self.coordinator.data.get("status") - mileage = None - if data: - status_data = getattr(data, self._status_type, None) - if status_data: - mileage = getattr(status_data, self._field, None) - if mileage == 0 or mileage is None: - mileage = None # Invalid or zero mileage - else: - mileage = mileage * self._factor - - # If mileage is None or zero, try to get from ChrgMgmtDataResp - if mileage is None: - charging_data = self.coordinator.data.get("charging") + """Return the state of the sensor.""" + try: + charging_data = getattr( + self.coordinator.data.get("charging"), self._data_source, None + ) if charging_data: - charging_status_data = getattr( - charging_data, self._charging_status_type, None - ) - if charging_status_data: - mileage = getattr(charging_status_data, self._field, None) - if mileage == 0 or mileage is None: - mileage = None - else: - mileage = mileage * self._factor + # Fetch the charging status to determine if the sensor should display data + charging_status = getattr(charging_data, "bmsChrgSts", None) - return mileage + if charging_status in [0, 5]: + return 0 + + # Get raw current and voltage + raw_current = getattr(charging_data, "bmsPackCrnt", None) + raw_voltage = getattr(charging_data, "bmsPackVol", None) + + if raw_current is not None and raw_voltage is not None: + # Apply decoding to current and voltage + decoded_current = 1000 - raw_current * CHARGING_CURRENT_FACTOR + decoded_voltage = raw_voltage * CHARGING_VOLTAGE_FACTOR + + # Calculate power in kW + power = decoded_current * decoded_voltage / 1000.0 + + return round(power, 2) + else: + return None + else: + LOGGER.error("No charging data available for %s", self._name) + return None + + except Exception as e: + LOGGER.error("Error retrieving charging power sensor %s: %s", self._name, e) + return None @property def device_info(self): @@ -997,40 +1214,32 @@ def device_info(self): } -class SAICMGChargingCurrentSensor(CoordinatorEntity, SensorEntity): - """Representation of a MG SAIC charging current sensor""" +# LAST UPDATE DATA SENSOR +class SAICMGLastUpdateSensor(CoordinatorEntity, SensorEntity): + """Sensor to display the timestamp of the last successful data update.""" def __init__( self, coordinator, entry, name, - field, device_class, unit, icon, state_class, - factor=None, - data_source="chrgMgmtData", - data_type="charging", + data_type, ): """Initialize the sensor.""" super().__init__(coordinator) self._name = name - self._field = field self._device_class = device_class self._unit = unit - self._state_class = state_class self._icon = icon - self._factor = factor - self._data_source = data_source - self._data_type = data_type - self._attr_device_class = device_class - self._attr_native_unit_of_measurement = unit - self._attr_icon = icon - self._attr_state_class = state_class + self._state_class = state_class + self._data_type = data_type # This can be None for this sensor vin_info = self.coordinator.data["info"][0] - self._unique_id = f"{entry.entry_id}_{vin_info.vin}_{field}_charge" + self._vin_info = vin_info + self._unique_id = f"{entry.entry_id}_{vin_info.vin}_last_update_time" @property def unique_id(self): @@ -1038,147 +1247,41 @@ def unique_id(self): @property def name(self): - vin_info = self.coordinator.data["info"][0] - return f"{vin_info.brandName} {vin_info.modelName} {self._name}" + return f"{self._vin_info.brandName} {self._vin_info.modelName} {self._name}" @property def available(self): """Return True if the entity is available.""" - required_data = self.coordinator.data.get(self._data_type) - return self.coordinator.last_update_success and required_data is not None + return ( + self.coordinator.last_update_success + and hasattr(self.coordinator, "last_update_time") + and self.coordinator.last_update_time is not None + ) @property def native_value(self): - """Return the state of the sensor""" - try: - charging_data = getattr( - self.coordinator.data.get("charging"), self._data_source, None - ) - if charging_data: - charging_status = getattr(charging_data, "bmsChrgSts", None) - - if charging_status in [0, 5]: - return 0 - - raw_value = getattr(charging_data, self._field, None) - - # Calculate Current - if raw_value is not None and self._factor is not None: - calculated_value = 1000 - (raw_value * self._factor) - return round(calculated_value, 2) - else: - return None - else: - LOGGER.error("No charging data available for %s", self._name) - return None - - except Exception as e: - LOGGER.error( - "Error retrieving charging current sensor %s: %s", - self._name, - e, - exc_info=True, - ) - return None - - @property - def device_info(self): - vin_info = self.coordinator.data["info"][0] - return { - "identifiers": {(DOMAIN, vin_info.vin)}, - "name": f"{vin_info.brandName} {vin_info.modelName}", - "manufacturer": vin_info.brandName, - "model": vin_info.modelName, - "serial_number": vin_info.vin, - } - - -class SAICMGChargingPowerSensor(CoordinatorEntity, SensorEntity): - """Sensor for Charging Power, calculated from voltage and current.""" - - def __init__( - self, - coordinator, - entry, - name, - device_class, - unit, - icon, - state_class, - data_source="chrgMgmtData", - data_type="charging", - ): - """Initialize the sensor.""" - super().__init__(coordinator) - self._name = name - self._device_class = device_class - self._unit = unit - self._state_class = state_class - self._icon = icon - self._data_source = data_source - self._attr_device_class = device_class - self._attr_native_unit_of_measurement = unit - self._attr_icon = icon - self._attr_state_class = state_class - self._data_type = data_type - vin_info = self.coordinator.data["info"][0] - self._unique_id = f"{entry.entry_id}_{vin_info.vin}_charging_power" + """Return the timestamp of the last successful data update.""" + return self.coordinator.last_update_time @property - def unique_id(self): - return self._unique_id + def device_class(self): + return self._device_class @property - def name(self): - vin_info = self.coordinator.data["info"][0] - return f"{vin_info.brandName} {vin_info.modelName} {self._name}" + def icon(self): + return self._icon @property - def available(self): - """Return True if the entity is available.""" - required_data = self.coordinator.data.get(self._data_type) - return self.coordinator.last_update_success and required_data is not None + def native_unit_of_measurement(self): + return self._unit @property - def native_value(self): - """Return the state of the sensor.""" - try: - charging_data = getattr( - self.coordinator.data.get("charging"), self._data_source, None - ) - if charging_data: - # Fetch the charging status to determine if the sensor should display data - charging_status = getattr(charging_data, "bmsChrgSts", None) - - if charging_status in [0, 5]: - return 0 - - # Get raw current and voltage - raw_current = getattr(charging_data, "bmsPackCrnt", None) - raw_voltage = getattr(charging_data, "bmsPackVol", None) - - if raw_current is not None and raw_voltage is not None: - # Apply decoding to current and voltage - decoded_current = 1000 - raw_current * CHARGING_CURRENT_FACTOR - decoded_voltage = raw_voltage * CHARGING_VOLTAGE_FACTOR - - # Calculate power in kW - power = decoded_current * decoded_voltage / 1000.0 - - return round(power, 2) - else: - return None - else: - LOGGER.error("No charging data available for %s", self._name) - return None - - except Exception as e: - LOGGER.error("Error retrieving charging power sensor %s: %s", self._name, e) - return None + def state_class(self): + return self._state_class @property def device_info(self): - vin_info = self.coordinator.data["info"][0] + vin_info = self._vin_info return { "identifiers": {(DOMAIN, vin_info.vin)}, "name": f"{vin_info.brandName} {vin_info.modelName}", diff --git a/custom_components/mg_saic/services.py b/custom_components/mg_saic/services.py index d713070..c0ea171 100644 --- a/custom_components/mg_saic/services.py +++ b/custom_components/mg_saic/services.py @@ -4,28 +4,27 @@ import asyncio from .api import SAICMGAPIClient -from .const import DOMAIN, LOGGER, VehicleWindowId +from .const import DOMAIN, LOGGER +SERVICE_CONTROL_CHARGING_PORT_LOCK = "control_charging_port_lock" +SERVICE_CONTROL_HEATED_SEATS = "control_heated_seats" +SERVICE_CONTROL_REAR_WINDOW_HEAT = "control_rear_window_heat" +SERVICE_CONTROL_SUNROOF = "control_sunroof" SERVICE_LOCK_VEHICLE = "lock_vehicle" SERVICE_UNLOCK_VEHICLE = "unlock_vehicle" SERVICE_START_AC = "start_ac" SERVICE_STOP_AC = "stop_ac" SERVICE_OPEN_TAILGATE = "open_tailgate" -SERVICE_TRIGGER_ALARM = "trigger_alarm" -SERVICE_START_CHARGING = "start_charging" -SERVICE_STOP_CHARGING = "stop_charging" -SERVICE_START_BATTERY_HEATING = "start_battery_heating" -SERVICE_STOP_BATTERY_HEATING = "stop_battery_heating" -SERVICE_CONTROL_REAR_WINDOW_HEAT = "control_rear_window_heat" -SERVICE_CONTROL_HEATED_SEATS = "control_heated_seats" -SERVICE_START_FRONT_DEFROST = "start_front_defrost" SERVICE_SET_TARGET_SOC = "set_target_soc" SERVICE_START_AC_WITH_SETTINGS = "start_ac_with_settings" +SERVICE_START_BATTERY_HEATING = "start_battery_heating" +SERVICE_START_CHARGING = "start_charging" +SERVICE_START_FRONT_DEFROST = "start_front_defrost" +SERVICE_STOP_BATTERY_HEATING = "stop_battery_heating" +SERVICE_STOP_CHARGING = "stop_charging" +SERVICE_TRIGGER_ALARM = "trigger_alarm" SERVICE_UPDATE_VEHICLE_DATA = "update_vehicle_data" -SERVICE_CONTROL_SUNROOF = "control_sunroof" -SERVICE_CONTROL_CHARGING_PORT_LOCK = "control_charging_port_lock" -SERVICE_VIN_SCHEMA = vol.Schema({vol.Required("vin"): cv.string}) SERVICE_ACTION_SCHEMA = vol.Schema( { @@ -34,10 +33,17 @@ } ) -SERVICE_SET_TARGET_SOC_SCHEMA = vol.Schema( +SERVICE_PORT_LOCK_SCHEMA = vol.Schema( { vol.Required("vin"): cv.string, - vol.Required("target_soc"): vol.In([40, 50, 60, 70, 80, 90, 100]), + vol.Required("unlock"): cv.boolean, + } +) + +SERVICE_REAR_WINDOW_DEFROST_ACTION_SCHEMA = vol.Schema( + { + vol.Required("vin"): cv.string, + vol.Required("action"): vol.In(["start", "stop"]), } ) @@ -49,20 +55,22 @@ } ) -SERVICE_SUNROOF_SCHEMA = vol.Schema( +SERVICE_SET_TARGET_SOC_SCHEMA = vol.Schema( { vol.Required("vin"): cv.string, - vol.Required("should_open"): cv.boolean, + vol.Required("target_soc"): vol.In([40, 50, 60, 70, 80, 90, 100]), } ) -SERVICE_PORT_LOCK_SCHEMA = vol.Schema( +SERVICE_SUNROOF_SCHEMA = vol.Schema( { vol.Required("vin"): cv.string, - vol.Required("unlock"): cv.boolean, + vol.Required("should_open"): cv.boolean, } ) +SERVICE_VIN_SCHEMA = vol.Schema({vol.Required("vin"): cv.string}) + async def async_setup_services(hass: HomeAssistant, client: SAICMGAPIClient) -> None: """Set up services for the MG SAIC integration.""" @@ -207,7 +215,7 @@ async def handle_control_rear_window_heat(call: ServiceCall) -> None: action = call.data["action"] try: await client.control_rear_window_heat(vin, action) - LOGGER.info("Rear window heat controlled successfully for VIN: %s", vin) + LOGGER.info("Rear window heat %sed for VIN: %s", action, vin) except Exception as e: LOGGER.error("Error controlling rear window heat for VIN %s: %s", vin, e) @@ -261,34 +269,49 @@ async def handle_control_charging_port_lock(call: ServiceCall) -> None: # Register services hass.services.async_register( - DOMAIN, SERVICE_LOCK_VEHICLE, handle_lock_vehicle, schema=SERVICE_VIN_SCHEMA + DOMAIN, + SERVICE_CONTROL_CHARGING_PORT_LOCK, + handle_control_charging_port_lock, + schema=SERVICE_PORT_LOCK_SCHEMA, ) hass.services.async_register( - DOMAIN, SERVICE_UNLOCK_VEHICLE, handle_unlock_vehicle, schema=SERVICE_VIN_SCHEMA + DOMAIN, + SERVICE_CONTROL_HEATED_SEATS, + handle_control_heated_seats, + schema=SERVICE_ACTION_SCHEMA, ) hass.services.async_register( - DOMAIN, SERVICE_START_AC, handle_start_ac, schema=SERVICE_VIN_SCHEMA + DOMAIN, + SERVICE_CONTROL_REAR_WINDOW_HEAT, + handle_control_rear_window_heat, + schema=SERVICE_REAR_WINDOW_DEFROST_ACTION_SCHEMA, ) hass.services.async_register( - DOMAIN, SERVICE_STOP_AC, handle_stop_ac, schema=SERVICE_VIN_SCHEMA + DOMAIN, + SERVICE_CONTROL_SUNROOF, + handle_control_sunroof, + schema=SERVICE_SUNROOF_SCHEMA, ) hass.services.async_register( - DOMAIN, - SERVICE_START_AC_WITH_SETTINGS, - handle_start_ac_with_settings, - schema=SERVICE_START_AC_WITH_SETTINGS_SCHEMA, + DOMAIN, SERVICE_LOCK_VEHICLE, handle_lock_vehicle, schema=SERVICE_VIN_SCHEMA ) hass.services.async_register( DOMAIN, SERVICE_OPEN_TAILGATE, handle_open_tailgate, schema=SERVICE_VIN_SCHEMA ) hass.services.async_register( - DOMAIN, SERVICE_TRIGGER_ALARM, handle_trigger_alarm, schema=SERVICE_VIN_SCHEMA + DOMAIN, + SERVICE_SET_TARGET_SOC, + handle_set_target_soc, + schema=SERVICE_SET_TARGET_SOC_SCHEMA, ) hass.services.async_register( - DOMAIN, SERVICE_START_CHARGING, handle_start_charging, schema=SERVICE_VIN_SCHEMA + DOMAIN, SERVICE_START_AC, handle_start_ac, schema=SERVICE_VIN_SCHEMA ) hass.services.async_register( - DOMAIN, SERVICE_STOP_CHARGING, handle_stop_charging, schema=SERVICE_VIN_SCHEMA + DOMAIN, + SERVICE_START_AC_WITH_SETTINGS, + handle_start_ac_with_settings, + schema=SERVICE_START_AC_WITH_SETTINGS_SCHEMA, ) hass.services.async_register( DOMAIN, @@ -297,52 +320,37 @@ async def handle_control_charging_port_lock(call: ServiceCall) -> None: schema=SERVICE_VIN_SCHEMA, ) hass.services.async_register( - DOMAIN, - SERVICE_STOP_BATTERY_HEATING, - handle_stop_battery_heating, - schema=SERVICE_VIN_SCHEMA, + DOMAIN, SERVICE_START_CHARGING, handle_start_charging, schema=SERVICE_VIN_SCHEMA ) hass.services.async_register( DOMAIN, - SERVICE_SET_TARGET_SOC, - handle_set_target_soc, - schema=SERVICE_SET_TARGET_SOC_SCHEMA, + SERVICE_START_FRONT_DEFROST, + handle_start_front_defrost, + schema=SERVICE_VIN_SCHEMA, ) hass.services.async_register( - DOMAIN, - SERVICE_CONTROL_REAR_WINDOW_HEAT, - handle_control_rear_window_heat, - schema=SERVICE_ACTION_SCHEMA, + DOMAIN, SERVICE_STOP_AC, handle_stop_ac, schema=SERVICE_VIN_SCHEMA ) hass.services.async_register( DOMAIN, - SERVICE_CONTROL_HEATED_SEATS, - handle_control_heated_seats, - schema=SERVICE_ACTION_SCHEMA, + SERVICE_STOP_BATTERY_HEATING, + handle_stop_battery_heating, + schema=SERVICE_VIN_SCHEMA, ) hass.services.async_register( - DOMAIN, - SERVICE_START_FRONT_DEFROST, - handle_start_front_defrost, - schema=SERVICE_VIN_SCHEMA, + DOMAIN, SERVICE_STOP_CHARGING, handle_stop_charging, schema=SERVICE_VIN_SCHEMA ) hass.services.async_register( - DOMAIN, - SERVICE_UPDATE_VEHICLE_DATA, - handle_update_vehicle_data, - schema=SERVICE_VIN_SCHEMA, + DOMAIN, SERVICE_TRIGGER_ALARM, handle_trigger_alarm, schema=SERVICE_VIN_SCHEMA ) hass.services.async_register( - DOMAIN, - SERVICE_CONTROL_SUNROOF, - handle_control_sunroof, - schema=SERVICE_SUNROOF_SCHEMA, + DOMAIN, SERVICE_UNLOCK_VEHICLE, handle_unlock_vehicle, schema=SERVICE_VIN_SCHEMA ) hass.services.async_register( DOMAIN, - SERVICE_CONTROL_CHARGING_PORT_LOCK, - handle_control_charging_port_lock, - schema=SERVICE_PORT_LOCK_SCHEMA, + SERVICE_UPDATE_VEHICLE_DATA, + handle_update_vehicle_data, + schema=SERVICE_VIN_SCHEMA, ) LOGGER.info("Services registered for MG SAIC integration.") @@ -350,23 +358,23 @@ async def handle_control_charging_port_lock(call: ServiceCall) -> None: async def async_unload_services(hass: HomeAssistant) -> None: """Unload MG SAIC services.""" + hass.services.async_remove(DOMAIN, SERVICE_CONTROL_CHARGING_PORT_LOCK) + hass.services.async_remove(DOMAIN, SERVICE_CONTROL_HEATED_SEATS) + hass.services.async_remove(DOMAIN, SERVICE_CONTROL_REAR_WINDOW_HEAT) + hass.services.async_remove(DOMAIN, SERVICE_CONTROL_SUNROOF) hass.services.async_remove(DOMAIN, SERVICE_LOCK_VEHICLE) - hass.services.async_remove(DOMAIN, SERVICE_UNLOCK_VEHICLE) + hass.services.async_remove(DOMAIN, SERVICE_OPEN_TAILGATE) + hass.services.async_remove(DOMAIN, SERVICE_SET_TARGET_SOC) hass.services.async_remove(DOMAIN, SERVICE_START_AC) - hass.services.async_remove(DOMAIN, SERVICE_STOP_AC) hass.services.async_remove(DOMAIN, SERVICE_START_AC_WITH_SETTINGS) - hass.services.async_remove(DOMAIN, SERVICE_OPEN_TAILGATE) - hass.services.async_remove(DOMAIN, SERVICE_TRIGGER_ALARM) - hass.services.async_remove(DOMAIN, SERVICE_START_CHARGING) - hass.services.async_remove(DOMAIN, SERVICE_STOP_CHARGING) hass.services.async_remove(DOMAIN, SERVICE_START_BATTERY_HEATING) - hass.services.async_remove(DOMAIN, SERVICE_STOP_BATTERY_HEATING) - hass.services.async_remove(DOMAIN, SERVICE_SET_TARGET_SOC) - hass.services.async_remove(DOMAIN, SERVICE_CONTROL_REAR_WINDOW_HEAT) - hass.services.async_remove(DOMAIN, SERVICE_CONTROL_HEATED_SEATS) + hass.services.async_remove(DOMAIN, SERVICE_START_CHARGING) hass.services.async_remove(DOMAIN, SERVICE_START_FRONT_DEFROST) + hass.services.async_remove(DOMAIN, SERVICE_STOP_AC) + hass.services.async_remove(DOMAIN, SERVICE_STOP_BATTERY_HEATING) + hass.services.async_remove(DOMAIN, SERVICE_STOP_CHARGING) + hass.services.async_remove(DOMAIN, SERVICE_TRIGGER_ALARM) + hass.services.async_remove(DOMAIN, SERVICE_UNLOCK_VEHICLE) hass.services.async_remove(DOMAIN, SERVICE_UPDATE_VEHICLE_DATA) - hass.services.async_remove(DOMAIN, SERVICE_CONTROL_SUNROOF) - hass.services.async_remove(DOMAIN, SERVICE_CONTROL_CHARGING_PORT_LOCK) LOGGER.info("Services unregistered for MG SAIC integration.") diff --git a/custom_components/mg_saic/services.yaml b/custom_components/mg_saic/services.yaml index 005ed84..3735255 100644 --- a/custom_components/mg_saic/services.yaml +++ b/custom_components/mg_saic/services.yaml @@ -1,53 +1,52 @@ -start_ac: - description: "Start the vehicle's AC system" - fields: - vin: - description: "Vehicle Identification Number" - example: "1HGCM82633A123456" - -stop_ac: - description: "Stop the vehicle's AC system" +control_charging_port_lock: + description: "Control the charging port lock (lock/unlock)." fields: vin: - description: "Vehicle Identification Number" + description: "Vehicle Identification Number." example: "1HGCM82633A123456" + unlock: + description: "True to unlock charging port, false to lock." + example: true -start_ac_with_settings: - description: "Start the vehicle's AC system with temperature and fan settings." +control_heated_seats: + description: "Control the heated seats" fields: vin: description: "Vehicle Identification Number" example: "1HGCM82633A123456" - temperature: - description: "Desired temperature in degrees Celsius" - example: 22.5 - fan_speed: - description: "Fan speed level (e.g., 1-7)" - example: 3 + action: + description: "Turn on (true) or off (false)" + example: true -start_charging: - description: "Start the vehicle charging process" +control_rear_window_heat: + description: "Control the rear window heat" fields: vin: description: "Vehicle Identification Number" example: "1HGCM82633A123456" + action: + description: "Action to perform: 'start' or 'stop'" + example: "start" -stop_charging: - description: "Stop the vehicle charging process" +control_sunroof: + description: "Control the sunroof (open or close)." fields: vin: - description: "Vehicle Identification Number" + description: "Vehicle Identification Number." example: "1HGCM82633A123456" + should_open: + description: "True to open the sunroof, false to close." + example: true -start_battery_heating: - description: "Start the vehicle battery heating process" +lock_vehicle: + description: "Lock the vehicle" fields: vin: description: "Vehicle Identification Number" example: "1HGCM82633A123456" -stop_battery_heating: - description: "Stop the vehicle battery heating process" +open_tailgate: + description: "Open the vehicle's tailgate" fields: vin: description: "Vehicle Identification Number" @@ -63,84 +62,85 @@ set_target_soc: description: "Desired target SOC percentage (10-100)" example: 80 -lock_vehicle: - description: "Lock the vehicle" +start_ac: + description: "Start the vehicle's AC system" fields: vin: description: "Vehicle Identification Number" example: "1HGCM82633A123456" -unlock_vehicle: - description: "Unlock the vehicle" +start_ac_with_settings: + description: "Start the vehicle's AC system with temperature and fan settings." + fields: + vin: + description: "Vehicle Identification Number" + example: "1HGCM82633A123456" + temperature: + description: "Desired temperature in degrees Celsius" + example: 22.5 + fan_speed: + description: "Fan speed level (e.g., 1-7)" + example: 3 + +start_battery_heating: + description: "Start the vehicle battery heating process" fields: vin: description: "Vehicle Identification Number" example: "1HGCM82633A123456" -open_tailgate: - description: "Open the vehicle's tailgate" +start_charging: + description: "Start the vehicle charging process" fields: vin: description: "Vehicle Identification Number" example: "1HGCM82633A123456" -trigger_alarm: - description: "Trigger the vehicle's alarm" +start_front_defrost: + description: "Start the front defrost system of the vehicle" fields: vin: description: "Vehicle Identification Number" example: "1HGCM82633A123456" -control_rear_window_heat: - description: "Control the rear window heat" +stop_ac: + description: "Stop the vehicle's AC system" fields: vin: description: "Vehicle Identification Number" example: "1HGCM82633A123456" - action: - description: "Turn on (true) or off (false)" - example: true -control_heated_seats: - description: "Control the heated seats" +stop_battery_heating: + description: "Stop the vehicle battery heating process" fields: vin: description: "Vehicle Identification Number" example: "1HGCM82633A123456" - action: - description: "Turn on (true) or off (false)" - example: true -start_front_defrost: - description: "Start the front defrost system of the vehicle" +stop_charging: + description: "Stop the vehicle charging process" fields: vin: description: "Vehicle Identification Number" example: "1HGCM82633A123456" -update_vehicle_data: - description: "Manually trigger a data update from the vehicle." +trigger_alarm: + description: "Trigger the vehicle's alarm" fields: vin: description: "Vehicle Identification Number" example: "1HGCM82633A123456" -control_sunroof: - description: "Control the sunroof (open or close)." +unlock_vehicle: + description: "Unlock the vehicle" fields: vin: - description: "Vehicle Identification Number." + description: "Vehicle Identification Number" example: "1HGCM82633A123456" - should_open: - description: "True to open the sunroof, false to close." - example: true -control_charging_port_lock: - description: "Control the charging port lock (lock/unlock)." +update_vehicle_data: + description: "Manually trigger a data update from the vehicle." fields: vin: - description: "Vehicle Identification Number." + description: "Vehicle Identification Number" example: "1HGCM82633A123456" - unlock: - description: "True to unlock charging port, false to lock." - example: true diff --git a/custom_components/mg_saic/switch.py b/custom_components/mg_saic/switch.py index c7ee6fc..7a730a8 100644 --- a/custom_components/mg_saic/switch.py +++ b/custom_components/mg_saic/switch.py @@ -1,6 +1,6 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, LOGGER, CHARGING_STATUS_CODES, VehicleWindowId +from .const import DOMAIN, LOGGER, CHARGING_STATUS_CODES async def async_setup_entry(hass, entry, async_add_entities): @@ -23,12 +23,18 @@ async def async_setup_entry(hass, entry, async_add_entities): for config in vin_info.vehicleModelConfiguration } - # Sunroof Switch - switches.append(SAICMGSunroofSwitch(coordinator, client, vin_info, vin)) - # AC Switch switches.append(SAICMGACSwitch(coordinator, client, vin_info, vin)) + # Front Defrost Switch (New) + switches.append(SAICMGFrontDefrostSwitch(coordinator, client, vin_info, vin)) + + # Rear Window Defrost + switches.append(SAICMGRearWindowDefrostSwitch(coordinator, client, vin_info, vin)) + + # Sunroof Switch + switches.append(SAICMGSunroofSwitch(coordinator, client, vin_info, vin)) + # Heated Seats Switch (if applicable) has_heated_seats = vehicle_config.get("HeatedSeat") == "1" @@ -129,7 +135,7 @@ def __init__(self, coordinator, client, vin_info, vin): client, vin_info, vin, - "Air Conditioning", + "AC Blowing", "mdi:air-conditioner", ) @@ -141,7 +147,7 @@ def is_on(self): basic_status = getattr(status, "basicVehicleStatus", None) if basic_status: ac_status = getattr(basic_status, "remoteClimateStatus", None) - return ac_status == 1 + return ac_status in (2, 3) return False @property @@ -153,43 +159,46 @@ def available(self): ) async def async_turn_on(self, **kwargs): - """Turn the AC on.""" + """Start AC Blowing.""" try: await self._client.start_ac(self._vin) - LOGGER.info("AC started for VIN: %s", self._vin) + LOGGER.info("AC Blowing started for VIN: %s", self._vin) await self.coordinator.async_request_refresh() except Exception as e: - LOGGER.error("Error starting AC for VIN %s: %s", self._vin, e) + LOGGER.error("Error starting AC Blowing for VIN %s: %s", self._vin, e) async def async_turn_off(self, **kwargs): - """Turn the AC off.""" + """Stop AC Blowing.""" try: await self._client.stop_ac(self._vin) - LOGGER.info("AC stopped for VIN: %s", self._vin) + LOGGER.info("AC Blowing stopped for VIN: %s", self._vin) await self.coordinator.async_request_refresh() except Exception as e: - LOGGER.error("Error stopping AC for VIN %s: %s", self._vin, e) + LOGGER.error("Error stopping AC Blowing for VIN %s: %s", self._vin, e) -class SAICMGHeatedSeatsSwitch(SAICMGVehicleSwitch): - """Switch to control the heated seats.""" +class SAICMGBatteryHeatingSwitch(SAICMGVehicleSwitch): + """Switch to control battery heating.""" def __init__(self, coordinator, client, vin_info, vin): super().__init__( - coordinator, client, vin_info, vin, "Heated Seats", "mdi:seat-recline-extra" + coordinator, + client, + vin_info, + vin, + "Battery Heating", + "mdi:fire", ) @property def is_on(self): - """Return true if heated seats are on.""" - status = self.coordinator.data.get("status") - if status: - basic_status = getattr(status, "basicVehicleStatus", None) - if basic_status: - # Update is_on to check frontLeftSeatHeatLevel and frontRightSeatHeatLevel - front_left_level = getattr(basic_status, "frontLeftSeatHeatLevel", 0) - front_right_level = getattr(basic_status, "frontRightSeatHeatLevel", 0) - return front_left_level > 0 or front_right_level > 0 + """Return true if battery heating is active.""" + charging_data = self.coordinator.data.get("charging") + if charging_data: + chrgMgmtData = getattr(charging_data, "chrgMgmtData", None) + if chrgMgmtData: + bmsPTCHeatResp = getattr(chrgMgmtData, "bmsPTCHeatResp", None) + return bmsPTCHeatResp == 1 return False @property @@ -197,26 +206,64 @@ def available(self): """Return True if the switch entity is available.""" return ( self.coordinator.last_update_success - and self.coordinator.data.get("status") is not None + and self.coordinator.data.get("charging") is not None ) async def async_turn_on(self, **kwargs): - """Turn the heated seats on.""" + """Start battery heating.""" try: - await self._client.control_heated_seats(self._vin, True) - LOGGER.info("Heated seats turned on for VIN: %s", self._vin) + await self._client.send_vehicle_charging_ptc_heat(self._vin, "start") + LOGGER.info("Battery heating started for VIN: %s", self._vin) await self.coordinator.async_request_refresh() except Exception as e: - LOGGER.error("Error turning on heated seats for VIN %s: %s", self._vin, e) + LOGGER.error("Error starting battery heating for VIN %s: %s", self._vin, e) async def async_turn_off(self, **kwargs): - """Turn the heated seats off.""" + """Stop battery heating.""" try: - await self._client.control_heated_seats(self._vin, False) - LOGGER.info("Heated seats turned off for VIN: %s", self._vin) + await self._client.send_vehicle_charging_ptc_heat(self._vin, "stop") + LOGGER.info("Battery heating stopped for VIN: %s", self._vin) await self.coordinator.async_request_refresh() except Exception as e: - LOGGER.error("Error turning off heated seats for VIN %s: %s", self._vin, e) + LOGGER.error("Error stopping battery heating for VIN %s: %s", self._vin, e) + + +class SAICMGChargingPortLockSwitch(SAICMGVehicleSwitch): + """Switch to control the charging port lock (lock/unlock).""" + + def __init__(self, coordinator, client, vin_info, vin): + super().__init__( + coordinator, client, vin_info, vin, "Charging Port Lock", "mdi:lock" + ) + + @property + def is_on(self): + """Return true if the charging port is locked.""" + charging_data = self.coordinator.data.get("charging") + if charging_data: + lock_status = getattr( + charging_data.chrgMgmtData, "ccuEleccLckCtrlDspCmd", None + ) + return lock_status == 1 # Assuming 1 represents locked + return False + + async def async_turn_on(self, **kwargs): + """Lock the charging port.""" + try: + await self._client.control_charging_port_lock(self._vin, unlock=False) + LOGGER.info("Charging port locked for VIN: %s", self._vin) + await self.coordinator.async_request_refresh() + except Exception as e: + LOGGER.error("Error locking charging port for VIN %s: %s", self._vin, e) + + async def async_turn_off(self, **kwargs): + """Unlock the charging port.""" + try: + await self._client.control_charging_port_lock(self._vin, unlock=True) + LOGGER.info("Charging port unlocked for VIN: %s", self._vin) + await self.coordinator.async_request_refresh() + except Exception as e: + LOGGER.error("Error unlocking charging port for VIN %s: %s", self._vin, e) class SAICMGChargingSwitch(SAICMGVehicleSwitch): @@ -265,8 +312,8 @@ async def async_turn_off(self, **kwargs): LOGGER.error("Error stopping charging for VIN %s: %s", self._vin, e) -class SAICMGBatteryHeatingSwitch(SAICMGVehicleSwitch): - """Switch to control battery heating.""" +class SAICMGFrontDefrostSwitch(SAICMGVehicleSwitch): + """Switch to control the front defrost.""" def __init__(self, coordinator, client, vin_info, vin): super().__init__( @@ -274,19 +321,61 @@ def __init__(self, coordinator, client, vin_info, vin): client, vin_info, vin, - "Battery Heating", - "mdi:fire", + "Front Defrost", + "mdi:car-defrost-front", ) @property def is_on(self): - """Return true if battery heating is active.""" - charging_data = self.coordinator.data.get("charging") - if charging_data: - chrgMgmtData = getattr(charging_data, "chrgMgmtData", None) - if chrgMgmtData: - bmsPTCHeatResp = getattr(chrgMgmtData, "bmsPTCHeatResp", None) - return bmsPTCHeatResp == 1 + """Return true if front defrost is on.""" + status = self.coordinator.data.get("status") + if status: + basic_status = getattr(status, "basicVehicleStatus", None) + if basic_status: + remote_climate_status = getattr( + basic_status, "remoteClimateStatus", None + ) + return remote_climate_status == 5 + return False + + async def async_turn_on(self, **kwargs): + """Start front defrost.""" + try: + await self._client.start_front_defrost(self._vin) + LOGGER.info("Front defrost started for VIN: %s", self._vin) + await self.coordinator.async_request_refresh() + except Exception as e: + LOGGER.error("Error starting front defrost for VIN %s: %s", self._vin, e) + + async def async_turn_off(self, **kwargs): + """Stop front defrost by stopping the AC.""" + try: + await self._client.stop_ac(self._vin) + LOGGER.info("Front defrost stopped (AC stopped) for VIN: %s", self._vin) + await self.coordinator.async_request_refresh() + except Exception as e: + LOGGER.error("Error stopping front defrost for VIN %s: %s", self._vin, e) + + +class SAICMGHeatedSeatsSwitch(SAICMGVehicleSwitch): + """Switch to control the heated seats.""" + + def __init__(self, coordinator, client, vin_info, vin): + super().__init__( + coordinator, client, vin_info, vin, "Heated Seats", "mdi:seat-recline-extra" + ) + + @property + def is_on(self): + """Return true if heated seats are on.""" + status = self.coordinator.data.get("status") + if status: + basic_status = getattr(status, "basicVehicleStatus", None) + if basic_status: + # Update is_on to check frontLeftSeatHeatLevel and frontRightSeatHeatLevel + front_left_level = getattr(basic_status, "frontLeftSeatHeatLevel", 0) + front_right_level = getattr(basic_status, "frontRightSeatHeatLevel", 0) + return front_left_level > 0 or front_right_level > 0 return False @property @@ -294,97 +383,106 @@ def available(self): """Return True if the switch entity is available.""" return ( self.coordinator.last_update_success - and self.coordinator.data.get("charging") is not None + and self.coordinator.data.get("status") is not None ) async def async_turn_on(self, **kwargs): - """Start battery heating.""" + """Turn the heated seats on.""" try: - await self._client.send_vehicle_charging_ptc_heat(self._vin, "start") - LOGGER.info("Battery heating started for VIN: %s", self._vin) + await self._client.control_heated_seats(self._vin, True) + LOGGER.info("Heated seats turned on for VIN: %s", self._vin) await self.coordinator.async_request_refresh() except Exception as e: - LOGGER.error("Error starting battery heating for VIN %s: %s", self._vin, e) + LOGGER.error("Error turning on heated seats for VIN %s: %s", self._vin, e) async def async_turn_off(self, **kwargs): - """Stop battery heating.""" + """Turn the heated seats off.""" try: - await self._client.send_vehicle_charging_ptc_heat(self._vin, "stop") - LOGGER.info("Battery heating stopped for VIN: %s", self._vin) + await self._client.control_heated_seats(self._vin, False) + LOGGER.info("Heated seats turned off for VIN: %s", self._vin) await self.coordinator.async_request_refresh() except Exception as e: - LOGGER.error("Error stopping battery heating for VIN %s: %s", self._vin, e) + LOGGER.error("Error turning off heated seats for VIN %s: %s", self._vin, e) -class SAICMGSunroofSwitch(SAICMGVehicleSwitch): - """Switch to control the sunroof (open/close).""" +class SAICMGRearWindowDefrostSwitch(SAICMGVehicleSwitch): + """Switch to control the rear window defrost.""" def __init__(self, coordinator, client, vin_info, vin): super().__init__( - coordinator, client, vin_info, vin, "Sunroof", "mdi:car-select" + coordinator, + client, + vin_info, + vin, + "Rear Window Defrost", + "mdi:car-defrost-rear", ) @property def is_on(self): - """Return true if the sunroof is open.""" + """Return true if rear window defrost is on.""" status = self.coordinator.data.get("status") if status: - sunroof_status = getattr(status.basicVehicleStatus, "sunroofStatus", None) - return sunroof_status == 1 + basic_status = getattr(status, "basicVehicleStatus", None) + if basic_status: + rear_window_heat_status = getattr(basic_status, "rmtHtdRrWndSt", None) + return rear_window_heat_status == 1 return False async def async_turn_on(self, **kwargs): - """Open the sunroof.""" + """Turn the rear window defrost on.""" try: - await self._client.control_sunroof(self._vin, "open") - LOGGER.info("Sunroof opened for VIN: %s", self._vin) + await self._client.control_rear_window_heat(self._vin, "start") + LOGGER.info("Rear window defrost turned on for VIN: %s", self._vin) await self.coordinator.async_request_refresh() except Exception as e: - LOGGER.error("Error opening sunroof for VIN %s: %s", self._vin, e) + LOGGER.error( + "Error turning on rear window defrost for VIN %s: %s", self._vin, e + ) async def async_turn_off(self, **kwargs): - """Close the sunroof.""" + """Turn the rear window defrost off.""" try: - await self._client.control_sunroof(self._vin, "close") - LOGGER.info("Sunroof closed for VIN: %s", self._vin) + await self._client.control_rear_window_heat(self._vin, "stop") + LOGGER.info("Rear window defrost turned off for VIN: %s", self._vin) await self.coordinator.async_request_refresh() except Exception as e: - LOGGER.error("Error closing sunroof for VIN %s: %s", self._vin, e) + LOGGER.error( + "Error turning off rear window defrost for VIN %s: %s", self._vin, e + ) -class SAICMGChargingPortLockSwitch(SAICMGVehicleSwitch): - """Switch to control the charging port lock (lock/unlock).""" +class SAICMGSunroofSwitch(SAICMGVehicleSwitch): + """Switch to control the sunroof (open/close).""" def __init__(self, coordinator, client, vin_info, vin): super().__init__( - coordinator, client, vin_info, vin, "Charging Port Lock", "mdi:lock" + coordinator, client, vin_info, vin, "Sunroof", "mdi:car-select" ) @property def is_on(self): - """Return true if the charging port is locked.""" - charging_data = self.coordinator.data.get("charging") - if charging_data: - lock_status = getattr( - charging_data.chrgMgmtData, "ccuEleccLckCtrlDspCmd", None - ) - return lock_status == 1 # Assuming 1 represents locked + """Return true if the sunroof is open.""" + status = self.coordinator.data.get("status") + if status: + sunroof_status = getattr(status.basicVehicleStatus, "sunroofStatus", None) + return sunroof_status == 1 return False async def async_turn_on(self, **kwargs): - """Lock the charging port.""" + """Open the sunroof.""" try: - await self._client.control_charging_port_lock(self._vin, unlock=False) - LOGGER.info("Charging port locked for VIN: %s", self._vin) + await self._client.control_sunroof(self._vin, "open") + LOGGER.info("Sunroof opened for VIN: %s", self._vin) await self.coordinator.async_request_refresh() except Exception as e: - LOGGER.error("Error locking charging port for VIN %s: %s", self._vin, e) + LOGGER.error("Error opening sunroof for VIN %s: %s", self._vin, e) async def async_turn_off(self, **kwargs): - """Unlock the charging port.""" + """Close the sunroof.""" try: - await self._client.control_charging_port_lock(self._vin, unlock=True) - LOGGER.info("Charging port unlocked for VIN: %s", self._vin) + await self._client.control_sunroof(self._vin, "close") + LOGGER.info("Sunroof closed for VIN: %s", self._vin) await self.coordinator.async_request_refresh() except Exception as e: - LOGGER.error("Error unlocking charging port for VIN %s: %s", self._vin, e) + LOGGER.error("Error closing sunroof for VIN %s: %s", self._vin, e) diff --git a/custom_components/mg_saic/translations/en.json b/custom_components/mg_saic/translations/en.json index 570568b..ab13cf2 100644 --- a/custom_components/mg_saic/translations/en.json +++ b/custom_components/mg_saic/translations/en.json @@ -35,81 +35,79 @@ } }, "services": { - "start_ac": { - "name": "Start AC", - "description": "Start the vehicle's AC system.", - "fields": { - "vin": { - "name": "VIN", - "description": "The vehicle identification number of the car to start AC." - } - } - }, - "stop_ac": { - "name": "Stop AC", - "description": "Stop the vehicle's AC system.", + "control_charging_port_lock": { + "name": "Control Charging Port Lock", + "description": "Control the charging port lock (lock/unlock).", "fields": { "vin": { "name": "VIN", - "description": "The vehicle identification number of the car to stop AC." + "description": "The vehicle identification number to control the charging port lock." + }, + "unlock": { + "name": "Unlock", + "description": "True to unlock the charging port, false to lock." } } }, - "start_ac_with_settings": { - "name": "Start AC with Settings", - "description": "Start the vehicle's AC system with specific temperature and fan speed.", + "control_heated_seats": { + "name": "Control Heated Seats", + "description": "Control the heated seats of the vehicle.", "fields": { "vin": { "name": "VIN", - "description": "Vehicle Identification Number." - }, - "temperature": { - "name": "Temperature", - "description": "Desired temperature in degrees Celsius." + "description": "The vehicle identification number of the car to control heated seats." }, - "fan_speed": { - "name": "Fan Speed", - "description": "Fan speed level (e.g., 1-7)." + "action": { + "name": "Action", + "description": "Turn on (true) or off (false)." } } }, - "start_charging": { - "name": "Start Charging", - "description": "Start the vehicle charging process.", + "control_rear_window_heat": { + "name": "Control Rear Window Heat", + "description": "Control the rear window heating of the vehicle.", "fields": { "vin": { "name": "VIN", - "description": "The vehicle identification number of the car to start charging." + "description": "The vehicle identification number of the car to control rear window heat." + }, + "action": { + "name": "Action", + "description": "Action to perform: 'start' or 'stop'" } } }, - "stop_charging": { - "name": "Stop Charging", - "description": "Stop the vehicle charging process.", + "control_sunroof": { + "name": "Control Sunroof", + "description": "Control the sunroof (open or close).", "fields": { "vin": { "name": "VIN", - "description": "The vehicle identification number of the car to stop charging." + "description": "The vehicle identification number to control the sunroof." + }, + "should_open": { + "name": "Should Open", + "description": "True to open the sunroof, false to close." } } }, - "start_battery_heating": { - "name": "Start Battery Heating", - "description": "Start the vehicle battery heating process.", + "lock_vehicle": { + "name": "Lock Vehicle", + "description": "Lock the vehicle.", "fields": { "vin": { "name": "VIN", - "description": "The vehicle identification number of the car to start battery heating." + "description": "The vehicle identification number of the car to lock." } } }, - "stop_battery_heating": { - "name": "Stop Battery Heating", - "description": "Stop the vehicle battery heating process.", + "open_tailgate": { + "name": "Open Tailgate", + "description": "Open the vehicle's tailgate.", "fields": { "vin": { "name": "VIN", - "description": "The vehicle identification number of the car to stop battery heating." + "description": "The vehicle identification number of the car to open the tailgate." } } }, @@ -127,121 +125,123 @@ } } }, - "lock_vehicle": { - "name": "Lock Vehicle", - "description": "Lock the vehicle.", + "start_ac": { + "name": "Start AC", + "description": "Start the vehicle's AC system.", "fields": { "vin": { "name": "VIN", - "description": "The vehicle identification number of the car to lock." + "description": "The vehicle identification number of the car to start AC." } } }, - "unlock_vehicle": { - "name": "Unlock Vehicle", - "description": "Unlock the vehicle.", + "start_ac_with_settings": { + "name": "Start AC with Settings", + "description": "Start the vehicle's AC system with specific temperature and fan speed.", "fields": { "vin": { "name": "VIN", - "description": "The vehicle identification number of the car to unlock." + "description": "Vehicle Identification Number." + }, + "temperature": { + "name": "Temperature", + "description": "Desired temperature in degrees Celsius." + }, + "fan_speed": { + "name": "Fan Speed", + "description": "Fan speed level (e.g., 1-7)." } } }, - "open_tailgate": { - "name": "Open Tailgate", - "description": "Open the vehicle's tailgate.", + "start_battery_heating": { + "name": "Start Battery Heating", + "description": "Start the vehicle battery heating process.", "fields": { "vin": { "name": "VIN", - "description": "The vehicle identification number of the car to open the tailgate." + "description": "The vehicle identification number of the car to start battery heating." } } }, - "trigger_alarm": { - "name": "Trigger Alarm", - "description": "Trigger the vehicle's alarm.", + "start_charging": { + "name": "Start Charging", + "description": "Start the vehicle charging process.", "fields": { "vin": { "name": "VIN", - "description": "The vehicle identification number of the car to trigger the alarm." + "description": "The vehicle identification number of the car to start charging." } } }, - "control_rear_window_heat": { - "name": "Control Rear Window Heat", - "description": "Control the rear window heating of the vehicle.", + "start_front_defrost": { + "name": "Start Front Defrost", + "description": "Start the front defrost system of the vehicle.", "fields": { "vin": { "name": "VIN", - "description": "The vehicle identification number of the car to control rear window heat." - }, - "action": { - "name": "Action", - "description": "Turn on (true) or off (false)." + "description": "The vehicle identification number of the car to start front defrost." } } }, - "control_heated_seats": { - "name": "Control Heated Seats", - "description": "Control the heated seats of the vehicle.", + "stop_ac": { + "name": "Stop AC", + "description": "Stop the vehicle's AC system.", "fields": { "vin": { "name": "VIN", - "description": "The vehicle identification number of the car to control heated seats." - }, - "action": { - "name": "Action", - "description": "Turn on (true) or off (false)." + "description": "The vehicle identification number of the car to stop AC." } } }, - "start_front_defrost": { - "name": "Start Front Defrost", - "description": "Start the front defrost system of the vehicle.", + "stop_battery_heating": { + "name": "Stop Battery Heating", + "description": "Stop the vehicle battery heating process.", "fields": { "vin": { "name": "VIN", - "description": "The vehicle identification number of the car to start front defrost." + "description": "The vehicle identification number of the car to stop battery heating." } } }, - "update_vehicle_data": { - "name": "Update Vehicle Data", - "description": "Manually trigger a data update from the vehicle.", + "stop_charging": { + "name": "Stop Charging", + "description": "Stop the vehicle charging process.", "fields": { "vin": { "name": "VIN", - "description": "The vehicle identification number of the car to update data." + "description": "The vehicle identification number of the car to stop charging." } } }, - "control_sunroof": { - "name": "Control Sunroof", - "description": "Control the sunroof (open or close).", - "fields": { - "vin": { - "name": "VIN", - "description": "The vehicle identification number to control the sunroof." - }, - "should_open": { - "name": "Should Open", - "description": "True to open the sunroof, false to close." - } + "trigger_alarm": { + "name": "Trigger Alarm", + "description": "Trigger the vehicle's alarm.", + "fields": { + "vin": { + "name": "VIN", + "description": "The vehicle identification number of the car to trigger the alarm." } + } }, - "control_charging_port_lock": { - "name": "Control Charging Port Lock", - "description": "Control the charging port lock (lock/unlock).", - "fields": { - "vin": { - "name": "VIN", - "description": "The vehicle identification number to control the charging port lock." - }, - "unlock": { - "name": "Unlock", - "description": "True to unlock the charging port, false to lock." - } + "unlock_vehicle": { + "name": "Unlock Vehicle", + "description": "Unlock the vehicle.", + "fields": { + "vin": { + "name": "VIN", + "description": "The vehicle identification number of the car to unlock." } + } + }, + "update_vehicle_data": { + "name": "Update Vehicle Data", + "description": "Manually trigger a data update from the vehicle.", + "fields": { + "vin": { + "name": "VIN", + "description": "The vehicle identification number of the car to update data." + } + } } }, "options": { @@ -249,7 +249,8 @@ "init": { "data": { "scan_interval": "Update Interval (in seconds)", - "charging_scan_interval": "Charging Update Interval (in seconds)" + "charging_scan_interval": "Charging Update Interval (in seconds)", + "powered_scan_interval": "Vehicle Powered On Update Interval (in seconds)" }, "description": "Define additional settings for MG/SAIC Integration", "title": "MG/SAIC Options" diff --git a/custom_components/mg_saic/translations/es.json b/custom_components/mg_saic/translations/es.json index 1aee05f..a26a949 100644 --- a/custom_components/mg_saic/translations/es.json +++ b/custom_components/mg_saic/translations/es.json @@ -35,81 +35,79 @@ } }, "services": { - "start_ac": { - "name": "Encender AC", - "description": "Encender el sistema AC del vehículo.", - "fields": { - "vin": { - "name": "VIN", - "description": "El número de identificación del vehículo para encender el AC." - } - } - }, - "stop_ac": { - "name": "Apagar AC", - "description": "Apagar el sistema AC del vehículo.", + "control_charging_port_lock": { + "name": "Controlar Bloqueo de Puerto de Carga", + "description": "Controlar el bloqueo del puerto de carga (bloquear/desbloquear).", "fields": { "vin": { "name": "VIN", - "description": "El número de identificación del vehículo para apagar el AC." + "description": "El número de identificación del vehículo para controlar el puerto de carga." + }, + "unlock": { + "name": "Desbloquear", + "description": "True para desbloquear el puerto de carga, false para bloquear." } } }, - "start_ac_with_settings": { - "name": "Encender AC con Configuración", - "description": "Encender el sistema de AC del vehículo con temperatura y velocidad de ventilador específicas.", + "control_heated_seats": { + "name": "Controlar Asientos Calefaccionados", + "description": "Controlar los asientos calefaccionados del vehículo.", "fields": { "vin": { "name": "VIN", - "description": "Número de identificación del vehículo." - }, - "temperature": { - "name": "Temperatura", - "description": "Temperatura deseada en grados Celsius." + "description": "El número de identificación del vehículo para controlar los asientos calefaccionados." }, - "fan_speed": { - "name": "Velocidad del Ventilador", - "description": "Nivel de velocidad del ventilador (por ejemplo, 1-7)." + "action": { + "name": "Acción", + "description": "Encender (true) o apagar (false)." } } }, - "start_charging": { - "name": "Iniciar Carga", - "description": "Iniciar el proceso de carga del vehículo.", + "control_rear_window_heat": { + "name": "Controlar Calefacción de la Ventana Trasera", + "description": "Controlar la calefacción de la ventana trasera del vehículo.", "fields": { "vin": { "name": "VIN", - "description": "El número de identificación del vehículo para iniciar la carga." + "description": "El número de identificación del vehículo para controlar la calefacción de la ventana trasera." + }, + "action": { + "name": "Acción", + "description": "Encender (start) o apagar (stop)." } } }, - "stop_charging": { - "name": "Detener Carga", - "description": "Detener el proceso de carga del vehículo.", + "control_sunroof": { + "name": "Controlar Techo Solar", + "description": "Controlar el techo solar (abrir o cerrar).", "fields": { "vin": { "name": "VIN", - "description": "El número de identificación del vehículo para detener la carga." + "description": "El número de identificación del vehículo para controlar el techo solar." + }, + "should_open": { + "name": "Abrir", + "description": "True para abrir el techo solar, false para cerrar." } } }, - "start_battery_heating": { - "name": "Iniciar Condicionamiento Bateria", - "description": "Iniciar el proceso de condicionamiento de la batería del vehículo.", + "lock_vehicle": { + "name": "Bloquear Vehículo", + "description": "Bloquear el vehículo.", "fields": { "vin": { "name": "VIN", - "description": "El número de identificación del vehículo para iniciar el condicionamiento de la bateria." + "description": "El número de identificación del vehículo para bloquear." } } }, - "stop_battery_heating": { - "name": "Detener Condicionamiento Bateria", - "description": "Detener el proceso de condicionamiento de la batería del vehículo.", + "open_tailgate": { + "name": "Abrir Portón Trasero", + "description": "Abrir el portón trasero del vehículo.", "fields": { "vin": { "name": "VIN", - "description": "El número de identificación del vehículo para detener el condicionamiento de la bateria." + "description": "El número de identificación del vehículo para abrir el portón trasero." } } }, @@ -127,121 +125,123 @@ } } }, - "lock_vehicle": { - "name": "Bloquear Vehículo", - "description": "Bloquear el vehículo.", + "start_ac": { + "name": "Encender AC", + "description": "Encender el sistema AC del vehículo.", "fields": { "vin": { "name": "VIN", - "description": "El número de identificación del vehículo para bloquear." + "description": "El número de identificación del vehículo para encender el AC." } } }, - "unlock_vehicle": { - "name": "Desbloquear Vehículo", - "description": "Desbloquear el vehículo.", + "start_ac_with_settings": { + "name": "Encender AC con Configuración", + "description": "Encender el sistema de AC del vehículo con temperatura y velocidad de ventilador específicas.", "fields": { "vin": { "name": "VIN", - "description": "El número de identificación del vehículo para desbloquear." + "description": "Número de identificación del vehículo." + }, + "temperature": { + "name": "Temperatura", + "description": "Temperatura deseada en grados Celsius." + }, + "fan_speed": { + "name": "Velocidad del Ventilador", + "description": "Nivel de velocidad del ventilador (por ejemplo, 1-7)." } } }, - "open_tailgate": { - "name": "Abrir Portón Trasero", - "description": "Abrir el portón trasero del vehículo.", + "start_battery_heating": { + "name": "Iniciar Condicionamiento Bateria", + "description": "Iniciar el proceso de condicionamiento de la batería del vehículo.", "fields": { "vin": { "name": "VIN", - "description": "El número de identificación del vehículo para abrir el portón trasero." + "description": "El número de identificación del vehículo para iniciar el condicionamiento de la bateria." } } }, - "trigger_alarm": { - "name": "Activar Alarma", - "description": "Activar la alarma del vehículo.", + "start_charging": { + "name": "Iniciar Carga", + "description": "Iniciar el proceso de carga del vehículo.", "fields": { "vin": { "name": "VIN", - "description": "El número de identificación del vehículo para activar la alarma." + "description": "El número de identificación del vehículo para iniciar la carga." } } }, - "control_rear_window_heat": { - "name": "Controlar Calefacción de la Ventana Trasera", - "description": "Controlar la calefacción de la ventana trasera del vehículo.", + "start_front_defrost": { + "name": "Encender Desempañador Delantero", + "description": "Encender el sistema de desempañador delantero del vehículo.", "fields": { "vin": { "name": "VIN", - "description": "El número de identificación del vehículo para controlar la calefacción de la ventana trasera." - }, - "action": { - "name": "Acción", - "description": "Encender (true) o apagar (false)." + "description": "El número de identificación del vehículo para encender el desempañador delantero." } } }, - "control_heated_seats": { - "name": "Controlar Asientos Calefaccionados", - "description": "Controlar los asientos calefaccionados del vehículo.", + "stop_ac": { + "name": "Apagar AC", + "description": "Apagar el sistema AC del vehículo.", "fields": { "vin": { "name": "VIN", - "description": "El número de identificación del vehículo para controlar los asientos calefaccionados." - }, - "action": { - "name": "Acción", - "description": "Encender (true) o apagar (false)." + "description": "El número de identificación del vehículo para apagar el AC." } } }, - "start_front_defrost": { - "name": "Encender Desempañador Delantero", - "description": "Encender el sistema de desempañador delantero del vehículo.", + "stop_battery_heating": { + "name": "Detener Condicionamiento Bateria", + "description": "Detener el proceso de condicionamiento de la batería del vehículo.", "fields": { "vin": { "name": "VIN", - "description": "El número de identificación del vehículo para encender el desempañador delantero." + "description": "El número de identificación del vehículo para detener el condicionamiento de la bateria." } } }, - "update_vehicle_data": { - "name": "Actualizar Datos del Vehículo", - "description": "Iniciar manualmente una actualización de datos desde el vehículo.", + "stop_charging": { + "name": "Detener Carga", + "description": "Detener el proceso de carga del vehículo.", "fields": { "vin": { "name": "VIN", - "description": "El número de identificación del vehículo para actualizar los datos." + "description": "El número de identificación del vehículo para detener la carga." } } }, - "control_sunroof": { - "name": "Controlar Techo Solar", - "description": "Controlar el techo solar (abrir o cerrar).", - "fields": { - "vin": { - "name": "VIN", - "description": "El número de identificación del vehículo para controlar el techo solar." - }, - "should_open": { - "name": "Abrir", - "description": "True para abrir el techo solar, false para cerrar." - } + "trigger_alarm": { + "name": "Activar Alarma", + "description": "Activar la alarma del vehículo.", + "fields": { + "vin": { + "name": "VIN", + "description": "El número de identificación del vehículo para activar la alarma." } + } }, - "control_charging_port_lock": { - "name": "Controlar Bloqueo de Puerto de Carga", - "description": "Controlar el bloqueo del puerto de carga (bloquear/desbloquear).", - "fields": { - "vin": { - "name": "VIN", - "description": "El número de identificación del vehículo para controlar el puerto de carga." - }, - "unlock": { - "name": "Desbloquear", - "description": "True para desbloquear el puerto de carga, false para bloquear." - } + "unlock_vehicle": { + "name": "Desbloquear Vehículo", + "description": "Desbloquear el vehículo.", + "fields": { + "vin": { + "name": "VIN", + "description": "El número de identificación del vehículo para desbloquear." } + } + }, + "update_vehicle_data": { + "name": "Actualizar Datos del Vehículo", + "description": "Iniciar manualmente una actualización de datos desde el vehículo.", + "fields": { + "vin": { + "name": "VIN", + "description": "El número de identificación del vehículo para actualizar los datos." + } + } } }, "options": { @@ -249,7 +249,8 @@ "init": { "data": { "scan_interval": "Intervalo de actualización (en segundos)", - "charging_scan_interval": "Intervalo de actualización durante la carga (en segundos)" + "charging_scan_interval": "Intervalo de actualización durante la carga (en segundos)", + "powered_scan_interval": "Intervalo de actualización durante si vehículo arrancado (en segundos)" }, "description": "Define ajustes adicionales para la Integración MG/SAIC", "title": "Opciones de MG/SAIC"