Compare commits

...

10 Commits

Author SHA1 Message Date
Norbert Rittel
5dce4a8eda Change one remaining string from "Overseerr" to "Seerr" (#164569) 2026-03-02 10:22:49 +01:00
Jan-Philipp Benecke
6fcc9da948 Fix large WebDAV backup metadata download (#164563)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 10:17:18 +01:00
epenet
bf93580ff9 Migrate modern_forms to runtime_data (#164570) 2026-03-02 10:10:03 +01:00
Jan-Philipp Benecke
0c2fe045d5 Bump aiowebdav2 to 0.6.1 (#164560) 2026-03-02 10:09:33 +01:00
Joost Lekkerkerker
e14a3a6b0e Fix SmartThings EHS power (#164395) 2026-03-02 08:35:37 +01:00
Joost Lekkerkerker
e032740e90 Add time platform to SmartThings (#164451) 2026-03-02 08:34:53 +01:00
Joost Lekkerkerker
78ad1e102d Add binary sensor for full dust bag in SmartThings (#164457) 2026-03-02 08:34:19 +01:00
Joost Lekkerkerker
4f97cc7b68 Add sound detection sensitivity select to SmartThings (#164466) 2026-03-02 08:33:47 +01:00
dependabot[bot]
df8f135532 Bump github/codeql-action from 4.32.3 to 4.32.4 (#164554) 2026-03-02 07:30:23 +01:00
J. Nick Koston
0066801b0f Bump yarl to 1.23.0 (#164542) 2026-03-02 07:22:37 +01:00
34 changed files with 718 additions and 73 deletions

View File

@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
category: "/language:python"

View File

@@ -8,12 +8,10 @@ from typing import Any, Concatenate
from aiomodernforms import ModernFormsConnectionError, ModernFormsError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import ModernFormsDataUpdateCoordinator
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
from .entity import ModernFormsDeviceEntity
PLATFORMS = [
@@ -26,15 +24,14 @@ PLATFORMS = [
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ModernFormsConfigEntry) -> bool:
"""Set up a Modern Forms device from a config entry."""
# Create Modern Forms instance for this entry
coordinator = ModernFormsDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
entry.runtime_data = coordinator
# Set up all platforms for this device/entry.
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -42,17 +39,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: ModernFormsConfigEntry
) -> bool:
"""Unload Modern Forms config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
del hass.data[DOMAIN][entry.entry_id]
if not hass.data[DOMAIN]:
del hass.data[DOMAIN]
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
def modernforms_exception_handler[

View File

@@ -3,23 +3,22 @@
from __future__ import annotations
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import CLEAR_TIMER, DOMAIN
from .coordinator import ModernFormsDataUpdateCoordinator
from .const import CLEAR_TIMER
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
from .entity import ModernFormsDeviceEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: ModernFormsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Modern Forms binary sensors."""
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
binary_sensors: list[ModernFormsBinarySensor] = [
ModernFormsFanSleepTimerActive(entry.entry_id, coordinator),

View File

@@ -20,6 +20,9 @@ SCAN_INTERVAL = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
type ModernFormsConfigEntry = ConfigEntry[ModernFormsDataUpdateCoordinator]
class ModernFormsDataUpdateCoordinator(DataUpdateCoordinator[ModernFormsDeviceState]):
"""Class to manage fetching Modern Forms data from single endpoint."""

View File

@@ -3,27 +3,23 @@
from __future__ import annotations
from dataclasses import asdict
from typing import TYPE_CHECKING, Any
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import ModernFormsDataUpdateCoordinator
from .coordinator import ModernFormsConfigEntry
REDACT_CONFIG = {CONF_MAC}
REDACT_DEVICE_INFO = {"mac_address", "owner"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: ModernFormsConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
if TYPE_CHECKING:
assert coordinator is not None
coordinator = entry.runtime_data
return {
"config_entry": async_redact_data(entry.as_dict(), REDACT_CONFIG),

View File

@@ -8,7 +8,6 @@ from aiomodernforms.const import FAN_POWER_OFF, FAN_POWER_ON
import voluptuous as vol
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -22,26 +21,23 @@ from . import modernforms_exception_handler
from .const import (
ATTR_SLEEP_TIME,
CLEAR_TIMER,
DOMAIN,
OPT_ON,
OPT_SPEED,
SERVICE_CLEAR_FAN_SLEEP_TIMER,
SERVICE_SET_FAN_SLEEP_TIMER,
)
from .coordinator import ModernFormsDataUpdateCoordinator
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
from .entity import ModernFormsDeviceEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: ModernFormsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a Modern Forms platform from config entry."""
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
coordinator = config_entry.runtime_data
platform = entity_platform.async_get_current_platform()

View File

@@ -8,7 +8,6 @@ from aiomodernforms.const import LIGHT_POWER_OFF, LIGHT_POWER_ON
import voluptuous as vol
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -21,13 +20,12 @@ from . import modernforms_exception_handler
from .const import (
ATTR_SLEEP_TIME,
CLEAR_TIMER,
DOMAIN,
OPT_BRIGHTNESS,
OPT_ON,
SERVICE_CLEAR_LIGHT_SLEEP_TIMER,
SERVICE_SET_LIGHT_SLEEP_TIMER,
)
from .coordinator import ModernFormsDataUpdateCoordinator
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
from .entity import ModernFormsDeviceEntity
BRIGHTNESS_RANGE = (1, 255)
@@ -35,14 +33,12 @@ BRIGHTNESS_RANGE = (1, 255)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: ModernFormsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a Modern Forms platform from config entry."""
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
coordinator = config_entry.runtime_data
# if no light unit installed no light entity
if not coordinator.data.info.light_type:

View File

@@ -5,24 +5,23 @@ from __future__ import annotations
from datetime import datetime
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util
from .const import CLEAR_TIMER, DOMAIN
from .coordinator import ModernFormsDataUpdateCoordinator
from .const import CLEAR_TIMER
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
from .entity import ModernFormsDeviceEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: ModernFormsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Modern Forms sensor based on a config entry."""
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
sensors: list[ModernFormsSensor] = [
ModernFormsFanTimerRemainingTimeSensor(entry.entry_id, coordinator),

View File

@@ -5,23 +5,21 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import modernforms_exception_handler
from .const import DOMAIN
from .coordinator import ModernFormsDataUpdateCoordinator
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
from .entity import ModernFormsDeviceEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: ModernFormsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Modern Forms switch based on a config entry."""
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
switches = [
ModernFormsAwaySwitch(entry.entry_id, coordinator),

View File

@@ -114,7 +114,7 @@
"message": "[%key:common::config_flow::error::invalid_api_key%]"
},
"connection_error": {
"message": "Error connecting to the Overseerr instance: {error}"
"message": "Error connecting to the Seerr instance: {error}"
}
},
"selector": {

View File

@@ -107,6 +107,7 @@ PLATFORMS = [
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.TIME,
Platform.UPDATE,
Platform.VACUUM,
Platform.VALVE,

View File

@@ -36,6 +36,7 @@ class SmartThingsBinarySensorEntityDescription(BinarySensorEntityDescription):
| None
) = None
component_translation_key: dict[str, str] | None = None
supported_states_attributes: Attribute | None = None
CAPABILITY_TO_SENSORS: dict[
@@ -188,6 +189,17 @@ CAPABILITY_TO_SENSORS: dict[
},
)
},
Capability.SAMSUNG_CE_ROBOT_CLEANER_DUST_BAG: {
Attribute.STATUS: SmartThingsBinarySensorEntityDescription(
key=Attribute.STATUS,
is_on_key="full",
component_translation_key={
"station": "robot_cleaner_dust_bag",
},
exists_fn=lambda component, _: component == "station",
supported_states_attributes=Attribute.SUPPORTED_STATUS,
)
},
}
@@ -237,6 +249,18 @@ async def async_setup_entry(
not description.category
or get_main_component_category(device) in description.category
)
and (
not description.supported_states_attributes
or (
isinstance(
options := device.status[component][capability][
description.supported_states_attributes
].value,
list,
)
and len(options) == 2
)
)
)
)

View File

@@ -21,6 +21,9 @@
"state": {
"on": "mdi:remote"
}
},
"robot_cleaner_dust_bag": {
"default": "mdi:delete"
}
},
"button": {
@@ -110,6 +113,9 @@
"soil_level": {
"default": "mdi:liquid-spot"
},
"sound_detection_sensitivity": {
"default": "mdi:home-sound-in"
},
"spin_level": {
"default": "mdi:rotate-right"
},
@@ -250,6 +256,14 @@
"off": "mdi:tumble-dryer-off"
}
}
},
"time": {
"do_not_disturb_end_time": {
"default": "mdi:bell-ring"
},
"do_not_disturb_start_time": {
"default": "mdi:bell-cancel"
}
}
}
}

View File

@@ -165,6 +165,15 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = {
extra_components=["hood"],
capability_ignore_list=[Capability.SAMSUNG_CE_CONNECTION_STATE],
),
Capability.SAMSUNG_CE_SOUND_DETECTION_SENSITIVITY: SmartThingsSelectDescription(
key=Capability.SAMSUNG_CE_SOUND_DETECTION_SENSITIVITY,
translation_key="sound_detection_sensitivity",
options_attribute=Attribute.SUPPORTED_LEVELS,
status_attribute=Attribute.LEVEL,
command=Command.SET_LEVEL,
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
),
Capability.CUSTOM_WASHER_SPIN_LEVEL: SmartThingsSelectDescription(
key=Capability.CUSTOM_WASHER_SPIN_LEVEL,
translation_key="spin_level",

View File

@@ -162,6 +162,13 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
use_temperature_unit: bool = False
deprecated: Callable[[ComponentStatus], tuple[str, str] | None] | None = None
component_translation_key: dict[str, str] | None = None
presentation_fn: (
Callable[
[str | None, str | float | int | datetime | None],
str | float | int | datetime | None,
]
| None
) = None
CAPABILITY_TO_SENSORS: dict[
@@ -763,6 +770,13 @@ CAPABILITY_TO_SENSORS: dict[
(value := cast(dict | None, status.value)) is not None
and "power" in value
),
presentation_fn=lambda presentation_id, value: (
value * 1000
if presentation_id is not None
and "EHS" in presentation_id
and isinstance(value, (int, float))
else value
),
),
SmartThingsSensorEntityDescription(
key="deltaEnergy_meter",
@@ -1347,7 +1361,12 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
res = self.get_attribute_value(self.capability, self._attribute)
if options_map := self.entity_description.options_map:
return options_map.get(res)
return self.entity_description.value_fn(res)
value = self.entity_description.value_fn(res)
if self.entity_description.presentation_fn:
value = self.entity_description.presentation_fn(
self.device.device.presentation_id, value
)
return value
@property
def native_unit_of_measurement(self) -> str | None:

View File

@@ -76,6 +76,9 @@
"remote_control": {
"name": "Remote control"
},
"robot_cleaner_dust_bag": {
"name": "Dust bag full"
},
"sub_remote_control": {
"name": "Upper washer remote control"
},
@@ -256,6 +259,14 @@
"up": "Up"
}
},
"sound_detection_sensitivity": {
"name": "Sound detection sensitivity",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"medium": "[%key:common::state::medium%]"
}
},
"spin_level": {
"name": "Spin level",
"state": {
@@ -934,6 +945,14 @@
"name": "Wrinkle prevent"
}
},
"time": {
"do_not_disturb_end_time": {
"name": "Do not disturb end time"
},
"do_not_disturb_start_time": {
"name": "Do not disturb start time"
}
},
"vacuum": {
"vacuum": {
"state_attributes": {

View File

@@ -0,0 +1,102 @@
"""Time platform for SmartThings."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import time
from pysmartthings import Attribute, Capability, Command, SmartThings
from homeassistant.components.time import TimeEntity, TimeEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FullDevice, SmartThingsConfigEntry
from .const import MAIN
from .entity import SmartThingsEntity
@dataclass(frozen=True, kw_only=True)
class SmartThingsTimeEntityDescription(TimeEntityDescription):
"""Describe a SmartThings time entity."""
attribute: Attribute
DND_ENTITIES = [
SmartThingsTimeEntityDescription(
key=Attribute.START_TIME,
translation_key="do_not_disturb_start_time",
attribute=Attribute.START_TIME,
entity_category=EntityCategory.CONFIG,
),
SmartThingsTimeEntityDescription(
key=Attribute.END_TIME,
translation_key="do_not_disturb_end_time",
attribute=Attribute.END_TIME,
entity_category=EntityCategory.CONFIG,
),
]
async def async_setup_entry(
hass: HomeAssistant,
entry: SmartThingsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add time entities for a config entry."""
entry_data = entry.runtime_data
async_add_entities(
SmartThingsDnDTime(entry_data.client, device, description)
for device in entry_data.devices.values()
if Capability.CUSTOM_DO_NOT_DISTURB_MODE in device.status.get(MAIN, {})
for description in DND_ENTITIES
)
class SmartThingsDnDTime(SmartThingsEntity, TimeEntity):
"""Define a SmartThings time entity."""
entity_description: SmartThingsTimeEntityDescription
def __init__(
self,
client: SmartThings,
device: FullDevice,
entity_description: SmartThingsTimeEntityDescription,
) -> None:
"""Initialize the time entity."""
super().__init__(client, device, {Capability.CUSTOM_DO_NOT_DISTURB_MODE})
self.entity_description = entity_description
self._attr_unique_id = f"{device.device.device_id}_{MAIN}_{Capability.CUSTOM_DO_NOT_DISTURB_MODE}_{entity_description.attribute}_{entity_description.attribute}"
async def async_set_value(self, value: time) -> None:
"""Set the time value."""
payload = {
"mode": self.get_attribute_value(
Capability.CUSTOM_DO_NOT_DISTURB_MODE, Attribute.DO_NOT_DISTURB
),
"startTime": self.get_attribute_value(
Capability.CUSTOM_DO_NOT_DISTURB_MODE, Attribute.START_TIME
),
"endTime": self.get_attribute_value(
Capability.CUSTOM_DO_NOT_DISTURB_MODE, Attribute.END_TIME
),
}
await self.execute_device_command(
Capability.CUSTOM_DO_NOT_DISTURB_MODE,
Command.SET_DO_NOT_DISTURB_MODE,
{
**payload,
self.entity_description.attribute: f"{value.hour:02d}{value.minute:02d}",
},
)
@property
def native_value(self) -> time:
"""Return the time value."""
state = self.get_attribute_value(
Capability.CUSTOM_DO_NOT_DISTURB_MODE, self.entity_description.attribute
)
return time(int(state[:2]), int(state[3:5]))

View File

@@ -222,8 +222,10 @@ class WebDavBackupAgent(BackupAgent):
async def _download_metadata(path: str) -> AgentBackup:
"""Download metadata file."""
iterator = await self._client.download_iter(path)
metadata = await anext(iterator)
return AgentBackup.from_dict(json_loads_object(metadata))
metadata_bytes = bytearray()
async for chunk in iterator:
metadata_bytes.extend(chunk)
return AgentBackup.from_dict(json_loads_object(metadata_bytes))
async def _list_metadata_files() -> dict[str, AgentBackup]:
"""List metadata files."""

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aiowebdav2"],
"quality_scale": "bronze",
"requirements": ["aiowebdav2==0.5.0"]
"requirements": ["aiowebdav2==0.6.1"]
}

View File

@@ -76,7 +76,7 @@ voluptuous-openapi==0.2.0
voluptuous-serialize==2.7.0
voluptuous==0.15.2
webrtc-models==0.3.0
yarl==1.22.0
yarl==1.23.0
zeroconf==0.148.0
# Constrain pycryptodome to avoid vulnerability

View File

@@ -82,7 +82,7 @@ dependencies = [
"voluptuous==0.15.2",
"voluptuous-serialize==2.7.0",
"voluptuous-openapi==0.2.0",
"yarl==1.22.0",
"yarl==1.23.0",
"webrtc-models==0.3.0",
"zeroconf==0.148.0",
]

2
requirements.txt generated
View File

@@ -60,5 +60,5 @@ voluptuous-openapi==0.2.0
voluptuous-serialize==2.7.0
voluptuous==0.15.2
webrtc-models==0.3.0
yarl==1.22.0
yarl==1.23.0
zeroconf==0.148.0

2
requirements_all.txt generated
View File

@@ -443,7 +443,7 @@ aiowaqi==3.1.0
aiowatttime==0.1.1
# homeassistant.components.webdav
aiowebdav2==0.5.0
aiowebdav2==0.6.1
# homeassistant.components.webostv
aiowebostv==0.7.5

View File

@@ -428,7 +428,7 @@ aiowaqi==3.1.0
aiowatttime==0.1.1
# homeassistant.components.webdav
aiowebdav2==0.5.0
aiowebdav2==0.6.1
# homeassistant.components.webostv
aiowebostv==0.7.5

View File

@@ -4,7 +4,6 @@ from unittest.mock import MagicMock, patch
from aiomodernforms import ModernFormsConnectionError
from homeassistant.components.modern_forms.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -31,11 +30,11 @@ async def test_unload_config_entry(
) -> None:
"""Test the Modern Forms configuration entry unloading."""
entry = await init_integration(hass, aioclient_mock)
assert hass.data[DOMAIN]
assert entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert not hass.data.get(DOMAIN)
assert entry.state is ConfigEntryState.NOT_LOADED
async def test_fan_only_device(

View File

@@ -84,7 +84,7 @@ async def test_service_get_requests_no_meta(
"get_requests",
OverseerrConnectionError("Timeout"),
HomeAssistantError,
"Error connecting to the Overseerr instance: Timeout",
"Error connecting to the Seerr instance: Timeout",
)
],
)

View File

@@ -1836,6 +1836,55 @@
'state': 'off',
})
# ---
# name: test_all_entities[da_rvc_map_01011][binary_sensor.robot_vacuum_dust_bag_full-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'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.robot_vacuum_dust_bag_full',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Dust bag full',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Dust bag full',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'robot_cleaner_dust_bag',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_station_samsungce.robotCleanerDustBag_status_status',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][binary_sensor.robot_vacuum_dust_bag_full-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot Vacuum Dust bag full',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.robot_vacuum_dust_bag_full',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_wm_dw_000001][binary_sensor.dishwasher_child_lock-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -533,6 +533,66 @@
'state': 'on',
})
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_sound_detection_sensitivity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'low',
'medium',
'high',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.robot_vacuum_sound_detection_sensitivity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Sound detection sensitivity',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Sound detection sensitivity',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'sound_detection_sensitivity',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_samsungce.soundDetectionSensitivity_level_level',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_sound_detection_sensitivity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot Vacuum Sound detection sensitivity',
'options': list([
'low',
'medium',
'high',
]),
}),
'context': <ANY>,
'entity_id': 'select.robot_vacuum_sound_detection_sensitivity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'medium',
})
# ---
# name: test_all_entities[da_wm_dw_000001][select.dishwasher-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -11504,7 +11504,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.015',
'state': '15.0',
})
# ---
# name: test_all_entities[da_sac_ehs_000001_sub][sensor.eco_heating_system_power_energy-entry]
@@ -11850,7 +11850,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.015',
'state': '15.0',
})
# ---
# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_power_energy-entry]
@@ -12196,7 +12196,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.015',
'state': '15.0',
})
# ---
# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_power_energy-entry]

View File

@@ -0,0 +1,197 @@
# serializer version: 1
# name: test_all_entities[da_ks_hood_01001][time.range_hood_do_not_disturb_end_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'time',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'time.range_hood_do_not_disturb_end_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Do not disturb end time',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Do not disturb end time',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'do_not_disturb_end_time',
'unique_id': 'fa5fca25-fa7a-1807-030a-2f72ee0f7bff_main_custom.doNotDisturbMode_endTime_endTime',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_hood_01001][time.range_hood_do_not_disturb_end_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Range hood Do not disturb end time',
}),
'context': <ANY>,
'entity_id': 'time.range_hood_do_not_disturb_end_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '00:00:00',
})
# ---
# name: test_all_entities[da_ks_hood_01001][time.range_hood_do_not_disturb_start_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'time',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'time.range_hood_do_not_disturb_start_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Do not disturb start time',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Do not disturb start time',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'do_not_disturb_start_time',
'unique_id': 'fa5fca25-fa7a-1807-030a-2f72ee0f7bff_main_custom.doNotDisturbMode_startTime_startTime',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_hood_01001][time.range_hood_do_not_disturb_start_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Range hood Do not disturb start time',
}),
'context': <ANY>,
'entity_id': 'time.range_hood_do_not_disturb_start_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '00:00:00',
})
# ---
# name: test_all_entities[da_rvc_map_01011][time.robot_vacuum_do_not_disturb_end_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'time',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'time.robot_vacuum_do_not_disturb_end_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Do not disturb end time',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Do not disturb end time',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'do_not_disturb_end_time',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_custom.doNotDisturbMode_endTime_endTime',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][time.robot_vacuum_do_not_disturb_end_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot Vacuum Do not disturb end time',
}),
'context': <ANY>,
'entity_id': 'time.robot_vacuum_do_not_disturb_end_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '06:00:00',
})
# ---
# name: test_all_entities[da_rvc_map_01011][time.robot_vacuum_do_not_disturb_start_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'time',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'time.robot_vacuum_do_not_disturb_start_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Do not disturb start time',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Do not disturb start time',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'do_not_disturb_start_time',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_custom.doNotDisturbMode_startTime_startTime',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][time.robot_vacuum_do_not_disturb_start_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot Vacuum Do not disturb start time',
}),
'context': <ANY>,
'entity_id': 'time.robot_vacuum_do_not_disturb_start_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '22:00:00',
})
# ---

View File

@@ -30,6 +30,7 @@ from . import (
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,

View File

@@ -0,0 +1,128 @@
"""Test for the SmartThings time platform."""
from unittest.mock import AsyncMock
from pysmartthings import Attribute, Capability, Command
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.smartthings import MAIN
from homeassistant.components.time import DOMAIN as TIME_DOMAIN, SERVICE_SET_VALUE
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TIME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration, snapshot_smartthings_entities, trigger_update
from tests.common import MockConfigEntry
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
await setup_integration(hass, mock_config_entry)
snapshot_smartthings_entities(hass, entity_registry, snapshot, Platform.TIME)
@pytest.mark.parametrize("device_fixture", ["da_rvc_map_01011"])
async def test_state_update(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test state update."""
await setup_integration(hass, mock_config_entry)
assert (
hass.states.get("time.robot_vacuum_do_not_disturb_end_time").state == "06:00:00"
)
await trigger_update(
hass,
devices,
"01b28624-5907-c8bc-0325-8ad23f03a637",
Capability.CUSTOM_DO_NOT_DISTURB_MODE,
Attribute.END_TIME,
"0800",
)
assert (
hass.states.get("time.robot_vacuum_do_not_disturb_end_time").state == "08:00:00"
)
@pytest.mark.parametrize("device_fixture", ["da_rvc_map_01011"])
async def test_set_value(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test setting a value."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
TIME_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: "time.robot_vacuum_do_not_disturb_end_time",
ATTR_TIME: "09:00:00",
},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"01b28624-5907-c8bc-0325-8ad23f03a637",
Capability.CUSTOM_DO_NOT_DISTURB_MODE,
Command.SET_DO_NOT_DISTURB_MODE,
MAIN,
argument={
"mode": "on",
"startTime": "2200",
"endTime": "0900",
},
)
@pytest.mark.parametrize("device_fixture", ["da_rvc_map_01011"])
async def test_dnd_mode_updates(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test setting a value."""
await setup_integration(hass, mock_config_entry)
await trigger_update(
hass,
devices,
"01b28624-5907-c8bc-0325-8ad23f03a637",
Capability.CUSTOM_DO_NOT_DISTURB_MODE,
Attribute.DO_NOT_DISTURB,
"off",
)
await hass.services.async_call(
TIME_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: "time.robot_vacuum_do_not_disturb_end_time",
ATTR_TIME: "09:00:00",
},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"01b28624-5907-c8bc-0325-8ad23f03a637",
Capability.CUSTOM_DO_NOT_DISTURB_MODE,
Command.SET_DO_NOT_DISTURB_MODE,
MAIN,
argument={
"mode": "off",
"startTime": "2200",
"endTime": "0900",
},
)

View File

@@ -42,6 +42,7 @@ async def _download_mock(path: str, timeout=None) -> AsyncIterator[bytes]:
"""Mock the download function."""
if path.endswith(".json"):
yield dumps(BACKUP_METADATA).encode()
return
yield b"backup data"

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from collections.abc import AsyncGenerator
from collections.abc import AsyncGenerator, AsyncIterator
from io import StringIO
from unittest.mock import Mock, patch
@@ -13,6 +13,7 @@ from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN, AgentBackup
from homeassistant.components.webdav.backup import async_register_backup_agents_listener
from homeassistant.components.webdav.const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.json import json_dumps
from homeassistant.setup import async_setup_component
from .const import BACKUP_METADATA
@@ -324,3 +325,44 @@ async def test_listeners_get_cleaned_up(hass: HomeAssistant) -> None:
remove_listener()
assert hass.data.get(DATA_BACKUP_AGENT_LISTENERS) is None
async def test_agents_list_backups_with_multi_chunk_metadata(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
webdav_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test listing backups when metadata is returned in multiple chunks."""
metadata_json = json_dumps(BACKUP_METADATA).encode()
mid = len(metadata_json) // 2
chunk1 = metadata_json[:mid]
chunk2 = metadata_json[mid:]
async def _multi_chunk_download(path: str, timeout=None) -> AsyncIterator[bytes]:
"""Mock download returning metadata in multiple chunks."""
if path.endswith(".json"):
yield chunk1
yield chunk2
return
yield b"backup data"
webdav_client.download_iter.side_effect = _multi_chunk_download
# Invalidate the metadata cache so the new mock is used
hass.config_entries.async_update_entry(
mock_config_entry, title=mock_config_entry.title
)
await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await client.send_json_auto_id({"type": "backup/info"})
response = await client.receive_json()
assert response["success"]
assert response["result"]["agent_errors"] == {}
backups = response["result"]["backups"]
assert len(backups) == 1
assert backups[0]["backup_id"] == BACKUP_METADATA["backup_id"]
assert backups[0]["name"] == BACKUP_METADATA["name"]