diff --git a/docs/mqtt.md b/docs/mqtt.md index ae4a61c..3bffd7b 100644 --- a/docs/mqtt.md +++ b/docs/mqtt.md @@ -36,6 +36,7 @@ For example, if a `start-stop-air-conditioning` message is sent, the MQTT server * `"IN_PROGRESS"`: Operation is currently being executed by the car. * `"COMPLETED_SUCCESS"`: Operation completed. * `"ERROR"`: An error occurred. Additional information in field `"errorCode"`. +* `"COMPLETED_WARNING"`: Extracted from analysing the smali files. We don't know the implications. ### /service-event diff --git a/myskoda/cli.py b/myskoda/cli.py index e9a5416..a0219eb 100644 --- a/myskoda/cli.py +++ b/myskoda/cli.py @@ -4,7 +4,10 @@ poetry run python3 -m myskoda.cli """ +from logging import DEBUG, INFO + import asyncclick as click +import coloredlogs from aiohttp import ClientSession from asyncclick.core import Context from termcolor import colored @@ -28,9 +31,11 @@ @click.version_option() @click.option("username", "--user", help="Username used for login.", required=True) @click.option("password", "--password", help="Password used for login.", required=True) +@click.option("verbose", "--verbose", help="Enable verbose logging.", is_flag=True) @click.pass_context -def cli(ctx: Context, username: str, password: str) -> None: +def cli(ctx: Context, username: str, password: str, verbose: bool) -> None: # noqa: FBT001 """Interact with the MySkoda API.""" + coloredlogs.install(level=DEBUG if verbose else INFO) ctx.ensure_object(dict) ctx.obj["username"] = username ctx.obj["password"] = password diff --git a/myskoda/const.py b/myskoda/const.py index c7c469f..65acea8 100644 --- a/myskoda/const.py +++ b/myskoda/const.py @@ -6,5 +6,5 @@ BASE_URL_SKODA = "https://mysmob.api.connect.skoda-auto.cz" BASE_URL_IDENT = "https://identity.vwgroup.io" -MQTT_HOST = "3.72.252.203" +MQTT_HOST = "mqtt.messagehub.de" MQTT_PORT = 8883 diff --git a/myskoda/models/mqtt.py b/myskoda/models/mqtt.py new file mode 100644 index 0000000..632b27a --- /dev/null +++ b/myskoda/models/mqtt.py @@ -0,0 +1,72 @@ +"""Models relaterd to the MQTT API.""" + +from datetime import datetime +from enum import StrEnum +from typing import Generic, TypeVar + +from pydantic import BaseModel, Field, validator + + +class OperationStatus(StrEnum): + IN_PROGRESS = "IN_PROGRESS" + COMPLETED_SUCCESS = "COMPLETED_SUCCESS" + COMPLETED_WARNING = "COMPLETED_WARNING" + ERROR = "ERROR" + + +class OperationRequest(BaseModel): + version: int + trace_id: str = Field(None, alias="traceId") + request_id: str = Field(None, alias="requestId") + operation: str + status: OperationStatus + error_code: str = Field(None, alias="errorCode") + + +class ServiceEventData(BaseModel): + user_id: str = Field(None, alias="userId") + vin: str + + +T = TypeVar("T", bound=ServiceEventData) + + +class ServiceEvent(BaseModel, Generic[T]): + version: int + trace_id: str = Field(None, alias="traceId") + timestamp: datetime = Field(None, alias="requestId") + producer: str + name: str + data: T + + +class ServiceEventChargingState(StrEnum): + CHARGING = "charging" + CHARGED_NOT_CONSERVING = "chargePurposeReachedAndNotConservationCharging" + NOT_READY = "notReadyForCharging" + READY = "readyForCharging" + + +class ServiceEventChargingData(ServiceEventData): + mode: str + state: ServiceEventChargingState + soc: int + charged_range: str = Field(None, alias="chargedRange") + time_to_finish: str | None = Field(None, alias="timeToFinish") + + @validator("soc") + def _parse_soc(cls, value: str) -> int: # noqa: N805 + return int(value) + + @validator("charged_range") + def _parse_charged_range(cls, value: str) -> int: # noqa: N805 + return int(value) + + @validator("time_to_finish") + def _parse_time_to_finish(cls, value: str) -> int | None: # noqa: N805 + if value == "null": + return None + return int(value) + + +ServiceEventCharging = ServiceEvent[ServiceEventChargingData] diff --git a/myskoda/mqtt.py b/myskoda/mqtt.py index 301ccc8..06e06dc 100644 --- a/myskoda/mqtt.py +++ b/myskoda/mqtt.py @@ -1,16 +1,173 @@ """MQTT client module for the MySkoda server.""" +import json import logging +import re import ssl -from typing import Any +from enum import StrEnum +from typing import Literal, cast from paho.mqtt.client import Client, MQTTMessage +from myskoda.models.mqtt import OperationRequest, ServiceEvent, ServiceEventCharging + from .const import MQTT_HOST, MQTT_PORT from .models.user import User from .rest_api import RestApi _LOGGER = logging.getLogger(__name__) +TOPIC_RE = re.compile("^(.*?)/(.*?)/(.*?)$") + + +class Topic(StrEnum): + UPDATE_BATTERY_SUPPORT = "UPDATE_BATTERY_SUPPORT" + LOCK_VEHICLE = "LOCK_VEHICLE" + WAKEUP = "WAKEUP" + SET_TARGET_TEMPERATURE = "SET_TARGET_TEMPERATURE" + START_STOP_AIR_CONDITIONING = "START_STOP_AIR_CONDITIONING" + START_STOP_WINDOW_HEATING = "START_STOP_WINDOW_HEATING" + START_STOP_CHARGING = "START_STOP_CHARGING" + APPLY_BACKUP = "APPLY_BACKUP" + HONK_AND_FLASH = "HONK_AND_FLASH" + AIR_CONDITIONING = "AIR_CONDITIONING" + CHARGING = "CHARGING" + ACCESS = "ACCESS" + LIGHTS = "LIGHTS" + + +class EventUpdateBatterySupport: + topic: Literal[Topic.UPDATE_BATTERY_SUPPORT] + payload: OperationRequest + + def __init__(self, payload: dict) -> None: # noqa: D107 + self.topic = Topic.UPDATE_BATTERY_SUPPORT + self.payload = OperationRequest(**payload) + + +class EventLockVehicle: + topic: Literal[Topic.LOCK_VEHICLE] + payload: OperationRequest + + def __init__(self, payload: dict) -> None: # noqa: D107 + self.topic = Topic.LOCK_VEHICLE + self.payload = OperationRequest(**payload) + + +class EventWakeup: + topic: Literal[Topic.WAKEUP] + payload: OperationRequest + + def __init__(self, payload: dict) -> None: # noqa: D107 + self.topic = Topic.WAKEUP + self.payload = OperationRequest(**payload) + + +class EventSetTargetTemperature: + topic: Literal[Topic.SET_TARGET_TEMPERATURE] + payload: OperationRequest + + def __init__(self, payload: dict) -> None: # noqa: D107 + self.topic = Topic.SET_TARGET_TEMPERATURE + self.payload = OperationRequest(**payload) + + +class EventStartStopAirConditioning: + topic: Literal[Topic.START_STOP_AIR_CONDITIONING] + payload: OperationRequest + + def __init__(self, payload: dict) -> None: # noqa: D107 + self.topic = Topic.START_STOP_AIR_CONDITIONING + self.payload = OperationRequest(**payload) + + +class EventStartStopWindowHeating: + topic: Literal[Topic.START_STOP_WINDOW_HEATING] + payload: OperationRequest + + def __init__(self, payload: dict) -> None: # noqa: D107 + self.topic = Topic.START_STOP_WINDOW_HEATING + self.payload = OperationRequest(**payload) + + +class EventStartStopCharging: + topic: Literal[Topic.START_STOP_CHARGING] + payload: OperationRequest + + def __init__(self, payload: dict) -> None: # noqa: D107 + self.topic = Topic.START_STOP_CHARGING + self.payload = OperationRequest(**payload) + + +class EventHonkAndFlash: + topic: Literal[Topic.HONK_AND_FLASH] + payload: OperationRequest + + def __init__(self, payload: dict) -> None: # noqa: D107 + self.topic = Topic.HONK_AND_FLASH + self.payload = OperationRequest(**payload) + + +class EventApplyBackup: + topic: Literal[Topic.APPLY_BACKUP] + payload: OperationRequest + + def __init__(self, payload: dict) -> None: # noqa: D107 + self.topic = Topic.APPLY_BACKUP + self.payload = OperationRequest(**payload) + + +class EventAirConditioning: + topic: Literal[Topic.AIR_CONDITIONING] + payload: ServiceEvent + + def __init__(self, payload: dict) -> None: # noqa: D107 + self.topic = Topic.AIR_CONDITIONING + self.payload = ServiceEvent(**payload) + + +class EventCharging: + topic: Literal[Topic.CHARGING] + payload: ServiceEventCharging + + def __init__(self, payload: dict) -> None: # noqa: D107 + self.topic = Topic.CHARGING + self.payload = ServiceEventCharging(**payload) + + +class EventAccess: + topic: Literal[Topic.ACCESS] + payload: ServiceEvent + + def __init__(self, payload: dict) -> None: # noqa: D107 + self.topic = Topic.ACCESS + self.payload = ServiceEvent(**payload) + + +class EventLights: + topic: Literal[Topic.LIGHTS] + payload: ServiceEvent + + def __init__(self, payload: dict) -> None: # noqa: D107 + self.topic = Topic.LIGHTS + self.payload = ServiceEvent(**payload) + + +Event = ( + EventUpdateBatterySupport + | EventLockVehicle + | EventWakeup + | EventSetTargetTemperature + | EventStartStopAirConditioning + | EventStartStopWindowHeating + | EventStartStopCharging + | EventApplyBackup + | EventHonkAndFlash + | EventApplyBackup + | EventAirConditioning + | EventCharging + | EventAccess + | EventLights +) class MQTT: @@ -31,7 +188,6 @@ async def connect(self) -> None: self.client.on_connect = self._on_connect self.client.on_message = self._on_message self.client.tls_set_context(context=ssl.create_default_context()) - self.client.tls_insecure_set(value=True) self.client.username_pw_set( self.user.id, await self.api.idk_session.get_access_token(self.api.session) ) @@ -41,8 +197,8 @@ def loop_forever(self) -> None: """Make the MQTT client process new messages until the current process is cancelled.""" self.client.loop_forever() - def _on_connect(self, client: Client, _userdata: Any, _flags: Any, reason: int) -> None: # noqa: ANN401 - print(f"MQTT Connected. {reason}") + def _on_connect(self, client: Client, _userdata: None, _flags: dict, _reason: int) -> None: + _LOGGER.info("MQTT Connected.") user_id = self.user.id for vin in self.vehicles: @@ -72,5 +228,51 @@ def _on_connect(self, client: Client, _userdata: Any, _flags: Any, reason: int) f"{user_id}/{vin}/operation-request/vehicle-services-backup/apply-backup" ) - def _on_message(self, _client: Client, _userdata: Any, msg: MQTTMessage) -> None: # noqa: ANN401 - print(f"{msg.topic}: {msg.payload}") + def _emit(self, event: Event) -> None: + print(event) + + def _on_message(self, _client: Client, _userdata: None, msg: MQTTMessage) -> None: # noqa: C901, PLR0912 + topic_match = TOPIC_RE.match(msg.topic) + + if not topic_match: + _LOGGER.warning("Unexpected MQTT topic encountered: %s", topic_match) + return + + [_user_id, vin, topic] = topic_match.groups() + data = cast(str, msg.payload) + + if len(data) == 0: + return + + _LOGGER.debug("Message received for %s (%s): %s", vin, topic, data) + + data = json.loads(msg.payload) + + if topic == "account-event/privacy": + pass + elif topic == "operation-request/charging/update-battery-support": + self._emit(EventUpdateBatterySupport(data)) + elif topic == "operation-request/vehicle-access/lock-vehicle": + self._emit(EventLockVehicle(data)) + elif topic == "operation-request/vehicle-wakeup/wakeup": + self._emit(EventWakeup(data)) + elif topic == "operation-request/air-conditioning/set-target-temperature": + self._emit(EventSetTargetTemperature(data)) + elif topic == "operation-request/air-conditioning/start-stop-air-conditioning": + self._emit(EventStartStopAirConditioning(data)) + elif topic == "operation-request/air-conditioning/start-stop-window-heating": + self._emit(EventStartStopWindowHeating(data)) + elif topic == "operation-request/charging/start-stop-charging": + self._emit(EventStartStopCharging(data)) + elif topic == "operation-request/vehicle-services-backup/apply-backup": + self._emit(EventApplyBackup(data)) + elif topic == "operation-request/vehicle-access/honk-and-flash": + self._emit(EventHonkAndFlash(data)) + elif topic == "service-event/air-conditioning": + self._emit(EventAirConditioning(data)) + elif topic == "service-event/charging": + self._emit(EventCharging(data)) + elif topic == "service-event/vehicle-status/access": + self._emit(EventAccess(data)) + elif topic == "service-event/vehicle-status/lights": + self._emit(EventLights(data)) diff --git a/myskoda/rest_api.py b/myskoda/rest_api.py index 2911582..255f7bd 100644 --- a/myskoda/rest_api.py +++ b/myskoda/rest_api.py @@ -154,7 +154,7 @@ async def get_user(self) -> User: f"{BASE_URL_SKODA}/api/v1/users", headers=await self._headers(), ) as response: - _LOGGER.debug("vin %s: Received user") + _LOGGER.debug("Received user") return User(**await response.json()) async def list_vehicles(self) -> list[str]: diff --git a/poetry.lock b/poetry.lock index 92cbbe1..5849a6d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -235,6 +235,23 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coloredlogs" +version = "15.0.1" +description = "Colored terminal output for Python's logging module" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] + +[package.dependencies] +humanfriendly = ">=9.1" + +[package.extras] +cron = ["capturer (>=2.4)"] + [[package]] name = "frozenlist" version = "1.4.1" @@ -321,17 +338,34 @@ files = [ {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, ] +[[package]] +name = "humanfriendly" +version = "10.0" +description = "Human friendly output for text interfaces using Python" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, + {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, +] + +[package.dependencies] +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} + [[package]] name = "idna" -version = "3.8" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "multidict" version = "6.1.0" @@ -533,6 +567,20 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +[[package]] +name = "pyreadline3" +version = "3.5.2" +description = "A python implementation of GNU readline." +optional = true +python-versions = ">=3.8" +files = [ + {file = "pyreadline3-3.5.2-py3-none-any.whl", hash = "sha256:a87d56791e2965b2b187e2ea33dcf664600842c997c0623c95cf8ef07db83de9"}, + {file = "pyreadline3-3.5.2.tar.gz", hash = "sha256:ba82292e52c5a3bb256b291af0c40b457c1e8699cac9a873abbcaac8aef3a1bb"}, +] + +[package.extras] +dev = ["build", "flake8", "pytest", "twine"] + [[package]] name = "pyright" version = "1.1.380" @@ -615,29 +663,29 @@ files = [ [[package]] name = "ruff" -version = "0.6.4" +version = "0.6.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.4-py3-none-linux_armv6l.whl", hash = "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258"}, - {file = "ruff-0.6.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60"}, - {file = "ruff-0.6.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa"}, - {file = "ruff-0.6.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6"}, - {file = "ruff-0.6.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d"}, - {file = "ruff-0.6.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa"}, - {file = "ruff-0.6.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1"}, - {file = "ruff-0.6.4-py3-none-win32.whl", hash = "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523"}, - {file = "ruff-0.6.4-py3-none-win_amd64.whl", hash = "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58"}, - {file = "ruff-0.6.4-py3-none-win_arm64.whl", hash = "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14"}, - {file = "ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212"}, + {file = "ruff-0.6.5-py3-none-linux_armv6l.whl", hash = "sha256:7e4e308f16e07c95fc7753fc1aaac690a323b2bb9f4ec5e844a97bb7fbebd748"}, + {file = "ruff-0.6.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:932cd69eefe4daf8c7d92bd6689f7e8182571cb934ea720af218929da7bd7d69"}, + {file = "ruff-0.6.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a8d42d11fff8d3143ff4da41742a98f8f233bf8890e9fe23077826818f8d680"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a50af6e828ee692fb10ff2dfe53f05caecf077f4210fae9677e06a808275754f"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:794ada3400a0d0b89e3015f1a7e01f4c97320ac665b7bc3ade24b50b54cb2972"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:381413ec47f71ce1d1c614f7779d88886f406f1fd53d289c77e4e533dc6ea200"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52e75a82bbc9b42e63c08d22ad0ac525117e72aee9729a069d7c4f235fc4d276"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09c72a833fd3551135ceddcba5ebdb68ff89225d30758027280968c9acdc7810"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:800c50371bdcb99b3c1551d5691e14d16d6f07063a518770254227f7f6e8c178"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e25ddd9cd63ba1f3bd51c1f09903904a6adf8429df34f17d728a8fa11174253"}, + {file = "ruff-0.6.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7291e64d7129f24d1b0c947ec3ec4c0076e958d1475c61202497c6aced35dd19"}, + {file = "ruff-0.6.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9ad7dfbd138d09d9a7e6931e6a7e797651ce29becd688be8a0d4d5f8177b4b0c"}, + {file = "ruff-0.6.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:005256d977021790cc52aa23d78f06bb5090dc0bfbd42de46d49c201533982ae"}, + {file = "ruff-0.6.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:482c1e6bfeb615eafc5899127b805d28e387bd87db38b2c0c41d271f5e58d8cc"}, + {file = "ruff-0.6.5-py3-none-win32.whl", hash = "sha256:cf4d3fa53644137f6a4a27a2b397381d16454a1566ae5335855c187fbf67e4f5"}, + {file = "ruff-0.6.5-py3-none-win_amd64.whl", hash = "sha256:3e42a57b58e3612051a636bc1ac4e6b838679530235520e8f095f7c44f706ff9"}, + {file = "ruff-0.6.5-py3-none-win_arm64.whl", hash = "sha256:51935067740773afdf97493ba9b8231279e9beef0f2a8079188c4776c25688e0"}, + {file = "ruff-0.6.5.tar.gz", hash = "sha256:4d32d87fab433c0cf285c3683dd4dae63be05fd7a1d65b3f5bf7cdd05a6b96fb"}, ] [[package]] @@ -793,9 +841,9 @@ idna = ">=2.0" multidict = ">=4.0" [extras] -cli = ["asyncclick", "termcolor"] +cli = ["asyncclick", "coloredlogs", "termcolor"] [metadata] lock-version = "2.0" python-versions = ">=3.12.0" -content-hash = "c9f718f60c75825328fdfd2f73f067e17bf92178888d5575202d494cd48dcdac" +content-hash = "65ca303518fe7c6680dd2cef4c3e5a1e243ea54ef05ff62fef0c38a296573285" diff --git a/pyproject.toml b/pyproject.toml index 0346a57..e5b9360 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ asyncio = "^3" asyncclick = { version = "^8.1.7.2", optional = true } termcolor = { version = "^2.4.0", optional = true } paho-mqtt = "^1" +coloredlogs = {version = "^15.0.1", optional = true} [tool.poetry.group.dev.dependencies] ruff = "^0.6.4" @@ -25,7 +26,7 @@ pyright = "^1.1.379" myskoda = "myskoda.cli:cli" [tool.poetry.extras] -cli = ["asyncclick", "termcolor"] +cli = ["asyncclick", "coloredlogs", "termcolor"] [build-system] requires = ["poetry-core"]