diff --git a/myskoda/cli.py b/myskoda/cli.py index 6d3180c2..da685c94 100644 --- a/myskoda/cli.py +++ b/myskoda/cli.py @@ -364,21 +364,116 @@ def on_event(event: Event) -> None: @cli.command() @click.option("temperature", "--temperature", type=float, required=True) +@click.option("timeout", "--timeout", type=float, default=300) @click.argument("vin") @click.pass_context -async def start_air_conditioning(ctx: Context, temperature: float, vin: str) -> None: +async def start_air_conditioning( + ctx: Context, + temperature: float, + timeout: float, # noqa: ASYNC109 + vin: str, +) -> None: """Start the air conditioning with the provided target temperature in °C.""" myskoda: MySkoda = ctx.obj["myskoda"] - await myskoda.start_air_conditioning(vin, temperature) + async with asyncio.timeout(timeout): + await myskoda.start_air_conditioning(vin, temperature) @cli.command() +@click.option("timeout", "--timeout", type=float, default=300) @click.argument("vin") @click.pass_context -async def stop_air_conditioning(ctx: Context, vin: str) -> None: +async def stop_air_conditioning(ctx: Context, timeout: float, vin: str) -> None: # noqa: ASYNC109 """Stop the air conditioning.""" myskoda: MySkoda = ctx.obj["myskoda"] - await myskoda.stop_air_conditioning(vin) + async with asyncio.timeout(timeout): + await myskoda.stop_air_conditioning(vin) + + +@cli.command() +@click.option("timeout", "--timeout", type=float, default=300) +@click.argument("vin") +@click.option("temperature", "--temperature", type=float, required=True) +@click.pass_context +async def set_target_temperature( + ctx: Context, + timeout: float, # noqa: ASYNC109 + vin: str, + temperature: float, +) -> None: + """Set the air conditioning's target temperature in °C.""" + myskoda: MySkoda = ctx.obj["myskoda"] + async with asyncio.timeout(timeout): + await myskoda.set_target_temperature(vin, temperature) + + +@cli.command() +@click.option("timeout", "--timeout", type=float, default=300) +@click.argument("vin") +@click.pass_context +async def start_window_heating(ctx: Context, timeout: float, vin: str) -> None: # noqa: ASYNC109 + """Start heating both the front and rear window.""" + myskoda: MySkoda = ctx.obj["myskoda"] + async with asyncio.timeout(timeout): + await myskoda.start_window_heating(vin) + + +@cli.command() +@click.option("timeout", "--timeout", type=float, default=300) +@click.argument("vin") +@click.pass_context +async def stop_window_heating(ctx: Context, timeout: float, vin: str) -> None: # noqa: ASYNC109 + """Stop heating both the front and rear window.""" + myskoda: MySkoda = ctx.obj["myskoda"] + async with asyncio.timeout(timeout): + await myskoda.stop_window_heating(vin) + + +@cli.command() +@click.option("timeout", "--timeout", type=float, default=300) +@click.argument("vin") +@click.option("limit", "--limit", type=float, required=True) +@click.pass_context +async def set_charge_limit(ctx: Context, timeout: float, vin: str, limit: int) -> None: # noqa: ASYNC109 + """Set the maximum charge limit in percent.""" + myskoda: MySkoda = ctx.obj["myskoda"] + async with asyncio.timeout(timeout): + await myskoda.set_charge_limit(vin, limit) + + +@cli.command() +@click.option("timeout", "--timeout", type=float, default=300) +@click.argument("vin") +@click.option("enabled", "--enabled", type=bool, required=True) +@click.pass_context +async def set_battery_care_mode(ctx: Context, timeout: float, vin: str, enabled: bool) -> None: # noqa: ASYNC109 + """Enable or disable the battery care mode.""" + myskoda: MySkoda = ctx.obj["myskoda"] + async with asyncio.timeout(timeout): + await myskoda.set_battery_care_mode(vin, enabled) + + +@cli.command() +@click.option("timeout", "--timeout", type=float, default=300) +@click.argument("vin") +@click.option("enabled", "--enabled", type=bool, required=True) +@click.pass_context +async def set_reduced_current_limit(ctx: Context, timeout: float, vin: str, enabled: bool) -> None: # noqa: ASYNC109 + """Enable reducing the current limit by which the car is charged.""" + myskoda: MySkoda = ctx.obj["myskoda"] + async with asyncio.timeout(timeout): + await myskoda.set_reduced_current_limit(vin, enabled) + + +@cli.command() +@click.option("timeout", "--timeout", type=float, default=300) +@click.argument("vin") +@click.pass_context +async def wakeup(ctx: Context, timeout: float, vin: str) -> None: # noqa: ASYNC109 + """Wake the vehicle up. Can be called maximum three times a day.""" + myskoda: MySkoda = ctx.obj["myskoda"] + async with asyncio.timeout(timeout): + await myskoda.wakeup(vin) def c_open(cond: OpenState) -> str: diff --git a/myskoda/mqtt.py b/myskoda/mqtt.py index 529ce311..ce2ef0e0 100644 --- a/myskoda/mqtt.py +++ b/myskoda/mqtt.py @@ -6,8 +6,7 @@ import ssl from asyncio import Future, get_event_loop from collections.abc import Callable -from enum import StrEnum -from typing import Literal, cast +from typing import cast from asyncio_paho.client import AsyncioPahoClient from paho.mqtt.client import MQTTMessage @@ -37,13 +36,7 @@ TOPIC_RE = re.compile("^(.*?)/(.*?)/(.*?)/(.*?)$") -class OperationListenerType(StrEnum): - OPERATION_NAME = "OPERATION_NAME" - TRACE_ID = "TRACE_ID" - - -class OperationListenerForName: - type: Literal[OperationListenerType.OPERATION_NAME] = OperationListenerType.OPERATION_NAME +class OperationListener: operation_name: OperationName future: Future[OperationRequest] @@ -54,19 +47,6 @@ def __init__( # noqa: D107 self.future = future -class OperationListenerForTraceId: - type: Literal[OperationListenerType.TRACE_ID] = OperationListenerType.TRACE_ID - trace_id: str - future: Future[OperationRequest] - - def __init__(self, trace_id: str, future: Future[OperationRequest]) -> None: # noqa: D107 - self.trace_id = trace_id - self.future = future - - -OperationListener = OperationListenerForTraceId | OperationListenerForName - - class Mqtt: api: RestApi user: User @@ -116,10 +96,10 @@ def _wait_for_connection(self) -> Future[None]: def wait_for_operation(self, operation_name: OperationName) -> Future[OperationRequest]: """Wait until the next operation of the specified type completes.""" - _LOGGER.debug("Waiting for operation %s to start and complete", operation_name) + _LOGGER.debug("Waiting for operation %s complete.", operation_name) future: Future[OperationRequest] = get_event_loop().create_future() - self._operation_listeners.append(OperationListenerForName(operation_name, future)) + self._operation_listeners.append(OperationListener(operation_name, future)) return future @@ -151,37 +131,27 @@ def _emit(self, event: Event) -> None: self._handle_operation(event) - def _handle_operation_in_progress(self, operation: OperationRequest) -> None: - listeners = self._operation_listeners - self._operation_listeners = [] - for listener in listeners: - if ( - listener.type != OperationListenerType.OPERATION_NAME - or listener.operation_name != operation.operation - ): - self._operation_listeners.append(listener) - continue - _LOGGER.debug( - "Converting listener for operation name '%s' to trace '%s'.", - operation.operation, - operation.trace_id, - ) - self._operation_listeners.append( - OperationListenerForTraceId(operation.trace_id, listener.future) - ) - def _handle_operation_completed(self, operation: OperationRequest) -> None: listeners = self._operation_listeners self._operation_listeners = [] for listener in listeners: - if ( - listener.type != OperationListenerType.TRACE_ID - or listener.trace_id != operation.trace_id - ): + if listener.operation_name != operation.operation: self._operation_listeners.append(listener) continue - _LOGGER.debug("Resolving listener for trace id '%s'.", operation.trace_id) - listener.future.set_result(operation) + + if operation.status == OperationStatus.ERROR: + _LOGGER.error( + "Resolving listener for operation '%s' with error '%s'.", + operation.operation, + operation.error_code, + ) + listener.future.set_exception(OperationFailedError(operation)) + else: + if operation.status == OperationStatus.COMPLETED_WARNING: + _LOGGER.warning("Operation '%s' completed with warnings.", operation.operation) + + _LOGGER.debug("Resolving listener for operation '%s'.", operation.operation) + listener.future.set_result(operation) def _handle_operation(self, event: Event) -> None: if event.type != EventType.OPERATION: @@ -193,7 +163,6 @@ def _handle_operation(self, event: Event) -> None: event.operation.operation, event.operation.trace_id, ) - self._handle_operation_in_progress(event.operation) return _LOGGER.debug( diff --git a/myskoda/myskoda.py b/myskoda/myskoda.py index a52a5c95..ae6910c6 100644 --- a/myskoda/myskoda.py +++ b/myskoda/myskoda.py @@ -96,7 +96,7 @@ async def start_window_heating(self, vin: str) -> None: async def set_target_temperature(self, vin: str, temperature: float) -> None: """Set the air conditioning's target temperature in °C.""" - future = self.mqtt.wait_for_operation(OperationName.UPDATE_TARGET_TEMPERATURE) + future = self.mqtt.wait_for_operation(OperationName.SET_AIR_CONDITIONING_TARGET_TEMPERATURE) await self.rest_api.set_target_temperature(vin, temperature) await future diff --git a/myskoda/rest_api.py b/myskoda/rest_api.py index 04bd6ed2..9e092150 100644 --- a/myskoda/rest_api.py +++ b/myskoda/rest_api.py @@ -278,7 +278,7 @@ async def set_charge_limit(self, vin: str, limit: int) -> None: # TODO @dvx76: Maybe refactor for FBT001 async def set_battery_care_mode(self, vin: str, enabled: bool) -> None: """Enable or disable the battery care mode.""" - _LOGGER.debug("Setting battery care mode for vehicle %s to %b", vin, enabled) + _LOGGER.debug("Setting battery care mode for vehicle %s to %r", vin, enabled) json_data = {"chargingCareMode": "ACTIVATED" if enabled else "DEACTIVATED"} async with self.session.put( f"{BASE_URL_SKODA}/api/v1/charging/{vin}/set-care-mode", @@ -290,7 +290,7 @@ async def set_battery_care_mode(self, vin: str, enabled: bool) -> None: # TODO @dvx76: Maybe refactor for FBT001 async def set_reduced_current_limit(self, vin: str, reduced: bool) -> None: """Enable reducing the current limit by which the car is charged.""" - _LOGGER.debug("Setting reduced charging for vehicle %s to %b", vin, reduced) + _LOGGER.debug("Setting reduced charging for vehicle %s to %r", vin, reduced) json_data = {"chargingCurrent": "REDUCED" if reduced else "MAXIMUM"} async with self.session.put( f"{BASE_URL_SKODA}/api/v1/charging/{vin}/set-charging-current",