diff --git a/.gitignore b/.gitignore index 42bc5ae..a6f8a93 100644 --- a/.gitignore +++ b/.gitignore @@ -147,3 +147,4 @@ token.json # Swagger YAML hcsdk.yaml +hcsdk-production.yaml diff --git a/pyproject.toml b/pyproject.toml index 82e982c..8ce705e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,6 +124,7 @@ ignore = [ "EM101", # raw-string-in-exception "EM102", # f-string-in-exception "ISC001", # single-line-implicit-string-concatenation + "PLR0913", # too-many-arguments "PLR2004", # magic-value-comparison "TC001", # typing-only-first-party-import "TC002", # typing-only-third-party-import diff --git a/scripts/swagger_model.py b/scripts/swagger_model.py index c02cca0..a2d325a 100644 --- a/scripts/swagger_model.py +++ b/scripts/swagger_model.py @@ -25,6 +25,7 @@ ("ArrayOfHomeAppliances", "homeappliances"): "HomeAppliance", ("ArrayOfEvents", "items"): "Event", ("Program", "options"): "Option", + ("Program", "constraints"): "ProgramConstraints", ("ArrayOfAvailablePrograms", "programs"): "EnumerateAvailableProgram", ( "EnumerateAvailableProgram", @@ -32,6 +33,8 @@ ): "EnumerateAvailableProgramConstraints", ("EnumerateAvailableProgramConstraints", "execution"): "Execution", ("ArrayOfPrograms", "programs"): "EnumerateProgram", + ("ArrayOfPrograms", "active"): "Program", + ("ArrayOfPrograms", "selected"): "Program", ("EnumerateProgram", "constraints"): "EnumerateProgramConstraints", ("EnumerateProgramConstraints", "execution"): "Execution", ("ProgramDefinition", "options"): "ProgramDefinitionOption", @@ -43,6 +46,8 @@ ("PutSettings", "data"): "PutSetting", ("ArrayOfStatus", "status"): "Status", ("Status", "constraints"): "StatusConstraints", + ("ArrayOfCommands", "commands"): "Command", + ("PutCommands", "data"): "PutCommand", } PARAMETER_ENUM_MAP = { "Accept": "ContentType", @@ -171,9 +176,12 @@ class DefinitionModelBase(ABC): definition: str all_definitions: list[str] description: str | None = None + generated_classes: set[str] = field(default_factory=set) @abstractmethod - def generate_code(self, definition: str, *, generate_class: bool = False) -> str: + def generate_code( + self, definition: str, *, generate_class: bool = False, required: bool = False + ) -> str: """Return the Python code as a string for this model.""" @staticmethod @@ -192,18 +200,24 @@ class {definition}(DataClassJSONMixin): class DefinitionModelUnknown(DefinitionModelBase): """Represent a string type model.""" - def generate_code(self, definition: str, *, generate_class: bool = False) -> str: + def generate_code( + self, definition: str, *, generate_class: bool = False, required: bool = False + ) -> str: """Return the Python code as a string for this model.""" - return "Any" + suffix = "" if required else " | None" + return f"Any{suffix}" @dataclass(kw_only=True) class DefinitionModelString(DefinitionModelBase): """Represent a string type model.""" - def generate_code(self, definition: str, *, generate_class: bool = False) -> str: + def generate_code( + self, definition: str, *, generate_class: bool = False, required: bool = False + ) -> str: """Return the Python code as a string for this model.""" - return "str" + suffix = "" if required else " | None" + return f"str{suffix}" @dataclass(kw_only=True) @@ -212,16 +226,18 @@ class DefinitionModelStringEnum(DefinitionModelBase): enum: list[str] - def generate_code(self, definition: str, *, generate_class: bool = False) -> str: + def generate_code( + self, definition: str, *, generate_class: bool = False, required: bool = False + ) -> str: """Return the Python code as a string for this model.""" if (description := self.description) is None: raise ValueError("Missing description") - suffix = "" + suffix = "" if required else " | None" if generate_class: code_enum = "\n ".join( f'{enum.upper()} = "{enum}"' for enum in self.enum ) - suffix = ( + self.generated_classes.add( "\n\n" f"class {definition.capitalize()}(StrEnum):\n" f' """{description.strip()}."""\n\n ' @@ -237,27 +253,36 @@ class DefinitionModelInteger(DefinitionModelBase): format_: str | None = None - def generate_code(self, definition: str, *, generate_class: bool = False) -> str: + def generate_code( + self, definition: str, *, generate_class: bool = False, required: bool = False + ) -> str: """Return the Python code as a string for this model.""" - return "int" + suffix = "" if required else " | None" + return f"int{suffix}" @dataclass(kw_only=True) class DefinitionModelBoolean(DefinitionModelBase): """Represent a boolean type model.""" - def generate_code(self, definition: str, *, generate_class: bool = False) -> str: + def generate_code( + self, definition: str, *, generate_class: bool = False, required: bool = False + ) -> str: """Return the Python code as a string for this model.""" - return "bool" + suffix = "" if required else " | None" + return f"bool{suffix}" @dataclass(kw_only=True) class DefinitionModelStringNumberBoolean(DefinitionModelBase): """Represent a union of string number and boolean type model.""" - def generate_code(self, definition: str, *, generate_class: bool = False) -> str: + def generate_code( + self, definition: str, *, generate_class: bool = False, required: bool = False + ) -> str: """Return the Python code as a string for this model.""" - return "str | Number | bool" + suffix = "" if required else " | None" + return f"str | float | bool{suffix}" @dataclass(kw_only=True) @@ -275,13 +300,15 @@ def __post_init__(self) -> None: data=self.raw_items, ) - def generate_code(self, definition: str, *, generate_class: bool = False) -> str: + def generate_code( + self, definition: str, *, generate_class: bool = False, required: bool = False + ) -> str: """Return the Python code as a string for this model.""" - suffix = "" + suffix = "" if required else " | None" if definition != self.definition and generate_class: item = definition if definition not in self.all_definitions: - suffix = ( + self.generated_classes.add( "\n\n" f"{self.generate_class(definition)} " f"{self.items.generate_code(definition)}" @@ -289,6 +316,9 @@ def generate_code(self, definition: str, *, generate_class: bool = False) -> str ) else: item = self.items.generate_code(definition) + + self.generated_classes.update(self.items.generated_classes) + self.items.generated_classes.clear() return f"list[{item}]{suffix}" @@ -312,12 +342,14 @@ def __post_init__(self) -> None: self.properties = properties - def generate_code(self, definition: str, *, generate_class: bool = False) -> str: + def generate_code( + self, definition: str, *, generate_class: bool = False, required: bool = False + ) -> str: """Return the Python code as a string for this model.""" if definition != self.definition and generate_class: - suffix = "" + suffix = "" if required else " | None" if definition not in self.all_definitions: - suffix = ( + self.generated_classes.add( "\n\n" f"{self.generate_class(definition)} " f"{self.generate_code(definition)}" @@ -326,32 +358,44 @@ def generate_code(self, definition: str, *, generate_class: bool = False) -> str return f"{definition}{suffix}" properties = "" sorted_properties = {} - if required := self.required: - for prop in required: + if required_properties := self.required: + for prop in required_properties: sorted_properties[prop] = self.properties[prop] sorted_properties.update(self.properties) generate_class = False original_definition = definition for prop, model in self.properties.items(): + required_property = bool( + required_properties and prop in required_properties + ) if nested_definition := DEFINITION_NESTED_MAP.get((definition, prop)): definition = nested_definition generate_class = True if prop in ("data", "error") and definition == self.definition: - properties += f" {model.generate_code(definition)}\n" + model_code = model.generate_code(definition, required=required_property) + properties += f" {model_code}\n" else: prop_code = model.generate_code( - definition, generate_class=generate_class + definition, + generate_class=generate_class, + required=required_property, ) properties += f" {prop}: {prop_code}\n" definition = original_definition - - return f"{properties}".strip() + self.generated_classes.update(model.generated_classes) + model.generated_classes.clear() + suffix = ( + "".join(self.generated_classes) + if self.definition == original_definition + else "" + ) + return f"{properties}{suffix}".strip() -def load_yaml() -> dict[str, Any]: +def load_yaml(path: str = "hcsdk-production.yaml") -> dict[str, Any]: """Load yaml.""" - raw = Path("hcsdk.yaml").read_text(encoding="utf-8") + raw = Path(path).read_text(encoding="utf-8") return load(raw, Loader=SafeLoader) @@ -445,7 +489,6 @@ def run() -> None: from dataclasses import dataclass from enum import StrEnum -from numbers import Number from typing import Any from mashumaro.mixins.json import DataClassJSONMixin diff --git a/src/aiohomeconnect/cli/__init__.py b/src/aiohomeconnect/cli/__init__.py index dace89b..a36c2e4 100644 --- a/src/aiohomeconnect/cli/__init__.py +++ b/src/aiohomeconnect/cli/__init__.py @@ -7,6 +7,8 @@ import typer import uvicorn +from aiohomeconnect.model import StatusKey + from .client import CLIClient, TokenManager cli = typer.Typer() @@ -74,7 +76,7 @@ async def _get_appliances( ) -> None: """Get the appliances.""" client = CLIClient(client_id, client_secret) - rich_print(await client.get_appliances()) + rich_print(await client.get_home_appliances()) @cli.command() @@ -86,7 +88,11 @@ def get_operation_state(client_id: str, client_secret: str, ha_id: str) -> None: async def _get_operation_state(client_id: str, client_secret: str, ha_id: str) -> None: """Get the operation state of the device.""" client = CLIClient(client_id, client_secret) - rich_print(await client.get_operation_state(ha_id)) + rich_print( + await client.get_status_value( + ha_id, status_key=StatusKey.BSH_COMMON_OPERATION_STATE + ) + ) if __name__ == "__main__": diff --git a/src/aiohomeconnect/client.py b/src/aiohomeconnect/client.py index 94e3450..fbee0f9 100644 --- a/src/aiohomeconnect/client.py +++ b/src/aiohomeconnect/client.py @@ -7,6 +7,36 @@ from httpx import AsyncClient, Response +from .model import ( + ArrayOfAvailablePrograms, + ArrayOfCommands, + ArrayOfEvents, + ArrayOfHomeAppliances, + ArrayOfImages, + ArrayOfOptions, + ArrayOfPrograms, + ArrayOfSettings, + ArrayOfStatus, + CommandKey, + ContentType, + GetSetting, + HomeAppliance, + Language, + Option, + OptionKey, + Program, + ProgramConstraints, + ProgramDefinition, + ProgramKey, + PutCommand, + PutCommands, + PutSetting, + PutSettings, + SettingKey, + Status, + StatusKey, +) + class AbstractAuth(ABC): """Abstract class to make authenticated requests.""" @@ -25,15 +55,16 @@ async def request(self, method: str, url: str, **kwargs: Any) -> Response: The url parameter must start with a slash. """ - headers = kwargs.get("headers") + headers = kwargs.pop("headers", None) headers = {} if headers is None else dict(headers) + headers = {key: val for key, val in headers.items() if val is not None} access_token = await self.async_get_access_token() headers["authorization"] = f"Bearer {access_token}" return await self.client.request( method, - f"{self.host}{url}", + f"{self.host}/api{url}", **kwargs, headers=headers, ) @@ -46,15 +77,656 @@ def __init__(self, auth: AbstractAuth) -> None: """Initialize the client.""" self._auth = auth - async def get_appliances(self) -> dict[str, Any]: - """Return all paired devices.""" - response = await self._auth.request("GET", "/api/homeappliances") - return response.json() + async def get_home_appliances(self) -> ArrayOfHomeAppliances: + """Get all home appliances which are paired with the logged-in user account. + + This endpoint returns a list of all home appliances which are paired + with the logged-in user account. All paired home appliances are returned + independent of their current connection state. The connection state can + be retrieved within the field 'connected' of the respective home appliance. + The haId is the primary access key for further API access to a specific + home appliance. + """ + response = await self._auth.request( + "GET", + "/homeappliances", + headers=None, + ) + return ArrayOfHomeAppliances.from_dict(response.json()["data"]) + + async def get_specific_appliance( + self, + ha_id: str, + ) -> HomeAppliance: + """Get a specific paired home appliance. + + This endpoint returns a specific home appliance which is paired with the + logged-in user account. It is returned independent of their current + connection state. The connection state can be retrieved within the field + 'connected' of the respective home appliance. + The haId is the primary access key for further API access to a specific + home appliance. + """ + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}", + headers=None, + ) + return HomeAppliance.from_dict(response.json()["data"]) + + async def get_all_programs( + self, + ha_id: str, + *, + accept_language: Language | None = None, + ) -> ArrayOfPrograms: + """Get all programs of a given home appliance.""" + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/programs", + headers={"Accept-Language": accept_language}, + ) + return ArrayOfPrograms.from_dict(response.json()["data"]) + + async def get_available_programs( + self, + ha_id: str, + *, + accept_language: Language | None = None, + ) -> ArrayOfAvailablePrograms: + """Get all currently available programs on the given home appliance.""" + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/programs/available", + headers={"Accept-Language": accept_language}, + ) + return ArrayOfAvailablePrograms.from_dict(response.json()["data"]) + + async def get_available_program( + self, + ha_id: str, + *, + program_key: ProgramKey, + accept_language: Language | None = None, + ) -> ProgramDefinition: + """Get a specific available program.""" + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/programs/available/{program_key}", + headers={"Accept-Language": accept_language}, + ) + return ProgramDefinition.from_dict(response.json()["data"]) + + async def get_active_program( + self, + ha_id: str, + *, + accept_language: Language | None = None, + ) -> Program: + """Get the active program.""" + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/programs/active", + headers={"Accept-Language": accept_language}, + ) + return Program.from_dict(response.json()["data"]) + + async def start_program( + self, + ha_id: str, + *, + program_key: ProgramKey, + name: str | None = None, + options: list[Option] | None = None, + constraints: ProgramConstraints | None = None, + accept_language: Language | None = None, + ) -> None: + """Start the given program. + + By putting a program object to this endpoint, the system will try to + start it if all preconditions are fulfilled: + * Home appliance is connected + * *Remote Control* and *Remote Control Start Allowed* is enabled + * No other program is currently active + + Furthermore, the program must exist on the home appliance and its + options must be set correctly. + Keys of program, which can be executed on an oven, are for instance: + * *Cooking.Oven.Program.HeatingMode.HotAir* + * *Cooking.Oven.Program.HeatingMode.TopBottomHeating* + * *Cooking.Oven.Program.HeatingMode.PizzaSetting* + * *Cooking.Oven.Program.HeatingMode.PreHeating* + + Keys for options of these oven programs are: + * *Cooking.Oven.Option.SetpointTemperature*: 30 - 250 °C + * *BSH.Common.Option.Duration*: 1 - 86340 seconds + + For further documentation, visit the appliance-specific programs pages: + * [Cleaning Robot](https://api-docs.home-connect.com/programs-and-options?#cleaning-robot) + * [Coffee Machine](https://api-docs.home-connect.com/programs-and-options?#coffee-machine) + * [Cooktop](https://api-docs.home-connect.com/programs-and-options?#cooktop) + * [Cook Processor](https://api-docs.home-connect.com/programs-and-options?#cook-processor) + * [Dishwasher](https://api-docs.home-connect.com/programs-and-options?#dishwasher) + * [Dryer](https://api-docs.home-connect.com/programs-and-options?#dryer) + * [Hood](https://api-docs.home-connect.com/programs-and-options?#hood) + * [Oven](https://api-docs.home-connect.com/programs-and-options?#oven) + * [Warming Drawer](https://api-docs.home-connect.com/programs-and-options?#warming-drawer) + * [Washer](https://api-docs.home-connect.com/programs-and-options?#washer) + * [Washer Dryer](https://api-docs.home-connect.com/programs-and-options?#washer-dryer) + + There are no programs available for freezers, fridge freezers, + refrigerators and wine coolers. + """ + program = Program( + key=program_key, name=name, options=options, constraints=constraints + ) + await self._auth.request( + "PUT", + f"/homeappliances/{ha_id}/programs/active", + headers={"Accept-Language": accept_language}, + data=program.to_dict(), + ) + + async def stop_program( + self, + ha_id: str, + *, + accept_language: Language | None = None, + ) -> None: + """Stop the active program.""" + await self._auth.request( + "DELETE", + f"/homeappliances/{ha_id}/programs/active", + headers={"Accept-Language": accept_language}, + ) + + async def get_active_program_options( + self, + ha_id: str, + *, + accept_language: Language | None = None, + ) -> ArrayOfOptions: + """Get all options of the active program. + + You can retrieve a list of options of the currently running program. + + For detailed documentation of the available options, + visit the appliance-specific programs pages: + * [Cleaning Robot](https://api-docs.home-connect.com/programs-and-options?#cleaning-robot) + * [Coffee Machine](https://api-docs.home-connect.com/programs-and-options?#coffee-machine) + * [Cooktop](https://api-docs.home-connect.com/programs-and-options?#cooktop) + * [Cook Processor](https://api-docs.home-connect.com/programs-and-options?#cook-processor) + * [Dishwasher](https://api-docs.home-connect.com/programs-and-options?#dishwasher) + * [Dryer](https://api-docs.home-connect.com/programs-and-options?#dryer) + * [Hood](https://api-docs.home-connect.com/programs-and-options?#hood) + * [Oven](https://api-docs.home-connect.com/programs-and-options?#oven) + * [Warming Drawer](https://api-docs.home-connect.com/programs-and-options?#warming-drawer) + * [Washer](https://api-docs.home-connect.com/programs-and-options?#washer) + * [Washer Dryer](https://api-docs.home-connect.com/programs-and-options?#washer-dryer) + """ + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/programs/active/options", + headers={"Accept-Language": accept_language}, + ) + return ArrayOfOptions.from_dict(response.json()["data"]) + + async def set_active_program_options( + self, + ha_id: str, + *, + array_of_options: ArrayOfOptions, + accept_language: Language | None = None, + ) -> None: + """Set all options of the active program. + + Update the options for the currently running program. + With this API endpoint, you have to provide all options with + their new values. If you want to update only one option, you can use the + endpoint specific to that option. + + Please note that changing options of the running program is currently only + supported by ovens. + """ + await self._auth.request( + "PUT", + f"/homeappliances/{ha_id}/programs/active/options", + headers={"Accept-Language": accept_language}, + data=array_of_options.to_dict(), + ) + + async def get_active_program_option( + self, + ha_id: str, + *, + option_key: OptionKey, + accept_language: Language | None = None, + ) -> Option: + """Get a specific option of the active program.""" + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/programs/active/options/{option_key}", + headers={"Accept-Language": accept_language}, + ) + return Option.from_dict(response.json()["data"]) + + async def set_active_program_option( + self, + ha_id: str, + *, + option_key: OptionKey, + value: Any, + name: str | None = None, + display_value: str | None = None, + unit: str | None = None, + accept_language: Language | None = None, + ) -> None: + """Set a specific option of the active program. + + This operation can be used to modify one specific option of the active + program, e.g. to extend the duration of the active program by + another 5 minutes. - async def get_operation_state(self, ha_id: str) -> dict[str, Any]: - """Return the operation state of the device.""" + Please note that changing options of the running program is currently only + supported by ovens. + """ + option = Option( + key=option_key, + name=name, + value=value, + display_value=display_value, + unit=unit, + ) + await self._auth.request( + "PUT", + f"/homeappliances/{ha_id}/programs/active/options/{option_key}", + headers={"Accept-Language": accept_language}, + data=option.to_dict(), + ) + + async def get_selected_program( + self, + ha_id: str, + *, + accept_language: Language | None = None, + ) -> Program: + """Get the selected program. + + In most cases the selected program is the program which is currently + shown on the display of the home appliance. This program can then be + manually adjusted or started on the home appliance itself. + """ + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/programs/selected", + headers={"Accept-Language": accept_language}, + ) + return Program.from_dict(response.json()["data"]) + + async def set_selected_program( + self, + ha_id: str, + *, + program_key: ProgramKey, + name: str | None = None, + options: list[Option] | None = None, + constraints: ProgramConstraints | None = None, + accept_language: Language | None = None, + ) -> None: + """Select the given program. + + In most cases the selected program is the program which is currently + shown on the display of the home appliance. This program can then be + manually adjusted or started on the home appliance itself. + + A selected program will not be started automatically. You don't have + to set a program as selected first if you intend to start it via API - + you can set it directly as active program. + + Selecting a program will update the available options and constraints + directly from the home appliance. Any changes to the available options + due to the state of the appliance is only reflected in the selected program. + """ + program = Program( + key=program_key, name=name, options=options, constraints=constraints + ) + await self._auth.request( + "PUT", + f"/homeappliances/{ha_id}/programs/selected", + headers={"Accept-Language": accept_language}, + data=program.to_dict(), + ) + + async def get_selected_program_options( + self, + ha_id: str, + *, + accept_language: Language | None = None, + ) -> ArrayOfOptions: + """Get all options of the selected program.""" + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/programs/selected/options", + headers={"Accept-Language": accept_language}, + ) + return ArrayOfOptions.from_dict(response.json()["data"]) + + async def set_selected_program_options( + self, + ha_id: str, + *, + array_of_options: ArrayOfOptions, + accept_language: Language | None = None, + ) -> None: + """Set all options of the selected program.""" + await self._auth.request( + "PUT", + f"/homeappliances/{ha_id}/programs/selected/options", + headers={"Accept-Language": accept_language}, + data=array_of_options.to_dict(), + ) + + async def get_selected_program_option( + self, + ha_id: str, + *, + option_key: OptionKey, + accept_language: Language | None = None, + ) -> Option: + """Get a specific option of the selected program.""" + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/programs/selected/options/{option_key}", + headers={"Accept-Language": accept_language}, + ) + return Option.from_dict(response.json()["data"]) + + async def set_selected_program_option( + self, + ha_id: str, + *, + option_key: OptionKey, + value: Any, + name: str | None = None, + display_value: str | None = None, + unit: str | None = None, + accept_language: Language | None = None, + ) -> None: + """Set a specific option of the selected program.""" + option = Option( + key=option_key, + name=name, + value=value, + display_value=display_value, + unit=unit, + ) + await self._auth.request( + "PUT", + f"/homeappliances/{ha_id}/programs/selected/options/{option_key}", + headers={"Accept-Language": accept_language}, + data=option.to_dict(), + ) + + async def get_images( + self, + ha_id: str, + *, + accept_language: Language | None = None, + ) -> ArrayOfImages: + """Get a list of available images.""" + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/images", + headers={"Accept-Language": accept_language}, + ) + return ArrayOfImages.from_dict(response.json()["data"]) + + async def get_image( + self, + ha_id: str, + *, + image_key: str, + ) -> None: + """Get a specific image.""" + await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/images/{image_key}", + headers=None, + ) + + async def get_settings( + self, + ha_id: str, + *, + accept_language: Language | None = None, + ) -> ArrayOfSettings: + """Get a list of available settings. + + Get a list of available setting of the home appliance. + + Further documentation + can be found [here](https://api-docs.home-connect.com/settings). + """ + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/settings", + headers={"Accept-Language": accept_language}, + ) + return ArrayOfSettings.from_dict(response.json()["data"]) + + async def set_settings( + self, + ha_id: str, + *, + put_settings: PutSettings, + accept_language: Language | None = None, + ) -> None: + """Set multiple settings.""" + await self._auth.request( + "PUT", + f"/homeappliances/{ha_id}/settings", + headers={"Accept-Language": accept_language}, + data=put_settings.to_dict(), + ) + + async def get_setting( + self, + ha_id: str, + *, + setting_key: SettingKey, + accept_language: Language | None = None, + ) -> GetSetting: + """Get a specific setting.""" + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/settings/{setting_key}", + headers={"Accept-Language": accept_language}, + ) + return GetSetting.from_dict(response.json()["data"]) + + async def set_setting( + self, + ha_id: str, + *, + setting_key: SettingKey, + value: Any, + accept_language: Language | None = None, + ) -> None: + """Set a specific setting.""" + put_setting = PutSetting(key=setting_key, value=value) + await self._auth.request( + "PUT", + f"/homeappliances/{ha_id}/settings/{setting_key}", + headers={"Accept-Language": accept_language}, + data=put_setting.to_dict(), + ) + + async def get_status( + self, + ha_id: str, + *, + accept_language: Language | None = None, + ) -> ArrayOfStatus: + """Get a list of available status. + + A detailed description of the available status + can be found [here](https://api-docs.home-connect.com/states). + """ + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/status", + headers={"Accept-Language": accept_language}, + ) + return ArrayOfStatus.from_dict(response.json()["data"]) + + async def get_status_value( + self, + ha_id: str, + *, + status_key: StatusKey, + accept_language: Language | None = None, + ) -> Status: + """Get a specific status. + + A detailed description of the available status + can be found [here](https://api-docs.home-connect.com/states). + """ + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/status/{status_key}", + headers={"Accept-Language": accept_language}, + ) + return Status.from_dict(response.json()["data"]) + + async def get_available_commands( + self, + ha_id: str, + *, + accept_language: Language | None = None, + ) -> ArrayOfCommands: + """Get a list of available and writable commands.""" + response = await self._auth.request( + "GET", + f"/homeappliances/{ha_id}/commands", + headers={"Accept-Language": accept_language}, + ) + return ArrayOfCommands.from_dict(response.json()["data"]) + + async def put_commands( + self, + ha_id: str, + *, + put_commands: PutCommands, + accept_language: Language | None = None, + ) -> None: + """Execute multiple commands.""" + await self._auth.request( + "PUT", + f"/homeappliances/{ha_id}/commands", + headers={"Accept-Language": accept_language}, + data=put_commands.to_dict(), + ) + + async def put_command( + self, + ha_id: str, + *, + command_key: CommandKey, + value: Any, + accept_language: Language | None = None, + ) -> None: + """Execute a specific command.""" + put_command = PutCommand(key=command_key, value=value) + await self._auth.request( + "PUT", + f"/homeappliances/{ha_id}/commands/{command_key}", + headers={"Accept-Language": accept_language}, + data=put_command.to_dict(), + ) + + async def get_all_events( + self, + *, + accept_language: Language | None = None, + ) -> ArrayOfEvents: + """Get stream of events for all appliances - NOT WORKING WITH SWAGGER. + + Server Sent Events are available as Eventsource API in JavaScript + and are implemented by various HTTP client libraries and tools + including curl. + + Unfortunately, SSE is not compatible to OpenAPI specs and can therefore + not be properly specified within this API description. + + An SSE event contains three parts separated by linebreaks: event, data and id. + Different events are separated by empty lines. + + The event field can be one of these types: + KEEP-ALIVE, STATUS, EVENT, NOTIFY, CONNECTED, DISCONNECTED, PAIRED, DEPAIRED. + + In case of all event types (except KEEP-ALIVE), + the "data" field is populated with the JSON object defined below. + + The id contains the home appliance ID. (except for KEEP-ALIVE event type) + + Further documentation can be found here: + * [Events availability matrix](https://api-docs.home-connect.com/events#availability-matrix) + * [Program changes](https://api-docs.home-connect.com/events#program-changes) + * [Option changes](https://api-docs.home-connect.com/events#option-changes) + * [Program progress changes](https://api-docs.home-connect.com/events#program-progress-changes) + * [Home appliance state changes](https://api-docs.home-connect.com/events#home-appliance-state-changes) + """ + response = await self._auth.request( + "GET", + "/homeappliances/events", + headers={"Accept-Language": accept_language}, + ) + return ArrayOfEvents.from_dict(response.json()["data"]) + + async def get_events( + self, + ha_id: str, + *, + accept_language: Language | None = None, + accept: ContentType | None = None, + ) -> ArrayOfEvents: + """Get stream of events for one appliance - NOT WORKING WITH SWAGGER. + + If you want to do a one-time query of the current status, you can ask for + the content-type `application/vnd.bsh.sdk.v1+json` and get the status + as normal HTTP response. + + If you want an ongoing stream of events in real time, ask for the content + type `text/event-stream` and you'll get a stream as Server Sent Events. + + Server Sent Events are available as Eventsource API in JavaScript + and are implemented by various HTTP client libraries and tools + including curl. + + Unfortunately, SSE is not compatible to OpenAPI specs and can therefore + not be properly specified within this API description. + + An SSE event contains three parts separated by linebreaks: event, data and id. + Different events are separated by empty lines. + + The event field can be one of these types: + KEEP-ALIVE, STATUS, EVENT, NOTIFY, CONNECTED, DISCONNECTED. + + In case of all event types (except KEEP-ALIVE), + the "data" field is populated with the JSON object defined below. + + The id contains the home appliance ID. + + Further documentation can be found here: + * [Events availability matrix](https://api-docs.home-connect.com/events#availability-matrix) + * [Program changes](https://api-docs.home-connect.com/events#program-changes) + * [Option changes](https://api-docs.home-connect.com/events#option-changes) + * [Program progress changes](https://api-docs.home-connect.com/events#program-progress-changes) + * [Home appliance state changes](https://api-docs.home-connect.com/events#home-appliance-state-changes) + """ response = await self._auth.request( "GET", - f"/api/homeappliances/{ha_id}/status/BSH.Common.Status.OperationState", + f"/homeappliances/{ha_id}/events", + headers={"Accept-Language": accept_language, "Accept": accept}, ) - return response.json() + return ArrayOfEvents.from_dict(response.json()["data"]) diff --git a/src/aiohomeconnect/model/__init__.py b/src/aiohomeconnect/model/__init__.py new file mode 100644 index 0000000..8a76bdd --- /dev/null +++ b/src/aiohomeconnect/model/__init__.py @@ -0,0 +1,88 @@ +"""Provide a model for the Home Connect API.""" + +from enum import StrEnum + +from .appliance import ( + ArrayOfHomeAppliances, + HomeAppliance, +) +from .command import ( + ArrayOfCommands, + CommandKey, + PutCommand, + PutCommands, +) +from .event import ( + ArrayOfEvents, + EventKey, +) +from .image import ( + ArrayOfImages, +) +from .program import ( + ArrayOfAvailablePrograms, + ArrayOfOptions, + ArrayOfPrograms, + Option, + OptionKey, + Program, + ProgramConstraints, + ProgramDefinition, + ProgramKey, +) +from .setting import ( + ArrayOfSettings, + GetSetting, + PutSetting, + PutSettings, + SettingKey, +) +from .status import ( + ArrayOfStatus, + Status, + StatusKey, +) + +__all__ = [ + "ArrayOfAvailablePrograms", + "ArrayOfCommands", + "ArrayOfEvents", + "ArrayOfHomeAppliances", + "ArrayOfImages", + "ArrayOfOptions", + "ArrayOfPrograms", + "ArrayOfSettings", + "ArrayOfStatus", + "CommandKey", + "EventKey", + "GetSetting", + "HomeAppliance", + "Option", + "OptionKey", + "Program", + "ProgramConstraints", + "ProgramDefinition", + "ProgramKey", + "PutCommand", + "PutCommands", + "PutSetting", + "PutSettings", + "SettingKey", + "Status", + "StatusKey", +] + + +class ContentType(StrEnum): + """Represent the content type for the response.""" + + APPLICATION_JSON = "application/vnd.bsh.sdk.v1+json" + EVENT_STREAM = "text/event-stream" + + +class Language(StrEnum): + """Represent the language for the response.""" + + DE = "de-DE" + EN = "en-US" + EN_GB = "en-GB" diff --git a/src/aiohomeconnect/model/appliance.py b/src/aiohomeconnect/model/appliance.py new file mode 100644 index 0000000..fa57f03 --- /dev/null +++ b/src/aiohomeconnect/model/appliance.py @@ -0,0 +1,28 @@ +"""Provide appliance models for the Home Connect API.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from mashumaro import field_options +from mashumaro.mixins.json import DataClassJSONMixin + + +@dataclass +class HomeAppliance(DataClassJSONMixin): + """Represent HomeAppliance.""" + + ha_id: str | None = field(metadata=field_options(alias="haId")) + name: str | None + type: str | None + brand: str | None + vib: str | None + e_number: str | None = field(metadata=field_options(alias="enumber")) + connected: bool | None + + +@dataclass +class ArrayOfHomeAppliances(DataClassJSONMixin): + """Object containing an array of home appliances.""" + + homeappliances: list[HomeAppliance] diff --git a/src/aiohomeconnect/model/command.py b/src/aiohomeconnect/model/command.py new file mode 100644 index 0000000..00e8563 --- /dev/null +++ b/src/aiohomeconnect/model/command.py @@ -0,0 +1,49 @@ +"""Provide command models for the Home Connect API.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum +from typing import Any + +from mashumaro.mixins.json import DataClassJSONMixin + + +@dataclass +class ArrayOfCommands(DataClassJSONMixin): + """Represent ArrayOfCommands.""" + + commands: list[Command] + + +@dataclass +class Command(DataClassJSONMixin): + """Represent Command.""" + + key: CommandKey + name: str | None + + +@dataclass +class PutCommand(DataClassJSONMixin): + """Represent PutCommand.""" + + key: CommandKey + value: Any + + +@dataclass +class PutCommands(DataClassJSONMixin): + """A list of commands of the home appliance.""" + + data: list[PutCommand] + + +class CommandKey(StrEnum): + """Represent a command key.""" + + BSH_COMMON_ACKNOWLEDGE_EVENT = "BSH.Common.Command.AcknowledgeEvent" + BSH_COMMON_OPEN_DOOR = "BSH.Common.Command.OpenDoor" + BSH_COMMON_PARTLY_OPEN_DOOR = "BSH.Common.Command.PartlyOpenDoor" + BSH_COMMON_PAUSE_PROGRAM = "BSH.Common.Command.PauseProgram" + BSH_COMMON_RESUME_PROGRAM = "BSH.Common.Command.ResumeProgram" diff --git a/src/aiohomeconnect/model/error.py b/src/aiohomeconnect/model/error.py new file mode 100644 index 0000000..feb9690 --- /dev/null +++ b/src/aiohomeconnect/model/error.py @@ -0,0 +1,135 @@ +"""Provide error models for the Home Connect API.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from mashumaro.mixins.json import DataClassJSONMixin + + +@dataclass +class UnauthorizedError(DataClassJSONMixin): + """Represent UnauthorizedError.""" + + key: str + description: str | None + + +@dataclass +class ForbiddenError(DataClassJSONMixin): + """Represent ForbiddenError.""" + + key: str + description: str | None + + +@dataclass +class NotFoundError(DataClassJSONMixin): + """Represent NotFoundError.""" + + key: str + description: str | None + + +@dataclass +class NoProgramSelectedError(DataClassJSONMixin): + """Represent NoProgramSelectedError.""" + + key: str + description: str | None + + +@dataclass +class NoProgramActiveError(DataClassJSONMixin): + """Represent NoProgramActiveError.""" + + key: str + description: str | None + + +@dataclass +class NotAcceptableError(DataClassJSONMixin): + """Represent NotAcceptableError.""" + + key: str + description: str | None + + +@dataclass +class RequestTimeoutError(DataClassJSONMixin): + """Represent RequestTimeoutError.""" + + key: str + description: str | None + + +@dataclass +class ConflictError(DataClassJSONMixin): + """Represent ConflictError.""" + + key: str + description: str | None + + +@dataclass +class SelectedProgramNotSetError(DataClassJSONMixin): + """Represent SelectedProgramNotSetError.""" + + key: str + description: str | None + + +@dataclass +class ActiveProgramNotSetError(DataClassJSONMixin): + """Represent ActiveProgramNotSetError.""" + + key: str + description: str | None + + +@dataclass +class WrongOperationStateError(DataClassJSONMixin): + """Represent WrongOperationStateError.""" + + key: str + description: str | None + + +@dataclass +class ProgramNotAvailableError(DataClassJSONMixin): + """Represent ProgramNotAvailableError.""" + + key: str + description: str | None + + +@dataclass +class UnsupportedMediaTypeError(DataClassJSONMixin): + """Represent UnsupportedMediaTypeError.""" + + key: str + description: str | None + + +@dataclass +class TooManyRequestsError(DataClassJSONMixin): + """Represent TooManyRequestsError.""" + + key: str + description: str | None + + +@dataclass +class InternalServerError(DataClassJSONMixin): + """Represent InternalServerError.""" + + key: str + description: str | None + + +@dataclass +class Conflict(DataClassJSONMixin): + """Represent Conflict.""" + + key: str + description: str | None diff --git a/src/aiohomeconnect/model/event.py b/src/aiohomeconnect/model/event.py new file mode 100644 index 0000000..d1e225b --- /dev/null +++ b/src/aiohomeconnect/model/event.py @@ -0,0 +1,44 @@ +"""Provide event models for the Home Connect API.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import StrEnum + +from mashumaro import field_options +from mashumaro.mixins.json import DataClassJSONMixin + + +@dataclass +class ArrayOfEvents(DataClassJSONMixin): + """Represent ArrayOfEvents.""" + + items: list[Event] + + +@dataclass +class Event(DataClassJSONMixin): + """Represent Event.""" + + key: EventKey + name: str | None + uri: str | None + timestamp: int + level: str + handling: str + value: str | float | bool + display_value: str | None = field(metadata=field_options(alias="displayvalue")) + unit: str | None + + +class EventKey(StrEnum): + """Represent an event key.""" + + # TODO(Martin Hjelmare): Add all event keys # noqa: FIX002 + # https://github.com/MartinHjelmare/aiohomeconnect/issues/21 + + BSH_COMMON_ROOT_SELECTED_PROGRAM = "BSH.Common.Root.SelectedProgram" + BSH_COMMON_ROOT_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram" + BSH_COMMON_OPTION_START_IN_RELATIVE = "BSH.Common.Option.StartInRelative" + BSH_COMMON_OPTION_FINISH_IN_RELATIVE = "BSH.Common.Option.FinishInRelative" + BSH_COMMON_OPTION_DURATION = "BSH.Common.Option.Duration" diff --git a/src/aiohomeconnect/model/image.py b/src/aiohomeconnect/model/image.py new file mode 100644 index 0000000..ebfeb6a --- /dev/null +++ b/src/aiohomeconnect/model/image.py @@ -0,0 +1,27 @@ +"""Provide image models for the Home Connect API.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from mashumaro import field_options +from mashumaro.mixins.json import DataClassJSONMixin + + +@dataclass +class ArrayOfImages(DataClassJSONMixin): + """List of images available from the home appliance.""" + + images: list[Image] + + +@dataclass +class Image(DataClassJSONMixin): + """Represent Image.""" + + key: str + name: str | None + image_key: str = field(metadata=field_options(alias="imagekey")) + preview_image_key: str = field(metadata=field_options(alias="previewimagekey")) + timestamp: int + quality: str diff --git a/src/aiohomeconnect/model/program.py b/src/aiohomeconnect/model/program.py new file mode 100644 index 0000000..49ffd04 --- /dev/null +++ b/src/aiohomeconnect/model/program.py @@ -0,0 +1,171 @@ +"""Provide program models for the Home Connect API.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import StrEnum +from typing import Any + +from mashumaro import field_options +from mashumaro.mixins.json import DataClassJSONMixin + + +@dataclass +class Program(DataClassJSONMixin): + """Represent Program.""" + + key: ProgramKey + name: str | None + options: list[Option] | None + constraints: ProgramConstraints | None + + +@dataclass +class ProgramConstraints(DataClassJSONMixin): + """Represent ProgramConstraints.""" + + access: str | None + + +@dataclass +class ArrayOfAvailablePrograms(DataClassJSONMixin): + """Represent ArrayOfAvailablePrograms.""" + + programs: list[EnumerateAvailableProgram] + + +@dataclass +class EnumerateAvailableProgramConstraints(DataClassJSONMixin): + """Represent EnumerateAvailableProgramConstraints.""" + + execution: Execution | None + + +@dataclass +class EnumerateAvailableProgram(DataClassJSONMixin): + """Represent EnumerateAvailableProgram.""" + + key: ProgramKey + name: str | None + constraints: EnumerateAvailableProgramConstraints | None + + +@dataclass +class ArrayOfPrograms(DataClassJSONMixin): + """Represent ArrayOfPrograms.""" + + programs: list[EnumerateProgram] + active: Program | None + selected: Program | None + + +@dataclass +class EnumerateProgramConstraints(DataClassJSONMixin): + """Represent EnumerateProgramConstraints.""" + + available: bool | None + execution: Execution | None + + +@dataclass +class EnumerateProgram(DataClassJSONMixin): + """Represent EnumerateProgram.""" + + key: ProgramKey + name: str | None + constraints: EnumerateProgramConstraints | None + + +class Execution(StrEnum): + """Execution right of the program.""" + + NONE = "none" + SELECT_ONLY = "selectonly" + START_ONLY = "startonly" + SELECT_AND_START = "selectandstart" + + +@dataclass +class ProgramDefinition(DataClassJSONMixin): + """Represent ProgramDefinition.""" + + key: ProgramKey + name: str | None + options: list[ProgramDefinitionOption] | None + + +@dataclass +class ProgramDefinitionConstraints(DataClassJSONMixin): + """Represent ProgramDefinitionConstraints.""" + + min: int | None + max: int | None + step_size: int | None = field(metadata=field_options(alias="stepsize")) + allowed_values: list[str | None] | None = field( + metadata=field_options(alias="allowedvalues") + ) + display_values: list[str | None] | None = field( + metadata=field_options(alias="displayvalues") + ) + default: Any | None + live_update: bool | None = field(metadata=field_options(alias="liveupdate")) + + +@dataclass +class ProgramDefinitionOption(DataClassJSONMixin): + """Represent ProgramDefinitionOption.""" + + key: OptionKey + name: str | None + type: str + unit: str | None + constraints: ProgramDefinitionConstraints | None + + +@dataclass +class Option(DataClassJSONMixin): + """Represent Option.""" + + key: OptionKey + name: str | None + value: Any + display_value: str | None = field(metadata=field_options(alias="displayvalue")) + unit: str | None + + +@dataclass +class ArrayOfOptions(DataClassJSONMixin): + """List of options.""" + + options: list[Option] + + +class OptionKey(StrEnum): + """Represent an option key.""" + + # TODO(Martin Hjelmare): Add all option keys # noqa: FIX002 + # https://github.com/MartinHjelmare/aiohomeconnect/issues/22 + + CONSUMER_PRODUCTS_CLEANING_ROBOT_CLEANING_MODE = ( + "ConsumerProducts.CleaningRobot.Option.CleaningMode" + ) + CONSUMER_PRODUCTS_CLEANING_ROBOT_REFERENCE_MAP_ID = ( + "ConsumerProducts.CleaningRobot.Option.ReferenceMapId" + ) + + +class ProgramKey(StrEnum): + """Represent a program key.""" + + # TODO(Martin Hjelmare): Add all program keys # noqa: FIX002 + # https://github.com/MartinHjelmare/aiohomeconnect/issues/23 + + CONSUMER_PRODUCTS_CLEANING_ROBOT_BASIC_GO_HOME = ( + "ConsumerProducts.CleaningRobot.Program.Basic.GoHome" + ) + CONSUMER_PRODUCTS_CLEANING_ROBOT_CLEANING_CLEAN_ALL = ( + "ConsumerProducts.CleaningRobot.Program.Cleaning.CleanAll" + ) + CONSUMER_PRODUCTS_CLEANING_ROBOT_CLEANING_CLEAN_MAP = ( + "ConsumerProducts.CleaningRobot.Program.Cleaning.CleanMap" + ) diff --git a/src/aiohomeconnect/model/setting.py b/src/aiohomeconnect/model/setting.py new file mode 100644 index 0000000..0e245ba --- /dev/null +++ b/src/aiohomeconnect/model/setting.py @@ -0,0 +1,176 @@ +"""Provide setting models for the Home Connect API.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import StrEnum +from typing import Any + +from mashumaro import field_options +from mashumaro.mixins.json import DataClassJSONMixin + + +@dataclass +class GetSetting(DataClassJSONMixin): + """Specific setting of the home appliance.""" + + key: SettingKey + name: str | None + value: Any + display_value: str | None = field(metadata=field_options(alias="displayvalue")) + unit: str | None + type: str | None + constraints: SettingConstraints | None + + +@dataclass +class SettingConstraints(DataClassJSONMixin): + """Represent SettingConstraints.""" + + min: int | None + max: int | None + step_size: int | None = field(metadata=field_options(alias="stepsize")) + allowed_values: list[str | None] | None = field( + metadata=field_options(alias="allowedvalues") + ) + display_values: list[str | None] | None = field( + metadata=field_options(alias="displayvalues") + ) + default: Any | None + access: str | None + + +@dataclass +class ArrayOfSettings(DataClassJSONMixin): + """List of settings of the home appliance.""" + + settings: list[GetSetting] + + +@dataclass +class PutSetting(DataClassJSONMixin): + """Specific setting of the home appliance.""" + + key: SettingKey + value: Any + + +@dataclass +class PutSettings(DataClassJSONMixin): + """List of settings of the home appliance.""" + + data: list[PutSetting] + + +class SettingKey(StrEnum): + """Represent a setting key.""" + + BSH_COMMON_POWER_STATE = "BSH.Common.Setting.PowerState" + BSH_COMMON_TEMPERATURE_UNIT = "BSH.Common.Setting.TemperatureUnit" + BSH_COMMON_LIQUID_VOLUME_UNIT = "BSH.Common.Setting.LiquidVolumeUnit" + BSH_COMMON_CHILD_LOCK = "BSH.Common.Setting.ChildLock" + BSH_COMMON_ALARM_CLOCK = "BSH.Common.Setting.AlarmClock" + BSH_COMMON_AMBIENT_LIGHT_ENABLED = "BSH.Common.Setting.AmbientLightEnabled" + BSH_COMMON_AMBIENT_LIGHT_BRIGHTNESS = "BSH.Common.Setting.AmbientLightBrightness" + BSH_COMMON_AMBIENT_LIGHT_COLOR = "BSH.Common.Setting.AmbientLightColor" + BSH_COMMON_AMBIENT_LIGHT_CUSTOM_COLOR = "BSH.Common.Setting.AmbientLightCustomColor" + CONSUMER_PRODUCTS_COFFEE_MAKER_CUP_WARMER = ( + "ConsumerProducts.CoffeeMaker.Setting.CupWarmer" + ) + CONSUMER_PRODUCTS_CLEANING_ROBOT_CURRENT_MAP = ( + "ConsumerProducts.CleaningRobot.Setting.CurrentMap" + ) + CONSUMER_PRODUCTS_CLEANING_ROBOT_NAME_OF_MAP_1 = ( + "ConsumerProducts.CleaningRobot.Setting.NameOfMap1" + ) + CONSUMER_PRODUCTS_CLEANING_ROBOT_NAME_OF_MAP_2 = ( + "ConsumerProducts.CleaningRobot.Setting.NameOfMap2" + ) + CONSUMER_PRODUCTS_CLEANING_ROBOT_NAME_OF_MAP_3 = ( + "ConsumerProducts.CleaningRobot.Setting.NameOfMap3" + ) + COOKING_COMMON_LIGHTING = "Cooking.Common.Setting.Lighting" + COOKING_COMMON_LIGHTING_BRIGHTNESS = "Cooking.Common.Setting.LightingBrightness" + COOKING_HOOD_COLOR_TEMPERATURE_PERCENT = ( + "Cooking.Hood.Setting.ColorTemperaturePercent" + ) + COOKING_HOOD_COLOR_TEMPERATURE = "Cooking.Hood.Setting.ColorTemperature" + COOKING_OVEN_SABBATH_MODE = "Cooking.Oven.Setting.SabbathMode" + LAUNDRY_CARE_WASHER_I_DOS_1_BASE_LEVEL = "LaundryCare.Washer.Setting.IDos1BaseLevel" + LAUNDRY_CARE_WASHER_I_DOS_2_BASE_LEVEL = "LaundryCare.Washer.Setting.IDos2BaseLevel" + REFRIGERATION_COMMON_BOTTLE_COOLER_SETPOINT_TEMPERATURE = ( + "Refrigeration.Common.Setting.BottleCooler.SetpointTemperature" + ) + REFRIGERATION_COMMON_CHILLER_LEFT_SETPOINT_TEMPERATURE = ( + "Refrigeration.Common.Setting.ChillerLeft.SetpointTemperature" + ) + REFRIGERATION_COMMON_CHILLER_COMMON_SETPOINT_TEMPERATURE = ( + "Refrigeration.Common.Setting.ChillerCommon.SetpointTemperature" + ) + REFRIGERATION_COMMON_CHILLER_RIGHT_SETPOINT_TEMPERATURE = ( + "Refrigeration.Common.Setting.ChillerRight.SetpointTemperature" + ) + REFRIGERATION_COMMON_DISPENSER_ENABLED = ( + "Refrigeration.Common.Setting.Dispenser.Enabled" + ) + REFRIGERATION_COMMON_DOOR_ASSISTANT_FRIDGE = ( + "Refrigeration.Common.Setting.Door.AssistantFridge" + ) + REFRIGERATION_COMMON_DOOR_ASSISTANT_FREEZER = ( + "Refrigeration.Common.Setting.Door.AssistantFreezer" + ) + REFRIGERATION_COMMON_DOOR_ASSISTANT_FORCE_FRIDGE = ( + "Refrigeration.Common.Setting.Door.AssistantForceFridge" + ) + REFRIGERATION_COMMON_DOOR_ASSISTANT_FORCE_FREEZER = ( + "Refrigeration.Common.Setting.Door.AssistantForceFreezer" + ) + REFRIGERATION_COMMON_DOOR_ASSISTANT_TIMEOUT_FRIDGE = ( + "Refrigeration.Common.Setting.Door.AssistantTimeoutFridge" + ) + REFRIGERATION_COMMON_DOOR_ASSISTANT_TIMEOUT_FREEZER = ( + "Refrigeration.Common.Setting.Door.AssistantTimeoutFreezer" + ) + REFRIGERATION_COMMON_DOOR_ASSISTANT_TRIGGER_FRIDGE = ( + "Refrigeration.Common.Setting.Door.AssistantTriggerFridge" + ) + REFRIGERATION_COMMON_DOOR_ASSISTANT_TRIGGER_FREEZER = ( + "Refrigeration.Common.Setting.Door.AssistantTriggerFreezer" + ) + REFRIGERATION_COMMON_ECO_MODE = "Refrigeration.Common.Setting.EcoMode" + REFRIGERATION_COMMON_FRESH_MODE = "Refrigeration.Common.Setting.FreshMode" + REFRIGERATION_COMMON_LIGHT_EXTERNAL_BRIGHTNESS = ( + "Refrigeration.Common.Setting.Light.External.Brightness" + ) + REFRIGERATION_COMMON_LIGHT_INTERNAL_BRIGHTNESS = ( + "Refrigeration.Common.Setting.Light.Internal.Brightness" + ) + REFRIGERATION_COMMON_LIGHT_EXTERNAL_POWER = ( + "Refrigeration.Common.Setting.Light.External.Power" + ) + REFRIGERATION_COMMON_LIGHT_INTERNAL_POWER = ( + "Refrigeration.Common.Setting.Light.Internal.Power" + ) + REFRIGERATION_COMMON_SABBATH_MODE = "Refrigeration.Common.Setting.SabbathMode" + REFRIGERATION_COMMON_VACATION_MODE = "Refrigeration.Common.Setting.VacationMode" + REFRIGERATION_COMMON_WINE_COMPARTMENT_SETPOINT_TEMPERATURE = ( + "Refrigeration.Common.Setting.WineCompartment.SetpointTemperature" + ) + REFRIGERATION_COMMON_WINE_COMPARTMENT_2_SETPOINT_TEMPERATURE = ( + "Refrigeration.Common.Setting.WineCompartment2.SetpointTemperature" + ) + REFRIGERATION_COMMON_WINE_COMPARTMENT_3_SETPOINT_TEMPERATURE = ( + "Refrigeration.Common.Setting.WineCompartment3.SetpointTemperature" + ) + REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_REFRIGERATOR = ( + "Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator" + ) + REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_FREEZER = ( + "Refrigeration.FridgeFreezer.Setting.SetpointTemperatureFreezer" + ) + REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_REFRIGERATOR = ( + "Refrigeration.FridgeFreezer.Setting.SuperModeRefrigerator" + ) + REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_FREEZER = ( + "Refrigeration.FridgeFreezer.Setting.SuperModeFreezer" + ) diff --git a/src/aiohomeconnect/model/status.py b/src/aiohomeconnect/model/status.py new file mode 100644 index 0000000..4a6a890 --- /dev/null +++ b/src/aiohomeconnect/model/status.py @@ -0,0 +1,129 @@ +"""Provide status models for the Home Connect API.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import StrEnum +from typing import Any + +from mashumaro import field_options +from mashumaro.mixins.json import DataClassJSONMixin + + +@dataclass +class Status(DataClassJSONMixin): + """Represent Status.""" + + key: StatusKey + name: str | None + value: Any + display_value: str | None = field(metadata=field_options(alias="displayvalue")) + unit: str | None + type: str | None + constraints: StatusConstraints | None + + +@dataclass +class StatusConstraints(DataClassJSONMixin): + """Represent StatusConstraints.""" + + min: int | None + max: int | None + step_size: int | None = field(metadata=field_options(alias="stepsize")) + allowed_values: list[str | None] | None = field( + metadata=field_options(alias="allowedvalues") + ) + display_values: list[str | None] | None = field( + metadata=field_options(alias="displayvalues") + ) + default: Any | None + access: str | None + + +@dataclass +class ArrayOfStatus(DataClassJSONMixin): + """List of status of the home appliance.""" + + status: list[Status] + + +class StatusKey(StrEnum): + """Represent a status key.""" + + BSH_COMMON_BATTERY_CHARGING_STATE = "BSH.Common.Status.BatteryChargingState" + BSH_COMMON_BATTERY_LEVEL = "BSH.Common.Status.BatteryLevel" + BSH_COMMON_CHARGING_CONNECTION = "BSH.Common.Status.ChargingConnection" + BSH_COMMON_DOOR_STATE = "BSH.Common.Status.DoorState" + BSH_COMMON_LOCAL_CONTROL_ACTIVE = "BSH.Common.Status.LocalControlActive" + BSH_COMMON_OPERATION_STATE = "BSH.Common.Status.OperationState" + BSH_COMMON_REMOTE_CONTROL_ACTIVE = "BSH.Common.Status.RemoteControlActive" + BSH_COMMON_REMOTE_CONTROL_START_ALLOWED = ( + "BSH.Common.Status.RemoteControlStartAllowed" + ) + BSH_COMMON_VIDEO_CAMERA_STATE = "BSH.Common.Status.Video.CameraState" + REFRIGERATION_COMMON_DOOR_BOTTLE_COOLER = ( + "Refrigeration.Common.Status.Door.BottleCooler" + ) + REFRIGERATION_COMMON_DOOR_CHILLER = "Refrigeration.Common.Status.Door.Chiller" + REFRIGERATION_COMMON_DOOR_CHILLER_COMMON = ( + "Refrigeration.Common.Status.Door.ChillerCommon" + ) + REFRIGERATION_COMMON_DOOR_CHILLER_LEFT = ( + "Refrigeration.Common.Status.Door.ChillerLeft" + ) + REFRIGERATION_COMMON_DOOR_CHILLER_RIGHT = ( + "Refrigeration.Common.Status.Door.ChillerRight" + ) + REFRIGERATION_COMMON_DOOR_FLEX_COMPARTMENT = ( + "Refrigeration.Common.Status.Door.FlexCompartment" + ) + REFRIGERATION_COMMON_DOOR_FREEZER = "Refrigeration.Common.Status.Door.Freezer" + REFRIGERATION_COMMON_DOOR_REFRIGERATOR = ( + "Refrigeration.Common.Status.Door.Refrigerator" + ) + REFRIGERATION_COMMON_DOOR_REFRIGERATOR_2 = ( + "Refrigeration.Common.Status.Door.Refrigerator2" + ) + REFRIGERATION_COMMON_DOOR_REFRIGERATOR_3 = ( + "Refrigeration.Common.Status.Door.Refrigerator3" + ) + REFRIGERATION_COMMON_DOOR_WINE_COMPARTMENT = ( + "Refrigeration.Common.Status.Door.WineCompartment" + ) + CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_COFFEE = ( + "ConsumerProducts.CoffeeMaker.Status.BeverageCounterCoffee" + ) + CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_COFFEE_AND_MILK = ( + "ConsumerProducts.CoffeeMaker.Status.BeverageCounterCoffeeAndMilk" + ) + CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_FROTHY_MILK = ( + "ConsumerProducts.CoffeeMaker.Status.BeverageCounterFrothyMilk" + ) + CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_HOT_MILK = ( + "ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotMilk" + ) + CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_HOT_WATER = ( + "ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotWater" + ) + CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_HOT_WATER_CUPS = ( + "ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotWaterCups" + ) + CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_MILK = ( + "ConsumerProducts.CoffeeMaker.Status.BeverageCounterMilk" + ) + CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_POWDER_COFFEE = ( + "ConsumerProducts.CoffeeMaker.Status.BeverageCounterPowderCoffee" + ) + CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_RISTRETTO_ESPRESSO = ( + "ConsumerProducts.CoffeeMaker.Status.BeverageCounterRistrettoEspresso" + ) + CONSUMER_PRODUCTS_CLEANING_ROBOT_DUST_BOX_INSERTED = ( + "ConsumerProducts.CleaningRobot.Status.DustBoxInserted" + ) + CONSUMER_PRODUCTS_CLEANING_ROBOT_LAST_SELECTED_MAP = ( + "ConsumerProducts.CleaningRobot.Status.LastSelectedMap" + ) + CONSUMER_PRODUCTS_CLEANING_ROBOT_LIFTED = ( + "ConsumerProducts.CleaningRobot.Status.Lifted" + ) + CONSUMER_PRODUCTS_CLEANING_ROBOT_LOST = "ConsumerProducts.CleaningRobot.Status.Lost" diff --git a/tests/test_client.py b/tests/test_client.py index 8d4b22f..20f4f6e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -20,7 +20,7 @@ async def test_abstract_auth(httpx_client: AsyncClient, httpx_mock: HTTPXMock) - """Test the abstract auth.""" url_query = "key1=value1&key2=value2" httpx_mock.add_response( - url=f"https://example.com/test?{url_query}", + url=f"https://example.com/api/test?{url_query}", json={"test": "test_result"}, )