mirror of
https://github.com/home-assistant/core.git
synced 2025-12-21 15:28:19 +00:00
Compare commits
13 Commits
input_bool
...
edenhaus-g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27ad5b0716 | ||
|
|
7a37120e77 | ||
|
|
41e88573bb | ||
|
|
27ee986b1b | ||
|
|
c9d21c1851 | ||
|
|
2afbdc5757 | ||
|
|
14cb8af9fe | ||
|
|
74ae0f8297 | ||
|
|
3050a5c896 | ||
|
|
9f886e66c7 | ||
|
|
3c752d4516 | ||
|
|
e4bfdf5b30 | ||
|
|
3e43424a73 |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -794,6 +794,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/intellifire/ @jeeftor
|
/tests/components/intellifire/ @jeeftor
|
||||||
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
|
/homeassistant/components/intent_script/ @arturpragacz
|
||||||
|
/tests/components/intent_script/ @arturpragacz
|
||||||
/homeassistant/components/intesishome/ @jnimmo
|
/homeassistant/components/intesishome/ @jnimmo
|
||||||
/homeassistant/components/iometer/ @jukrebs
|
/homeassistant/components/iometer/ @jukrebs
|
||||||
/tests/components/iometer/ @jukrebs
|
/tests/components/iometer/ @jukrebs
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
|||||||
"device_tracker",
|
"device_tracker",
|
||||||
"fan",
|
"fan",
|
||||||
"humidifier",
|
"humidifier",
|
||||||
"input_boolean",
|
|
||||||
"lawn_mower",
|
"lawn_mower",
|
||||||
"light",
|
"light",
|
||||||
"lock",
|
"lock",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ async def async_register_dynalite_frontend(hass: HomeAssistant):
|
|||||||
frontend_url_path=DOMAIN,
|
frontend_url_path=DOMAIN,
|
||||||
config_panel_domain=DOMAIN,
|
config_panel_domain=DOMAIN,
|
||||||
webcomponent_name="dynalite-panel",
|
webcomponent_name="dynalite-panel",
|
||||||
module_url=f"{URL_BASE}/entrypoint-{build_id}.js",
|
module_url=f"{URL_BASE}/entrypoint.{build_id}.js",
|
||||||
embed_iframe=True,
|
embed_iframe=True,
|
||||||
require_admin=True,
|
require_admin=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["go2rtc-client==0.3.0"],
|
"requirements": ["go2rtc-client==0.4.0"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
|
|||||||
device_type=device_type,
|
device_type=device_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# For NVRs or devices with no detected events, try to fetch events from ISAPI
|
||||||
|
if device_type == "NVR" or not camera.current_event_states:
|
||||||
|
|
||||||
|
def fetch_and_inject_nvr_events() -> None:
|
||||||
|
"""Fetch and inject NVR events in a single executor job."""
|
||||||
|
if nvr_events := camera.get_event_triggers(None):
|
||||||
|
camera.inject_events(nvr_events)
|
||||||
|
|
||||||
|
await hass.async_add_executor_job(fetch_and_inject_nvr_events)
|
||||||
|
|
||||||
# Start the event stream
|
# Start the event stream
|
||||||
await hass.async_add_executor_job(camera.start_stream)
|
await hass.async_add_executor_job(camera.start_stream)
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,5 @@
|
|||||||
"turn_on": {
|
"turn_on": {
|
||||||
"service": "mdi:toggle-switch"
|
"service": "mdi:toggle-switch"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"triggers": {
|
|
||||||
"turned_off": {
|
|
||||||
"trigger": "mdi:toggle-switch-off"
|
|
||||||
},
|
|
||||||
"turned_on": {
|
|
||||||
"trigger": "mdi:toggle-switch"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
|
||||||
"trigger_behavior_description": "The behavior of the targeted input booleans to trigger on.",
|
|
||||||
"trigger_behavior_name": "Behavior"
|
|
||||||
},
|
|
||||||
"entity_component": {
|
"entity_component": {
|
||||||
"_": {
|
"_": {
|
||||||
"name": "[%key:component::input_boolean::title%]",
|
"name": "[%key:component::input_boolean::title%]",
|
||||||
@@ -21,15 +17,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"trigger_behavior": {
|
|
||||||
"options": {
|
|
||||||
"any": "Any",
|
|
||||||
"first": "First",
|
|
||||||
"last": "Last"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"services": {
|
"services": {
|
||||||
"reload": {
|
"reload": {
|
||||||
"description": "Reloads helpers from the YAML-configuration.",
|
"description": "Reloads helpers from the YAML-configuration.",
|
||||||
@@ -48,27 +35,5 @@
|
|||||||
"name": "[%key:common::action::turn_on%]"
|
"name": "[%key:common::action::turn_on%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Input boolean",
|
"title": "Input boolean"
|
||||||
"triggers": {
|
|
||||||
"turned_off": {
|
|
||||||
"description": "Triggers after one or more input booleans turn off.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::input_boolean::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::input_boolean::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Input boolean turned off"
|
|
||||||
},
|
|
||||||
"turned_on": {
|
|
||||||
"description": "Triggers after one or more input booleans turn on.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::input_boolean::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::input_boolean::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Input boolean turned on"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
"""Provides triggers for input booleans."""
|
|
||||||
|
|
||||||
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 input booleans."""
|
|
||||||
return TRIGGERS
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
.trigger_common: &trigger_common
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
domain: input_boolean
|
|
||||||
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,7 +107,7 @@ async def async_register_insteon_frontend(hass: HomeAssistant):
|
|||||||
frontend_url_path=DOMAIN,
|
frontend_url_path=DOMAIN,
|
||||||
webcomponent_name="insteon-frontend",
|
webcomponent_name="insteon-frontend",
|
||||||
config_panel_domain=DOMAIN,
|
config_panel_domain=DOMAIN,
|
||||||
module_url=f"{URL_BASE}/entrypoint-{build_id}.js",
|
module_url=f"{URL_BASE}/entrypoint.{build_id}.js",
|
||||||
embed_iframe=True,
|
embed_iframe=True,
|
||||||
require_admin=True,
|
require_admin=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -282,6 +282,8 @@ async def websocket_reset_properties(
|
|||||||
notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND)
|
notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
for prop in device.configuration.values():
|
||||||
|
prop.new_value = None
|
||||||
for prop in device.operating_flags:
|
for prop in device.operating_flags:
|
||||||
device.operating_flags[prop].new_value = None
|
device.operating_flags[prop].new_value = None
|
||||||
for prop in device.properties:
|
for prop in device.properties:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"loggers": ["pyinsteon", "pypubsub"],
|
"loggers": ["pyinsteon", "pypubsub"],
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"pyinsteon==1.6.4",
|
"pyinsteon==1.6.4",
|
||||||
"insteon-frontend-home-assistant==0.5.0"
|
"insteon-frontend-home-assistant==0.6.0"
|
||||||
],
|
],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"usb": [
|
"usb": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "intent_script",
|
"domain": "intent_script",
|
||||||
"name": "Intent Script",
|
"name": "Intent Script",
|
||||||
"codeowners": [],
|
"codeowners": ["@arturpragacz"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/intent_script",
|
"documentation": "https://www.home-assistant.io/integrations/intent_script",
|
||||||
"quality_scale": "internal"
|
"quality_scale": "internal"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["uiprotect", "unifi_discovery"],
|
"loggers": ["uiprotect", "unifi_discovery"],
|
||||||
|
"quality_scale": "platinum",
|
||||||
"requirements": ["uiprotect==7.33.2", "unifi-discovery==1.2.0"],
|
"requirements": ["uiprotect==7.33.2", "unifi-discovery==1.2.0"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
|
|||||||
68
homeassistant/components/unifiprotect/quality_scale.yaml
Normal file
68
homeassistant/components/unifiprotect/quality_scale.yaml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup: done
|
||||||
|
appropriate-polling:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration is push-based using WebSockets (iot_class local_push).
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions: done
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup: done
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
runtime-data: done
|
||||||
|
test-before-configure: done
|
||||||
|
test-before-setup: done
|
||||||
|
unique-config-entry: done
|
||||||
|
|
||||||
|
# Silver
|
||||||
|
action-exceptions: done
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters: done
|
||||||
|
docs-installation-parameters: done
|
||||||
|
entity-unavailable: done
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable: done
|
||||||
|
parallel-updates: done
|
||||||
|
reauthentication-flow: done
|
||||||
|
test-coverage:
|
||||||
|
status: done
|
||||||
|
comment: Diagnostics tests could use snapshot testing.
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: done
|
||||||
|
discovery-update-info: done
|
||||||
|
discovery: done
|
||||||
|
docs-data-update: done
|
||||||
|
docs-examples: done
|
||||||
|
docs-known-limitations: done
|
||||||
|
docs-supported-devices: done
|
||||||
|
docs-supported-functions: done
|
||||||
|
docs-troubleshooting: done
|
||||||
|
docs-use-cases: done
|
||||||
|
dynamic-devices: done
|
||||||
|
entity-category: done
|
||||||
|
entity-device-class:
|
||||||
|
status: done
|
||||||
|
comment: "Planned improvement: remove doorbell occupancy binary sensor and keep the event sensor after a solution for https://github.com/home-assistant/core/issues/145941 is available."
|
||||||
|
entity-disabled-by-default: done
|
||||||
|
entity-translations:
|
||||||
|
status: done
|
||||||
|
comment: "Planned improvement: camera insecure is not translated but will be dropped soon with public api migration"
|
||||||
|
exception-translations: done
|
||||||
|
icon-translations: done
|
||||||
|
reconfiguration-flow: done
|
||||||
|
repair-issues: done
|
||||||
|
stale-devices: done
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: done
|
||||||
|
strict-typing: done
|
||||||
@@ -48,37 +48,37 @@ _KEY_LIGHT_MOTION = "light_motion"
|
|||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
HDR_MODES = [
|
HDR_MODES = [
|
||||||
{"id": "always", "name": "Always On"},
|
{"id": "always", "name": "always"},
|
||||||
{"id": "off", "name": "Always Off"},
|
{"id": "off", "name": "off"},
|
||||||
{"id": "auto", "name": "Auto"},
|
{"id": "auto", "name": "auto"},
|
||||||
]
|
]
|
||||||
|
|
||||||
INFRARED_MODES = [
|
INFRARED_MODES = [
|
||||||
{"id": IRLEDMode.AUTO.value, "name": "Auto"},
|
{"id": IRLEDMode.AUTO.value, "name": "auto"},
|
||||||
{"id": IRLEDMode.ON.value, "name": "Always Enable"},
|
{"id": IRLEDMode.ON.value, "name": "on"},
|
||||||
{"id": IRLEDMode.AUTO_NO_LED.value, "name": "Auto (Filter Only, no LED's)"},
|
{"id": IRLEDMode.AUTO_NO_LED.value, "name": "auto_filter_only"},
|
||||||
{"id": IRLEDMode.CUSTOM.value, "name": "Auto (Custom Lux)"},
|
{"id": IRLEDMode.CUSTOM.value, "name": "custom"},
|
||||||
{"id": IRLEDMode.OFF.value, "name": "Always Disable"},
|
{"id": IRLEDMode.OFF.value, "name": "off"},
|
||||||
]
|
]
|
||||||
|
|
||||||
CHIME_TYPES = [
|
CHIME_TYPES = [
|
||||||
{"id": ChimeType.NONE.value, "name": "None"},
|
{"id": ChimeType.NONE.value, "name": "none"},
|
||||||
{"id": ChimeType.MECHANICAL.value, "name": "Mechanical"},
|
{"id": ChimeType.MECHANICAL.value, "name": "mechanical"},
|
||||||
{"id": ChimeType.DIGITAL.value, "name": "Digital"},
|
{"id": ChimeType.DIGITAL.value, "name": "digital"},
|
||||||
]
|
]
|
||||||
|
|
||||||
MOUNT_TYPES = [
|
MOUNT_TYPES = [
|
||||||
{"id": MountType.NONE.value, "name": "None"},
|
{"id": MountType.NONE.value, "name": MountType.NONE.value},
|
||||||
{"id": MountType.DOOR.value, "name": "Door"},
|
{"id": MountType.DOOR.value, "name": MountType.DOOR.value},
|
||||||
{"id": MountType.WINDOW.value, "name": "Window"},
|
{"id": MountType.WINDOW.value, "name": MountType.WINDOW.value},
|
||||||
{"id": MountType.GARAGE.value, "name": "Garage"},
|
{"id": MountType.GARAGE.value, "name": MountType.GARAGE.value},
|
||||||
{"id": MountType.LEAK.value, "name": "Leak"},
|
{"id": MountType.LEAK.value, "name": MountType.LEAK.value},
|
||||||
]
|
]
|
||||||
|
|
||||||
LIGHT_MODE_MOTION = "On Motion - Always"
|
LIGHT_MODE_MOTION = "motion"
|
||||||
LIGHT_MODE_MOTION_DARK = "On Motion - When Dark"
|
LIGHT_MODE_MOTION_DARK = "motion_dark"
|
||||||
LIGHT_MODE_DARK = "When Dark"
|
LIGHT_MODE_DARK = "when_dark"
|
||||||
LIGHT_MODE_OFF = "Manual"
|
LIGHT_MODE_OFF = "manual"
|
||||||
LIGHT_MODES = [LIGHT_MODE_MOTION, LIGHT_MODE_DARK, LIGHT_MODE_OFF]
|
LIGHT_MODES = [LIGHT_MODE_MOTION, LIGHT_MODE_DARK, LIGHT_MODE_OFF]
|
||||||
|
|
||||||
LIGHT_MODE_TO_SETTINGS = {
|
LIGHT_MODE_TO_SETTINGS = {
|
||||||
@@ -93,13 +93,13 @@ LIGHT_MODE_TO_SETTINGS = {
|
|||||||
|
|
||||||
MOTION_MODE_TO_LIGHT_MODE = [
|
MOTION_MODE_TO_LIGHT_MODE = [
|
||||||
{"id": LightModeType.MOTION.value, "name": LIGHT_MODE_MOTION},
|
{"id": LightModeType.MOTION.value, "name": LIGHT_MODE_MOTION},
|
||||||
{"id": f"{LightModeType.MOTION.value}Dark", "name": LIGHT_MODE_MOTION_DARK},
|
{"id": f"{LightModeType.MOTION.value}_dark", "name": LIGHT_MODE_MOTION_DARK},
|
||||||
{"id": LightModeType.WHEN_DARK.value, "name": LIGHT_MODE_DARK},
|
{"id": LightModeType.WHEN_DARK.value, "name": LIGHT_MODE_DARK},
|
||||||
{"id": LightModeType.MANUAL.value, "name": LIGHT_MODE_OFF},
|
{"id": LightModeType.MANUAL.value, "name": LIGHT_MODE_OFF},
|
||||||
]
|
]
|
||||||
|
|
||||||
DEVICE_RECORDING_MODES = [
|
DEVICE_RECORDING_MODES = [
|
||||||
{"id": mode.value, "name": mode.value.title()} for mode in list(RecordingMode)
|
{"id": mode.value, "name": mode.value} for mode in list(RecordingMode)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -350,31 +350,68 @@
|
|||||||
},
|
},
|
||||||
"select": {
|
"select": {
|
||||||
"chime_type": {
|
"chime_type": {
|
||||||
"name": "Chime type"
|
"name": "Chime type",
|
||||||
|
"state": {
|
||||||
|
"digital": "Digital",
|
||||||
|
"mechanical": "Mechanical",
|
||||||
|
"none": "[%key:common::state::off%]"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"doorbell_text": {
|
"doorbell_text": {
|
||||||
"name": "Doorbell text"
|
"name": "Doorbell text"
|
||||||
},
|
},
|
||||||
"hdr_mode": {
|
"hdr_mode": {
|
||||||
"name": "[%key:component::unifiprotect::entity::binary_sensor::hdr_mode::name%]"
|
"name": "[%key:component::unifiprotect::entity::binary_sensor::hdr_mode::name%]",
|
||||||
|
"state": {
|
||||||
|
"always": "Always on",
|
||||||
|
"auto": "Auto",
|
||||||
|
"off": "Always off"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"infrared_mode": {
|
"infrared_mode": {
|
||||||
"name": "Infrared mode"
|
"name": "Infrared mode",
|
||||||
|
"state": {
|
||||||
|
"auto": "Auto",
|
||||||
|
"auto_filter_only": "Auto (filter only, no LEDs)",
|
||||||
|
"custom": "Auto (custom lux)",
|
||||||
|
"off": "Always disable",
|
||||||
|
"on": "Always enable"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"light_mode": {
|
"light_mode": {
|
||||||
"name": "Light mode"
|
"name": "Light mode",
|
||||||
|
"state": {
|
||||||
|
"manual": "Manual",
|
||||||
|
"motion": "On motion - always",
|
||||||
|
"motion_dark": "On motion - when dark",
|
||||||
|
"when_dark": "When dark"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"liveview": {
|
"liveview": {
|
||||||
"name": "Liveview"
|
"name": "Liveview"
|
||||||
},
|
},
|
||||||
"mount_type": {
|
"mount_type": {
|
||||||
"name": "Mount type"
|
"name": "Mount type",
|
||||||
|
"state": {
|
||||||
|
"door": "Door",
|
||||||
|
"garage": "Garage",
|
||||||
|
"leak": "Leak",
|
||||||
|
"none": "[%key:common::state::off%]",
|
||||||
|
"window": "Window"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"paired_camera": {
|
"paired_camera": {
|
||||||
"name": "Paired camera"
|
"name": "Paired camera"
|
||||||
},
|
},
|
||||||
"recording_mode": {
|
"recording_mode": {
|
||||||
"name": "Recording mode"
|
"name": "Recording mode",
|
||||||
|
"state": {
|
||||||
|
"adaptive": "Adaptive",
|
||||||
|
"always": "Always",
|
||||||
|
"detections": "Detections",
|
||||||
|
"never": "Never",
|
||||||
|
"schedule": "Schedule"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ def async_get_light_motion_current(obj: Light) -> str:
|
|||||||
obj.light_mode_settings.mode is LightModeType.MOTION
|
obj.light_mode_settings.mode is LightModeType.MOTION
|
||||||
and obj.light_mode_settings.enable_at is LightModeEnableType.DARK
|
and obj.light_mode_settings.enable_at is LightModeEnableType.DARK
|
||||||
):
|
):
|
||||||
return f"{LightModeType.MOTION.value}Dark"
|
return f"{LightModeType.MOTION.value}_dark"
|
||||||
return obj.light_mode_settings.mode.value
|
return obj.light_mode_settings.mode.value
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -95,4 +95,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
|
||||||
|
|||||||
@@ -25,7 +25,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
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ cryptography==46.0.2
|
|||||||
dbus-fast==3.1.2
|
dbus-fast==3.1.2
|
||||||
file-read-backwards==2.0.0
|
file-read-backwards==2.0.0
|
||||||
fnv-hash-fast==1.6.0
|
fnv-hash-fast==1.6.0
|
||||||
go2rtc-client==0.3.0
|
go2rtc-client==0.4.0
|
||||||
ha-ffmpeg==3.2.2
|
ha-ffmpeg==3.2.2
|
||||||
habluetooth==5.8.0
|
habluetooth==5.8.0
|
||||||
hass-nabucasa==1.7.0
|
hass-nabucasa==1.7.0
|
||||||
|
|||||||
4
requirements_all.txt
generated
4
requirements_all.txt
generated
@@ -1068,7 +1068,7 @@ gitterpy==0.1.7
|
|||||||
glances-api==0.8.0
|
glances-api==0.8.0
|
||||||
|
|
||||||
# homeassistant.components.go2rtc
|
# homeassistant.components.go2rtc
|
||||||
go2rtc-client==0.3.0
|
go2rtc-client==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.goalzero
|
# homeassistant.components.goalzero
|
||||||
goalzero==0.2.2
|
goalzero==0.2.2
|
||||||
@@ -1291,7 +1291,7 @@ influxdb==5.3.1
|
|||||||
inkbird-ble==1.1.1
|
inkbird-ble==1.1.1
|
||||||
|
|
||||||
# homeassistant.components.insteon
|
# homeassistant.components.insteon
|
||||||
insteon-frontend-home-assistant==0.5.0
|
insteon-frontend-home-assistant==0.6.0
|
||||||
|
|
||||||
# homeassistant.components.intellifire
|
# homeassistant.components.intellifire
|
||||||
intellifire4py==4.2.1
|
intellifire4py==4.2.1
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
astroid==4.0.1
|
astroid==4.0.1
|
||||||
coverage==7.10.6
|
coverage==7.10.6
|
||||||
freezegun==1.5.2
|
freezegun==1.5.2
|
||||||
go2rtc-client==0.3.0
|
|
||||||
# librt is an internal mypy dependency
|
# librt is an internal mypy dependency
|
||||||
librt==0.2.1
|
librt==0.2.1
|
||||||
license-expression==30.4.3
|
license-expression==30.4.3
|
||||||
|
|||||||
4
requirements_test_all.txt
generated
4
requirements_test_all.txt
generated
@@ -944,7 +944,7 @@ gios==6.1.2
|
|||||||
glances-api==0.8.0
|
glances-api==0.8.0
|
||||||
|
|
||||||
# homeassistant.components.go2rtc
|
# homeassistant.components.go2rtc
|
||||||
go2rtc-client==0.3.0
|
go2rtc-client==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.goalzero
|
# homeassistant.components.goalzero
|
||||||
goalzero==0.2.2
|
goalzero==0.2.2
|
||||||
@@ -1137,7 +1137,7 @@ influxdb==5.3.1
|
|||||||
inkbird-ble==1.1.1
|
inkbird-ble==1.1.1
|
||||||
|
|
||||||
# homeassistant.components.insteon
|
# homeassistant.components.insteon
|
||||||
insteon-frontend-home-assistant==0.5.0
|
insteon-frontend-home-assistant==0.6.0
|
||||||
|
|
||||||
# homeassistant.components.intellifire
|
# homeassistant.components.intellifire
|
||||||
intellifire4py==4.2.1
|
intellifire4py==4.2.1
|
||||||
|
|||||||
@@ -1008,7 +1008,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
|||||||
"unifi",
|
"unifi",
|
||||||
"unifi_direct",
|
"unifi_direct",
|
||||||
"unifiled",
|
"unifiled",
|
||||||
"unifiprotect",
|
|
||||||
"universal",
|
"universal",
|
||||||
"upb",
|
"upb",
|
||||||
"upc_connect",
|
"upc_connect",
|
||||||
@@ -2032,7 +2031,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
|||||||
"unifi",
|
"unifi",
|
||||||
"unifi_direct",
|
"unifi_direct",
|
||||||
"unifiled",
|
"unifiled",
|
||||||
"unifiprotect",
|
|
||||||
"universal",
|
"universal",
|
||||||
"upb",
|
"upb",
|
||||||
"upc_connect",
|
"upc_connect",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -82,9 +82,20 @@ def mock_hikcamera() -> Generator[MagicMock]:
|
|||||||
None,
|
None,
|
||||||
"2024-01-01T00:00:00Z",
|
"2024-01-01T00:00:00Z",
|
||||||
)
|
)
|
||||||
|
camera.get_event_triggers.return_value = {}
|
||||||
yield hikcamera_mock
|
yield hikcamera_mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_hik_nvr(mock_hikcamera: MagicMock) -> MagicMock:
|
||||||
|
"""Return a mocked HikCamera configured as an NVR."""
|
||||||
|
camera = mock_hikcamera.return_value
|
||||||
|
camera.get_type = "NVR"
|
||||||
|
camera.current_event_states = {}
|
||||||
|
camera.get_event_triggers.return_value = {"Motion": [1, 2]}
|
||||||
|
return mock_hikcamera
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def init_integration(
|
async def init_integration(
|
||||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_hikcamera: MagicMock
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_hikcamera: MagicMock
|
||||||
|
|||||||
@@ -89,3 +89,16 @@ async def test_setup_entry_no_device_id(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_entry_nvr_fetches_events(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_hik_nvr: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup fetches NVR events for NVR devices."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||||
|
mock_hik_nvr.return_value.get_event_triggers.assert_called_once()
|
||||||
|
mock_hik_nvr.return_value.inject_events.assert_called_once()
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
"""Test input boolean triggers."""
|
|
||||||
|
|
||||||
from collections.abc import Generator
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from homeassistant.components.input_boolean 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 (
|
|
||||||
StateDescription,
|
|
||||||
arm_trigger,
|
|
||||||
parametrize_target_entities,
|
|
||||||
parametrize_trigger_states,
|
|
||||||
set_or_remove_state,
|
|
||||||
target_entities,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
|
||||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
|
||||||
"""Stub copying the blueprints to the config folder."""
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="enable_experimental_triggers_conditions")
|
|
||||||
def enable_experimental_triggers_conditions() -> Generator[None]:
|
|
||||||
"""Enable experimental triggers and conditions."""
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.labs.async_is_preview_feature_enabled",
|
|
||||||
return_value=True,
|
|
||||||
):
|
|
||||||
yield
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
async def target_input_booleans(hass: HomeAssistant) -> list[str]:
|
|
||||||
"""Create multiple input_boolean entities associated with different targets."""
|
|
||||||
return (await target_entities(hass, DOMAIN))["included"]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"trigger_key",
|
|
||||||
[
|
|
||||||
"input_boolean.turned_off",
|
|
||||||
"input_boolean.turned_on",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def test_input_boolean_triggers_gated_by_labs_flag(
|
|
||||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
|
|
||||||
) -> None:
|
|
||||||
"""Test the input_boolean 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_experimental_triggers_conditions")
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
|
||||||
parametrize_target_entities(DOMAIN),
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("trigger", "states"),
|
|
||||||
[
|
|
||||||
*parametrize_trigger_states(
|
|
||||||
trigger="input_boolean.turned_off",
|
|
||||||
target_states=[STATE_OFF],
|
|
||||||
other_states=[STATE_ON],
|
|
||||||
),
|
|
||||||
*parametrize_trigger_states(
|
|
||||||
trigger="input_boolean.turned_on",
|
|
||||||
target_states=[STATE_ON],
|
|
||||||
other_states=[STATE_OFF],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def test_input_boolean_state_trigger_behavior_any(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
service_calls: list[ServiceCall],
|
|
||||||
target_input_booleans: list[str],
|
|
||||||
trigger_target_config: dict,
|
|
||||||
entity_id: str,
|
|
||||||
entities_in_target: int,
|
|
||||||
trigger: str,
|
|
||||||
states: list[StateDescription],
|
|
||||||
) -> None:
|
|
||||||
"""Test that the input_boolean state trigger fires when any input_boolean state changes to a specific state."""
|
|
||||||
other_entity_ids = set(target_input_booleans) - {entity_id}
|
|
||||||
|
|
||||||
# Set all input_booleans, including the tested one, to the initial state
|
|
||||||
for eid in target_input_booleans:
|
|
||||||
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 if changing other input_booleans 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_experimental_triggers_conditions")
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
|
||||||
parametrize_target_entities(DOMAIN),
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("trigger", "states"),
|
|
||||||
[
|
|
||||||
*parametrize_trigger_states(
|
|
||||||
trigger="input_boolean.turned_off",
|
|
||||||
target_states=[STATE_OFF],
|
|
||||||
other_states=[STATE_ON],
|
|
||||||
),
|
|
||||||
*parametrize_trigger_states(
|
|
||||||
trigger="input_boolean.turned_on",
|
|
||||||
target_states=[STATE_ON],
|
|
||||||
other_states=[STATE_OFF],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def test_input_boolean_state_trigger_behavior_first(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
service_calls: list[ServiceCall],
|
|
||||||
target_input_booleans: list[str],
|
|
||||||
trigger_target_config: dict,
|
|
||||||
entity_id: str,
|
|
||||||
entities_in_target: int,
|
|
||||||
trigger: str,
|
|
||||||
states: list[StateDescription],
|
|
||||||
) -> None:
|
|
||||||
"""Test that the input_boolean state trigger fires when the first input_boolean changes to a specific state."""
|
|
||||||
other_entity_ids = set(target_input_booleans) - {entity_id}
|
|
||||||
|
|
||||||
# Set all input_booleans, including the tested one, to the initial state
|
|
||||||
for eid in target_input_booleans:
|
|
||||||
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 input_booleans 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_experimental_triggers_conditions")
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
|
||||||
parametrize_target_entities(DOMAIN),
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("trigger", "states"),
|
|
||||||
[
|
|
||||||
*parametrize_trigger_states(
|
|
||||||
trigger="input_boolean.turned_off",
|
|
||||||
target_states=[STATE_OFF],
|
|
||||||
other_states=[STATE_ON],
|
|
||||||
),
|
|
||||||
*parametrize_trigger_states(
|
|
||||||
trigger="input_boolean.turned_on",
|
|
||||||
target_states=[STATE_ON],
|
|
||||||
other_states=[STATE_OFF],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def test_input_boolean_state_trigger_behavior_last(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
service_calls: list[ServiceCall],
|
|
||||||
target_input_booleans: list[str],
|
|
||||||
trigger_target_config: dict,
|
|
||||||
entity_id: str,
|
|
||||||
entities_in_target: int,
|
|
||||||
trigger: str,
|
|
||||||
states: list[StateDescription],
|
|
||||||
) -> None:
|
|
||||||
"""Test that the input_boolean state trigger fires when the last input_boolean changes to a specific state."""
|
|
||||||
other_entity_ids = set(target_input_booleans) - {entity_id}
|
|
||||||
|
|
||||||
# Set all input_booleans, including the tested one, to the initial state
|
|
||||||
for eid in target_input_booleans:
|
|
||||||
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()
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ async def test_select_setup_light(
|
|||||||
await init_entry(hass, ufp, [light])
|
await init_entry(hass, ufp, [light])
|
||||||
assert_entity_counts(hass, Platform.SELECT, 2, 2)
|
assert_entity_counts(hass, Platform.SELECT, 2, 2)
|
||||||
|
|
||||||
expected_values = ("On Motion - When Dark", "Not Paired")
|
expected_values = ("motion_dark", "Not Paired")
|
||||||
|
|
||||||
for index, description in enumerate(LIGHT_SELECTS):
|
for index, description in enumerate(LIGHT_SELECTS):
|
||||||
unique_id, entity_id = await ids_from_device_description(
|
unique_id, entity_id = await ids_from_device_description(
|
||||||
@@ -153,11 +153,11 @@ async def test_select_setup_camera_all(
|
|||||||
assert_entity_counts(hass, Platform.SELECT, 5, 5)
|
assert_entity_counts(hass, Platform.SELECT, 5, 5)
|
||||||
|
|
||||||
expected_values = (
|
expected_values = (
|
||||||
"Always",
|
"always",
|
||||||
"Auto",
|
"auto",
|
||||||
"Default Message (Welcome)",
|
"Default Message (Welcome)",
|
||||||
"None",
|
"none",
|
||||||
"Always Off",
|
"off",
|
||||||
)
|
)
|
||||||
|
|
||||||
for index, description in enumerate(CAMERA_SELECTS):
|
for index, description in enumerate(CAMERA_SELECTS):
|
||||||
@@ -186,7 +186,7 @@ async def test_select_setup_camera_none(
|
|||||||
await init_entry(hass, ufp, [camera])
|
await init_entry(hass, ufp, [camera])
|
||||||
assert_entity_counts(hass, Platform.SELECT, 2, 2)
|
assert_entity_counts(hass, Platform.SELECT, 2, 2)
|
||||||
|
|
||||||
expected_values = ("Always", "Auto", "Default Message (Welcome)")
|
expected_values = ("always", "auto", "Default Message (Welcome)")
|
||||||
|
|
||||||
for index, description in enumerate(CAMERA_SELECTS):
|
for index, description in enumerate(CAMERA_SELECTS):
|
||||||
if index == 2:
|
if index == 2:
|
||||||
@@ -403,7 +403,7 @@ async def test_select_set_option_camera_recording(
|
|||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"select",
|
"select",
|
||||||
"select_option",
|
"select_option",
|
||||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Never"},
|
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "never"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -428,7 +428,7 @@ async def test_select_set_option_camera_ir(
|
|||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"select",
|
"select",
|
||||||
"select_option",
|
"select_option",
|
||||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Always Enable"},
|
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "on"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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