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
================================================
# 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:
client_secret:
lang:
cache_path:
```
* 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:
client_secret:
lang:
cache_path:
interval:
```
* 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 = """Miele@home Auth
{}
""".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:
client_secret:
lang:
cache_path:
```
* 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.