Repository: HomeAssistant-Mods/home-assistant-miele
Branch: master
Commit: 38e06adc78f7
Files: 13
Total size: 69.8 KB
Directory structure:
gitextract_hihk12rd/
├── .github/
│ └── workflows/
│ └── combined.yaml
├── .gitignore
├── README.md
├── custom_components/
│ └── miele/
│ ├── __init__.py
│ ├── binary_sensor.py
│ ├── fan.py
│ ├── light.py
│ ├── manifest.json
│ ├── miele_at_home.py
│ ├── sensor.py
│ └── services.yaml
├── hacs.json
└── info.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/combined.yaml
================================================
name: "Validation And Formatting"
on:
push:
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
name: Download repo
with:
fetch-depth: 0
- uses: actions/setup-python@v2
name: Setup Python
- uses: actions/cache@v2
name: Cache
with:
path: |
~/.cache/pip
key: custom-component-ci
- uses: hacs/action@main
with:
CATEGORY: integration
- uses: KTibow/ha-blueprint@stable
name: CI
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
**/*.pyc
.idea/
*.iml
================================================
FILE: README.md
================================================
<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>
<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>
# Home Assistant support for Miele@home connected appliances
## Introduction
This 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.
## Prerequisite
* 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.
* 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```.
## HACS Install
We are now included in the default Repo of HACS. This is the recomanded way to install this integration.
* Install HACS if you haven't yet, instructions to install HACS can be found here : https://hacs.xyz/docs/installation/prerequisites
* Open the HACS component from your sidebar -> click integrations -> Search for Miele and install the Integration.
* Enable the new platform in your ```configuration.yaml```:
```
miele:
client_id: <your Miele ClientID>
client_secret: <your Miele ClientSecret>
lang: <optional. en=english, de=german>
cache_path: <optional. where to store the cached access token>
```
* Restart Home Assistant.
* 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.
Done. 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.
## Manual Installation of the custom component
* 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:
```
- miele
- __init__.py
- miele_at_home.py
- binary_sensor.py
- light.py
- sensor.py
```
* Enable the new platform in your ```configuration.yaml```:
```
miele:
client_id: <your Miele ClientID>
client_secret: <your Miele ClientSecret>
lang: <optional. en=english, de=german>
cache_path: <optional. where to store the cached access token>
interval: <optional. the interval between miele polling updates>
```
* Restart Home Assistant.
* 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.
Done. 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.
## Questions
Please 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.
================================================
FILE: custom_components/miele/__init__.py
================================================
"""
Support for Miele.
"""
import asyncio
import functools
import logging
from datetime import timedelta
from importlib import import_module
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from aiohttp import web
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import callback
from homeassistant.helpers import network
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.network import get_url
from homeassistant.helpers.storage import STORAGE_DIR
from .miele_at_home import MieleClient, MieleOAuth
_LOGGER = logging.getLogger(__name__)
DEVICES = []
DEFAULT_NAME = "Miele@home"
DOMAIN = "miele"
_CONFIGURING = {}
DATA_OAUTH = "oauth"
DATA_DEVICES = "devices"
DATA_CLIENT = "client"
SERVICE_ACTION = "action"
SERVICE_START_PROGRAM = "start_program"
SERVICE_STOP_PROGRAM = "stop_program"
SCOPE = "code"
DEFAULT_LANG = "en"
DEFAULT_INTERVAL = 5
AUTH_CALLBACK_PATH = "/api/miele/callback"
AUTH_CALLBACK_NAME = "api:miele:callback"
CONF_CLIENT_ID = "client_id"
CONF_CLIENT_SECRET = "client_secret"
CONF_LANG = "lang"
CONF_CACHE_PATH = "cache_path"
CONF_INTERVAL = "interval"
CONFIGURATOR_LINK_NAME = "Link Miele account"
CONFIGURATOR_SUBMIT_CAPTION = "I have authorized Miele@home."
CONFIGURATOR_DESCRIPTION = (
"To link your Miele account, " "click the link, login, and authorize:"
)
CONFIGURATOR_DESCRIPTION_IMAGE = (
"https://api.mcs3.miele.com/assets/images/miele_logo.svg"
)
MIELE_COMPONENTS = ["binary_sensor", "light", "sensor", "fan"]
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
vol.Optional(CONF_LANG): cv.string,
vol.Optional(CONF_CACHE_PATH): cv.string,
vol.Optional(CONF_INTERVAL): cv.positive_int,
}
),
},
extra=vol.ALLOW_EXTRA,
)
CAPABILITIES = {
"1": [
"ProgramID",
"status",
"programType",
"programPhase",
"remainingTime",
"startTime",
"targetTemperature.0",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
"elapsedTime",
"spinningSpeed",
"ecoFeedback.energyConsumption",
"ecoFeedback.waterConsumption",
],
"2": [
"ProgramID",
"status",
"programType",
"programPhase",
"remainingTime",
"startTime",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
"elapsedTime",
"dryingStep",
"ecoFeedback.energyConsumption",
],
"7": [
"ProgramID",
"status",
"programType",
"programPhase",
"remainingTime",
"startTime",
"signalInfo",
"signalFailure",
"remoteEnable",
"elapsedTime",
"ecoFeedback.energyConsumption",
"ecoFeedback.waterConsumption",
],
"12": [
"ProgramID",
"status",
"programType",
"programPhase",
"remainingTime",
"startTime",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
"elapsedTime",
],
"13": [
"ProgramID",
"status",
"programType",
"programPhase",
"remainingTime",
"startTime",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
"elapsedTime",
],
"14": ["status", "signalFailure", "plateStep"],
"15": [
"ProgramID",
"status",
"programType",
"programPhase",
"remainingTime",
"startTime",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
"elapsedTime",
],
"16": [
"ProgramID",
"status",
"programType",
"programPhase",
"remainingTime",
"startTime",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
"elapsedTime",
],
"17": [
"ProgramID",
"status",
"programPhase",
"signalInfo",
"signalFailure",
"remoteEnable",
],
"18": [
"status",
"signalInfo",
"signalFailure",
"remoteEnable",
"ventilationStep",
],
"19": [
"status",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
],
"20": [
"status",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
],
"21": [
"status",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
],
"23": [
"ProgramID",
"status",
"programType",
"signalInfo",
"signalFailure",
"remoteEnable",
"batteryLevel",
],
"24": [
"ProgramID",
"status",
"programType",
"programPhase",
"remainingTime",
"targetTemperature.0",
"startTime",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
"elapsedTime",
"spinningSpeed",
"dryingStep",
"ecoFeedback.energyConsumption",
"ecoFeedback.waterConsumption",
],
"25": [
"status",
"startTime",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"elapsedTime",
],
"27": ["status", "signalFailure", "plateStep"],
"31": [
"ProgramID",
"status",
"programType",
"programPhase",
"remainingTime",
"startTime",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
"elapsedTime",
],
"32": [
"status",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
],
"33": [
"status",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
],
"34": [
"status",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
],
"45": [
"ProgramID",
"status",
"programType",
"programPhase",
"remainingTime",
"startTime",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
"elapsedTime",
],
"67": [
"ProgramID",
"status",
"programType",
"programPhase",
"remainingTime",
"startTime",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"signalDoor",
"remoteEnable",
"elapsedTime",
],
"68": [
"status",
"targetTemperature",
"temperature",
"signalInfo",
"signalFailure",
"remoteEnable",
],
}
def request_configuration(hass, config, oauth):
"""Request Miele authorization."""
async def miele_configuration_callback(callback_data):
if not hass.data[DOMAIN][DATA_OAUTH].authorized:
configurator.async_notify_errors(
_CONFIGURING[DOMAIN], "Failed to register, please try again."
)
return
if DOMAIN in _CONFIGURING:
req_config = _CONFIGURING.pop(DOMAIN)
hass.components.configurator.async_request_done(req_config)
await async_setup(hass, config)
_LOGGER.info("Requesting authorization...")
configurator = hass.components.configurator
_CONFIGURING[DOMAIN] = configurator.async_request_config(
DEFAULT_NAME,
miele_configuration_callback,
link_name=CONFIGURATOR_LINK_NAME,
link_url=oauth.authorization_url,
description=CONFIGURATOR_DESCRIPTION,
description_image=CONFIGURATOR_DESCRIPTION_IMAGE,
submit_caption=CONFIGURATOR_SUBMIT_CAPTION,
)
return
def create_sensor(client, hass, home_device, lang):
return MieleDevice(hass, client, home_device, lang)
def _to_dict(items):
# Replace with map()
result = {}
for item in items:
ident = item["ident"]
result[ident["deviceIdentLabel"]["fabNumber"]] = item
return result
async def async_setup(hass, config):
"""Set up the Miele platform."""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
if DATA_OAUTH not in hass.data[DOMAIN]:
callback_url = "{}{}".format(
network.get_url(hass, allow_external=True, prefer_external=True),
AUTH_CALLBACK_PATH,
)
cache = config[DOMAIN].get(
CONF_CACHE_PATH, hass.config.path(STORAGE_DIR, f".miele-token-cache")
)
hass.data[DOMAIN][DATA_OAUTH] = MieleOAuth(
hass,
config[DOMAIN].get(CONF_CLIENT_ID),
config[DOMAIN].get(CONF_CLIENT_SECRET),
redirect_uri=callback_url,
cache_path=cache,
)
if not hass.data[DOMAIN][DATA_OAUTH].authorized:
_LOGGER.info("no token; requesting authorization")
hass.http.register_view(
MieleAuthCallbackView(config, hass.data[DOMAIN][DATA_OAUTH])
)
request_configuration(hass, config, hass.data[DOMAIN][DATA_OAUTH])
return True
lang = config[DOMAIN].get(CONF_LANG, DEFAULT_LANG)
component = EntityComponent(_LOGGER, DOMAIN, hass)
client = MieleClient(hass, hass.data[DOMAIN][DATA_OAUTH])
hass.data[DOMAIN][DATA_CLIENT] = client
data_get_devices = await client.get_devices(lang)
hass.data[DOMAIN][DATA_DEVICES] = _to_dict(data_get_devices)
DEVICES.extend(
[
create_sensor(client, hass, home_device, lang)
for k, home_device in hass.data[DOMAIN][DATA_DEVICES].items()
]
)
await component.async_add_entities(DEVICES, False)
for component in MIELE_COMPONENTS:
load_platform(hass, component, DOMAIN, {}, config)
async def refresh_devices(event_time):
_LOGGER.debug("Attempting to update Miele devices")
try:
device_state = await client.get_devices(lang)
except:
device_state = None
if device_state is None:
_LOGGER.error("Did not receive Miele devices")
else:
hass.data[DOMAIN][DATA_DEVICES] = _to_dict(device_state)
for device in DEVICES:
device.async_schedule_update_ha_state(True)
for component in MIELE_COMPONENTS:
platform = import_module(".{}".format(component), __name__)
platform.update_device_state()
register_services(hass)
interval = timedelta(seconds=config[DOMAIN].get(CONF_INTERVAL, DEFAULT_INTERVAL))
async_track_time_interval(hass, refresh_devices, interval)
return True
def register_services(hass):
"""Register all services for Miele devices."""
hass.services.async_register(DOMAIN, SERVICE_ACTION, _action_service)
hass.services.async_register(DOMAIN, SERVICE_START_PROGRAM, _action_start_program)
hass.services.async_register(DOMAIN, SERVICE_STOP_PROGRAM, _action_stop_program)
async def _apply_service(service, service_func, *service_func_args):
entity_ids = service.data.get("entity_id")
_devices = []
if entity_ids:
_devices.extend(
[device for device in DEVICES if device.entity_id in entity_ids]
)
device_ids = service.data.get("device_id")
if device_ids:
_devices.extend(
[device for device in DEVICES if device.unique_id in device_ids]
)
for device in _devices:
await service_func(device, *service_func_args)
async def _action_service(service):
body = service.data.get("body")
await _apply_service(service, MieleDevice.action, body)
async def _action_start_program(service):
program_id = service.data.get("program_id")
await _apply_service(service, MieleDevice.start_program, program_id)
async def _action_stop_program(service):
body = {"processAction": 2}
await _apply_service(service, MieleDevice.action, body)
class MieleAuthCallbackView(HomeAssistantView):
"""Miele Authorization Callback View."""
requires_auth = False
url = AUTH_CALLBACK_PATH
name = AUTH_CALLBACK_NAME
def __init__(self, config, oauth):
"""Initialize."""
self.config = config
self.oauth = oauth
@callback
async def get(self, request):
"""Receive authorization token."""
hass = request.app["hass"]
from oauthlib.oauth2.rfc6749.errors import (
MismatchingStateError,
MissingTokenError,
)
response_message = """Miele@home has been successfully authorized!
You can close this window now!"""
result = None
if request.query.get("code") is not None:
try:
func = functools.partial(
self.oauth.get_access_token, request.query["code"]
)
result = await hass.async_add_executor_job(func)
except MissingTokenError as error:
_LOGGER.error("Missing token: %s", error)
response_message = """Something went wrong when
attempting authenticating with Miele@home. The error
encountered was {}. Please try again!""".format(
error
)
except MismatchingStateError as error:
_LOGGER.error("Mismatched state, CSRF error: %s", error)
response_message = """Something went wrong when
attempting authenticating with Miele@home. The error
encountered was {}. Please try again!""".format(
error
)
else:
_LOGGER.error("Unknown error when authorizing")
response_message = """Something went wrong when
attempting authenticating with Miele@home.
An unknown error occurred. Please try again!
"""
html_response = """<html><head><title>Miele@home Auth</title></head>
<body><h1>{}</h1></body></html>""".format(
response_message
)
response = web.Response(
body=html_response, content_type="text/html", status=200, headers=None
)
response.enable_compression()
return response
class MieleDevice(Entity):
def __init__(self, hass, client, home_device, lang):
self._hass = hass
self._client = client
self._home_device = home_device
self._lang = lang
@property
def unique_id(self):
"""Return the unique ID for this sensor."""
return self._home_device["ident"]["deviceIdentLabel"]["fabNumber"]
@property
def name(self):
"""Return the name of the sensor."""
ident = self._home_device["ident"]
result = ident["deviceName"]
if len(result) == 0:
result = ident["type"]["value_localized"]
return result
@property
def state(self):
"""Return the state of the sensor."""
result = self._home_device["state"]["status"]["value_localized"]
if result == None:
result = self._home_device["state"]["status"]["value_raw"]
return result
@property
def extra_state_attributes(self):
"""Attributes."""
result = {}
result["state_raw"] = self._home_device["state"]["status"]["value_raw"]
result["model"] = self._home_device["ident"]["deviceIdentLabel"]["techType"]
result["device_type"] = self._home_device["ident"]["type"]["value_localized"]
result["fabrication_number"] = self._home_device["ident"]["deviceIdentLabel"][
"fabNumber"
]
result["gateway_type"] = self._home_device["ident"]["xkmIdentLabel"]["techType"]
result["gateway_version"] = self._home_device["ident"]["xkmIdentLabel"][
"releaseVersion"
]
return result
async def action(self, action):
await self._client.action(self.unique_id, action)
async def start_program(self, program_id):
await self._client.start_program(self.unique_id, program_id)
async def async_update(self):
if not self.unique_id in self._hass.data[DOMAIN][DATA_DEVICES]:
_LOGGER.debug("Miele device not found: {}".format(self.unique_id))
else:
self._home_device = self._hass.data[DOMAIN][DATA_DEVICES][self.unique_id]
================================================
FILE: custom_components/miele/binary_sensor.py
================================================
import logging
from datetime import timedelta
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.helpers.entity import Entity
from custom_components.miele import CAPABILITIES, DATA_DEVICES
from custom_components.miele import DOMAIN as MIELE_DOMAIN
PLATFORMS = ["miele"]
_LOGGER = logging.getLogger(__name__)
ALL_DEVICES = []
def state_capability(type, state):
type_str = str(type)
if state in CAPABILITIES[type_str]:
return True
def _map_key(key):
if key == "signalInfo":
return "Info"
elif key == "signalFailure":
return "Failure"
elif key == "signalDoor":
return "Door"
elif key == "mobileStart":
return "MobileStart"
# pylint: disable=W0612
def setup_platform(hass, config, add_devices, discovery_info=None):
global ALL_DEVICES
devices = hass.data[MIELE_DOMAIN][DATA_DEVICES]
for k, device in devices.items():
device_state = device["state"]
device_type = device["ident"]["type"]["value_raw"]
binary_devices = []
if "signalInfo" in device_state and state_capability(
type=device_type, state="signalInfo"
):
binary_devices.append(MieleBinarySensor(hass, device, "signalInfo"))
if "signalFailure" in device_state and state_capability(
type=device_type, state="signalFailure"
):
binary_devices.append(MieleBinarySensor(hass, device, "signalFailure"))
if "signalDoor" in device_state and state_capability(
type=device_type, state="signalDoor"
):
binary_devices.append(MieleBinarySensor(hass, device, "signalDoor"))
if "remoteEnable" in device_state and state_capability(
type=device_type, state="remoteEnable"
):
remote_state = device_state["remoteEnable"]
if "mobileStart" in remote_state:
binary_devices.append(
MieleBinarySensor(hass, device, "remoteEnable.mobileStart")
)
add_devices(binary_devices)
ALL_DEVICES = ALL_DEVICES + binary_devices
def update_device_state():
for device in ALL_DEVICES:
try:
device.async_schedule_update_ha_state(True)
except (AssertionError, AttributeError):
_LOGGER.debug(
"Component most likely is disabled manually, if not please report to developer"
"{}".format(device.entity_id)
)
class MieleBinarySensor(BinarySensorEntity):
def __init__(self, hass, device, key):
self._hass = hass
self._device = device
self._keys = key.split(".")
self._key = self._keys[-1]
self._ha_key = _map_key(self._key)
@property
def device_id(self):
"""Return the unique ID for this sensor."""
return self._device["ident"]["deviceIdentLabel"]["fabNumber"]
@property
def unique_id(self):
"""Return the unique ID for this sensor."""
return self.device_id + "_" + self._ha_key
@property
def name(self):
"""Return the name of the sensor."""
ident = self._device["ident"]
result = ident["deviceName"]
if len(result) == 0:
return ident["type"]["value_localized"] + " " + self._ha_key
else:
return result + " " + self._ha_key
@property
def is_on(self):
"""Return the state of the sensor."""
current_val = self._device["state"]
for k in self._keys:
current_val = current_val[k]
return bool(current_val)
@property
def device_class(self):
if self._key == "signalDoor":
return "door"
elif self._key == "mobileStart":
return "running"
else:
return "problem"
async def async_update(self):
if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]:
_LOGGER.debug("Miele device not found: {}".format(self.device_id))
else:
self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id]
================================================
FILE: custom_components/miele/fan.py
================================================
import logging
import math
from datetime import timedelta
from typing import Optional
from homeassistant.components.fan import FanEntityFeature, FanEntity
from homeassistant.helpers.entity import Entity
from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
from custom_components.miele import DATA_CLIENT, DATA_DEVICES
from custom_components.miele import DOMAIN as MIELE_DOMAIN
PLATFORMS = ["miele"]
_LOGGER = logging.getLogger(__name__)
ALL_DEVICES = []
SUPPORTED_TYPES = [18]
SPEED_RANGE = (1, 4)
# pylint: disable=W0612
def setup_platform(hass, config, add_devices, discovery_info=None):
global ALL_DEVICES
devices = hass.data[MIELE_DOMAIN][DATA_DEVICES]
for k, device in devices.items():
device_type = device["ident"]["type"]
fan_devices = []
if device_type["value_raw"] in SUPPORTED_TYPES:
fan_devices.append(MieleFan(hass, device))
add_devices(fan_devices)
ALL_DEVICES = ALL_DEVICES + fan_devices
def update_device_state():
for device in ALL_DEVICES:
try:
device.async_schedule_update_ha_state(True)
except (AssertionError, AttributeError):
_LOGGER.debug(
"Component most likely is disabled manually, if not please report to developer"
"{}".format(device.entity_id)
)
class MieleFan(FanEntity):
def __init__(self, hass, device):
self._hass = hass
self._device = device
self._ha_key = "fan"
self._current_speed = 0
@property
def device_id(self):
"""Return the unique ID for this fan."""
return self._device["ident"]["deviceIdentLabel"]["fabNumber"]
@property
def unique_id(self):
"""Return the unique ID for this fan."""
return self.device_id
@property
def name(self):
"""Return the name of the fan."""
ident = self._device["ident"]
result = ident["deviceName"]
if len(result) == 0:
return ident["type"]["value_localized"]
else:
return result
@property
def is_on(self):
"""Return the state of the fan."""
value_raw = self._device["state"]["ventilationStep"]["value_raw"]
return value_raw != None and value_raw != 0
@property
def supported_features(self):
"""Flag supported features."""
return FanEntityFeature.SET_SPEED
@property
def speed(self):
"""Return the current speed"""
return self._device["state"]["ventilationStep"]["value_raw"]
@property
def percentage(self) -> Optional[int]:
"""Return the current speed percentage."""
return ranged_value_to_percentage(SPEED_RANGE, self._current_speed)
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
return int_states_in_range(SPEED_RANGE)
def turn_on(self, percentage: Optional[int] = None, **kwargs) -> None:
"""Turn on the fan."""
value_in_range = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
if percentage == "0":
self.turn_off()
client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]
client.action(device_id=self.device_id, body={"powerOn": True})
async def async_turn_on(self, percentage: Optional[int] = None, **kwargs):
"""Turn on the fan."""
if percentage == "0":
await self.async_turn_off()
elif percentage is not None:
await self.async_set_percentage(percentage=percentage)
client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]
await client.action(device_id=self.device_id, body={"powerOn": True})
else:
_LOGGER.debug("Turning on")
client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]
await client.action(device_id=self.device_id, body={"powerOn": True})
def turn_off(self, **kwargs):
_LOGGER.debug("Turning off")
client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]
client.action(device_id=self.device_id, body={"powerOff": True})
async def async_turn_off(self, **kwargs):
client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]
await client.action(device_id=self.device_id, body={"powerOff": True})
def set_percentage(self, percentage: int) -> None:
"""Set the speed percentage of the fan.""" # TODO:
value_in_range = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
self._current_speed = value_in_range
_LOGGER.debug("Setting speed to : {}".format(value_in_range))
client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]
client.action(
device_id=self.device_id, body={"ventilationStep": value_in_range}
)
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed percentage of the fan.""" #
value_in_range = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
self._current_speed = value_in_range
_LOGGER.debug("Setting speed to : {}".format(value_in_range))
client = self._hass.data[MIELE_DOMAIN][DATA_CLIENT]
await client.action(
device_id=self.device_id, body={"ventilationStep": value_in_range}
)
async def async_update(self):
if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]:
_LOGGER.debug("Miele device not found: {}".format(self.device_id))
else:
self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id]
================================================
FILE: custom_components/miele/light.py
================================================
import logging
from datetime import timedelta
from homeassistant.components.light import LightEntity
from homeassistant.helpers.entity import Entity
from custom_components.miele import DATA_CLIENT, DATA_DEVICES
from custom_components.miele import DOMAIN as MIELE_DOMAIN
PLATFORMS = ["miele"]
_LOGGER = logging.getLogger(__name__)
ALL_DEVICES = []
SUPPORTED_TYPES = [17, 18, 32, 33, 34, 68]
# pylint: disable=W0612
def setup_platform(hass, config, add_devices, discovery_info=None):
global ALL_DEVICES
devices = hass.data[MIELE_DOMAIN][DATA_DEVICES]
for k, device in devices.items():
device_type = device["ident"]["type"]
light_devices = []
if device_type["value_raw"] in SUPPORTED_TYPES:
light_devices.append(MieleLight(hass, device))
add_devices(light_devices)
ALL_DEVICES = ALL_DEVICES + light_devices
def update_device_state():
for device in ALL_DEVICES:
try:
device.async_schedule_update_ha_state(True)
except (AssertionError, AttributeError):
_LOGGER.debug(
"Component most likely is disabled manually, if not please report to developer"
"{}".format(device.entity_id)
)
class MieleLight(LightEntity):
def __init__(self, hass, device):
self._hass = hass
self._device = device
self._ha_key = "light"
@property
def device_id(self):
"""Return the unique ID for this light."""
return self._device["ident"]["deviceIdentLabel"]["fabNumber"]
@property
def unique_id(self):
"""Return the unique ID for this light."""
return self.device_id
@property
def name(self):
"""Return the name of the light."""
ident = self._device["ident"]
result = ident["deviceName"]
if len(result) == 0:
return ident["type"]["value_localized"]
else:
return result
@property
def is_on(self):
"""Return the state of the light."""
return self._device["state"]["light"] == 1
def turn_on(self, **kwargs):
service_parameters = {"device_id": self.device_id, "body": {"light": 1}}
self._hass.services.call(MIELE_DOMAIN, "action", service_parameters)
def turn_off(self, **kwargs):
service_parameters = {"device_id": self.device_id, "body": {"light": 2}}
self._hass.services.call(MIELE_DOMAIN, "action", service_parameters)
async def async_update(self):
if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]:
_LOGGER.debug("Miele device not found: {}".format(self.device_id))
else:
self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id]
================================================
FILE: custom_components/miele/manifest.json
================================================
{
"domain": "miele",
"name": "Miele@home",
"documentation": "https://github.com/HomeAssistant-Mods/home-assistant-miele",
"issue_tracker": "https://github.com/HomeAssistant-Mods/home-assistant-miele/issues",
"version": "v2021.10.12",
"iot_class": "cloud_polling",
"requirements": [
"requests_oauthlib>=1.3.0"
],
"dependencies": [
"http","configurator"
],
"codeowners": [
"@kloknibor",
"@docbobo"
]
}
================================================
FILE: custom_components/miele/miele_at_home.py
================================================
import asyncio
import functools
import json
import logging
import os
from datetime import timedelta
from requests.exceptions import ConnectionError
from requests_oauthlib import OAuth2Session
_LOGGER = logging.getLogger(__name__)
class MieleClient(object):
DEVICES_URL = "https://api.mcs3.miele.com/v1/devices"
ACTION_URL = "https://api.mcs3.miele.com/v1/devices/{0}/actions"
PROGRAMS_URL = "https://api.mcs3.miele.com/v1/devices/{0}/programs"
def __init__(self, hass, session):
self._session = session
self.hass = hass
async def _get_devices_raw(self, lang):
_LOGGER.debug("Requesting Miele device update")
try:
func = functools.partial(
self._session._session.get,
MieleClient.DEVICES_URL,
params={"language": lang},
)
devices = await self.hass.async_add_executor_job(func)
if devices.status_code == 401:
_LOGGER.info("Request unauthorized - attempting token refresh")
if await self._session.refresh_token(self.hass):
return await self._get_devices_raw(lang)
if devices.status_code != 200:
_LOGGER.debug(
"Failed to retrieve devices: {}".format(devices.status_code)
)
return None
return devices.json()
except ConnectionError as err:
_LOGGER.error("Failed to retrieve Miele devices: {0}".format(err))
return None
async def get_devices(self, lang="en"):
home_devices = await self._get_devices_raw(lang)
if home_devices is None:
return None
result = []
for home_device in home_devices:
result.append(home_devices[home_device])
return result
def get_device(self, device_id, lang="en"):
devices = self._get_devices_raw(lang)
if devices is None:
return None
if devices is not None:
return devices[device_id]
return None
async def action(self, device_id, body):
_LOGGER.debug("Executing device action for {}{}".format(device_id, body))
try:
headers = {"Content-Type": "application/json"}
func = functools.partial(
self._session._session.put,
MieleClient.ACTION_URL.format(device_id),
data=json.dumps(body),
headers=headers,
)
result = await self.hass.async_add_executor_job(func)
if result.status_code == 401:
_LOGGER.info("Request unauthorized - attempting token refresh")
if await self._session.refresh_token(self.hass):
if self._session.authorized:
return self.action(device_id, body)
else:
self._session._delete_token()
self._session.new_session()
return self.action(device_id, body)
if result.status_code == 200:
return result.json()
elif result.status_code == 204:
return None
else:
_LOGGER.error(
"Failed to execute device action for {}: {} {}".format(
device_id, result.status_code, result.json()
)
)
return None
except ConnectionError as err:
_LOGGER.error("Failed to execute device action: {}".format(err))
return None
async def start_program(self, device_id, program_id):
_LOGGER.debug("Starting program {} for {}".format(program_id, device_id))
try:
headers = {"Content-Type": "application/json"}
func = functools.partial(
self._session._session.put,
MieleClient.PROGRAMS_URL.format(device_id),
data=json.dumps({"programId": program_id}),
headers=headers,
)
result = await self.hass.async_add_executor_job(func)
if result.status_code == 401:
_LOGGER.info("Request unauthorized - attempting token refresh")
if await self._session.refresh_token(self.hass):
if self._session.authorized:
return self.start_program(device_id, program_id)
else:
self._session._delete_token()
self._session.new_session()
return self.start_program(device_id, program_id)
if result.status_code == 200:
return result.json()
elif result.status_code == 204:
return None
else:
_LOGGER.error(
"Failed to execute start program for {}: {} {}".format(
device_id, result.status_code, result.json()
)
)
return None
except ConnectionError as err:
_LOGGER.error("Failed to execute start program: {}".format(err))
return None
class MieleOAuth(object):
"""
Implements Authorization Code Flow for Miele@home implementation.
"""
OAUTH_AUTHORIZE_URL = "https://api.mcs3.miele.com/thirdparty/login"
OAUTH_TOKEN_URL = "https://api.mcs3.miele.com/thirdparty/token"
def __init__(self, hass, client_id, client_secret, redirect_uri, cache_path=None):
self._client_id = client_id
self._client_secret = client_secret
self._cache_path = cache_path
self._redirect_uri = redirect_uri
self._token = self._get_cached_token()
self._extra = {
"client_id": self._client_id,
"client_secret": self._client_secret,
}
self._session = OAuth2Session(
self._client_id,
auto_refresh_url=MieleOAuth.OAUTH_TOKEN_URL,
redirect_uri=redirect_uri,
token=self._token,
token_updater=self._save_token,
auto_refresh_kwargs=self._extra,
)
if self.authorized:
asyncio.create_task(self.refresh_token(hass))
@property
def authorized(self):
return self._session.authorized
@property
def authorization_url(self):
return self._session.authorization_url(
MieleOAuth.OAUTH_AUTHORIZE_URL, state="login"
)[0]
def get_access_token(self, client_code):
token = self._session.fetch_token(
MieleOAuth.OAUTH_TOKEN_URL,
code=client_code,
include_client_id=True,
client_secret=self._client_secret,
)
self._save_token(token)
return token
async def refresh_token(self, hass):
body = "client_id={}&client_secret={}&".format(
self._client_id, self._client_secret
)
self._token = await hass.async_add_executor_job(
self.sync_refresh_token,
MieleOAuth.OAUTH_TOKEN_URL,
body,
self._token["refresh_token"],
)
self._save_token(self._token)
def sync_refresh_token(self, token_url, body, refresh_token):
try:
return self._session.refresh_token(
token_url, body=body, refresh_token=refresh_token
)
except:
self._remove_token()
def _get_cached_token(self):
token = None
if self._cache_path:
try:
f = open(self._cache_path)
token_info_string = f.read()
f.close()
token = json.loads(token_info_string)
except IOError:
pass
return token
def _delete_token(self):
if self._cache_path:
try:
os.remove(self._cache_path)
except IOError:
_LOGGER.warn("Unable to delete cached token")
self._token = None
def _new_session(self, redirect_uri):
self._session = OAuth2Session(
self._client_id,
auto_refresh_url=MieleOAuth.OAUTH_TOKEN_URL,
redirect_uri=self._redirect_uri,
token=self._token,
token_updater=self._save_token,
auto_refresh_kwargs=self._extra,
)
if self.authorized:
self.refresh_token()
def _save_token(self, token):
_LOGGER.debug("trying to save new token")
if self._cache_path:
try:
f = open(self._cache_path, "w")
f.write(json.dumps(token))
f.close()
except IOError:
_LOGGER.warn(
"Couldn't write token cache to {0}".format(self._cache_path)
)
pass
self._token = token
def _remove_token(self):
_LOGGER.debug("trying to REMOVE token to create it again on next startup so please re-startup and try again!")
if self._cache_path:
try:
os.remove(self._cache_path)
except IOError:
_LOGGER.warn(
"Couldn't delte token cache to {0}".format(self._cache_path)
)
pass
================================================
FILE: custom_components/miele/sensor.py
================================================
import logging
from datetime import datetime, timedelta
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.helpers.entity import Entity
from custom_components.miele import CAPABILITIES, DATA_DEVICES
from custom_components.miele import DOMAIN as MIELE_DOMAIN
PLATFORMS = ["miele"]
_LOGGER = logging.getLogger(__name__)
ALL_DEVICES = []
# https://www.miele.com/developer/swagger-ui/swagger.html#/
STATUS_OFF = 1
STATUS_ON = 2
STATUS_PROGRAMMED = 3
STATUS_PROGRAMMED_WAITING_TO_START = 4
STATUS_RUNNING = 5
STATUS_PAUSE = 6
STATUS_END_PROGRAMMED = 7
STATUS_FAILURE = 8
STATUS_PROGRAMME_INTERRUPTED = 9
STATUS_IDLE = 10
STATUS_RINSE_HOLD = 11
STATUS_SERVICE = 12
STATUS_SUPERFREEZING = 13
STATUS_SUPERCOOLING = 14
STATUS_SUPERHEATING = 15
STATUS_SUPERCOOLING_SUPERFREEZING = 146
STATUS_NOT_CONNECTED = 255
def _map_key(key):
if key == "status":
return "Status"
elif key == "ProgramID":
return "Program ID"
elif key == "programType":
return "Program Type"
elif key == "programPhase":
return "Program Phase"
elif key == "targetTemperature":
return "Target Temperature"
elif key == "temperature":
return "Temperature"
elif key == "dryingStep":
return "Drying Step"
elif key == "spinningSpeed":
return "Spin Speed"
elif key == "remainingTime":
return "Remaining Time"
elif key == "elapsedTime":
return "Elapsed Time"
elif key == "startTime":
return "Start Time"
elif key == "energyConsumption":
return "Energy"
elif key == "waterConsumption":
return "Water Consumption"
elif key == "batteryLevel":
return "Battery Level"
elif key == "energyForecast":
return "Energy cons. forecast"
elif key == "waterForecast":
return "Water cons. forecast"
def state_capability(type, state):
type_str = str(type)
if state in CAPABILITIES[type_str]:
return True
def _is_running(device_status):
return device_status in [
STATUS_RUNNING,
STATUS_PAUSE,
STATUS_END_PROGRAMMED,
STATUS_PROGRAMME_INTERRUPTED,
STATUS_RINSE_HOLD,
]
def _is_terminated(device_status):
return device_status in [STATUS_END_PROGRAMMED, STATUS_PROGRAMME_INTERRUPTED]
def _to_seconds(time_array):
if len(time_array) == 3:
return time_array[0] * 3600 + time_array[1] * 60 + time_array[2]
elif len(time_array) == 2:
return time_array[0] * 3600 + time_array[1] * 60
else:
return 0
# pylint: disable=W0612
def setup_platform(hass, config, add_devices, discovery_info=None):
global ALL_DEVICES
devices = hass.data[MIELE_DOMAIN][DATA_DEVICES]
for k, device in devices.items():
device_state = device["state"]
device_type = device["ident"]["type"]["value_raw"]
sensors = []
if "status" in device_state and state_capability(
type=device_type, state="status"
):
sensors.append(MieleStatusSensor(hass, device, "status"))
if "ProgramID" in device_state and state_capability(
type=device_type, state="ProgramID"
):
sensors.append(MieleTextSensor(hass, device, "ProgramID"))
if "programPhase" in device_state and state_capability(
type=device_type, state="programPhase"
):
sensors.append(MieleTextSensor(hass, device, "programPhase"))
if "targetTemperature" in device_state and state_capability(
type=device_type, state="targetTemperature"
):
for i, val in enumerate(device_state["targetTemperature"]):
sensors.append(
MieleTemperatureSensor(hass, device, "targetTemperature", i)
)
# washer, washer-dryer and dishwasher only have first target temperarure sensor
if "targetTemperature" in device_state and state_capability(
type=device_type, state="targetTemperature.0"
):
sensors.append(
MieleTemperatureSensor(hass, device, "targetTemperature", 0, True)
)
if "temperature" in device_state and state_capability(
type=device_type, state="temperature"
):
for i, val in enumerate(device_state["temperature"]):
sensors.append(MieleTemperatureSensor(hass, device, "temperature", i))
if "dryingStep" in device_state and state_capability(
type=device_type, state="dryingStep"
):
sensors.append(MieleTextSensor(hass, device, "dryingStep"))
if "spinningSpeed" in device_state and state_capability(
type=device_type, state="spinningSpeed"
):
sensors.append(MieleTextSensor(hass, device, "spinningSpeed"))
if "remainingTime" in device_state and state_capability(
type=device_type, state="remainingTime"
):
sensors.append(MieleTimeSensor(hass, device, "remainingTime", True))
if "startTime" in device_state and state_capability(
type=device_type, state="startTime"
):
sensors.append(MieleTimeSensor(hass, device, "startTime"))
if "elapsedTime" in device_state and state_capability(
type=device_type, state="elapsedTime"
):
sensors.append(MieleTimeSensor(hass, device, "elapsedTime"))
if "ecoFeedback" in device_state and state_capability(
type=device_type, state="ecoFeedback.energyConsumption"
):
sensors.append(
MieleConsumptionSensor(
hass, device, "energyConsumption", "kWh", SensorDeviceClass.ENERGY
)
)
if "ecoFeedback" in device_state and state_capability(
type=device_type, state="ecoFeedback.waterConsumption"
):
sensors.append(
MieleConsumptionForecastSensor(hass, device, "energyForecast")
)
if "ecoFeedback" in device_state and state_capability(
type=device_type, state="ecoFeedback.waterConsumption"
):
sensors.append(
MieleConsumptionSensor(hass, device, "waterConsumption", "L", None)
)
sensors.append(
MieleConsumptionForecastSensor(hass, device, "waterForecast")
)
if "batteryLevel" in device_state and state_capability(
type=device_type, state="batteryLevel"
):
sensors.append(MieleBatterySensor(hass, device, "batteryLevel"))
add_devices(sensors)
ALL_DEVICES = ALL_DEVICES + sensors
def update_device_state():
for device in ALL_DEVICES:
try:
device.async_schedule_update_ha_state(True)
except (AssertionError, AttributeError):
_LOGGER.debug(
"Component most likely is disabled manually, if not please report to developer"
"{}".format(device.entity_id)
)
class MieleRawSensor(Entity):
def __init__(self, hass, device, key):
self._hass = hass
self._device = device
self._key = key
@property
def device_id(self):
"""Return the unique ID for this sensor."""
return self._device["ident"]["deviceIdentLabel"]["fabNumber"]
@property
def unique_id(self):
"""Return the unique ID for this sensor."""
return self.device_id + "_" + self._key
@property
def name(self):
"""Return the name of the sensor."""
ident = self._device["ident"]
result = ident["deviceName"]
if len(result) == 0:
return ident["type"]["value_localized"] + " " + _map_key(self._key)
else:
return result + " " + _map_key(self._key)
@property
def state(self):
"""Return the state of the sensor."""
return self._device["state"][self._key]["value_raw"]
async def async_update(self):
if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]:
_LOGGER.debug("Miele device disappeared: {}".format(self.device_id))
else:
self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id]
class MieleSensorEntity(SensorEntity):
def __init__(self, hass, device, key):
self._hass = hass
self._device = device
self._key = key
@property
def device_id(self):
"""Return the unique ID for this sensor."""
return self._device["ident"]["deviceIdentLabel"]["fabNumber"]
@property
def unique_id(self):
"""Return the unique ID for this sensor."""
return self.device_id + "_" + self._key
@property
def name(self):
"""Return the name of the sensor."""
ident = self._device["ident"]
result = ident["deviceName"]
if len(result) == 0:
return ident["type"]["value_localized"] + " " + _map_key(self._key)
else:
return result + " " + _map_key(self._key)
async def async_update(self):
if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]:
_LOGGER.debug("Miele device disappeared: {}".format(self.device_id))
else:
self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id]
class MieleStatusSensor(MieleRawSensor):
@property
def state(self):
"""Return the state of the sensor."""
result = self._device["state"]["status"]["value_localized"]
if result == None:
result = self._device["state"]["status"]["value_raw"]
return result
@property
def extra_state_attributes(self):
"""Attributes."""
device_state = self._device["state"]
attributes = {}
if "ProgramID" in device_state:
attributes["ProgramID"] = device_state["ProgramID"]["value_localized"]
attributes["rawProgramID"] = device_state["ProgramID"]["value_raw"]
if "programType" in device_state:
attributes["programType"] = device_state["programType"]["value_localized"]
attributes["rawProgramType"] = device_state["programType"]["value_raw"]
if "programPhase" in device_state:
attributes["programPhase"] = device_state["programPhase"]["value_localized"]
attributes["rawProgramPhase"] = device_state["programPhase"]["value_raw"]
if "dryingStep" in device_state:
attributes["dryingStep"] = device_state["dryingStep"]["value_localized"]
attributes["rawDryingStep"] = device_state["dryingStep"]["value_raw"]
if "spinningSpeed" in device_state:
attributes["spinningSpeed"] = device_state["spinningSpeed"][
"value_localized"
]
attributes["rawSpinningSpeed"] = device_state["spinningSpeed"]["value_raw"]
if "ventilationStep" in device_state:
attributes["ventilationStep"] = device_state["ventilationStep"][
"value_localized"
]
attributes["rawVentilationStep"] = device_state["ventilationStep"][
"value_raw"
]
if "plateStep" in device_state:
plate_steps = 1
for plateStep in device_state["plateStep"]:
attributes["plateStep" + str(plate_steps)] = plateStep[
"value_localized"
]
attributes["rawPlateStep" + str(plate_steps)] = plateStep["value_raw"]
plate_steps += 1
if "ecoFeedback" in device_state and device_state["ecoFeedback"] is not None:
if "currentWaterConsumption" in device_state["ecoFeedback"]:
attributes["currentWaterConsumption"] = device_state["ecoFeedback"][
"currentWaterConsumption"
]["value"]
attributes["currentWaterConsumptionUnit"] = device_state["ecoFeedback"][
"currentWaterConsumption"
]["unit"]
if "currentEnergyConsumption" in device_state["ecoFeedback"]:
attributes["currentEnergyConsumption"] = device_state["ecoFeedback"][
"currentEnergyConsumption"
]["value"]
attributes["currentEnergyConsumptionUnit"] = device_state[
"ecoFeedback"
]["currentEnergyConsumption"]["unit"]
if "waterForecast" in device_state["ecoFeedback"]:
attributes["waterForecast"] = device_state["ecoFeedback"][
"waterForecast"
]
if "energyForecast" in device_state["ecoFeedback"]:
attributes["energyForecast"] = device_state["ecoFeedback"][
"energyForecast"
]
# Programs will only be running of both remainingTime and elapsedTime indicate
# a value > 0
if "remainingTime" in device_state and "elapsedTime" in device_state:
remainingTime = _to_seconds(device_state["remainingTime"])
elapsedTime = _to_seconds(device_state["elapsedTime"])
if "startTime" in device_state:
startTime = _to_seconds(device_state["startTime"])
else:
startTime = 0
# Calculate progress
if (elapsedTime + remainingTime) == 0:
attributes["progress"] = None
else:
attributes["progress"] = round(
elapsedTime / (elapsedTime + remainingTime) * 100, 1
)
# Calculate end time
if remainingTime == 0:
attributes["finishTime"] = None
else:
now = datetime.now()
attributes["finishTime"] = (
now
+ timedelta(seconds=startTime)
+ timedelta(seconds=remainingTime)
).strftime("%H:%M")
# Calculate start time
if startTime == 0:
now = datetime.now()
attributes["kickoffTime"] = (
now - timedelta(seconds=elapsedTime)
).strftime("%H:%M")
else:
now = datetime.now()
attributes["kickoffTime"] = (
now + timedelta(seconds=startTime)
).strftime("%H:%M")
return attributes
class MieleConsumptionSensor(MieleSensorEntity):
def __init__(self, hass, device, key, measurement, device_class):
super().__init__(hass, device, key)
self._attr_native_unit_of_measurement = measurement
self._cached_consumption = -1
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
self._attr_device_class = device_class
@property
def state(self):
"""Return the state of the sensor."""
device_state = self._device["state"]
device_status_value = self._device["state"]["status"]["value_raw"]
if (
not _is_running(device_status_value)
and device_status_value != STATUS_NOT_CONNECTED
):
self._cached_consumption = -1
return 0
if self._cached_consumption >= 0:
if (
"ecoFeedback" not in device_state
or device_state["ecoFeedback"] is None
or device_status_value == STATUS_NOT_CONNECTED
):
# Sometimes the Miele API seems to return a null ecoFeedback
# object even though the Miele device is running. Or if the the
# Miele device has lost the connection to the Miele cloud, the
# status is "not connected". Either way, we need to return the
# last known value until the API starts returning something
# sane again, otherwise the statistics generated from this
# sensor would be messed up.
return self._cached_consumption
consumption = 0
if self._key == "energyConsumption":
if "currentEnergyConsumption" in device_state["ecoFeedback"]:
consumption_container = device_state["ecoFeedback"][
"currentEnergyConsumption"
]
if consumption_container["unit"] == "kWh":
consumption = consumption_container["value"]
elif consumption_container["unit"] == "Wh":
consumption = consumption_container["value"] / 1000.0
else:
return self._cached_consumption
elif self._key == "waterConsumption":
if "currentWaterConsumption" in device_state["ecoFeedback"]:
consumption = device_state["ecoFeedback"]["currentWaterConsumption"][
"value"
]
else:
return self._cached_consumption
self._cached_consumption = consumption
return consumption
class MieleTimeSensor(MieleRawSensor):
def __init__(self, hass, device, key, decreasing=False):
super().__init__(hass, device, key)
self._init_value = "--:--"
self._cached_time = self._init_value
self._decreasing = decreasing
@property
def state(self):
"""Return the state of the sensor."""
state_value = self._device["state"][self._key]
device_status_value = self._device["state"]["status"]["value_raw"]
formatted_value = None
if len(state_value) == 2:
formatted_value = "{:02d}:{:02d}".format(state_value[0], state_value[1])
if (
not _is_running(device_status_value)
and device_status_value != STATUS_NOT_CONNECTED
):
self._cached_time = self._init_value
return formatted_value
if self._cached_time != self._init_value:
# As for energy consumption, also this information could become "00:00"
# when appliance is not reachable. Provide cached value in that case.
# Some appliances also clear time status when terminating program.
if self._decreasing and _is_terminated(device_status_value):
return formatted_value
elif (
formatted_value is None
or device_status_value == STATUS_NOT_CONNECTED
or _is_terminated(device_status_value)
):
return self._cached_time
self._cached_time = formatted_value
return formatted_value
class MieleTemperatureSensor(Entity):
def __init__(self, hass, device, key, index, force_int=False):
self._hass = hass
self._device = device
self._key = key
self._index = index
self._force_int = force_int
@property
def device_id(self):
"""Return the unique ID for this sensor."""
return self._device["ident"]["deviceIdentLabel"]["fabNumber"]
@property
def unique_id(self):
"""Return the unique ID for this sensor."""
return self.device_id + "_" + self._key + "_{}".format(self._index)
@property
def name(self):
"""Return the name of the sensor."""
ident = self._device["ident"]
result = ident["deviceName"]
if len(result) == 0:
return "{} {} {}".format(
ident["type"]["value_localized"], _map_key(self._key), self._index
)
else:
return "{} {} {}".format(result, _map_key(self._key), self._index)
@property
def state(self):
"""Return the state of the sensor."""
state_value = self._device["state"][self._key][self._index]["value_raw"]
if state_value == -32768:
return None
elif self._force_int:
return int(state_value / 100)
else:
return state_value / 100
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
if self._device["state"][self._key][self._index]["unit"] == "Celsius":
return "°C"
elif self._device["state"][self._key][self._index]["unit"] == "Fahrenheit":
return "°F"
@property
def device_class(self):
return "temperature"
async def async_update(self):
if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]:
_LOGGER.debug(" Miele device disappeared: {}".format(self.device_id))
else:
self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id]
class MieleTextSensor(MieleRawSensor):
@property
def state(self):
"""Return the state of the sensor."""
result = self._device["state"][self._key]["value_localized"]
if result == "":
result = None
return result
class MieleBatterySensor(MieleSensorEntity):
def __init__(self, hass, device, key):
super().__init__(hass, device, key)
self._attr_device_class = SensorDeviceClass.BATTERY
self._attr_native_unit_of_measurement = "%"
self._attr_state_class = SensorStateClass.MEASUREMENT
@property
def state(self):
return self._device["state"][self._key]
class MieleConsumptionForecastSensor(MieleSensorEntity):
def __init__(self, hass, device, key):
super().__init__(hass, device, key)
self._attr_native_unit_of_measurement = "%"
self._attr_state_class = SensorStateClass.MEASUREMENT
@property
def state(self):
"""Return the state of the sensor."""
device_state = self._device["state"]
if (
device_state["ecoFeedback"] is not None
and self._key in device_state["ecoFeedback"]
):
return device_state["ecoFeedback"][self._key] * 100
return None
================================================
FILE: custom_components/miele/services.yaml
================================================
# Example services.yaml entry
action:
# Description of the service
description: Runs action of Miele device
# Different fields that your service accepts
fields:
# Key of the field
entity_id:
# Description of the field
description: Name(s) of the entities to set (optional, either set this or device_id)
# Example value that can be passed for this field
example: "miele.washing_machine"
device_id:
# Description of the field
description: fab number of device to set (optional, either set this or entity_id)
# Example value that can be passed for this field
example: "000123456789"
body:
description: The command to send
example: "{\"powerOff\": true}"
start_program:
# Description of the service
description: Starts a program on a Miele device
# Different fields that your service accepts
fields:
# Key of the field
entity_id:
# Description of the field
description: Name(s) of the entities to set (optional, either set this or device_id)
# Example value that can be passed for this field
example: "miele.washing_machine"
device_id:
# Description of the field
description: fab number of device to set (optional, either set this or entity_id)
# Example value that can be passed for this field
example: "000123456789"
program_id:
description: The program id to start
example: 1
stop_program:
# Description of the service
description: Stops program on a Miele device
# Different fields that your service accepts
fields:
# Key of the field
entity_id:
# Description of the field
description: Name(s) of the entities to set (optional, either set this or device_id)
# Example value that can be passed for this field
example: "miele.washing_machine"
device_id:
# Description of the field
description: fab number of device to set (optional, either set this or entity_id)
# Example value that can be passed for this field
example: "000123456789"
================================================
FILE: hacs.json
================================================
{
"name": "Miele integration",
"homeassistant": "2025.1.0"
}
================================================
FILE: info.md
================================================
# Home Assistant support for Miele@home connected appliances
## Introduction
This 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.
## Prerequisite
* 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.
* The ```requests_oauthlib``` library as part of your HA installation. Please install via ```pip3 install requests_oauthlib```.
For Hassbian you need to install this via :
```
cd /srv/
sudo chown homeassistant:homeassistant homeassistant
sudo su -s /bin/bash homeassistant
cd /srv/homeassistant
source bin/activate
pip3 install requests_oauthlib
```
* Following the [instructions on the Miele developer site](https://www.miele.com/developer/getinvolved.html), you need to request your personal ```ClientID``` and ```ClientSecret```.
## Installation of the custom component
* 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:
```
- miele
- __init__.py
- miele_at_home.py
- binary_sensor.py
- light.py
- sensor.py
```
* Enabled the new platform in your ```configuration.yaml```:
```
miele:
client_id: <your Miele ClientID>
client_secret: <your Miele ClientSecret>
lang: <optional. en=english, de=german>
cache_path: <optional. where to store the cached access token>
```
* Restart Home Assistant.
* 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.
Done. 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.
## Questions
Please 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.
gitextract_hihk12rd/ ├── .github/ │ └── workflows/ │ └── combined.yaml ├── .gitignore ├── README.md ├── custom_components/ │ └── miele/ │ ├── __init__.py │ ├── binary_sensor.py │ ├── fan.py │ ├── light.py │ ├── manifest.json │ ├── miele_at_home.py │ ├── sensor.py │ └── services.yaml ├── hacs.json └── info.md
SYMBOL INDEX (128 symbols across 6 files)
FILE: custom_components/miele/__init__.py
function request_configuration (line 346) | def request_configuration(hass, config, oauth):
function create_sensor (line 376) | def create_sensor(client, hass, home_device, lang):
function _to_dict (line 380) | def _to_dict(items):
function async_setup (line 390) | async def async_setup(hass, config):
function register_services (line 465) | def register_services(hass):
function _apply_service (line 472) | async def _apply_service(service, service_func, *service_func_args):
function _action_service (line 491) | async def _action_service(service):
function _action_start_program (line 496) | async def _action_start_program(service):
function _action_stop_program (line 501) | async def _action_stop_program(service):
class MieleAuthCallbackView (line 506) | class MieleAuthCallbackView(HomeAssistantView):
method __init__ (line 513) | def __init__(self, config, oauth):
method get (line 519) | async def get(self, request):
class MieleDevice (line 573) | class MieleDevice(Entity):
method __init__ (line 574) | def __init__(self, hass, client, home_device, lang):
method unique_id (line 581) | def unique_id(self):
method name (line 586) | def name(self):
method state (line 598) | def state(self):
method extra_state_attributes (line 608) | def extra_state_attributes(self):
method action (line 627) | async def action(self, action):
method start_program (line 630) | async def start_program(self, program_id):
method async_update (line 633) | async def async_update(self):
FILE: custom_components/miele/binary_sensor.py
function state_capability (line 17) | def state_capability(type, state):
function _map_key (line 23) | def _map_key(key):
function setup_platform (line 35) | def setup_platform(hass, config, add_devices, discovery_info=None):
function update_device_state (line 69) | def update_device_state():
class MieleBinarySensor (line 80) | class MieleBinarySensor(BinarySensorEntity):
method __init__ (line 81) | def __init__(self, hass, device, key):
method device_id (line 89) | def device_id(self):
method unique_id (line 94) | def unique_id(self):
method name (line 99) | def name(self):
method is_on (line 110) | def is_on(self):
method device_class (line 118) | def device_class(self):
method async_update (line 126) | async def async_update(self):
FILE: custom_components/miele/fan.py
function setup_platform (line 30) | def setup_platform(hass, config, add_devices, discovery_info=None):
function update_device_state (line 45) | def update_device_state():
class MieleFan (line 56) | class MieleFan(FanEntity):
method __init__ (line 57) | def __init__(self, hass, device):
method device_id (line 64) | def device_id(self):
method unique_id (line 69) | def unique_id(self):
method name (line 74) | def name(self):
method is_on (line 85) | def is_on(self):
method supported_features (line 91) | def supported_features(self):
method speed (line 96) | def speed(self):
method percentage (line 101) | def percentage(self) -> Optional[int]:
method speed_count (line 106) | def speed_count(self) -> int:
method turn_on (line 110) | def turn_on(self, percentage: Optional[int] = None, **kwargs) -> None:
method async_turn_on (line 118) | async def async_turn_on(self, percentage: Optional[int] = None, **kwar...
method turn_off (line 131) | def turn_off(self, **kwargs):
method async_turn_off (line 136) | async def async_turn_off(self, **kwargs):
method set_percentage (line 140) | def set_percentage(self, percentage: int) -> None:
method async_set_percentage (line 150) | async def async_set_percentage(self, percentage: int) -> None:
method async_update (line 160) | async def async_update(self):
FILE: custom_components/miele/light.py
function setup_platform (line 20) | def setup_platform(hass, config, add_devices, discovery_info=None):
function update_device_state (line 35) | def update_device_state():
class MieleLight (line 46) | class MieleLight(LightEntity):
method __init__ (line 47) | def __init__(self, hass, device):
method device_id (line 53) | def device_id(self):
method unique_id (line 58) | def unique_id(self):
method name (line 63) | def name(self):
method is_on (line 74) | def is_on(self):
method turn_on (line 78) | def turn_on(self, **kwargs):
method turn_off (line 82) | def turn_off(self, **kwargs):
method async_update (line 86) | async def async_update(self):
FILE: custom_components/miele/miele_at_home.py
class MieleClient (line 14) | class MieleClient(object):
method __init__ (line 19) | def __init__(self, hass, session):
method _get_devices_raw (line 23) | async def _get_devices_raw(self, lang):
method get_devices (line 49) | async def get_devices(self, lang="en"):
method get_device (line 60) | def get_device(self, device_id, lang="en"):
method action (line 70) | async def action(self, device_id, body):
method start_program (line 108) | async def start_program(self, device_id, program_id):
class MieleOAuth (line 147) | class MieleOAuth(object):
method __init__ (line 155) | def __init__(self, hass, client_id, client_secret, redirect_uri, cache...
method authorized (line 181) | def authorized(self):
method authorization_url (line 185) | def authorization_url(self):
method get_access_token (line 190) | def get_access_token(self, client_code):
method refresh_token (line 201) | async def refresh_token(self, hass):
method sync_refresh_token (line 213) | def sync_refresh_token(self, token_url, body, refresh_token):
method _get_cached_token (line 221) | def _get_cached_token(self):
method _delete_token (line 235) | def _delete_token(self):
method _new_session (line 245) | def _new_session(self, redirect_uri):
method _save_token (line 258) | def _save_token(self, token):
method _remove_token (line 273) | def _remove_token(self):
FILE: custom_components/miele/sensor.py
function _map_key (line 41) | def _map_key(key):
function state_capability (line 76) | def state_capability(type, state):
function _is_running (line 82) | def _is_running(device_status):
function _is_terminated (line 92) | def _is_terminated(device_status):
function _to_seconds (line 96) | def _to_seconds(time_array):
function setup_platform (line 106) | def setup_platform(hass, config, add_devices, discovery_info=None):
function update_device_state (line 210) | def update_device_state():
class MieleRawSensor (line 221) | class MieleRawSensor(Entity):
method __init__ (line 222) | def __init__(self, hass, device, key):
method device_id (line 228) | def device_id(self):
method unique_id (line 233) | def unique_id(self):
method name (line 238) | def name(self):
method state (line 249) | def state(self):
method async_update (line 254) | async def async_update(self):
class MieleSensorEntity (line 261) | class MieleSensorEntity(SensorEntity):
method __init__ (line 262) | def __init__(self, hass, device, key):
method device_id (line 268) | def device_id(self):
method unique_id (line 273) | def unique_id(self):
method name (line 278) | def name(self):
method async_update (line 288) | async def async_update(self):
class MieleStatusSensor (line 295) | class MieleStatusSensor(MieleRawSensor):
method state (line 297) | def state(self):
method extra_state_attributes (line 306) | def extra_state_attributes(self):
class MieleConsumptionSensor (line 419) | class MieleConsumptionSensor(MieleSensorEntity):
method __init__ (line 420) | def __init__(self, hass, device, key, measurement, device_class):
method state (line 429) | def state(self):
class MieleTimeSensor (line 482) | class MieleTimeSensor(MieleRawSensor):
method __init__ (line 483) | def __init__(self, hass, device, key, decreasing=False):
method state (line 490) | def state(self):
class MieleTemperatureSensor (line 522) | class MieleTemperatureSensor(Entity):
method __init__ (line 523) | def __init__(self, hass, device, key, index, force_int=False):
method device_id (line 531) | def device_id(self):
method unique_id (line 536) | def unique_id(self):
method name (line 541) | def name(self):
method state (line 554) | def state(self):
method unit_of_measurement (line 565) | def unit_of_measurement(self):
method device_class (line 573) | def device_class(self):
method async_update (line 576) | async def async_update(self):
class MieleTextSensor (line 583) | class MieleTextSensor(MieleRawSensor):
method state (line 585) | def state(self):
class MieleBatterySensor (line 594) | class MieleBatterySensor(MieleSensorEntity):
method __init__ (line 595) | def __init__(self, hass, device, key):
method state (line 602) | def state(self):
class MieleConsumptionForecastSensor (line 606) | class MieleConsumptionForecastSensor(MieleSensorEntity):
method __init__ (line 607) | def __init__(self, hass, device, key):
method state (line 613) | def state(self):
Condensed preview — 13 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (76K chars).
[
{
"path": ".github/workflows/combined.yaml",
"chars": 622,
"preview": "name: \"Validation And Formatting\"\non:\n push:\n pull_request:\njobs:\n ci:\n runs-on: ubuntu-latest\n steps:\n - "
},
{
"path": ".gitignore",
"chars": 21,
"preview": "**/*.pyc\n.idea/\n*.iml"
},
{
"path": "README.md",
"chars": 4159,
"preview": "<a href=\"https://www.buymeacoffee.com/robink\" target=\"_blank\"><img src=\"https://www.buymeacoffee.com/assets/img/custom_i"
},
{
"path": "custom_components/miele/__init__.py",
"chars": 17547,
"preview": "\"\"\"\nSupport for Miele.\n\"\"\"\nimport asyncio\nimport functools\nimport logging\nfrom datetime import timedelta\nfrom importlib "
},
{
"path": "custom_components/miele/binary_sensor.py",
"chars": 4124,
"preview": "import logging\nfrom datetime import timedelta\n\nfrom homeassistant.components.binary_sensor import BinarySensorEntity\nfro"
},
{
"path": "custom_components/miele/fan.py",
"chars": 5673,
"preview": "import logging\nimport math\nfrom datetime import timedelta\nfrom typing import Optional\n\nfrom homeassistant.components.fan"
},
{
"path": "custom_components/miele/light.py",
"chars": 2766,
"preview": "import logging\nfrom datetime import timedelta\n\nfrom homeassistant.components.light import LightEntity\nfrom homeassistant"
},
{
"path": "custom_components/miele/manifest.json",
"chars": 440,
"preview": "{\n \"domain\": \"miele\",\n \"name\": \"Miele@home\",\n \"documentation\": \"https://github.com/HomeAssistant-Mods/home-assistant-"
},
{
"path": "custom_components/miele/miele_at_home.py",
"chars": 9344,
"preview": "import asyncio\nimport functools\nimport json\nimport logging\nimport os\nfrom datetime import timedelta\n\nfrom requests.excep"
},
{
"path": "custom_components/miele/sensor.py",
"chars": 21969,
"preview": "import logging\nfrom datetime import datetime, timedelta\n\nfrom homeassistant.components.sensor import (\n SensorDeviceC"
},
{
"path": "custom_components/miele/services.yaml",
"chars": 2074,
"preview": "# Example services.yaml entry\n\naction:\n # Description of the service\n description: Runs action of Miele device\n # Dif"
},
{
"path": "hacs.json",
"chars": 64,
"preview": "{\n \"name\": \"Miele integration\",\n \"homeassistant\": \"2025.1.0\"\n}"
},
{
"path": "info.md",
"chars": 2624,
"preview": "# Home Assistant support for Miele@home connected appliances\n\n## Introduction\n\nThis project exposes Miele state informat"
}
]
About this extraction
This page contains the full source code of the HomeAssistant-Mods/home-assistant-miele GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 13 files (69.8 KB), approximately 15.9k tokens, and a symbol index with 128 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.