Compare commits

...

10 Commits

Author SHA1 Message Date
Daniel Hjelseth Høyer
fc1e0e4428 Tibber refresh token for real time streaming
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-19 06:36:52 +01:00
Jamie Magee
836353015b Detect new garage doors automatically in aladdin_connect (#165004) 2026-03-17 20:04:31 +01:00
Allen Porter
c57ffd4d78 Update python-roborock dependency to 4.25.0. (#165800)
Co-authored-by: Ludovic BOUÉ <lboue@users.noreply.github.com>
2026-03-17 19:58:18 +01:00
prana-dev-official
cbebfdf149 Add number platform for Prana integration (#165816) 2026-03-17 19:53:50 +01:00
Ludovic BOUÉ
d8ed9ca66f Fix timestamps in chess_com test diagnostics (#165829) 2026-03-17 19:30:08 +01:00
Cody
5caf8a5b83 Make Season integration timezone aware (#164876) 2026-03-17 18:09:25 +01:00
Aidan Timson
c05210683e Demo valve registry entry and device (#165803)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 17:37:21 +01:00
Joost Lekkerkerker
aa8dd4bb66 Add microfiber filter fixture to SmartThings (#165808) 2026-03-17 17:21:51 +01:00
Joost Lekkerkerker
ee7d6157d9 Fix Indevolt button snapshot (#165812) 2026-03-17 17:19:03 +01:00
Manu
adec1d128c Add exception handling to media source in Radio Browser integration (#164653) 2026-03-17 17:13:11 +01:00
39 changed files with 1383 additions and 147 deletions

View File

@@ -46,19 +46,10 @@ async def async_setup_entry(
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
)
try:
doors = await client.get_doors()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(err) from err
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err
coordinator = AladdinConnectCoordinator(hass, entry, client)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = {
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)
for door in doors
}
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -100,7 +91,7 @@ def remove_stale_devices(
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
all_device_ids = set(config_entry.runtime_data)
all_device_ids = set(config_entry.runtime_data.data)
for device_entry in device_entries:
device_id: str | None = None

View File

@@ -11,22 +11,24 @@ from genie_partner_sdk.model import GarageDoor
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
_LOGGER = logging.getLogger(__name__)
type AladdinConnectConfigEntry = ConfigEntry[dict[str, AladdinConnectCoordinator]]
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
SCAN_INTERVAL = timedelta(seconds=15)
class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
class AladdinConnectCoordinator(DataUpdateCoordinator[dict[str, GarageDoor]]):
"""Coordinator for Aladdin Connect integration."""
config_entry: AladdinConnectConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: AladdinConnectConfigEntry,
client: AladdinConnectClient,
garage_door: GarageDoor,
) -> None:
"""Initialize the coordinator."""
super().__init__(
@@ -37,18 +39,16 @@ class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
update_interval=SCAN_INTERVAL,
)
self.client = client
self.data = garage_door
async def _async_update_data(self) -> GarageDoor:
async def _async_update_data(self) -> dict[str, GarageDoor]:
"""Fetch data from the Aladdin Connect API."""
try:
await self.client.update_door(self.data.device_id, self.data.door_number)
doors = await self.client.get_doors()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(err) from err
raise UpdateFailed(f"Error communicating with API: {err}") from err
except aiohttp.ClientError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
self.data.status = self.client.get_door_status(
self.data.device_id, self.data.door_number
)
self.data.battery_level = self.client.get_battery_status(
self.data.device_id, self.data.door_number
)
return self.data
return {door.unique_id: door for door in doors}

View File

@@ -7,7 +7,7 @@ from typing import Any
import aiohttp
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -24,11 +24,22 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the cover platform."""
coordinators = entry.runtime_data
coordinator = entry.runtime_data
known_devices: set[str] = set()
async_add_entities(
AladdinCoverEntity(coordinator) for coordinator in coordinators.values()
)
@callback
def _async_add_new_devices() -> None:
"""Detect and add entities for new doors."""
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AladdinCoverEntity(coordinator, door_id) for door_id in new_devices
)
_async_add_new_devices()
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
@@ -38,10 +49,10 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
_attr_supported_features = SUPPORTED_FEATURES
_attr_name = None
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
"""Initialize the Aladdin Connect cover."""
super().__init__(coordinator)
self._attr_unique_id = coordinator.data.unique_id
super().__init__(coordinator, door_id)
self._attr_unique_id = door_id
async def async_open_cover(self, **kwargs: Any) -> None:
"""Issue open command to cover."""
@@ -66,16 +77,16 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
@property
def is_closed(self) -> bool | None:
"""Update is closed attribute."""
if (status := self.coordinator.data.status) is None:
if (status := self.door.status) is None:
return None
return status == "closed"
@property
def is_closing(self) -> bool | None:
"""Update is closing attribute."""
return self.coordinator.data.status == "closing"
return self.door.status == "closing"
@property
def is_opening(self) -> bool | None:
"""Update is opening attribute."""
return self.coordinator.data.status == "opening"
return self.door.status == "opening"

View File

@@ -20,13 +20,13 @@ async def async_get_config_entry_diagnostics(
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
"doors": {
uid: {
"device_id": coordinator.data.device_id,
"door_number": coordinator.data.door_number,
"name": coordinator.data.name,
"status": coordinator.data.status,
"link_status": coordinator.data.link_status,
"battery_level": coordinator.data.battery_level,
"device_id": door.device_id,
"door_number": door.door_number,
"name": door.name,
"status": door.status,
"link_status": door.link_status,
"battery_level": door.battery_level,
}
for uid, coordinator in config_entry.runtime_data.items()
for uid, door in config_entry.runtime_data.data.items()
},
}

View File

@@ -1,6 +1,7 @@
"""Base class for Aladdin Connect entities."""
from genie_partner_sdk.client import AladdinConnectClient
from genie_partner_sdk.model import GarageDoor
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -14,17 +15,28 @@ class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
_attr_has_entity_name = True
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
"""Initialize Aladdin Connect entity."""
super().__init__(coordinator)
device = coordinator.data
self._door_id = door_id
door = self.door
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.unique_id)},
identifiers={(DOMAIN, door.unique_id)},
manufacturer="Aladdin Connect",
name=device.name,
name=door.name,
)
self._device_id = device.device_id
self._number = device.door_number
self._device_id = door.device_id
self._number = door.door_number
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._door_id in self.coordinator.data
@property
def door(self) -> GarageDoor:
"""Return the garage door data."""
return self.coordinator.data[self._door_id]
@property
def client(self) -> AladdinConnectClient:

View File

@@ -57,7 +57,7 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: todo
dynamic-devices: done
entity-category: done
entity-device-class: done
entity-disabled-by-default: done

View File

@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
@@ -49,13 +49,24 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Aladdin Connect sensor devices."""
coordinators = entry.runtime_data
coordinator = entry.runtime_data
known_devices: set[str] = set()
async_add_entities(
AladdinConnectSensor(coordinator, description)
for coordinator in coordinators.values()
for description in SENSOR_TYPES
)
@callback
def _async_add_new_devices() -> None:
"""Detect and add entities for new doors."""
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AladdinConnectSensor(coordinator, door_id, description)
for door_id in new_devices
for description in SENSOR_TYPES
)
_async_add_new_devices()
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
@@ -66,14 +77,15 @@ class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
def __init__(
self,
coordinator: AladdinConnectCoordinator,
door_id: str,
entity_description: AladdinConnectSensorEntityDescription,
) -> None:
"""Initialize the Aladdin Connect sensor."""
super().__init__(coordinator)
super().__init__(coordinator, door_id)
self.entity_description = entity_description
self._attr_unique_id = f"{coordinator.data.unique_id}-{entity_description.key}"
self._attr_unique_id = f"{door_id}-{entity_description.key}"
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data)
return self.entity_description.value_fn(self.door)

View File

@@ -9,9 +9,12 @@ from typing import Any
from homeassistant.components.valve import ValveEntity, ValveEntityFeature, ValveState
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_track_utc_time_change
from . import DOMAIN
OPEN_CLOSE_DELAY = 2 # Used to give a realistic open/close experience in frontend
@@ -23,10 +26,10 @@ async def async_setup_entry(
"""Set up the Demo config entry."""
async_add_entities(
[
DemoValve("Front Garden", ValveState.OPEN),
DemoValve("Orchard", ValveState.CLOSED),
DemoValve("Back Garden", ValveState.CLOSED, position=70),
DemoValve("Trees", ValveState.CLOSED, position=30),
DemoValve("valve_1", "Front Garden", ValveState.OPEN),
DemoValve("valve_2", "Orchard", ValveState.CLOSED),
DemoValve("valve_3", "Back Garden", ValveState.CLOSED, position=70),
DemoValve("valve_4", "Trees", ValveState.CLOSED, position=30),
]
)
@@ -34,17 +37,24 @@ async def async_setup_entry(
class DemoValve(ValveEntity):
"""Representation of a Demo valve."""
_attr_has_entity_name = True
_attr_name = None
_attr_should_poll = False
def __init__(
self,
unique_id: str,
name: str,
state: str,
moveable: bool = True,
position: int | None = None,
) -> None:
"""Initialize the valve."""
self._attr_name = name
self._attr_unique_id = unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=name,
)
if moveable:
self._attr_supported_features = (
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE

View File

@@ -14,7 +14,7 @@ from .coordinator import PranaConfigEntry, PranaCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.FAN, Platform.SENSOR, Platform.SWITCH]
PLATFORMS = [Platform.FAN, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: PranaConfigEntry) -> bool:

View File

@@ -8,6 +8,20 @@
"default": "mdi:arrow-expand-left"
}
},
"number": {
"display_brightness": {
"default": "mdi:brightness-6",
"state": {
"0": "mdi:brightness-2",
"1": "mdi:brightness-4",
"2": "mdi:brightness-4",
"3": "mdi:brightness-5",
"4": "mdi:brightness-5",
"5": "mdi:brightness-7",
"6": "mdi:brightness-7"
}
}
},
"sensor": {
"inside_temperature": {
"default": "mdi:home-thermometer"

View File

@@ -0,0 +1,80 @@
"""Number platform for Prana integration."""
from collections.abc import Callable
from dataclasses import dataclass
from enum import StrEnum
from typing import Any
from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import PranaConfigEntry, PranaCoordinator
from .entity import PranaBaseEntity
PARALLEL_UPDATES = 1
class PranaNumberType(StrEnum):
"""Enumerates Prana number types exposed by the device API."""
DISPLAY_BRIGHTNESS = "display_brightness"
@dataclass(frozen=True, kw_only=True)
class PranaNumberEntityDescription(NumberEntityDescription):
"""Description of a Prana number entity."""
key: PranaNumberType
value_fn: Callable[[PranaCoordinator], float | None]
set_value_fn: Callable[[Any, float], Any]
ENTITIES: tuple[PranaNumberEntityDescription, ...] = (
PranaNumberEntityDescription(
key=PranaNumberType.DISPLAY_BRIGHTNESS,
translation_key="display_brightness",
native_min_value=0,
native_max_value=6,
native_step=1,
mode=NumberMode.SLIDER,
entity_category=EntityCategory.CONFIG,
value_fn=lambda coord: coord.data.brightness,
set_value_fn=lambda api, val: api.set_brightness(
0 if val == 0 else 2 ** (int(val) - 1)
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: PranaConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Prana number entities from a config entry."""
async_add_entities(
PranaNumber(entry.runtime_data, entity_description)
for entity_description in ENTITIES
)
class PranaNumber(PranaBaseEntity, NumberEntity):
"""Representation of a Prana number entity."""
entity_description: PranaNumberEntityDescription
@property
def native_value(self) -> float | None:
"""Return the entity value."""
return self.entity_description.value_fn(self.coordinator)
async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
await self.entity_description.set_value_fn(self.coordinator.api_client, value)
await self.coordinator.async_refresh()

View File

@@ -49,6 +49,11 @@
}
}
},
"number": {
"display_brightness": {
"name": "Display brightness"
}
},
"sensor": {
"inside_temperature": {
"name": "Inside temperature"

View File

@@ -4,10 +4,11 @@ from __future__ import annotations
import mimetypes
from aiodns.error import DNSError
import pycountry
from radios import FilterBy, Order, RadioBrowser, Station
from radios import FilterBy, Order, RadioBrowser, RadioBrowserError, Station
from homeassistant.components.media_player import MediaClass, MediaType
from homeassistant.components.media_player import BrowseError, MediaClass, MediaType
from homeassistant.components.media_source import (
BrowseMediaSource,
MediaSource,
@@ -15,6 +16,7 @@ from homeassistant.components.media_source import (
PlayMedia,
Unresolvable,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, callback
from homeassistant.util.location import vincenty
@@ -55,9 +57,20 @@ class RadioMediaSource(MediaSource):
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve selected Radio station to a streaming URL."""
radios = self.radios
station = await radios.station(uuid=item.identifier)
if self.entry.state != ConfigEntryState.LOADED:
raise Unresolvable(
translation_domain=DOMAIN,
translation_key="config_entry_not_ready",
)
radios = self.radios
try:
station = await radios.station(uuid=item.identifier)
except (DNSError, RadioBrowserError) as e:
raise Unresolvable(
translation_domain=DOMAIN,
translation_key="radio_browser_error",
) from e
if not station:
raise Unresolvable("Radio station is no longer available")
@@ -74,25 +87,37 @@ class RadioMediaSource(MediaSource):
item: MediaSourceItem,
) -> BrowseMediaSource:
"""Return media."""
if self.entry.state != ConfigEntryState.LOADED:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="config_entry_not_ready",
)
radios = self.radios
return BrowseMediaSource(
domain=DOMAIN,
identifier=None,
media_class=MediaClass.CHANNEL,
media_content_type=MediaType.MUSIC,
title=self.entry.title,
can_play=False,
can_expand=True,
children_media_class=MediaClass.DIRECTORY,
children=[
*await self._async_build_popular(radios, item),
*await self._async_build_by_tag(radios, item),
*await self._async_build_by_language(radios, item),
*await self._async_build_local(radios, item),
*await self._async_build_by_country(radios, item),
],
)
try:
return BrowseMediaSource(
domain=DOMAIN,
identifier=None,
media_class=MediaClass.CHANNEL,
media_content_type=MediaType.MUSIC,
title=self.entry.title,
can_play=False,
can_expand=True,
children_media_class=MediaClass.DIRECTORY,
children=[
*await self._async_build_popular(radios, item),
*await self._async_build_by_tag(radios, item),
*await self._async_build_by_language(radios, item),
*await self._async_build_local(radios, item),
*await self._async_build_by_country(radios, item),
],
)
except (DNSError, RadioBrowserError) as e:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="radio_browser_error",
) from e
@callback
@staticmethod

View File

@@ -5,5 +5,13 @@
"description": "Do you want to add Radio Browser to Home Assistant?"
}
}
},
"exceptions": {
"config_entry_not_ready": {
"message": "Radio Browser integration is not ready"
},
"radio_browser_error": {
"message": "Error occurred while communicating with Radio Browser"
}
}
}

View File

@@ -20,7 +20,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==4.20.0",
"python-roborock==4.25.0",
"vacuum-map-parser-roborock==0.1.4"
]
}

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import date, datetime
from datetime import datetime
import ephem
@@ -12,7 +12,7 @@ from homeassistant.const import CONF_TYPE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.dt import utcnow
from homeassistant.util import dt as dt_util
from .const import DOMAIN, TYPE_ASTRONOMICAL
@@ -50,7 +50,7 @@ async def async_setup_entry(
def get_season(
current_date: date, hemisphere: str, season_tracking_type: str
current_datetime: datetime, hemisphere: str, season_tracking_type: str
) -> str | None:
"""Calculate the current season."""
@@ -58,22 +58,36 @@ def get_season(
return None
if season_tracking_type == TYPE_ASTRONOMICAL:
spring_start = ephem.next_equinox(str(current_date.year)).datetime()
summer_start = ephem.next_solstice(str(current_date.year)).datetime()
autumn_start = ephem.next_equinox(spring_start).datetime()
winter_start = ephem.next_solstice(summer_start).datetime()
spring_start = (
ephem.next_equinox(str(current_datetime.year))
.datetime()
.replace(tzinfo=dt_util.UTC)
)
summer_start = (
ephem.next_solstice(str(current_datetime.year))
.datetime()
.replace(tzinfo=dt_util.UTC)
)
autumn_start = (
ephem.next_equinox(spring_start).datetime().replace(tzinfo=dt_util.UTC)
)
winter_start = (
ephem.next_solstice(summer_start).datetime().replace(tzinfo=dt_util.UTC)
)
else:
spring_start = datetime(2017, 3, 1).replace(year=current_date.year)
spring_start = current_datetime.replace(
month=3, day=1, hour=0, minute=0, second=0, microsecond=0
)
summer_start = spring_start.replace(month=6)
autumn_start = spring_start.replace(month=9)
winter_start = spring_start.replace(month=12)
season = STATE_WINTER
if spring_start <= current_date < summer_start:
if spring_start <= current_datetime < summer_start:
season = STATE_SPRING
elif summer_start <= current_date < autumn_start:
elif summer_start <= current_datetime < autumn_start:
season = STATE_SUMMER
elif autumn_start <= current_date < winter_start:
elif autumn_start <= current_datetime < winter_start:
season = STATE_AUTUMN
# If user is located in the southern hemisphere swap the season
@@ -104,6 +118,4 @@ class SeasonSensorEntity(SensorEntity):
def update(self) -> None:
"""Update season."""
self._attr_native_value = get_season(
utcnow().replace(tzinfo=None), self.hemisphere, self.type
)
self._attr_native_value = get_season(dt_util.now(), self.hemisphere, self.type)

View File

@@ -55,7 +55,7 @@ class TibberRuntimeData:
time_zone=dt_util.get_default_time_zone(),
ssl=ssl_util.get_default_context(),
)
self._client.set_access_token(access_token)
await self._client.set_access_token(access_token)
return self._client

2
requirements_all.txt generated
View File

@@ -2651,7 +2651,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==4.20.0
python-roborock==4.25.0
# homeassistant.components.smarttub
python-smarttub==0.0.47

View File

@@ -2247,7 +2247,7 @@ python-pooldose==0.8.6
python-rabbitair==0.0.8
# homeassistant.components.roborock
python-roborock==4.20.0
python-roborock==4.25.0
# homeassistant.components.smarttub
python-smarttub==0.0.47

View File

@@ -125,7 +125,7 @@ async def test_cover_unavailable(
assert state is not None
assert state.state != STATE_UNAVAILABLE
mock_aladdin_connect_api.update_door.side_effect = aiohttp.ClientError()
mock_aladdin_connect_api.get_doors.side_effect = aiohttp.ClientError()
freezer.tick(15)
async_fire_time_changed(hass)
await hass.async_block_till_done()

View File

@@ -5,16 +5,17 @@ from unittest.mock import AsyncMock, patch
from aiohttp import ClientConnectionError, RequestInfo
from aiohttp.client_exceptions import ClientResponseError
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.aladdin_connect import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import init_integration
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_setup_entry(
@@ -137,3 +138,49 @@ async def test_remove_stale_devices(
)
assert len(device_entries) == 1
assert device_entries[0].identifiers == {(DOMAIN, "test_device_id-1")}
async def test_dynamic_devices(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_aladdin_connect_api: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test new devices are automatically discovered on coordinator refresh."""
await init_integration(hass, mock_config_entry)
# Initially one door -> one cover entity + one sensor entity
device_entries = dr.async_entries_for_config_entry(
device_registry, mock_config_entry.entry_id
)
assert len(device_entries) == 1
assert hass.states.get("cover.test_door") is not None
# Simulate a new door appearing on the API
mock_door_2 = AsyncMock()
mock_door_2.device_id = "test_device_id_2"
mock_door_2.door_number = 1
mock_door_2.name = "Test Door 2"
mock_door_2.status = "open"
mock_door_2.link_status = "connected"
mock_door_2.battery_level = 80
mock_door_2.unique_id = f"{mock_door_2.device_id}-{mock_door_2.door_number}"
existing_door = mock_aladdin_connect_api.get_doors.return_value[0]
mock_aladdin_connect_api.get_doors.return_value = [existing_door, mock_door_2]
# Trigger coordinator refresh
freezer.tick(15)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Now two devices should exist
device_entries = dr.async_entries_for_config_entry(
device_registry, mock_config_entry.entry_id
)
assert len(device_entries) == 2
# New cover entity should exist
assert hass.states.get("cover.test_door_2") is not None

View File

@@ -49,7 +49,7 @@ async def test_sensor_unavailable(
assert state is not None
assert state.state != STATE_UNAVAILABLE
mock_aladdin_connect_api.update_door.side_effect = aiohttp.ClientError()
mock_aladdin_connect_api.get_doors.side_effect = aiohttp.ClientError()
freezer.tick(15)
async_fire_time_changed(hass)
await hass.async_block_till_done()

View File

@@ -8,8 +8,8 @@
'fide': None,
'followers': 2,
'is_streamer': False,
'joined': '2026-02-20T11:48:14',
'last_online': '2026-03-06T13:32:59',
'joined': '2026-02-20T10:48:14',
'last_online': '2026-03-06T12:32:59',
'location': 'Utrecht',
'name': 'Joost',
'player_id': 532748851,

View File

@@ -1,5 +1,6 @@
"""The tests for the Demo valve platform."""
from collections.abc import Generator
from datetime import timedelta
from unittest.mock import patch
@@ -17,10 +18,9 @@ from homeassistant.components.valve import (
)
from homeassistant.const import ATTR_ENTITY_ID, EVENT_STATE_CHANGED, Platform
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from tests.common import async_capture_events, async_fire_time_changed
from tests.common import MockConfigEntry, async_capture_events, async_fire_time_changed
FRONT_GARDEN = "valve.front_garden"
ORCHARD = "valve.orchard"
@@ -28,7 +28,7 @@ BACK_GARDEN = "valve.back_garden"
@pytest.fixture
async def valve_only() -> None:
def valve_only() -> Generator[None]:
"""Enable only the valve platform."""
with patch(
"homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM",
@@ -38,11 +38,12 @@ async def valve_only() -> None:
@pytest.fixture(autouse=True)
async def setup_comp(hass: HomeAssistant, valve_only: None):
"""Set up demo component."""
assert await async_setup_component(
hass, VALVE_DOMAIN, {VALVE_DOMAIN: {"platform": DOMAIN}}
)
async def setup_comp(hass: HomeAssistant, valve_only: None) -> None:
"""Set up demo component from config entry."""
config_entry = MockConfigEntry(domain=DOMAIN)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
@@ -50,6 +51,7 @@ async def setup_comp(hass: HomeAssistant, valve_only: None):
async def test_closing(hass: HomeAssistant) -> None:
"""Test the closing of a valve."""
state = hass.states.get(FRONT_GARDEN)
assert state is not None
assert state.state == ValveState.OPEN
await hass.async_block_till_done()
@@ -63,9 +65,11 @@ async def test_closing(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert state_changes[0].data["entity_id"] == FRONT_GARDEN
assert state_changes[0].data["new_state"] is not None
assert state_changes[0].data["new_state"].state == ValveState.CLOSING
assert state_changes[1].data["entity_id"] == FRONT_GARDEN
assert state_changes[1].data["new_state"] is not None
assert state_changes[1].data["new_state"].state == ValveState.CLOSED
@@ -73,6 +77,7 @@ async def test_closing(hass: HomeAssistant) -> None:
async def test_opening(hass: HomeAssistant) -> None:
"""Test the opening of a valve."""
state = hass.states.get(ORCHARD)
assert state is not None
assert state.state == ValveState.CLOSED
await hass.async_block_till_done()
@@ -83,15 +88,18 @@ async def test_opening(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert state_changes[0].data["entity_id"] == ORCHARD
assert state_changes[0].data["new_state"] is not None
assert state_changes[0].data["new_state"].state == ValveState.OPENING
assert state_changes[1].data["entity_id"] == ORCHARD
assert state_changes[1].data["new_state"] is not None
assert state_changes[1].data["new_state"].state == ValveState.OPEN
async def test_set_valve_position(hass: HomeAssistant) -> None:
"""Test moving the valve to a specific position."""
state = hass.states.get(BACK_GARDEN)
assert state is not None
assert state.attributes[ATTR_CURRENT_POSITION] == 70
# close to 10%
@@ -102,6 +110,7 @@ async def test_set_valve_position(hass: HomeAssistant) -> None:
blocking=True,
)
state = hass.states.get(BACK_GARDEN)
assert state is not None
assert state.state == ValveState.CLOSING
for _ in range(6):
@@ -110,6 +119,7 @@ async def test_set_valve_position(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
state = hass.states.get(BACK_GARDEN)
assert state is not None
assert state.attributes[ATTR_CURRENT_POSITION] == 10
assert state.state == ValveState.OPEN
@@ -121,6 +131,7 @@ async def test_set_valve_position(hass: HomeAssistant) -> None:
blocking=True,
)
state = hass.states.get(BACK_GARDEN)
assert state is not None
assert state.state == ValveState.OPENING
for _ in range(7):
@@ -129,6 +140,7 @@ async def test_set_valve_position(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
state = hass.states.get(BACK_GARDEN)
assert state is not None
assert state.attributes[ATTR_CURRENT_POSITION] == 80
assert state.state == ValveState.OPEN

View File

@@ -1,8 +1,9 @@
# serializer version: 1
# name: test_button[1][button.bk1600_enable_standby_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
@@ -50,8 +51,9 @@
# ---
# name: test_button[2][button.cms_sf2000_enable_standby_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,

View File

@@ -0,0 +1,60 @@
# serializer version: 1
# name: test_numbers[number.prana_recuperator_display_brightness-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'max': 6,
'min': 0,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.prana_recuperator_display_brightness',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Display brightness',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Display brightness',
'platform': 'prana',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'display_brightness',
'unique_id': 'ECC9FFE0E574_display_brightness',
'unit_of_measurement': None,
})
# ---
# name: test_numbers[number.prana_recuperator_display_brightness-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'PRANA RECUPERATOR Display brightness',
'max': 6,
'min': 0,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.prana_recuperator_display_brightness',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '6',
})
# ---

View File

@@ -0,0 +1,76 @@
"""Integration-style tests for Prana numbers."""
from unittest.mock import MagicMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.number import (
ATTR_VALUE,
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import async_init_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_numbers(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
mock_prana_api: MagicMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test the Prana numbers snapshot."""
with patch("homeassistant.components.prana.PLATFORMS", [Platform.NUMBER]):
await async_init_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
("input_value", "expected_api_value"),
[
(0.0, 0), # 0 -> 0
(1.0, 1), # 2^(1-1) -> 1
(2.0, 2), # 2^(2-1) -> 2
(3.0, 4), # 2^(3-1) -> 4
(4.0, 8), # 2^(4-1) -> 8
(5.0, 16), # 2^(5-1) -> 16
(6.0, 32), # 2^(6-1) -> 32
],
)
async def test_number_actions(
hass: HomeAssistant,
mock_prana_api: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
input_value: float,
expected_api_value: int,
) -> None:
"""Test setting number values calls the API with correct math conversion."""
await async_init_integration(hass, mock_config_entry)
entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert entries
target = "number.prana_recuperator_display_brightness"
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: target,
ATTR_VALUE: input_value,
},
blocking=True,
)
mock_prana_api.set_brightness.assert_called_with(expected_api_value)

View File

@@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from homeassistant.components.radio_browser.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@@ -39,10 +40,15 @@ async def init_integration(
) -> MockConfigEntry:
"""Set up the Radio Browser integration for testing."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
with patch(
"homeassistant.components.radio_browser.RadioBrowser",
autospec=True,
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state == ConfigEntryState.LOADED
return mock_config_entry

View File

@@ -1,15 +1,20 @@
"""Tests for radio_browser media_source."""
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, patch
from aiodns.error import DNSError
import pytest
from radios import FilterBy, Order
from radios import FilterBy, Order, RadioBrowserError
from homeassistant.components import media_source
from homeassistant.components.media_player import BrowseError
from homeassistant.components.radio_browser.media_source import async_get_media_source
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
DOMAIN = "radio_browser"
@@ -71,3 +76,113 @@ async def test_browsing_local(
assert other_browse is not None
assert other_browse.title == "My Radios"
assert len(other_browse.children) == 0
@pytest.mark.parametrize(
"exception",
[DNSError, RadioBrowserError],
)
async def test_browsing_exceptions(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
exception: Exception,
) -> None:
"""Test browsing exceptions."""
with patch(
"homeassistant.components.radio_browser.RadioBrowser",
autospec=True,
) as mock_browser:
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state == ConfigEntryState.LOADED
mock_browser.return_value.stations.side_effect = exception
with pytest.raises(BrowseError) as exc_info:
await media_source.async_browse_media(
hass, f"{media_source.URI_SCHEME}{DOMAIN}/popular"
)
assert exc_info.value.translation_key == "radio_browser_error"
async def test_browsing_not_ready(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test browsing config entry not ready."""
with patch(
"homeassistant.components.radio_browser.RadioBrowser",
autospec=True,
) as mock_browser:
mock_browser.return_value.stats.side_effect = RadioBrowserError
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY
with pytest.raises(BrowseError) as exc_info:
await media_source.async_browse_media(
hass, f"{media_source.URI_SCHEME}{DOMAIN}/popular"
)
assert exc_info.value.translation_key == "config_entry_not_ready"
@pytest.mark.parametrize(
"exception",
[DNSError, RadioBrowserError],
)
async def test_resolve_media_exceptions(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
exception: Exception,
) -> None:
"""Test resolving media exceptions."""
with patch(
"homeassistant.components.radio_browser.RadioBrowser",
autospec=True,
) as mock_browser:
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state == ConfigEntryState.LOADED
mock_browser.return_value.station.side_effect = exception
with pytest.raises(media_source.Unresolvable) as exc_info:
await media_source.async_resolve_media(
hass, f"{media_source.URI_SCHEME}{DOMAIN}/123456", None
)
assert exc_info.value.translation_key == "radio_browser_error"
async def test_resolve_media_not_ready(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test resolving media config entry not ready."""
with patch(
"homeassistant.components.radio_browser.RadioBrowser",
autospec=True,
) as mock_browser:
mock_browser.return_value.stats.side_effect = RadioBrowserError
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY
with pytest.raises(media_source.Unresolvable) as exc_info:
await media_source.async_resolve_media(
hass, f"{media_source.URI_SCHEME}{DOMAIN}/123456", None
)
assert exc_info.value.translation_key == "config_entry_not_ready"

View File

@@ -1,6 +1,7 @@
"""The tests for the Season integration."""
from datetime import datetime
from zoneinfo import ZoneInfo
from freezegun import freeze_time
import pytest
@@ -20,6 +21,8 @@ from homeassistant.components.sensor import ATTR_OPTIONS, SensorDeviceClass
from homeassistant.const import ATTR_DEVICE_CLASS, CONF_TYPE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.util.dt import UTC
from tests.common import MockConfigEntry
@@ -44,25 +47,25 @@ HEMISPHERE_EMPTY = {
}
NORTHERN_PARAMETERS = [
(TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0), STATE_SUMMER),
(TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0), STATE_SUMMER),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0), STATE_AUTUMN),
(TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0), STATE_AUTUMN),
(TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0), STATE_WINTER),
(TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0), STATE_WINTER),
(TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0), STATE_SPRING),
(TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0), STATE_SPRING),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_SUMMER),
(TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0, tzinfo=UTC), STATE_SUMMER),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0, tzinfo=UTC), STATE_AUTUMN),
(TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_AUTUMN),
(TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0, tzinfo=UTC), STATE_WINTER),
(TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0, tzinfo=UTC), STATE_WINTER),
(TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0, tzinfo=UTC), STATE_SPRING),
(TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0, tzinfo=UTC), STATE_SPRING),
]
SOUTHERN_PARAMETERS = [
(TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0), STATE_SUMMER),
(TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0), STATE_SUMMER),
(TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0), STATE_AUTUMN),
(TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0), STATE_AUTUMN),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0), STATE_WINTER),
(TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0), STATE_WINTER),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0), STATE_SPRING),
(TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0), STATE_SPRING),
(TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0, tzinfo=UTC), STATE_SUMMER),
(TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0, tzinfo=UTC), STATE_SUMMER),
(TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0, tzinfo=UTC), STATE_AUTUMN),
(TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0, tzinfo=UTC), STATE_AUTUMN),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_WINTER),
(TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0, tzinfo=UTC), STATE_WINTER),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0, tzinfo=UTC), STATE_SPRING),
(TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_SPRING),
]
@@ -154,7 +157,7 @@ async def test_season_equator(
hass.config.latitude = HEMISPHERE_EQUATOR["homeassistant"]["latitude"]
mock_config_entry.add_to_hass(hass)
with freeze_time(datetime(2017, 9, 3, 0, 0)):
with freeze_time(datetime(2017, 9, 3, 0, 0, tzinfo=UTC)):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
@@ -165,3 +168,43 @@ async def test_season_equator(
entry = entity_registry.async_get("sensor.season")
assert entry
assert entry.unique_id == mock_config_entry.entry_id
async def test_season_local_midnight(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that season changes at local midnight, not UTC."""
await hass.config.async_set_time_zone("Australia/Sydney")
hass.config.latitude = HEMISPHERE_SOUTHERN["homeassistant"]["latitude"]
mock_config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(
mock_config_entry,
unique_id=TYPE_METEOROLOGICAL,
data={CONF_TYPE: TYPE_METEOROLOGICAL},
)
sydney_tz = ZoneInfo("Australia/Sydney")
# The day before autumn starts, at 23:59:59 local time (summer)
day_before = datetime(2017, 2, 28, 23, 59, 59, tzinfo=sydney_tz)
with freeze_time(day_before):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.season")
assert state
assert state.state == STATE_SUMMER
# Exactly midnight local time (autumn)
midnight = datetime(2017, 3, 1, 0, 0, 0, tzinfo=sydney_tz)
with freeze_time(midnight):
await async_update_entity(hass, "sensor.season")
await hass.async_block_till_done()
state = hass.states.get("sensor.season")
assert state
assert state.state == STATE_AUTUMN

View File

@@ -68,6 +68,7 @@ DEVICE_FIXTURES = [
"da_wm_wm_100002",
"da_wm_wm_000001",
"da_wm_wm_000001_1",
"da_wm_mf_01001",
"da_wm_sc_000001",
"da_wm_dw_01011",
"da_rvc_normal_000001",

View File

@@ -0,0 +1,331 @@
{
"components": {
"main": {
"refresh": {},
"execute": {
"data": {
"value": null
}
},
"samsungce.deviceIdentification": {
"micomAssayCode": {
"value": "20349241",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"modelName": {
"value": null
},
"serialNumber": {
"value": null
},
"serialNumberExtra": {
"value": null
},
"releaseCountry": {
"value": null
},
"modelClassificationCode": {
"value": "3A000000001511000A90020200000000",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"description": {
"value": "AMF-WW-TP1-22-COMMON_FT-MF/DC92-03492A_0001",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"releaseYear": {
"value": 21,
"timestamp": "2025-06-16T00:39:32.549Z"
},
"binaryId": {
"value": "AMF-WW-TP1-22-COMMON",
"timestamp": "2026-03-17T09:56:51.547Z"
}
},
"switch": {
"switch": {
"value": "on",
"timestamp": "2026-03-17T09:56:51.608Z"
}
},
"sec.wifiConfiguration": {
"autoReconnection": {
"value": null
},
"minVersion": {
"value": null
},
"supportedWiFiFreq": {
"value": null
},
"supportedAuthType": {
"value": null
},
"protocolType": {
"value": null
}
},
"samsungce.microfiberFilterOperatingState": {
"operatingState": {
"value": "ready",
"timestamp": "2026-03-17T10:27:51.168Z"
},
"supportedJobStates": {
"value": [
"none",
"filtering",
"bypassing",
"waiting",
"stopping",
"sensing"
],
"timestamp": "2026-03-17T07:49:18.985Z"
},
"supportedOperatingStates": {
"value": ["ready", "running", "paused"],
"timestamp": "2026-03-17T07:49:18.985Z"
},
"microfiberFilterJobState": {
"value": "waiting",
"timestamp": "2026-03-17T10:27:51.168Z"
}
},
"samsungce.selfCheck": {
"result": {
"value": null
},
"supportedActions": {
"value": null
},
"progress": {
"value": null
},
"errors": {
"value": null
},
"status": {
"value": null
}
},
"samsungce.softwareVersion": {
"versions": {
"value": [
{
"id": "0",
"swType": "Software",
"versionNumber": "03334A230323(A603)",
"description": "AMF-WW-TP1-22-COMMON|20349241|3A000000001511000A90020200000000"
},
{
"id": "1",
"swType": "Firmware",
"versionNumber": "23051057,FFFFFFFF",
"description": "Firmware_1_DB_20349241230510571FFFFFFFFFFFFFFFFFFFFFFFFFFE(018020349241FFFFFFFF_30000000)(FileDown:0)(Type:0)"
}
],
"timestamp": "2026-03-17T09:56:51.548Z"
}
},
"samsungce.microfiberFilterSettings": {
"bypassMode": {
"value": "disabled",
"timestamp": "2026-03-17T09:56:51.095Z"
}
},
"ocf": {
"st": {
"value": null
},
"mndt": {
"value": null
},
"mnfv": {
"value": "AMF-WW-TP1-22-COMMON_30230323",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnhw": {
"value": "Realtek",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"di": {
"value": "42e80b4d-24c4-a810-11b3-f90375c56a39",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnsl": {
"value": "http://www.samsung.com",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"dmv": {
"value": "res.1.1.0,sh.1.1.0",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"n": {
"value": "[microfiber] Samsung",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnmo": {
"value": "AMF-WW-TP1-22-COMMON|20349241|3A000000001511000A90020200000000",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"vid": {
"value": "DA-WM-MF-01001",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnmn": {
"value": "Samsung Electronics",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnml": {
"value": "http://www.samsung.com",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnpv": {
"value": "DAWIT 2.0",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnos": {
"value": "TizenRT 3.1",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"pi": {
"value": "42e80b4d-24c4-a810-11b3-f90375c56a39",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"icv": {
"value": "core.1.1.0",
"timestamp": "2025-06-18T08:56:52.092Z"
}
},
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": [],
"timestamp": "2026-03-17T09:19:46.018Z"
}
},
"samsungce.driverVersion": {
"versionNumber": {
"value": 25040101,
"timestamp": "2025-06-16T01:24:28.272Z"
}
},
"samsungce.softwareUpdate": {
"targetModule": {
"value": {},
"timestamp": "2026-03-17T09:56:51.716Z"
},
"otnDUID": {
"value": "MTCHUODPC4IYE",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"lastUpdatedDate": {
"value": null
},
"availableModules": {
"value": [],
"timestamp": "2026-03-17T09:56:51.548Z"
},
"newVersionAvailable": {
"value": false,
"timestamp": "2026-03-17T09:56:51.548Z"
},
"operatingState": {
"value": null
},
"progress": {
"value": null
}
},
"sec.diagnosticsInformation": {
"logType": {
"value": ["errCode", "dump"],
"timestamp": "2026-03-17T09:56:51.548Z"
},
"endpoint": {
"value": "SSM",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"minVersion": {
"value": "1.0",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"signinPermission": {
"value": null
},
"setupId": {
"value": "WM0",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"protocolType": {
"value": "wifi_https",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"tsId": {
"value": null
},
"mnId": {
"value": "0AJT",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"dumpType": {
"value": "file",
"timestamp": "2026-03-17T09:56:51.548Z"
}
},
"samsungce.bladeFilter": {
"bladeFilterStatus": {
"value": null
},
"bladeFilterLastResetDate": {
"value": null
},
"bladeFilterUsage": {
"value": null
},
"bladeFilterResetType": {
"value": null
},
"bladeFilterUsageStep": {
"value": null
},
"bladeFilterCapacity": {
"value": null
}
},
"samsungce.microfiberFilterStatus": {
"supportedStatus": {
"value": ["blockage", "normal"],
"timestamp": "2026-03-17T07:49:18.985Z"
},
"status": {
"value": "normal",
"timestamp": "2026-03-17T07:49:18.985Z"
}
},
"custom.waterFilter": {
"waterFilterUsageStep": {
"value": 1,
"timestamp": "2026-03-17T09:19:46.018Z"
},
"waterFilterResetType": {
"value": ["replaceable"],
"timestamp": "2026-03-17T09:19:46.018Z"
},
"waterFilterCapacity": {
"value": 12,
"unit": "Hour",
"timestamp": "2026-03-17T09:19:46.018Z"
},
"waterFilterLastResetDate": {
"value": null
},
"waterFilterUsage": {
"value": 78,
"timestamp": "2026-03-17T10:17:49.492Z"
},
"waterFilterStatus": {
"value": "normal",
"timestamp": "2026-03-17T09:19:46.018Z"
}
}
}
}
}

View File

@@ -0,0 +1,126 @@
{
"items": [
{
"deviceId": "42e80b4d-24c4-a810-11b3-f90375c56a39",
"name": "[microfiber] Samsung",
"label": "Filtro in microfibra",
"manufacturerName": "Samsung Electronics",
"presentationId": "DA-WM-MF-01001",
"deviceManufacturerCode": "Samsung Electronics",
"locationId": "dc3da2dd-adb0-41b9-9367-9dafc2637386",
"ownerId": "98f99f44-0f42-c20c-a48f-e53c911e27c7",
"roomId": "4696910e-f24d-4831-817b-b8b6b49ed885",
"deviceTypeName": "x.com.st.d.microfiberfilter",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "ocf",
"version": 1
},
{
"id": "execute",
"version": 1
},
{
"id": "refresh",
"version": 1
},
{
"id": "switch",
"version": 1
},
{
"id": "samsungce.bladeFilter",
"version": 1
},
{
"id": "samsungce.deviceIdentification",
"version": 1
},
{
"id": "samsungce.driverVersion",
"version": 1
},
{
"id": "samsungce.microfiberFilterOperatingState",
"version": 1
},
{
"id": "samsungce.microfiberFilterSettings",
"version": 1
},
{
"id": "samsungce.microfiberFilterStatus",
"version": 1
},
{
"id": "samsungce.selfCheck",
"version": 1
},
{
"id": "samsungce.softwareUpdate",
"version": 1
},
{
"id": "samsungce.softwareVersion",
"version": 1
},
{
"id": "sec.diagnosticsInformation",
"version": 1
},
{
"id": "sec.wifiConfiguration",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
},
{
"id": "custom.waterFilter",
"version": 1
}
],
"categories": [
{
"name": "MicroFiberFilter",
"categoryType": "manufacturer"
}
],
"optional": false
}
],
"createTime": "2025-05-06T11:48:20.516Z",
"profile": {
"id": "b40c8b41-e933-334b-8597-d721a881e2ee"
},
"ocf": {
"ocfDeviceType": "x.com.st.d.microfiberfilter",
"name": "[microfiber] Samsung",
"specVersion": "core.1.1.0",
"verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0",
"manufacturerName": "Samsung Electronics",
"modelNumber": "AMF-WW-TP1-22-COMMON|20349241|3A000000001511000A90020200000000",
"platformVersion": "DAWIT 2.0",
"platformOS": "TizenRT 3.1",
"hwVersion": "Realtek",
"firmwareVersion": "AMF-WW-TP1-22-COMMON_30230323",
"vendorId": "DA-WM-MF-01001",
"vendorResourceClientServerVersion": "Realtek Release 3.1.220727",
"lastSignupTime": "2025-05-06T11:48:20.456199900Z",
"transferCandidate": false,
"additionalAuthCodeRequired": false
},
"type": "OCF",
"restrictionTier": 0,
"allowed": null,
"executionContext": "CLOUD",
"relationships": []
}
],
"_links": {}
}

View File

@@ -2325,6 +2325,57 @@
'state': 'off',
})
# ---
# name: test_all_entities[da_wm_mf_01001][binary_sensor.filtro_in_microfibra_filter_status-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.filtro_in_microfibra_filter_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Filter status',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Filter status',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'filter_status',
'unique_id': '42e80b4d-24c4-a810-11b3-f90375c56a39_main_custom.waterFilter_waterFilterStatus_waterFilterStatus',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_mf_01001][binary_sensor.filtro_in_microfibra_filter_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Filtro in microfibra Filter status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.filtro_in_microfibra_filter_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_wm_sc_000001][binary_sensor.airdresser_child_lock-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -549,3 +549,53 @@
'state': 'unknown',
})
# ---
# name: test_all_entities[da_wm_mf_01001][button.filtro_in_microfibra_reset_water_filter-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.filtro_in_microfibra_reset_water_filter',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Reset water filter',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset water filter',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'reset_water_filter',
'unique_id': '42e80b4d-24c4-a810-11b3-f90375c56a39_main_custom.waterFilter_resetWaterFilter',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_mf_01001][button.filtro_in_microfibra_reset_water_filter-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Filtro in microfibra Reset water filter',
}),
'context': <ANY>,
'entity_id': 'button.filtro_in_microfibra_reset_water_filter',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@@ -1270,6 +1270,37 @@
'via_device_id': None,
})
# ---
# name: test_devices[da_wm_mf_01001]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://account.smartthings.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': 'Realtek',
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'42e80b4d-24c4-a810-11b3-f90375c56a39',
),
}),
'labels': set({
}),
'manufacturer': 'Samsung Electronics',
'model': 'AMF-WW-TP1-22-COMMON',
'model_id': None,
'name': 'Filtro in microfibra',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': 'AMF-WW-TP1-22-COMMON_30230323',
'via_device_id': None,
})
# ---
# name: test_devices[da_wm_sc_000001]
DeviceRegistryEntrySnapshot({
'area_id': None,

View File

@@ -727,8 +727,9 @@
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_sound_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'options': list([

View File

@@ -14360,6 +14360,60 @@
'state': '1336.2',
})
# ---
# name: test_all_entities[da_wm_mf_01001][sensor.filtro_in_microfibra_water_filter_usage-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.filtro_in_microfibra_water_filter_usage',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Water filter usage',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Water filter usage',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'water_filter_usage',
'unique_id': '42e80b4d-24c4-a810-11b3-f90375c56a39_main_custom.waterFilter_waterFilterUsage_waterFilterUsage',
'unit_of_measurement': '%',
})
# ---
# name: test_all_entities[da_wm_mf_01001][sensor.filtro_in_microfibra_water_filter_usage-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Filtro in microfibra Water filter usage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.filtro_in_microfibra_water_filter_usage',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '78',
})
# ---
# name: test_all_entities[da_wm_sc_000001][sensor.airdresser_completion_time-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -1499,6 +1499,56 @@
'state': 'off',
})
# ---
# name: test_all_entities[da_wm_mf_01001][switch.filtro_in_microfibra-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.filtro_in_microfibra',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '42e80b4d-24c4-a810-11b3-f90375c56a39_main_switch_switch_switch',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_mf_01001][switch.filtro_in_microfibra-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Filtro in microfibra',
}),
'context': <ANY>,
'entity_id': 'switch.filtro_in_microfibra',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[da_wm_sc_000001][switch.airdresser_auto_cycle_link-entry]
EntityRegistryEntrySnapshot({
'aliases': list([