Miele - fix core temperature reading (#167476)

This commit is contained in:
Andrea Turri
2026-04-06 12:08:29 +02:00
committed by GitHub
parent 4ad2f752a3
commit 0eaa8d38db
3 changed files with 141 additions and 6 deletions

View File

@@ -19,9 +19,13 @@ LIGHT = "light"
LIGHT_ON = 1
LIGHT_OFF = 2
# API "no reading" sentinels. Most temperatures use centidegrees (-32768 -> -327.68 °C).
# Some devices report the int16 minimum already in degrees after scaling (-3276800 raw -> -32768 °C).
DISABLED_TEMP_ENTITIES = (
-32768 / 100,
-32766 / 100,
-32768.0,
-32766.0,
)

View File

@@ -93,7 +93,14 @@ def _convert_temperature(
"""Convert temperature object to readable value."""
if index >= len(value_list):
return None
raw_value = cast(int, value_list[index].temperature) / 100.0
raw = value_list[index].temperature
if raw is None:
return None
try:
raw_centi = int(raw)
except TypeError, ValueError:
return None
raw_value = raw_centi / 100.0
if raw_value in DISABLED_TEMP_ENTITIES:
return None
return raw_value
@@ -639,6 +646,7 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition[MieleDevice], ...]] = (
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_core_temperature",
@@ -840,9 +848,9 @@ async def async_setup_entry(
and definition.description.value_fn(device) is None
and definition.description.zone != 1
):
# all appliances supporting temperature have at least zone 1, for other zones
# don't create entity if API signals that datapoint is disabled, unless the sensor
# already appeared in the past (= it provided a valid value)
# Optional temperature datapoints (extra fridge zones, oven food probe): only
# create the entity after the API first reports a valid reading, then keep it
# so state can return to unknown when the datapoint is inactive.
return _is_entity_registered(unique_id)
if (
definition.description.key == "state_plate_step"

View File

@@ -4,11 +4,12 @@ from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory
from pymiele import MieleDevices
from pymiele import MieleDevices, MieleTemperature
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.miele.const import DOMAIN
from homeassistant.components.miele.sensor import _convert_temperature
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
@@ -96,7 +97,7 @@ async def test_oven_temperatures_scenario(
) -> None:
"""Parametrized test for verifying temperature sensors for oven devices."""
# Initial state when the oven is and created for the first time - don't know if it supports core temperature (probe)
# Initial state when the oven is created for the first time — no core probe entities yet
check_sensor_state(hass, "sensor.oven_temperature", "unknown", 0)
check_sensor_state(hass, "sensor.oven_target_temperature", "unknown", 0)
check_sensor_state(hass, "sensor.oven_core_temperature", None, 0)
@@ -206,6 +207,95 @@ def check_sensor_state(
)
@pytest.mark.parametrize("load_device_file", ["oven.json"])
@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)])
async def test_oven_core_probe_sensors_unknown_when_inactive(
hass: HomeAssistant,
mock_miele_client: MagicMock,
setup_platform: None,
device_fixture: MieleDevices,
freezer: FrozenDateTimeFactory,
) -> None:
"""Oven food-probe (core) sensors must not expose API inactive sentinels as temperatures.
Miele uses raw value -32768 (centidegrees) when the probe is not in use. After the
probe has reported a valid reading once, those entities must stay in the UI but
their state must be unknown—not a bogus numeric temperature.
"""
core_temp = "sensor.oven_core_temperature"
core_target = "sensor.oven_core_target_temperature"
assert hass.states.get(core_temp) is None
assert hass.states.get(core_target) is None
device_fixture["DummyOven"]["state"]["coreTargetTemperature"][0]["value_raw"] = 3000
device_fixture["DummyOven"]["state"]["coreTargetTemperature"][0][
"value_localized"
] = 30.0
device_fixture["DummyOven"]["state"]["coreTemperature"][0]["value_raw"] = 2200
device_fixture["DummyOven"]["state"]["coreTemperature"][0]["value_localized"] = 22.0
freezer.tick(timedelta(seconds=130))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(core_temp) is not None
assert hass.states.get(core_temp).state == "22.0"
assert hass.states.get(core_target) is not None
assert hass.states.get(core_target).state == "30.0"
device_fixture["DummyOven"]["state"]["coreTargetTemperature"][0][
"value_raw"
] = -32768
device_fixture["DummyOven"]["state"]["coreTargetTemperature"][0][
"value_localized"
] = None
device_fixture["DummyOven"]["state"]["coreTemperature"][0]["value_raw"] = -32768
device_fixture["DummyOven"]["state"]["coreTemperature"][0]["value_localized"] = None
freezer.tick(timedelta(seconds=130))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(core_temp).state == STATE_UNKNOWN
assert hass.states.get(core_target).state == STATE_UNKNOWN
@pytest.mark.parametrize("load_device_file", ["oven.json"])
@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)])
async def test_oven_core_probe_unknown_when_inactive_raw_scaled(
hass: HomeAssistant,
mock_miele_client: MagicMock,
setup_platform: None,
device_fixture: MieleDevices,
freezer: FrozenDateTimeFactory,
) -> None:
"""Some ovens report int16-min as centidegrees (-32768 -> -327.68 °C); others as -3276800 raw (-32768 °C).
Both must map to unknown, not a numeric sensor state.
"""
core_temp = "sensor.oven_core_temperature"
device_fixture["DummyOven"]["state"]["coreTemperature"][0]["value_raw"] = 2200
device_fixture["DummyOven"]["state"]["coreTemperature"][0]["value_localized"] = 22.0
freezer.tick(timedelta(seconds=130))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(core_temp) is not None
assert hass.states.get(core_temp).state == "22.0"
device_fixture["DummyOven"]["state"]["coreTemperature"][0]["value_raw"] = -3276800
device_fixture["DummyOven"]["state"]["coreTemperature"][0]["value_localized"] = None
freezer.tick(timedelta(seconds=130))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(core_temp).state == STATE_UNKNOWN
@pytest.mark.parametrize("load_device_file", ["oven.json"])
@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)])
async def test_temperature_sensor_registry_lookup(
@@ -747,3 +837,36 @@ async def test_elapsed_time_sensor_restored(
state = hass.states.get(entity_id_abs)
assert state is not None
assert state.state == "2025-05-31T14:15:00+00:00"
def _core_temperature_entry(value_raw: object | None) -> MieleTemperature:
"""Build a MieleTemperature like the API returns for core/zone readings."""
return MieleTemperature({"value_raw": value_raw})
@pytest.mark.parametrize(
("entries", "index", "expected"),
[
([], 0, None),
([_core_temperature_entry(2200)], 1, None),
([_core_temperature_entry(None)], 0, None),
([_core_temperature_entry(-32768)], 0, None),
([_core_temperature_entry(-32766)], 0, None),
([_core_temperature_entry(-3276800)], 0, None),
([_core_temperature_entry(-3276600)], 0, None),
([_core_temperature_entry(2150)], 0, 21.5),
],
)
def test_convert_temperature(
entries: list[MieleTemperature],
index: int,
expected: float | None,
) -> None:
"""Cover _convert_temperature branches (sentinels, scaling, bounds, valid values)."""
assert _convert_temperature(entries, index) == expected
def test_convert_temperature_invalid_raw_types() -> None:
"""int() must not raise: bad API payloads become unknown."""
assert _convert_temperature([_core_temperature_entry("n/a")], 0) is None
assert _convert_temperature([_core_temperature_entry([1])], 0) is None