From 00e37500b041a347da61b4ea13473dc43cb8c8db Mon Sep 17 00:00:00 2001 From: Jacob Berelman <630000+stickpin@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:38:56 +0100 Subject: [PATCH] Enable Window Heater + Preparation for more climatisation options Small fix for auxiliary_climatisation sensor as well. --- volkswagencarnet/vw_connection.py | 54 +++++++++++++++++++++++++++++-- volkswagencarnet/vw_vehicle.py | 13 ++++---- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/volkswagencarnet/vw_connection.py b/volkswagencarnet/vw_connection.py index e8c74036..09e40e63 100644 --- a/volkswagencarnet/vw_connection.py +++ b/volkswagencarnet/vw_connection.py @@ -16,7 +16,7 @@ import jwt import logging from aiohttp import ClientSession, ClientTimeout, client_exceptions -from aiohttp.hdrs import METH_GET, METH_POST +from aiohttp.hdrs import METH_GET, METH_POST, METH_PUT from bs4 import BeautifulSoup from json import dumps as to_json from urllib.parse import urljoin, parse_qs, urlparse @@ -487,6 +487,22 @@ async def post(self, url, vin="", tries=0, return_raw=False, **data): else: raise + async def put(self, url, vin="", tries=0, return_raw=False, **data): + """Perform a put query.""" + try: + if data: + return await self._request(METH_PUT, self._make_url(url, vin), return_raw=return_raw, **data) + else: + return await self._request(METH_PUT, self._make_url(url, vin), return_raw=return_raw) + except client_exceptions.ClientResponseError as error: + if error.status == 429 and tries < MAX_RETRIES_ON_RATE_LIMIT: + delay = randint(1, 3 + tries * 2) + _LOGGER.debug(f"Server side throttled. Waiting {delay}, try {tries + 1}") + await asyncio.sleep(delay) + return await self.post(url, vin, tries + 1, return_raw=return_raw, **data) + else: + raise + # Construct URL from request, home region and variables def _make_url(self, ref, vin=""): # TODO after verifying that we don't need home region handling anymore, this method should be completely removed @@ -835,6 +851,10 @@ async def get_request_status(self, vin, sectionId, requestId): url = ( f"fs-car/bs/$sectionId/v1/{BRAND}/{self._session_country}/vehicles/$vin/climater/actions/$requestId" ) + elif sectionId == "window_heating": + url = ( + f"{BASE_API}/vehicle/v1/vehicles/{vin}/selectivestatus?jobs=climatisation" + ) elif sectionId == "batterycharge": url = ( f"fs-car/bs/$sectionId/v1/{BRAND}/{self._session_country}/vehicles/$vin/charger/actions/$requestId" @@ -851,8 +871,12 @@ async def get_request_status(self, vin, sectionId, requestId): url = re.sub("\\$requestId", requestId, url) response = await self.get(url, vin) + # window_heating + window_heating_status = response.get("climatisation", {}).get("windowHeatingStatus", {}).get("requests", {})[0].get("status", False) + if window_heating_status: + result = window_heating_status # Pre-heater, ??? - if response.get("requestStatusResponse", {}).get("status", False): + elif response.get("requestStatusResponse", {}).get("status", False): result = response.get("requestStatusResponse", {}).get("status", False) # For electric charging, climatisation and departure timers elif response.get("action", {}).get("actionState", False): @@ -1041,6 +1065,32 @@ async def setClimater(self, vin, data, spin): self._session_headers.pop("X-securityToken", None) raise + async def setWindowHeater(self, vin, action): + """Execute window heating actions.""" + + action = "start" if action else "stop" + + try: + response_raw = None + if action in ["start", "stop"]: + response_raw = await self.post(f"{BASE_API}/vehicle/v1/vehicles/{vin}/windowheating/{action}", json={}, return_raw=True) + + response = await response_raw.json(loads=json_loads) + if not response: + raise Exception("Invalid or no response") + elif response == 429: + return dict({"id": None, "state": "Throttled", "rate_limit_remaining": 0}) + else: + request_id = response.get("data", {}).get("requestID", 0) + remaining = response_raw.headers.get("Vcf-Remaining-Calls") + _LOGGER.debug( + f'Request for window heating returned with request id: {request_id},' + f" remaining requests: {remaining}" + ) + return dict({"id": str(request_id), "rate_limit_remaining": remaining}) + except Exception as e: + raise Exception("Unknown error during setWindowHeater") from e + async def setPreHeater(self, vin, data, spin): """Petrol/diesel parking heater actions.""" content_type = None diff --git a/volkswagencarnet/vw_vehicle.py b/volkswagencarnet/vw_vehicle.py index 14cda2bb..a08a99df 100644 --- a/volkswagencarnet/vw_vehicle.py +++ b/volkswagencarnet/vw_vehicle.py @@ -68,7 +68,8 @@ def __init__(self, conn, url): "tripStatistics": {"active": False}, "measurements": {"active": False}, "honkAndFlash": {"active": False}, - "parkingPosition": {"active": False} + "parkingPosition": {"active": False}, + "climatisation": {"active": False} # "rheating_v1": {"active": False}, # "rclima_v1": {"active": False}, # "statusreport_v1": {"active": False}, @@ -326,7 +327,7 @@ async def wait_for_request(self, section, request, retry_count=36): try: status = await self._connection.get_request_status(self.vin, section, request) _LOGGER.debug(f"Request ID {request}: {status}") - if status == "In progress": + if status == "in_progress": self._requests["state"] = "In progress" await asyncio.sleep(5) return await self.wait_for_request(section, request) @@ -436,12 +437,11 @@ async def set_climatisation_temp(self, temperature=20): async def set_window_heating(self, action="stop"): """Turn on/off window heater.""" if self.is_window_heater_supported: - if action in ["start", "stop"]: - data = {"action": {"type": action + "WindowHeating"}} - else: + if not action in ["start", "stop"]: _LOGGER.error(f'Window heater action "{action}" is not supported.') raise Exception(f'Window heater action "{action}" is not supported.') - return await self.set_climater(data) + response = await self._connection.setWindowHeater(self.vin, (action == "start")) + return await self._handle_response(response=response, topic="window_heating", error_msg=f"Failed to {action} window heating") else: _LOGGER.error("No climatisation support.") raise Exception("No climatisation support.") @@ -1330,6 +1330,7 @@ def auxiliary_climatisation(self) -> bool: climatisation_state = find_path(self.attrs, "climatisation.climatisationStatus.value.climatisationState") if climatisation_state in ["heating", "heatingAuxiliary", "on"]: return True + return False @property def auxiliary_climatisation_last_updated(self) -> datetime: