mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 23:08:12 +00:00
Compare commits
9 Commits
knx-text-u
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dc9084f06 | ||
|
|
39ba36d642 | ||
|
|
5009560f57 | ||
|
|
41e88573bb | ||
|
|
27ee986b1b | ||
|
|
c9d21c1851 | ||
|
|
2afbdc5757 | ||
|
|
14cb8af9fe | ||
|
|
74ae0f8297 |
@@ -12,7 +12,7 @@ from homeassistant.helpers import entity_registry as er
|
|||||||
|
|
||||||
from . import BeoConfigEntry
|
from . import BeoConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .util import get_device_buttons
|
from .util import get_device_buttons, get_remote_keys, get_remotes
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
@@ -53,4 +53,23 @@ async def async_get_config_entry_diagnostics(
|
|||||||
state_dict.pop("context")
|
state_dict.pop("context")
|
||||||
data[f"{device_button}_event"] = state_dict
|
data[f"{device_button}_event"] = state_dict
|
||||||
|
|
||||||
|
# Get remotes
|
||||||
|
for remote in await get_remotes(config_entry.runtime_data.client):
|
||||||
|
# Get key Event entity states (if enabled)
|
||||||
|
for key_type in get_remote_keys():
|
||||||
|
if entity_id := entity_registry.async_get_entity_id(
|
||||||
|
EVENT_DOMAIN,
|
||||||
|
DOMAIN,
|
||||||
|
f"{remote.serial_number}_{config_entry.unique_id}_{key_type}",
|
||||||
|
):
|
||||||
|
if state := hass.states.get(entity_id):
|
||||||
|
state_dict = dict(state.as_dict())
|
||||||
|
|
||||||
|
# Remove context as it is not relevant
|
||||||
|
state_dict.pop("context")
|
||||||
|
data[f"remote_{remote.serial_number}_{key_type}_event"] = state_dict
|
||||||
|
|
||||||
|
# Add remote Mozart model
|
||||||
|
data[f"remote_{remote.serial_number}"] = dict(remote)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -16,11 +16,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
|
|
||||||
from . import BeoConfigEntry
|
from . import BeoConfigEntry
|
||||||
from .const import (
|
from .const import (
|
||||||
BEO_REMOTE_CONTROL_KEYS,
|
|
||||||
BEO_REMOTE_KEY_EVENTS,
|
BEO_REMOTE_KEY_EVENTS,
|
||||||
BEO_REMOTE_KEYS,
|
|
||||||
BEO_REMOTE_SUBMENU_CONTROL,
|
|
||||||
BEO_REMOTE_SUBMENU_LIGHT,
|
|
||||||
CONNECTION_STATUS,
|
CONNECTION_STATUS,
|
||||||
DEVICE_BUTTON_EVENTS,
|
DEVICE_BUTTON_EVENTS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@@ -29,7 +25,7 @@ from .const import (
|
|||||||
WebsocketNotification,
|
WebsocketNotification,
|
||||||
)
|
)
|
||||||
from .entity import BeoEntity
|
from .entity import BeoEntity
|
||||||
from .util import get_device_buttons, get_remotes
|
from .util import get_device_buttons, get_remote_keys, get_remotes
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
@@ -40,38 +36,19 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Event entities from config entry."""
|
"""Set up Event entities from config entry."""
|
||||||
entities: list[BeoEvent] = []
|
entities: list[BeoEvent] = [
|
||||||
|
|
||||||
async_add_entities(
|
|
||||||
BeoButtonEvent(config_entry, button_type)
|
BeoButtonEvent(config_entry, button_type)
|
||||||
for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
|
for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
|
||||||
)
|
]
|
||||||
|
|
||||||
# Check for connected Beoremote One
|
# Check for connected Beoremote One
|
||||||
remotes = await get_remotes(config_entry.runtime_data.client)
|
remotes = await get_remotes(config_entry.runtime_data.client)
|
||||||
|
|
||||||
for remote in remotes:
|
for remote in remotes:
|
||||||
# Add Light keys
|
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
[
|
||||||
BeoRemoteKeyEvent(
|
BeoRemoteKeyEvent(config_entry, remote, key_type)
|
||||||
config_entry,
|
for key_type in get_remote_keys()
|
||||||
remote,
|
|
||||||
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
|
|
||||||
)
|
|
||||||
for key_type in BEO_REMOTE_KEYS
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add Control keys
|
|
||||||
entities.extend(
|
|
||||||
[
|
|
||||||
BeoRemoteKeyEvent(
|
|
||||||
config_entry,
|
|
||||||
remote,
|
|
||||||
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
|
|
||||||
)
|
|
||||||
for key_type in (*BEO_REMOTE_KEYS, *BEO_REMOTE_CONTROL_KEYS)
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,16 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.device_registry import DeviceEntry
|
from homeassistant.helpers.device_registry import DeviceEntry
|
||||||
|
|
||||||
from .const import DEVICE_BUTTONS, DOMAIN, BeoButtons, BeoModel
|
from .const import (
|
||||||
|
BEO_REMOTE_CONTROL_KEYS,
|
||||||
|
BEO_REMOTE_KEYS,
|
||||||
|
BEO_REMOTE_SUBMENU_CONTROL,
|
||||||
|
BEO_REMOTE_SUBMENU_LIGHT,
|
||||||
|
DEVICE_BUTTONS,
|
||||||
|
DOMAIN,
|
||||||
|
BeoButtons,
|
||||||
|
BeoModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
|
def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
|
||||||
@@ -64,3 +73,14 @@ def get_device_buttons(model: BeoModel) -> list[str]:
|
|||||||
buttons.remove(BeoButtons.BLUETOOTH)
|
buttons.remove(BeoButtons.BLUETOOTH)
|
||||||
|
|
||||||
return buttons
|
return buttons
|
||||||
|
|
||||||
|
|
||||||
|
def get_remote_keys() -> list[str]:
|
||||||
|
"""Get remote keys for the Beoremote One. Formatted for Home Assistant use."""
|
||||||
|
return [
|
||||||
|
*[f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}" for key_type in BEO_REMOTE_KEYS],
|
||||||
|
*[
|
||||||
|
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}"
|
||||||
|
for key_type in (*BEO_REMOTE_KEYS, *BEO_REMOTE_CONTROL_KEYS)
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ MEDIA_MODES = {
|
|||||||
"Favorites": "FAVORITES",
|
"Favorites": "FAVORITES",
|
||||||
"Internet Radio": "IRADIO",
|
"Internet Radio": "IRADIO",
|
||||||
"USB/IPOD": "USB/IPOD",
|
"USB/IPOD": "USB/IPOD",
|
||||||
|
"USB": "USB",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Sub-modes of 'NET/USB'
|
# Sub-modes of 'NET/USB'
|
||||||
@@ -279,7 +280,7 @@ class DenonDevice(MediaPlayerEntity):
|
|||||||
def mute_volume(self, mute: bool) -> None:
|
def mute_volume(self, mute: bool) -> None:
|
||||||
"""Mute (true) or unmute (false) media player."""
|
"""Mute (true) or unmute (false) media player."""
|
||||||
mute_status = "ON" if mute else "OFF"
|
mute_status = "ON" if mute else "OFF"
|
||||||
self.telnet_command(f"MU{mute_status})")
|
self.telnet_command(f"MU{mute_status}")
|
||||||
|
|
||||||
def media_play(self) -> None:
|
def media_play(self) -> None:
|
||||||
"""Play media player."""
|
"""Play media player."""
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@adrianmo"],
|
"codeowners": ["@adrianmo"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/meteoclimatic",
|
"documentation": "https://www.home-assistant.io/integrations/meteoclimatic",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["meteoclimatic"],
|
"loggers": ["meteoclimatic"],
|
||||||
"requirements": ["pymeteoclimatic==0.1.0"]
|
"requirements": ["pymeteoclimatic==0.1.0"]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@MrHarcombe", "@avee87"],
|
"codeowners": ["@MrHarcombe", "@avee87"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/metoffice",
|
"documentation": "https://www.home-assistant.io/integrations/metoffice",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["datapoint"],
|
"loggers": ["datapoint"],
|
||||||
"requirements": ["datapoint==0.12.1"]
|
"requirements": ["datapoint==0.12.1"]
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ from .entity import PooldoseEntity
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
BINARY_SENSOR_DESCRIPTIONS: tuple[BinarySensorEntityDescription, ...] = (
|
BINARY_SENSOR_DESCRIPTIONS: tuple[BinarySensorEntityDescription, ...] = (
|
||||||
BinarySensorEntityDescription(
|
BinarySensorEntityDescription(
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
NUMBER_DESCRIPTIONS: tuple[NumberEntityDescription, ...] = (
|
NUMBER_DESCRIPTIONS: tuple[NumberEntityDescription, ...] = (
|
||||||
NumberEntityDescription(
|
NumberEntityDescription(
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ rules:
|
|||||||
entity-unavailable: done
|
entity-unavailable: done
|
||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: done
|
log-when-unavailable: done
|
||||||
parallel-updates: todo
|
parallel-updates: done
|
||||||
reauthentication-flow:
|
reauthentication-flow:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: This integration does not need authentication for the local API.
|
comment: This integration does not need authentication for the local API.
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class PooldoseSelectEntityDescription(SelectEntityDescription):
|
class PooldoseSelectEntityDescription(SelectEntityDescription):
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ from .entity import PooldoseEntity
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class PooldoseSensorEntityDescription(SensorEntityDescription):
|
class PooldoseSensorEntityDescription(SensorEntityDescription):
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
SWITCH_DESCRIPTIONS: tuple[SwitchEntityDescription, ...] = (
|
SWITCH_DESCRIPTIONS: tuple[SwitchEntityDescription, ...] = (
|
||||||
SwitchEntityDescription(
|
SwitchEntityDescription(
|
||||||
|
|||||||
@@ -11,5 +11,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
|
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioqsw"],
|
"loggers": ["aioqsw"],
|
||||||
"requirements": ["aioqsw==0.4.1"]
|
"requirements": ["aioqsw==0.4.2"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Sequence
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PySrDaliGateway import DaliGateway
|
from PySrDaliGateway import DaliGateway, Device
|
||||||
from PySrDaliGateway.exceptions import DaliGatewayError
|
from PySrDaliGateway.exceptions import DaliGatewayError
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@@ -28,6 +29,38 @@ _PLATFORMS: list[Platform] = [Platform.LIGHT, Platform.SCENE]
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_missing_devices(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: DaliCenterConfigEntry,
|
||||||
|
devices: Sequence[Device],
|
||||||
|
gateway_identifier: tuple[str, str],
|
||||||
|
) -> None:
|
||||||
|
"""Detach devices that are no longer provided by the gateway."""
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
known_device_ids = {device.dev_id for device in devices}
|
||||||
|
|
||||||
|
for device_entry in dr.async_entries_for_config_entry(
|
||||||
|
device_registry, entry.entry_id
|
||||||
|
):
|
||||||
|
if gateway_identifier in device_entry.identifiers:
|
||||||
|
continue
|
||||||
|
|
||||||
|
domain_device_ids = {
|
||||||
|
identifier[1]
|
||||||
|
for identifier in device_entry.identifiers
|
||||||
|
if identifier[0] == DOMAIN
|
||||||
|
}
|
||||||
|
|
||||||
|
if not domain_device_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if domain_device_ids.isdisjoint(known_device_ids):
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device_entry.id,
|
||||||
|
remove_config_entry_id=entry.entry_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: DaliCenterConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: DaliCenterConfigEntry) -> bool:
|
||||||
"""Set up Sunricher DALI from a config entry."""
|
"""Set up Sunricher DALI from a config entry."""
|
||||||
|
|
||||||
@@ -70,6 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaliCenterConfigEntry) -
|
|||||||
model="SR-GW-EDA",
|
model="SR-GW-EDA",
|
||||||
serial_number=gw_sn,
|
serial_number=gw_sn,
|
||||||
)
|
)
|
||||||
|
_remove_missing_devices(hass, entry, devices, (DOMAIN, gw_sn))
|
||||||
|
|
||||||
entry.runtime_data = DaliCenterData(
|
entry.runtime_data = DaliCenterData(
|
||||||
gateway=gateway,
|
gateway=gateway,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from pyvlx import PyVLX, PyVLXException
|
from pyvlx import PyVLX, PyVLXException
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_MAC,
|
CONF_MAC,
|
||||||
@@ -12,13 +12,54 @@ from homeassistant.const import (
|
|||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady, ServiceValidationError
|
||||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
from homeassistant.helpers import (
|
||||||
|
config_validation as cv,
|
||||||
|
device_registry as dr,
|
||||||
|
issue_registry as ir,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER, PLATFORMS
|
from .const import DOMAIN, LOGGER, PLATFORMS
|
||||||
|
|
||||||
type VeluxConfigEntry = ConfigEntry[PyVLX]
|
type VeluxConfigEntry = ConfigEntry[PyVLX]
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up the Velux component."""
|
||||||
|
|
||||||
|
async def async_reboot_gateway(service_call: ServiceCall) -> None:
|
||||||
|
"""Reboot the gateway (deprecated - use button entity instead)."""
|
||||||
|
ir.async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
"deprecated_reboot_service",
|
||||||
|
is_fixable=False,
|
||||||
|
issue_domain=DOMAIN,
|
||||||
|
severity=ir.IssueSeverity.WARNING,
|
||||||
|
translation_key="deprecated_reboot_service",
|
||||||
|
breaks_in_ha_version="2026.6.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find a loaded config entry to get the PyVLX instance
|
||||||
|
# We assume only one gateway is set up or we just reboot the first one found
|
||||||
|
# (this is no change to the previous behavior, the alternative would be to reboot all)
|
||||||
|
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||||
|
if entry.state is ConfigEntryState.LOADED:
|
||||||
|
await entry.runtime_data.reboot_gateway()
|
||||||
|
return
|
||||||
|
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="no_gateway_loaded",
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.services.async_register(DOMAIN, "reboot_gateway", async_reboot_gateway)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> bool:
|
||||||
"""Set up the velux component."""
|
"""Set up the velux component."""
|
||||||
@@ -67,27 +108,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> boo
|
|||||||
LOGGER.debug("Velux interface terminated")
|
LOGGER.debug("Velux interface terminated")
|
||||||
await pyvlx.disconnect()
|
await pyvlx.disconnect()
|
||||||
|
|
||||||
async def async_reboot_gateway(service_call: ServiceCall) -> None:
|
|
||||||
"""Reboot the gateway (deprecated - use button entity instead)."""
|
|
||||||
ir.async_create_issue(
|
|
||||||
hass,
|
|
||||||
DOMAIN,
|
|
||||||
"deprecated_reboot_service",
|
|
||||||
is_fixable=False,
|
|
||||||
issue_domain=DOMAIN,
|
|
||||||
severity=ir.IssueSeverity.WARNING,
|
|
||||||
translation_key="deprecated_reboot_service",
|
|
||||||
breaks_in_ha_version="2026.6.0",
|
|
||||||
)
|
|
||||||
|
|
||||||
await pyvlx.reboot_gateway()
|
|
||||||
|
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.async_register(DOMAIN, "reboot_gateway", async_reboot_gateway)
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -95,4 +119,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> boo
|
|||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
|
# Disconnect from gateway only after platforms are successfully unloaded.
|
||||||
|
# Disconnecting will reboot the gateway in the pyvlx library, which is needed to allow new
|
||||||
|
# connections to be made later.
|
||||||
|
await entry.runtime_data.disconnect()
|
||||||
|
return unload_ok
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
rules:
|
rules:
|
||||||
# Bronze
|
# Bronze
|
||||||
action-setup:
|
action-setup: done
|
||||||
status: todo
|
|
||||||
comment: needs to move to async_setup
|
|
||||||
appropriate-polling: done
|
appropriate-polling: done
|
||||||
brands: done
|
brands: done
|
||||||
common-modules: done
|
common-modules: done
|
||||||
@@ -25,7 +23,7 @@ rules:
|
|||||||
|
|
||||||
# Silver
|
# Silver
|
||||||
action-exceptions: todo
|
action-exceptions: todo
|
||||||
config-entry-unloading: todo
|
config-entry-unloading: done
|
||||||
docs-configuration-parameters: todo
|
docs-configuration-parameters: todo
|
||||||
docs-installation-parameters: todo
|
docs-installation-parameters: todo
|
||||||
entity-unavailable: todo
|
entity-unavailable: todo
|
||||||
|
|||||||
@@ -37,6 +37,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
|
"no_gateway_loaded": {
|
||||||
|
"message": "No loaded Velux gateway found"
|
||||||
|
},
|
||||||
"reboot_failed": {
|
"reboot_failed": {
|
||||||
"message": "Failed to reboot gateway. Try again in a few moments or power cycle the device manually"
|
"message": "Failed to reboot gateway. Try again in a few moments or power cycle the device manually"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3956,13 +3956,13 @@
|
|||||||
},
|
},
|
||||||
"meteoclimatic": {
|
"meteoclimatic": {
|
||||||
"name": "Meteoclimatic",
|
"name": "Meteoclimatic",
|
||||||
"integration_type": "hub",
|
"integration_type": "service",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
},
|
},
|
||||||
"metoffice": {
|
"metoffice": {
|
||||||
"name": "Met Office",
|
"name": "Met Office",
|
||||||
"integration_type": "hub",
|
"integration_type": "service",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
},
|
},
|
||||||
|
|||||||
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -369,7 +369,7 @@ aiopvpc==4.2.2
|
|||||||
aiopyarr==23.4.0
|
aiopyarr==23.4.0
|
||||||
|
|
||||||
# homeassistant.components.qnap_qsw
|
# homeassistant.components.qnap_qsw
|
||||||
aioqsw==0.4.1
|
aioqsw==0.4.2
|
||||||
|
|
||||||
# homeassistant.components.rainforest_raven
|
# homeassistant.components.rainforest_raven
|
||||||
aioraven==0.7.1
|
aioraven==0.7.1
|
||||||
|
|||||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -354,7 +354,7 @@ aiopvpc==4.2.2
|
|||||||
aiopyarr==23.4.0
|
aiopyarr==23.4.0
|
||||||
|
|
||||||
# homeassistant.components.qnap_qsw
|
# homeassistant.components.qnap_qsw
|
||||||
aioqsw==0.4.1
|
aioqsw==0.4.2
|
||||||
|
|
||||||
# homeassistant.components.rainforest_raven
|
# homeassistant.components.rainforest_raven
|
||||||
aioraven==0.7.1
|
aioraven==0.7.1
|
||||||
|
|||||||
@@ -82,6 +82,30 @@
|
|||||||
'entity_id': 'media_player.beosound_balance_11111111',
|
'entity_id': 'media_player.beosound_balance_11111111',
|
||||||
'state': 'playing',
|
'state': 'playing',
|
||||||
}),
|
}),
|
||||||
|
'remote_55555555': dict({
|
||||||
|
'address': '',
|
||||||
|
'app_version': '1.0.0',
|
||||||
|
'battery_level': 50,
|
||||||
|
'connected': True,
|
||||||
|
'db_version': None,
|
||||||
|
'last_seen': None,
|
||||||
|
'name': 'BEORC',
|
||||||
|
'serial_number': '55555555',
|
||||||
|
'updated': None,
|
||||||
|
}),
|
||||||
|
'remote_55555555_Control/Play_event': dict({
|
||||||
|
'attributes': dict({
|
||||||
|
'device_class': 'button',
|
||||||
|
'event_type': None,
|
||||||
|
'event_types': list([
|
||||||
|
'key_press',
|
||||||
|
'key_release',
|
||||||
|
]),
|
||||||
|
'friendly_name': 'Beoremote One-55555555-11111111 Control - Play',
|
||||||
|
}),
|
||||||
|
'entity_id': 'event.beoremote_one_55555555_11111111_control_play',
|
||||||
|
'state': 'unknown',
|
||||||
|
}),
|
||||||
'websocket_connected': False,
|
'websocket_connected': False,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||||
|
|
||||||
from .conftest import mock_websocket_connection
|
from .conftest import mock_websocket_connection
|
||||||
from .const import TEST_BUTTON_EVENT_ENTITY_ID
|
from .const import TEST_BUTTON_EVENT_ENTITY_ID, TEST_REMOTE_KEY_EVENT_ENTITY_ID
|
||||||
|
|
||||||
from tests.common import AsyncMock, MockConfigEntry
|
from tests.common import AsyncMock, MockConfigEntry
|
||||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||||
@@ -25,8 +25,11 @@ async def test_async_get_config_entry_diagnostics(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test config entry diagnostics."""
|
"""Test config entry diagnostics."""
|
||||||
|
|
||||||
# Enable an Event entity
|
# Enable a button and remote key Event entity
|
||||||
entity_registry.async_update_entity(TEST_BUTTON_EVENT_ENTITY_ID, disabled_by=None)
|
entity_registry.async_update_entity(TEST_BUTTON_EVENT_ENTITY_ID, disabled_by=None)
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
TEST_REMOTE_KEY_EVENT_ENTITY_ID, disabled_by=None
|
||||||
|
)
|
||||||
hass.config_entries.async_schedule_reload(mock_config_entry.entry_id)
|
hass.config_entries.async_schedule_reload(mock_config_entry.entry_id)
|
||||||
|
|
||||||
# Re-trigger WebSocket events after the reload
|
# Re-trigger WebSocket events after the reload
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ from unittest.mock import MagicMock
|
|||||||
from PySrDaliGateway.exceptions import DaliGatewayError
|
from PySrDaliGateway.exceptions import DaliGatewayError
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.sunricher_dali.const import DOMAIN
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@@ -63,6 +64,23 @@ async def test_setup_entry_connection_error(
|
|||||||
mock_gateway.connect.assert_called_once()
|
mock_gateway.connect.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_entry_discovery_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_gateway: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup fails when device discovery fails."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
mock_gateway.discover_devices.side_effect = DaliGatewayError("Discovery failed")
|
||||||
|
|
||||||
|
assert not await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
mock_gateway.connect.assert_called_once()
|
||||||
|
mock_gateway.discover_devices.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
async def test_unload_entry(
|
async def test_unload_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
@@ -80,3 +98,40 @@ async def test_unload_entry(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remove_stale_devices(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_gateway: MagicMock,
|
||||||
|
mock_devices: list[MagicMock],
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test stale devices are removed when device list decreases."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
devices_before = dr.async_entries_for_config_entry(
|
||||||
|
device_registry, mock_config_entry.entry_id
|
||||||
|
)
|
||||||
|
initial_count = len(devices_before)
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
mock_gateway.discover_devices.return_value = mock_devices[:2]
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
devices_after = dr.async_entries_for_config_entry(
|
||||||
|
device_registry, mock_config_entry.entry_id
|
||||||
|
)
|
||||||
|
assert len(devices_after) < initial_count
|
||||||
|
|
||||||
|
gateway_device = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, mock_gateway.gw_sn)}
|
||||||
|
)
|
||||||
|
assert gateway_device is not None
|
||||||
|
assert mock_config_entry.entry_id in gateway_device.config_entries
|
||||||
|
|||||||
@@ -2,16 +2,22 @@
|
|||||||
|
|
||||||
These tests verify that setup retries (ConfigEntryNotReady) are triggered
|
These tests verify that setup retries (ConfigEntryNotReady) are triggered
|
||||||
when scene or node loading fails.
|
when scene or node loading fails.
|
||||||
|
|
||||||
|
They also verify that unloading the integration properly disconnects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
from pyvlx.exception import PyVLXException
|
from pyvlx.exception import PyVLXException
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from tests.common import AsyncMock, ConfigEntry
|
from tests.common import AsyncMock, ConfigEntry, MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_retry_on_nodes_failure(
|
async def test_setup_retry_on_nodes_failure(
|
||||||
@@ -53,3 +59,44 @@ async def test_setup_retry_on_oserror_during_scenes(
|
|||||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
mock_pyvlx.load_scenes.assert_awaited_once()
|
mock_pyvlx.load_scenes.assert_awaited_once()
|
||||||
mock_pyvlx.load_nodes.assert_not_called()
|
mock_pyvlx.load_nodes.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def platform() -> Platform:
|
||||||
|
"""Fixture to specify platform to test."""
|
||||||
|
return Platform.COVER
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_integration")
|
||||||
|
async def test_unload_calls_disconnect(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_pyvlx
|
||||||
|
) -> None:
|
||||||
|
"""Test that unloading the config entry disconnects from the gateway."""
|
||||||
|
|
||||||
|
# Unload the entry
|
||||||
|
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Verify disconnect was called
|
||||||
|
mock_pyvlx.disconnect.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_integration")
|
||||||
|
async def test_unload_does_not_disconnect_if_platform_unload_fails(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_pyvlx
|
||||||
|
) -> None:
|
||||||
|
"""Test that disconnect is not called if platform unload fails."""
|
||||||
|
|
||||||
|
# Mock platform unload to fail
|
||||||
|
with patch(
|
||||||
|
"homeassistant.config_entries.ConfigEntries.async_unload_platforms",
|
||||||
|
return_value=False,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Verify unload failed
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
# Verify disconnect was NOT called since platform unload failed
|
||||||
|
mock_pyvlx.disconnect.assert_not_awaited()
|
||||||
|
|||||||
Reference in New Issue
Block a user