mirror of
https://github.com/home-assistant/core.git
synced 2026-03-02 13:56:56 +01:00
Compare commits
30 Commits
remote/add
...
python-3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09546525b6 | ||
|
|
37a59bd23c | ||
|
|
6793a98c08 | ||
|
|
0ee4589ba3 | ||
|
|
55bd4b00b4 | ||
|
|
43f5f922e3 | ||
|
|
5dd6dcc215 | ||
|
|
8bf894a514 | ||
|
|
d3c67f2ae1 | ||
|
|
b60a282b60 | ||
|
|
0da1d40a19 | ||
|
|
aa3be915a0 | ||
|
|
0d97bfbc59 | ||
|
|
fe830337c9 | ||
|
|
5210b7d847 | ||
|
|
2f7ed4040b | ||
|
|
6376ba93a7 | ||
|
|
fd3a1cc9f4 | ||
|
|
208013ab76 | ||
|
|
770b3f910e | ||
|
|
5dce4a8eda | ||
|
|
6fcc9da948 | ||
|
|
bf93580ff9 | ||
|
|
0c2fe045d5 | ||
|
|
e14a3a6b0e | ||
|
|
e032740e90 | ||
|
|
78ad1e102d | ||
|
|
4f97cc7b68 | ||
|
|
df8f135532 | ||
|
|
0066801b0f |
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@@ -10,12 +10,12 @@ on:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: core
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
DEFAULT_PYTHON: "3.14.3"
|
||||
PIP_TIMEOUT: 60
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
# Base image version from https://github.com/home-assistant/docker
|
||||
BASE_IMAGE_VERSION: "2026.01.0"
|
||||
BASE_IMAGE_VERSION: "2026.02.0"
|
||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||
|
||||
permissions: {}
|
||||
|
||||
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -41,8 +41,8 @@ env:
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2026.4"
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
ALL_PYTHON_VERSIONS: "['3.14.2']"
|
||||
DEFAULT_PYTHON: "3.14.3"
|
||||
ALL_PYTHON_VERSIONS: "['3.14.3']"
|
||||
# 10.3 is the oldest supported version
|
||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||
# 10.6 is the current long-term-support
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -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"
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -16,7 +16,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
DEFAULT_PYTHON: "3.14.3"
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
|
||||
2
.github/workflows/wheels.yml
vendored
2
.github/workflows/wheels.yml
vendored
@@ -17,7 +17,7 @@ on:
|
||||
- "script/gen_requirements_all.py"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
DEFAULT_PYTHON: "3.14.3"
|
||||
|
||||
permissions: {}
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==12.0.0"]
|
||||
"requirements": ["aioamazondevices==12.0.2"]
|
||||
}
|
||||
|
||||
@@ -30,5 +30,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.4"]
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
|
||||
}
|
||||
|
||||
@@ -149,7 +149,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"lock",
|
||||
"media_player",
|
||||
"person",
|
||||
"remote",
|
||||
"scene",
|
||||
"siren",
|
||||
"switch",
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"mqtt": ["fully/deviceInfo/+"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-fullykiosk==0.0.14"]
|
||||
"requirements": ["python-fullykiosk==0.0.15"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiogithubapi"],
|
||||
"requirements": ["aiogithubapi==24.6.0"]
|
||||
"requirements": ["aiogithubapi==26.0.0"]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from chip.clusters.Types import NullValue
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
|
||||
@@ -156,11 +157,17 @@ def _get_attr(obj: Any, attr: str) -> Any:
|
||||
"""Get attribute from object or dict.
|
||||
|
||||
Matter SDK responses can be either dataclass objects or dicts depending on
|
||||
the SDK version and serialization context.
|
||||
the SDK version and serialization context. NullValue (a truthy,
|
||||
non-iterable singleton) is normalized to None.
|
||||
"""
|
||||
if isinstance(obj, dict):
|
||||
return obj.get(attr)
|
||||
return getattr(obj, attr, None)
|
||||
value = obj.get(attr)
|
||||
else:
|
||||
value = getattr(obj, attr, None)
|
||||
# The Matter SDK uses NullValue for nullable fields instead of None.
|
||||
if value is NullValue:
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def _get_supported_credential_types(feature_map: int) -> list[str]:
|
||||
@@ -598,6 +605,13 @@ _CREDENTIAL_TYPE_FEATURE_MAP: dict[str, int] = {
|
||||
CRED_TYPE_FACE: DoorLockFeature.kFaceCredentials,
|
||||
}
|
||||
|
||||
# Map credential type strings to the capacity attribute for slot iteration.
|
||||
# Biometric types have no dedicated capacity attribute; fall back to total users.
|
||||
_CREDENTIAL_TYPE_CAPACITY_ATTR = {
|
||||
CRED_TYPE_PIN: clusters.DoorLock.Attributes.NumberOfPINUsersSupported,
|
||||
CRED_TYPE_RFID: clusters.DoorLock.Attributes.NumberOfRFIDUsersSupported,
|
||||
}
|
||||
|
||||
|
||||
def _validate_credential_type_support(
|
||||
lock_endpoint: MatterEndpoint, credential_type: str
|
||||
@@ -736,13 +750,15 @@ async def set_lock_credential(
|
||||
operation_type = clusters.DoorLock.Enums.DataOperationTypeEnum.kAdd
|
||||
|
||||
if credential_index is None:
|
||||
# Auto-find first available credential slot
|
||||
# Auto-find first available credential slot.
|
||||
# Use the credential-type-specific capacity as the upper bound.
|
||||
max_creds_attr = _CREDENTIAL_TYPE_CAPACITY_ATTR.get(
|
||||
credential_type,
|
||||
clusters.DoorLock.Attributes.NumberOfTotalUsersSupported,
|
||||
)
|
||||
max_creds_raw = lock_endpoint.get_attribute_value(None, max_creds_attr)
|
||||
max_creds = (
|
||||
lock_endpoint.get_attribute_value(
|
||||
None,
|
||||
clusters.DoorLock.Attributes.NumberOfCredentialsSupportedPerUser,
|
||||
)
|
||||
or 5
|
||||
max_creds_raw if isinstance(max_creds_raw, int) and max_creds_raw > 0 else 5
|
||||
)
|
||||
for idx in range(1, max_creds + 1):
|
||||
status_response = await matter_client.send_device_command(
|
||||
|
||||
@@ -3,19 +3,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MedcomBleUpdateCoordinator
|
||||
from .coordinator import MedcomBleConfigEntry, MedcomBleUpdateCoordinator
|
||||
|
||||
# Supported platforms
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MedcomBleConfigEntry) -> bool:
|
||||
"""Set up Medcom BLE radiation monitor from a config entry."""
|
||||
|
||||
address = entry.unique_id
|
||||
@@ -31,16 +29,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MedcomBleConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -18,13 +18,17 @@ from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type MedcomBleConfigEntry = ConfigEntry[MedcomBleUpdateCoordinator]
|
||||
|
||||
|
||||
class MedcomBleUpdateCoordinator(DataUpdateCoordinator[MedcomBleDevice]):
|
||||
"""Coordinator for Medcom BLE radiation monitor data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: MedcomBleConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, address: str) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, entry: MedcomBleConfigEntry, address: str
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
@@ -15,8 +14,8 @@ from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceIn
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, UNIT_CPM
|
||||
from .coordinator import MedcomBleUpdateCoordinator
|
||||
from .const import UNIT_CPM
|
||||
from .coordinator import MedcomBleConfigEntry, MedcomBleUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,12 +31,12 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
entry: MedcomBleConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Medcom BLE radiation monitor sensors."""
|
||||
|
||||
coordinator: MedcomBleUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities = []
|
||||
_LOGGER.debug("got sensors: %s", coordinator.data.sensors)
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
"""Support for Meteoclimatic weather data."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import MeteoclimaticUpdateCoordinator
|
||||
from .const import PLATFORMS
|
||||
from .coordinator import MeteoclimaticConfigEntry, MeteoclimaticUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: MeteoclimaticConfigEntry
|
||||
) -> bool:
|
||||
"""Set up a Meteoclimatic entry."""
|
||||
coordinator = MeteoclimaticUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: MeteoclimaticConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -14,13 +14,15 @@ from .const import CONF_STATION_CODE, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type MeteoclimaticConfigEntry = ConfigEntry[MeteoclimaticUpdateCoordinator]
|
||||
|
||||
|
||||
class MeteoclimaticUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Coordinator for Meteoclimatic weather data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: MeteoclimaticConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
def __init__(self, hass: HomeAssistant, entry: MeteoclimaticConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self._station_code = entry.data[CONF_STATION_CODE]
|
||||
super().__init__(
|
||||
|
||||
@@ -6,7 +6,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
DEGREE,
|
||||
PERCENTAGE,
|
||||
@@ -21,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, DOMAIN, MANUFACTURER, MODEL
|
||||
from .coordinator import MeteoclimaticUpdateCoordinator
|
||||
from .coordinator import MeteoclimaticConfigEntry, MeteoclimaticUpdateCoordinator
|
||||
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
@@ -113,11 +112,11 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MeteoclimaticConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Meteoclimatic sensor platform."""
|
||||
coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
[MeteoclimaticSensor(coordinator, description) for description in SENSOR_TYPES],
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING
|
||||
from meteoclimatic import Condition
|
||||
|
||||
from homeassistant.components.weather import WeatherEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
@@ -13,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, CONDITION_MAP, DOMAIN, MANUFACTURER, MODEL
|
||||
from .coordinator import MeteoclimaticUpdateCoordinator
|
||||
from .coordinator import MeteoclimaticConfigEntry, MeteoclimaticUpdateCoordinator
|
||||
|
||||
|
||||
def format_condition(condition):
|
||||
@@ -27,11 +26,11 @@ def format_condition(condition):
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MeteoclimaticConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Meteoclimatic weather platform."""
|
||||
coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities([MeteoclimaticWeather(coordinator)], False)
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from datapoint.Forecast import Forecast
|
||||
from datapoint.Manager import Manager
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -19,10 +17,8 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
|
||||
|
||||
from .const import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
METOFFICE_COORDINATES,
|
||||
METOFFICE_DAILY_COORDINATOR,
|
||||
@@ -30,9 +26,7 @@ from .const import (
|
||||
METOFFICE_NAME,
|
||||
METOFFICE_TWICE_DAILY_COORDINATOR,
|
||||
)
|
||||
from .helpers import fetch_data
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .coordinator import MetOfficeUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
|
||||
@@ -40,55 +34,43 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Met Office entry."""
|
||||
|
||||
latitude = entry.data[CONF_LATITUDE]
|
||||
longitude = entry.data[CONF_LONGITUDE]
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
site_name = entry.data[CONF_NAME]
|
||||
latitude: float = entry.data[CONF_LATITUDE]
|
||||
longitude: float = entry.data[CONF_LONGITUDE]
|
||||
api_key: str = entry.data[CONF_API_KEY]
|
||||
site_name: str = entry.data[CONF_NAME]
|
||||
|
||||
coordinates = f"{latitude}_{longitude}"
|
||||
|
||||
connection = Manager(api_key=api_key)
|
||||
|
||||
async def async_update_hourly() -> Forecast:
|
||||
return await hass.async_add_executor_job(
|
||||
fetch_data, connection, latitude, longitude, "hourly"
|
||||
)
|
||||
|
||||
async def async_update_daily() -> Forecast:
|
||||
return await hass.async_add_executor_job(
|
||||
fetch_data, connection, latitude, longitude, "daily"
|
||||
)
|
||||
|
||||
async def async_update_twice_daily() -> Forecast:
|
||||
return await hass.async_add_executor_job(
|
||||
fetch_data, connection, latitude, longitude, "twice-daily"
|
||||
)
|
||||
|
||||
metoffice_hourly_coordinator = TimestampDataUpdateCoordinator(
|
||||
metoffice_hourly_coordinator = MetOfficeUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
entry,
|
||||
name=f"MetOffice Hourly Coordinator for {site_name}",
|
||||
update_method=async_update_hourly,
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
connection=connection,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
frequency="hourly",
|
||||
)
|
||||
|
||||
metoffice_daily_coordinator = TimestampDataUpdateCoordinator(
|
||||
metoffice_daily_coordinator = MetOfficeUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
entry,
|
||||
name=f"MetOffice Daily Coordinator for {site_name}",
|
||||
update_method=async_update_daily,
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
connection=connection,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
frequency="daily",
|
||||
)
|
||||
|
||||
metoffice_twice_daily_coordinator = TimestampDataUpdateCoordinator(
|
||||
metoffice_twice_daily_coordinator = MetOfficeUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
entry,
|
||||
name=f"MetOffice Twice Daily Coordinator for {site_name}",
|
||||
update_method=async_update_twice_daily,
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
connection=connection,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
frequency="twice-daily",
|
||||
)
|
||||
|
||||
metoffice_hass_data = hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
82
homeassistant/components/metoffice/coordinator.py
Normal file
82
homeassistant/components/metoffice/coordinator.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Data update coordinator for the Met Office integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
from datapoint.exceptions import APIException
|
||||
from datapoint.Forecast import Forecast
|
||||
from datapoint.Manager import Manager
|
||||
from requests import HTTPError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
TimestampDataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MetOfficeUpdateCoordinator(TimestampDataUpdateCoordinator[Forecast]):
|
||||
"""Coordinator for Met Office forecast data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
name: str,
|
||||
connection: Manager,
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
frequency: Literal["daily", "twice-daily", "hourly"],
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=name,
|
||||
config_entry=entry,
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
self._connection = connection
|
||||
self._latitude = latitude
|
||||
self._longitude = longitude
|
||||
self._frequency = frequency
|
||||
|
||||
async def _async_update_data(self) -> Forecast:
|
||||
"""Get data from Met Office."""
|
||||
return await self.hass.async_add_executor_job(
|
||||
fetch_data,
|
||||
self._connection,
|
||||
self._latitude,
|
||||
self._longitude,
|
||||
self._frequency,
|
||||
)
|
||||
|
||||
|
||||
def fetch_data(
|
||||
connection: Manager,
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
frequency: Literal["daily", "twice-daily", "hourly"],
|
||||
) -> Forecast:
|
||||
"""Fetch weather and forecast from Datapoint API."""
|
||||
try:
|
||||
return connection.get_forecast(
|
||||
latitude, longitude, frequency, convert_weather_code=False
|
||||
)
|
||||
except (ValueError, APIException) as err:
|
||||
_LOGGER.error("Check Met Office connection: %s", err.args)
|
||||
raise UpdateFailed from err
|
||||
except HTTPError as err:
|
||||
if err.response.status_code == 401:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
raise
|
||||
@@ -2,38 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Literal
|
||||
|
||||
from datapoint.exceptions import APIException
|
||||
from datapoint.Forecast import Forecast
|
||||
from datapoint.Manager import Manager
|
||||
from requests import HTTPError
|
||||
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def fetch_data(
|
||||
connection: Manager,
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
frequency: Literal["daily", "twice-daily", "hourly"],
|
||||
) -> Forecast:
|
||||
"""Fetch weather and forecast from Datapoint API."""
|
||||
try:
|
||||
return connection.get_forecast(
|
||||
latitude, longitude, frequency, convert_weather_code=False
|
||||
)
|
||||
except (ValueError, APIException) as err:
|
||||
_LOGGER.error("Check Met Office connection: %s", err.args)
|
||||
raise UpdateFailed from err
|
||||
except HTTPError as err:
|
||||
if err.response.status_code == 401:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
raise
|
||||
from typing import Any
|
||||
|
||||
|
||||
def get_attribute(data: dict[str, Any] | None, attr_name: str) -> Any | None:
|
||||
|
||||
@@ -5,8 +5,6 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from datapoint.Forecast import Forecast
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
EntityCategory,
|
||||
@@ -29,10 +27,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import get_device_info
|
||||
from .const import (
|
||||
@@ -43,6 +38,7 @@ from .const import (
|
||||
METOFFICE_HOURLY_COORDINATOR,
|
||||
METOFFICE_NAME,
|
||||
)
|
||||
from .coordinator import MetOfficeUpdateCoordinator
|
||||
from .helpers import get_attribute
|
||||
|
||||
ATTR_LAST_UPDATE = "last_update"
|
||||
@@ -220,7 +216,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class MetOfficeCurrentSensor(
|
||||
CoordinatorEntity[DataUpdateCoordinator[Forecast]], SensorEntity
|
||||
CoordinatorEntity[MetOfficeUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Implementation of a Met Office current weather condition sensor."""
|
||||
|
||||
@@ -231,7 +227,7 @@ class MetOfficeCurrentSensor(
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator[Forecast],
|
||||
coordinator: MetOfficeUpdateCoordinator,
|
||||
hass_data: dict[str, Any],
|
||||
description: MetOfficeSensorEntityDescription,
|
||||
) -> None:
|
||||
|
||||
@@ -5,8 +5,6 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Any, cast
|
||||
|
||||
from datapoint.Forecast import Forecast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_IS_DAYTIME,
|
||||
@@ -35,7 +33,6 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
|
||||
|
||||
from . import get_device_info
|
||||
from .const import (
|
||||
@@ -52,6 +49,7 @@ from .const import (
|
||||
METOFFICE_TWICE_DAILY_COORDINATOR,
|
||||
NIGHT_FORECAST_ATTRIBUTE_MAP,
|
||||
)
|
||||
from .coordinator import MetOfficeUpdateCoordinator
|
||||
from .helpers import get_attribute
|
||||
|
||||
|
||||
@@ -153,9 +151,9 @@ def _populate_forecast_data(
|
||||
|
||||
class MetOfficeWeather(
|
||||
CoordinatorWeatherEntity[
|
||||
TimestampDataUpdateCoordinator[Forecast],
|
||||
TimestampDataUpdateCoordinator[Forecast],
|
||||
TimestampDataUpdateCoordinator[Forecast],
|
||||
MetOfficeUpdateCoordinator,
|
||||
MetOfficeUpdateCoordinator,
|
||||
MetOfficeUpdateCoordinator,
|
||||
]
|
||||
):
|
||||
"""Implementation of a Met Office weather condition."""
|
||||
@@ -177,9 +175,9 @@ class MetOfficeWeather(
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator_daily: TimestampDataUpdateCoordinator[Forecast],
|
||||
coordinator_hourly: TimestampDataUpdateCoordinator[Forecast],
|
||||
coordinator_twice_daily: TimestampDataUpdateCoordinator[Forecast],
|
||||
coordinator_daily: MetOfficeUpdateCoordinator,
|
||||
coordinator_hourly: MetOfficeUpdateCoordinator,
|
||||
coordinator_twice_daily: MetOfficeUpdateCoordinator,
|
||||
hass_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialise the platform with a data instance."""
|
||||
@@ -266,7 +264,7 @@ class MetOfficeWeather(
|
||||
def _async_forecast_daily(self) -> list[WeatherForecast] | None:
|
||||
"""Return the daily forecast in native units."""
|
||||
coordinator = cast(
|
||||
TimestampDataUpdateCoordinator[Forecast],
|
||||
MetOfficeUpdateCoordinator,
|
||||
self.forecast_coordinators["daily"],
|
||||
)
|
||||
timesteps = coordinator.data.timesteps
|
||||
@@ -283,7 +281,7 @@ class MetOfficeWeather(
|
||||
def _async_forecast_hourly(self) -> list[WeatherForecast] | None:
|
||||
"""Return the hourly forecast in native units."""
|
||||
coordinator = cast(
|
||||
TimestampDataUpdateCoordinator[Forecast],
|
||||
MetOfficeUpdateCoordinator,
|
||||
self.forecast_coordinators["hourly"],
|
||||
)
|
||||
|
||||
@@ -301,7 +299,7 @@ class MetOfficeWeather(
|
||||
def _async_forecast_twice_daily(self) -> list[WeatherForecast] | None:
|
||||
"""Return the twice daily forecast in native units."""
|
||||
coordinator = cast(
|
||||
TimestampDataUpdateCoordinator[Forecast],
|
||||
MetOfficeUpdateCoordinator,
|
||||
self.forecast_coordinators["twice_daily"],
|
||||
)
|
||||
timesteps = coordinator.data.timesteps
|
||||
|
||||
@@ -13,22 +13,25 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .const import PLATFORMS
|
||||
from .coordinator import MicroBeesUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
type MicroBeesConfigEntry = ConfigEntry[HomeAssistantMicroBeesData]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HomeAssistantMicroBeesData:
|
||||
"""Microbees data stored in the Home Assistant data object."""
|
||||
"""Microbees data stored in the config entry runtime_data."""
|
||||
|
||||
connector: MicroBees
|
||||
coordinator: MicroBeesUpdateCoordinator
|
||||
session: config_entry_oauth2_flow.OAuth2Session
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: MicroBeesConfigEntry) -> bool:
|
||||
"""Migrate entry."""
|
||||
_LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
|
||||
|
||||
@@ -45,7 +48,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MicroBeesConfigEntry) -> bool:
|
||||
"""Set up microBees from a config entry."""
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
@@ -67,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
microbees = MicroBees(token=session.token[CONF_ACCESS_TOKEN])
|
||||
coordinator = MicroBeesUpdateCoordinator(hass, entry, microbees)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantMicroBeesData(
|
||||
entry.runtime_data = HomeAssistantMicroBeesData(
|
||||
connector=microbees,
|
||||
coordinator=coordinator,
|
||||
session=session,
|
||||
@@ -76,9 +79,6 @@ 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: MicroBeesConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -7,11 +7,10 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import MicroBeesConfigEntry
|
||||
from .coordinator import MicroBeesUpdateCoordinator
|
||||
from .entity import MicroBeesEntity
|
||||
|
||||
@@ -37,13 +36,11 @@ BINARYSENSOR_TYPES = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MicroBeesConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the microBees binary sensor platform."""
|
||||
coordinator: MicroBeesUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
].coordinator
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
async_add_entities(
|
||||
MBBinarySensor(coordinator, entity_description, bee_id, binary_sensor.id)
|
||||
for bee_id, bee in coordinator.data.bees.items()
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import MicroBeesConfigEntry
|
||||
from .coordinator import MicroBeesUpdateCoordinator
|
||||
from .entity import MicroBeesActuatorEntity
|
||||
|
||||
@@ -16,13 +15,11 @@ BUTTON_TRANSLATIONS = {51: "button_gate", 91: "button_panic"}
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MicroBeesConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the microBees button platform."""
|
||||
coordinator: MicroBeesUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
].coordinator
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
async_add_entities(
|
||||
MBButton(coordinator, bee_id, button.id)
|
||||
for bee_id, bee in coordinator.data.bees.items()
|
||||
|
||||
@@ -7,13 +7,12 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import MicroBeesConfigEntry
|
||||
from .coordinator import MicroBeesUpdateCoordinator
|
||||
from .entity import MicroBeesActuatorEntity
|
||||
|
||||
@@ -27,13 +26,11 @@ THERMOVALVE_SENSOR_ID = 782
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MicroBeesConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the microBees climate platform."""
|
||||
coordinator: MicroBeesUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
].coordinator
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
async_add_entities(
|
||||
MBClimate(
|
||||
coordinator,
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
"""The microBees Coordinator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import aiohttp
|
||||
from microBeesPy import Actuator, Bee, MicroBees, MicroBeesException, Sensor
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MicroBeesConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -29,10 +34,13 @@ class MicroBeesCoordinatorData:
|
||||
class MicroBeesUpdateCoordinator(DataUpdateCoordinator[MicroBeesCoordinatorData]):
|
||||
"""MicroBees coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: MicroBeesConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, microbees: MicroBees
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MicroBeesConfigEntry,
|
||||
microbees: MicroBees,
|
||||
) -> None:
|
||||
"""Initialize microBees coordinator."""
|
||||
super().__init__(
|
||||
|
||||
@@ -9,14 +9,12 @@ from homeassistant.components.cover import (
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MicroBeesUpdateCoordinator
|
||||
from . import MicroBeesConfigEntry
|
||||
from .entity import MicroBeesEntity
|
||||
|
||||
COVER_IDS = {47: "roller_shutter"}
|
||||
@@ -24,13 +22,11 @@ COVER_IDS = {47: "roller_shutter"}
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MicroBeesConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the microBees cover platform."""
|
||||
coordinator: MicroBeesUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
].coordinator
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
|
||||
async_add_entities(
|
||||
MBCover(
|
||||
|
||||
@@ -3,25 +3,22 @@
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.light import ATTR_RGBW_COLOR, ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import MicroBeesConfigEntry
|
||||
from .coordinator import MicroBeesUpdateCoordinator
|
||||
from .entity import MicroBeesActuatorEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MicroBeesConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Config entry."""
|
||||
coordinator: MicroBeesUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
].coordinator
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
async_add_entities(
|
||||
MBLight(coordinator, bee_id, light.id)
|
||||
for bee_id, bee in coordinator.data.bees.items()
|
||||
|
||||
@@ -8,7 +8,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
@@ -19,7 +18,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import MicroBeesConfigEntry
|
||||
from .coordinator import MicroBeesUpdateCoordinator
|
||||
from .entity import MicroBeesEntity
|
||||
|
||||
@@ -64,11 +63,11 @@ SENSOR_TYPES = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MicroBeesConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Config entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id].coordinator
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
|
||||
async_add_entities(
|
||||
MBSensor(coordinator, desc, bee_id, sensor.id)
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import MicroBeesConfigEntry
|
||||
from .coordinator import MicroBeesUpdateCoordinator
|
||||
from .entity import MicroBeesActuatorEntity
|
||||
|
||||
@@ -18,11 +17,11 @@ SWITCH_PRODUCT_IDS = {25, 26, 27, 35, 38, 46, 63, 64, 65, 86}
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MicroBeesConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Config entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id].coordinator
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
|
||||
async_add_entities(
|
||||
MBSwitch(coordinator, bee_id, switch.id)
|
||||
|
||||
@@ -3,16 +3,20 @@
|
||||
from phone_modem import PhoneModem
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import CONF_DEVICE, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DATA_KEY_API, DOMAIN, EXCEPTIONS
|
||||
from .const import EXCEPTIONS
|
||||
|
||||
PLATFORMS = [Platform.BUTTON, Platform.SENSOR]
|
||||
|
||||
type ModemCallerIdConfigEntry = ConfigEntry[PhoneModem]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ModemCallerIdConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Modem Caller ID from a config entry."""
|
||||
device = entry.data[CONF_DEVICE]
|
||||
api = PhoneModem(device)
|
||||
@@ -21,17 +25,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except EXCEPTIONS as ex:
|
||||
raise ConfigEntryNotReady(f"Unable to open port: {device}") from ex
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_KEY_API: api}
|
||||
entry.async_on_unload(api.close)
|
||||
|
||||
async def _async_on_hass_stop(event: Event) -> None:
|
||||
"""HA is shutting down, close modem port."""
|
||||
api.close()
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_on_hass_stop)
|
||||
)
|
||||
|
||||
entry.runtime_data = api
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: ModemCallerIdConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
api = hass.data[DOMAIN].pop(entry.entry_id)[DATA_KEY_API]
|
||||
await api.close()
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -5,26 +5,25 @@ from __future__ import annotations
|
||||
from phone_modem import PhoneModem
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DATA_KEY_API, DOMAIN
|
||||
from . import ModemCallerIdConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: ModemCallerIdConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Modem Caller ID sensor."""
|
||||
api = hass.data[DOMAIN][entry.entry_id][DATA_KEY_API]
|
||||
async_add_entities(
|
||||
[
|
||||
PhoneModemButton(
|
||||
api,
|
||||
entry.runtime_data,
|
||||
entry.data[CONF_DEVICE],
|
||||
entry.entry_id,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Final
|
||||
from phone_modem import exceptions
|
||||
from serial import SerialException
|
||||
|
||||
DATA_KEY_API = "api"
|
||||
DEFAULT_NAME = "Phone Modem"
|
||||
DOMAIN = "modem_callerid"
|
||||
|
||||
|
||||
@@ -5,40 +5,30 @@ from __future__ import annotations
|
||||
from phone_modem import PhoneModem
|
||||
|
||||
from homeassistant.components.sensor import RestoreSensor
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_IDLE
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.const import STATE_IDLE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CID, DATA_KEY_API, DOMAIN
|
||||
from . import ModemCallerIdConfigEntry
|
||||
from .const import CID, DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: ModemCallerIdConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Modem Caller ID sensor."""
|
||||
api = hass.data[DOMAIN][entry.entry_id][DATA_KEY_API]
|
||||
async_add_entities(
|
||||
[
|
||||
ModemCalleridSensor(
|
||||
api,
|
||||
entry.runtime_data,
|
||||
entry.entry_id,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
async def _async_on_hass_stop(event: Event) -> None:
|
||||
"""HA is shutting down, close modem port."""
|
||||
if hass.data[DOMAIN][entry.entry_id][DATA_KEY_API]:
|
||||
await hass.data[DOMAIN][entry.entry_id][DATA_KEY_API].close()
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_on_hass_stop)
|
||||
)
|
||||
|
||||
|
||||
class ModemCalleridSensor(RestoreSensor):
|
||||
"""Implementation of USB modem caller ID sensor."""
|
||||
|
||||
@@ -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[
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -4,41 +4,33 @@ from __future__ import annotations
|
||||
|
||||
from moehlenhoff_alpha2 import Alpha2Base
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import Alpha2BaseCoordinator
|
||||
from .coordinator import Alpha2BaseCoordinator, Alpha2ConfigEntry
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: Alpha2ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
base = Alpha2Base(entry.data[CONF_HOST])
|
||||
coordinator = Alpha2BaseCoordinator(hass, entry, base)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: Alpha2ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok and entry.entry_id in hass.data[DOMAIN]:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
async def update_listener(hass: HomeAssistant, entry: Alpha2ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
@@ -4,24 +4,22 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import Alpha2BaseCoordinator
|
||||
from .coordinator import Alpha2BaseCoordinator, Alpha2ConfigEntry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: Alpha2ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Alpha2 sensor entities from a config_entry."""
|
||||
|
||||
coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
Alpha2IODeviceBatterySensor(coordinator, io_device_id)
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
"""Button entity to set the time of the Alpha2 base."""
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import Alpha2BaseCoordinator
|
||||
from .coordinator import Alpha2BaseCoordinator, Alpha2ConfigEntry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: Alpha2ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Alpha2 button entities."""
|
||||
|
||||
coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities([Alpha2TimeSyncButton(coordinator, config_entry.entry_id)])
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Support for Alpha2 room control unit via Alpha2 base."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
@@ -9,26 +8,23 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, PRESET_AUTO, PRESET_DAY, PRESET_NIGHT
|
||||
from .coordinator import Alpha2BaseCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .const import PRESET_AUTO, PRESET_DAY, PRESET_NIGHT
|
||||
from .coordinator import Alpha2BaseCoordinator, Alpha2ConfigEntry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: Alpha2ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Alpha2Climate entities from a config_entry."""
|
||||
|
||||
coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
Alpha2Climate(coordinator, heat_area_id)
|
||||
|
||||
@@ -17,14 +17,16 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
type Alpha2ConfigEntry = ConfigEntry[Alpha2BaseCoordinator]
|
||||
|
||||
|
||||
class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||
"""Keep the base instance in one place and centralize the update."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: Alpha2ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, base: Alpha2Base
|
||||
self, hass: HomeAssistant, config_entry: Alpha2ConfigEntry, base: Alpha2Base
|
||||
) -> None:
|
||||
"""Initialize Alpha2Base data updater."""
|
||||
self.base = base
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
"""Support for Alpha2 heat control valve opening sensors."""
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import Alpha2BaseCoordinator
|
||||
from .coordinator import Alpha2BaseCoordinator, Alpha2ConfigEntry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: Alpha2ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Alpha2 sensor entities from a config_entry."""
|
||||
|
||||
coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
# HEATCTRL attribute ACTOR_PERCENT is not available in older firmware versions
|
||||
async_add_entities(
|
||||
|
||||
@@ -56,7 +56,6 @@ from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
ATTR_EVENT_TYPE,
|
||||
@@ -69,7 +68,6 @@ from .const import (
|
||||
CONF_SURVEILLANCE_USERNAME,
|
||||
CONF_WEBHOOK_SET,
|
||||
CONF_WEBHOOK_SET_OVERWRITE,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DEFAULT_WEBHOOK_SET,
|
||||
DEFAULT_WEBHOOK_SET_OVERWRITE,
|
||||
DOMAIN,
|
||||
@@ -84,6 +82,7 @@ from .const import (
|
||||
WEB_HOOK_SENTINEL_KEY,
|
||||
WEB_HOOK_SENTINEL_VALUE,
|
||||
)
|
||||
from .coordinator import MotionEyeUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = [CAMERA_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN]
|
||||
@@ -308,20 +307,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass, DOMAIN, "motionEye", entry.data[CONF_WEBHOOK_ID], handle_webhook
|
||||
)
|
||||
|
||||
async def async_update_data() -> dict[str, Any] | None:
|
||||
try:
|
||||
return await client.async_get_cameras()
|
||||
except MotionEyeClientError as exc:
|
||||
raise UpdateFailed("Error communicating with API") from exc
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_method=async_update_data,
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
coordinator = MotionEyeUpdateCoordinator(hass, entry, client)
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
CONF_CLIENT: client,
|
||||
CONF_COORDINATOR: coordinator,
|
||||
|
||||
@@ -43,7 +43,6 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import get_camera_from_cameras, is_acceptable_camera, listen_for_new_cameras
|
||||
from .const import (
|
||||
@@ -60,6 +59,7 @@ from .const import (
|
||||
SERVICE_SNAPSHOT,
|
||||
TYPE_MOTIONEYE_MJPEG_CAMERA,
|
||||
)
|
||||
from .coordinator import MotionEyeUpdateCoordinator
|
||||
from .entity import MotionEyeEntity
|
||||
|
||||
PLATFORMS = [Platform.CAMERA]
|
||||
@@ -153,7 +153,7 @@ class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera):
|
||||
password: str,
|
||||
camera: dict[str, Any],
|
||||
client: MotionEyeClient,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
coordinator: MotionEyeUpdateCoordinator,
|
||||
options: Mapping[str, str],
|
||||
) -> None:
|
||||
"""Initialize a MJPEG camera."""
|
||||
|
||||
41
homeassistant/components/motioneye/coordinator.py
Normal file
41
homeassistant/components/motioneye/coordinator.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Coordinator for the motionEye integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from motioneye_client.client import MotionEyeClient, MotionEyeClientError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MotionEyeUpdateCoordinator(DataUpdateCoordinator[dict[str, Any] | None]):
|
||||
"""Coordinator for motionEye data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, entry: ConfigEntry, client: MotionEyeClient
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
config_entry=entry,
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
self.client = client
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any] | None:
|
||||
try:
|
||||
return await self.client.async_get_cameras()
|
||||
except MotionEyeClientError as exc:
|
||||
raise UpdateFailed("Error communicating with API") from exc
|
||||
@@ -10,12 +10,10 @@ from motioneye_client.const import KEY_ID
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import get_motioneye_device_identifier
|
||||
from .coordinator import MotionEyeUpdateCoordinator
|
||||
|
||||
|
||||
def get_motioneye_entity_unique_id(
|
||||
@@ -25,7 +23,7 @@ def get_motioneye_entity_unique_id(
|
||||
return f"{config_entry_id}_{camera_id}_{entity_type}"
|
||||
|
||||
|
||||
class MotionEyeEntity(CoordinatorEntity):
|
||||
class MotionEyeEntity(CoordinatorEntity[MotionEyeUpdateCoordinator]):
|
||||
"""Base class for motionEye entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
@@ -36,7 +34,7 @@ class MotionEyeEntity(CoordinatorEntity):
|
||||
type_name: str,
|
||||
camera: dict[str, Any],
|
||||
client: MotionEyeClient,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
coordinator: MotionEyeUpdateCoordinator,
|
||||
options: Mapping[str, Any],
|
||||
entity_description: EntityDescription | None = None,
|
||||
) -> None:
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from motioneye_client.client import MotionEyeClient
|
||||
@@ -14,14 +13,12 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import get_camera_from_cameras, listen_for_new_cameras
|
||||
from .const import CONF_CLIENT, CONF_COORDINATOR, DOMAIN, TYPE_MOTIONEYE_ACTION_SENSOR
|
||||
from .coordinator import MotionEyeUpdateCoordinator
|
||||
from .entity import MotionEyeEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -59,7 +56,7 @@ class MotionEyeActionSensor(MotionEyeEntity, SensorEntity):
|
||||
config_entry_id: str,
|
||||
camera: dict[str, Any],
|
||||
client: MotionEyeClient,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
coordinator: MotionEyeUpdateCoordinator,
|
||||
options: Mapping[str, str],
|
||||
) -> None:
|
||||
"""Initialize an action sensor."""
|
||||
|
||||
@@ -20,10 +20,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import get_camera_from_cameras, listen_for_new_cameras
|
||||
from .const import CONF_CLIENT, CONF_COORDINATOR, DOMAIN, TYPE_MOTIONEYE_SWITCH_BASE
|
||||
from .coordinator import MotionEyeUpdateCoordinator
|
||||
from .entity import MotionEyeEntity
|
||||
|
||||
MOTIONEYE_SWITCHES = [
|
||||
@@ -102,7 +102,7 @@ class MotionEyeSwitch(MotionEyeEntity, SwitchEntity):
|
||||
config_entry_id: str,
|
||||
camera: dict[str, Any],
|
||||
client: MotionEyeClient,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
coordinator: MotionEyeUpdateCoordinator,
|
||||
options: Mapping[str, str],
|
||||
entity_description: SwitchEntityDescription,
|
||||
) -> None:
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -90,7 +90,7 @@ async def validate_input(hass: HomeAssistant, user_input: dict[str, Any]) -> Non
|
||||
password=user_input[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
await pyload.login()
|
||||
await pyload.get_status()
|
||||
|
||||
|
||||
class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@@ -64,19 +64,12 @@ class PyLoadCoordinator(DataUpdateCoordinator[PyLoadData]):
|
||||
**await self.pyload.get_status(),
|
||||
free_space=await self.pyload.free_space(),
|
||||
)
|
||||
except InvalidAuth:
|
||||
try:
|
||||
await self.pyload.login()
|
||||
except InvalidAuth as exc:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_authentication_exception",
|
||||
translation_placeholders={CONF_USERNAME: self.pyload.username},
|
||||
) from exc
|
||||
_LOGGER.debug(
|
||||
"Unable to retrieve data due to cookie expiration, retrying after 20 seconds"
|
||||
)
|
||||
return self.data
|
||||
except InvalidAuth as e:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_authentication_exception",
|
||||
translation_placeholders={CONF_USERNAME: self.pyload.username},
|
||||
) from e
|
||||
except CannotConnect as e:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
@@ -92,7 +85,6 @@ class PyLoadCoordinator(DataUpdateCoordinator[PyLoadData]):
|
||||
"""Set up the coordinator."""
|
||||
|
||||
try:
|
||||
await self.pyload.login()
|
||||
self.version = await self.pyload.version()
|
||||
except CannotConnect as e:
|
||||
raise ConfigEntryNotReady(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyloadapi"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["PyLoadAPI==1.4.2"]
|
||||
"requirements": ["PyLoadAPI==2.0.0"]
|
||||
}
|
||||
|
||||
@@ -26,13 +26,5 @@
|
||||
"turn_on": {
|
||||
"service": "mdi:remote"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"turned_off": {
|
||||
"trigger": "mdi:remote-off"
|
||||
},
|
||||
"turned_on": {
|
||||
"trigger": "mdi:remote"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted remotes to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"toggle": "[%key:common::device_automation::action_type::toggle%]",
|
||||
@@ -31,15 +27,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"delete_command": {
|
||||
"description": "Deletes a command or a list of commands from the database.",
|
||||
@@ -126,27 +113,5 @@
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
}
|
||||
},
|
||||
"title": "Remote",
|
||||
"triggers": {
|
||||
"turned_off": {
|
||||
"description": "Triggers when one or more remotes turned off.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::remote::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::remote::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Remote turned off"
|
||||
},
|
||||
"turned_on": {
|
||||
"description": "Triggers when one or more remotes turned on.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::remote::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::remote::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Remote turned on"
|
||||
}
|
||||
}
|
||||
"title": "Remote"
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
"""Provides triggers for remotes."""
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for remotes."""
|
||||
return TRIGGERS
|
||||
@@ -1,18 +0,0 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: remote
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
@@ -107,6 +107,7 @@ PLATFORMS = [
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.TIME,
|
||||
Platform.UPDATE,
|
||||
Platform.VACUUM,
|
||||
Platform.VALVE,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
"state": {
|
||||
"on": "mdi:remote"
|
||||
}
|
||||
},
|
||||
"robot_cleaner_dust_bag": {
|
||||
"default": "mdi:delete"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
@@ -99,6 +102,9 @@
|
||||
"robot_cleaner_driving_mode": {
|
||||
"default": "mdi:car-cog"
|
||||
},
|
||||
"robot_cleaner_water_spray_level": {
|
||||
"default": "mdi:spray-bottle"
|
||||
},
|
||||
"selected_zone": {
|
||||
"state": {
|
||||
"all": "mdi:card",
|
||||
@@ -110,6 +116,9 @@
|
||||
"soil_level": {
|
||||
"default": "mdi:liquid-spot"
|
||||
},
|
||||
"sound_detection_sensitivity": {
|
||||
"default": "mdi:home-sound-in"
|
||||
},
|
||||
"spin_level": {
|
||||
"default": "mdi:rotate-right"
|
||||
},
|
||||
@@ -250,6 +259,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,14 @@ WASHER_SOIL_LEVEL_TO_HA = {
|
||||
"down": "down",
|
||||
}
|
||||
|
||||
WATER_SPRAY_LEVEL_TO_HA = {
|
||||
"high": "high",
|
||||
"mediumHigh": "moderate_high",
|
||||
"medium": "medium",
|
||||
"mediumLow": "moderate_low",
|
||||
"low": "low",
|
||||
}
|
||||
|
||||
WASHER_SPIN_LEVEL_TO_HA = {
|
||||
"none": "none",
|
||||
"rinseHold": "rinse_hold",
|
||||
@@ -165,6 +173,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",
|
||||
@@ -193,6 +210,15 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = {
|
||||
options_map=WASHER_WATER_TEMPERATURE_TO_HA,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
Capability.SAMSUNG_CE_ROBOT_CLEANER_WATER_SPRAY_LEVEL: SmartThingsSelectDescription(
|
||||
key=Capability.SAMSUNG_CE_ROBOT_CLEANER_WATER_SPRAY_LEVEL,
|
||||
translation_key="robot_cleaner_water_spray_level",
|
||||
options_attribute=Attribute.SUPPORTED_WATER_SPRAY_LEVELS,
|
||||
status_attribute=Attribute.WATER_SPRAY_LEVEL,
|
||||
command=Command.SET_WATER_SPRAY_LEVEL,
|
||||
options_map=WATER_SPRAY_LEVEL_TO_HA,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
Capability.SAMSUNG_CE_ROBOT_CLEANER_DRIVING_MODE: SmartThingsSelectDescription(
|
||||
key=Capability.SAMSUNG_CE_ROBOT_CLEANER_DRIVING_MODE,
|
||||
translation_key="robot_cleaner_driving_mode",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
@@ -234,6 +237,16 @@
|
||||
"walls_first": "Walls first"
|
||||
}
|
||||
},
|
||||
"robot_cleaner_water_spray_level": {
|
||||
"name": "Water level",
|
||||
"state": {
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"medium": "Medium",
|
||||
"moderate_high": "Moderate high",
|
||||
"moderate_low": "Moderate low"
|
||||
}
|
||||
},
|
||||
"selected_zone": {
|
||||
"name": "Selected zone",
|
||||
"state": {
|
||||
@@ -256,6 +269,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 +955,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": {
|
||||
|
||||
102
homeassistant/components/smartthings/time.py
Normal file
102
homeassistant/components/smartthings/time.py
Normal 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]))
|
||||
@@ -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."""
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiowebdav2"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiowebdav2==0.5.0"]
|
||||
"requirements": ["aiowebdav2==0.6.1"]
|
||||
}
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["socketio", "engineio", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.4"]
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["yalexs-ble==3.2.4"]
|
||||
"requirements": ["yalexs-ble==3.2.7"]
|
||||
}
|
||||
|
||||
@@ -961,7 +961,8 @@ class HomeAssistant:
|
||||
|
||||
async def async_block_till_done(self, wait_background_tasks: bool = False) -> None:
|
||||
"""Block until all pending work is done."""
|
||||
# To flush out any call_soon_threadsafe
|
||||
# Sleep twice to flush out any call_soon_threadsafe
|
||||
await asyncio.sleep(0)
|
||||
await asyncio.sleep(0)
|
||||
start_time: float | None = None
|
||||
current_task = asyncio.current_task()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
aiodhcpwatcher==1.2.1
|
||||
aiodiscover==2.7.1
|
||||
aiodns==4.0.0
|
||||
aiogithubapi==24.6.0
|
||||
aiogithubapi==26.0.0
|
||||
aiohasupervisor==0.3.3
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
aiohttp-fast-zlib==0.3.0
|
||||
@@ -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
|
||||
|
||||
@@ -27,7 +27,7 @@ dependencies = [
|
||||
# aiogithubapi is needed by frontend; frontend is unconditionally imported at
|
||||
# module level in `bootstrap.py` and its requirements thus need to be in
|
||||
# requirements.txt to ensure they are always installed
|
||||
"aiogithubapi==24.6.0",
|
||||
"aiogithubapi==26.0.0",
|
||||
# Integrations may depend on hassio integration without listing it to
|
||||
# change behavior based on presence of supervisor. Deprecated with #127228
|
||||
# Lib can be removed with 2025.11
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
4
requirements.txt
generated
4
requirements.txt
generated
@@ -4,7 +4,7 @@
|
||||
|
||||
# Home Assistant Core
|
||||
aiodns==4.0.0
|
||||
aiogithubapi==24.6.0
|
||||
aiogithubapi==26.0.0
|
||||
aiohasupervisor==0.3.3
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
aiohttp-fast-zlib==0.3.0
|
||||
@@ -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
|
||||
|
||||
12
requirements_all.txt
generated
12
requirements_all.txt
generated
@@ -56,7 +56,7 @@ PyFlume==0.6.5
|
||||
PyFronius==0.8.0
|
||||
|
||||
# homeassistant.components.pyload
|
||||
PyLoadAPI==1.4.2
|
||||
PyLoadAPI==2.0.0
|
||||
|
||||
# homeassistant.components.met_eireann
|
||||
PyMetEireann==2024.11.0
|
||||
@@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.5
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==12.0.0
|
||||
aioamazondevices==12.0.2
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -267,7 +267,7 @@ aioftp==0.21.3
|
||||
aioghost==0.4.0
|
||||
|
||||
# homeassistant.components.github
|
||||
aiogithubapi==24.6.0
|
||||
aiogithubapi==26.0.0
|
||||
|
||||
# homeassistant.components.guardian
|
||||
aioguardian==2026.01.1
|
||||
@@ -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
|
||||
@@ -2551,7 +2551,7 @@ python-etherscan-api==0.0.3
|
||||
python-family-hub-local==0.0.2
|
||||
|
||||
# homeassistant.components.fully_kiosk
|
||||
python-fullykiosk==0.0.14
|
||||
python-fullykiosk==0.0.15
|
||||
|
||||
# homeassistant.components.gc100
|
||||
python-gc100==1.0.3a0
|
||||
@@ -3307,7 +3307,7 @@ yalesmartalarmclient==0.4.3
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==3.2.4
|
||||
yalexs-ble==3.2.7
|
||||
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
|
||||
12
requirements_test_all.txt
generated
12
requirements_test_all.txt
generated
@@ -56,7 +56,7 @@ PyFlume==0.6.5
|
||||
PyFronius==0.8.0
|
||||
|
||||
# homeassistant.components.pyload
|
||||
PyLoadAPI==1.4.2
|
||||
PyLoadAPI==2.0.0
|
||||
|
||||
# homeassistant.components.met_eireann
|
||||
PyMetEireann==2024.11.0
|
||||
@@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.5
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==12.0.0
|
||||
aioamazondevices==12.0.2
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -255,7 +255,7 @@ aioflo==2021.11.0
|
||||
aioghost==0.4.0
|
||||
|
||||
# homeassistant.components.github
|
||||
aiogithubapi==24.6.0
|
||||
aiogithubapi==26.0.0
|
||||
|
||||
# homeassistant.components.guardian
|
||||
aioguardian==2026.01.1
|
||||
@@ -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
|
||||
@@ -2162,7 +2162,7 @@ python-bsblan==5.1.0
|
||||
python-ecobee-api==0.3.2
|
||||
|
||||
# homeassistant.components.fully_kiosk
|
||||
python-fullykiosk==0.0.14
|
||||
python-fullykiosk==0.0.15
|
||||
|
||||
# homeassistant.components.google_drive
|
||||
python-google-drive-api==0.1.0
|
||||
@@ -2786,7 +2786,7 @@ yalesmartalarmclient==0.4.3
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==3.2.4
|
||||
yalexs-ble==3.2.7
|
||||
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
|
||||
@@ -149,7 +149,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
},
|
||||
"flux_led": {"flux-led": {"async-timeout"}},
|
||||
"foobot": {"foobot-async": {"async-timeout"}},
|
||||
"github": {"aiogithubapi": {"async-timeout"}},
|
||||
"harmony": {"aioharmony": {"async-timeout"}},
|
||||
"here_travel_time": {
|
||||
"here-routing": {"async-timeout"},
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, call
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from chip.clusters.Objects import NullValue
|
||||
from matter_server.client.models.node import MatterNode
|
||||
from matter_server.common.errors import MatterError
|
||||
from matter_server.common.models import EventType, MatterNodeEvent
|
||||
@@ -37,10 +38,12 @@ from .common import (
|
||||
# Feature map bits
|
||||
_FEATURE_PIN = 1 # kPinCredential (bit 0)
|
||||
_FEATURE_RFID = 2 # kRfidCredential (bit 1)
|
||||
_FEATURE_FINGER = 4 # kFingerCredentials (bit 2)
|
||||
_FEATURE_USR = 256 # kUser (bit 8)
|
||||
_FEATURE_USR_PIN = _FEATURE_USR | _FEATURE_PIN # 257
|
||||
_FEATURE_USR_RFID = _FEATURE_USR | _FEATURE_RFID # 258
|
||||
_FEATURE_USR_PIN_RFID = _FEATURE_USR | _FEATURE_PIN | _FEATURE_RFID # 259
|
||||
_FEATURE_USR_FINGER = _FEATURE_USR | _FEATURE_FINGER # 260
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("matter_devices")
|
||||
@@ -516,6 +519,47 @@ async def test_clear_lock_user_service(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@pytest.mark.parametrize("attributes", [{"1/257/65532": _FEATURE_USR_PIN}])
|
||||
async def test_clear_lock_user_credentials_nullvalue(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test clear_lock_user handles NullValue credentials from Matter SDK."""
|
||||
matter_client.send_device_command = AsyncMock(
|
||||
side_effect=[
|
||||
# GetUser returns NullValue for credentials (truthy but not iterable)
|
||||
{"userStatus": 1, "credentials": NullValue},
|
||||
None, # ClearUser
|
||||
]
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"clear_lock_user",
|
||||
{
|
||||
ATTR_ENTITY_ID: "lock.mock_door_lock",
|
||||
ATTR_USER_INDEX: 1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# GetUser + ClearUser (no ClearCredential since NullValue means no credentials)
|
||||
assert matter_client.send_device_command.call_count == 2
|
||||
assert matter_client.send_device_command.call_args_list[0] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.GetUser(userIndex=1),
|
||||
)
|
||||
assert matter_client.send_device_command.call_args_list[1] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.ClearUser(userIndex=1),
|
||||
timed_request_timeout_ms=10000,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@pytest.mark.parametrize("attributes", [{"1/257/65532": _FEATURE_USR_PIN}])
|
||||
async def test_clear_lock_user_clears_credentials_first(
|
||||
@@ -975,6 +1019,53 @@ async def test_get_lock_users_with_credentials(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@pytest.mark.parametrize("attributes", [{"1/257/65532": _FEATURE_USR_PIN}])
|
||||
async def test_get_lock_users_with_nullvalue_credentials(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test get_lock_users handles NullValue credentials from Matter SDK."""
|
||||
matter_client.send_device_command = AsyncMock(
|
||||
side_effect=[
|
||||
{
|
||||
"userIndex": 1,
|
||||
"userStatus": 1,
|
||||
"userName": "User No Creds",
|
||||
"userUniqueID": 100,
|
||||
"userType": 0,
|
||||
"credentialRule": 0,
|
||||
"credentials": NullValue,
|
||||
"nextUserIndex": None,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
result = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"get_lock_users",
|
||||
{ATTR_ENTITY_ID: "lock.mock_door_lock"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
assert matter_client.send_device_command.call_count == 1
|
||||
assert matter_client.send_device_command.call_args == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.GetUser(userIndex=1),
|
||||
)
|
||||
|
||||
lock_users = result["lock.mock_door_lock"]
|
||||
assert len(lock_users["users"]) == 1
|
||||
user = lock_users["users"][0]
|
||||
assert user["user_index"] == 1
|
||||
assert user["user_name"] == "User No Creds"
|
||||
assert user["user_unique_id"] == 100
|
||||
assert user["credentials"] == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@pytest.mark.parametrize("attributes", [{"1/257/65532": _FEATURE_USR_PIN}])
|
||||
@pytest.mark.parametrize(
|
||||
@@ -1116,6 +1207,8 @@ async def test_set_lock_credential_pin(
|
||||
[
|
||||
{
|
||||
"1/257/65532": _FEATURE_USR_PIN,
|
||||
"1/257/18": 3, # NumberOfPINUsersSupported
|
||||
"1/257/28": 2, # NumberOfCredentialsSupportedPerUser (must NOT be used)
|
||||
"1/257/24": 4, # MinPINCodeLength
|
||||
"1/257/23": 8, # MaxPINCodeLength
|
||||
}
|
||||
@@ -1126,19 +1219,24 @@ async def test_set_lock_credential_auto_find_slot(
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test set_lock_credential auto-finds first available slot."""
|
||||
"""Test set_lock_credential auto-finds first available PIN slot."""
|
||||
# Place the empty slot at index 3 (the last position within
|
||||
# NumberOfPINUsersSupported=3) so the test would fail if the code
|
||||
# used NumberOfCredentialsSupportedPerUser=2 instead.
|
||||
matter_client.send_device_command = AsyncMock(
|
||||
side_effect=[
|
||||
# GetCredentialStatus(1): occupied
|
||||
{"credentialExists": True, "userIndex": 1, "nextCredentialIndex": 2},
|
||||
# GetCredentialStatus(2): empty
|
||||
# GetCredentialStatus(2): occupied
|
||||
{"credentialExists": True, "userIndex": 2, "nextCredentialIndex": 3},
|
||||
# GetCredentialStatus(3): empty — found at the bound limit
|
||||
{
|
||||
"credentialExists": False,
|
||||
"userIndex": None,
|
||||
"nextCredentialIndex": 3,
|
||||
"nextCredentialIndex": None,
|
||||
},
|
||||
# SetCredential response
|
||||
{"status": 0, "userIndex": 1, "nextCredentialIndex": 3},
|
||||
{"status": 0, "userIndex": 1, "nextCredentialIndex": None},
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1155,19 +1253,20 @@ async def test_set_lock_credential_auto_find_slot(
|
||||
)
|
||||
|
||||
assert result["lock.mock_door_lock"] == {
|
||||
"credential_index": 2,
|
||||
"credential_index": 3,
|
||||
"user_index": 1,
|
||||
"next_credential_index": 3,
|
||||
"next_credential_index": None,
|
||||
}
|
||||
|
||||
assert matter_client.send_device_command.call_count == 3
|
||||
# Verify SetCredential was called with kAdd for the empty slot at index 2
|
||||
set_cred_cmd = matter_client.send_device_command.call_args_list[2]
|
||||
# 3 GetCredentialStatus calls + 1 SetCredential = 4 total
|
||||
assert matter_client.send_device_command.call_count == 4
|
||||
# Verify SetCredential was called with kAdd for the empty slot at index 3
|
||||
set_cred_cmd = matter_client.send_device_command.call_args_list[3]
|
||||
assert (
|
||||
set_cred_cmd.kwargs["command"].operationType
|
||||
== clusters.DoorLock.Enums.DataOperationTypeEnum.kAdd
|
||||
)
|
||||
assert set_cred_cmd.kwargs["command"].credential.credentialIndex == 2
|
||||
assert set_cred_cmd.kwargs["command"].credential.credentialIndex == 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@@ -1364,6 +1463,8 @@ async def test_set_lock_credential_status_failure(
|
||||
[
|
||||
{
|
||||
"1/257/65532": _FEATURE_USR_PIN,
|
||||
"1/257/18": 3, # NumberOfPINUsersSupported
|
||||
"1/257/28": 5, # NumberOfCredentialsSupportedPerUser (should NOT be used)
|
||||
"1/257/24": 4, # MinPINCodeLength
|
||||
"1/257/23": 8, # MaxPINCodeLength
|
||||
}
|
||||
@@ -1397,6 +1498,78 @@ async def test_set_lock_credential_no_available_slot(
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
# Verify it iterated over NumberOfPINUsersSupported (3), not
|
||||
# NumberOfCredentialsSupportedPerUser (5)
|
||||
assert matter_client.send_device_command.call_count == 3
|
||||
pin_type = clusters.DoorLock.Enums.CredentialTypeEnum.kPin
|
||||
for idx in range(3):
|
||||
assert matter_client.send_device_command.call_args_list[idx] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.GetCredentialStatus(
|
||||
credential=clusters.DoorLock.Structs.CredentialStruct(
|
||||
credentialType=pin_type,
|
||||
credentialIndex=idx + 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@pytest.mark.parametrize(
|
||||
"attributes",
|
||||
[
|
||||
{
|
||||
"1/257/65532": _FEATURE_USR_PIN,
|
||||
"1/257/18": None, # NumberOfPINUsersSupported not available
|
||||
"1/257/24": 4, # MinPINCodeLength
|
||||
"1/257/23": 8, # MaxPINCodeLength
|
||||
}
|
||||
],
|
||||
)
|
||||
async def test_set_lock_credential_auto_find_defaults_to_five(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test set_lock_credential falls back to 5 slots when capacity attribute is None."""
|
||||
# All GetCredentialStatus calls return occupied
|
||||
matter_client.send_device_command = AsyncMock(
|
||||
return_value={
|
||||
"credentialExists": True,
|
||||
"userIndex": 1,
|
||||
"nextCredentialIndex": None,
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(ServiceValidationError, match="No available credential slots"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_lock_credential",
|
||||
{
|
||||
ATTR_ENTITY_ID: "lock.mock_door_lock",
|
||||
ATTR_CREDENTIAL_TYPE: "pin",
|
||||
ATTR_CREDENTIAL_DATA: "1234",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
# With NumberOfPINUsersSupported=None, falls back to default of 5
|
||||
assert matter_client.send_device_command.call_count == 5
|
||||
pin_type = clusters.DoorLock.Enums.CredentialTypeEnum.kPin
|
||||
for idx in range(5):
|
||||
assert matter_client.send_device_command.call_args_list[idx] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.GetCredentialStatus(
|
||||
credential=clusters.DoorLock.Structs.CredentialStruct(
|
||||
credentialType=pin_type,
|
||||
credentialIndex=idx + 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@pytest.mark.parametrize("attributes", [{"1/257/65532": _FEATURE_USR_PIN}])
|
||||
@@ -1646,6 +1819,207 @@ async def test_set_lock_credential_rfid(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@pytest.mark.parametrize(
|
||||
"attributes",
|
||||
[
|
||||
{
|
||||
"1/257/65532": _FEATURE_USR_RFID,
|
||||
"1/257/19": 3, # NumberOfRFIDUsersSupported
|
||||
"1/257/28": 2, # NumberOfCredentialsSupportedPerUser (must NOT be used)
|
||||
"1/257/26": 4, # MinRFIDCodeLength
|
||||
"1/257/25": 20, # MaxRFIDCodeLength
|
||||
}
|
||||
],
|
||||
)
|
||||
async def test_set_lock_credential_rfid_auto_find_slot(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test set_lock_credential auto-finds RFID slot using NumberOfRFIDUsersSupported."""
|
||||
# Place the empty slot at index 3 (the last position within
|
||||
# NumberOfRFIDUsersSupported=3) so the test would fail if the code
|
||||
# used a smaller bound like NumberOfCredentialsSupportedPerUser=2
|
||||
# or stopped iterating too early.
|
||||
matter_client.send_device_command = AsyncMock(
|
||||
side_effect=[
|
||||
# GetCredentialStatus(1): occupied
|
||||
{"credentialExists": True, "userIndex": 1, "nextCredentialIndex": 2},
|
||||
# GetCredentialStatus(2): occupied
|
||||
{"credentialExists": True, "userIndex": 2, "nextCredentialIndex": 3},
|
||||
# GetCredentialStatus(3): empty — found at the bound limit
|
||||
{
|
||||
"credentialExists": False,
|
||||
"userIndex": None,
|
||||
"nextCredentialIndex": None,
|
||||
},
|
||||
# SetCredential response
|
||||
{"status": 0, "userIndex": 1, "nextCredentialIndex": None},
|
||||
]
|
||||
)
|
||||
|
||||
result = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_lock_credential",
|
||||
{
|
||||
ATTR_ENTITY_ID: "lock.mock_door_lock",
|
||||
ATTR_CREDENTIAL_TYPE: "rfid",
|
||||
ATTR_CREDENTIAL_DATA: "AABBCCDD",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
assert result["lock.mock_door_lock"] == {
|
||||
"credential_index": 3,
|
||||
"user_index": 1,
|
||||
"next_credential_index": None,
|
||||
}
|
||||
|
||||
# 3 GetCredentialStatus calls + 1 SetCredential = 4 total
|
||||
assert matter_client.send_device_command.call_count == 4
|
||||
# Verify SetCredential was called with kAdd for the empty slot at index 3
|
||||
set_cred_cmd = matter_client.send_device_command.call_args_list[3]
|
||||
assert (
|
||||
set_cred_cmd.kwargs["command"].operationType
|
||||
== clusters.DoorLock.Enums.DataOperationTypeEnum.kAdd
|
||||
)
|
||||
assert set_cred_cmd.kwargs["command"].credential.credentialIndex == 3
|
||||
assert (
|
||||
set_cred_cmd.kwargs["command"].credential.credentialType
|
||||
== clusters.DoorLock.Enums.CredentialTypeEnum.kRfid
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@pytest.mark.parametrize(
|
||||
"attributes",
|
||||
[
|
||||
{
|
||||
"1/257/65532": _FEATURE_USR_RFID,
|
||||
"1/257/19": 3, # NumberOfRFIDUsersSupported
|
||||
"1/257/28": 5, # NumberOfCredentialsSupportedPerUser (should NOT be used)
|
||||
"1/257/26": 4, # MinRFIDCodeLength
|
||||
"1/257/25": 20, # MaxRFIDCodeLength
|
||||
}
|
||||
],
|
||||
)
|
||||
async def test_set_lock_credential_rfid_no_available_slot(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test set_lock_credential RFID raises error when all slots are full."""
|
||||
matter_client.send_device_command = AsyncMock(
|
||||
return_value={
|
||||
"credentialExists": True,
|
||||
"userIndex": 1,
|
||||
"nextCredentialIndex": None,
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(ServiceValidationError, match="No available credential slots"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_lock_credential",
|
||||
{
|
||||
ATTR_ENTITY_ID: "lock.mock_door_lock",
|
||||
ATTR_CREDENTIAL_TYPE: "rfid",
|
||||
ATTR_CREDENTIAL_DATA: "AABBCCDD",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
# Verify it iterated over NumberOfRFIDUsersSupported (3), not
|
||||
# NumberOfCredentialsSupportedPerUser (5)
|
||||
assert matter_client.send_device_command.call_count == 3
|
||||
rfid_type = clusters.DoorLock.Enums.CredentialTypeEnum.kRfid
|
||||
for idx in range(3):
|
||||
assert matter_client.send_device_command.call_args_list[idx] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.GetCredentialStatus(
|
||||
credential=clusters.DoorLock.Structs.CredentialStruct(
|
||||
credentialType=rfid_type,
|
||||
credentialIndex=idx + 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@pytest.mark.parametrize(
|
||||
"attributes",
|
||||
[
|
||||
{
|
||||
"1/257/65532": _FEATURE_USR_FINGER,
|
||||
"1/257/17": 3, # NumberOfTotalUsersSupported (fallback for biometrics)
|
||||
"1/257/18": 10, # NumberOfPINUsersSupported (should NOT be used)
|
||||
"1/257/28": 2, # NumberOfCredentialsSupportedPerUser (should NOT be used)
|
||||
}
|
||||
],
|
||||
)
|
||||
async def test_set_lock_credential_fingerprint_auto_find_slot(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test set_lock_credential auto-finds fingerprint slot using NumberOfTotalUsersSupported."""
|
||||
# Place the empty slot at index 3 (the last position within
|
||||
# NumberOfTotalUsersSupported=3) so the test would fail if the code
|
||||
# used NumberOfPINUsersSupported (10) or NumberOfCredentialsSupportedPerUser (2).
|
||||
matter_client.send_device_command = AsyncMock(
|
||||
side_effect=[
|
||||
# GetCredentialStatus(1): occupied
|
||||
{"credentialExists": True, "userIndex": 1, "nextCredentialIndex": 2},
|
||||
# GetCredentialStatus(2): occupied
|
||||
{"credentialExists": True, "userIndex": 2, "nextCredentialIndex": 3},
|
||||
# GetCredentialStatus(3): empty — found at the bound limit
|
||||
{
|
||||
"credentialExists": False,
|
||||
"userIndex": None,
|
||||
"nextCredentialIndex": None,
|
||||
},
|
||||
# SetCredential response
|
||||
{"status": 0, "userIndex": 1, "nextCredentialIndex": None},
|
||||
]
|
||||
)
|
||||
|
||||
result = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_lock_credential",
|
||||
{
|
||||
ATTR_ENTITY_ID: "lock.mock_door_lock",
|
||||
ATTR_CREDENTIAL_TYPE: "fingerprint",
|
||||
ATTR_CREDENTIAL_DATA: "AABBCCDD",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
assert result["lock.mock_door_lock"] == {
|
||||
"credential_index": 3,
|
||||
"user_index": 1,
|
||||
"next_credential_index": None,
|
||||
}
|
||||
|
||||
# 3 GetCredentialStatus calls + 1 SetCredential = 4 total
|
||||
assert matter_client.send_device_command.call_count == 4
|
||||
# Verify SetCredential was called with kAdd for the empty slot at index 3
|
||||
set_cred_cmd = matter_client.send_device_command.call_args_list[3]
|
||||
assert (
|
||||
set_cred_cmd.kwargs["command"].operationType
|
||||
== clusters.DoorLock.Enums.DataOperationTypeEnum.kAdd
|
||||
)
|
||||
assert set_cred_cmd.kwargs["command"].credential.credentialIndex == 3
|
||||
assert (
|
||||
set_cred_cmd.kwargs["command"].credential.credentialType
|
||||
== clusters.DoorLock.Enums.CredentialTypeEnum.kFingerprint
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@pytest.mark.parametrize(
|
||||
"attributes",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from pyloadapi.types import LoginResponse, StatusServerResponse
|
||||
from pyloadapi.types import StatusServerResponse
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.pyload.const import DEFAULT_NAME, DOMAIN
|
||||
@@ -76,18 +76,6 @@ def mock_pyloadapi() -> Generator[MagicMock]:
|
||||
client = mock_client.return_value
|
||||
client.username = "username"
|
||||
client.api_url = "https://pyload.local:8000/"
|
||||
client.login.return_value = LoginResponse(
|
||||
{
|
||||
"_permanent": True,
|
||||
"authenticated": True,
|
||||
"id": 2,
|
||||
"name": "username",
|
||||
"role": 0,
|
||||
"perms": 0,
|
||||
"template": "default",
|
||||
"_flashes": [["message", "Logged in successfully"]],
|
||||
}
|
||||
)
|
||||
|
||||
client.get_status.return_value = StatusServerResponse(
|
||||
{
|
||||
|
||||
@@ -1,823 +1,4 @@
|
||||
# serializer version: 1
|
||||
# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_active_downloads-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'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.pyload_active_downloads',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Active downloads',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Active downloads',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.ACTIVE: 'active'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_active',
|
||||
'unit_of_measurement': 'downloads',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_active_downloads-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'pyLoad Active downloads',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'downloads',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_active_downloads',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_in_queue-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'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.pyload_downloads_in_queue',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Downloads in queue',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Downloads in queue',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.QUEUE: 'queue'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_queue',
|
||||
'unit_of_measurement': 'downloads',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_in_queue-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'pyLoad Downloads in queue',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'downloads',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_downloads_in_queue',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_free_space-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': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pyload_free_space',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Free space',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfInformation.GIBIBYTES: 'GiB'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_SIZE: 'data_size'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Free space',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.FREE_SPACE: 'free_space'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_free_space',
|
||||
'unit_of_measurement': <UnitOfInformation.GIBIBYTES: 'GiB'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_free_space-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_size',
|
||||
'friendly_name': 'pyLoad Free space',
|
||||
'unit_of_measurement': <UnitOfInformation.GIBIBYTES: 'GiB'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_free_space',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_speed-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': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Speed',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfDataRate.MEGABITS_PER_SECOND: 'Mbit/s'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_RATE: 'data_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Speed',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.SPEED: 'speed'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABITS_PER_SECOND: 'Mbit/s'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_rate',
|
||||
'friendly_name': 'pyLoad Speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABITS_PER_SECOND: 'Mbit/s'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_total_downloads-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'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.pyload_total_downloads',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Total downloads',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Total downloads',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.TOTAL: 'total'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_total',
|
||||
'unit_of_measurement': 'downloads',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_total_downloads-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'pyLoad Total downloads',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'downloads',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_total_downloads',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_active_downloads-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'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.pyload_active_downloads',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Active downloads',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Active downloads',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.ACTIVE: 'active'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_active',
|
||||
'unit_of_measurement': 'downloads',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_active_downloads-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'pyLoad Active downloads',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'downloads',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_active_downloads',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_in_queue-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'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.pyload_downloads_in_queue',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Downloads in queue',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Downloads in queue',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.QUEUE: 'queue'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_queue',
|
||||
'unit_of_measurement': 'downloads',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_in_queue-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'pyLoad Downloads in queue',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'downloads',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_downloads_in_queue',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '6',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_free_space-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': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pyload_free_space',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Free space',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfInformation.GIBIBYTES: 'GiB'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_SIZE: 'data_size'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Free space',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.FREE_SPACE: 'free_space'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_free_space',
|
||||
'unit_of_measurement': <UnitOfInformation.GIBIBYTES: 'GiB'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_free_space-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_size',
|
||||
'friendly_name': 'pyLoad Free space',
|
||||
'unit_of_measurement': <UnitOfInformation.GIBIBYTES: 'GiB'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_free_space',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '93.1322574606165',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_speed-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': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Speed',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfDataRate.MEGABITS_PER_SECOND: 'Mbit/s'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_RATE: 'data_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Speed',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.SPEED: 'speed'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABITS_PER_SECOND: 'Mbit/s'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_rate',
|
||||
'friendly_name': 'pyLoad Speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABITS_PER_SECOND: 'Mbit/s'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '43.247704',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_total_downloads-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'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.pyload_total_downloads',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Total downloads',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Total downloads',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.TOTAL: 'total'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_total',
|
||||
'unit_of_measurement': 'downloads',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_total_downloads-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'pyLoad Total downloads',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'downloads',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_total_downloads',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '37',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[ParserError][sensor.pyload_active_downloads-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'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.pyload_active_downloads',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Active downloads',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Active downloads',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.ACTIVE: 'active'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_active',
|
||||
'unit_of_measurement': 'downloads',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[ParserError][sensor.pyload_active_downloads-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'pyLoad Active downloads',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'downloads',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_active_downloads',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_in_queue-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'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.pyload_downloads_in_queue',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Downloads in queue',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Downloads in queue',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.QUEUE: 'queue'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_queue',
|
||||
'unit_of_measurement': 'downloads',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_in_queue-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'pyLoad Downloads in queue',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'downloads',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_downloads_in_queue',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[ParserError][sensor.pyload_free_space-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': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pyload_free_space',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Free space',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfInformation.GIBIBYTES: 'GiB'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_SIZE: 'data_size'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Free space',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.FREE_SPACE: 'free_space'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_free_space',
|
||||
'unit_of_measurement': <UnitOfInformation.GIBIBYTES: 'GiB'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[ParserError][sensor.pyload_free_space-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_size',
|
||||
'friendly_name': 'pyLoad Free space',
|
||||
'unit_of_measurement': <UnitOfInformation.GIBIBYTES: 'GiB'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_free_space',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[ParserError][sensor.pyload_speed-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': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Speed',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfDataRate.MEGABITS_PER_SECOND: 'Mbit/s'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_RATE: 'data_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Speed',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.SPEED: 'speed'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABITS_PER_SECOND: 'Mbit/s'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[ParserError][sensor.pyload_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_rate',
|
||||
'friendly_name': 'pyLoad Speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABITS_PER_SECOND: 'Mbit/s'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[ParserError][sensor.pyload_total_downloads-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'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.pyload_total_downloads',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Total downloads',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Total downloads',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PyLoadSensorEntity.TOTAL: 'total'>,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_total',
|
||||
'unit_of_measurement': 'downloads',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[ParserError][sensor.pyload_total_downloads-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'pyLoad Total downloads',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'downloads',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_total_downloads',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[sensor.pyload_active_downloads-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -65,7 +65,7 @@ async def test_form_errors(
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
mock_pyloadapi.login.side_effect = exception
|
||||
mock_pyloadapi.get_status.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -75,7 +75,7 @@ async def test_form_errors(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
|
||||
mock_pyloadapi.login.side_effect = None
|
||||
mock_pyloadapi.get_status.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
@@ -159,7 +159,7 @@ async def test_reauth_errors(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
mock_pyloadapi.login.side_effect = side_effect
|
||||
mock_pyloadapi.get_status.side_effect = side_effect
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
REAUTH_INPUT,
|
||||
@@ -168,7 +168,7 @@ async def test_reauth_errors(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error_text}
|
||||
|
||||
mock_pyloadapi.login.side_effect = None
|
||||
mock_pyloadapi.get_status.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
REAUTH_INPUT,
|
||||
@@ -231,7 +231,7 @@ async def test_reconfigure_errors(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
mock_pyloadapi.login.side_effect = side_effect
|
||||
mock_pyloadapi.get_status.side_effect = side_effect
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
@@ -240,7 +240,7 @@ async def test_reconfigure_errors(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error_text}
|
||||
|
||||
mock_pyloadapi.login.side_effect = None
|
||||
mock_pyloadapi.get_status.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
@@ -261,7 +261,7 @@ async def test_hassio_discovery(
|
||||
) -> None:
|
||||
"""Test flow started from Supervisor discovery."""
|
||||
|
||||
mock_pyloadapi.login.side_effect = InvalidAuth
|
||||
mock_pyloadapi.get_status.side_effect = InvalidAuth
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
@@ -273,7 +273,7 @@ async def test_hassio_discovery(
|
||||
assert result["step_id"] == "hassio_confirm"
|
||||
assert result["errors"] is None
|
||||
|
||||
mock_pyloadapi.login.side_effect = None
|
||||
mock_pyloadapi.get_status.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_USERNAME: "pyload", CONF_PASSWORD: "pyload"}
|
||||
@@ -325,7 +325,7 @@ async def test_hassio_discovery_errors(
|
||||
) -> None:
|
||||
"""Test flow started from Supervisor discovery."""
|
||||
|
||||
mock_pyloadapi.login.side_effect = side_effect
|
||||
mock_pyloadapi.get_status.side_effect = side_effect
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
@@ -344,7 +344,7 @@ async def test_hassio_discovery_errors(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error_text}
|
||||
|
||||
mock_pyloadapi.login.side_effect = None
|
||||
mock_pyloadapi.get_status.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_USERNAME: "pyload", CONF_PASSWORD: "pyload"}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"""Test pyLoad init."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError
|
||||
import pytest
|
||||
|
||||
@@ -11,7 +9,7 @@ from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import CONF_PATH, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_entry_setup_unload(
|
||||
@@ -44,7 +42,7 @@ async def test_config_entry_setup_errors(
|
||||
side_effect: Exception,
|
||||
) -> None:
|
||||
"""Test config entry not ready."""
|
||||
mock_pyloadapi.login.side_effect = side_effect
|
||||
mock_pyloadapi.version.side_effect = side_effect
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -58,7 +56,7 @@ async def test_config_entry_setup_invalid_auth(
|
||||
mock_pyloadapi: MagicMock,
|
||||
) -> None:
|
||||
"""Test config entry authentication."""
|
||||
mock_pyloadapi.login.side_effect = InvalidAuth
|
||||
mock_pyloadapi.version.side_effect = InvalidAuth
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -72,25 +70,61 @@ async def test_coordinator_update_invalid_auth(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_pyloadapi: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test coordinator authentication."""
|
||||
mock_pyloadapi.get_status.side_effect = InvalidAuth
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_pyloadapi.login.side_effect = InvalidAuth
|
||||
mock_pyloadapi.get_status.side_effect = InvalidAuth
|
||||
|
||||
freezer.tick(timedelta(seconds=20))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
assert any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
|
||||
async def test_coordinator_setup_invalid_auth(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_pyloadapi: MagicMock,
|
||||
) -> None:
|
||||
"""Test coordinator setup authentication."""
|
||||
mock_pyloadapi.version.side_effect = InvalidAuth
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
assert any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "state"),
|
||||
[
|
||||
(CannotConnect, ConfigEntryState.SETUP_RETRY),
|
||||
(InvalidAuth, ConfigEntryState.SETUP_ERROR),
|
||||
(ParserError, ConfigEntryState.SETUP_RETRY),
|
||||
],
|
||||
)
|
||||
async def test_coordinator_update_errors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_pyloadapi: MagicMock,
|
||||
exception: Exception,
|
||||
state: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Test coordinator setup authentication."""
|
||||
mock_pyloadapi.get_status.side_effect = exception
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is state
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_pyloadapi")
|
||||
async def test_migration(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -3,18 +3,15 @@
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.pyload.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -42,78 +39,3 @@ async def test_setup(
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[CannotConnect, InvalidAuth, ParserError],
|
||||
)
|
||||
async def test_sensor_update_exceptions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
exception: Exception,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test if pyLoad sensors go unavailable when exceptions occur (except ParserErrors)."""
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_pyloadapi.get_status.side_effect = exception
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_sensor_invalid_auth(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test invalid auth during sensor update."""
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_pyloadapi.get_status.side_effect = InvalidAuth
|
||||
mock_pyloadapi.login.side_effect = InvalidAuth
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
"Authentication failed for username, verify your login credentials"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_pyload_pre_0_5_0(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setup of the pyload sensor platform."""
|
||||
mock_pyloadapi.get_status.return_value = {
|
||||
"pause": False,
|
||||
"active": 1,
|
||||
"queue": 6,
|
||||
"total": 37,
|
||||
"speed": 5405963.0,
|
||||
"download": True,
|
||||
"reconnect": False,
|
||||
}
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
"""Test remote trigger."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.remote import DOMAIN
|
||||
from homeassistant.const import ATTR_LABEL_ID, CONF_ENTITY_ID, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
|
||||
from tests.components import (
|
||||
TriggerStateDescription,
|
||||
arm_trigger,
|
||||
parametrize_target_entities,
|
||||
parametrize_trigger_states,
|
||||
set_or_remove_state,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_remotes(hass: HomeAssistant) -> list[str]:
|
||||
"""Create multiple remotes entities associated with different targets."""
|
||||
return (await target_entities(hass, DOMAIN))["included"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_key",
|
||||
["remote.turned_on", "remote.turned_off"],
|
||||
)
|
||||
async def test_remote_triggers_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
|
||||
) -> None:
|
||||
"""Test the remote triggers are gated by the labs flag."""
|
||||
await arm_trigger(hass, trigger_key, None, {ATTR_LABEL_ID: "test_label"})
|
||||
assert (
|
||||
"Unnamed automation failed to setup triggers and has been disabled: Trigger "
|
||||
f"'{trigger_key}' requires the experimental 'New triggers and conditions' "
|
||||
"feature to be enabled in Home Assistant Labs settings (feature flag: "
|
||||
"'new_triggers_conditions')"
|
||||
) in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="remote.turned_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="remote.turned_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_remote_state_trigger_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_remotes: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test that the remote triggers when any remote changes to a specific state."""
|
||||
other_entity_ids = set(target_remotes) - {entity_id}
|
||||
|
||||
# Set all remotes, including the tested remote, to the initial state
|
||||
for eid in target_remotes:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Check that changing other remotes also triggers
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == (entities_in_target - 1) * state["count"]
|
||||
service_calls.clear()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="remote.turned_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="remote.turned_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_remote_state_trigger_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_remotes: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test that the remote triggers when the first remote changes to a specific state."""
|
||||
other_entity_ids = set(target_remotes) - {entity_id}
|
||||
|
||||
# Set all remotes, including the tested remote, to the initial state
|
||||
for eid in target_remotes:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "first"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Triggering other remotes should not cause the trigger to fire again
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="remote.turned_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="remote.turned_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_remote_state_trigger_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_remotes: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test that the remote triggers when the last remote changes to a specific state."""
|
||||
other_entity_ids = set(target_remotes) - {entity_id}
|
||||
|
||||
# Set all remotes, including the tested remote, to the initial state
|
||||
for eid in target_remotes:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
@@ -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({
|
||||
|
||||
@@ -533,6 +533,130 @@
|
||||
'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_rvc_map_01011][select.robot_vacuum_water_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'high',
|
||||
'moderate_high',
|
||||
'medium',
|
||||
'moderate_low',
|
||||
'low',
|
||||
]),
|
||||
}),
|
||||
'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_water_level',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Water level',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Water level',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'robot_cleaner_water_spray_level',
|
||||
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_samsungce.robotCleanerWaterSprayLevel_waterSprayLevel_waterSprayLevel',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_water_level-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Robot Vacuum Water level',
|
||||
'options': list([
|
||||
'high',
|
||||
'moderate_high',
|
||||
'medium',
|
||||
'moderate_low',
|
||||
'low',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.robot_vacuum_water_level',
|
||||
'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({
|
||||
|
||||
@@ -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]
|
||||
|
||||
197
tests/components/smartthings/snapshots/test_time.ambr
Normal file
197
tests/components/smartthings/snapshots/test_time.ambr
Normal 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',
|
||||
})
|
||||
# ---
|
||||
@@ -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,
|
||||
|
||||
128
tests/components/smartthings/test_time.py
Normal file
128
tests/components/smartthings/test_time.py
Normal 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",
|
||||
},
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user