mirror of
https://github.com/home-assistant/core.git
synced 2025-12-18 22:08:14 +00:00
Compare commits
16 Commits
matter_tes
...
input_bool
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee0230f3b1 | ||
|
|
851fd467fe | ||
|
|
c418d9750b | ||
|
|
e96d614076 | ||
|
|
f0a5e0a023 | ||
|
|
6ac6b86060 | ||
|
|
3909171b1a | ||
|
|
769029505f | ||
|
|
080ec3524b | ||
|
|
48d671ad5f | ||
|
|
7115db5d22 | ||
|
|
d0c8792e4b | ||
|
|
84d7c37502 | ||
|
|
8a10638470 | ||
|
|
10dd53ffc2 | ||
|
|
d10148a175 |
@@ -624,13 +624,16 @@ async def async_enable_logging(
|
||||
|
||||
if log_file is None:
|
||||
default_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
if "SUPERVISOR" in os.environ:
|
||||
_LOGGER.info("Running in Supervisor, not logging to file")
|
||||
if "SUPERVISOR" in os.environ and "HA_DUPLICATE_LOG_FILE" not in os.environ:
|
||||
# Rename the default log file if it exists, since previous versions created
|
||||
# it even on Supervisor
|
||||
if os.path.isfile(default_log_path):
|
||||
with contextlib.suppress(OSError):
|
||||
os.rename(default_log_path, f"{default_log_path}.old")
|
||||
def rename_old_file() -> None:
|
||||
"""Rename old log file in executor."""
|
||||
if os.path.isfile(default_log_path):
|
||||
with contextlib.suppress(OSError):
|
||||
os.rename(default_log_path, f"{default_log_path}.old")
|
||||
|
||||
await hass.async_add_executor_job(rename_old_file)
|
||||
err_log_path = None
|
||||
else:
|
||||
err_log_path = default_log_path
|
||||
|
||||
@@ -4,10 +4,10 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity import get_supported_features
|
||||
from homeassistant.helpers.trigger import (
|
||||
EntityStateTriggerBase,
|
||||
EntityTargetStateTriggerBase,
|
||||
Trigger,
|
||||
make_conditional_entity_state_trigger,
|
||||
make_entity_state_trigger,
|
||||
make_entity_target_state_trigger,
|
||||
make_entity_transition_trigger,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState
|
||||
@@ -21,7 +21,7 @@ def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool
|
||||
return False
|
||||
|
||||
|
||||
class EntityStateTriggerRequiredFeatures(EntityStateTriggerBase):
|
||||
class EntityStateTriggerRequiredFeatures(EntityTargetStateTriggerBase):
|
||||
"""Trigger for entity state changes."""
|
||||
|
||||
_required_features: int
|
||||
@@ -38,7 +38,7 @@ class EntityStateTriggerRequiredFeatures(EntityStateTriggerBase):
|
||||
|
||||
def make_entity_state_trigger_required_features(
|
||||
domain: str, to_state: str, required_features: int
|
||||
) -> type[EntityStateTriggerBase]:
|
||||
) -> type[EntityTargetStateTriggerBase]:
|
||||
"""Create an entity state trigger class."""
|
||||
|
||||
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
||||
@@ -52,7 +52,7 @@ def make_entity_state_trigger_required_features(
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"armed": make_conditional_entity_state_trigger(
|
||||
"armed": make_entity_transition_trigger(
|
||||
DOMAIN,
|
||||
from_states={
|
||||
AlarmControlPanelState.ARMING,
|
||||
@@ -89,8 +89,12 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
AlarmControlPanelState.ARMED_VACATION,
|
||||
AlarmControlPanelEntityFeature.ARM_VACATION,
|
||||
),
|
||||
"disarmed": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.DISARMED),
|
||||
"triggered": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.TRIGGERED),
|
||||
"disarmed": make_entity_target_state_trigger(
|
||||
DOMAIN, AlarmControlPanelState.DISARMED
|
||||
),
|
||||
"triggered": make_entity_target_state_trigger(
|
||||
DOMAIN, AlarmControlPanelState.TRIGGERED
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
"""Provides triggers for assist satellites."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import AssistSatelliteState
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"idle": make_entity_state_trigger(DOMAIN, AssistSatelliteState.IDLE),
|
||||
"listening": make_entity_state_trigger(DOMAIN, AssistSatelliteState.LISTENING),
|
||||
"processing": make_entity_state_trigger(DOMAIN, AssistSatelliteState.PROCESSING),
|
||||
"responding": make_entity_state_trigger(DOMAIN, AssistSatelliteState.RESPONDING),
|
||||
"idle": make_entity_target_state_trigger(DOMAIN, AssistSatelliteState.IDLE),
|
||||
"listening": make_entity_target_state_trigger(
|
||||
DOMAIN, AssistSatelliteState.LISTENING
|
||||
),
|
||||
"processing": make_entity_target_state_trigger(
|
||||
DOMAIN, AssistSatelliteState.PROCESSING
|
||||
),
|
||||
"responding": make_entity_target_state_trigger(
|
||||
DOMAIN, AssistSatelliteState.RESPONDING
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/autarco",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["autarco==3.2.0"]
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"fan",
|
||||
"input_boolean",
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"media_player",
|
||||
|
||||
@@ -4,7 +4,7 @@ from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity import get_device_class
|
||||
from homeassistant.helpers.trigger import EntityStateTriggerBase, Trigger
|
||||
from homeassistant.helpers.trigger import EntityTargetStateTriggerBase, Trigger
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
|
||||
from . import DOMAIN, BinarySensorDeviceClass
|
||||
@@ -20,7 +20,7 @@ def get_device_class_or_undefined(
|
||||
return UNDEFINED
|
||||
|
||||
|
||||
class BinarySensorOnOffTrigger(EntityStateTriggerBase):
|
||||
class BinarySensorOnOffTrigger(EntityTargetStateTriggerBase):
|
||||
"""Class for binary sensor on/off triggers."""
|
||||
|
||||
_device_class: BinarySensorDeviceClass | None
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
make_conditional_entity_state_trigger,
|
||||
make_entity_state_attribute_trigger,
|
||||
make_entity_state_trigger,
|
||||
make_entity_target_state_attribute_trigger,
|
||||
make_entity_target_state_trigger,
|
||||
make_entity_transition_trigger,
|
||||
)
|
||||
|
||||
from .const import ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"started_cooling": make_entity_state_attribute_trigger(
|
||||
"started_cooling": make_entity_target_state_attribute_trigger(
|
||||
DOMAIN, ATTR_HVAC_ACTION, HVACAction.COOLING
|
||||
),
|
||||
"started_drying": make_entity_state_attribute_trigger(
|
||||
"started_drying": make_entity_target_state_attribute_trigger(
|
||||
DOMAIN, ATTR_HVAC_ACTION, HVACAction.DRYING
|
||||
),
|
||||
"turned_off": make_entity_state_trigger(DOMAIN, HVACMode.OFF),
|
||||
"turned_on": make_conditional_entity_state_trigger(
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, HVACMode.OFF),
|
||||
"turned_on": make_entity_transition_trigger(
|
||||
DOMAIN,
|
||||
from_states={
|
||||
HVACMode.OFF,
|
||||
@@ -32,7 +32,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
HVACMode.HEAT_COOL,
|
||||
},
|
||||
),
|
||||
"started_heating": make_entity_state_attribute_trigger(
|
||||
"started_heating": make_entity_target_state_attribute_trigger(
|
||||
DOMAIN, ATTR_HVAC_ACTION, HVACAction.HEATING
|
||||
),
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@ from homeassistant.const import STATE_HOME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
make_entity_from_state_trigger,
|
||||
make_entity_state_trigger,
|
||||
make_entity_origin_state_trigger,
|
||||
make_entity_target_state_trigger,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"entered_home": make_entity_state_trigger(DOMAIN, STATE_HOME),
|
||||
"left_home": make_entity_from_state_trigger(DOMAIN, from_state=STATE_HOME),
|
||||
"entered_home": make_entity_target_state_trigger(DOMAIN, STATE_HOME),
|
||||
"left_home": make_entity_origin_state_trigger(DOMAIN, from_state=STATE_HOME),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turned_off": make_entity_state_trigger(DOMAIN, STATE_OFF),
|
||||
"turned_on": make_entity_state_trigger(DOMAIN, STATE_ON),
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
|
||||
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ class IcloudAccount:
|
||||
|
||||
if self.api.requires_2fa:
|
||||
# Trigger a new log in to ensure the user enters the 2FA code again.
|
||||
raise PyiCloudFailedLoginException # noqa: TRY301
|
||||
raise PyiCloudFailedLoginException("2FA Required") # noqa: TRY301
|
||||
|
||||
except PyiCloudFailedLoginException:
|
||||
self.api = None
|
||||
|
||||
@@ -261,7 +261,8 @@ class ImprovBLEConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if self._can_identify is None:
|
||||
try:
|
||||
self._can_identify = await self._try_call(device.can_identify())
|
||||
await self._try_call(device.ensure_connected())
|
||||
self._can_identify = device.can_identify
|
||||
except AbortFlow as err:
|
||||
return self.async_abort(reason=err.reason)
|
||||
if self._can_identify:
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/improv_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["py-improv-ble-client==1.0.3"]
|
||||
"requirements": ["py-improv-ble-client==2.0.1"]
|
||||
}
|
||||
|
||||
@@ -20,5 +20,13 @@
|
||||
"turn_on": {
|
||||
"service": "mdi:toggle-switch"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"turned_off": {
|
||||
"trigger": "mdi:toggle-switch-off"
|
||||
},
|
||||
"turned_on": {
|
||||
"trigger": "mdi:toggle-switch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted input booleans to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "[%key:component::input_boolean::title%]",
|
||||
@@ -17,6 +21,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reload": {
|
||||
"description": "Reloads helpers from the YAML-configuration.",
|
||||
@@ -35,5 +48,27 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
homeassistant/components/input_boolean/trigger.py
Normal file
17
homeassistant/components/input_boolean/trigger.py
Normal 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
|
||||
18
homeassistant/components/input_boolean/triggers.yaml
Normal file
18
homeassistant/components/input_boolean/triggers.yaml
Normal 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
|
||||
@@ -1,15 +1,17 @@
|
||||
"""Provides triggers for lawn mowers."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
|
||||
from .const import DOMAIN, LawnMowerActivity
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"docked": make_entity_state_trigger(DOMAIN, LawnMowerActivity.DOCKED),
|
||||
"errored": make_entity_state_trigger(DOMAIN, LawnMowerActivity.ERROR),
|
||||
"paused_mowing": make_entity_state_trigger(DOMAIN, LawnMowerActivity.PAUSED),
|
||||
"started_mowing": make_entity_state_trigger(DOMAIN, LawnMowerActivity.MOWING),
|
||||
"docked": make_entity_target_state_trigger(DOMAIN, LawnMowerActivity.DOCKED),
|
||||
"errored": make_entity_target_state_trigger(DOMAIN, LawnMowerActivity.ERROR),
|
||||
"paused_mowing": make_entity_target_state_trigger(DOMAIN, LawnMowerActivity.PAUSED),
|
||||
"started_mowing": make_entity_target_state_trigger(
|
||||
DOMAIN, LawnMowerActivity.MOWING
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turned_off": make_entity_state_trigger(DOMAIN, STATE_OFF),
|
||||
"turned_on": make_entity_state_trigger(DOMAIN, STATE_ON),
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
|
||||
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Provides triggers for media players."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_conditional_entity_state_trigger
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_transition_trigger
|
||||
|
||||
from . import MediaPlayerState
|
||||
from .const import DOMAIN
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"stopped_playing": make_conditional_entity_state_trigger(
|
||||
"stopped_playing": make_entity_transition_trigger(
|
||||
DOMAIN,
|
||||
from_states={
|
||||
MediaPlayerState.BUFFERING,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"domain": "meteo_france",
|
||||
"name": "M\u00e9t\u00e9o-France",
|
||||
"name": "Météo-France",
|
||||
"codeowners": ["@hacf-fr", "@oncleben31", "@Quentame"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/meteo_france",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["meteofrance_api"],
|
||||
"requirements": ["meteofrance-api==1.4.0"]
|
||||
|
||||
@@ -62,7 +62,7 @@ SENSOR_TYPES: tuple[MeteoFranceSensorEntityDescription, ...] = (
|
||||
key="pressure",
|
||||
name="Pressure",
|
||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
data_path="current_forecast:sea_level",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mill",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["mill", "mill_local"],
|
||||
"requirements": ["millheater==0.14.1", "mill-local==0.3.0"]
|
||||
"requirements": ["millheater==0.14.1", "mill-local==0.5.0"]
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ _PLATFORMS: list[Platform] = [
|
||||
Platform.TIME,
|
||||
Platform.SWITCH,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
]
|
||||
|
||||
PLATFORM_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
@@ -5,14 +5,18 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pynintendoauth.exceptions import InvalidOAuthConfigurationException
|
||||
from pynintendoauth.exceptions import (
|
||||
HttpException,
|
||||
InvalidOAuthConfigurationException,
|
||||
InvalidSessionTokenException,
|
||||
)
|
||||
from pynintendoparental import Authenticator, NintendoParental
|
||||
from pynintendoparental.exceptions import NoDevicesFoundException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -58,3 +62,13 @@ class NintendoUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_devices_found",
|
||||
) from err
|
||||
except InvalidSessionTokenException as err:
|
||||
_LOGGER.debug("Session token invalid, will renew on next update")
|
||||
raise UpdateFailed from err
|
||||
except HttpException as err:
|
||||
if err.error_code == "update_required":
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_required",
|
||||
) from err
|
||||
raise UpdateFailed(retry_after=900) from err
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Nintendo Switch Parental Controls select entity definitions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from pynintendoparental.enum import DeviceTimerMode
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import NintendoParentalControlsConfigEntry, NintendoUpdateCoordinator
|
||||
from .entity import Device, NintendoDevice
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
class NintendoParentalSelect(StrEnum):
|
||||
"""Store keys for Nintendo Parental Controls select entities."""
|
||||
|
||||
TIMER_MODE = "timer_mode"
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class NintendoParentalControlsSelectEntityDescription(SelectEntityDescription):
|
||||
"""Description for Nintendo Parental Controls select entities."""
|
||||
|
||||
get_option: Callable[[Device], DeviceTimerMode | None]
|
||||
set_option_fn: Callable[[Device, DeviceTimerMode], Coroutine[Any, Any, None]]
|
||||
options_enum: type[DeviceTimerMode]
|
||||
|
||||
|
||||
SELECT_DESCRIPTIONS: tuple[NintendoParentalControlsSelectEntityDescription, ...] = (
|
||||
NintendoParentalControlsSelectEntityDescription(
|
||||
key=NintendoParentalSelect.TIMER_MODE,
|
||||
translation_key=NintendoParentalSelect.TIMER_MODE,
|
||||
get_option=lambda device: device.timer_mode,
|
||||
set_option_fn=lambda device, option: device.set_timer_mode(option),
|
||||
options_enum=DeviceTimerMode,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: NintendoParentalControlsConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the select platform."""
|
||||
async_add_devices(
|
||||
NintendoParentalSelectEntity(
|
||||
coordinator=entry.runtime_data,
|
||||
device=device,
|
||||
description=description,
|
||||
)
|
||||
for device in entry.runtime_data.api.devices.values()
|
||||
for description in SELECT_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class NintendoParentalSelectEntity(NintendoDevice, SelectEntity):
|
||||
"""Nintendo Parental Controls select entity."""
|
||||
|
||||
entity_description: NintendoParentalControlsSelectEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NintendoUpdateCoordinator,
|
||||
device: Device,
|
||||
description: NintendoParentalControlsSelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the select entity."""
|
||||
super().__init__(coordinator=coordinator, device=device, key=description.key)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the current selected option."""
|
||||
option = self.entity_description.get_option(self._device)
|
||||
return option.name.lower() if option else None
|
||||
|
||||
@property
|
||||
def options(self) -> list[str]:
|
||||
"""Return a list of available options."""
|
||||
return [option.name.lower() for option in self.entity_description.options_enum]
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
enum_option = self.entity_description.options_enum[option.upper()]
|
||||
await self.entity_description.set_option_fn(self._device, enum_option)
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -37,6 +37,15 @@
|
||||
"name": "Max screentime today"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"timer_mode": {
|
||||
"name": "Restriction mode",
|
||||
"state": {
|
||||
"daily": "Same for all days",
|
||||
"each_day_of_the_week": "Different for each day"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"playing_time": {
|
||||
"name": "Used screen time"
|
||||
@@ -74,6 +83,9 @@
|
||||
},
|
||||
"no_devices_found": {
|
||||
"message": "No Nintendo devices found for this account."
|
||||
},
|
||||
"update_required": {
|
||||
"message": "The Nintendo Switch parental controls integration requires an update due to changes in Nintendo's API."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==3.12.2",
|
||||
"python-roborock==3.18.0",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turned_on": make_entity_state_trigger(DOMAIN, STATE_ON),
|
||||
"turned_off": make_entity_state_trigger(DOMAIN, STATE_OFF),
|
||||
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ from .const import (
|
||||
ATTR_PASSWORD,
|
||||
ATTR_QUESTION,
|
||||
ATTR_REACTION,
|
||||
ATTR_REPLY_TO_MSGID,
|
||||
ATTR_RESIZE_KEYBOARD,
|
||||
ATTR_SHOW_ALERT,
|
||||
ATTR_STICKER_ID,
|
||||
@@ -126,21 +127,26 @@ BASE_SERVICE_SCHEMA = vol.Schema(
|
||||
vol.Optional(ATTR_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(ATTR_MESSAGE_TAG): cv.string,
|
||||
vol.Optional(ATTR_MESSAGE_THREAD_ID): vol.Coerce(int),
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_SEND_MESSAGE = vol.All(
|
||||
cv.deprecated(ATTR_TIMEOUT),
|
||||
BASE_SERVICE_SCHEMA.extend(
|
||||
{vol.Required(ATTR_MESSAGE): cv.string, vol.Optional(ATTR_TITLE): cv.string}
|
||||
{
|
||||
vol.Required(ATTR_MESSAGE): cv.string,
|
||||
vol.Optional(ATTR_TITLE): cv.string,
|
||||
vol.Optional(ATTR_REPLY_TO_MSGID): vol.Coerce(int),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_SEND_CHAT_ACTION = vol.All(
|
||||
cv.deprecated(ATTR_TIMEOUT),
|
||||
BASE_SERVICE_SCHEMA.extend(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CONFIG_ENTRY_ID): cv.string,
|
||||
vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
vol.Required(ATTR_CHAT_ACTION): vol.In(
|
||||
(
|
||||
CHAT_ACTION_TYPING,
|
||||
@@ -156,6 +162,7 @@ SERVICE_SCHEMA_SEND_CHAT_ACTION = vol.All(
|
||||
CHAT_ACTION_UPLOAD_VIDEO_NOTE,
|
||||
)
|
||||
),
|
||||
vol.Optional(ATTR_MESSAGE_THREAD_ID): vol.Coerce(int),
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -169,6 +176,7 @@ SERVICE_SCHEMA_BASE_SEND_FILE = BASE_SERVICE_SCHEMA.extend(
|
||||
vol.Optional(ATTR_PASSWORD): cv.string,
|
||||
vol.Optional(ATTR_AUTHENTICATION): cv.string,
|
||||
vol.Optional(ATTR_VERIFY_SSL): cv.boolean,
|
||||
vol.Optional(ATTR_REPLY_TO_MSGID): vol.Coerce(int),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -188,6 +196,7 @@ SERVICE_SCHEMA_SEND_LOCATION = vol.All(
|
||||
{
|
||||
vol.Required(ATTR_LONGITUDE): cv.string,
|
||||
vol.Required(ATTR_LATITUDE): cv.string,
|
||||
vol.Optional(ATTR_REPLY_TO_MSGID): vol.Coerce(int),
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -205,18 +214,25 @@ SERVICE_SCHEMA_SEND_POLL = vol.All(
|
||||
vol.Optional(ATTR_ALLOWS_MULTIPLE_ANSWERS, default=False): cv.boolean,
|
||||
vol.Optional(ATTR_DISABLE_NOTIF): cv.boolean,
|
||||
vol.Optional(ATTR_MESSAGE_THREAD_ID): vol.Coerce(int),
|
||||
vol.Optional(ATTR_REPLY_TO_MSGID): vol.Coerce(int),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_EDIT_MESSAGE = vol.All(
|
||||
cv.deprecated(ATTR_TIMEOUT),
|
||||
SERVICE_SCHEMA_BASE_SEND_FILE.extend(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CONFIG_ENTRY_ID): cv.string,
|
||||
vol.Optional(ATTR_TITLE): cv.string,
|
||||
vol.Required(ATTR_MESSAGE): cv.string,
|
||||
vol.Required(ATTR_MESSAGEID): vol.Any(
|
||||
cv.positive_int, vol.All(cv.string, "last")
|
||||
),
|
||||
vol.Required(ATTR_CHAT_ID): vol.Coerce(int),
|
||||
vol.Optional(ATTR_PARSER): cv.string,
|
||||
vol.Optional(ATTR_KEYBOARD_INLINE): cv.ensure_list,
|
||||
vol.Optional(ATTR_DISABLE_WEB_PREV): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -783,6 +783,7 @@ class TelegramNotificationService:
|
||||
None,
|
||||
chat_id=chat_id,
|
||||
action=chat_action,
|
||||
message_thread_id=kwargs.get(ATTR_MESSAGE_THREAD_ID),
|
||||
context=context,
|
||||
)
|
||||
result[chat_id] = is_successful
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
from homeassistant.const import STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"update_became_available": make_entity_state_trigger(DOMAIN, STATE_ON),
|
||||
"update_became_available": make_entity_target_state_trigger(DOMAIN, STATE_ON),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
"""Provides triggers for vacuum cleaners."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
|
||||
from .const import DOMAIN, VacuumActivity
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"docked": make_entity_state_trigger(DOMAIN, VacuumActivity.DOCKED),
|
||||
"errored": make_entity_state_trigger(DOMAIN, VacuumActivity.ERROR),
|
||||
"paused_cleaning": make_entity_state_trigger(DOMAIN, VacuumActivity.PAUSED),
|
||||
"started_cleaning": make_entity_state_trigger(DOMAIN, VacuumActivity.CLEANING),
|
||||
"docked": make_entity_target_state_trigger(DOMAIN, VacuumActivity.DOCKED),
|
||||
"errored": make_entity_target_state_trigger(DOMAIN, VacuumActivity.ERROR),
|
||||
"paused_cleaning": make_entity_target_state_trigger(DOMAIN, VacuumActivity.PAUSED),
|
||||
"started_cleaning": make_entity_target_state_trigger(
|
||||
DOMAIN, VacuumActivity.CLEANING
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3938,7 +3938,7 @@
|
||||
},
|
||||
"meteo_france": {
|
||||
"name": "M\u00e9t\u00e9o-France",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
|
||||
@@ -430,8 +430,8 @@ class EntityTriggerBase(Trigger):
|
||||
)
|
||||
|
||||
|
||||
class EntityStateTriggerBase(EntityTriggerBase):
|
||||
"""Trigger for entity state changes."""
|
||||
class EntityTargetStateTriggerBase(EntityTriggerBase):
|
||||
"""Trigger for entity state changes to a specific state."""
|
||||
|
||||
_to_state: str
|
||||
|
||||
@@ -440,8 +440,8 @@ class EntityStateTriggerBase(EntityTriggerBase):
|
||||
return state.state == self._to_state
|
||||
|
||||
|
||||
class ConditionalEntityStateTriggerBase(EntityTriggerBase):
|
||||
"""Class for entity state changes where the from state is restricted."""
|
||||
class EntityTransitionTriggerBase(EntityTriggerBase):
|
||||
"""Trigger for entity state changes between specific states."""
|
||||
|
||||
_from_states: set[str]
|
||||
_to_states: set[str]
|
||||
@@ -458,8 +458,8 @@ class ConditionalEntityStateTriggerBase(EntityTriggerBase):
|
||||
return state.state in self._to_states
|
||||
|
||||
|
||||
class EntityFromStateTriggerBase(EntityTriggerBase):
|
||||
"""Class for entity state changes from a specific state."""
|
||||
class EntityOriginStateTriggerBase(EntityTriggerBase):
|
||||
"""Trigger for entity state changes from a specific state."""
|
||||
|
||||
_from_state: str
|
||||
|
||||
@@ -474,8 +474,8 @@ class EntityFromStateTriggerBase(EntityTriggerBase):
|
||||
return state.state != self._from_state
|
||||
|
||||
|
||||
class EntityStateAttributeTriggerBase(EntityTriggerBase):
|
||||
"""Trigger for entity state attribute changes."""
|
||||
class EntityTargetStateAttributeTriggerBase(EntityTriggerBase):
|
||||
"""Trigger for entity state attribute changes to a specific state."""
|
||||
|
||||
_attribute: str
|
||||
_attribute_to_state: str
|
||||
@@ -494,12 +494,12 @@ class EntityStateAttributeTriggerBase(EntityTriggerBase):
|
||||
return state.attributes.get(self._attribute) == self._attribute_to_state
|
||||
|
||||
|
||||
def make_entity_state_trigger(
|
||||
def make_entity_target_state_trigger(
|
||||
domain: str, to_state: str
|
||||
) -> type[EntityStateTriggerBase]:
|
||||
"""Create an entity state trigger class."""
|
||||
) -> type[EntityTargetStateTriggerBase]:
|
||||
"""Create a trigger for entity state changes to a specific state."""
|
||||
|
||||
class CustomTrigger(EntityStateTriggerBase):
|
||||
class CustomTrigger(EntityTargetStateTriggerBase):
|
||||
"""Trigger for entity state changes."""
|
||||
|
||||
_domain = domain
|
||||
@@ -508,12 +508,12 @@ def make_entity_state_trigger(
|
||||
return CustomTrigger
|
||||
|
||||
|
||||
def make_conditional_entity_state_trigger(
|
||||
def make_entity_transition_trigger(
|
||||
domain: str, *, from_states: set[str], to_states: set[str]
|
||||
) -> type[ConditionalEntityStateTriggerBase]:
|
||||
"""Create a conditional entity state trigger class."""
|
||||
) -> type[EntityTransitionTriggerBase]:
|
||||
"""Create a trigger for entity state changes between specific states."""
|
||||
|
||||
class CustomTrigger(ConditionalEntityStateTriggerBase):
|
||||
class CustomTrigger(EntityTransitionTriggerBase):
|
||||
"""Trigger for conditional entity state changes."""
|
||||
|
||||
_domain = domain
|
||||
@@ -523,12 +523,12 @@ def make_conditional_entity_state_trigger(
|
||||
return CustomTrigger
|
||||
|
||||
|
||||
def make_entity_from_state_trigger(
|
||||
def make_entity_origin_state_trigger(
|
||||
domain: str, *, from_state: str
|
||||
) -> type[EntityFromStateTriggerBase]:
|
||||
"""Create an entity "from state" trigger class."""
|
||||
) -> type[EntityOriginStateTriggerBase]:
|
||||
"""Create a trigger for entity state changes from a specific state."""
|
||||
|
||||
class CustomTrigger(EntityFromStateTriggerBase):
|
||||
class CustomTrigger(EntityOriginStateTriggerBase):
|
||||
"""Trigger for entity "from state" changes."""
|
||||
|
||||
_domain = domain
|
||||
@@ -537,12 +537,12 @@ def make_entity_from_state_trigger(
|
||||
return CustomTrigger
|
||||
|
||||
|
||||
def make_entity_state_attribute_trigger(
|
||||
def make_entity_target_state_attribute_trigger(
|
||||
domain: str, attribute: str, to_state: str
|
||||
) -> type[EntityStateAttributeTriggerBase]:
|
||||
"""Create an entity state attribute trigger class."""
|
||||
) -> type[EntityTargetStateAttributeTriggerBase]:
|
||||
"""Create a trigger for entity state attribute changes to a specific state."""
|
||||
|
||||
class CustomTrigger(EntityStateAttributeTriggerBase):
|
||||
class CustomTrigger(EntityTargetStateAttributeTriggerBase):
|
||||
"""Trigger for entity state changes."""
|
||||
|
||||
_domain = domain
|
||||
@@ -1135,6 +1135,5 @@ async def async_get_all_descriptions(
|
||||
description["target"] = target
|
||||
|
||||
new_descriptions_cache[missing_trigger] = description
|
||||
|
||||
hass.data[TRIGGER_DESCRIPTION_CACHE] = new_descriptions_cache
|
||||
return new_descriptions_cache
|
||||
|
||||
6
requirements_all.txt
generated
6
requirements_all.txt
generated
@@ -1482,7 +1482,7 @@ micloud==0.5
|
||||
microBeesPy==0.3.5
|
||||
|
||||
# homeassistant.components.mill
|
||||
mill-local==0.3.0
|
||||
mill-local==0.5.0
|
||||
|
||||
# homeassistant.components.mill
|
||||
millheater==0.14.1
|
||||
@@ -1810,7 +1810,7 @@ py-dactyl==2.0.4
|
||||
py-dormakaba-dkey==1.0.6
|
||||
|
||||
# homeassistant.components.improv_ble
|
||||
py-improv-ble-client==1.0.3
|
||||
py-improv-ble-client==2.0.1
|
||||
|
||||
# homeassistant.components.madvr
|
||||
py-madvr2==1.6.40
|
||||
@@ -2575,7 +2575,7 @@ python-rabbitair==0.0.8
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==3.12.2
|
||||
python-roborock==3.18.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.46
|
||||
|
||||
6
requirements_test_all.txt
generated
6
requirements_test_all.txt
generated
@@ -1289,7 +1289,7 @@ micloud==0.5
|
||||
microBeesPy==0.3.5
|
||||
|
||||
# homeassistant.components.mill
|
||||
mill-local==0.3.0
|
||||
mill-local==0.5.0
|
||||
|
||||
# homeassistant.components.mill
|
||||
millheater==0.14.1
|
||||
@@ -1550,7 +1550,7 @@ py-dactyl==2.0.4
|
||||
py-dormakaba-dkey==1.0.6
|
||||
|
||||
# homeassistant.components.improv_ble
|
||||
py-improv-ble-client==1.0.3
|
||||
py-improv-ble-client==2.0.1
|
||||
|
||||
# homeassistant.components.madvr
|
||||
py-madvr2==1.6.40
|
||||
@@ -2159,7 +2159,7 @@ python-pooldose==0.8.1
|
||||
python-rabbitair==0.0.8
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==3.12.2
|
||||
python-roborock==3.18.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.46
|
||||
|
||||
@@ -1176,7 +1176,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"aten_pe",
|
||||
"atome",
|
||||
"august",
|
||||
"autarco",
|
||||
"aurora",
|
||||
"aurora_abb_powerone",
|
||||
"aussie_broadband",
|
||||
|
||||
@@ -148,6 +148,22 @@ def mock_stream_source_fixture() -> Generator[AsyncMock]:
|
||||
yield mock_stream_source
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_create_stream")
|
||||
def mock_create_stream_fixture() -> Generator[Mock]:
|
||||
"""Fixture to mock create_stream and prevent real stream threads."""
|
||||
mock_stream = Mock()
|
||||
mock_stream.add_provider = Mock()
|
||||
mock_stream.start = AsyncMock()
|
||||
mock_stream.endpoint_url = Mock(return_value="http://home.assistant/playlist.m3u8")
|
||||
mock_stream.set_update_callback = Mock()
|
||||
mock_stream.available = True
|
||||
with patch(
|
||||
"homeassistant.components.camera.create_stream",
|
||||
return_value=mock_stream,
|
||||
):
|
||||
yield mock_stream
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None:
|
||||
"""Initialize test WebRTC cameras with native RTC support."""
|
||||
|
||||
@@ -346,20 +346,14 @@ async def test_websocket_stream_no_source(
|
||||
|
||||
@pytest.mark.usefixtures("mock_camera", "mock_stream")
|
||||
async def test_websocket_camera_stream(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_create_stream: Mock
|
||||
) -> None:
|
||||
"""Test camera/stream websocket command."""
|
||||
await async_setup_component(hass, "camera", {})
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.camera.Stream.endpoint_url",
|
||||
return_value="http://home.assistant/playlist.m3u8",
|
||||
) as mock_stream_view_url,
|
||||
patch(
|
||||
"homeassistant.components.demo.camera.DemoCamera.stream_source",
|
||||
return_value="http://example.com",
|
||||
),
|
||||
with patch(
|
||||
"homeassistant.components.demo.camera.DemoCamera.stream_source",
|
||||
return_value="http://example.com",
|
||||
):
|
||||
# Request playlist through WebSocket
|
||||
client = await hass_ws_client(hass)
|
||||
@@ -369,7 +363,7 @@ async def test_websocket_camera_stream(
|
||||
msg = await client.receive_json()
|
||||
|
||||
# Assert WebSocket response
|
||||
assert mock_stream_view_url.called
|
||||
assert mock_create_stream.endpoint_url.called
|
||||
assert msg["id"] == 6
|
||||
assert msg["type"] == TYPE_RESULT
|
||||
assert msg["success"]
|
||||
@@ -505,21 +499,18 @@ async def test_play_stream_service_no_source(hass: HomeAssistant) -> None:
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_camera", "mock_stream")
|
||||
async def test_handle_play_stream_service(hass: HomeAssistant) -> None:
|
||||
async def test_handle_play_stream_service(
|
||||
hass: HomeAssistant, mock_create_stream: Mock
|
||||
) -> None:
|
||||
"""Test camera play_stream service."""
|
||||
await async_process_ha_core_config(
|
||||
hass,
|
||||
{"external_url": "https://example.com"},
|
||||
)
|
||||
await async_setup_component(hass, "media_player", {})
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.camera.Stream.endpoint_url",
|
||||
) as mock_request_stream,
|
||||
patch(
|
||||
"homeassistant.components.demo.camera.DemoCamera.stream_source",
|
||||
return_value="http://example.com",
|
||||
),
|
||||
with patch(
|
||||
"homeassistant.components.demo.camera.DemoCamera.stream_source",
|
||||
return_value="http://example.com",
|
||||
):
|
||||
# Call service
|
||||
await hass.services.async_call(
|
||||
@@ -533,17 +524,14 @@ async def test_handle_play_stream_service(hass: HomeAssistant) -> None:
|
||||
)
|
||||
# So long as we request the stream, the rest should be covered
|
||||
# by the play_media service tests.
|
||||
assert mock_request_stream.called
|
||||
assert mock_create_stream.endpoint_url.called
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_stream")
|
||||
async def test_no_preload_stream(hass: HomeAssistant) -> None:
|
||||
async def test_no_preload_stream(hass: HomeAssistant, mock_create_stream: Mock) -> None:
|
||||
"""Test camera preload preference."""
|
||||
demo_settings = camera.DynamicStreamSettings()
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.camera.Stream.endpoint_url",
|
||||
) as mock_request_stream,
|
||||
patch(
|
||||
"homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings",
|
||||
return_value=demo_settings,
|
||||
@@ -557,15 +545,14 @@ async def test_no_preload_stream(hass: HomeAssistant) -> None:
|
||||
await async_setup_component(hass, "camera", {DOMAIN: {"platform": "demo"}})
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
assert not mock_request_stream.called
|
||||
assert not mock_create_stream.endpoint_url.called
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_stream")
|
||||
async def test_preload_stream(hass: HomeAssistant) -> None:
|
||||
async def test_preload_stream(hass: HomeAssistant, mock_create_stream: Mock) -> None:
|
||||
"""Test camera preload preference."""
|
||||
demo_settings = camera.DynamicStreamSettings(preload_stream=True)
|
||||
with (
|
||||
patch("homeassistant.components.camera.create_stream") as mock_create_stream,
|
||||
patch(
|
||||
"homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings",
|
||||
return_value=demo_settings,
|
||||
@@ -575,14 +562,13 @@ async def test_preload_stream(hass: HomeAssistant) -> None:
|
||||
return_value="http://example.com",
|
||||
),
|
||||
):
|
||||
mock_create_stream.return_value.start = AsyncMock()
|
||||
assert await async_setup_component(
|
||||
hass, "camera", {DOMAIN: {"platform": "demo"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_create_stream.called
|
||||
assert mock_create_stream.start.called
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_camera")
|
||||
@@ -694,25 +680,16 @@ async def test_state_streaming(hass: HomeAssistant) -> None:
|
||||
assert demo_camera.state == camera.CameraState.STREAMING
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_camera", "mock_stream")
|
||||
@pytest.mark.usefixtures("mock_camera", "mock_stream", "mock_create_stream")
|
||||
async def test_stream_unavailable(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_create_stream: Mock
|
||||
) -> None:
|
||||
"""Camera state."""
|
||||
await async_setup_component(hass, "camera", {})
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.camera.Stream.endpoint_url",
|
||||
return_value="http://home.assistant/playlist.m3u8",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.demo.camera.DemoCamera.stream_source",
|
||||
return_value="http://example.com",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.camera.Stream.set_update_callback",
|
||||
) as mock_update_callback,
|
||||
with patch(
|
||||
"homeassistant.components.demo.camera.DemoCamera.stream_source",
|
||||
return_value="http://example.com",
|
||||
):
|
||||
# Request playlist through WebSocket. We just want to create the stream
|
||||
# but don't care about the result.
|
||||
@@ -721,26 +698,22 @@ async def test_stream_unavailable(
|
||||
{"id": 10, "type": "camera/stream", "entity_id": "camera.demo_camera"}
|
||||
)
|
||||
await client.receive_json()
|
||||
assert mock_update_callback.called
|
||||
assert mock_create_stream.set_update_callback.called
|
||||
|
||||
# Simulate the stream going unavailable
|
||||
callback = mock_update_callback.call_args.args[0]
|
||||
with patch(
|
||||
"homeassistant.components.camera.Stream.available", new_callable=lambda: False
|
||||
):
|
||||
callback()
|
||||
await hass.async_block_till_done()
|
||||
callback = mock_create_stream.set_update_callback.call_args.args[0]
|
||||
mock_create_stream.available = False
|
||||
callback()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
demo_camera = hass.states.get("camera.demo_camera")
|
||||
assert demo_camera is not None
|
||||
assert demo_camera.state == STATE_UNAVAILABLE
|
||||
|
||||
# Simulate stream becomes available
|
||||
with patch(
|
||||
"homeassistant.components.camera.Stream.available", new_callable=lambda: True
|
||||
):
|
||||
callback()
|
||||
await hass.async_block_till_done()
|
||||
mock_create_stream.available = True
|
||||
callback()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
demo_camera = hass.states.get("camera.demo_camera")
|
||||
assert demo_camera is not None
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
from bleak.exc import BleakError
|
||||
from improv_ble_client import (
|
||||
@@ -294,8 +294,13 @@ async def test_bluetooth_rediscovery_after_successful_provision(
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
|
||||
# Start provisioning
|
||||
with patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", return_value=False
|
||||
with (
|
||||
patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify",
|
||||
return_value=False,
|
||||
new_callable=PropertyMock,
|
||||
),
|
||||
patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -375,8 +380,13 @@ async def _test_common_success_with_identify(
|
||||
hass: HomeAssistant, result: FlowResult, address: str
|
||||
) -> None:
|
||||
"""Test bluetooth and user flow success paths."""
|
||||
with patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", return_value=True
|
||||
with (
|
||||
patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify",
|
||||
return_value=True,
|
||||
new_callable=PropertyMock,
|
||||
),
|
||||
patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -420,8 +430,13 @@ async def _test_common_success_wo_identify(
|
||||
placeholders: dict[str, str] | None = None,
|
||||
) -> None:
|
||||
"""Test bluetooth and user flow success paths."""
|
||||
with patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", return_value=False
|
||||
with (
|
||||
patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify",
|
||||
return_value=False,
|
||||
new_callable=PropertyMock,
|
||||
),
|
||||
patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -475,8 +490,13 @@ async def _test_common_success_wo_identify_w_authorize(
|
||||
hass: HomeAssistant, result: FlowResult, address: str
|
||||
) -> None:
|
||||
"""Test bluetooth and user flow success paths."""
|
||||
with patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", return_value=False
|
||||
with (
|
||||
patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify",
|
||||
return_value=False,
|
||||
new_callable=PropertyMock,
|
||||
),
|
||||
patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -571,7 +591,7 @@ async def test_bluetooth_step_already_in_progress(hass: HomeAssistant) -> None:
|
||||
(improv_ble_errors.CharacteristicMissingError, "characteristic_missing"),
|
||||
],
|
||||
)
|
||||
async def test_can_identify_fails(hass: HomeAssistant, exc, error) -> None:
|
||||
async def test_ensure_connected_fails(hass: HomeAssistant, exc, error) -> None:
|
||||
"""Test bluetooth flow with error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
@@ -588,7 +608,8 @@ async def test_can_identify_fails(hass: HomeAssistant, exc, error) -> None:
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", side_effect=exc
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected",
|
||||
side_effect=exc,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -622,8 +643,13 @@ async def test_identify_fails(hass: HomeAssistant, exc, error) -> None:
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", return_value=True
|
||||
with (
|
||||
patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify",
|
||||
return_value=True,
|
||||
new_callable=PropertyMock,
|
||||
),
|
||||
patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -665,8 +691,13 @@ async def test_need_authorization_fails(hass: HomeAssistant, exc, error) -> None
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", return_value=False
|
||||
with (
|
||||
patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify",
|
||||
return_value=False,
|
||||
new_callable=PropertyMock,
|
||||
),
|
||||
patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -709,8 +740,13 @@ async def test_authorize_fails(hass: HomeAssistant, exc, error) -> None:
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", return_value=False
|
||||
with (
|
||||
patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify",
|
||||
return_value=False,
|
||||
new_callable=PropertyMock,
|
||||
),
|
||||
patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -752,8 +788,13 @@ async def _test_provision_error(hass: HomeAssistant, exc) -> str:
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", return_value=False
|
||||
with (
|
||||
patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify",
|
||||
return_value=False,
|
||||
new_callable=PropertyMock,
|
||||
),
|
||||
patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -878,8 +919,13 @@ async def test_flow_chaining_with_next_flow(hass: HomeAssistant) -> None:
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
|
||||
# Start provisioning
|
||||
with patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", return_value=False
|
||||
with (
|
||||
patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify",
|
||||
return_value=False,
|
||||
new_callable=PropertyMock,
|
||||
),
|
||||
patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -946,8 +992,13 @@ async def test_flow_chaining_timeout(hass: HomeAssistant) -> None:
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
|
||||
# Start provisioning
|
||||
with patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", return_value=False
|
||||
with (
|
||||
patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify",
|
||||
return_value=False,
|
||||
new_callable=PropertyMock,
|
||||
),
|
||||
patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -998,8 +1049,13 @@ async def test_flow_chaining_with_redirect_url(hass: HomeAssistant) -> None:
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
|
||||
# Start provisioning
|
||||
with patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", return_value=False
|
||||
with (
|
||||
patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify",
|
||||
return_value=False,
|
||||
new_callable=PropertyMock,
|
||||
),
|
||||
patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -1069,8 +1125,13 @@ async def test_flow_chaining_future_already_done(
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
|
||||
# Start provisioning
|
||||
with patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify", return_value=False
|
||||
with (
|
||||
patch(
|
||||
f"{IMPROV_BLE}.config_flow.ImprovBLEClient.can_identify",
|
||||
return_value=False,
|
||||
new_callable=PropertyMock,
|
||||
),
|
||||
patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.ensure_connected"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
|
||||
228
tests/components/input_boolean/test_trigger.py
Normal file
228
tests/components/input_boolean/test_trigger.py
Normal 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()
|
||||
@@ -391,7 +391,7 @@
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PRESSURE: 'pressure'>,
|
||||
'original_device_class': <SensorDeviceClass.ATMOSPHERIC_PRESSURE: 'atmospheric_pressure'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'La Clusaz Pressure',
|
||||
'platform': 'meteo_france',
|
||||
@@ -407,7 +407,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by Météo-France',
|
||||
'device_class': 'pressure',
|
||||
'device_class': 'atmospheric_pressure',
|
||||
'friendly_name': 'La Clusaz Pressure',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPressure.HPA: 'hPa'>,
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from pynintendoparental import NintendoParental
|
||||
from pynintendoparental.device import Device
|
||||
from pynintendoparental.enum import DeviceTimerMode
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.nintendo_parental_controls.const import DOMAIN
|
||||
@@ -39,9 +40,11 @@ def mock_nintendo_device() -> Device:
|
||||
mock.today_playing_time = 110
|
||||
mock.today_time_remaining = 10
|
||||
mock.bedtime_alarm = time(hour=19)
|
||||
mock.timer_mode = DeviceTimerMode.DAILY
|
||||
mock.add_extra_time.return_value = None
|
||||
mock.set_bedtime_alarm.return_value = None
|
||||
mock.update_max_daily_playtime.return_value = None
|
||||
mock.set_timer_mode.return_value = None
|
||||
mock.forced_termination_mode = True
|
||||
mock.model = "Test Model"
|
||||
mock.generation = "P00"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# serializer version: 1
|
||||
# name: test_select[select.home_assistant_test_restriction_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'daily',
|
||||
'each_day_of_the_week',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.home_assistant_test_restriction_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Restriction mode',
|
||||
'platform': 'nintendo_parental_controls',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <NintendoParentalSelect.TIMER_MODE: 'timer_mode'>,
|
||||
'unique_id': 'testdevid_timer_mode',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_select[select.home_assistant_test_restriction_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Home Assistant Test Restriction mode',
|
||||
'options': list([
|
||||
'daily',
|
||||
'each_day_of_the_week',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.home_assistant_test_restriction_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'daily',
|
||||
})
|
||||
# ---
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from pynintendoauth.exceptions import InvalidOAuthConfigurationException
|
||||
from pynintendoauth.exceptions import (
|
||||
HttpException,
|
||||
InvalidOAuthConfigurationException,
|
||||
InvalidSessionTokenException,
|
||||
)
|
||||
from pynintendoparental.exceptions import NoDevicesFoundException
|
||||
import pytest
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -14,16 +19,62 @@ from . import setup_integration
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_invalid_authentication(
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "translation_key", "expected_state", "expected_log_message"),
|
||||
[
|
||||
(
|
||||
InvalidOAuthConfigurationException(
|
||||
status_code=401, message="Authentication failed"
|
||||
),
|
||||
"invalid_auth",
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
None,
|
||||
),
|
||||
(
|
||||
NoDevicesFoundException(),
|
||||
"no_devices_found",
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
None,
|
||||
),
|
||||
(
|
||||
HttpException(
|
||||
status_code=400, error_code="update_required", message="Update required"
|
||||
),
|
||||
"update_required",
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
None,
|
||||
),
|
||||
(
|
||||
HttpException(
|
||||
status_code=500, error_code="unknown", message="Unknown error"
|
||||
),
|
||||
None,
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
None,
|
||||
),
|
||||
(
|
||||
InvalidSessionTokenException(
|
||||
status_code=403, error_code="invalid_token", message="Invalid token"
|
||||
),
|
||||
None,
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
"Session token invalid, will renew on next update",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_update_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_nintendo_client: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
exception: Exception,
|
||||
translation_key: str,
|
||||
expected_state: ConfigEntryState,
|
||||
expected_log_message: str | None,
|
||||
) -> None:
|
||||
"""Test handling of invalid authentication."""
|
||||
mock_nintendo_client.update.side_effect = InvalidOAuthConfigurationException(
|
||||
status_code=401, message="Authentication failed"
|
||||
)
|
||||
"""Test handling of update errors."""
|
||||
mock_nintendo_client.update.side_effect = exception
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
@@ -32,25 +83,13 @@ async def test_invalid_authentication(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert len(entries) == 0
|
||||
# Ensure the config entry is marked as error
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
# Ensure the config entry is marked as expected state
|
||||
assert mock_config_entry.state is expected_state
|
||||
|
||||
async def test_no_devices(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_nintendo_client: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test handling of invalid authentication."""
|
||||
mock_nintendo_client.update.side_effect = NoDevicesFoundException()
|
||||
# Ensure the correct translation key is used in the error
|
||||
assert mock_config_entry.error_reason_translation_key == translation_key
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
# Ensure no entities are created
|
||||
entries = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert len(entries) == 0
|
||||
# Ensure the config entry is marked as error
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
# If there's an expected log message, check that it was logged
|
||||
if expected_log_message:
|
||||
assert expected_log_message in caplog.text
|
||||
|
||||
64
tests/components/nintendo_parental_controls/test_select.py
Normal file
64
tests/components/nintendo_parental_controls/test_select.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Tests for Nintendo Switch Parental Controls select platform."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from pynintendoparental.enum import DeviceTimerMode
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.select import (
|
||||
ATTR_OPTION,
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
async def test_select(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_nintendo_client: AsyncMock,
|
||||
mock_nintendo_device: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test select platform."""
|
||||
with patch(
|
||||
"homeassistant.components.nintendo_parental_controls._PLATFORMS",
|
||||
[Platform.SELECT],
|
||||
):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_select_option(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_nintendo_client: AsyncMock,
|
||||
mock_nintendo_device: AsyncMock,
|
||||
) -> None:
|
||||
"""Test select option service."""
|
||||
with patch(
|
||||
"homeassistant.components.nintendo_parental_controls._PLATFORMS",
|
||||
[Platform.SELECT],
|
||||
):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
ATTR_ENTITY_ID: "select.home_assistant_test_restriction_mode",
|
||||
ATTR_OPTION: DeviceTimerMode.EACH_DAY_OF_THE_WEEK.name.lower(),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_nintendo_device.set_timer_mode.assert_awaited_once_with(
|
||||
DeviceTimerMode.EACH_DAY_OF_THE_WEEK
|
||||
)
|
||||
@@ -158,7 +158,6 @@ async def test_polling_platform_init(
|
||||
(
|
||||
SERVICE_SEND_LOCATION,
|
||||
{
|
||||
ATTR_MESSAGE: "test_message",
|
||||
ATTR_MESSAGE_THREAD_ID: "123",
|
||||
ATTR_LONGITUDE: "1.123",
|
||||
ATTR_LATITUDE: "1.123",
|
||||
@@ -414,6 +413,7 @@ async def test_send_chat_action(
|
||||
CONF_CONFIG_ENTRY_ID: mock_broadcast_config_entry.entry_id,
|
||||
ATTR_TARGET: [123456],
|
||||
ATTR_CHAT_ACTION: CHAT_ACTION_TYPING,
|
||||
ATTR_MESSAGE_THREAD_ID: 123,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
@@ -421,7 +421,9 @@ async def test_send_chat_action(
|
||||
|
||||
await hass.async_block_till_done()
|
||||
mock.assert_called_once()
|
||||
mock.assert_called_with(chat_id=123456, action=CHAT_ACTION_TYPING)
|
||||
mock.assert_called_with(
|
||||
chat_id=123456, action=CHAT_ACTION_TYPING, message_thread_id=123
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -1505,7 +1507,6 @@ async def test_set_message_reaction(
|
||||
SERVICE_SEND_LOCATION,
|
||||
{
|
||||
ATTR_TARGET: 654321,
|
||||
ATTR_MESSAGE: "test_message",
|
||||
ATTR_MESSAGE_THREAD_ID: "123",
|
||||
ATTR_LONGITUDE: "1.123",
|
||||
ATTR_LATITUDE: "1.123",
|
||||
|
||||
@@ -130,8 +130,16 @@ async def test_async_enable_logging(
|
||||
cleanup_log_files()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("extra_env", "log_file_count", "old_log_file_count"),
|
||||
[({}, 0, 1), ({"HA_DUPLICATE_LOG_FILE": "1"}, 1, 0)],
|
||||
)
|
||||
async def test_async_enable_logging_supervisor(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
extra_env: dict[str, str],
|
||||
log_file_count: int,
|
||||
old_log_file_count: int,
|
||||
) -> None:
|
||||
"""Test to ensure the default log file is not created on Supervisor installations."""
|
||||
|
||||
@@ -141,14 +149,14 @@ async def test_async_enable_logging_supervisor(
|
||||
assert len(glob.glob(ARG_LOG_FILE)) == 0
|
||||
|
||||
with (
|
||||
patch.dict(os.environ, {"SUPERVISOR": "1"}),
|
||||
patch.dict(os.environ, {"SUPERVISOR": "1", **extra_env}),
|
||||
patch(
|
||||
"homeassistant.bootstrap.async_activate_log_queue_handler"
|
||||
) as mock_async_activate_log_queue_handler,
|
||||
patch("logging.getLogger"),
|
||||
):
|
||||
await bootstrap.async_enable_logging(hass)
|
||||
assert len(glob.glob(CONFIG_LOG_FILE)) == 0
|
||||
assert len(glob.glob(CONFIG_LOG_FILE)) == log_file_count
|
||||
mock_async_activate_log_queue_handler.assert_called_once()
|
||||
mock_async_activate_log_queue_handler.reset_mock()
|
||||
|
||||
@@ -162,9 +170,10 @@ async def test_async_enable_logging_supervisor(
|
||||
await hass.async_add_executor_job(write_log_file)
|
||||
assert len(glob.glob(CONFIG_LOG_FILE)) == 1
|
||||
assert len(glob.glob(f"{CONFIG_LOG_FILE}.old")) == 0
|
||||
|
||||
await bootstrap.async_enable_logging(hass)
|
||||
assert len(glob.glob(CONFIG_LOG_FILE)) == 0
|
||||
assert len(glob.glob(f"{CONFIG_LOG_FILE}.old")) == 1
|
||||
assert len(glob.glob(CONFIG_LOG_FILE)) == log_file_count
|
||||
assert len(glob.glob(f"{CONFIG_LOG_FILE}.old")) == old_log_file_count
|
||||
mock_async_activate_log_queue_handler.assert_called_once()
|
||||
mock_async_activate_log_queue_handler.reset_mock()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user