diff --git a/homeassistant/components/miele/binary_sensor.py b/homeassistant/components/miele/binary_sensor.py index 5eb9eccc5df361..b56dbf52e42538 100644 --- a/homeassistant/components/miele/binary_sensor.py +++ b/homeassistant/components/miele/binary_sensor.py @@ -264,12 +264,16 @@ async def async_setup_entry( """Set up the binary sensor platform.""" coordinator = config_entry.runtime_data - async_add_entities( - MieleBinarySensor(coordinator, device_id, definition.description) - for device_id, device in coordinator.data.devices.items() - for definition in BINARY_SENSOR_TYPES - if device.device_type in definition.types - ) + def _async_add_new_devices(new_devices: dict[str, MieleDevice]) -> None: + async_add_entities( + MieleBinarySensor(coordinator, device_id, definition.description) + for device_id, device in new_devices.items() + for definition in BINARY_SENSOR_TYPES + if device.device_type in definition.types + ) + + coordinator.new_device_callbacks.append(_async_add_new_devices) + _async_add_new_devices(coordinator.data.devices) class MieleBinarySensor(MieleEntity, BinarySensorEntity): diff --git a/homeassistant/components/miele/button.py b/homeassistant/components/miele/button.py index e4aacc5124cba7..8b7eeb9ad87fbf 100644 --- a/homeassistant/components/miele/button.py +++ b/homeassistant/components/miele/button.py @@ -7,6 +7,7 @@ from typing import Final import aiohttp +from pymiele import MieleDevice from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.core import HomeAssistant @@ -112,12 +113,16 @@ async def async_setup_entry( """Set up the button platform.""" coordinator = config_entry.runtime_data - async_add_entities( - MieleButton(coordinator, device_id, definition.description) - for device_id, device in coordinator.data.devices.items() - for definition in BUTTON_TYPES - if device.device_type in definition.types - ) + def _async_add_new_devices(new_devices: dict[str, MieleDevice]) -> None: + async_add_entities( + MieleButton(coordinator, device_id, definition.description) + for device_id, device in new_devices.items() + for definition in BUTTON_TYPES + if device.device_type in definition.types + ) + + coordinator.new_device_callbacks.append(_async_add_new_devices) + _async_add_new_devices(coordinator.data.devices) class MieleButton(MieleEntity, ButtonEntity): diff --git a/homeassistant/components/miele/climate.py b/homeassistant/components/miele/climate.py index 3b591965d2f9b8..6026b7ab8dc11d 100644 --- a/homeassistant/components/miele/climate.py +++ b/homeassistant/components/miele/climate.py @@ -132,15 +132,22 @@ async def async_setup_entry( """Set up the climate platform.""" coordinator = config_entry.runtime_data - async_add_entities( - MieleClimate(coordinator, device_id, definition.description) - for device_id, device in coordinator.data.devices.items() - for definition in CLIMATE_TYPES - if ( - device.device_type in definition.types - and (definition.description.value_fn(device) not in DISABLED_TEMP_ENTITIES) + def _async_add_new_devices(new_devices: dict[str, MieleDevice]) -> None: + async_add_entities( + MieleClimate(coordinator, device_id, definition.description) + for device_id, device in new_devices.items() + for definition in CLIMATE_TYPES + if ( + device.device_type in definition.types + and ( + definition.description.value_fn(device) + not in DISABLED_TEMP_ENTITIES + ) + ) ) - ) + + coordinator.new_device_callbacks.append(_async_add_new_devices) + _async_add_new_devices(coordinator.data.devices) class MieleClimate(MieleEntity, ClimateEntity): diff --git a/homeassistant/components/miele/coordinator.py b/homeassistant/components/miele/coordinator.py index 8902f0f173ae15..2a9a556092174c 100644 --- a/homeassistant/components/miele/coordinator.py +++ b/homeassistant/components/miele/coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio.timeouts +from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta import logging @@ -33,6 +34,10 @@ class MieleCoordinatorData: class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]): """Coordinator for Miele data.""" + new_device_callbacks: list[Callable[[dict[str, MieleDevice]], None]] = [] + known_devices: set[str] = set() + devices: dict[str, MieleDevice] = {} + def __init__( self, hass: HomeAssistant, @@ -60,8 +65,24 @@ async def _async_update_data(self) -> MieleCoordinatorData: for device_id in devices: actions_json = await self.api.get_actions(device_id) actions[device_id] = MieleAction(actions_json) + self.devices = devices + self._async_add_devices() return MieleCoordinatorData(devices=devices, actions=actions) + def _async_add_devices(self) -> None: + """Add new devices.""" + current_devices = set(self.devices) + new_devices = current_devices - self.known_devices + if new_devices: + self.known_devices.update(new_devices) + for callback in self.new_device_callbacks: + callback( + { + device_id: self.data.devices[device_id] + for device_id in new_devices + } + ) + async def callback_update_data(self, devices_json: dict[str, dict]) -> None: """Handle data update from the API.""" devices = { diff --git a/homeassistant/components/miele/light.py b/homeassistant/components/miele/light.py index 0fbc8124be88d4..0de6befa98fe7c 100644 --- a/homeassistant/components/miele/light.py +++ b/homeassistant/components/miele/light.py @@ -86,12 +86,16 @@ async def async_setup_entry( """Set up the light platform.""" coordinator = config_entry.runtime_data - async_add_entities( - MieleLight(coordinator, device_id, definition.description) - for device_id, device in coordinator.data.devices.items() - for definition in LIGHT_TYPES - if device.device_type in definition.types - ) + def _async_add_new_devices(new_devices: dict[str, MieleDevice]) -> None: + async_add_entities( + MieleLight(coordinator, device_id, definition.description) + for device_id, device in new_devices.items() + for definition in LIGHT_TYPES + if device.device_type in definition.types + ) + + coordinator.new_device_callbacks.append(_async_add_new_devices) + _async_add_new_devices(coordinator.data.devices) class MieleLight(MieleEntity, LightEntity): diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index b2ddd695042487..5efd7ff9b65910 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -428,34 +428,39 @@ async def async_setup_entry( """Set up the sensor platform.""" coordinator = config_entry.runtime_data - entities: list = [] - entity_class: type[MieleSensor] - for device_id, device in coordinator.data.devices.items(): - for definition in SENSOR_TYPES: - if device.device_type in definition.types: - match definition.description.key: - case "state_status": - entity_class = MieleStatusSensor - case "state_program_id": - entity_class = MieleProgramIdSensor - case "state_program_phase": - entity_class = MielePhaseSensor - case "state_program_type": - entity_class = MieleTypeSensor - case _: - entity_class = MieleSensor - if ( - definition.description.device_class == SensorDeviceClass.TEMPERATURE - and definition.description.value_fn(device) - == DISABLED_TEMPERATURE / 100 - ): - # Don't create entity if API signals that datapoint is disabled - continue - entities.append( - entity_class(coordinator, device_id, definition.description) - ) - - async_add_entities(entities) + def _async_add_new_devices(new_devices: dict[str, MieleDevice]) -> None: + entities: list = [] + entity_class: type[MieleSensor] + for device_id, device in new_devices.items(): + for definition in SENSOR_TYPES: + if device.device_type in definition.types: + match definition.description.key: + case "state_status": + entity_class = MieleStatusSensor + case "state_program_id": + entity_class = MieleProgramIdSensor + case "state_program_phase": + entity_class = MielePhaseSensor + case "state_program_type": + entity_class = MieleTypeSensor + case _: + entity_class = MieleSensor + entities.append( + entity_class(coordinator, device_id, definition.description) + ) + + if ( + definition.description.device_class + == SensorDeviceClass.TEMPERATURE + and definition.description.value_fn(device) + == DISABLED_TEMPERATURE / 100 + ): + # Don't create entity if API signals that datapoint is disabled + continue + async_add_entities(entities) + + coordinator.new_device_callbacks.append(_async_add_new_devices) + _async_add_new_devices(coordinator.data.devices) APPLIANCE_ICONS = { diff --git a/homeassistant/components/miele/switch.py b/homeassistant/components/miele/switch.py index 74a9f0c4785f30..75714ff07b15ad 100644 --- a/homeassistant/components/miele/switch.py +++ b/homeassistant/components/miele/switch.py @@ -117,21 +117,25 @@ async def async_setup_entry( """Set up the switch platform.""" coordinator = config_entry.runtime_data - entities: list = [] - entity_class: type[MieleSwitch] - for device_id, device in coordinator.data.devices.items(): - for definition in SWITCH_TYPES: - if device.device_type in definition.types: - match definition.description.key: - case "poweronoff": - entity_class = MielePowerSwitch - case "supercooling" | "superfreezing": - entity_class = MieleSuperSwitch - - entities.append( - entity_class(coordinator, device_id, definition.description) - ) - async_add_entities(entities) + def _async_add_new_devices(new_devices: dict[str, MieleDevice]) -> None: + entities: list = [] + entity_class: type[MieleSwitch] + for device_id, device in new_devices.items(): + for definition in SWITCH_TYPES: + if device.device_type in definition.types: + match definition.description.key: + case "poweronoff": + entity_class = MielePowerSwitch + case "supercooling" | "superfreezing": + entity_class = MieleSuperSwitch + + entities.append( + entity_class(coordinator, device_id, definition.description) + ) + async_add_entities(entities) + + coordinator.new_device_callbacks.append(_async_add_new_devices) + _async_add_new_devices(coordinator.data.devices) class MieleSwitch(MieleEntity, SwitchEntity): diff --git a/tests/components/miele/fixtures/action_washing_machine.json b/tests/components/miele/fixtures/action_washing_machine.json index 5e8e00306f4ca2..363d3ae6c63919 100644 --- a/tests/components/miele/fixtures/action_washing_machine.json +++ b/tests/components/miele/fixtures/action_washing_machine.json @@ -5,7 +5,13 @@ "startTime": [], "ventilationStep": [], "programId": [], - "targetTemperature": [], + "targetTemperature": [ + { + "zone": 1, + "min": -28, + "max": -14 + } + ], "deviceName": true, "powerOn": true, "powerOff": false, diff --git a/tests/components/miele/snapshots/test_diagnostics.ambr b/tests/components/miele/snapshots/test_diagnostics.ambr index aa564205867c3e..92e312f8d737e7 100644 --- a/tests/components/miele/snapshots/test_diagnostics.ambr +++ b/tests/components/miele/snapshots/test_diagnostics.ambr @@ -36,6 +36,11 @@ 'startTime': list([ ]), 'targetTemperature': list([ + dict({ + 'max': -14, + 'min': -28, + 'zone': 1, + }), ]), 'ventilationStep': list([ ]), @@ -64,6 +69,11 @@ 'startTime': list([ ]), 'targetTemperature': list([ + dict({ + 'max': -14, + 'min': -28, + 'zone': 1, + }), ]), 'ventilationStep': list([ ]), @@ -92,6 +102,11 @@ 'startTime': list([ ]), 'targetTemperature': list([ + dict({ + 'max': -14, + 'min': -28, + 'zone': 1, + }), ]), 'ventilationStep': list([ ]), @@ -120,6 +135,11 @@ 'startTime': list([ ]), 'targetTemperature': list([ + dict({ + 'max': -14, + 'min': -28, + 'zone': 1, + }), ]), 'ventilationStep': list([ ]), @@ -689,6 +709,11 @@ 'startTime': list([ ]), 'targetTemperature': list([ + dict({ + 'max': -14, + 'min': -28, + 'zone': 1, + }), ]), 'ventilationStep': list([ ]), diff --git a/tests/components/miele/test_binary_sensor.py b/tests/components/miele/test_binary_sensor.py index fe1f4b896c5f37..224b3849ed6353 100644 --- a/tests/components/miele/test_binary_sensor.py +++ b/tests/components/miele/test_binary_sensor.py @@ -1,7 +1,9 @@ """Tests for miele binary sensor module.""" +from datetime import timedelta from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion @@ -9,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @pytest.mark.parametrize("platforms", [(BINARY_SENSOR_DOMAIN,)]) @@ -21,7 +23,15 @@ async def test_binary_sensor_states( snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, setup_platform: None, + freezer: FrozenDateTimeFactory, ) -> None: """Test binary sensor state.""" await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.freezer_door").state == "off" + assert hass.states.get("binary_sensor.hood_problem").state == "off" diff --git a/tests/components/miele/test_init.py b/tests/components/miele/test_init.py index 7a81ef78065cf2..bce482652d0ceb 100644 --- a/tests/components/miele/test_init.py +++ b/tests/components/miele/test_init.py @@ -1,10 +1,12 @@ """Tests for init module.""" +from datetime import timedelta import http import time from unittest.mock import MagicMock from aiohttp import ClientConnectionError +from freezegun.api import FrozenDateTimeFactory from pymiele import OAUTH2_TOKEN import pytest from syrupy import SnapshotAssertion @@ -17,7 +19,7 @@ from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import WebSocketGenerator @@ -122,6 +124,63 @@ async def test_device_info( assert device_entry == snapshot +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_setup_platforms( + hass: HomeAssistant, + mock_miele_client: MagicMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that all platforms are set up.""" + + await setup_integration(hass, mock_config_entry) + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.freezer_door").state == "off" + assert hass.states.get("binary_sensor.hood_problem").state == "off" + + assert ( + hass.states.get("button.washing_machine_start").object_id + == "washing_machine_start" + ) + + assert hass.states.get("climate.freezer_freezer").state == "cool" + assert hass.states.get("light.hood_light").state == "on" + + assert hass.states.get("sensor.freezer_temperature").state == "-18.0" + assert hass.states.get("sensor.washing_machine").state == "off" + + assert hass.states.get("switch.washing_machine_power").state == "off" + + +@pytest.mark.parametrize( + "load_action_file", + ["action_freezer.json"], + ids=[ + "freezer", + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_setup_climate_platform( + hass: HomeAssistant, + mock_miele_client: MagicMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that platforms are set up.""" + + await setup_integration(hass, mock_config_entry) + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("climate.freezer_freezer").state == "cool" + + async def test_device_remove_devices( hass: HomeAssistant, hass_ws_client: WebSocketGenerator,