mirror of
https://github.com/home-assistant/core.git
synced 2026-06-18 09:52:57 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2f11d25e1 |
@@ -30,7 +30,7 @@ from homeassistant.exceptions import (
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADDITIONAL_SETTINGS
|
||||
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS
|
||||
from .coordinator import (
|
||||
AirOSConfigEntry,
|
||||
AirOSDataUpdateCoordinator,
|
||||
@@ -55,14 +55,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
|
||||
# By default airOS 8 comes with self-signed SSL certificates,
|
||||
# with no option in the web UI to change or upload a custom certificate.
|
||||
session = async_get_clientsession(
|
||||
hass, verify_ssl=entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_VERIFY_SSL]
|
||||
hass, verify_ssl=entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL]
|
||||
)
|
||||
|
||||
conn_data = {
|
||||
CONF_HOST: entry.data[CONF_HOST],
|
||||
CONF_USERNAME: entry.data[CONF_USERNAME],
|
||||
CONF_PASSWORD: entry.data[CONF_PASSWORD],
|
||||
"use_ssl": entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_SSL],
|
||||
"use_ssl": entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
|
||||
"session": session,
|
||||
}
|
||||
|
||||
@@ -116,15 +116,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||
"""Migrate old config entry."""
|
||||
|
||||
# 1.1 Migrate config_entry to add additional ssl settings
|
||||
# 1.1 Migrate config_entry to add advanced ssl settings
|
||||
if entry.version == 1 and entry.minor_version == 1:
|
||||
new_minor_version = 2
|
||||
new_data = {**entry.data}
|
||||
additional_data = {
|
||||
advanced_data = {
|
||||
CONF_SSL: DEFAULT_SSL,
|
||||
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
|
||||
}
|
||||
new_data[SECTION_ADDITIONAL_SETTINGS] = additional_data
|
||||
new_data[SECTION_ADVANCED_SETTINGS] = advanced_data
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
|
||||
@@ -52,7 +52,7 @@ from .const import (
|
||||
HOSTNAME,
|
||||
IP_ADDRESS,
|
||||
MAC_ADDRESS,
|
||||
SECTION_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -66,7 +66,7 @@ STEP_DISCOVERY_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(SECTION_ADDITIONAL_SETTINGS): section(
|
||||
vol.Required(SECTION_ADVANCED_SETTINGS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_SSL, default=DEFAULT_SSL): bool,
|
||||
@@ -134,7 +134,7 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# with no option in the web UI to change or upload a custom certificate.
|
||||
session = async_get_clientsession(
|
||||
self.hass,
|
||||
verify_ssl=config_data[SECTION_ADDITIONAL_SETTINGS][CONF_VERIFY_SSL],
|
||||
verify_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL],
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -143,7 +143,7 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
username=config_data[CONF_USERNAME],
|
||||
password=config_data[CONF_PASSWORD],
|
||||
session=session,
|
||||
use_ssl=config_data[SECTION_ADDITIONAL_SETTINGS][CONF_SSL],
|
||||
use_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
|
||||
)
|
||||
|
||||
except (
|
||||
@@ -234,18 +234,18 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
autocomplete="current-password",
|
||||
)
|
||||
),
|
||||
vol.Required(SECTION_ADDITIONAL_SETTINGS): section(
|
||||
vol.Required(SECTION_ADVANCED_SETTINGS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_SSL,
|
||||
default=current_data[SECTION_ADDITIONAL_SETTINGS][
|
||||
default=current_data[SECTION_ADVANCED_SETTINGS][
|
||||
CONF_SSL
|
||||
],
|
||||
): bool,
|
||||
vol.Required(
|
||||
CONF_VERIFY_SSL,
|
||||
default=current_data[SECTION_ADDITIONAL_SETTINGS][
|
||||
default=current_data[SECTION_ADVANCED_SETTINGS][
|
||||
CONF_VERIFY_SSL
|
||||
],
|
||||
): bool,
|
||||
|
||||
@@ -12,7 +12,7 @@ MANUFACTURER = "Ubiquiti"
|
||||
DEFAULT_VERIFY_SSL = False
|
||||
DEFAULT_SSL = True
|
||||
|
||||
SECTION_ADDITIONAL_SETTINGS = "additional_settings"
|
||||
SECTION_ADVANCED_SETTINGS = "advanced_settings"
|
||||
|
||||
# Discovery related
|
||||
DEFAULT_USERNAME = "ubnt"
|
||||
|
||||
@@ -4,7 +4,7 @@ from homeassistant.const import CONF_HOST, CONF_SSL
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER, SECTION_ADDITIONAL_SETTINGS
|
||||
from .const import DOMAIN, MANUFACTURER, SECTION_ADVANCED_SETTINGS
|
||||
from .coordinator import AirOSDataUpdateCoordinator
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]):
|
||||
airos_data = self.coordinator.data
|
||||
url_schema = (
|
||||
"https"
|
||||
if coordinator.config_entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_SSL]
|
||||
if coordinator.config_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL]
|
||||
else "http"
|
||||
)
|
||||
|
||||
|
||||
@@ -33,16 +33,16 @@
|
||||
},
|
||||
"description": "Enter the username and password for {device_name}",
|
||||
"sections": {
|
||||
"additional_settings": {
|
||||
"advanced_settings": {
|
||||
"data": {
|
||||
"ssl": "[%key:component::airos::config::step::manual::sections::additional_settings::data::ssl%]",
|
||||
"ssl": "[%key:component::airos::config::step::manual::sections::advanced_settings::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"ssl": "[%key:component::airos::config::step::manual::sections::additional_settings::data_description::ssl%]",
|
||||
"verify_ssl": "[%key:component::airos::config::step::manual::sections::additional_settings::data_description::verify_ssl%]"
|
||||
"ssl": "[%key:component::airos::config::step::manual::sections::advanced_settings::data_description::ssl%]",
|
||||
"verify_ssl": "[%key:component::airos::config::step::manual::sections::advanced_settings::data_description::verify_ssl%]"
|
||||
},
|
||||
"name": "[%key:component::airos::config::step::manual::sections::additional_settings::name%]"
|
||||
"name": "[%key:component::airos::config::step::manual::sections::advanced_settings::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58,7 +58,7 @@
|
||||
"username": "Administrator username for the airOS device, normally 'ubnt'"
|
||||
},
|
||||
"sections": {
|
||||
"additional_settings": {
|
||||
"advanced_settings": {
|
||||
"data": {
|
||||
"ssl": "Use HTTPS",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
@@ -67,7 +67,7 @@
|
||||
"ssl": "Whether the connection should be encrypted (required for most devices)",
|
||||
"verify_ssl": "Whether the certificate should be verified when using HTTPS. This should be off for self-signed certificates"
|
||||
},
|
||||
"name": "Additional settings"
|
||||
"name": "Advanced settings"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -87,16 +87,16 @@
|
||||
"password": "[%key:component::airos::config::step::manual::data_description::password%]"
|
||||
},
|
||||
"sections": {
|
||||
"additional_settings": {
|
||||
"advanced_settings": {
|
||||
"data": {
|
||||
"ssl": "[%key:component::airos::config::step::manual::sections::additional_settings::data::ssl%]",
|
||||
"ssl": "[%key:component::airos::config::step::manual::sections::advanced_settings::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"ssl": "[%key:component::airos::config::step::manual::sections::additional_settings::data_description::ssl%]",
|
||||
"verify_ssl": "[%key:component::airos::config::step::manual::sections::additional_settings::data_description::verify_ssl%]"
|
||||
"ssl": "[%key:component::airos::config::step::manual::sections::advanced_settings::data_description::ssl%]",
|
||||
"verify_ssl": "[%key:component::airos::config::step::manual::sections::advanced_settings::data_description::verify_ssl%]"
|
||||
},
|
||||
"name": "[%key:component::airos::config::step::manual::sections::additional_settings::name%]"
|
||||
"name": "[%key:component::airos::config::step::manual::sections::advanced_settings::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
PLATFORMS = [Platform.BUTTON, Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
"""Button platform for Edifier infrared integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from infrared_protocols.codes.edifier.models import EdifierCommandSet, EdifierModel
|
||||
from infrared_protocols.codes.edifier.r1280db import EdifierR1280DBCode
|
||||
from infrared_protocols.codes.edifier.r1700bt import EdifierR1700BTCode
|
||||
from infrared_protocols.codes.edifier.rc20g import EdifierRC20GCode
|
||||
from infrared_protocols.codes.edifier.s360db import EdifierS360DBCode
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.components.infrared import InfraredEmitterConsumerEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MODEL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CONF_COMMAND_SET, CONF_INFRARED_ENTITY_ID, EdifierCode
|
||||
from .entity import EdifierIrEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EdifierIrButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describes Edifier IR button entity."""
|
||||
|
||||
command_code: EdifierCode
|
||||
|
||||
|
||||
COMMAND_SET_BUTTONS: dict[
|
||||
EdifierCommandSet,
|
||||
tuple[EdifierIrButtonEntityDescription, ...],
|
||||
] = {
|
||||
EdifierCommandSet.R1700BT: (
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="bluetooth",
|
||||
translation_key="bluetooth",
|
||||
command_code=EdifierR1700BTCode.BLUETOOTH,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="line_1",
|
||||
translation_key="line_1",
|
||||
command_code=EdifierR1700BTCode.LINE_1,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="line_2",
|
||||
translation_key="line_2",
|
||||
command_code=EdifierR1700BTCode.LINE_2,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="fx_on",
|
||||
translation_key="fx_on",
|
||||
command_code=EdifierR1700BTCode.FX_ON,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="fx_off",
|
||||
translation_key="fx_off",
|
||||
command_code=EdifierR1700BTCode.FX_OFF,
|
||||
),
|
||||
),
|
||||
EdifierCommandSet.R1280DB: (
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="bluetooth",
|
||||
translation_key="bluetooth",
|
||||
command_code=EdifierR1280DBCode.BLUETOOTH,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="line_1",
|
||||
translation_key="line_1",
|
||||
command_code=EdifierR1280DBCode.LINE_1,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="line_2",
|
||||
translation_key="line_2",
|
||||
command_code=EdifierR1280DBCode.LINE_2,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="optical",
|
||||
translation_key="optical",
|
||||
command_code=EdifierR1280DBCode.OPTICAL,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="coax",
|
||||
translation_key="coax",
|
||||
command_code=EdifierR1280DBCode.COAX,
|
||||
),
|
||||
),
|
||||
EdifierCommandSet.S360DB: (
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="bluetooth",
|
||||
translation_key="bluetooth",
|
||||
command_code=EdifierS360DBCode.BLUETOOTH,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="optical",
|
||||
translation_key="optical",
|
||||
command_code=EdifierS360DBCode.OPTICAL,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="coax",
|
||||
translation_key="coax",
|
||||
command_code=EdifierS360DBCode.COAX,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="pc",
|
||||
translation_key="pc",
|
||||
command_code=EdifierS360DBCode.PC,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="aux",
|
||||
translation_key="aux",
|
||||
command_code=EdifierS360DBCode.AUX,
|
||||
),
|
||||
),
|
||||
EdifierCommandSet.RC20G: (
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="bluetooth",
|
||||
translation_key="bluetooth",
|
||||
command_code=EdifierRC20GCode.BLUETOOTH,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="pc",
|
||||
translation_key="pc",
|
||||
command_code=EdifierRC20GCode.PC,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="aux",
|
||||
translation_key="aux",
|
||||
command_code=EdifierRC20GCode.AUX,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="optical",
|
||||
translation_key="optical",
|
||||
command_code=EdifierRC20GCode.OPTICAL,
|
||||
),
|
||||
EdifierIrButtonEntityDescription(
|
||||
key="coax",
|
||||
translation_key="coax",
|
||||
command_code=EdifierRC20GCode.COAX,
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Edifier IR buttons from a config entry."""
|
||||
infrared_entity_id = entry.data[CONF_INFRARED_ENTITY_ID]
|
||||
command_set = EdifierCommandSet(entry.data[CONF_COMMAND_SET])
|
||||
model = EdifierModel(entry.data[CONF_MODEL])
|
||||
async_add_entities(
|
||||
EdifierIrButton(entry, model, infrared_entity_id, description)
|
||||
for description in COMMAND_SET_BUTTONS.get(command_set, ())
|
||||
)
|
||||
|
||||
|
||||
class EdifierIrButton(EdifierIrEntity, InfraredEmitterConsumerEntity, ButtonEntity):
|
||||
"""Edifier IR button entity."""
|
||||
|
||||
entity_description: EdifierIrButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry: ConfigEntry,
|
||||
model: EdifierModel,
|
||||
infrared_entity_id: str,
|
||||
description: EdifierIrButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Edifier IR button."""
|
||||
super().__init__(entry, model, unique_id_suffix=description.key)
|
||||
self._infrared_emitter_entity_id = infrared_entity_id
|
||||
self.entity_description = description
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
await self._send_command(self.entity_description.command_code.to_command())
|
||||
@@ -18,5 +18,36 @@
|
||||
"title": "Set up Edifier IR speaker"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"aux": {
|
||||
"name": "AUX"
|
||||
},
|
||||
"bluetooth": {
|
||||
"name": "Bluetooth"
|
||||
},
|
||||
"coax": {
|
||||
"name": "Coaxial"
|
||||
},
|
||||
"fx_off": {
|
||||
"name": "FX off"
|
||||
},
|
||||
"fx_on": {
|
||||
"name": "FX on"
|
||||
},
|
||||
"line_1": {
|
||||
"name": "Line 1"
|
||||
},
|
||||
"line_2": {
|
||||
"name": "Line 2"
|
||||
},
|
||||
"optical": {
|
||||
"name": "Optical"
|
||||
},
|
||||
"pc": {
|
||||
"name": "PC"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,17 +308,17 @@ class Events(Base):
|
||||
def from_event(event: Event) -> Events:
|
||||
"""Create an event database object from a native event."""
|
||||
context = event.context
|
||||
# The unused legacy columns (event_type, event_data, time_fired,
|
||||
# context_id, context_user_id, context_parent_id) are nullable with no
|
||||
# default, so they are intentionally left unset here. Assigning them
|
||||
# None would still insert NULL, but each assignment goes through
|
||||
# SQLAlchemy's instrumented attribute machinery, which is a measurable
|
||||
# cost when run for every recorded event.
|
||||
return Events(
|
||||
event_type=None,
|
||||
event_data=None,
|
||||
origin_idx=event.origin.idx,
|
||||
time_fired=None,
|
||||
time_fired_ts=event.time_fired_timestamp,
|
||||
context_id=None,
|
||||
context_id_bin=ulid_to_bytes_or_none(context.id),
|
||||
context_user_id=None,
|
||||
context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id),
|
||||
context_parent_id=None,
|
||||
context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id),
|
||||
)
|
||||
|
||||
@@ -491,18 +491,19 @@ class States(Base):
|
||||
else:
|
||||
last_reported_ts = state.last_reported_timestamp
|
||||
context = event.context
|
||||
# The unused legacy columns (entity_id, attributes, context_id,
|
||||
# context_user_id, context_parent_id, last_updated, last_changed) are
|
||||
# nullable with no default, so they are intentionally left unset here.
|
||||
# Assigning them None would still insert NULL, but each assignment goes
|
||||
# through SQLAlchemy's instrumented attribute machinery, which is a
|
||||
# measurable cost when run for every recorded state change.
|
||||
return States(
|
||||
state=state_value,
|
||||
entity_id=None,
|
||||
attributes=None,
|
||||
context_id=None,
|
||||
context_id_bin=ulid_to_bytes_or_none(context.id),
|
||||
context_user_id=None,
|
||||
context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id),
|
||||
context_parent_id=None,
|
||||
context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id),
|
||||
origin_idx=event.origin.idx,
|
||||
last_updated=None,
|
||||
last_changed=None,
|
||||
last_updated_ts=last_updated_ts,
|
||||
last_changed_ts=last_changed_ts,
|
||||
last_reported_ts=last_reported_ts,
|
||||
@@ -559,13 +560,8 @@ class StateAttributes(Base):
|
||||
# None state means the state was removed from the state machine
|
||||
if (state := event.data["new_state"]) is None:
|
||||
return b"{}"
|
||||
if (state_info := state.state_info) and (
|
||||
unrecorded_attributes := state_info["unrecorded_attributes"]
|
||||
):
|
||||
# The entity has unrecorded attributes, so a combined exclude set
|
||||
# has to be built. The common case (no unrecorded attributes) falls
|
||||
# through to the shared constant below without allocating a set per
|
||||
# recorded state change.
|
||||
if state_info := state.state_info:
|
||||
unrecorded_attributes = state_info["unrecorded_attributes"]
|
||||
exclude_attrs = {
|
||||
*ALL_DOMAIN_EXCLUDE_ATTRS,
|
||||
*unrecorded_attributes,
|
||||
|
||||
@@ -10,6 +10,7 @@ from datetime import datetime, time as dt_time, timedelta
|
||||
import functools as ft
|
||||
import inspect
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -145,6 +146,10 @@ _PLATFORM_ALIASES: dict[str | None, str | None] = {
|
||||
"trigger": None,
|
||||
}
|
||||
|
||||
INPUT_ENTITY_ID = re.compile(
|
||||
r"^input_(?:select|text|number|boolean|datetime)\.(?!.+__)(?!_)[\da-z_]+(?<!_)$"
|
||||
)
|
||||
|
||||
|
||||
CONDITION_DESCRIPTION_CACHE: HassKey[dict[str, dict[str, Any] | None]] = HassKey(
|
||||
"condition_description_cache"
|
||||
@@ -1716,7 +1721,7 @@ def state(
|
||||
state_value = req_state_value
|
||||
if (
|
||||
isinstance(req_state_value, str)
|
||||
and cv.INPUT_ENTITY_ID.match(req_state_value) is not None
|
||||
and INPUT_ENTITY_ID.match(req_state_value) is not None
|
||||
):
|
||||
if not (state_entity := hass.states.get(req_state_value)):
|
||||
raise ConditionErrorMessage(
|
||||
|
||||
@@ -1549,10 +1549,6 @@ NUMERIC_STATE_CONDITION_SCHEMA = vol.All(
|
||||
has_at_least_one_key(CONF_BELOW, CONF_ABOVE),
|
||||
)
|
||||
|
||||
INPUT_ENTITY_ID = re.compile(
|
||||
r"^input_(?:select|text|number|boolean|datetime)\.(?!.+__)(?!_)[\da-z_]+(?<!_)$"
|
||||
)
|
||||
|
||||
STATE_CONDITION_BASE_SCHEMA = {
|
||||
**CONDITION_BASE_SCHEMA,
|
||||
vol.Required(CONF_CONDITION): "state",
|
||||
@@ -1589,22 +1585,7 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict[str, Any]:
|
||||
else:
|
||||
validated = STATE_CONDITION_STATE_SCHEMA(value)
|
||||
|
||||
validated = key_dependency("for", "state")(validated)
|
||||
|
||||
if CONF_FOR in validated:
|
||||
# `for` measures how long the entity has held its current state via
|
||||
# last_changed, so it only supports a single literal state. It cannot
|
||||
# track an attribute, a list of states, or a state resolved from
|
||||
# another entity.
|
||||
if CONF_ATTRIBUTE in validated:
|
||||
raise vol.Invalid("Cannot use 'for' with an attribute")
|
||||
state = validated[CONF_STATE]
|
||||
if isinstance(state, list):
|
||||
raise vol.Invalid("Cannot use 'for' with a list of states")
|
||||
if INPUT_ENTITY_ID.match(state):
|
||||
raise vol.Invalid("Cannot use 'for' with a state referencing an entity")
|
||||
|
||||
return validated
|
||||
return key_dependency("for", "state")(validated)
|
||||
|
||||
|
||||
TEMPLATE_CONDITION_SCHEMA = vol.Schema(
|
||||
|
||||
@@ -639,7 +639,7 @@
|
||||
}),
|
||||
}),
|
||||
'entry_data': dict({
|
||||
'additional_settings': dict({
|
||||
'advanced_settings': dict({
|
||||
'ssl': True,
|
||||
'verify_ssl': False,
|
||||
}),
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.components.airos.const import (
|
||||
HOSTNAME,
|
||||
IP_ADDRESS,
|
||||
MAC_ADDRESS,
|
||||
SECTION_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_DHCP,
|
||||
@@ -48,7 +48,7 @@ NEW_PASSWORD = "new_password"
|
||||
REAUTH_STEP = "reauth_confirm"
|
||||
RECONFIGURE_STEP = "reconfigure"
|
||||
|
||||
MOCK_ADDITIONAL_SETTINGS = {
|
||||
MOCK_ADVANCED_SETTINGS = {
|
||||
CONF_SSL: True,
|
||||
CONF_VERIFY_SSL: False,
|
||||
}
|
||||
@@ -57,7 +57,7 @@ MOCK_CONFIG = {
|
||||
CONF_HOST: "1.1.1.1",
|
||||
CONF_USERNAME: DEFAULT_USERNAME,
|
||||
CONF_PASSWORD: "test-password",
|
||||
SECTION_ADDITIONAL_SETTINGS: MOCK_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS: MOCK_ADVANCED_SETTINGS,
|
||||
}
|
||||
MOCK_CONFIG_REAUTH = {
|
||||
CONF_HOST: "1.1.1.1",
|
||||
@@ -410,7 +410,7 @@ async def test_successful_reconfigure(
|
||||
|
||||
user_input = {
|
||||
CONF_PASSWORD: NEW_PASSWORD,
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_SSL: True,
|
||||
CONF_VERIFY_SSL: True,
|
||||
},
|
||||
@@ -426,8 +426,8 @@ async def test_successful_reconfigure(
|
||||
|
||||
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
||||
assert updated_entry.data[CONF_PASSWORD] == NEW_PASSWORD
|
||||
assert updated_entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_SSL] is True
|
||||
assert updated_entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_VERIFY_SSL] is True
|
||||
assert updated_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL] is True
|
||||
assert updated_entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL] is True
|
||||
|
||||
assert updated_entry.data[CONF_HOST] == MOCK_CONFIG[CONF_HOST]
|
||||
assert updated_entry.data[CONF_USERNAME] == MOCK_CONFIG[CONF_USERNAME]
|
||||
@@ -468,7 +468,7 @@ async def test_reconfigure_flow_failure(
|
||||
|
||||
user_input = {
|
||||
CONF_PASSWORD: NEW_PASSWORD,
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_SSL: True,
|
||||
CONF_VERIFY_SSL: True,
|
||||
},
|
||||
@@ -525,7 +525,7 @@ async def test_reconfigure_unique_id_mismatch(
|
||||
|
||||
user_input = {
|
||||
CONF_PASSWORD: NEW_PASSWORD,
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_SSL: True,
|
||||
CONF_VERIFY_SSL: True,
|
||||
},
|
||||
@@ -546,8 +546,8 @@ async def test_reconfigure_unique_id_mismatch(
|
||||
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
||||
assert updated_entry.data[CONF_PASSWORD] == MOCK_CONFIG[CONF_PASSWORD]
|
||||
assert (
|
||||
updated_entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_SSL]
|
||||
== MOCK_CONFIG[SECTION_ADDITIONAL_SETTINGS][CONF_SSL]
|
||||
updated_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL]
|
||||
== MOCK_CONFIG[SECTION_ADVANCED_SETTINGS][CONF_SSL]
|
||||
)
|
||||
|
||||
|
||||
@@ -611,7 +611,7 @@ async def test_discover_flow_one_device_found(
|
||||
{
|
||||
CONF_USERNAME: DEFAULT_USERNAME,
|
||||
CONF_PASSWORD: "test-password",
|
||||
SECTION_ADDITIONAL_SETTINGS: MOCK_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS: MOCK_ADVANCED_SETTINGS,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -687,7 +687,7 @@ async def test_discover_flow_multiple_devices_found(
|
||||
{
|
||||
CONF_USERNAME: DEFAULT_USERNAME,
|
||||
CONF_PASSWORD: "test-password",
|
||||
SECTION_ADDITIONAL_SETTINGS: MOCK_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS: MOCK_ADVANCED_SETTINGS,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -785,7 +785,7 @@ async def test_configure_device_flow_exceptions(
|
||||
{
|
||||
CONF_USERNAME: "wrong-user",
|
||||
CONF_PASSWORD: "wrong-password",
|
||||
SECTION_ADDITIONAL_SETTINGS: MOCK_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS: MOCK_ADVANCED_SETTINGS,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -801,7 +801,7 @@ async def test_configure_device_flow_exceptions(
|
||||
{
|
||||
CONF_USERNAME: DEFAULT_USERNAME,
|
||||
CONF_PASSWORD: "some-password",
|
||||
SECTION_ADDITIONAL_SETTINGS: MOCK_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS: MOCK_ADVANCED_SETTINGS,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.airos.const import (
|
||||
DEFAULT_SSL,
|
||||
DEFAULT_VERIFY_SSL,
|
||||
DOMAIN,
|
||||
SECTION_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from homeassistant.components.airos.coordinator import async_fetch_airos_data
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
@@ -46,7 +46,7 @@ MOCK_CONFIG_PLAIN = {
|
||||
CONF_HOST: "1.1.1.1",
|
||||
CONF_USERNAME: "ubnt",
|
||||
CONF_PASSWORD: "test-password",
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_SSL: False,
|
||||
CONF_VERIFY_SSL: False,
|
||||
},
|
||||
@@ -56,7 +56,7 @@ MOCK_CONFIG_V1_2 = {
|
||||
CONF_HOST: "1.1.1.1",
|
||||
CONF_USERNAME: "ubnt",
|
||||
CONF_PASSWORD: "test-password",
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_SSL: DEFAULT_SSL,
|
||||
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
|
||||
},
|
||||
@@ -86,8 +86,8 @@ async def test_setup_entry_with_default_ssl(
|
||||
use_ssl=DEFAULT_SSL,
|
||||
)
|
||||
|
||||
assert mock_config_entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_SSL] is True
|
||||
assert mock_config_entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_VERIFY_SSL] is False
|
||||
assert mock_config_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL] is True
|
||||
assert mock_config_entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL] is False
|
||||
|
||||
|
||||
async def test_setup_entry_without_ssl(
|
||||
@@ -120,8 +120,8 @@ async def test_setup_entry_without_ssl(
|
||||
use_ssl=False,
|
||||
)
|
||||
|
||||
assert entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_SSL] is False
|
||||
assert entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_VERIFY_SSL] is False
|
||||
assert entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL] is False
|
||||
assert entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL] is False
|
||||
|
||||
|
||||
async def test_ssl_migrate_entry(
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[button.edifier_r1700bt_bluetooth-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': None,
|
||||
'entity_id': 'button.edifier_r1700bt_bluetooth',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Bluetooth',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Bluetooth',
|
||||
'platform': 'edifier_infrared',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'bluetooth',
|
||||
'unique_id': '01JTEST0000000000000000000_bluetooth',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[button.edifier_r1700bt_bluetooth-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Edifier R1700BT Bluetooth',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.edifier_r1700bt_bluetooth',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[button.edifier_r1700bt_fx_off-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': None,
|
||||
'entity_id': 'button.edifier_r1700bt_fx_off',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'FX off',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'FX off',
|
||||
'platform': 'edifier_infrared',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'fx_off',
|
||||
'unique_id': '01JTEST0000000000000000000_fx_off',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[button.edifier_r1700bt_fx_off-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Edifier R1700BT FX off',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.edifier_r1700bt_fx_off',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[button.edifier_r1700bt_fx_on-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': None,
|
||||
'entity_id': 'button.edifier_r1700bt_fx_on',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'FX on',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'FX on',
|
||||
'platform': 'edifier_infrared',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'fx_on',
|
||||
'unique_id': '01JTEST0000000000000000000_fx_on',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[button.edifier_r1700bt_fx_on-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Edifier R1700BT FX on',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.edifier_r1700bt_fx_on',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[button.edifier_r1700bt_line_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': None,
|
||||
'entity_id': 'button.edifier_r1700bt_line_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Line 1',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Line 1',
|
||||
'platform': 'edifier_infrared',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'line_1',
|
||||
'unique_id': '01JTEST0000000000000000000_line_1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[button.edifier_r1700bt_line_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Edifier R1700BT Line 1',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.edifier_r1700bt_line_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[button.edifier_r1700bt_line_2-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': None,
|
||||
'entity_id': 'button.edifier_r1700bt_line_2',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Line 2',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Line 2',
|
||||
'platform': 'edifier_infrared',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'line_2',
|
||||
'unique_id': '01JTEST0000000000000000000_line_2',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[button.edifier_r1700bt_line_2-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Edifier R1700BT Line 2',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.edifier_r1700bt_line_2',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Tests for the Edifier Infrared button platform."""
|
||||
|
||||
from infrared_protocols.codes.edifier.r1700bt import EdifierR1700BTCode
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.components.common import assert_availability_follows_source_entity
|
||||
from tests.components.infrared import EMITTER_ENTITY_ID
|
||||
from tests.components.infrared.common import MockInfraredEmitterEntity
|
||||
|
||||
BLUETOOTH_BUTTON_ENTITY_ID = "button.edifier_r1700bt_bluetooth"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Return platforms to set up."""
|
||||
return [Platform.BUTTON]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the button entities are created with correct attributes."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "expected_code"),
|
||||
[
|
||||
("button.edifier_r1700bt_bluetooth", EdifierR1700BTCode.BLUETOOTH),
|
||||
("button.edifier_r1700bt_line_1", EdifierR1700BTCode.LINE_1),
|
||||
("button.edifier_r1700bt_line_2", EdifierR1700BTCode.LINE_2),
|
||||
("button.edifier_r1700bt_fx_on", EdifierR1700BTCode.FX_ON),
|
||||
("button.edifier_r1700bt_fx_off", EdifierR1700BTCode.FX_OFF),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_button_press_sends_correct_code(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
entity_id: str,
|
||||
expected_code: EdifierR1700BTCode,
|
||||
) -> None:
|
||||
"""Test each button press sends the correct IR code."""
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_infrared_emitter_entity.send_command_calls) == 1
|
||||
assert mock_infrared_emitter_entity.send_command_calls[0] == expected_code
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_button_availability_follows_ir_entity(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test button becomes unavailable when IR entity is unavailable."""
|
||||
await assert_availability_follows_source_entity(
|
||||
hass, BLUETOOTH_BUTTON_ENTITY_ID, EMITTER_ENTITY_ID
|
||||
)
|
||||
@@ -1462,44 +1462,6 @@ async def test_state_for_invalid_template(
|
||||
assert not test.async_check()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("extra_config", "error"),
|
||||
[
|
||||
pytest.param(
|
||||
{"attribute": "battery_level"},
|
||||
r"Cannot use 'for' with an attribute",
|
||||
id="attribute",
|
||||
),
|
||||
pytest.param(
|
||||
{"state": ["100", "200"]},
|
||||
r"Cannot use 'for' with a list of states",
|
||||
id="list_of_states",
|
||||
),
|
||||
pytest.param(
|
||||
{"state": "input_number.threshold"},
|
||||
r"Cannot use 'for' with a state referencing an entity",
|
||||
id="state_from_entity",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_state_for_not_allowed(extra_config: dict[str, Any], error: str) -> None:
|
||||
"""Test state condition rejects `for` with unsupported `state`/`attribute`.
|
||||
|
||||
`for` is anchored to the entity's last_changed, which only advances on state
|
||||
changes and reflects a single current state. It therefore cannot be combined
|
||||
with an attribute, a list of states, or a state resolved from another entity.
|
||||
"""
|
||||
config = {
|
||||
"condition": "state",
|
||||
"entity_id": "sensor.temperature",
|
||||
"state": "100",
|
||||
"for": {"seconds": 5},
|
||||
**extra_config,
|
||||
}
|
||||
with pytest.raises(vol.Invalid, match=error):
|
||||
cv.CONDITION_SCHEMA(config)
|
||||
|
||||
|
||||
async def test_state_unknown_attribute(hass: HomeAssistant) -> None:
|
||||
"""Test that state returns False on unknown attribute."""
|
||||
# Unknown attribute
|
||||
@@ -1532,44 +1494,6 @@ async def test_state_unknown_attribute(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("req_state", "attribute_value", "expected"),
|
||||
[
|
||||
# A list `state` is matched as alternatives, so the attribute value must
|
||||
# equal one of the items; the list itself is never compared as a whole.
|
||||
pytest.param(["a", "b"], "a", True, id="item_in_list"),
|
||||
pytest.param(["a", "b"], ["a", "b"], False, id="list_is_not_an_item"),
|
||||
# Nesting the list makes the list value itself one of the items to match.
|
||||
pytest.param([["a", "b"]], ["a", "b"], True, id="list_in_list_of_lists"),
|
||||
pytest.param([["a", "b"]], "a", False, id="scalar_not_in_list_of_lists"),
|
||||
],
|
||||
)
|
||||
async def test_state_attribute_list_matching(
|
||||
hass: HomeAssistant,
|
||||
req_state: list[Any],
|
||||
attribute_value: str | list[str],
|
||||
expected: bool,
|
||||
) -> None:
|
||||
"""Test how a state-attribute condition matches against a list `state`.
|
||||
|
||||
A list `state` is treated as alternatives (match any item), so a list-valued
|
||||
attribute only matches when the list is nested as an item of `state`. This
|
||||
documents the current behavior; the implementation is unchanged.
|
||||
"""
|
||||
config = {
|
||||
"condition": "state",
|
||||
"entity_id": "sensor.test",
|
||||
"attribute": "options",
|
||||
"state": req_state,
|
||||
}
|
||||
config = cv.CONDITION_SCHEMA(config)
|
||||
config = await condition.async_validate_condition_config(hass, config)
|
||||
test = await condition.async_from_config(hass, config)
|
||||
|
||||
hass.states.async_set("sensor.test", "on", {"options": attribute_value})
|
||||
assert test.async_check() is expected
|
||||
|
||||
|
||||
async def test_state_multiple_entities(hass: HomeAssistant) -> None:
|
||||
"""Test with multiple entities in condition."""
|
||||
config = {
|
||||
|
||||
Reference in New Issue
Block a user