diff --git a/homeassistant/components/midea/icons.json b/homeassistant/components/midea/icons.json index 1d09a42c062e..a80e1afe3735 100644 --- a/homeassistant/components/midea/icons.json +++ b/homeassistant/components/midea/icons.json @@ -73,6 +73,9 @@ }, "target_indoor_fan_speed": { "default": "mdi:fan-clock" + }, + "time_remaining": { + "default": "mdi:progress-clock" } }, "switch": { diff --git a/homeassistant/components/midea/sensor.py b/homeassistant/components/midea/sensor.py index b61d3bfeb50e..d426cad11ff5 100644 --- a/homeassistant/components/midea/sensor.py +++ b/homeassistant/components/midea/sensor.py @@ -1,6 +1,7 @@ """Midea Sensor entities.""" from dataclasses import dataclass +from datetime import datetime, timedelta from typing import cast, override from midealocal.const import DeviceType @@ -31,6 +32,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.util import dt as dt_util from .entity import MideaConfigEntry, MideaEntity @@ -469,6 +471,17 @@ SENSOR_ENTITIES: list[MideaSensorEntityDescription] = [ "freeze_warm", ], ), + MideaSensorEntityDescription( + key="time_remaining", + translation_key="time_remaining", + device_class=SensorDeviceClass.TIMESTAMP, + ), + MideaSensorEntityDescription( + key="heating_time_remaining", + translation_key="time_remaining", + device_class=SensorDeviceClass.TIMESTAMP, + models=[DeviceType.E2], + ), MideaSensorEntityDescription( key="wash_time", translation_key="wash_time", @@ -710,9 +723,16 @@ class MideaSensor(MideaEntity, SensorEntity): @property @override - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Native value of the sensor.""" value = self._device.get_attribute(self.entity_description.key) if value == "unknown": return None + if self.entity_description.device_class == SensorDeviceClass.TIMESTAMP: + if not isinstance(value, (int, float)) or value <= 0: + return None + # round to the closest minute + return (dt_util.utcnow() + timedelta(seconds=30)).replace( + second=0, microsecond=0 + ) + timedelta(minutes=value) return cast("StateType", value) diff --git a/homeassistant/components/midea/strings.json b/homeassistant/components/midea/strings.json index fc64be64d832..4dd19acf76bc 100644 --- a/homeassistant/components/midea/strings.json +++ b/homeassistant/components/midea/strings.json @@ -629,6 +629,9 @@ "temperature_raw": { "name": "Raw temperature" }, + "time_remaining": { + "name": "Time remaining" + }, "top_compartment_temperature": { "name": "Top compartment temperature" }, diff --git a/tests/components/midea/snapshots/test_sensor.ambr b/tests/components/midea/snapshots/test_sensor.ambr index aa201b3c68e6..8136f3cf1c02 100644 --- a/tests/components/midea/snapshots/test_sensor.ambr +++ b/tests/components/midea/snapshots/test_sensor.ambr @@ -762,6 +762,57 @@ 'state': '21.0', }) # --- +# name: test_all_entities[e8][sensor.electric_slow_cooker_time_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.electric_slow_cooker_time_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Time remaining', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Time remaining', + 'platform': 'midea', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time_remaining', + 'unique_id': '12345678_time_remaining', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[e8][sensor.electric_slow_cooker_time_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'timestamp', + : 'Electric Slow Cooker Time remaining', + }), + 'context': , + 'entity_id': 'sensor.electric_slow_cooker_time_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2026-01-09T13:00:00+00:00', + }) +# --- # name: test_all_entities[ea][sensor.electric_rice_cooker_bottom_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -1126,6 +1177,57 @@ 'state': 'heat_rice', }) # --- +# name: test_all_entities[ea][sensor.electric_rice_cooker_time_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.electric_rice_cooker_time_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Time remaining', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Time remaining', + 'platform': 'midea', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time_remaining', + 'unique_id': '12345678_time_remaining', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[ea][sensor.electric_rice_cooker_time_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'timestamp', + : 'Electric Rice Cooker Time remaining', + }), + 'context': , + 'entity_id': 'sensor.electric_rice_cooker_time_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2026-01-09T12:05:00+00:00', + }) +# --- # name: test_all_entities[ea][sensor.electric_rice_cooker_top_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -1492,6 +1594,57 @@ 'state': 'diy', }) # --- +# name: test_all_entities[ec][sensor.electric_pressure_cooker_time_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.electric_pressure_cooker_time_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Time remaining', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Time remaining', + 'platform': 'midea', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time_remaining', + 'unique_id': '12345678_time_remaining', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[ec][sensor.electric_pressure_cooker_time_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'timestamp', + : 'Electric Pressure Cooker Time remaining', + }), + 'context': , + 'entity_id': 'sensor.electric_pressure_cooker_time_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2026-01-09T12:05:00+00:00', + }) +# --- # name: test_all_entities[ec][sensor.electric_pressure_cooker_top_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/midea/test_sensor.py b/tests/components/midea/test_sensor.py index 72ad01a4da27..a4e7da803bf0 100644 --- a/tests/components/midea/test_sensor.py +++ b/tests/components/midea/test_sensor.py @@ -3,6 +3,7 @@ from collections.abc import Callable from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from midealocal.const import DeviceType from midealocal.devices.ac import DeviceAttributes as ACAttributes from midealocal.devices.c3 import DeviceAttributes as C3Attributes @@ -72,7 +73,7 @@ from tests.common import MockConfigEntry, snapshot_platform DeviceType.E8, attributes={ E8Attributes.status: 1, - E8Attributes.time_remaining: 3600, + E8Attributes.time_remaining: 60, E8Attributes.keep_warm_remaining: 1800, E8Attributes.working_time: 7200, E8Attributes.target_temperature: 22.0, @@ -179,12 +180,14 @@ from tests.common import MockConfigEntry, snapshot_platform ) async def test_all_entities( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, device: DummyDevice, mock_config_entry: Callable[[DummyDevice], MockConfigEntry], snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, ) -> None: """Test sensor entities are created.""" + freezer.move_to("2026-01-09 12:00:00+00:00") config_entry = mock_config_entry(device) with patch("homeassistant.components.midea._PLATFORMS", [Platform.SENSOR]): await setup_integration(hass, config_entry, device) @@ -249,6 +252,41 @@ async def test_sensor_state_update( assert state.state == "unknown" +async def test_time_remaining_sensor_as_timestamp( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + set_device_attribute: SetDeviceAttribute, + mock_config_entry: Callable[[DummyDevice], MockConfigEntry], +) -> None: + """Test time_remaining is reported as an absolute timestamp in minutes.""" + freezer.move_to("2026-01-09 12:00:00+00:00") + device = DummyDevice( + DeviceType.E8, + attributes={E8Attributes.time_remaining: 90}, + ) + config_entry = mock_config_entry(device) + with patch("homeassistant.components.midea._PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, config_entry, device) + + entity_entry = entity_entries(hass, config_entry)[ + f"{TEST_DEVICE_ID}_time_remaining" + ] + + state = hass.states.get(entity_entry.entity_id) + assert state is not None + assert state.state == "2026-01-09T13:30:00+00:00" + + await set_device_attribute(device, E8Attributes.time_remaining, 0) + state = hass.states.get(entity_entry.entity_id) + assert state is not None + assert state.state == "unknown" + + await set_device_attribute(device, E8Attributes.time_remaining, None) + state = hass.states.get(entity_entry.entity_id) + assert state is not None + assert state.state == "unknown" + + @pytest.mark.parametrize( ("model", "expected_options"), [