Compare commits

..

4 Commits

Author SHA1 Message Date
Michael
f9ec003124 Merge branch 'dev' into input_boolean/add-domain-driven-triggers 2025-12-20 10:00:21 +01:00
mib1185
ee0230f3b1 use renamed helpers 2025-12-17 20:06:03 +00:00
mib1185
851fd467fe Merge branch 'dev' into input_boolean/add-domain-driven-triggers 2025-12-17 20:05:20 +00:00
mib1185
d10148a175 add turned_off and turned_on triggers 2025-12-12 20:53:03 +00:00
46 changed files with 398 additions and 428 deletions

2
CODEOWNERS generated
View File

@@ -794,8 +794,6 @@ 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

View File

@@ -132,6 +132,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"device_tracker", "device_tracker",
"fan", "fan",
"humidifier", "humidifier",
"input_boolean",
"lawn_mower", "lawn_mower",
"light", "light",
"lock", "lock",

View File

@@ -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, get_remote_keys, get_remotes from .util import get_device_buttons
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
@@ -53,23 +53,4 @@ 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

View File

@@ -16,7 +16,11 @@ 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,
@@ -25,7 +29,7 @@ from .const import (
WebsocketNotification, WebsocketNotification,
) )
from .entity import BeoEntity from .entity import BeoEntity
from .util import get_device_buttons, get_remote_keys, get_remotes from .util import get_device_buttons, get_remotes
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@@ -36,19 +40,38 @@ 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(config_entry, remote, key_type) BeoRemoteKeyEvent(
for key_type in get_remote_keys() config_entry,
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)
] ]
) )

View File

@@ -11,16 +11,7 @@ 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 ( from .const import DEVICE_BUTTONS, DOMAIN, BeoButtons, BeoModel
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:
@@ -73,14 +64,3 @@ 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)
],
]

View File

@@ -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,
) )

View File

@@ -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.4.0"], "requirements": ["go2rtc-client==0.3.0"],
"single_config_entry": true "single_config_entry": true
} }

View File

@@ -70,16 +70,6 @@ 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)

View File

@@ -20,5 +20,13 @@
"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"
}
} }
} }

View File

@@ -1,4 +1,8 @@
{ {
"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%]",
@@ -17,6 +21,15 @@
} }
} }
}, },
"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.",
@@ -35,5 +48,27 @@
"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"
}
}
} }

View File

@@ -0,0 +1,17 @@
"""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

View File

@@ -0,0 +1,18 @@
.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

View File

@@ -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,
) )

View File

@@ -282,8 +282,6 @@ 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:

View File

@@ -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.6.0" "insteon-frontend-home-assistant==0.5.0"
], ],
"single_config_entry": true, "single_config_entry": true,
"usb": [ "usb": [

View File

@@ -1,7 +1,7 @@
{ {
"domain": "intent_script", "domain": "intent_script",
"name": "Intent Script", "name": "Intent Script",
"codeowners": ["@arturpragacz"], "codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/intent_script", "documentation": "https://www.home-assistant.io/integrations/intent_script",
"quality_scale": "internal" "quality_scale": "internal"
} }

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -19,8 +19,6 @@ 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(

View File

@@ -26,8 +26,6 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
NUMBER_DESCRIPTIONS: tuple[NumberEntityDescription, ...] = ( NUMBER_DESCRIPTIONS: tuple[NumberEntityDescription, ...] = (
NumberEntityDescription( NumberEntityDescription(

View File

@@ -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: done parallel-updates: todo
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.

View File

@@ -20,8 +20,6 @@ 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):

View File

@@ -27,8 +27,6 @@ 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):

View File

@@ -18,8 +18,6 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
SWITCH_DESCRIPTIONS: tuple[SwitchEntityDescription, ...] = ( SWITCH_DESCRIPTIONS: tuple[SwitchEntityDescription, ...] = (
SwitchEntityDescription( SwitchEntityDescription(

View File

@@ -3,10 +3,9 @@
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, Device from PySrDaliGateway import DaliGateway
from PySrDaliGateway.exceptions import DaliGatewayError from PySrDaliGateway.exceptions import DaliGatewayError
from homeassistant.const import ( from homeassistant.const import (
@@ -29,38 +28,6 @@ _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."""
@@ -103,7 +70,6 @@ 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,

View File

@@ -40,7 +40,6 @@
"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": [
{ {

View File

@@ -1,68 +0,0 @@
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

View File

@@ -48,37 +48,37 @@ _KEY_LIGHT_MOTION = "light_motion"
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
HDR_MODES = [ HDR_MODES = [
{"id": "always", "name": "always"}, {"id": "always", "name": "Always On"},
{"id": "off", "name": "off"}, {"id": "off", "name": "Always 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": "on"}, {"id": IRLEDMode.ON.value, "name": "Always Enable"},
{"id": IRLEDMode.AUTO_NO_LED.value, "name": "auto_filter_only"}, {"id": IRLEDMode.AUTO_NO_LED.value, "name": "Auto (Filter Only, no LED's)"},
{"id": IRLEDMode.CUSTOM.value, "name": "custom"}, {"id": IRLEDMode.CUSTOM.value, "name": "Auto (Custom Lux)"},
{"id": IRLEDMode.OFF.value, "name": "off"}, {"id": IRLEDMode.OFF.value, "name": "Always Disable"},
] ]
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": MountType.NONE.value}, {"id": MountType.NONE.value, "name": "None"},
{"id": MountType.DOOR.value, "name": MountType.DOOR.value}, {"id": MountType.DOOR.value, "name": "Door"},
{"id": MountType.WINDOW.value, "name": MountType.WINDOW.value}, {"id": MountType.WINDOW.value, "name": "Window"},
{"id": MountType.GARAGE.value, "name": MountType.GARAGE.value}, {"id": MountType.GARAGE.value, "name": "Garage"},
{"id": MountType.LEAK.value, "name": MountType.LEAK.value}, {"id": MountType.LEAK.value, "name": "Leak"},
] ]
LIGHT_MODE_MOTION = "motion" LIGHT_MODE_MOTION = "On Motion - Always"
LIGHT_MODE_MOTION_DARK = "motion_dark" LIGHT_MODE_MOTION_DARK = "On Motion - When 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} for mode in list(RecordingMode) {"id": mode.value, "name": mode.value.title()} for mode in list(RecordingMode)
] ]

View File

@@ -350,68 +350,31 @@
}, },
"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": {

View File

@@ -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

View File

@@ -95,9 +95,4 @@ 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."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return 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

View File

@@ -25,7 +25,7 @@ rules:
# Silver # Silver
action-exceptions: todo action-exceptions: todo
config-entry-unloading: done config-entry-unloading: todo
docs-configuration-parameters: todo docs-configuration-parameters: todo
docs-installation-parameters: todo docs-installation-parameters: todo
entity-unavailable: todo entity-unavailable: todo

View File

@@ -3956,13 +3956,13 @@
}, },
"meteoclimatic": { "meteoclimatic": {
"name": "Meteoclimatic", "name": "Meteoclimatic",
"integration_type": "service", "integration_type": "hub",
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
}, },
"metoffice": { "metoffice": {
"name": "Met Office", "name": "Met Office",
"integration_type": "service", "integration_type": "hub",
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
}, },

View File

@@ -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.4.0 go2rtc-client==0.3.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
View File

@@ -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.4.0 go2rtc-client==0.3.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.6.0 insteon-frontend-home-assistant==0.5.0
# homeassistant.components.intellifire # homeassistant.components.intellifire
intellifire4py==4.2.1 intellifire4py==4.2.1

View File

@@ -10,6 +10,7 @@
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

View File

@@ -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.4.0 go2rtc-client==0.3.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.6.0 insteon-frontend-home-assistant==0.5.0
# homeassistant.components.intellifire # homeassistant.components.intellifire
intellifire4py==4.2.1 intellifire4py==4.2.1

View File

@@ -1008,6 +1008,7 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"unifi", "unifi",
"unifi_direct", "unifi_direct",
"unifiled", "unifiled",
"unifiprotect",
"universal", "universal",
"upb", "upb",
"upc_connect", "upc_connect",
@@ -2031,6 +2032,7 @@ INTEGRATIONS_WITHOUT_SCALE = [
"unifi", "unifi",
"unifi_direct", "unifi_direct",
"unifiled", "unifiled",
"unifiprotect",
"universal", "universal",
"upb", "upb",
"upc_connect", "upc_connect",

View File

@@ -82,30 +82,6 @@
'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,
}) })
# --- # ---

View File

@@ -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, TEST_REMOTE_KEY_EVENT_ENTITY_ID from .const import TEST_BUTTON_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,11 +25,8 @@ async def test_async_get_config_entry_diagnostics(
) -> None: ) -> None:
"""Test config entry diagnostics.""" """Test config entry diagnostics."""
# Enable a button and remote key Event entity # Enable an 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

View File

@@ -82,20 +82,9 @@ 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

View File

@@ -89,16 +89,3 @@ 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()

View File

@@ -0,0 +1,228 @@
"""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()

View File

@@ -5,10 +5,9 @@ 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
from homeassistant.helpers import device_registry as dr import homeassistant.helpers.device_registry as dr
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@@ -64,23 +63,6 @@ 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,
@@ -98,40 +80,3 @@ 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

View File

@@ -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 = ("motion_dark", "Not Paired") expected_values = ("On Motion - When 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",
"off", "Always 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: "on"}, {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Always Enable"},
blocking=True, blocking=True,
) )

View File

@@ -2,22 +2,16 @@
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, MockConfigEntry from tests.common import AsyncMock, ConfigEntry
async def test_setup_retry_on_nodes_failure( async def test_setup_retry_on_nodes_failure(
@@ -59,44 +53,3 @@ 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()