Add miele diagnostics platform (#142900)

This commit is contained in:
Åke Strandberg 2025-04-17 11:42:07 +02:00 committed by GitHub
parent cadbb623c7
commit 7d13c2d854
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 963 additions and 2 deletions

View File

@ -0,0 +1,80 @@
"""Diagnostics support for Miele."""
from __future__ import annotations
import hashlib
from typing import Any, cast
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from .coordinator import MieleConfigEntry
TO_REDACT = {"access_token", "refresh_token", "fabNumber"}
def hash_identifier(key: str) -> str:
"""Hash the identifier string."""
return f"**REDACTED_{hashlib.sha256(key.encode()).hexdigest()[:16]}"
def redact_identifiers(in_data: dict[str, Any]) -> dict[str, Any]:
"""Redact identifiers from the data."""
for key in in_data:
in_data[hash_identifier(key)] = in_data.pop(key)
return in_data
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: MieleConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
miele_data = {
"devices": redact_identifiers(
{
device_id: device_data.raw
for device_id, device_data in config_entry.runtime_data.data.devices.items()
}
),
"actions": redact_identifiers(
{
device_id: action_data.raw
for device_id, action_data in config_entry.runtime_data.data.actions.items()
}
),
}
return {
"config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT),
"miele_data": async_redact_data(miele_data, TO_REDACT),
}
async def async_get_device_diagnostics(
hass: HomeAssistant, config_entry: MieleConfigEntry, device: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a device."""
info = {
"manufacturer": device.manufacturer,
"model": device.model,
}
coordinator = config_entry.runtime_data
device_id = cast(str, device.serial_number)
miele_data = {
"devices": {
hash_identifier(device_id): coordinator.data.devices[device_id].raw
},
"actions": {
hash_identifier(device_id): coordinator.data.actions[device_id].raw
},
"programs": "Not implemented",
}
return {
"info": async_redact_data(info, TO_REDACT),
"data": async_redact_data(config_entry.data, TO_REDACT),
"miele_data": async_redact_data(miele_data, TO_REDACT),
}

View File

@ -17,7 +17,7 @@ from homeassistant.setup import async_setup_component
from .const import CLIENT_ID, CLIENT_SECRET
from tests.common import MockConfigEntry, load_json_object_fixture
from tests.common import MockConfigEntry, load_fixture, load_json_object_fixture
@pytest.fixture(name="expires_at")
@ -91,10 +91,23 @@ def action_fixture(load_action_file: str) -> MieleAction:
return load_json_object_fixture(load_action_file, DOMAIN)
@pytest.fixture(scope="package")
def load_programs_file() -> str:
"""Fixture for loading programs file."""
return "programs_washing_machine.json"
@pytest.fixture
def programs_fixture(load_programs_file: str) -> list[dict]:
"""Fixture for available programs."""
return load_fixture(load_programs_file, DOMAIN)
@pytest.fixture
def mock_miele_client(
device_fixture,
action_fixture,
programs_fixture,
) -> Generator[MagicMock]:
"""Mock a Miele client."""
@ -106,6 +119,7 @@ def mock_miele_client(
client.get_devices.return_value = device_fixture
client.get_actions.return_value = action_fixture
client.get_programs.return_value = programs_fixture
yield client

View File

@ -352,7 +352,18 @@
"key_localized": "Fan level"
},
"plateStep": [],
"ecoFeedback": null,
"ecoFeedback": {
"currentWaterConsumption": {
"unit": "l",
"value": 0.0
},
"currentEnergyConsumption": {
"unit": "kWh",
"value": 0.0
},
"waterForecast": 0.0,
"energyForecast": 0.1
},
"batteryLevel": null
}
}

View File

@ -0,0 +1,117 @@
[
{
"programId": 146,
"program": "QuickPowerWash",
"parameters": {}
},
{
"programId": 123,
"program": "Dark garments / Denim",
"parameters": {}
},
{
"programId": 190,
"program": "ECO 40-60 ",
"parameters": {}
},
{
"programId": 27,
"program": "Proofing",
"parameters": {}
},
{
"programId": 23,
"program": "Shirts",
"parameters": {}
},
{
"programId": 9,
"program": "Silks ",
"parameters": {}
},
{
"programId": 8,
"program": "Woollens ",
"parameters": {}
},
{
"programId": 4,
"program": "Delicates",
"parameters": {}
},
{
"programId": 3,
"program": "Minimum iron",
"parameters": {}
},
{
"programId": 1,
"program": "Cottons",
"parameters": {}
},
{
"programId": 69,
"program": "Cottons hygiene",
"parameters": {}
},
{
"programId": 37,
"program": "Outerwear",
"parameters": {}
},
{
"programId": 122,
"program": "Express 20",
"parameters": {}
},
{
"programId": 29,
"program": "Sportswear",
"parameters": {}
},
{
"programId": 31,
"program": "Automatic plus",
"parameters": {}
},
{
"programId": 39,
"program": "Pillows",
"parameters": {}
},
{
"programId": 22,
"program": "Curtains",
"parameters": {}
},
{
"programId": 129,
"program": "Down filled items",
"parameters": {}
},
{
"programId": 53,
"program": "First wash",
"parameters": {}
},
{
"programId": 95,
"program": "Down duvets",
"parameters": {}
},
{
"programId": 52,
"program": "Separate rinse / Starch",
"parameters": {}
},
{
"programId": 21,
"program": "Drain / Spin",
"parameters": {}
},
{
"programId": 91,
"program": "Clean machine",
"parameters": {}
}
]

View File

@ -0,0 +1,670 @@
# serializer version: 1
# name: test_diagnostics_config_entry
dict({
'config_entry_data': dict({
'auth_implementation': 'miele',
'token': dict({
'access_token': '**REDACTED**',
'expires_in': 86399,
'refresh_token': '**REDACTED**',
'token_type': 'Bearer',
}),
}),
'miele_data': dict({
'actions': dict({
'**REDACTED_019aa577ad1c330d': dict({
'ambientLight': list([
]),
'colors': list([
]),
'deviceName': True,
'light': list([
]),
'modes': list([
]),
'powerOff': False,
'powerOn': True,
'processAction': list([
]),
'programId': list([
]),
'runOnTime': list([
]),
'startTime': list([
]),
'targetTemperature': list([
]),
'ventilationStep': list([
]),
}),
'**REDACTED_57d53e72806e88b4': dict({
'ambientLight': list([
]),
'colors': list([
]),
'deviceName': True,
'light': list([
]),
'modes': list([
]),
'powerOff': False,
'powerOn': True,
'processAction': list([
]),
'programId': list([
]),
'runOnTime': list([
]),
'startTime': list([
]),
'targetTemperature': list([
]),
'ventilationStep': list([
]),
}),
'**REDACTED_c9fe55cdf70786ca': dict({
'ambientLight': list([
]),
'colors': list([
]),
'deviceName': True,
'light': list([
]),
'modes': list([
]),
'powerOff': False,
'powerOn': True,
'processAction': list([
]),
'programId': list([
]),
'runOnTime': list([
]),
'startTime': list([
]),
'targetTemperature': list([
]),
'ventilationStep': list([
]),
}),
}),
'devices': dict({
'**REDACTED_019aa577ad1c330d': dict({
'ident': dict({
'deviceIdentLabel': dict({
'fabIndex': '17',
'fabNumber': '**REDACTED**',
'matNumber': '10804770',
'swids': list([
'4497',
]),
'techType': 'KS 28423 D ed/c',
}),
'deviceName': '',
'protocolVersion': 201,
'type': dict({
'key_localized': 'Device type',
'value_localized': 'Refrigerator',
'value_raw': 19,
}),
'xkmIdentLabel': dict({
'releaseVersion': '31.17',
'techType': 'EK042',
}),
}),
'state': dict({
'ProgramID': dict({
'key_localized': 'Program name',
'value_localized': '',
'value_raw': 0,
}),
'ambientLight': None,
'batteryLevel': None,
'coreTargetTemperature': list([
]),
'coreTemperature': list([
]),
'dryingStep': dict({
'key_localized': 'Drying level',
'value_localized': '',
'value_raw': None,
}),
'ecoFeedback': None,
'elapsedTime': list([
]),
'light': None,
'plateStep': list([
]),
'programPhase': dict({
'key_localized': 'Program phase',
'value_localized': '',
'value_raw': 0,
}),
'programType': dict({
'key_localized': 'Program type',
'value_localized': '',
'value_raw': 0,
}),
'remainingTime': list([
0,
0,
]),
'remoteEnable': dict({
'fullRemoteControl': True,
'mobileStart': False,
'smartGrid': False,
}),
'signalDoor': False,
'signalFailure': False,
'signalInfo': False,
'spinningSpeed': dict({
'key_localized': 'Spin speed',
'unit': 'rpm',
'value_localized': None,
'value_raw': None,
}),
'startTime': list([
0,
0,
]),
'status': dict({
'key_localized': 'status',
'value_localized': 'In use',
'value_raw': 5,
}),
'targetTemperature': list([
dict({
'unit': 'Celsius',
'value_localized': 4,
'value_raw': 400,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
]),
'temperature': list([
dict({
'unit': 'Celsius',
'value_localized': 4,
'value_raw': 400,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
]),
'ventilationStep': dict({
'key_localized': 'Fan level',
'value_localized': '',
'value_raw': None,
}),
}),
}),
'**REDACTED_57d53e72806e88b4': dict({
'ident': dict({
'deviceIdentLabel': dict({
'fabIndex': '21',
'fabNumber': '**REDACTED**',
'matNumber': '10805070',
'swids': list([
'4497',
]),
'techType': 'FNS 28463 E ed/',
}),
'deviceName': '',
'protocolVersion': 201,
'type': dict({
'key_localized': 'Device type',
'value_localized': 'Freezer',
'value_raw': 20,
}),
'xkmIdentLabel': dict({
'releaseVersion': '31.17',
'techType': 'EK042',
}),
}),
'state': dict({
'ProgramID': dict({
'key_localized': 'Program name',
'value_localized': '',
'value_raw': 0,
}),
'ambientLight': None,
'batteryLevel': None,
'coreTargetTemperature': list([
]),
'coreTemperature': list([
]),
'dryingStep': dict({
'key_localized': 'Drying level',
'value_localized': '',
'value_raw': None,
}),
'ecoFeedback': None,
'elapsedTime': list([
]),
'light': None,
'plateStep': list([
]),
'programPhase': dict({
'key_localized': 'Program phase',
'value_localized': '',
'value_raw': 0,
}),
'programType': dict({
'key_localized': 'Program type',
'value_localized': '',
'value_raw': 0,
}),
'remainingTime': list([
0,
0,
]),
'remoteEnable': dict({
'fullRemoteControl': True,
'mobileStart': False,
'smartGrid': False,
}),
'signalDoor': False,
'signalFailure': False,
'signalInfo': False,
'spinningSpeed': dict({
'key_localized': 'Spin speed',
'unit': 'rpm',
'value_localized': None,
'value_raw': None,
}),
'startTime': list([
0,
0,
]),
'status': dict({
'key_localized': 'status',
'value_localized': 'In use',
'value_raw': 5,
}),
'targetTemperature': list([
dict({
'unit': 'Celsius',
'value_localized': -18,
'value_raw': -1800,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
]),
'temperature': list([
dict({
'unit': 'Celsius',
'value_localized': -18,
'value_raw': -1800,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
]),
'ventilationStep': dict({
'key_localized': 'Fan level',
'value_localized': '',
'value_raw': None,
}),
}),
}),
'**REDACTED_c9fe55cdf70786ca': dict({
'ident': dict({
'deviceIdentLabel': dict({
'fabIndex': '44',
'fabNumber': '**REDACTED**',
'matNumber': '11387290',
'swids': list([
'5975',
'20456',
'25213',
'25191',
'25446',
'25205',
'25447',
'25319',
]),
'techType': 'WCI870',
}),
'deviceName': '',
'protocolVersion': 4,
'type': dict({
'key_localized': 'Device type',
'value_localized': 'Washing machine',
'value_raw': 1,
}),
'xkmIdentLabel': dict({
'releaseVersion': '08.32',
'techType': 'EK057',
}),
}),
'state': dict({
'ProgramID': dict({
'key_localized': 'Program name',
'value_localized': '',
'value_raw': 0,
}),
'ambientLight': None,
'batteryLevel': None,
'coreTargetTemperature': list([
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
]),
'coreTemperature': list([
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
]),
'dryingStep': dict({
'key_localized': 'Drying level',
'value_localized': '',
'value_raw': None,
}),
'ecoFeedback': dict({
'currentEnergyConsumption': dict({
'unit': 'kWh',
'value': 0.0,
}),
'currentWaterConsumption': dict({
'unit': 'l',
'value': 0.0,
}),
'energyForecast': 0.1,
'waterForecast': 0.0,
}),
'elapsedTime': list([
0,
0,
]),
'light': None,
'plateStep': list([
]),
'programPhase': dict({
'key_localized': 'Program phase',
'value_localized': '',
'value_raw': 0,
}),
'programType': dict({
'key_localized': 'Program type',
'value_localized': '',
'value_raw': 0,
}),
'remainingTime': list([
0,
0,
]),
'remoteEnable': dict({
'fullRemoteControl': True,
'mobileStart': False,
'smartGrid': False,
}),
'signalDoor': True,
'signalFailure': False,
'signalInfo': False,
'spinningSpeed': dict({
'key_localized': 'Spin speed',
'unit': 'rpm',
'value_localized': None,
'value_raw': None,
}),
'startTime': list([
0,
0,
]),
'status': dict({
'key_localized': 'status',
'value_localized': 'Off',
'value_raw': 1,
}),
'targetTemperature': list([
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
]),
'temperature': list([
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
]),
'ventilationStep': dict({
'key_localized': 'Fan level',
'value_localized': '',
'value_raw': None,
}),
}),
}),
}),
}),
})
# ---
# name: test_diagnostics_device
dict({
'data': dict({
'auth_implementation': 'miele',
'token': dict({
'access_token': '**REDACTED**',
'expires_in': 86399,
'refresh_token': '**REDACTED**',
'token_type': 'Bearer',
}),
}),
'info': dict({
'manufacturer': 'Miele',
'model': 'FNS 28463 E ed/',
}),
'miele_data': dict({
'actions': dict({
'**REDACTED_57d53e72806e88b4': dict({
'ambientLight': list([
]),
'colors': list([
]),
'deviceName': True,
'light': list([
]),
'modes': list([
]),
'powerOff': False,
'powerOn': True,
'processAction': list([
]),
'programId': list([
]),
'runOnTime': list([
]),
'startTime': list([
]),
'targetTemperature': list([
]),
'ventilationStep': list([
]),
}),
}),
'devices': dict({
'**REDACTED_57d53e72806e88b4': dict({
'ident': dict({
'deviceIdentLabel': dict({
'fabIndex': '21',
'fabNumber': '**REDACTED**',
'matNumber': '10805070',
'swids': list([
'4497',
]),
'techType': 'FNS 28463 E ed/',
}),
'deviceName': '',
'protocolVersion': 201,
'type': dict({
'key_localized': 'Device type',
'value_localized': 'Freezer',
'value_raw': 20,
}),
'xkmIdentLabel': dict({
'releaseVersion': '31.17',
'techType': 'EK042',
}),
}),
'state': dict({
'ProgramID': dict({
'key_localized': 'Program name',
'value_localized': '',
'value_raw': 0,
}),
'ambientLight': None,
'batteryLevel': None,
'coreTargetTemperature': list([
]),
'coreTemperature': list([
]),
'dryingStep': dict({
'key_localized': 'Drying level',
'value_localized': '',
'value_raw': None,
}),
'ecoFeedback': None,
'elapsedTime': list([
]),
'light': None,
'plateStep': list([
]),
'programPhase': dict({
'key_localized': 'Program phase',
'value_localized': '',
'value_raw': 0,
}),
'programType': dict({
'key_localized': 'Program type',
'value_localized': '',
'value_raw': 0,
}),
'remainingTime': list([
0,
0,
]),
'remoteEnable': dict({
'fullRemoteControl': True,
'mobileStart': False,
'smartGrid': False,
}),
'signalDoor': False,
'signalFailure': False,
'signalInfo': False,
'spinningSpeed': dict({
'key_localized': 'Spin speed',
'unit': 'rpm',
'value_localized': None,
'value_raw': None,
}),
'startTime': list([
0,
0,
]),
'status': dict({
'key_localized': 'status',
'value_localized': 'In use',
'value_raw': 5,
}),
'targetTemperature': list([
dict({
'unit': 'Celsius',
'value_localized': -18,
'value_raw': -1800,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
]),
'temperature': list([
dict({
'unit': 'Celsius',
'value_localized': -18,
'value_raw': -1800,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
dict({
'unit': 'Celsius',
'value_localized': None,
'value_raw': -32768,
}),
]),
'ventilationStep': dict({
'key_localized': 'Fan level',
'value_localized': '',
'value_raw': None,
}),
}),
}),
}),
'programs': 'Not implemented',
}),
})
# ---

View File

@ -0,0 +1,69 @@
"""Tests for the diagnostics data provided by the miele integration."""
from collections.abc import Generator
from unittest.mock import MagicMock
from syrupy import SnapshotAssertion
from syrupy.filters import paths
from homeassistant.components.miele.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceRegistry
from . import setup_integration
from tests.common import MockConfigEntry
from tests.components.diagnostics import (
get_diagnostics_for_config_entry,
get_diagnostics_for_device,
)
from tests.typing import ClientSessionGenerator
async def test_diagnostics_config_entry(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_miele_client: Generator[MagicMock],
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test diagnostics for config entry."""
await setup_integration(hass, mock_config_entry)
result = await get_diagnostics_for_config_entry(
hass, hass_client, mock_config_entry
)
assert result == snapshot(
exclude=paths(
"config_entry_data.token.expires_at",
"miele_test.entry_id",
)
)
async def test_diagnostics_device(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
device_registry: DeviceRegistry,
mock_miele_client: Generator[MagicMock],
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test diagnostics for device."""
TEST_DEVICE = "Dummy_Appliance_1"
await setup_integration(hass, mock_config_entry)
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, TEST_DEVICE)})
assert device_entry is not None
result = await get_diagnostics_for_device(
hass, hass_client, mock_config_entry, device_entry
)
assert result == snapshot(
exclude=paths(
"data.token.expires_at",
"miele_test.entry_id",
)
)