mirror of
https://github.com/home-assistant/core.git
synced 2026-04-06 23:47:33 +00:00
Compare commits
22 Commits
media-sour
...
update-tra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20211eb225 | ||
|
|
8ab627cd44 | ||
|
|
4870ea907d | ||
|
|
68888100f6 | ||
|
|
4e81f3ac15 | ||
|
|
dcefdc7bf2 | ||
|
|
99e2ac2d2f | ||
|
|
f65693b6e8 | ||
|
|
380f0c4588 | ||
|
|
136ffc898b | ||
|
|
5474fd77b2 | ||
|
|
7b43714adf | ||
|
|
ab903f7fea | ||
|
|
8774d4ae75 | ||
|
|
e7cc87be4c | ||
|
|
951ae92668 | ||
|
|
f323289d2a | ||
|
|
b66bdd444e | ||
|
|
e04bb5932d | ||
|
|
b21f69ab7b | ||
|
|
00ab475d0f | ||
|
|
81114f4f82 |
@@ -3,11 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from pytradfri import Gateway, RequestError
|
||||
from pytradfri.api.aiocoap_api import APIFactory
|
||||
from pytradfri.command import Command
|
||||
from pytradfri.device import Device
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -21,18 +19,10 @@ from homeassistant.helpers.dispatcher import (
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from .const import (
|
||||
CONF_GATEWAY_ID,
|
||||
CONF_IDENTITY,
|
||||
CONF_KEY,
|
||||
COORDINATOR,
|
||||
COORDINATOR_LIST,
|
||||
DOMAIN,
|
||||
FACTORY,
|
||||
KEY_API,
|
||||
LOGGER,
|
||||
)
|
||||
from .const import CONF_GATEWAY_ID, CONF_IDENTITY, CONF_KEY, DOMAIN, LOGGER
|
||||
from .coordinator import TradfriDeviceDataUpdateCoordinator
|
||||
from .migration import migrate_device_identifier
|
||||
from .models import TradfriData
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.COVER,
|
||||
@@ -50,15 +40,14 @@ async def async_setup_entry(
|
||||
entry: ConfigEntry,
|
||||
) -> bool:
|
||||
"""Create a gateway."""
|
||||
tradfri_data: dict[str, Any] = {}
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = tradfri_data
|
||||
# Migrate old integer device identifier to string, added in core-2022.7.0.
|
||||
migrate_device_identifier(hass, entry)
|
||||
|
||||
factory = await APIFactory.init(
|
||||
entry.data[CONF_HOST],
|
||||
psk_id=entry.data[CONF_IDENTITY],
|
||||
psk=entry.data[CONF_KEY],
|
||||
)
|
||||
tradfri_data[FACTORY] = factory # Used for async_unload_entry
|
||||
|
||||
async def on_hass_stop(event: Event) -> None:
|
||||
"""Close connection when hass stops."""
|
||||
@@ -74,10 +63,8 @@ async def async_setup_entry(
|
||||
|
||||
try:
|
||||
gateway_info = await api(gateway.get_gateway_info(), timeout=TIMEOUT_API)
|
||||
devices_commands: Command = await api(
|
||||
gateway.get_devices(), timeout=TIMEOUT_API
|
||||
)
|
||||
devices: list[Device] = await api(devices_commands, timeout=TIMEOUT_API)
|
||||
devices_commands = await api(gateway.get_devices(), timeout=TIMEOUT_API)
|
||||
devices = await api(devices_commands, timeout=TIMEOUT_API)
|
||||
|
||||
except RequestError as exc:
|
||||
await factory.shutdown()
|
||||
@@ -98,11 +85,7 @@ async def async_setup_entry(
|
||||
remove_stale_devices(hass, entry, devices)
|
||||
|
||||
# Setup the device coordinators
|
||||
coordinator_data = {
|
||||
CONF_GATEWAY_ID: gateway,
|
||||
KEY_API: api,
|
||||
COORDINATOR_LIST: [],
|
||||
}
|
||||
coordinators: list[TradfriDeviceDataUpdateCoordinator] = []
|
||||
|
||||
for device in devices:
|
||||
coordinator = TradfriDeviceDataUpdateCoordinator(
|
||||
@@ -113,9 +96,12 @@ async def async_setup_entry(
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, SIGNAL_GW, coordinator.set_hub_available)
|
||||
)
|
||||
coordinator_data[COORDINATOR_LIST].append(coordinator)
|
||||
coordinators.append(coordinator)
|
||||
|
||||
tradfri_data[COORDINATOR] = coordinator_data
|
||||
tradfri_data = TradfriData(
|
||||
api=api, coordinators=coordinators, factory=factory, gateway=gateway
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = tradfri_data
|
||||
|
||||
async def async_keep_alive(now: datetime) -> None:
|
||||
if hass.is_stopping:
|
||||
@@ -143,8 +129,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
tradfri_data = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
factory = tradfri_data[FACTORY]
|
||||
tradfri_data: TradfriData = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
factory = tradfri_data.factory
|
||||
await factory.shutdown()
|
||||
|
||||
return unload_ok
|
||||
@@ -159,7 +145,7 @@ def remove_stale_devices(
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry.entry_id
|
||||
)
|
||||
all_device_ids = {device.id for device in devices}
|
||||
all_device_ids = {str(device.id) for device in devices}
|
||||
|
||||
for device_entry in device_entries:
|
||||
device_id: str | None = None
|
||||
@@ -169,7 +155,9 @@ def remove_stale_devices(
|
||||
if identifier[0] != DOMAIN:
|
||||
continue
|
||||
|
||||
_id = identifier[1]
|
||||
# The device id in the identifier was not copied from integer to string
|
||||
# when setting entity device info. Copy here to make sure it's a string.
|
||||
_id = str(identifier[1])
|
||||
|
||||
# Identify gateway device.
|
||||
if _id == config_entry.data[CONF_GATEWAY_ID]:
|
||||
|
||||
@@ -7,8 +7,4 @@ LOGGER = logging.getLogger(__package__)
|
||||
CONF_GATEWAY_ID = "gateway_id"
|
||||
CONF_IDENTITY = "identity"
|
||||
CONF_KEY = "key"
|
||||
COORDINATOR = "coordinator"
|
||||
COORDINATOR_LIST = "coordinator_list"
|
||||
DOMAIN = "tradfri"
|
||||
FACTORY = "tradfri_factory"
|
||||
KEY_API = "tradfri_api"
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
|
||||
from pytradfri.command import Command
|
||||
from pytradfri.api.aiocoap_api import APIRequestProtocol
|
||||
from pytradfri.device import Device
|
||||
from pytradfri.error import RequestError
|
||||
from pytradfri.resource import ApiResource
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -27,8 +27,9 @@ class TradfriDeviceDataUpdateCoordinator(DataUpdateCoordinator[Device]):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
config_entry: ConfigEntry,
|
||||
api: Callable[[Command | list[Command]], Any],
|
||||
api: APIRequestProtocol,
|
||||
device: Device,
|
||||
) -> None:
|
||||
"""Initialize device coordinator."""
|
||||
@@ -52,8 +53,9 @@ class TradfriDeviceDataUpdateCoordinator(DataUpdateCoordinator[Device]):
|
||||
await self.async_request_refresh()
|
||||
|
||||
@callback
|
||||
def _observe_update(self, device: Device) -> None:
|
||||
def _observe_update(self, device: ApiResource) -> None:
|
||||
"""Update the coordinator for a device when a change is detected."""
|
||||
device = cast(Device, device)
|
||||
self.async_set_updated_data(data=device)
|
||||
|
||||
@callback
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from pytradfri.command import Command
|
||||
from pytradfri.api.aiocoap_api import APIRequestProtocol
|
||||
|
||||
from homeassistant.components.cover import ATTR_POSITION, CoverEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import CONF_GATEWAY_ID, COORDINATOR, COORDINATOR_LIST, DOMAIN, KEY_API
|
||||
from .const import CONF_GATEWAY_ID, DOMAIN
|
||||
from .coordinator import TradfriDeviceDataUpdateCoordinator
|
||||
from .entity import TradfriBaseEntity
|
||||
from .models import TradfriData
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -24,8 +24,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Load Tradfri covers based on a config entry."""
|
||||
gateway_id = config_entry.data[CONF_GATEWAY_ID]
|
||||
coordinator_data = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
|
||||
api = coordinator_data[KEY_API]
|
||||
tradfri_data: TradfriData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api = tradfri_data.api
|
||||
|
||||
async_add_entities(
|
||||
TradfriCover(
|
||||
@@ -33,7 +33,7 @@ async def async_setup_entry(
|
||||
api,
|
||||
gateway_id,
|
||||
)
|
||||
for device_coordinator in coordinator_data[COORDINATOR_LIST]
|
||||
for device_coordinator in tradfri_data.coordinators
|
||||
if device_coordinator.device.has_blind_control
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ class TradfriCover(TradfriBaseEntity, CoverEntity):
|
||||
def __init__(
|
||||
self,
|
||||
device_coordinator: TradfriDeviceDataUpdateCoordinator,
|
||||
api: Callable[[Command | list[Command]], Any],
|
||||
api: APIRequestProtocol,
|
||||
gateway_id: str,
|
||||
) -> None:
|
||||
"""Initialize a switch."""
|
||||
@@ -56,12 +56,14 @@ class TradfriCover(TradfriBaseEntity, CoverEntity):
|
||||
gateway_id=gateway_id,
|
||||
)
|
||||
|
||||
self._device_control = self._device.blind_control
|
||||
self._device_data = self._device_control.blinds[0]
|
||||
device_control = self._device.blind_control
|
||||
assert device_control # blind_control is ensured when creating the entity
|
||||
self._device_control = device_control
|
||||
self._device_data = device_control.blinds[0]
|
||||
|
||||
def _refresh(self) -> None:
|
||||
"""Refresh the device."""
|
||||
self._device_data = self.coordinator.data.blind_control.blinds[0]
|
||||
self._device_data = self._device_control.blinds[0]
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str] | None:
|
||||
@@ -74,32 +76,22 @@ class TradfriCover(TradfriBaseEntity, CoverEntity):
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
if not self._device_data:
|
||||
return None
|
||||
return 100 - cast(int, self._device_data.current_cover_position)
|
||||
return 100 - self._device_data.current_cover_position
|
||||
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover to a specific position."""
|
||||
if not self._device_control:
|
||||
return
|
||||
await self._api(self._device_control.set_state(100 - kwargs[ATTR_POSITION]))
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
if not self._device_control:
|
||||
return
|
||||
await self._api(self._device_control.set_state(0))
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close cover."""
|
||||
if not self._device_control:
|
||||
return
|
||||
await self._api(self._device_control.set_state(100))
|
||||
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Close cover."""
|
||||
if not self._device_control:
|
||||
return
|
||||
await self._api(self._device_control.trigger_blind())
|
||||
|
||||
@property
|
||||
|
||||
@@ -8,15 +8,15 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import CONF_GATEWAY_ID, COORDINATOR, COORDINATOR_LIST, DOMAIN
|
||||
from .const import CONF_GATEWAY_ID, DOMAIN
|
||||
from .models import TradfriData
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics the Tradfri platform."""
|
||||
entry_data = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator_data = entry_data[COORDINATOR]
|
||||
tradfri_data: TradfriData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device = cast(
|
||||
@@ -28,7 +28,7 @@ async def async_get_config_entry_diagnostics(
|
||||
|
||||
device_data: list = [
|
||||
coordinator.device.device_info.model_number
|
||||
for coordinator in coordinator_data[COORDINATOR_LIST]
|
||||
for coordinator in tradfri_data.coordinators
|
||||
]
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,8 +5,9 @@ from __future__ import annotations
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from pytradfri.api.aiocoap_api import APIRequestProtocol
|
||||
from pytradfri.command import Command
|
||||
from pytradfri.device import Device
|
||||
from pytradfri.error import RequestError
|
||||
@@ -20,7 +21,7 @@ from .coordinator import TradfriDeviceDataUpdateCoordinator
|
||||
|
||||
|
||||
def handle_error(
|
||||
func: Callable[[Command | list[Command]], Any],
|
||||
func: APIRequestProtocol,
|
||||
) -> Callable[[Command | list[Command]], Coroutine[Any, Any, None]]:
|
||||
"""Handle tradfri api call error."""
|
||||
|
||||
@@ -44,7 +45,7 @@ class TradfriBaseEntity(CoordinatorEntity[TradfriDeviceDataUpdateCoordinator]):
|
||||
self,
|
||||
device_coordinator: TradfriDeviceDataUpdateCoordinator,
|
||||
gateway_id: str,
|
||||
api: Callable[[Command | list[Command]], Any],
|
||||
api: APIRequestProtocol,
|
||||
) -> None:
|
||||
"""Initialize a device."""
|
||||
super().__init__(device_coordinator)
|
||||
@@ -58,7 +59,7 @@ class TradfriBaseEntity(CoordinatorEntity[TradfriDeviceDataUpdateCoordinator]):
|
||||
|
||||
info = self._device.device_info
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._device_id)},
|
||||
identifiers={(DOMAIN, str(self._device.id))},
|
||||
manufacturer=info.manufacturer,
|
||||
model=info.model_number,
|
||||
name=self._device.name,
|
||||
@@ -84,4 +85,4 @@ class TradfriBaseEntity(CoordinatorEntity[TradfriDeviceDataUpdateCoordinator]):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return cast(bool, self._device.reachable) and super().available
|
||||
return self._device.reachable and super().available
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from pytradfri.command import Command
|
||||
from pytradfri.api.aiocoap_api import APIRequestProtocol
|
||||
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import CONF_GATEWAY_ID, COORDINATOR, COORDINATOR_LIST, DOMAIN, KEY_API
|
||||
from .const import CONF_GATEWAY_ID, DOMAIN
|
||||
from .coordinator import TradfriDeviceDataUpdateCoordinator
|
||||
from .entity import TradfriBaseEntity
|
||||
from .models import TradfriData
|
||||
|
||||
ATTR_AUTO = "Auto"
|
||||
ATTR_MAX_FAN_STEPS = 49
|
||||
@@ -37,8 +37,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Load Tradfri switches based on a config entry."""
|
||||
gateway_id = config_entry.data[CONF_GATEWAY_ID]
|
||||
coordinator_data = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
|
||||
api = coordinator_data[KEY_API]
|
||||
tradfri_data: TradfriData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api = tradfri_data.api
|
||||
|
||||
async_add_entities(
|
||||
TradfriAirPurifierFan(
|
||||
@@ -46,7 +46,7 @@ async def async_setup_entry(
|
||||
api,
|
||||
gateway_id,
|
||||
)
|
||||
for device_coordinator in coordinator_data[COORDINATOR_LIST]
|
||||
for device_coordinator in tradfri_data.coordinators
|
||||
if device_coordinator.device.has_air_purifier_control
|
||||
)
|
||||
|
||||
@@ -73,7 +73,7 @@ class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity):
|
||||
def __init__(
|
||||
self,
|
||||
device_coordinator: TradfriDeviceDataUpdateCoordinator,
|
||||
api: Callable[[Command | list[Command]], Any],
|
||||
api: APIRequestProtocol,
|
||||
gateway_id: str,
|
||||
) -> None:
|
||||
"""Initialize a switch."""
|
||||
@@ -83,37 +83,30 @@ class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity):
|
||||
gateway_id=gateway_id,
|
||||
)
|
||||
|
||||
self._device_control = self._device.air_purifier_control
|
||||
self._device_data = self._device_control.air_purifiers[0]
|
||||
device_control = self._device.air_purifier_control
|
||||
assert (
|
||||
device_control # air_purifier_control is ensured when creating the entity
|
||||
)
|
||||
self._device_control = device_control
|
||||
self._device_data = device_control.air_purifiers[0]
|
||||
|
||||
def _refresh(self) -> None:
|
||||
"""Refresh the device."""
|
||||
self._device_data = self.coordinator.data.air_purifier_control.air_purifiers[0]
|
||||
self._device_data = self._device_control.air_purifiers[0]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if switch is on."""
|
||||
if not self._device_data:
|
||||
return False
|
||||
return cast(bool, self._device_data.state)
|
||||
return self._device_data.state
|
||||
|
||||
@property
|
||||
def percentage(self) -> int | None:
|
||||
def percentage(self) -> int:
|
||||
"""Return the current speed percentage."""
|
||||
if not self._device_data:
|
||||
return None
|
||||
|
||||
if self._device_data.fan_speed:
|
||||
return _from_fan_speed(self._device_data.fan_speed)
|
||||
|
||||
return None
|
||||
return _from_fan_speed(self._device_data.fan_speed)
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode."""
|
||||
if not self._device_data:
|
||||
return None
|
||||
|
||||
if self._device_data.is_auto_mode:
|
||||
return ATTR_AUTO
|
||||
|
||||
@@ -121,10 +114,8 @@ class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
if not self._device_control:
|
||||
return
|
||||
|
||||
# Preset must be 'Auto'
|
||||
if not preset_mode == ATTR_AUTO:
|
||||
raise ValueError("Preset must be 'Auto'.")
|
||||
|
||||
await self._api(self._device_control.turn_on_auto_mode())
|
||||
|
||||
@@ -135,9 +126,6 @@ class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Turn on the fan. Auto-mode if no argument is given."""
|
||||
if not self._device_control:
|
||||
return
|
||||
|
||||
if percentage is not None:
|
||||
await self.async_set_percentage(percentage)
|
||||
return
|
||||
@@ -147,9 +135,6 @@ class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity):
|
||||
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the speed percentage of the fan."""
|
||||
if not self._device_control:
|
||||
return
|
||||
|
||||
if percentage == 0:
|
||||
await self.async_turn_off()
|
||||
return
|
||||
@@ -160,6 +145,4 @@ class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity):
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the fan."""
|
||||
if not self._device_control:
|
||||
return
|
||||
await self._api(self._device_control.turn_off())
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from pytradfri.command import Command
|
||||
from pytradfri.api.aiocoap_api import APIRequestProtocol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
@@ -22,9 +21,10 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from .const import CONF_GATEWAY_ID, COORDINATOR, COORDINATOR_LIST, DOMAIN, KEY_API
|
||||
from .const import CONF_GATEWAY_ID, DOMAIN
|
||||
from .coordinator import TradfriDeviceDataUpdateCoordinator
|
||||
from .entity import TradfriBaseEntity
|
||||
from .models import TradfriData
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -34,8 +34,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Load Tradfri lights based on a config entry."""
|
||||
gateway_id = config_entry.data[CONF_GATEWAY_ID]
|
||||
coordinator_data = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
|
||||
api = coordinator_data[KEY_API]
|
||||
tradfri_data: TradfriData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api = tradfri_data.api
|
||||
|
||||
async_add_entities(
|
||||
TradfriLight(
|
||||
@@ -43,7 +43,7 @@ async def async_setup_entry(
|
||||
api,
|
||||
gateway_id,
|
||||
)
|
||||
for device_coordinator in coordinator_data[COORDINATOR_LIST]
|
||||
for device_coordinator in tradfri_data.coordinators
|
||||
if device_coordinator.device.has_light_control
|
||||
)
|
||||
|
||||
@@ -58,7 +58,7 @@ class TradfriLight(TradfriBaseEntity, LightEntity):
|
||||
def __init__(
|
||||
self,
|
||||
device_coordinator: TradfriDeviceDataUpdateCoordinator,
|
||||
api: Callable[[Command | list[Command]], Any],
|
||||
api: APIRequestProtocol,
|
||||
gateway_id: str,
|
||||
) -> None:
|
||||
"""Initialize a Light."""
|
||||
@@ -68,46 +68,42 @@ class TradfriLight(TradfriBaseEntity, LightEntity):
|
||||
gateway_id=gateway_id,
|
||||
)
|
||||
|
||||
self._device_control = self._device.light_control
|
||||
self._device_data = self._device_control.lights[0]
|
||||
device_control = self._device.light_control
|
||||
assert device_control # light_control is ensured when creating the entity
|
||||
self._device_control = device_control
|
||||
device_data = device_control.lights[0]
|
||||
self._device_data = device_data
|
||||
|
||||
self._attr_unique_id = f"light-{gateway_id}-{self._device_id}"
|
||||
self._hs_color = None
|
||||
|
||||
# Calculate supported color modes
|
||||
modes: set[ColorMode] = {ColorMode.ONOFF}
|
||||
if self._device.light_control.can_set_color:
|
||||
if device_data.supports_hsb_xy_color:
|
||||
modes.add(ColorMode.HS)
|
||||
if self._device.light_control.can_set_temp:
|
||||
if device_data.supports_color_temp:
|
||||
modes.add(ColorMode.COLOR_TEMP)
|
||||
if self._device.light_control.can_set_dimmer:
|
||||
if device_data.supports_dimmer:
|
||||
modes.add(ColorMode.BRIGHTNESS)
|
||||
self._attr_supported_color_modes = filter_supported_color_modes(modes)
|
||||
if len(self._attr_supported_color_modes) == 1:
|
||||
self._fixed_color_mode = next(iter(self._attr_supported_color_modes))
|
||||
|
||||
if self._device_control:
|
||||
self._attr_max_color_temp_kelvin = (
|
||||
color_util.color_temperature_mired_to_kelvin(
|
||||
self._device_control.min_mireds
|
||||
)
|
||||
)
|
||||
self._attr_min_color_temp_kelvin = (
|
||||
color_util.color_temperature_mired_to_kelvin(
|
||||
self._device_control.max_mireds
|
||||
)
|
||||
)
|
||||
self._attr_max_color_temp_kelvin = color_util.color_temperature_mired_to_kelvin(
|
||||
device_control.min_mireds
|
||||
)
|
||||
self._attr_min_color_temp_kelvin = color_util.color_temperature_mired_to_kelvin(
|
||||
device_control.max_mireds
|
||||
)
|
||||
|
||||
def _refresh(self) -> None:
|
||||
"""Refresh the device."""
|
||||
self._device_data = self.coordinator.data.light_control.lights[0]
|
||||
self._device_data = self._device_control.lights[0]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if light is on."""
|
||||
if not self._device_data:
|
||||
return False
|
||||
return cast(bool, self._device_data.state)
|
||||
return self._device_data.state
|
||||
|
||||
@property
|
||||
def color_mode(self) -> ColorMode | None:
|
||||
@@ -121,36 +117,29 @@ class TradfriLight(TradfriBaseEntity, LightEntity):
|
||||
@property
|
||||
def brightness(self) -> int | None:
|
||||
"""Return the brightness of the light."""
|
||||
if not self._device_data:
|
||||
return None
|
||||
return cast(int, self._device_data.dimmer)
|
||||
return self._device_data.dimmer
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self) -> int | None:
|
||||
"""Return the color temperature value in Kelvin."""
|
||||
if not self._device_data or not (color_temp := self._device_data.color_temp):
|
||||
if (color_temp := self._device_data.color_temp) is None:
|
||||
return None
|
||||
return color_util.color_temperature_mired_to_kelvin(color_temp)
|
||||
|
||||
@property
|
||||
def hs_color(self) -> tuple[float, float] | None:
|
||||
"""HS color of the light."""
|
||||
if not self._device_control or not self._device_data:
|
||||
hsbxy = self._device_data.hsb_xy_color
|
||||
if hsbxy is None:
|
||||
return None
|
||||
if self._device_control.can_set_color:
|
||||
hsbxy = self._device_data.hsb_xy_color
|
||||
hue = hsbxy[0] / (self._device_control.max_hue / 360)
|
||||
sat = hsbxy[1] / (self._device_control.max_saturation / 100)
|
||||
if hue is not None and sat is not None:
|
||||
return hue, sat
|
||||
return None
|
||||
hue = hsbxy[0] / (self._device_control.max_hue / 360)
|
||||
sat = hsbxy[1] / (self._device_control.max_saturation / 100)
|
||||
return hue, sat
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn off."""
|
||||
# This allows transitioning to off, but resets the brightness
|
||||
# to 1 for the next set_state(True) command
|
||||
if not self._device_control:
|
||||
return
|
||||
transition_time = None
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
transition_time = int(kwargs[ATTR_TRANSITION]) * 10
|
||||
@@ -165,81 +154,75 @@ class TradfriLight(TradfriBaseEntity, LightEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn on."""
|
||||
if not self._device_control:
|
||||
return
|
||||
transition_time = None
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
transition_time = int(kwargs[ATTR_TRANSITION]) * 10
|
||||
|
||||
dimmer_command = None
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
brightness = min(brightness, 254)
|
||||
dimmer_data = {
|
||||
"dimmer": brightness,
|
||||
"transition_time": transition_time,
|
||||
}
|
||||
dimmer_command = self._device_control.set_dimmer(**dimmer_data)
|
||||
|
||||
dimmer_command = self._device_control.set_dimmer(
|
||||
dimmer=brightness, transition_time=transition_time
|
||||
)
|
||||
transition_time = None
|
||||
else:
|
||||
dimmer_command = self._device_control.set_state(True)
|
||||
|
||||
color_command = None
|
||||
if ATTR_HS_COLOR in kwargs and self._device_control.can_set_color:
|
||||
if ATTR_HS_COLOR in kwargs and self._device_data.supports_hsb_xy_color:
|
||||
hue = int(kwargs[ATTR_HS_COLOR][0] * (self._device_control.max_hue / 360))
|
||||
sat = int(
|
||||
kwargs[ATTR_HS_COLOR][1] * (self._device_control.max_saturation / 100)
|
||||
)
|
||||
color_data = {
|
||||
"hue": hue,
|
||||
"saturation": sat,
|
||||
"transition_time": transition_time,
|
||||
}
|
||||
color_command = self._device_control.set_hsb(**color_data)
|
||||
|
||||
color_command = self._device_control.set_hsb(
|
||||
hue=hue, saturation=sat, transition_time=transition_time
|
||||
)
|
||||
transition_time = None
|
||||
|
||||
temp_command = None
|
||||
if ATTR_COLOR_TEMP_KELVIN in kwargs and (
|
||||
self._device_control.can_set_temp or self._device_control.can_set_color
|
||||
self._device_data.supports_color_temp
|
||||
or self._device_data.supports_hsb_xy_color
|
||||
):
|
||||
temp_k = kwargs[ATTR_COLOR_TEMP_KELVIN]
|
||||
# White Spectrum bulb
|
||||
if self._device_control.can_set_temp:
|
||||
if self._device_data.supports_color_temp:
|
||||
temp = color_util.color_temperature_kelvin_to_mired(temp_k)
|
||||
if temp < (min_mireds := self._device_control.min_mireds):
|
||||
temp = min_mireds
|
||||
elif temp > (max_mireds := self._device_control.max_mireds):
|
||||
temp = max_mireds
|
||||
temp_data = {
|
||||
"color_temp": temp,
|
||||
"transition_time": transition_time,
|
||||
}
|
||||
temp_command = self._device_control.set_color_temp(**temp_data)
|
||||
|
||||
temp_command = self._device_control.set_color_temp(
|
||||
color_temp=temp, transition_time=transition_time
|
||||
)
|
||||
transition_time = None
|
||||
# Color bulb (CWS)
|
||||
# color_temp needs to be set with hue/saturation
|
||||
elif self._device_control.can_set_color:
|
||||
elif self._device_data.supports_hsb_xy_color:
|
||||
hs_color = color_util.color_temperature_to_hs(temp_k)
|
||||
hue = int(hs_color[0] * (self._device_control.max_hue / 360))
|
||||
sat = int(hs_color[1] * (self._device_control.max_saturation / 100))
|
||||
color_data = {
|
||||
"hue": hue,
|
||||
"saturation": sat,
|
||||
"transition_time": transition_time,
|
||||
}
|
||||
color_command = self._device_control.set_hsb(**color_data)
|
||||
|
||||
color_command = self._device_control.set_hsb(
|
||||
hue=hue, saturation=sat, transition_time=transition_time
|
||||
)
|
||||
transition_time = None
|
||||
|
||||
# HSB can always be set, but color temp + brightness is bulb dependent
|
||||
if (command := dimmer_command) is not None:
|
||||
command += color_command
|
||||
else:
|
||||
command = color_command
|
||||
command = dimmer_command
|
||||
if color_command is not None:
|
||||
command = self._device_control.combine_commands(
|
||||
[dimmer_command, color_command]
|
||||
)
|
||||
|
||||
if self._device_control.can_combine_commands:
|
||||
await self._api(command + temp_command)
|
||||
if self._device_control.can_combine_commands and temp_command is not None:
|
||||
await self._api(
|
||||
self._device_control.combine_commands([command, temp_command])
|
||||
)
|
||||
else:
|
||||
if temp_command is not None:
|
||||
await self._api(temp_command)
|
||||
if command is not None:
|
||||
await self._api(command)
|
||||
await self._api(command)
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
},
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pytradfri"],
|
||||
"requirements": ["pytradfri[async]==9.0.1"]
|
||||
"requirements": ["pytradfri[async]==14.0.0"]
|
||||
}
|
||||
|
||||
32
homeassistant/components/tradfri/migration.py
Normal file
32
homeassistant/components/tradfri/migration.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Provide migration tools for the Tradfri integration."""
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def migrate_device_identifier(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Migrate device identifier to new format."""
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry.entry_id
|
||||
)
|
||||
|
||||
for device_entry in device_entries:
|
||||
device_identifiers = set(device_entry.identifiers)
|
||||
|
||||
for identifier in device_entry.identifiers:
|
||||
if identifier[0] == DOMAIN and isinstance(
|
||||
identifier[1], int # type: ignore[unreachable]
|
||||
):
|
||||
device_identifiers.remove(identifier) # type: ignore[unreachable]
|
||||
# Copy pytradfri device id to string.
|
||||
device_identifiers.add((DOMAIN, str(identifier[1])))
|
||||
break
|
||||
|
||||
if device_identifiers != device_entry.identifiers:
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, new_identifiers=device_identifiers
|
||||
)
|
||||
19
homeassistant/components/tradfri/models.py
Normal file
19
homeassistant/components/tradfri/models.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""Provide a model for the Tradfri integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pytradfri import Gateway
|
||||
from pytradfri.api.aiocoap_api import APIFactory, APIRequestProtocol
|
||||
|
||||
from .coordinator import TradfriDeviceDataUpdateCoordinator
|
||||
|
||||
|
||||
@dataclass
|
||||
class TradfriData:
|
||||
"""Data for the Tradfri integration."""
|
||||
|
||||
api: APIRequestProtocol
|
||||
coordinators: list[TradfriDeviceDataUpdateCoordinator]
|
||||
factory: APIFactory
|
||||
gateway: Gateway
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from pytradfri.command import Command
|
||||
from pytradfri.api.aiocoap_api import APIRequestProtocol
|
||||
from pytradfri.device import Device
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -26,16 +26,10 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
CONF_GATEWAY_ID,
|
||||
COORDINATOR,
|
||||
COORDINATOR_LIST,
|
||||
DOMAIN,
|
||||
KEY_API,
|
||||
LOGGER,
|
||||
)
|
||||
from .const import CONF_GATEWAY_ID, DOMAIN, LOGGER
|
||||
from .coordinator import TradfriDeviceDataUpdateCoordinator
|
||||
from .entity import TradfriBaseEntity
|
||||
from .models import TradfriData
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -49,22 +43,20 @@ def _get_air_quality(device: Device) -> int | None:
|
||||
"""Fetch the air quality value."""
|
||||
assert device.air_purifier_control is not None
|
||||
if (
|
||||
device.air_purifier_control.air_purifiers[0].air_quality == 65535
|
||||
): # The sensor returns 65535 if the fan is turned off
|
||||
device_control := device.air_purifier_control
|
||||
) is None or device_control.air_purifiers[
|
||||
0
|
||||
].air_quality == 65535: # The sensor returns 65535 if the fan is turned off
|
||||
return None
|
||||
|
||||
return cast(int, device.air_purifier_control.air_purifiers[0].air_quality)
|
||||
return device_control.air_purifiers[0].air_quality
|
||||
|
||||
|
||||
def _get_filter_time_left(device: Device) -> int:
|
||||
"""Fetch the filter's remaining lifetime (in hours)."""
|
||||
assert device.air_purifier_control is not None
|
||||
return round(
|
||||
cast(
|
||||
int, device.air_purifier_control.air_purifiers[0].filter_lifetime_remaining
|
||||
)
|
||||
/ 60
|
||||
)
|
||||
device_control = device.air_purifier_control
|
||||
assert device_control # air_purifier_control is ensured when creating the entity
|
||||
return round(device_control.air_purifiers[0].filter_lifetime_remaining / 60)
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS_BATTERY: tuple[TradfriSensorEntityDescription, ...] = (
|
||||
@@ -73,7 +65,7 @@ SENSOR_DESCRIPTIONS_BATTERY: tuple[TradfriSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value=lambda device: cast(int, device.device_info.battery_level),
|
||||
value=lambda device: device.device_info.battery_level,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -132,12 +124,12 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up a Tradfri config entry."""
|
||||
gateway_id = config_entry.data[CONF_GATEWAY_ID]
|
||||
coordinator_data = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
|
||||
api = coordinator_data[KEY_API]
|
||||
tradfri_data: TradfriData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api = tradfri_data.api
|
||||
|
||||
entities: list[TradfriSensor] = []
|
||||
|
||||
for device_coordinator in coordinator_data[COORDINATOR_LIST]:
|
||||
for device_coordinator in tradfri_data.coordinators:
|
||||
if (
|
||||
not device_coordinator.device.has_light_control
|
||||
and not device_coordinator.device.has_socket_control
|
||||
@@ -178,7 +170,7 @@ class TradfriSensor(TradfriBaseEntity, SensorEntity):
|
||||
def __init__(
|
||||
self,
|
||||
device_coordinator: TradfriDeviceDataUpdateCoordinator,
|
||||
api: Callable[[Command | list[Command]], Any],
|
||||
api: APIRequestProtocol,
|
||||
gateway_id: str,
|
||||
description: TradfriSensorEntityDescription,
|
||||
) -> None:
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from pytradfri.command import Command
|
||||
from pytradfri.api.aiocoap_api import APIRequestProtocol
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import CONF_GATEWAY_ID, COORDINATOR, COORDINATOR_LIST, DOMAIN, KEY_API
|
||||
from .const import CONF_GATEWAY_ID, DOMAIN
|
||||
from .coordinator import TradfriDeviceDataUpdateCoordinator
|
||||
from .entity import TradfriBaseEntity
|
||||
from .models import TradfriData
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -24,8 +24,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Load Tradfri switches based on a config entry."""
|
||||
gateway_id = config_entry.data[CONF_GATEWAY_ID]
|
||||
coordinator_data = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
|
||||
api = coordinator_data[KEY_API]
|
||||
tradfri_data: TradfriData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api = tradfri_data.api
|
||||
|
||||
async_add_entities(
|
||||
TradfriSwitch(
|
||||
@@ -33,7 +33,7 @@ async def async_setup_entry(
|
||||
api,
|
||||
gateway_id,
|
||||
)
|
||||
for device_coordinator in coordinator_data[COORDINATOR_LIST]
|
||||
for device_coordinator in tradfri_data.coordinators
|
||||
if device_coordinator.device.has_socket_control
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ class TradfriSwitch(TradfriBaseEntity, SwitchEntity):
|
||||
def __init__(
|
||||
self,
|
||||
device_coordinator: TradfriDeviceDataUpdateCoordinator,
|
||||
api: Callable[[Command | list[Command]], Any],
|
||||
api: APIRequestProtocol,
|
||||
gateway_id: str,
|
||||
) -> None:
|
||||
"""Initialize a switch."""
|
||||
@@ -56,28 +56,24 @@ class TradfriSwitch(TradfriBaseEntity, SwitchEntity):
|
||||
gateway_id=gateway_id,
|
||||
)
|
||||
|
||||
self._device_control = self._device.socket_control
|
||||
self._device_data = self._device_control.sockets[0]
|
||||
device_control = self._device.socket_control
|
||||
assert device_control # socket_control is ensured when creating the entity
|
||||
self._device_control = device_control
|
||||
self._device_data = device_control.sockets[0]
|
||||
|
||||
def _refresh(self) -> None:
|
||||
"""Refresh the device."""
|
||||
self._device_data = self.coordinator.data.socket_control.sockets[0]
|
||||
self._device_data = self._device_control.sockets[0]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if switch is on."""
|
||||
if not self._device_data:
|
||||
return False
|
||||
return cast(bool, self._device_data.state)
|
||||
return self._device_data.state
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Instruct the switch to turn off."""
|
||||
if not self._device_control:
|
||||
return
|
||||
await self._api(self._device_control.set_state(False))
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the switch to turn on."""
|
||||
if not self._device_control:
|
||||
return
|
||||
await self._api(self._device_control.set_state(True))
|
||||
|
||||
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -2498,7 +2498,7 @@ pytouchlinesl==0.3.0
|
||||
pytraccar==2.1.1
|
||||
|
||||
# homeassistant.components.tradfri
|
||||
pytradfri[async]==9.0.1
|
||||
pytradfri[async]==14.0.0
|
||||
|
||||
# homeassistant.components.trafikverket_camera
|
||||
# homeassistant.components.trafikverket_ferry
|
||||
|
||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -2022,7 +2022,7 @@ pytouchlinesl==0.3.0
|
||||
pytraccar==2.1.1
|
||||
|
||||
# homeassistant.components.tradfri
|
||||
pytradfri[async]==9.0.1
|
||||
pytradfri[async]==14.0.0
|
||||
|
||||
# homeassistant.components.trafikverket_camera
|
||||
# homeassistant.components.trafikverket_ferry
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
"""Common tools used for the Tradfri test suite."""
|
||||
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from pytradfri.command import Command
|
||||
from pytradfri.const import ATTR_ID
|
||||
from pytradfri.device import Device
|
||||
from pytradfri.device import Device, DeviceResponse
|
||||
from pytradfri.gateway import Gateway
|
||||
|
||||
from homeassistant.components import tradfri
|
||||
@@ -25,13 +23,13 @@ class CommandStore:
|
||||
mock_responses: dict[str, Any]
|
||||
|
||||
def register_device(
|
||||
self, gateway: Gateway, device_response: dict[str, Any]
|
||||
self, gateway: Gateway, device_response: DeviceResponse
|
||||
) -> None:
|
||||
"""Register device response."""
|
||||
get_devices_command = gateway.get_devices()
|
||||
self.register_response(get_devices_command, [device_response[ATTR_ID]])
|
||||
get_device_command = gateway.get_device(device_response[ATTR_ID])
|
||||
self.register_response(get_device_command, device_response)
|
||||
self.register_response(get_devices_command, [device_response.id])
|
||||
get_device_command = gateway.get_device(str(device_response.id))
|
||||
self.register_response(get_device_command, device_response.dict(by_alias=True))
|
||||
|
||||
def register_response(self, command: Command, response: Any) -> None:
|
||||
"""Register command response."""
|
||||
@@ -62,7 +60,7 @@ class CommandStore:
|
||||
assert observe_command
|
||||
|
||||
device_path = "/".join(str(v) for v in device.path)
|
||||
device_state = deepcopy(device.raw)
|
||||
device_state = device.raw.dict(by_alias=True)
|
||||
|
||||
# Create a default observed state based on the sent commands.
|
||||
for command in self.sent_commands:
|
||||
|
||||
@@ -9,13 +9,12 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from pytradfri.command import Command
|
||||
from pytradfri.const import ATTR_FIRMWARE_VERSION, ATTR_GATEWAY_ID
|
||||
from pytradfri.device import Device
|
||||
from pytradfri.gateway import Gateway
|
||||
|
||||
from homeassistant.components.tradfri.const import DOMAIN
|
||||
|
||||
from . import GATEWAY_ID, TRADFRI_PATH
|
||||
from . import TRADFRI_PATH
|
||||
from .common import CommandStore
|
||||
|
||||
from tests.common import load_fixture
|
||||
@@ -30,12 +29,12 @@ def mock_entry_setup() -> Generator[AsyncMock]:
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_gateway", autouse=True)
|
||||
def mock_gateway_fixture(command_store: CommandStore) -> Gateway:
|
||||
def mock_gateway_fixture(command_store: CommandStore, gateway_response: str) -> Gateway:
|
||||
"""Mock a Tradfri gateway."""
|
||||
gateway = Gateway()
|
||||
command_store.register_response(
|
||||
gateway.get_gateway_info(),
|
||||
{ATTR_GATEWAY_ID: GATEWAY_ID, ATTR_FIRMWARE_VERSION: "1.2.1234"},
|
||||
json.loads(gateway_response),
|
||||
)
|
||||
command_store.register_response(
|
||||
gateway.get_devices(),
|
||||
@@ -96,6 +95,12 @@ def device(
|
||||
return device
|
||||
|
||||
|
||||
@pytest.fixture(scope="package")
|
||||
def gateway_response() -> str:
|
||||
"""Return a gateway response."""
|
||||
return load_fixture("gateway.json", DOMAIN)
|
||||
|
||||
|
||||
@pytest.fixture(scope="package")
|
||||
def air_purifier() -> str:
|
||||
"""Return an air purifier response."""
|
||||
|
||||
44
tests/components/tradfri/fixtures/gateway.json
Normal file
44
tests/components/tradfri/fixtures/gateway.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"9023": "xyz.pool.ntp.pool",
|
||||
"9029": "1.2.1234",
|
||||
"9054": 0,
|
||||
"9055": 0,
|
||||
"9059": 1509788799,
|
||||
"9060": "2017-11-04T09:46:39.046784Z",
|
||||
"9061": 0,
|
||||
"9062": 0,
|
||||
"9066": 5,
|
||||
"9069": 1509474847,
|
||||
"9071": 1,
|
||||
"9072": 0,
|
||||
"9073": 0,
|
||||
"9074": 0,
|
||||
"9075": 0,
|
||||
"9076": 0,
|
||||
"9077": 0,
|
||||
"9078": 0,
|
||||
"9079": 0,
|
||||
"9080": 0,
|
||||
"9081": "mock-gateway-id",
|
||||
"9082": true,
|
||||
"9083": "123-45-67",
|
||||
"9092": 0,
|
||||
"9093": 0,
|
||||
"9103": "blablablabla12.iot.eu-central-1.amazonaws.com",
|
||||
"9105": 0,
|
||||
"9106": 0,
|
||||
"9107": 0,
|
||||
"9118": 0,
|
||||
"9200": "abc12345-a123-b345-c567-123abc123456",
|
||||
"9201": 1,
|
||||
"9202": 1234567890,
|
||||
"9204": 1,
|
||||
"9208": 1234567890,
|
||||
"9209": 1234567890,
|
||||
"9211": 0,
|
||||
"9232": 1234567,
|
||||
"9234": 1,
|
||||
"9235": "SE",
|
||||
"9236": 3600,
|
||||
"9266": 2
|
||||
}
|
||||
@@ -93,7 +93,7 @@ async def test_fan_available(
|
||||
ATTR_AIR_PURIFIER_MODE: 0,
|
||||
},
|
||||
STATE_OFF,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
),
|
||||
(
|
||||
@@ -145,7 +145,7 @@ async def test_fan_available(
|
||||
ATTR_AIR_PURIFIER_MODE: 0,
|
||||
},
|
||||
STATE_OFF,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -246,7 +246,9 @@ async def test_turn_on(
|
||||
) -> None:
|
||||
"""Test turning on a light."""
|
||||
# Make sure the light is off.
|
||||
device.raw[ATTR_LIGHT_CONTROL][0][ATTR_DEVICE_STATE] = 0
|
||||
light_control = device.raw.light_control
|
||||
assert light_control
|
||||
light_control[0].state = 0
|
||||
await setup_integration(hass)
|
||||
|
||||
await hass.services.async_call(
|
||||
|
||||
48
tests/components/tradfri/test_migration.py
Normal file
48
tests/components/tradfri/test_migration.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""Test the tradfri migration tools."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from pytradfri.device import Device
|
||||
|
||||
from homeassistant.components.tradfri.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
|
||||
from . import GATEWAY_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["air_purifier"], indirect=True)
|
||||
async def test_migrate_device_identifier(
|
||||
hass: HomeAssistant,
|
||||
mock_api_factory: MagicMock,
|
||||
device: Device,
|
||||
) -> None:
|
||||
"""Test migrate device identifier."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"host": "mock-host",
|
||||
"identity": "mock-identity",
|
||||
"key": "mock-key",
|
||||
"gateway_id": GATEWAY_ID,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, 65551)}, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
assert device_entry.identifiers == {(DOMAIN, 65551)} # type: ignore[comparison-overlap]
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
migrated_device_entry = device_registry.async_get(device_entry.id)
|
||||
|
||||
assert migrated_device_entry
|
||||
assert migrated_device_entry.identifiers == {(DOMAIN, "65551")}
|
||||
Reference in New Issue
Block a user