[
  {
    "path": ".github/workflows/combined.yaml",
    "content": "name: \"Validation And Formatting\"\non:\n  push:\n  pull_request:\njobs:\n  ci:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n        name: Download repo\n        with:\n          fetch-depth: 0\n      - uses: actions/setup-python@v2\n        name: Setup Python\n      - uses: actions/cache@v2\n        name: Cache\n        with:\n          path: |\n            ~/.cache/pip\n          key: custom-component-ci\n      - uses: hacs/action@main\n        with:\n          CATEGORY: integration\n      - uses: KTibow/ha-blueprint@stable\n        name: CI\n        with:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "**/*.pyc\n.idea/\n*.iml"
  },
  {
    "path": "README.md",
    "content": "<a href=\"https://www.buymeacoffee.com/robink\" target=\"_blank\"><img src=\"https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png\" alt=\"Buy Me A Coffee\" style=\"height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;\" ></a>\n\n<a href=\"https://paypal.me/robinkolk\"><img src=\"https://www.paypalobjects.com/en_US/NL/i/btn/btn_donateCC_LG.gif\" title=\"PayPal - The safer, easier way to pay online!\" alt=\"Donate with PayPal button\"></a>\n\n# Home Assistant support for Miele@home connected appliances\n\n## Introduction\n\nThis project exposes Miele state information of appliances connected to a Miele user account. This is achieved by communicating with the Miele Cloud Service, which exposes both applicances connected to a Miele@home Gateway XGW3000, as well as those devices connected via WiFi Con@ct.\n\n## Prerequisite\n\n* A running version of [Home Assistant](https://home-assistant.io). While earlier versions may work, the custom component has been developed and tested with version 0.76.x.\n\n* Following the [instructions on the Miele developer site](https://www.miele.com/f/com/en/register_api.aspx), you need to request your personal ```ClientID``` and ```ClientSecret```.\n\n## HACS Install\nWe are now included in the default Repo of HACS. This is the recomanded way to install this integration. \n\n* Install HACS if you haven't yet, instructions to install HACS can be found here : https://hacs.xyz/docs/installation/prerequisites\n\n* Open the HACS component from your sidebar -> click integrations -> Search for Miele and install the Integration.\n\n* Enable the new platform in your ```configuration.yaml```:\n\n```\nmiele:\n    client_id: <your Miele ClientID>\n    client_secret: <your Miele ClientSecret>\n    lang: <optional. en=english, de=german>\n    cache_path: <optional. where to store the cached access token>\n```\n\n* Restart Home Assistant.\n* The Home Assistant Web UI will show you a UI to configure the Miele platform. Follow the instructions to log into the Miele Cloud Service. This will communicate back an authentication token that will be cached to communicate with the Cloud Service.\n\nDone. If you follow all the instructions, the Miele integration should be up and running. All Miele devices that you can see in your Mobile application should now be also visible in Home Assistant (miele.*). In addition, there will be a number of ```binary_sensors``` and ```sensors``` that can be used for automation.\n\n## Manual Installation of the custom component\n\n* Copy the content of this repository into your ```custom_components``` folder, which is a subdirectory of your Home Assistant configuration directory. By default, this directory is located under ```~/.home-assistant```. The structure of the ```custom_components``` directory should look like this afterwards:\n\n```\n- miele\n    - __init__.py\n    - miele_at_home.py\n    - binary_sensor.py\n    - light.py\n    - sensor.py\n```\n\n* Enable the new platform in your ```configuration.yaml```:\n\n```\nmiele:\n    client_id: <your Miele ClientID>\n    client_secret: <your Miele ClientSecret>\n    lang: <optional. en=english, de=german>\n    cache_path: <optional. where to store the cached access token>\n    interval: <optional. the interval between miele polling updates>\n```\n\n* Restart Home Assistant.\n* The Home Assistant Web UI will show you a UI to configure the Miele platform. Follow the instructions to log into the Miele Cloud Service. This will communicate back an authentication token that will be cached to communicate with the Cloud Service.\n\nDone. If you follow all the instructions, the Miele integration should be up and running. All Miele devices that you can see in your Mobile application should now be also visible in Home Assistant (miele.*). In addition, there will be a number of ```binary_sensors``` and ```sensors``` that can be used for automation.\n\n## Questions\n\nPlease see the [Miele@home, miele@mobile component](https://community.home-assistant.io/t/miele-home-miele-mobile-component/64508) discussion thread on the Home Assistant community site.\n"
  },
  {
    "path": "custom_components/miele/__init__.py",
    "content": "\"\"\"\nSupport for Miele.\n\"\"\"\nimport asyncio\nimport functools\nimport logging\nfrom datetime import timedelta\nfrom importlib import import_module\n\nimport homeassistant.helpers.config_validation as cv\nimport voluptuous as vol\nfrom aiohttp import web\nfrom homeassistant.components.http import HomeAssistantView\nfrom homeassistant.core import callback\nfrom homeassistant.helpers import network\nfrom homeassistant.helpers.discovery import load_platform\nfrom homeassistant.helpers.entity import Entity\nfrom homeassistant.helpers.entity_component import EntityComponent\nfrom homeassistant.helpers.event import async_track_time_interval\nfrom homeassistant.helpers.network import get_url\nfrom homeassistant.helpers.storage import STORAGE_DIR\n\nfrom .miele_at_home import MieleClient, MieleOAuth\n\n_LOGGER = logging.getLogger(__name__)\n\nDEVICES = []\n\nDEFAULT_NAME = \"Miele@home\"\nDOMAIN = \"miele\"\n\n_CONFIGURING = {}\n\nDATA_OAUTH = \"oauth\"\nDATA_DEVICES = \"devices\"\nDATA_CLIENT = \"client\"\nSERVICE_ACTION = \"action\"\nSERVICE_START_PROGRAM = \"start_program\"\nSERVICE_STOP_PROGRAM = \"stop_program\"\nSCOPE = \"code\"\nDEFAULT_LANG = \"en\"\nDEFAULT_INTERVAL = 5\nAUTH_CALLBACK_PATH = \"/api/miele/callback\"\nAUTH_CALLBACK_NAME = \"api:miele:callback\"\nCONF_CLIENT_ID = \"client_id\"\nCONF_CLIENT_SECRET = \"client_secret\"\nCONF_LANG = \"lang\"\nCONF_CACHE_PATH = \"cache_path\"\nCONF_INTERVAL = \"interval\"\nCONFIGURATOR_LINK_NAME = \"Link Miele account\"\nCONFIGURATOR_SUBMIT_CAPTION = \"I have authorized Miele@home.\"\nCONFIGURATOR_DESCRIPTION = (\n    \"To link your Miele account, \" \"click the link, login, and authorize:\"\n)\nCONFIGURATOR_DESCRIPTION_IMAGE = (\n    \"https://api.mcs3.miele.com/assets/images/miele_logo.svg\"\n)\n\nMIELE_COMPONENTS = [\"binary_sensor\", \"light\", \"sensor\", \"fan\"]\n\nCONFIG_SCHEMA = vol.Schema(\n    {\n        DOMAIN: vol.Schema(\n            {\n                vol.Required(CONF_CLIENT_ID): cv.string,\n                vol.Required(CONF_CLIENT_SECRET): cv.string,\n                vol.Optional(CONF_LANG): cv.string,\n                vol.Optional(CONF_CACHE_PATH): cv.string,\n                vol.Optional(CONF_INTERVAL): cv.positive_int,\n            }\n        ),\n    },\n    extra=vol.ALLOW_EXTRA,\n)\n\nCAPABILITIES = {\n    \"1\": [\n        \"ProgramID\",\n        \"status\",\n        \"programType\",\n        \"programPhase\",\n        \"remainingTime\",\n        \"startTime\",\n        \"targetTemperature.0\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n        \"elapsedTime\",\n        \"spinningSpeed\",\n        \"ecoFeedback.energyConsumption\",\n        \"ecoFeedback.waterConsumption\",\n    ],\n    \"2\": [\n        \"ProgramID\",\n        \"status\",\n        \"programType\",\n        \"programPhase\",\n        \"remainingTime\",\n        \"startTime\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n        \"elapsedTime\",\n        \"dryingStep\",\n        \"ecoFeedback.energyConsumption\",\n    ],\n    \"7\": [\n        \"ProgramID\",\n        \"status\",\n        \"programType\",\n        \"programPhase\",\n        \"remainingTime\",\n        \"startTime\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"remoteEnable\",\n        \"elapsedTime\",\n        \"ecoFeedback.energyConsumption\",\n        \"ecoFeedback.waterConsumption\",\n    ],\n    \"12\": [\n        \"ProgramID\",\n        \"status\",\n        \"programType\",\n        \"programPhase\",\n        \"remainingTime\",\n        \"startTime\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n        \"elapsedTime\",\n    ],\n    \"13\": [\n        \"ProgramID\",\n        \"status\",\n        \"programType\",\n        \"programPhase\",\n        \"remainingTime\",\n        \"startTime\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n        \"elapsedTime\",\n    ],\n    \"14\": [\"status\", \"signalFailure\", \"plateStep\"],\n    \"15\": [\n        \"ProgramID\",\n        \"status\",\n        \"programType\",\n        \"programPhase\",\n        \"remainingTime\",\n        \"startTime\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n        \"elapsedTime\",\n    ],\n    \"16\": [\n        \"ProgramID\",\n        \"status\",\n        \"programType\",\n        \"programPhase\",\n        \"remainingTime\",\n        \"startTime\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n        \"elapsedTime\",\n    ],\n    \"17\": [\n        \"ProgramID\",\n        \"status\",\n        \"programPhase\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"remoteEnable\",\n    ],\n    \"18\": [\n        \"status\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"remoteEnable\",\n        \"ventilationStep\",\n    ],\n    \"19\": [\n        \"status\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n    ],\n    \"20\": [\n        \"status\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n    ],\n    \"21\": [\n        \"status\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n    ],\n    \"23\": [\n        \"ProgramID\",\n        \"status\",\n        \"programType\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"remoteEnable\",\n        \"batteryLevel\",\n    ],\n    \"24\": [\n        \"ProgramID\",\n        \"status\",\n        \"programType\",\n        \"programPhase\",\n        \"remainingTime\",\n        \"targetTemperature.0\",\n        \"startTime\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n        \"elapsedTime\",\n        \"spinningSpeed\",\n        \"dryingStep\",\n        \"ecoFeedback.energyConsumption\",\n        \"ecoFeedback.waterConsumption\",\n    ],\n    \"25\": [\n        \"status\",\n        \"startTime\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"elapsedTime\",\n    ],\n    \"27\": [\"status\", \"signalFailure\", \"plateStep\"],\n    \"31\": [\n        \"ProgramID\",\n        \"status\",\n        \"programType\",\n        \"programPhase\",\n        \"remainingTime\",\n        \"startTime\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n        \"elapsedTime\",\n    ],\n    \"32\": [\n        \"status\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n    ],\n    \"33\": [\n        \"status\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n    ],\n    \"34\": [\n        \"status\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n    ],\n    \"45\": [\n        \"ProgramID\",\n        \"status\",\n        \"programType\",\n        \"programPhase\",\n        \"remainingTime\",\n        \"startTime\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n        \"elapsedTime\",\n    ],\n    \"67\": [\n        \"ProgramID\",\n        \"status\",\n        \"programType\",\n        \"programPhase\",\n        \"remainingTime\",\n        \"startTime\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"signalDoor\",\n        \"remoteEnable\",\n        \"elapsedTime\",\n    ],\n    \"68\": [\n        \"status\",\n        \"targetTemperature\",\n        \"temperature\",\n        \"signalInfo\",\n        \"signalFailure\",\n        \"remoteEnable\",\n    ],\n}\n\n\ndef request_configuration(hass, config, oauth):\n    \"\"\"Request Miele authorization.\"\"\"\n\n    async def miele_configuration_callback(callback_data):\n        if not hass.data[DOMAIN][DATA_OAUTH].authorized:\n            configurator.async_notify_errors(\n                _CONFIGURING[DOMAIN], \"Failed to register, please try again.\"\n            )\n            return\n\n        if DOMAIN in _CONFIGURING:\n            req_config = _CONFIGURING.pop(DOMAIN)\n            hass.components.configurator.async_request_done(req_config)\n\n        await async_setup(hass, config)\n\n    _LOGGER.info(\"Requesting authorization...\")\n    configurator = hass.components.configurator\n    _CONFIGURING[DOMAIN] = configurator.async_request_config(\n        DEFAULT_NAME,\n        miele_configuration_callback,\n        link_name=CONFIGURATOR_LINK_NAME,\n        link_url=oauth.authorization_url,\n        description=CONFIGURATOR_DESCRIPTION,\n        description_image=CONFIGURATOR_DESCRIPTION_IMAGE,\n        submit_caption=CONFIGURATOR_SUBMIT_CAPTION,\n    )\n    return\n\n\ndef create_sensor(client, hass, home_device, lang):\n    return MieleDevice(hass, client, home_device, lang)\n\n\ndef _to_dict(items):\n    # Replace with map()\n    result = {}\n    for item in items:\n        ident = item[\"ident\"]\n        result[ident[\"deviceIdentLabel\"][\"fabNumber\"]] = item\n\n    return result\n\n\nasync def async_setup(hass, config):\n    \"\"\"Set up the Miele platform.\"\"\"\n\n    if DOMAIN not in hass.data:\n        hass.data[DOMAIN] = {}\n\n    if DATA_OAUTH not in hass.data[DOMAIN]:\n        callback_url = \"{}{}\".format(\n            network.get_url(hass, allow_external=True, prefer_external=True),\n            AUTH_CALLBACK_PATH,\n        )\n        cache = config[DOMAIN].get(\n            CONF_CACHE_PATH, hass.config.path(STORAGE_DIR, f\".miele-token-cache\")\n        )\n        hass.data[DOMAIN][DATA_OAUTH] = MieleOAuth(\n            hass,\n            config[DOMAIN].get(CONF_CLIENT_ID),\n            config[DOMAIN].get(CONF_CLIENT_SECRET),\n            redirect_uri=callback_url,\n            cache_path=cache,\n        )\n\n    if not hass.data[DOMAIN][DATA_OAUTH].authorized:\n        _LOGGER.info(\"no token; requesting authorization\")\n        hass.http.register_view(\n            MieleAuthCallbackView(config, hass.data[DOMAIN][DATA_OAUTH])\n        )\n        request_configuration(hass, config, hass.data[DOMAIN][DATA_OAUTH])\n        return True\n\n    lang = config[DOMAIN].get(CONF_LANG, DEFAULT_LANG)\n\n    component = EntityComponent(_LOGGER, DOMAIN, hass)\n\n    client = MieleClient(hass, hass.data[DOMAIN][DATA_OAUTH])\n    hass.data[DOMAIN][DATA_CLIENT] = client\n    data_get_devices = await client.get_devices(lang)\n    hass.data[DOMAIN][DATA_DEVICES] = _to_dict(data_get_devices)\n\n    DEVICES.extend(\n        [\n            create_sensor(client, hass, home_device, lang)\n            for k, home_device in hass.data[DOMAIN][DATA_DEVICES].items()\n        ]\n    )\n    await component.async_add_entities(DEVICES, False)\n\n    for component in MIELE_COMPONENTS:\n        load_platform(hass, component, DOMAIN, {}, config)\n\n    async def refresh_devices(event_time):\n        _LOGGER.debug(\"Attempting to update Miele devices\")\n        try:\n            device_state = await client.get_devices(lang)\n        except:\n            device_state = None\n        if device_state is None:\n            _LOGGER.error(\"Did not receive Miele devices\")\n        else:\n            hass.data[DOMAIN][DATA_DEVICES] = _to_dict(device_state)\n            for device in DEVICES:\n                device.async_schedule_update_ha_state(True)\n\n            for component in MIELE_COMPONENTS:\n                platform = import_module(\".{}\".format(component), __name__)\n                platform.update_device_state()\n\n    register_services(hass)\n    interval = timedelta(seconds=config[DOMAIN].get(CONF_INTERVAL, DEFAULT_INTERVAL))\n\n    async_track_time_interval(hass, refresh_devices, interval)\n\n    return True\n\n\ndef register_services(hass):\n    \"\"\"Register all services for Miele devices.\"\"\"\n    hass.services.async_register(DOMAIN, SERVICE_ACTION, _action_service)\n    hass.services.async_register(DOMAIN, SERVICE_START_PROGRAM, _action_start_program)\n    hass.services.async_register(DOMAIN, SERVICE_STOP_PROGRAM, _action_stop_program)\n\n\nasync def _apply_service(service, service_func, *service_func_args):\n    entity_ids = service.data.get(\"entity_id\")\n\n    _devices = []\n    if entity_ids:\n        _devices.extend(\n            [device for device in DEVICES if device.entity_id in entity_ids]\n        )\n\n    device_ids = service.data.get(\"device_id\")\n    if device_ids:\n        _devices.extend(\n            [device for device in DEVICES if device.unique_id in device_ids]\n        )\n\n    for device in _devices:\n        await service_func(device, *service_func_args)\n\n\nasync def _action_service(service):\n    body = service.data.get(\"body\")\n    await _apply_service(service, MieleDevice.action, body)\n\n\nasync def _action_start_program(service):\n    program_id = service.data.get(\"program_id\")\n    await _apply_service(service, MieleDevice.start_program, program_id)\n\n\nasync def _action_stop_program(service):\n    body = {\"processAction\": 2}\n    await _apply_service(service, MieleDevice.action, body)\n\n\nclass MieleAuthCallbackView(HomeAssistantView):\n    \"\"\"Miele Authorization Callback View.\"\"\"\n\n    requires_auth = False\n    url = AUTH_CALLBACK_PATH\n    name = AUTH_CALLBACK_NAME\n\n    def __init__(self, config, oauth):\n        \"\"\"Initialize.\"\"\"\n        self.config = config\n        self.oauth = oauth\n\n    @callback\n    async def get(self, request):\n        \"\"\"Receive authorization token.\"\"\"\n        hass = request.app[\"hass\"]\n\n        from oauthlib.oauth2.rfc6749.errors import (\n            MismatchingStateError,\n            MissingTokenError,\n        )\n\n        response_message = \"\"\"Miele@home has been successfully authorized!\n        You can close this window now!\"\"\"\n\n        result = None\n        if request.query.get(\"code\") is not None:\n            try:\n                func = functools.partial(\n                    self.oauth.get_access_token, request.query[\"code\"]\n                )\n\n                result = await hass.async_add_executor_job(func)\n            except MissingTokenError as error:\n                _LOGGER.error(\"Missing token: %s\", error)\n                response_message = \"\"\"Something went wrong when\n                attempting authenticating with Miele@home. The error\n                encountered was {}. Please try again!\"\"\".format(\n                    error\n                )\n            except MismatchingStateError as error:\n                _LOGGER.error(\"Mismatched state, CSRF error: %s\", error)\n                response_message = \"\"\"Something went wrong when\n                attempting authenticating with Miele@home. The error\n                encountered was {}. Please try again!\"\"\".format(\n                    error\n                )\n        else:\n            _LOGGER.error(\"Unknown error when authorizing\")\n            response_message = \"\"\"Something went wrong when\n                attempting authenticating with Miele@home.\n                An unknown error occurred. Please try again!\n                \"\"\"\n\n        html_response = \"\"\"<html><head><title>Miele@home Auth</title></head>\n        <body><h1>{}</h1></body></html>\"\"\".format(\n            response_message\n        )\n\n        response = web.Response(\n            body=html_response, content_type=\"text/html\", status=200, headers=None\n        )\n        response.enable_compression()\n\n        return response\n\n\nclass MieleDevice(Entity):\n    def __init__(self, hass, client, home_device, lang):\n        self._hass = hass\n        self._client = client\n        self._home_device = home_device\n        self._lang = lang\n\n    @property\n    def unique_id(self):\n        \"\"\"Return the unique ID for this sensor.\"\"\"\n        return self._home_device[\"ident\"][\"deviceIdentLabel\"][\"fabNumber\"]\n\n    @property\n    def name(self):\n        \"\"\"Return the name of the sensor.\"\"\"\n\n        ident = self._home_device[\"ident\"]\n\n        result = ident[\"deviceName\"]\n        if len(result) == 0:\n            result = ident[\"type\"][\"value_localized\"]\n\n        return result\n\n    @property\n    def state(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n\n        result = self._home_device[\"state\"][\"status\"][\"value_localized\"]\n        if result == None:\n            result = self._home_device[\"state\"][\"status\"][\"value_raw\"]\n\n        return result\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Attributes.\"\"\"\n\n        result = {}\n        result[\"state_raw\"] = self._home_device[\"state\"][\"status\"][\"value_raw\"]\n\n        result[\"model\"] = self._home_device[\"ident\"][\"deviceIdentLabel\"][\"techType\"]\n        result[\"device_type\"] = self._home_device[\"ident\"][\"type\"][\"value_localized\"]\n        result[\"fabrication_number\"] = self._home_device[\"ident\"][\"deviceIdentLabel\"][\n            \"fabNumber\"\n        ]\n\n        result[\"gateway_type\"] = self._home_device[\"ident\"][\"xkmIdentLabel\"][\"techType\"]\n        result[\"gateway_version\"] = self._home_device[\"ident\"][\"xkmIdentLabel\"][\n            \"releaseVersion\"\n        ]\n\n        return result\n\n    async def action(self, action):\n        await self._client.action(self.unique_id, action)\n\n    async def start_program(self, program_id):\n        await self._client.start_program(self.unique_id, program_id)\n\n    async def async_update(self):\n        if not self.unique_id in self._hass.data[DOMAIN][DATA_DEVICES]:\n            _LOGGER.debug(\"Miele device not found: {}\".format(self.unique_id))\n        else:\n            self._home_device = self._hass.data[DOMAIN][DATA_DEVICES][self.unique_id]\n"
  },
  {
    "path": "custom_components/miele/binary_sensor.py",
    "content": "import logging\nfrom datetime import timedelta\n\nfrom homeassistant.components.binary_sensor import BinarySensorEntity\nfrom homeassistant.helpers.entity import Entity\n\nfrom custom_components.miele import CAPABILITIES, DATA_DEVICES\nfrom custom_components.miele import DOMAIN as MIELE_DOMAIN\n\nPLATFORMS = [\"miele\"]\n\n_LOGGER = logging.getLogger(__name__)\n\nALL_DEVICES = []\n\n\ndef state_capability(type, state):\n    type_str = str(type)\n    if state in CAPABILITIES[type_str]:\n        return True\n\n\ndef _map_key(key):\n    if key == \"signalInfo\":\n        return \"Info\"\n    elif key == \"signalFailure\":\n        return \"Failure\"\n    elif key == \"signalDoor\":\n        return \"Door\"\n    elif key == \"mobileStart\":\n        return \"MobileStart\"\n\n\n# pylint: disable=W0612\ndef setup_platform(hass, config, add_devices, discovery_info=None):\n    global ALL_DEVICES\n\n    devices = hass.data[MIELE_DOMAIN][DATA_DEVICES]\n    for k, device in devices.items():\n        device_state = device[\"state\"]\n        device_type = device[\"ident\"][\"type\"][\"value_raw\"]\n\n        binary_devices = []\n        if \"signalInfo\" in device_state and state_capability(\n            type=device_type, state=\"signalInfo\"\n        ):\n            binary_devices.append(MieleBinarySensor(hass, device, \"signalInfo\"))\n        if \"signalFailure\" in device_state and state_capability(\n            type=device_type, state=\"signalFailure\"\n        ):\n            binary_devices.append(MieleBinarySensor(hass, device, \"signalFailure\"))\n        if \"signalDoor\" in device_state and state_capability(\n            type=device_type, state=\"signalDoor\"\n        ):\n            binary_devices.append(MieleBinarySensor(hass, device, \"signalDoor\"))\n        if \"remoteEnable\" in device_state and state_capability(\n            type=device_type, state=\"remoteEnable\"\n        ):\n            remote_state = device_state[\"remoteEnable\"]\n            if \"mobileStart\" in remote_state:\n                binary_devices.append(\n                    MieleBinarySensor(hass, device, \"remoteEnable.mobileStart\")\n                )\n\n        add_devices(binary_devices)\n        ALL_DEVICES = ALL_DEVICES + binary_devices\n\n\ndef update_device_state():\n    for device in ALL_DEVICES:\n        try:\n            device.async_schedule_update_ha_state(True)\n        except (AssertionError, AttributeError):\n            _LOGGER.debug(\n                \"Component most likely is disabled manually, if not please report to developer\"\n                \"{}\".format(device.entity_id)\n            )\n\n\nclass MieleBinarySensor(BinarySensorEntity):\n    def __init__(self, hass, device, key):\n        self._hass = hass\n        self._device = device\n        self._keys = key.split(\".\")\n        self._key = self._keys[-1]\n        self._ha_key = _map_key(self._key)\n\n    @property\n    def device_id(self):\n        \"\"\"Return the unique ID for this sensor.\"\"\"\n        return self._device[\"ident\"][\"deviceIdentLabel\"][\"fabNumber\"]\n\n    @property\n    def unique_id(self):\n        \"\"\"Return the unique ID for this sensor.\"\"\"\n        return self.device_id + \"_\" + self._ha_key\n\n    @property\n    def name(self):\n        \"\"\"Return the name of the sensor.\"\"\"\n        ident = self._device[\"ident\"]\n\n        result = ident[\"deviceName\"]\n        if len(result) == 0:\n            return ident[\"type\"][\"value_localized\"] + \" \" + self._ha_key\n        else:\n            return result + \" \" + self._ha_key\n\n    @property\n    def is_on(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        current_val = self._device[\"state\"]\n        for k in self._keys:\n            current_val = current_val[k]\n        return bool(current_val)\n\n    @property\n    def device_class(self):\n        if self._key == \"signalDoor\":\n            return \"door\"\n        elif self._key == \"mobileStart\":\n            return \"running\"\n        else:\n            return \"problem\"\n\n    async def async_update(self):\n        if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]:\n            _LOGGER.debug(\"Miele device not found: {}\".format(self.device_id))\n        else:\n            self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id]\n"
  },
  {
    "path": "custom_components/miele/fan.py",
    "content": "import logging\nimport math\nfrom datetime import timedelta\nfrom typing import Optional\n\nfrom homeassistant.components.fan import FanEntityFeature, FanEntity\nfrom homeassistant.helpers.entity import Entity\nfrom homeassistant.util.percentage import (\n    int_states_in_range,\n    percentage_to_ranged_value,\n    ranged_value_to_percentage,\n)\n\nfrom custom_components.miele import DATA_CLIENT, DATA_DEVICES\nfrom custom_components.miele import DOMAIN as MIELE_DOMAIN\n\nPLATFORMS = [\"miele\"]\n\n_LOGGER = logging.getLogger(__name__)\n\nALL_DEVICES = []\n\nSUPPORTED_TYPES = [18]\n\n\nSPEED_RANGE = (1, 4)\n\n\n# pylint: disable=W0612\ndef setup_platform(hass, config, add_devices, discovery_info=None):\n    global ALL_DEVICES\n\n    devices = hass.data[MIELE_DOMAIN][DATA_DEVICES]\n    for k, device in devices.items():\n        device_type = device[\"ident\"][\"type\"]\n\n        fan_devices = []\n        if device_type[\"value_raw\"] in SUPPORTED_TYPES:\n            fan_devices.append(MieleFan(hass, device))\n\n        add_devices(fan_devices)\n        ALL_DEVICES = ALL_DEVICES + fan_devices\n\n\ndef update_device_state():\n    for device in ALL_DEVICES:\n        try:\n            device.async_schedule_update_ha_state(True)\n        except (AssertionError, AttributeError):\n            _LOGGER.debug(\n                \"Component most likely is disabled manually, if not please report to developer\"\n                \"{}\".format(device.entity_id)\n            )\n\n\nclass MieleFan(FanEntity):\n    def __init__(self, hass, device):\n        self._hass = hass\n        self._device = device\n        self._ha_key = \"fan\"\n        self._current_speed = 0\n\n    @property\n    def device_id(self):\n        \"\"\"Return the unique ID for this fan.\"\"\"\n        return self._device[\"ident\"][\"deviceIdentLabel\"][\"fabNumber\"]\n\n    @property\n    def unique_id(self):\n        \"\"\"Return the unique ID for this fan.\"\"\"\n        return self.device_id\n\n    @property\n    def name(self):\n        \"\"\"Return the name of the fan.\"\"\"\n        ident = self._device[\"ident\"]\n\n        result = ident[\"deviceName\"]\n        if len(result) == 0:\n            return ident[\"type\"][\"value_localized\"]\n        else:\n            return result\n\n    @property\n    def is_on(self):\n        \"\"\"Return the state of the fan.\"\"\"\n        value_raw = self._device[\"state\"][\"ventilationStep\"][\"value_raw\"]\n        return value_raw != None and value_raw != 0\n\n    @property\n    def supported_features(self):\n        \"\"\"Flag supported features.\"\"\"\n        return FanEntityFeature.SET_SPEED\n\n    @property\n    def speed(self):\n        \"\"\"Return the current speed\"\"\"\n        return self._device[\"state\"][\"ventilationStep\"][\"value_raw\"]\n\n    @property\n    def percentage(self) -> Optional[int]:\n        \"\"\"Return the current speed percentage.\"\"\"\n        return ranged_value_to_percentage(SPEED_RANGE, self._current_speed)\n\n    @property\n    def speed_count(self) -> int:\n        \"\"\"Return the number of speeds the fan supports.\"\"\"\n        return int_states_in_range(SPEED_RANGE)\n\n    def turn_on(self, percentage: Optional[int] = None, **kwargs) -> None:\n        \"\"\"Turn on the fan.\"\"\"\n        value_in_range = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))\n        if percentage == \"0\":\n            self.turn_off()\n        client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]\n        client.action(device_id=self.device_id, body={\"powerOn\": True})\n\n    async def async_turn_on(self, percentage: Optional[int] = None, **kwargs):\n        \"\"\"Turn on the fan.\"\"\"\n        if percentage == \"0\":\n            await self.async_turn_off()\n        elif percentage is not None:\n            await self.async_set_percentage(percentage=percentage)\n            client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]\n            await client.action(device_id=self.device_id, body={\"powerOn\": True})\n        else:\n            _LOGGER.debug(\"Turning on\")\n            client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]\n            await client.action(device_id=self.device_id, body={\"powerOn\": True})\n\n    def turn_off(self, **kwargs):\n        _LOGGER.debug(\"Turning off\")\n        client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]\n        client.action(device_id=self.device_id, body={\"powerOff\": True})\n\n    async def async_turn_off(self, **kwargs):\n        client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]\n        await client.action(device_id=self.device_id, body={\"powerOff\": True})\n\n    def set_percentage(self, percentage: int) -> None:\n        \"\"\"Set the speed percentage of the fan.\"\"\"  # TODO:\n        value_in_range = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))\n        self._current_speed = value_in_range\n        _LOGGER.debug(\"Setting speed to : {}\".format(value_in_range))\n        client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]\n        client.action(\n            device_id=self.device_id, body={\"ventilationStep\": value_in_range}\n        )\n\n    async def async_set_percentage(self, percentage: int) -> None:\n        \"\"\"Set the speed percentage of the fan.\"\"\"  #\n        value_in_range = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))\n        self._current_speed = value_in_range\n        _LOGGER.debug(\"Setting speed to : {}\".format(value_in_range))\n        client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]\n        await client.action(\n            device_id=self.device_id, body={\"ventilationStep\": value_in_range}\n        )\n\n    async def async_update(self):\n        if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]:\n            _LOGGER.debug(\"Miele device not found: {}\".format(self.device_id))\n        else:\n            self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id]\n"
  },
  {
    "path": "custom_components/miele/light.py",
    "content": "import logging\nfrom datetime import timedelta\n\nfrom homeassistant.components.light import LightEntity\nfrom homeassistant.helpers.entity import Entity\n\nfrom custom_components.miele import DATA_CLIENT, DATA_DEVICES\nfrom custom_components.miele import DOMAIN as MIELE_DOMAIN\n\nPLATFORMS = [\"miele\"]\n\n_LOGGER = logging.getLogger(__name__)\n\nALL_DEVICES = []\n\nSUPPORTED_TYPES = [17, 18, 32, 33, 34, 68]\n\n\n# pylint: disable=W0612\ndef setup_platform(hass, config, add_devices, discovery_info=None):\n    global ALL_DEVICES\n\n    devices = hass.data[MIELE_DOMAIN][DATA_DEVICES]\n    for k, device in devices.items():\n        device_type = device[\"ident\"][\"type\"]\n\n        light_devices = []\n        if device_type[\"value_raw\"] in SUPPORTED_TYPES:\n            light_devices.append(MieleLight(hass, device))\n\n        add_devices(light_devices)\n        ALL_DEVICES = ALL_DEVICES + light_devices\n\n\ndef update_device_state():\n    for device in ALL_DEVICES:\n        try:\n            device.async_schedule_update_ha_state(True)\n        except (AssertionError, AttributeError):\n            _LOGGER.debug(\n                \"Component most likely is disabled manually, if not please report to developer\"\n                \"{}\".format(device.entity_id)\n            )\n\n\nclass MieleLight(LightEntity):\n    def __init__(self, hass, device):\n        self._hass = hass\n        self._device = device\n        self._ha_key = \"light\"\n\n    @property\n    def device_id(self):\n        \"\"\"Return the unique ID for this light.\"\"\"\n        return self._device[\"ident\"][\"deviceIdentLabel\"][\"fabNumber\"]\n\n    @property\n    def unique_id(self):\n        \"\"\"Return the unique ID for this light.\"\"\"\n        return self.device_id\n\n    @property\n    def name(self):\n        \"\"\"Return the name of the light.\"\"\"\n        ident = self._device[\"ident\"]\n\n        result = ident[\"deviceName\"]\n        if len(result) == 0:\n            return ident[\"type\"][\"value_localized\"]\n        else:\n            return result\n\n    @property\n    def is_on(self):\n        \"\"\"Return the state of the light.\"\"\"\n        return self._device[\"state\"][\"light\"] == 1\n\n    def turn_on(self, **kwargs):\n        service_parameters = {\"device_id\": self.device_id, \"body\": {\"light\": 1}}\n        self._hass.services.call(MIELE_DOMAIN, \"action\", service_parameters)\n\n    def turn_off(self, **kwargs):\n        service_parameters = {\"device_id\": self.device_id, \"body\": {\"light\": 2}}\n        self._hass.services.call(MIELE_DOMAIN, \"action\", service_parameters)\n\n    async def async_update(self):\n        if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]:\n            _LOGGER.debug(\"Miele device not found: {}\".format(self.device_id))\n        else:\n            self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id]\n"
  },
  {
    "path": "custom_components/miele/manifest.json",
    "content": "{\n  \"domain\": \"miele\",\n  \"name\": \"Miele@home\",\n  \"documentation\": \"https://github.com/HomeAssistant-Mods/home-assistant-miele\",\n  \"issue_tracker\": \"https://github.com/HomeAssistant-Mods/home-assistant-miele/issues\",\n  \"version\": \"v2021.10.12\",\n  \"iot_class\": \"cloud_polling\",\n  \"requirements\": [\n    \"requests_oauthlib>=1.3.0\"\n  ],\n  \"dependencies\": [\n    \"http\",\"configurator\"\n  ],\n  \"codeowners\": [\n    \"@kloknibor\",\n    \"@docbobo\"\n  ]\n}\n"
  },
  {
    "path": "custom_components/miele/miele_at_home.py",
    "content": "import asyncio\nimport functools\nimport json\nimport logging\nimport os\nfrom datetime import timedelta\n\nfrom requests.exceptions import ConnectionError\nfrom requests_oauthlib import OAuth2Session\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass MieleClient(object):\n    DEVICES_URL = \"https://api.mcs3.miele.com/v1/devices\"\n    ACTION_URL = \"https://api.mcs3.miele.com/v1/devices/{0}/actions\"\n    PROGRAMS_URL = \"https://api.mcs3.miele.com/v1/devices/{0}/programs\"\n\n    def __init__(self, hass, session):\n        self._session = session\n        self.hass = hass\n\n    async def _get_devices_raw(self, lang):\n        _LOGGER.debug(\"Requesting Miele device update\")\n        try:\n            func = functools.partial(\n                self._session._session.get,\n                MieleClient.DEVICES_URL,\n                params={\"language\": lang},\n            )\n            devices = await self.hass.async_add_executor_job(func)\n            if devices.status_code == 401:\n                _LOGGER.info(\"Request unauthorized - attempting token refresh\")\n                if await self._session.refresh_token(self.hass):\n                    return await self._get_devices_raw(lang)\n\n            if devices.status_code != 200:\n                _LOGGER.debug(\n                    \"Failed to retrieve devices: {}\".format(devices.status_code)\n                )\n                return None\n\n            return devices.json()\n\n        except ConnectionError as err:\n            _LOGGER.error(\"Failed to retrieve Miele devices: {0}\".format(err))\n            return None\n\n    async def get_devices(self, lang=\"en\"):\n        home_devices = await self._get_devices_raw(lang)\n        if home_devices is None:\n            return None\n\n        result = []\n        for home_device in home_devices:\n            result.append(home_devices[home_device])\n\n        return result\n\n    def get_device(self, device_id, lang=\"en\"):\n        devices = self._get_devices_raw(lang)\n        if devices is None:\n            return None\n\n        if devices is not None:\n            return devices[device_id]\n\n        return None\n\n    async def action(self, device_id, body):\n        _LOGGER.debug(\"Executing device action for {}{}\".format(device_id, body))\n        try:\n            headers = {\"Content-Type\": \"application/json\"}\n            func = functools.partial(\n                self._session._session.put,\n                MieleClient.ACTION_URL.format(device_id),\n                data=json.dumps(body),\n                headers=headers,\n            )\n            result = await self.hass.async_add_executor_job(func)\n            if result.status_code == 401:\n                _LOGGER.info(\"Request unauthorized - attempting token refresh\")\n\n                if await self._session.refresh_token(self.hass):\n                    if self._session.authorized:\n                        return self.action(device_id, body)\n                    else:\n                        self._session._delete_token()\n                        self._session.new_session()\n                        return self.action(device_id, body)\n\n            if result.status_code == 200:\n                return result.json()\n            elif result.status_code == 204:\n                return None\n            else:\n                _LOGGER.error(\n                    \"Failed to execute device action for {}: {} {}\".format(\n                        device_id, result.status_code, result.json()\n                    )\n                )\n                return None\n\n        except ConnectionError as err:\n            _LOGGER.error(\"Failed to execute device action: {}\".format(err))\n            return None\n\n    async def start_program(self, device_id, program_id):\n        _LOGGER.debug(\"Starting program {} for {}\".format(program_id, device_id))\n        try:\n            headers = {\"Content-Type\": \"application/json\"}\n            func = functools.partial(\n                self._session._session.put,\n                MieleClient.PROGRAMS_URL.format(device_id),\n                data=json.dumps({\"programId\": program_id}),\n                headers=headers,\n            )\n            result = await self.hass.async_add_executor_job(func)\n            if result.status_code == 401:\n                _LOGGER.info(\"Request unauthorized - attempting token refresh\")\n\n                if await self._session.refresh_token(self.hass):\n                    if self._session.authorized:\n                        return self.start_program(device_id, program_id)\n                    else:\n                        self._session._delete_token()\n                        self._session.new_session()\n                        return self.start_program(device_id, program_id)\n\n            if result.status_code == 200:\n                return result.json()\n            elif result.status_code == 204:\n                return None\n            else:\n                _LOGGER.error(\n                    \"Failed to execute start program for {}: {} {}\".format(\n                        device_id, result.status_code, result.json()\n                    )\n                )\n                return None\n\n        except ConnectionError as err:\n            _LOGGER.error(\"Failed to execute start program: {}\".format(err))\n            return None\n\n\nclass MieleOAuth(object):\n    \"\"\"\n    Implements Authorization Code Flow for Miele@home implementation.\n    \"\"\"\n\n    OAUTH_AUTHORIZE_URL = \"https://api.mcs3.miele.com/thirdparty/login\"\n    OAUTH_TOKEN_URL = \"https://api.mcs3.miele.com/thirdparty/token\"\n\n    def __init__(self, hass, client_id, client_secret, redirect_uri, cache_path=None):\n        self._client_id = client_id\n        self._client_secret = client_secret\n        self._cache_path = cache_path\n        self._redirect_uri = redirect_uri\n\n        self._token = self._get_cached_token()\n\n        self._extra = {\n            \"client_id\": self._client_id,\n            \"client_secret\": self._client_secret,\n        }\n\n        self._session = OAuth2Session(\n            self._client_id,\n            auto_refresh_url=MieleOAuth.OAUTH_TOKEN_URL,\n            redirect_uri=redirect_uri,\n            token=self._token,\n            token_updater=self._save_token,\n            auto_refresh_kwargs=self._extra,\n        )\n\n        if self.authorized:\n            asyncio.create_task(self.refresh_token(hass))\n\n    @property\n    def authorized(self):\n        return self._session.authorized\n\n    @property\n    def authorization_url(self):\n        return self._session.authorization_url(\n            MieleOAuth.OAUTH_AUTHORIZE_URL, state=\"login\"\n        )[0]\n\n    def get_access_token(self, client_code):\n        token = self._session.fetch_token(\n            MieleOAuth.OAUTH_TOKEN_URL,\n            code=client_code,\n            include_client_id=True,\n            client_secret=self._client_secret,\n        )\n        self._save_token(token)\n\n        return token\n\n    async def refresh_token(self, hass):\n        body = \"client_id={}&client_secret={}&\".format(\n            self._client_id, self._client_secret\n        )\n        self._token = await hass.async_add_executor_job(\n            self.sync_refresh_token,\n            MieleOAuth.OAUTH_TOKEN_URL,\n            body,\n            self._token[\"refresh_token\"],\n        )\n        self._save_token(self._token)\n\n    def sync_refresh_token(self, token_url, body, refresh_token):\n        try:\n            return self._session.refresh_token(\n                token_url, body=body, refresh_token=refresh_token\n            )\n        except:\n            self._remove_token()\n\n    def _get_cached_token(self):\n        token = None\n        if self._cache_path:\n            try:\n                f = open(self._cache_path)\n                token_info_string = f.read()\n                f.close()\n                token = json.loads(token_info_string)\n\n            except IOError:\n                pass\n\n        return token\n\n    def _delete_token(self):\n        if self._cache_path:\n            try:\n                os.remove(self._cache_path)\n\n            except IOError:\n                _LOGGER.warn(\"Unable to delete cached token\")\n\n        self._token = None\n\n    def _new_session(self, redirect_uri):\n        self._session = OAuth2Session(\n            self._client_id,\n            auto_refresh_url=MieleOAuth.OAUTH_TOKEN_URL,\n            redirect_uri=self._redirect_uri,\n            token=self._token,\n            token_updater=self._save_token,\n            auto_refresh_kwargs=self._extra,\n        )\n\n        if self.authorized:\n            self.refresh_token()\n\n    def _save_token(self, token):\n        _LOGGER.debug(\"trying to save new token\")\n        if self._cache_path:\n            try:\n                f = open(self._cache_path, \"w\")\n                f.write(json.dumps(token))\n                f.close()\n            except IOError:\n                _LOGGER.warn(\n                    \"Couldn't write token cache to {0}\".format(self._cache_path)\n                )\n                pass\n\n        self._token = token\n\n    def _remove_token(self):\n        _LOGGER.debug(\"trying to REMOVE token to create it again on next startup so please re-startup and try again!\")\n        if self._cache_path:\n            try:\n                os.remove(self._cache_path)\n            except IOError:\n                _LOGGER.warn(\n                    \"Couldn't delte token cache to {0}\".format(self._cache_path)\n                )\n                pass\n"
  },
  {
    "path": "custom_components/miele/sensor.py",
    "content": "import logging\nfrom datetime import datetime, timedelta\n\nfrom homeassistant.components.sensor import (\n    SensorDeviceClass,\n    SensorEntity,\n    SensorStateClass,\n)\n\nfrom homeassistant.helpers.entity import Entity\n\nfrom custom_components.miele import CAPABILITIES, DATA_DEVICES\nfrom custom_components.miele import DOMAIN as MIELE_DOMAIN\n\nPLATFORMS = [\"miele\"]\n\n_LOGGER = logging.getLogger(__name__)\n\nALL_DEVICES = []\n\n# https://www.miele.com/developer/swagger-ui/swagger.html#/\nSTATUS_OFF = 1\nSTATUS_ON = 2\nSTATUS_PROGRAMMED = 3\nSTATUS_PROGRAMMED_WAITING_TO_START = 4\nSTATUS_RUNNING = 5\nSTATUS_PAUSE = 6\nSTATUS_END_PROGRAMMED = 7\nSTATUS_FAILURE = 8\nSTATUS_PROGRAMME_INTERRUPTED = 9\nSTATUS_IDLE = 10\nSTATUS_RINSE_HOLD = 11\nSTATUS_SERVICE = 12\nSTATUS_SUPERFREEZING = 13\nSTATUS_SUPERCOOLING = 14\nSTATUS_SUPERHEATING = 15\nSTATUS_SUPERCOOLING_SUPERFREEZING = 146\nSTATUS_NOT_CONNECTED = 255\n\n\ndef _map_key(key):\n    if key == \"status\":\n        return \"Status\"\n    elif key == \"ProgramID\":\n        return \"Program ID\"\n    elif key == \"programType\":\n        return \"Program Type\"\n    elif key == \"programPhase\":\n        return \"Program Phase\"\n    elif key == \"targetTemperature\":\n        return \"Target Temperature\"\n    elif key == \"temperature\":\n        return \"Temperature\"\n    elif key == \"dryingStep\":\n        return \"Drying Step\"\n    elif key == \"spinningSpeed\":\n        return \"Spin Speed\"\n    elif key == \"remainingTime\":\n        return \"Remaining Time\"\n    elif key == \"elapsedTime\":\n        return \"Elapsed Time\"\n    elif key == \"startTime\":\n        return \"Start Time\"\n    elif key == \"energyConsumption\":\n        return \"Energy\"\n    elif key == \"waterConsumption\":\n        return \"Water Consumption\"\n    elif key == \"batteryLevel\":\n        return \"Battery Level\"\n    elif key == \"energyForecast\":\n        return \"Energy cons. forecast\"\n    elif key == \"waterForecast\":\n        return \"Water cons. forecast\"\n\n\ndef state_capability(type, state):\n    type_str = str(type)\n    if state in CAPABILITIES[type_str]:\n        return True\n\n\ndef _is_running(device_status):\n    return device_status in [\n        STATUS_RUNNING,\n        STATUS_PAUSE,\n        STATUS_END_PROGRAMMED,\n        STATUS_PROGRAMME_INTERRUPTED,\n        STATUS_RINSE_HOLD,\n    ]\n\n\ndef _is_terminated(device_status):\n    return device_status in [STATUS_END_PROGRAMMED, STATUS_PROGRAMME_INTERRUPTED]\n\n\ndef _to_seconds(time_array):\n    if len(time_array) == 3:\n        return time_array[0] * 3600 + time_array[1] * 60 + time_array[2]\n    elif len(time_array) == 2:\n        return time_array[0] * 3600 + time_array[1] * 60\n    else:\n        return 0\n\n\n# pylint: disable=W0612\ndef setup_platform(hass, config, add_devices, discovery_info=None):\n    global ALL_DEVICES\n\n    devices = hass.data[MIELE_DOMAIN][DATA_DEVICES]\n    for k, device in devices.items():\n        device_state = device[\"state\"]\n        device_type = device[\"ident\"][\"type\"][\"value_raw\"]\n\n        sensors = []\n        if \"status\" in device_state and state_capability(\n            type=device_type, state=\"status\"\n        ):\n            sensors.append(MieleStatusSensor(hass, device, \"status\"))\n\n        if \"ProgramID\" in device_state and state_capability(\n            type=device_type, state=\"ProgramID\"\n        ):\n            sensors.append(MieleTextSensor(hass, device, \"ProgramID\"))\n\n        if \"programPhase\" in device_state and state_capability(\n            type=device_type, state=\"programPhase\"\n        ):\n            sensors.append(MieleTextSensor(hass, device, \"programPhase\"))\n\n        if \"targetTemperature\" in device_state and state_capability(\n            type=device_type, state=\"targetTemperature\"\n        ):\n            for i, val in enumerate(device_state[\"targetTemperature\"]):\n                sensors.append(\n                    MieleTemperatureSensor(hass, device, \"targetTemperature\", i)\n                )\n\n        # washer, washer-dryer and dishwasher only have first target temperarure sensor\n        if \"targetTemperature\" in device_state and state_capability(\n            type=device_type, state=\"targetTemperature.0\"\n        ):\n            sensors.append(\n                MieleTemperatureSensor(hass, device, \"targetTemperature\", 0, True)\n            )\n\n        if \"temperature\" in device_state and state_capability(\n            type=device_type, state=\"temperature\"\n        ):\n            for i, val in enumerate(device_state[\"temperature\"]):\n                sensors.append(MieleTemperatureSensor(hass, device, \"temperature\", i))\n\n        if \"dryingStep\" in device_state and state_capability(\n            type=device_type, state=\"dryingStep\"\n        ):\n            sensors.append(MieleTextSensor(hass, device, \"dryingStep\"))\n\n        if \"spinningSpeed\" in device_state and state_capability(\n            type=device_type, state=\"spinningSpeed\"\n        ):\n            sensors.append(MieleTextSensor(hass, device, \"spinningSpeed\"))\n\n        if \"remainingTime\" in device_state and state_capability(\n            type=device_type, state=\"remainingTime\"\n        ):\n            sensors.append(MieleTimeSensor(hass, device, \"remainingTime\", True))\n        if \"startTime\" in device_state and state_capability(\n            type=device_type, state=\"startTime\"\n        ):\n            sensors.append(MieleTimeSensor(hass, device, \"startTime\"))\n        if \"elapsedTime\" in device_state and state_capability(\n            type=device_type, state=\"elapsedTime\"\n        ):\n            sensors.append(MieleTimeSensor(hass, device, \"elapsedTime\"))\n\n        if \"ecoFeedback\" in device_state and state_capability(\n            type=device_type, state=\"ecoFeedback.energyConsumption\"\n        ):\n            sensors.append(\n                MieleConsumptionSensor(\n                    hass, device, \"energyConsumption\", \"kWh\", SensorDeviceClass.ENERGY\n                )\n            )\n\n        if \"ecoFeedback\" in device_state and state_capability(\n            type=device_type, state=\"ecoFeedback.waterConsumption\"\n        ):\n            sensors.append(\n                MieleConsumptionForecastSensor(hass, device, \"energyForecast\")\n            )\n\n        if \"ecoFeedback\" in device_state and state_capability(\n            type=device_type, state=\"ecoFeedback.waterConsumption\"\n        ):\n            sensors.append(\n                MieleConsumptionSensor(hass, device, \"waterConsumption\", \"L\", None)\n            )\n            sensors.append(\n                MieleConsumptionForecastSensor(hass, device, \"waterForecast\")\n            )\n\n        if \"batteryLevel\" in device_state and state_capability(\n            type=device_type, state=\"batteryLevel\"\n        ):\n            sensors.append(MieleBatterySensor(hass, device, \"batteryLevel\"))\n\n        add_devices(sensors)\n        ALL_DEVICES = ALL_DEVICES + sensors\n\n\ndef update_device_state():\n    for device in ALL_DEVICES:\n        try:\n            device.async_schedule_update_ha_state(True)\n        except (AssertionError, AttributeError):\n            _LOGGER.debug(\n                \"Component most likely is disabled manually, if not please report to developer\"\n                \"{}\".format(device.entity_id)\n            )\n\n\nclass MieleRawSensor(Entity):\n    def __init__(self, hass, device, key):\n        self._hass = hass\n        self._device = device\n        self._key = key\n\n    @property\n    def device_id(self):\n        \"\"\"Return the unique ID for this sensor.\"\"\"\n        return self._device[\"ident\"][\"deviceIdentLabel\"][\"fabNumber\"]\n\n    @property\n    def unique_id(self):\n        \"\"\"Return the unique ID for this sensor.\"\"\"\n        return self.device_id + \"_\" + self._key\n\n    @property\n    def name(self):\n        \"\"\"Return the name of the sensor.\"\"\"\n        ident = self._device[\"ident\"]\n\n        result = ident[\"deviceName\"]\n        if len(result) == 0:\n            return ident[\"type\"][\"value_localized\"] + \" \" + _map_key(self._key)\n        else:\n            return result + \" \" + _map_key(self._key)\n\n    @property\n    def state(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n\n        return self._device[\"state\"][self._key][\"value_raw\"]\n\n    async def async_update(self):\n        if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]:\n            _LOGGER.debug(\"Miele device disappeared: {}\".format(self.device_id))\n        else:\n            self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id]\n\n\nclass MieleSensorEntity(SensorEntity):\n    def __init__(self, hass, device, key):\n        self._hass = hass\n        self._device = device\n        self._key = key\n\n    @property\n    def device_id(self):\n        \"\"\"Return the unique ID for this sensor.\"\"\"\n        return self._device[\"ident\"][\"deviceIdentLabel\"][\"fabNumber\"]\n\n    @property\n    def unique_id(self):\n        \"\"\"Return the unique ID for this sensor.\"\"\"\n        return self.device_id + \"_\" + self._key\n\n    @property\n    def name(self):\n        \"\"\"Return the name of the sensor.\"\"\"\n        ident = self._device[\"ident\"]\n\n        result = ident[\"deviceName\"]\n        if len(result) == 0:\n            return ident[\"type\"][\"value_localized\"] + \" \" + _map_key(self._key)\n        else:\n            return result + \" \" + _map_key(self._key)\n\n    async def async_update(self):\n        if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]:\n            _LOGGER.debug(\"Miele device disappeared: {}\".format(self.device_id))\n        else:\n            self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id]\n\n\nclass MieleStatusSensor(MieleRawSensor):\n    @property\n    def state(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        result = self._device[\"state\"][\"status\"][\"value_localized\"]\n        if result == None:\n            result = self._device[\"state\"][\"status\"][\"value_raw\"]\n\n        return result\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Attributes.\"\"\"\n        device_state = self._device[\"state\"]\n\n        attributes = {}\n        if \"ProgramID\" in device_state:\n            attributes[\"ProgramID\"] = device_state[\"ProgramID\"][\"value_localized\"]\n            attributes[\"rawProgramID\"] = device_state[\"ProgramID\"][\"value_raw\"]\n\n        if \"programType\" in device_state:\n            attributes[\"programType\"] = device_state[\"programType\"][\"value_localized\"]\n            attributes[\"rawProgramType\"] = device_state[\"programType\"][\"value_raw\"]\n\n        if \"programPhase\" in device_state:\n            attributes[\"programPhase\"] = device_state[\"programPhase\"][\"value_localized\"]\n            attributes[\"rawProgramPhase\"] = device_state[\"programPhase\"][\"value_raw\"]\n\n        if \"dryingStep\" in device_state:\n            attributes[\"dryingStep\"] = device_state[\"dryingStep\"][\"value_localized\"]\n            attributes[\"rawDryingStep\"] = device_state[\"dryingStep\"][\"value_raw\"]\n\n        if \"spinningSpeed\" in device_state:\n            attributes[\"spinningSpeed\"] = device_state[\"spinningSpeed\"][\n                \"value_localized\"\n            ]\n            attributes[\"rawSpinningSpeed\"] = device_state[\"spinningSpeed\"][\"value_raw\"]\n\n        if \"ventilationStep\" in device_state:\n            attributes[\"ventilationStep\"] = device_state[\"ventilationStep\"][\n                \"value_localized\"\n            ]\n            attributes[\"rawVentilationStep\"] = device_state[\"ventilationStep\"][\n                \"value_raw\"\n            ]\n\n        if \"plateStep\" in device_state:\n            plate_steps = 1\n            for plateStep in device_state[\"plateStep\"]:\n                attributes[\"plateStep\" + str(plate_steps)] = plateStep[\n                    \"value_localized\"\n                ]\n                attributes[\"rawPlateStep\" + str(plate_steps)] = plateStep[\"value_raw\"]\n                plate_steps += 1\n\n        if \"ecoFeedback\" in device_state and device_state[\"ecoFeedback\"] is not None:\n            if \"currentWaterConsumption\" in device_state[\"ecoFeedback\"]:\n                attributes[\"currentWaterConsumption\"] = device_state[\"ecoFeedback\"][\n                    \"currentWaterConsumption\"\n                ][\"value\"]\n                attributes[\"currentWaterConsumptionUnit\"] = device_state[\"ecoFeedback\"][\n                    \"currentWaterConsumption\"\n                ][\"unit\"]\n            if \"currentEnergyConsumption\" in device_state[\"ecoFeedback\"]:\n                attributes[\"currentEnergyConsumption\"] = device_state[\"ecoFeedback\"][\n                    \"currentEnergyConsumption\"\n                ][\"value\"]\n                attributes[\"currentEnergyConsumptionUnit\"] = device_state[\n                    \"ecoFeedback\"\n                ][\"currentEnergyConsumption\"][\"unit\"]\n            if \"waterForecast\" in device_state[\"ecoFeedback\"]:\n                attributes[\"waterForecast\"] = device_state[\"ecoFeedback\"][\n                    \"waterForecast\"\n                ]\n            if \"energyForecast\" in device_state[\"ecoFeedback\"]:\n                attributes[\"energyForecast\"] = device_state[\"ecoFeedback\"][\n                    \"energyForecast\"\n                ]\n\n        # Programs will only be running of both remainingTime and elapsedTime indicate\n        # a value > 0\n        if \"remainingTime\" in device_state and \"elapsedTime\" in device_state:\n            remainingTime = _to_seconds(device_state[\"remainingTime\"])\n            elapsedTime = _to_seconds(device_state[\"elapsedTime\"])\n\n            if \"startTime\" in device_state:\n                startTime = _to_seconds(device_state[\"startTime\"])\n            else:\n                startTime = 0\n\n            # Calculate progress\n            if (elapsedTime + remainingTime) == 0:\n                attributes[\"progress\"] = None\n            else:\n                attributes[\"progress\"] = round(\n                    elapsedTime / (elapsedTime + remainingTime) * 100, 1\n                )\n\n            # Calculate end time\n            if remainingTime == 0:\n                attributes[\"finishTime\"] = None\n            else:\n                now = datetime.now()\n                attributes[\"finishTime\"] = (\n                    now\n                    + timedelta(seconds=startTime)\n                    + timedelta(seconds=remainingTime)\n                ).strftime(\"%H:%M\")\n\n            # Calculate start time\n            if startTime == 0:\n                now = datetime.now()\n                attributes[\"kickoffTime\"] = (\n                    now - timedelta(seconds=elapsedTime)\n                ).strftime(\"%H:%M\")\n            else:\n                now = datetime.now()\n                attributes[\"kickoffTime\"] = (\n                    now + timedelta(seconds=startTime)\n                ).strftime(\"%H:%M\")\n\n        return attributes\n\n\nclass MieleConsumptionSensor(MieleSensorEntity):\n    def __init__(self, hass, device, key, measurement, device_class):\n        super().__init__(hass, device, key)\n\n        self._attr_native_unit_of_measurement = measurement\n        self._cached_consumption = -1\n        self._attr_state_class = SensorStateClass.TOTAL_INCREASING\n        self._attr_device_class = device_class\n\n    @property\n    def state(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        device_state = self._device[\"state\"]\n        device_status_value = self._device[\"state\"][\"status\"][\"value_raw\"]\n\n        if (\n            not _is_running(device_status_value)\n            and device_status_value != STATUS_NOT_CONNECTED\n        ):\n            self._cached_consumption = -1\n            return 0\n\n        if self._cached_consumption >= 0:\n            if (\n                \"ecoFeedback\" not in device_state\n                or device_state[\"ecoFeedback\"] is None\n                or device_status_value == STATUS_NOT_CONNECTED\n            ):\n                # Sometimes the Miele API seems to return a null ecoFeedback\n                # object even though the Miele device is running. Or if the the\n                # Miele device has lost the connection to the Miele cloud, the\n                # status is \"not connected\". Either way, we need to return the\n                # last known value until the API starts returning something\n                # sane again, otherwise the statistics generated from this\n                # sensor would be messed up.\n                return self._cached_consumption\n\n        consumption = 0\n        if self._key == \"energyConsumption\":\n            if \"currentEnergyConsumption\" in device_state[\"ecoFeedback\"]:\n                consumption_container = device_state[\"ecoFeedback\"][\n                    \"currentEnergyConsumption\"\n                ]\n\n                if consumption_container[\"unit\"] == \"kWh\":\n                    consumption = consumption_container[\"value\"]\n                elif consumption_container[\"unit\"] == \"Wh\":\n                    consumption = consumption_container[\"value\"] / 1000.0\n            else:\n                return self._cached_consumption\n\n        elif self._key == \"waterConsumption\":\n            if \"currentWaterConsumption\" in device_state[\"ecoFeedback\"]:\n                consumption = device_state[\"ecoFeedback\"][\"currentWaterConsumption\"][\n                    \"value\"\n                ]\n            else:\n                return self._cached_consumption\n\n        self._cached_consumption = consumption\n        return consumption\n\n\nclass MieleTimeSensor(MieleRawSensor):\n    def __init__(self, hass, device, key, decreasing=False):\n        super().__init__(hass, device, key)\n        self._init_value = \"--:--\"\n        self._cached_time = self._init_value\n        self._decreasing = decreasing\n\n    @property\n    def state(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        state_value = self._device[\"state\"][self._key]\n        device_status_value = self._device[\"state\"][\"status\"][\"value_raw\"]\n        formatted_value = None\n        if len(state_value) == 2:\n            formatted_value = \"{:02d}:{:02d}\".format(state_value[0], state_value[1])\n\n        if (\n            not _is_running(device_status_value)\n            and device_status_value != STATUS_NOT_CONNECTED\n        ):\n            self._cached_time = self._init_value\n            return formatted_value\n\n        if self._cached_time != self._init_value:\n            # As for energy consumption, also this information could become \"00:00\"\n            # when appliance is not reachable. Provide cached value in that case.\n            # Some appliances also clear time status when terminating program.\n            if self._decreasing and _is_terminated(device_status_value):\n                return formatted_value\n            elif (\n                formatted_value is None\n                or device_status_value == STATUS_NOT_CONNECTED\n                or _is_terminated(device_status_value)\n            ):\n                return self._cached_time\n\n        self._cached_time = formatted_value\n        return formatted_value\n\n\nclass MieleTemperatureSensor(Entity):\n    def __init__(self, hass, device, key, index, force_int=False):\n        self._hass = hass\n        self._device = device\n        self._key = key\n        self._index = index\n        self._force_int = force_int\n\n    @property\n    def device_id(self):\n        \"\"\"Return the unique ID for this sensor.\"\"\"\n        return self._device[\"ident\"][\"deviceIdentLabel\"][\"fabNumber\"]\n\n    @property\n    def unique_id(self):\n        \"\"\"Return the unique ID for this sensor.\"\"\"\n        return self.device_id + \"_\" + self._key + \"_{}\".format(self._index)\n\n    @property\n    def name(self):\n        \"\"\"Return the name of the sensor.\"\"\"\n        ident = self._device[\"ident\"]\n\n        result = ident[\"deviceName\"]\n        if len(result) == 0:\n            return \"{} {} {}\".format(\n                ident[\"type\"][\"value_localized\"], _map_key(self._key), self._index\n            )\n        else:\n            return \"{} {} {}\".format(result, _map_key(self._key), self._index)\n\n    @property\n    def state(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        state_value = self._device[\"state\"][self._key][self._index][\"value_raw\"]\n        if state_value == -32768:\n            return None\n        elif self._force_int:\n            return int(state_value / 100)\n        else:\n            return state_value / 100\n\n    @property\n    def unit_of_measurement(self):\n        \"\"\"Return the unit of measurement of this entity, if any.\"\"\"\n        if self._device[\"state\"][self._key][self._index][\"unit\"] == \"Celsius\":\n            return \"°C\"\n        elif self._device[\"state\"][self._key][self._index][\"unit\"] == \"Fahrenheit\":\n            return \"°F\"\n\n    @property\n    def device_class(self):\n        return \"temperature\"\n\n    async def async_update(self):\n        if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]:\n            _LOGGER.debug(\" Miele device disappeared: {}\".format(self.device_id))\n        else:\n            self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id]\n\n\nclass MieleTextSensor(MieleRawSensor):\n    @property\n    def state(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        result = self._device[\"state\"][self._key][\"value_localized\"]\n        if result == \"\":\n            result = None\n\n        return result\n\n\nclass MieleBatterySensor(MieleSensorEntity):\n    def __init__(self, hass, device, key):\n        super().__init__(hass, device, key)\n        self._attr_device_class = SensorDeviceClass.BATTERY\n        self._attr_native_unit_of_measurement = \"%\"\n        self._attr_state_class = SensorStateClass.MEASUREMENT\n\n    @property\n    def state(self):\n        return self._device[\"state\"][self._key]\n\n\nclass MieleConsumptionForecastSensor(MieleSensorEntity):\n    def __init__(self, hass, device, key):\n        super().__init__(hass, device, key)\n        self._attr_native_unit_of_measurement = \"%\"\n        self._attr_state_class = SensorStateClass.MEASUREMENT\n\n    @property\n    def state(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        device_state = self._device[\"state\"]\n\n        if (\n            device_state[\"ecoFeedback\"] is not None\n            and self._key in device_state[\"ecoFeedback\"]\n        ):\n            return device_state[\"ecoFeedback\"][self._key] * 100\n\n        return None\n"
  },
  {
    "path": "custom_components/miele/services.yaml",
    "content": "# Example services.yaml entry\n\naction:\n  # Description of the service\n  description: Runs action of Miele device\n  # Different fields that your service accepts\n  fields:\n    # Key of the field\n    entity_id:\n      # Description of the field\n      description: Name(s) of the entities to set (optional, either set this or device_id)\n      # Example value that can be passed for this field\n      example: \"miele.washing_machine\"\n    device_id:\n      # Description of the field\n      description: fab number of device to set (optional, either set this or entity_id)\n      # Example value that can be passed for this field\n      example: \"000123456789\"\n    body:\n      description: The command to send\n      example: \"{\\\"powerOff\\\": true}\"\n\nstart_program:\n  # Description of the service\n  description: Starts a program on a Miele device\n  # Different fields that your service accepts\n  fields:\n    # Key of the field\n    entity_id:\n      # Description of the field\n      description: Name(s) of the entities to set (optional, either set this or device_id)\n      # Example value that can be passed for this field\n      example: \"miele.washing_machine\"\n    device_id:\n      # Description of the field\n      description: fab number of device to set (optional, either set this or entity_id)\n      # Example value that can be passed for this field\n      example: \"000123456789\"\n    program_id:\n      description: The program id to start\n      example: 1\n\nstop_program:\n  # Description of the service\n  description: Stops program on a Miele device\n  # Different fields that your service accepts\n  fields:\n    # Key of the field\n    entity_id:\n      # Description of the field\n      description: Name(s) of the entities to set (optional, either set this or device_id)\n      # Example value that can be passed for this field\n      example: \"miele.washing_machine\"\n    device_id:\n      # Description of the field\n      description: fab number of device to set (optional, either set this or entity_id)\n      # Example value that can be passed for this field\n      example: \"000123456789\"\n"
  },
  {
    "path": "hacs.json",
    "content": "{\n  \"name\": \"Miele integration\",\n  \"homeassistant\": \"2025.1.0\"\n}"
  },
  {
    "path": "info.md",
    "content": "# Home Assistant support for Miele@home connected appliances\n\n## Introduction\n\nThis project exposes Miele state information of appliances connected to a Miele user account. This is achieved by communicating with the Miele Cloud Service, which exposes both applicances connected to a Miele@home Gateway XGW3000, as well as those devices connected via WiFi Con@ct.\n\n## Prerequisite\n\n* A running version of [Home Assistant](https://home-assistant.io). While earlier versions may work, the custom component has been developed and tested with version 0.76.x.\n* The ```requests_oauthlib``` library as part of your HA installation. Please install via ```pip3 install requests_oauthlib```.\n  For Hassbian you need to install this via :\n```\ncd /srv/\nsudo chown homeassistant:homeassistant homeassistant\nsudo su -s /bin/bash homeassistant\ncd /srv/homeassistant\nsource bin/activate\npip3 install requests_oauthlib\n```\n\n* Following the [instructions on the Miele developer site](https://www.miele.com/developer/getinvolved.html), you need to request your personal ```ClientID``` and ```ClientSecret```.\n\n## Installation of the custom component\n\n* Copy the content of this repository into your ```custom_components``` folder, which is a subdirectory of your Home Assistant configuration directory. By default, this directory is located under ```~/.home-assistant```. The structure of the ```custom_components``` directory should look like this afterwards:\n\n```\n- miele\n    - __init__.py\n    - miele_at_home.py\n    - binary_sensor.py\n    - light.py\n    - sensor.py\n```\n\n* Enabled the new platform in your ```configuration.yaml```:\n\n```\nmiele:\n    client_id: <your Miele ClientID>\n    client_secret: <your Miele ClientSecret>\n    lang: <optional. en=english, de=german>\n    cache_path: <optional. where to store the cached access token>\n```\n\n* Restart Home Assistant.\n* The Home Assistant Web UI will show you a UI to configure the Miele platform. Follow the instructions to log into the Miele Cloud Service. This will communicate back an authentication token that will be cached to communicate with the Cloud Service.\n\nDone. If you follow all the instructions, the Miele integration should be up and running. All Miele devices that you can see in your Mobile application should now be also visible in Home Assistant (miele.*). In addition, there will be a number of ```binary_sensors``` and ```sensors``` that can be used for automation.\n\n## Questions\n\nPlease see the [Miele@home, miele@mobile component](https://community.home-assistant.io/t/miele-home-miele-mobile-component/64508) discussion thread on the Home Assistant community site."
  }
]