mirror of
https://github.com/home-assistant/core.git
synced 2026-03-03 06:17:01 +01:00
Compare commits
10 Commits
epenet/202
...
python-3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09546525b6 | ||
|
|
37a59bd23c | ||
|
|
6793a98c08 | ||
|
|
0ee4589ba3 | ||
|
|
55bd4b00b4 | ||
|
|
43f5f922e3 | ||
|
|
5dd6dcc215 | ||
|
|
8bf894a514 | ||
|
|
d3c67f2ae1 | ||
|
|
b60a282b60 |
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
|
||||
|
||||
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: {}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -102,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",
|
||||
|
||||
@@ -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",
|
||||
@@ -202,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",
|
||||
|
||||
@@ -237,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": {
|
||||
|
||||
@@ -139,10 +139,10 @@ async def async_setup_entry(
|
||||
action_wrapper=_AlarmActionWrapper(
|
||||
master_mode.dpcode, master_mode
|
||||
),
|
||||
changed_by_wrapper=_AlarmChangedByWrapper.find_dpcode( # type: ignore[arg-type]
|
||||
changed_by_wrapper=_AlarmChangedByWrapper.find_dpcode(
|
||||
device, DPCode.ALARM_MSG
|
||||
),
|
||||
state_wrapper=_AlarmStateWrapper( # type: ignore[arg-type]
|
||||
state_wrapper=_AlarmStateWrapper(
|
||||
master_mode.dpcode, master_mode
|
||||
),
|
||||
)
|
||||
|
||||
@@ -177,7 +177,7 @@ class _HvacModeWrapper(DPCodeEnumWrapper):
|
||||
return None
|
||||
return TUYA_HVAC_TO_HA[raw]
|
||||
|
||||
def _convert_value_to_raw_value(
|
||||
def _convert_value_to_raw_value( # type: ignore[override]
|
||||
self,
|
||||
device: CustomerDevice,
|
||||
value: HVACMode,
|
||||
@@ -358,7 +358,7 @@ async def async_setup_entry(
|
||||
device,
|
||||
manager,
|
||||
CLIMATE_DESCRIPTIONS[device.category],
|
||||
current_humidity_wrapper=_RoundedIntegerWrapper.find_dpcode( # type: ignore[arg-type]
|
||||
current_humidity_wrapper=_RoundedIntegerWrapper.find_dpcode(
|
||||
device, DPCode.HUMIDITY_CURRENT
|
||||
),
|
||||
current_temperature_wrapper=temperature_wrappers[0],
|
||||
@@ -367,7 +367,7 @@ async def async_setup_entry(
|
||||
(DPCode.FAN_SPEED_ENUM, DPCode.LEVEL, DPCode.WINDSPEED),
|
||||
prefer_function=True,
|
||||
),
|
||||
hvac_mode_wrapper=_HvacModeWrapper.find_dpcode( # type: ignore[arg-type]
|
||||
hvac_mode_wrapper=_HvacModeWrapper.find_dpcode(
|
||||
device, DPCode.MODE, prefer_function=True
|
||||
),
|
||||
preset_wrapper=_PresetWrapper.find_dpcode(
|
||||
@@ -378,7 +378,7 @@ async def async_setup_entry(
|
||||
switch_wrapper=DPCodeBooleanWrapper.find_dpcode(
|
||||
device, DPCode.SWITCH, prefer_function=True
|
||||
),
|
||||
target_humidity_wrapper=_RoundedIntegerWrapper.find_dpcode( # type: ignore[arg-type]
|
||||
target_humidity_wrapper=_RoundedIntegerWrapper.find_dpcode(
|
||||
device, DPCode.HUMIDITY_SET, prefer_function=True
|
||||
),
|
||||
temperature_unit=temperature_wrappers[2],
|
||||
|
||||
@@ -87,7 +87,7 @@ class _InstructionBooleanWrapper(DPCodeBooleanWrapper):
|
||||
options = ["open", "close"]
|
||||
_ACTION_MAPPINGS = {"open": True, "close": False}
|
||||
|
||||
def _convert_value_to_raw_value(self, device: CustomerDevice, value: str) -> bool:
|
||||
def _convert_value_to_raw_value(self, device: CustomerDevice, value: str) -> bool: # type: ignore[override]
|
||||
return self._ACTION_MAPPINGS[value]
|
||||
|
||||
|
||||
@@ -291,19 +291,19 @@ async def async_setup_entry(
|
||||
device,
|
||||
manager,
|
||||
description,
|
||||
current_position=description.position_wrapper.find_dpcode( # type: ignore[arg-type]
|
||||
current_position=description.position_wrapper.find_dpcode(
|
||||
device, description.current_position
|
||||
),
|
||||
current_state_wrapper=description.current_state_wrapper.find_dpcode( # type: ignore[arg-type]
|
||||
current_state_wrapper=description.current_state_wrapper.find_dpcode(
|
||||
device, description.current_state
|
||||
),
|
||||
instruction_wrapper=_get_instruction_wrapper(
|
||||
device, description
|
||||
),
|
||||
set_position=description.position_wrapper.find_dpcode( # type: ignore[arg-type]
|
||||
set_position=description.position_wrapper.find_dpcode(
|
||||
device, description.set_position, prefer_function=True
|
||||
),
|
||||
tilt_position=description.position_wrapper.find_dpcode( # type: ignore[arg-type]
|
||||
tilt_position=description.position_wrapper.find_dpcode(
|
||||
device,
|
||||
(DPCode.ANGLE_HORIZONTAL, DPCode.ANGLE_VERTICAL),
|
||||
prefer_function=True,
|
||||
|
||||
@@ -49,7 +49,7 @@ class _AlarmMessageWrapper(DPCodeStringWrapper):
|
||||
super().__init__(dpcode, type_information)
|
||||
self.options = ["triggered"]
|
||||
|
||||
def read_device_status( # type: ignore[override]
|
||||
def read_device_status(
|
||||
self, device: CustomerDevice
|
||||
) -> tuple[str, dict[str, Any]] | None:
|
||||
"""Return the event attributes for the alarm message."""
|
||||
|
||||
@@ -154,7 +154,7 @@ async def async_setup_entry(
|
||||
oscillate_wrapper=DPCodeBooleanWrapper.find_dpcode(
|
||||
device, _OSCILLATE_DPCODES, prefer_function=True
|
||||
),
|
||||
speed_wrapper=_get_speed_wrapper(device), # type: ignore[arg-type]
|
||||
speed_wrapper=_get_speed_wrapper(device),
|
||||
switch_wrapper=DPCodeBooleanWrapper.find_dpcode(
|
||||
device, _SWITCH_DPCODES, prefer_function=True
|
||||
),
|
||||
|
||||
@@ -104,7 +104,7 @@ async def async_setup_entry(
|
||||
device,
|
||||
manager,
|
||||
description,
|
||||
current_humidity_wrapper=_RoundedIntegerWrapper.find_dpcode( # type: ignore[arg-type]
|
||||
current_humidity_wrapper=_RoundedIntegerWrapper.find_dpcode(
|
||||
device, description.current_humidity
|
||||
),
|
||||
mode_wrapper=DPCodeEnumWrapper.find_dpcode(
|
||||
@@ -115,7 +115,7 @@ async def async_setup_entry(
|
||||
description.dpcode or description.key,
|
||||
prefer_function=True,
|
||||
),
|
||||
target_humidity_wrapper=_RoundedIntegerWrapper.find_dpcode( # type: ignore[arg-type]
|
||||
target_humidity_wrapper=_RoundedIntegerWrapper.find_dpcode(
|
||||
device, description.humidity, prefer_function=True
|
||||
),
|
||||
)
|
||||
|
||||
@@ -633,17 +633,17 @@ async def async_setup_entry(
|
||||
manager,
|
||||
description,
|
||||
brightness_wrapper=(
|
||||
brightness_wrapper := _get_brightness_wrapper( # type: ignore[arg-type]
|
||||
brightness_wrapper := _get_brightness_wrapper(
|
||||
device, description
|
||||
)
|
||||
),
|
||||
color_data_wrapper=_get_color_data_wrapper( # type: ignore[arg-type]
|
||||
color_data_wrapper=_get_color_data_wrapper(
|
||||
device, description, brightness_wrapper
|
||||
),
|
||||
color_mode_wrapper=DPCodeEnumWrapper.find_dpcode(
|
||||
device, description.color_mode, prefer_function=True
|
||||
),
|
||||
color_temp_wrapper=_ColorTempWrapper.find_dpcode( # type: ignore[arg-type]
|
||||
color_temp_wrapper=_ColorTempWrapper.find_dpcode(
|
||||
device, description.color_temp, prefer_function=True
|
||||
),
|
||||
switch_wrapper=switch_wrapper,
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["tuya_sharing"],
|
||||
"requirements": [
|
||||
"tuya-device-handlers==0.0.11",
|
||||
"tuya-device-handlers==0.0.10",
|
||||
"tuya-device-sharing-sdk==0.2.8"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -3121,7 +3121,7 @@ ttls==1.8.3
|
||||
ttn_client==1.2.3
|
||||
|
||||
# homeassistant.components.tuya
|
||||
tuya-device-handlers==0.0.11
|
||||
tuya-device-handlers==0.0.10
|
||||
|
||||
# homeassistant.components.tuya
|
||||
tuya-device-sharing-sdk==0.2.8
|
||||
|
||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -2624,7 +2624,7 @@ ttls==1.8.3
|
||||
ttn_client==1.2.3
|
||||
|
||||
# homeassistant.components.tuya
|
||||
tuya-device-handlers==0.0.11
|
||||
tuya-device-handlers==0.0.10
|
||||
|
||||
# homeassistant.components.tuya
|
||||
tuya-device-sharing-sdk==0.2.8
|
||||
|
||||
@@ -593,6 +593,70 @@
|
||||
'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({
|
||||
|
||||
Reference in New Issue
Block a user