Store Switcher runtime data in config entry (#118054)

This commit is contained in:
Shay Levy 2024-05-25 13:17:33 +03:00 committed by GitHub
parent 3f76b865fa
commit e8226a8056
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 42 additions and 73 deletions

View File

@ -4,15 +4,14 @@ from __future__ import annotations
import logging import logging
from aioswitcher.bridge import SwitcherBridge
from aioswitcher.device import SwitcherBase from aioswitcher.device import SwitcherBase
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from .const import DATA_DEVICE, DOMAIN
from .coordinator import SwitcherDataUpdateCoordinator from .coordinator import SwitcherDataUpdateCoordinator
from .utils import async_start_bridge, async_stop_bridge
PLATFORMS = [ PLATFORMS = [
Platform.BUTTON, Platform.BUTTON,
@ -25,20 +24,20 @@ PLATFORMS = [
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: type SwitcherConfigEntry = ConfigEntry[dict[str, SwitcherDataUpdateCoordinator]]
async def async_setup_entry(hass: HomeAssistant, entry: SwitcherConfigEntry) -> bool:
"""Set up Switcher from a config entry.""" """Set up Switcher from a config entry."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][DATA_DEVICE] = {}
@callback @callback
def on_device_data_callback(device: SwitcherBase) -> None: def on_device_data_callback(device: SwitcherBase) -> None:
"""Use as a callback for device data.""" """Use as a callback for device data."""
coordinators = entry.runtime_data
# Existing device update device data # Existing device update device data
if device.device_id in hass.data[DOMAIN][DATA_DEVICE]: if coordinator := coordinators.get(device.device_id):
coordinator: SwitcherDataUpdateCoordinator = hass.data[DOMAIN][DATA_DEVICE][
device.device_id
]
coordinator.async_set_updated_data(device) coordinator.async_set_updated_data(device)
return return
@ -52,18 +51,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device.device_type.hex_rep, device.device_type.hex_rep,
) )
coordinator = hass.data[DOMAIN][DATA_DEVICE][device.device_id] = ( coordinator = SwitcherDataUpdateCoordinator(hass, entry, device)
SwitcherDataUpdateCoordinator(hass, entry, device)
)
coordinator.async_setup() coordinator.async_setup()
coordinators[device.device_id] = coordinator
# Must be ready before dispatcher is called # Must be ready before dispatcher is called
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await async_start_bridge(hass, on_device_data_callback) entry.runtime_data = {}
bridge = SwitcherBridge(on_device_data_callback)
await bridge.start()
async def stop_bridge(event: Event) -> None: async def stop_bridge(event: Event | None = None) -> None:
await async_stop_bridge(hass) await bridge.stop()
entry.async_on_unload(stop_bridge)
entry.async_on_unload( entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge)
@ -72,12 +74,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: SwitcherConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
await async_stop_bridge(hass) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(DATA_DEVICE)
return unload_ok

View File

@ -15,7 +15,6 @@ from aioswitcher.api.remotes import SwitcherBreezeRemote
from aioswitcher.device import DeviceCategory from aioswitcher.device import DeviceCategory
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -25,6 +24,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import SwitcherConfigEntry
from .const import SIGNAL_DEVICE_ADD from .const import SIGNAL_DEVICE_ADD
from .coordinator import SwitcherDataUpdateCoordinator from .coordinator import SwitcherDataUpdateCoordinator
from .utils import get_breeze_remote_manager from .utils import get_breeze_remote_manager
@ -78,7 +78,7 @@ THERMOSTAT_BUTTONS = [
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: SwitcherConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up Switcher button from config entry.""" """Set up Switcher button from config entry."""

View File

@ -25,7 +25,6 @@ from homeassistant.components.climate import (
ClimateEntityFeature, ClimateEntityFeature,
HVACMode, HVACMode,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -35,6 +34,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import SwitcherConfigEntry
from .const import SIGNAL_DEVICE_ADD from .const import SIGNAL_DEVICE_ADD
from .coordinator import SwitcherDataUpdateCoordinator from .coordinator import SwitcherDataUpdateCoordinator
from .utils import get_breeze_remote_manager from .utils import get_breeze_remote_manager
@ -61,7 +61,7 @@ HA_TO_DEVICE_FAN = {value: key for key, value in DEVICE_FAN_TO_HA.items()}
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: SwitcherConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up Switcher climate from config entry.""" """Set up Switcher climate from config entry."""

View File

@ -2,9 +2,6 @@
DOMAIN = "switcher_kis" DOMAIN = "switcher_kis"
DATA_BRIDGE = "bridge"
DATA_DEVICE = "device"
DISCOVERY_TIME_SEC = 12 DISCOVERY_TIME_SEC = 12
SIGNAL_DEVICE_ADD = "switcher_device_add" SIGNAL_DEVICE_ADD = "switcher_device_add"

View File

@ -6,24 +6,23 @@ from dataclasses import asdict
from typing import Any from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DATA_DEVICE, DOMAIN from . import SwitcherConfigEntry
TO_REDACT = {"device_id", "device_key", "ip_address", "mac_address"} TO_REDACT = {"device_id", "device_key", "ip_address", "mac_address"}
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: SwitcherConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
devices = hass.data[DOMAIN][DATA_DEVICE] coordinators = entry.runtime_data
return async_redact_data( return async_redact_data(
{ {
"entry": entry.as_dict(), "entry": entry.as_dict(),
"devices": [asdict(devices[d].data) for d in devices], "devices": [asdict(coordinators[d].data) for d in coordinators],
}, },
TO_REDACT, TO_REDACT,
) )

View File

@ -3,9 +3,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable
import logging import logging
from typing import Any
from aioswitcher.api.remotes import SwitcherBreezeRemoteManager from aioswitcher.api.remotes import SwitcherBreezeRemoteManager
from aioswitcher.bridge import SwitcherBase, SwitcherBridge from aioswitcher.bridge import SwitcherBase, SwitcherBridge
@ -13,29 +11,11 @@ from aioswitcher.bridge import SwitcherBase, SwitcherBridge
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import singleton from homeassistant.helpers import singleton
from .const import DATA_BRIDGE, DISCOVERY_TIME_SEC, DOMAIN from .const import DISCOVERY_TIME_SEC
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_start_bridge(
hass: HomeAssistant, on_device_callback: Callable[[SwitcherBase], Any]
) -> None:
"""Start switcher UDP bridge."""
bridge = hass.data[DOMAIN][DATA_BRIDGE] = SwitcherBridge(on_device_callback)
_LOGGER.debug("Starting Switcher bridge")
await bridge.start()
async def async_stop_bridge(hass: HomeAssistant) -> None:
"""Stop switcher UDP bridge."""
bridge: SwitcherBridge = hass.data[DOMAIN].get(DATA_BRIDGE)
if bridge is not None:
_LOGGER.debug("Stopping Switcher bridge")
await bridge.stop()
hass.data[DOMAIN].pop(DATA_BRIDGE)
async def async_has_devices(hass: HomeAssistant) -> bool: async def async_has_devices(hass: HomeAssistant) -> bool:
"""Discover Switcher devices.""" """Discover Switcher devices."""
_LOGGER.debug("Starting discovery") _LOGGER.debug("Starting discovery")

View File

@ -18,9 +18,15 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]:
@pytest.fixture @pytest.fixture
def mock_bridge(request): def mock_bridge(request):
"""Return a mocked SwitcherBridge.""" """Return a mocked SwitcherBridge."""
with patch( with (
"homeassistant.components.switcher_kis.utils.SwitcherBridge", autospec=True patch(
) as bridge_mock: "homeassistant.components.switcher_kis.SwitcherBridge", autospec=True
) as bridge_mock,
patch(
"homeassistant.components.switcher_kis.utils.SwitcherBridge",
new=bridge_mock,
),
):
bridge = bridge_mock.return_value bridge = bridge_mock.return_value
bridge.devices = [] bridge.devices = []

View File

@ -4,11 +4,7 @@ from datetime import timedelta
import pytest import pytest
from homeassistant.components.switcher_kis.const import ( from homeassistant.components.switcher_kis.const import MAX_UPDATE_INTERVAL_SEC
DATA_DEVICE,
DOMAIN,
MAX_UPDATE_INTERVAL_SEC,
)
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -24,15 +20,14 @@ async def test_update_fail(
hass: HomeAssistant, mock_bridge, caplog: pytest.LogCaptureFixture hass: HomeAssistant, mock_bridge, caplog: pytest.LogCaptureFixture
) -> None: ) -> None:
"""Test entities state unavailable when updates fail..""" """Test entities state unavailable when updates fail.."""
await init_integration(hass) entry = await init_integration(hass)
assert mock_bridge assert mock_bridge
mock_bridge.mock_callbacks(DUMMY_SWITCHER_DEVICES) mock_bridge.mock_callbacks(DUMMY_SWITCHER_DEVICES)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_bridge.is_running is True assert mock_bridge.is_running is True
assert len(hass.data[DOMAIN]) == 2 assert len(entry.runtime_data) == 2
assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2
async_fire_time_changed( async_fire_time_changed(
hass, dt_util.utcnow() + timedelta(seconds=MAX_UPDATE_INTERVAL_SEC + 1) hass, dt_util.utcnow() + timedelta(seconds=MAX_UPDATE_INTERVAL_SEC + 1)
@ -77,11 +72,9 @@ async def test_entry_unload(hass: HomeAssistant, mock_bridge) -> None:
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
assert mock_bridge.is_running is True assert mock_bridge.is_running is True
assert len(hass.data[DOMAIN]) == 2
await hass.config_entries.async_unload(entry.entry_id) await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED assert entry.state is ConfigEntryState.NOT_LOADED
assert mock_bridge.is_running is False assert mock_bridge.is_running is False
assert len(hass.data[DOMAIN]) == 0

View File

@ -2,7 +2,6 @@
import pytest import pytest
from homeassistant.components.switcher_kis.const import DATA_DEVICE, DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util import slugify from homeassistant.util import slugify
@ -32,12 +31,11 @@ DEVICE_SENSORS_TUPLE = (
@pytest.mark.parametrize("mock_bridge", [DUMMY_SWITCHER_DEVICES], indirect=True) @pytest.mark.parametrize("mock_bridge", [DUMMY_SWITCHER_DEVICES], indirect=True)
async def test_sensor_platform(hass: HomeAssistant, mock_bridge) -> None: async def test_sensor_platform(hass: HomeAssistant, mock_bridge) -> None:
"""Test sensor platform.""" """Test sensor platform."""
await init_integration(hass) entry = await init_integration(hass)
assert mock_bridge assert mock_bridge
assert mock_bridge.is_running is True assert mock_bridge.is_running is True
assert len(hass.data[DOMAIN]) == 2 assert len(entry.runtime_data) == 2
assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2
for device, sensors in DEVICE_SENSORS_TUPLE: for device, sensors in DEVICE_SENSORS_TUPLE:
for sensor, field in sensors: for sensor, field in sensors: