Compare commits

..

12 Commits

Author SHA1 Message Date
Ariel Ebersberger 3cb69dff34 Rename "advanced_options" section to "additional_options" in SQL 2026-06-29 13:36:16 +02:00
Ariel Ebersberger ff460901b7 Rename "advanced_options" section to "additional_options" in DNS IP (#175105) 2026-06-29 13:14:58 +02:00
Ariel Ebersberger 30512f08a8 Rename "advanced" step to "additional" in OpenAI Conversation (#175104) 2026-06-29 13:14:19 +02:00
Ariel Ebersberger 9dd1a59d50 Rename "advanced" step to "additional" in Anthropic (#175101) 2026-06-29 13:14:01 +02:00
TrojanHorsePower 91aded4474 Update vsure to 2.8.0 (#175060) 2026-06-29 11:46:21 +02:00
Erwin Douna 543eab3354 Portainer add utility.py for duplicate code (#175082) 2026-06-29 11:04:02 +02:00
Ariel Ebersberger 47b331a869 Rename "Advanced settings" title in OpenAI Conversation (#175096) 2026-06-29 11:00:21 +02:00
Ariel Ebersberger 696dd45803 Rename "Advanced settings" title in Anthropic (#175095) 2026-06-29 10:59:53 +02:00
Ariel Ebersberger f92239877f Rename "Advanced migration/setup" options in ZHA (#175094) 2026-06-29 10:59:35 +02:00
Ariel Ebersberger 45ceb13937 Rename "Advanced settings" sections in Scrape (#175098) 2026-06-29 10:58:26 +02:00
Ariel Ebersberger c5aeee8097 Rename "(advanced)" service name in Music Assistant (#175097) 2026-06-29 10:58:04 +02:00
Erik Montnemery bfc750b608 Tell bots that we like small try-clauses (#174654) 2026-06-29 11:16:46 +03:00
31 changed files with 299 additions and 803 deletions
+1
View File
@@ -54,3 +54,4 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- Keep comments concise. Prefer one short line stating the non-obvious constraint, or no comment at all.
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why (non-obvious constraints, surprising behavior, or workarounds), never what. Never add comments that justify a change by referencing what the code looked like before.
- Do not add section or divider comments (e.g. `# --- XYZ Triggers ---`) inside or outside of functions, since those can easily become stale and be misleading.
- When catching exceptions, try-clauses should be as small as possible, i.e. avoid wrapping large blocks of code in a try-clause, and avoid catching exceptions from functions that are not expected to raise them.
+1
View File
@@ -43,3 +43,4 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- Keep comments concise. Prefer one short line stating the non-obvious constraint, or no comment at all.
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why (non-obvious constraints, surprising behavior, or workarounds), never what. Never add comments that justify a change by referencing what the code looked like before.
- Do not add section or divider comments (e.g. `# --- XYZ Triggers ---`) inside or outside of functions, since those can easily become stale and be misleading.
- When catching exceptions, try-clauses should be as small as possible, i.e. avoid wrapping large blocks of code in a try-clause, and avoid catching exceptions from functions that are not expected to raise them.
@@ -298,7 +298,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
):
self.options.pop(CONF_LLM_HASS_API)
if not errors:
return await self.async_step_advanced()
return await self.async_step_additional()
return self.async_show_form(
step_id="init",
@@ -308,10 +308,10 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
errors=errors or None,
)
async def async_step_advanced(
async def async_step_additional(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Manage advanced options."""
"""Manage additional options."""
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {}
@@ -360,7 +360,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
return await self.async_step_model()
return self.async_show_form(
step_id="advanced",
step_id="additional",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(step_schema), self.options
),
@@ -48,16 +48,16 @@
"user": "Add AI task"
},
"step": {
"advanced": {
"additional": {
"data": {
"chat_model": "[%key:common::generic::model%]",
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data::prompt_caching%]"
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::additional::data::prompt_caching%]"
},
"data_description": {
"chat_model": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::chat_model%]",
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::prompt_caching%]"
"chat_model": "[%key:component::anthropic::config_subentries::conversation::step::additional::data_description::chat_model%]",
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::additional::data_description::prompt_caching%]"
},
"title": "[%key:component::anthropic::config_subentries::conversation::step::advanced::title%]"
"title": "[%key:component::anthropic::config_subentries::conversation::step::additional::title%]"
},
"init": {
"data": {
@@ -115,7 +115,7 @@
"user": "Add conversation agent"
},
"step": {
"advanced": {
"additional": {
"data": {
"chat_model": "[%key:common::generic::model%]",
"prompt_caching": "Caching strategy"
@@ -124,7 +124,7 @@
"chat_model": "The model to serve the responses.",
"prompt_caching": "Optimize your API cost and response times based on your usage."
},
"title": "Advanced settings"
"title": "Additional settings"
},
"init": {
"data": {
@@ -20,7 +20,7 @@ from homeassistant.data_entry_flow import SectionConfig, section
from homeassistant.helpers import config_validation as cv
from .const import (
CONF_ADVANCED_OPTIONS,
CONF_ADDITIONAL_OPTIONS,
CONF_HOSTNAME,
CONF_IPV4,
CONF_IPV6,
@@ -39,7 +39,7 @@ from .const import (
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string,
vol.Required(CONF_ADVANCED_OPTIONS): section(
vol.Required(CONF_ADDITIONAL_OPTIONS): section(
vol.Schema(
{
vol.Optional(CONF_RESOLVER): cv.string,
@@ -117,13 +117,13 @@ class DnsIPConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input:
hostname = user_input[CONF_HOSTNAME]
name = DEFAULT_NAME if hostname == DEFAULT_HOSTNAME else hostname
advanced_options = user_input[CONF_ADVANCED_OPTIONS]
resolver = advanced_options.get(CONF_RESOLVER, DEFAULT_RESOLVER)
resolver_ipv6 = advanced_options.get(
additional_options = user_input[CONF_ADDITIONAL_OPTIONS]
resolver = additional_options.get(CONF_RESOLVER, DEFAULT_RESOLVER)
resolver_ipv6 = additional_options.get(
CONF_RESOLVER_IPV6, DEFAULT_RESOLVER_IPV6
)
port = advanced_options.get(CONF_PORT, DEFAULT_PORT)
port_ipv6 = advanced_options.get(CONF_PORT_IPV6, DEFAULT_PORT)
port = additional_options.get(CONF_PORT, DEFAULT_PORT)
port_ipv6 = additional_options.get(CONF_PORT_IPV6, DEFAULT_PORT)
validate = await async_validate_hostname(
hostname, resolver, resolver_ipv6, port, port_ipv6
+1 -1
View File
@@ -12,7 +12,7 @@ CONF_PORT_IPV6 = "port_ipv6"
CONF_IPV4 = "ipv4"
CONF_IPV6 = "ipv6"
CONF_IPV6_V4 = "ipv6_v4"
CONF_ADVANCED_OPTIONS = "advanced_options"
CONF_ADDITIONAL_OPTIONS = "additional_options"
DEFAULT_HOSTNAME = "myip.opendns.com"
DEFAULT_IPV6 = False
+9 -9
View File
@@ -15,7 +15,7 @@
"hostname": "The hostname for which to perform the DNS query."
},
"sections": {
"advanced_options": {
"additional_options": {
"data": {
"port": "IPv4 port",
"port_ipv6": "IPv6 port",
@@ -63,16 +63,16 @@
"step": {
"init": {
"data": {
"port": "[%key:component::dnsip::config::step::user::sections::advanced_options::data::port%]",
"port_ipv6": "[%key:component::dnsip::config::step::user::sections::advanced_options::data::port_ipv6%]",
"resolver": "[%key:component::dnsip::config::step::user::sections::advanced_options::data::resolver%]",
"resolver_ipv6": "[%key:component::dnsip::config::step::user::sections::advanced_options::data::resolver_ipv6%]"
"port": "[%key:component::dnsip::config::step::user::sections::additional_options::data::port%]",
"port_ipv6": "[%key:component::dnsip::config::step::user::sections::additional_options::data::port_ipv6%]",
"resolver": "[%key:component::dnsip::config::step::user::sections::additional_options::data::resolver%]",
"resolver_ipv6": "[%key:component::dnsip::config::step::user::sections::additional_options::data::resolver_ipv6%]"
},
"data_description": {
"port": "[%key:component::dnsip::config::step::user::sections::advanced_options::data_description::port%]",
"port_ipv6": "[%key:component::dnsip::config::step::user::sections::advanced_options::data_description::port_ipv6%]",
"resolver": "[%key:component::dnsip::config::step::user::sections::advanced_options::data_description::resolver%]",
"resolver_ipv6": "[%key:component::dnsip::config::step::user::sections::advanced_options::data_description::resolver_ipv6%]"
"port": "[%key:component::dnsip::config::step::user::sections::additional_options::data_description::port%]",
"port_ipv6": "[%key:component::dnsip::config::step::user::sections::additional_options::data_description::port_ipv6%]",
"resolver": "[%key:component::dnsip::config::step::user::sections::additional_options::data_description::resolver%]",
"resolver_ipv6": "[%key:component::dnsip::config::step::user::sections::additional_options::data_description::resolver_ipv6%]"
},
"description": "Optionally change resolvers and ports."
}
@@ -374,7 +374,7 @@
},
"get_queue": {
"description": "Retrieves the details of the currently active queue of a Music Assistant player.",
"name": "Get playerQueue details (advanced)"
"name": "Get playerQueue details"
},
"play_announcement": {
"description": "Plays an announcement on a Music Assistant player with more fine-grained control options.",
@@ -326,7 +326,7 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
options.update(user_input)
if CONF_LLM_HASS_API in options and CONF_LLM_HASS_API not in user_input:
options.pop(CONF_LLM_HASS_API)
return await self.async_step_advanced()
return await self.async_step_additional()
return self.async_show_form(
step_id="init",
@@ -335,10 +335,10 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
),
)
async def async_step_advanced(
async def async_step_additional(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Manage advanced options."""
"""Manage additional options."""
options = self.options
errors: dict[str, str] = {}
@@ -374,7 +374,7 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
return await self.async_step_model()
return self.async_show_form(
step_id="advanced",
step_id="additional",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(step_schema), options
),
@@ -47,18 +47,18 @@
"user": "Add AI task"
},
"step": {
"advanced": {
"additional": {
"data": {
"chat_model": "[%key:common::generic::model%]",
"max_tokens": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::max_tokens%]",
"store_responses": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::store_responses%]",
"temperature": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::temperature%]",
"top_p": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::top_p%]"
"max_tokens": "[%key:component::openai_conversation::config_subentries::conversation::step::additional::data::max_tokens%]",
"store_responses": "[%key:component::openai_conversation::config_subentries::conversation::step::additional::data::store_responses%]",
"temperature": "[%key:component::openai_conversation::config_subentries::conversation::step::additional::data::temperature%]",
"top_p": "[%key:component::openai_conversation::config_subentries::conversation::step::additional::data::top_p%]"
},
"data_description": {
"store_responses": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data_description::store_responses%]"
"store_responses": "[%key:component::openai_conversation::config_subentries::conversation::step::additional::data_description::store_responses%]"
},
"title": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::title%]"
"title": "[%key:component::openai_conversation::config_subentries::conversation::step::additional::title%]"
},
"init": {
"data": {
@@ -109,7 +109,7 @@
"user": "Add conversation agent"
},
"step": {
"advanced": {
"additional": {
"data": {
"chat_model": "[%key:common::generic::model%]",
"max_tokens": "Maximum tokens to return in response",
@@ -120,7 +120,7 @@
"data_description": {
"store_responses": "If enabled, requests and responses are stored by OpenAI and visible in your OpenAI dashboard logs"
},
"title": "Advanced settings"
"title": "Additional settings"
},
"init": {
"data": {
@@ -34,6 +34,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
from .util import sanitize_container_name
type PortainerConfigEntry = ConfigEntry[PortainerCoordinator]
@@ -263,7 +264,7 @@ class PortainerCoordinator(
# Map containers, started and stopped
for container in containers:
container_name = self._get_container_name(container.names[0])
container_name = sanitize_container_name(container.names[0])
prev_container = (
prev_endpoint.containers.get(container_name)
if prev_endpoint
@@ -313,7 +314,7 @@ class PortainerCoordinator(
container_stats = dict(
zip(
(
self._get_container_name(container.names[0])
sanitize_container_name(container.names[0])
for container in active_containers
),
await asyncio.gather(
@@ -431,10 +432,6 @@ class PortainerCoordinator(
for stack_callback in self.new_stacks_callbacks:
stack_callback(new_stack_data)
def _get_container_name(self, container_name: str) -> str:
"""Sanitize to get a proper container name."""
return container_name.replace("/", " ").strip()
class PortainerDockerDiskSpaceCoordinator(
PortainerBaseCoordinator[dict[int, DockerSystemDF]]
+2 -1
View File
@@ -19,6 +19,7 @@ from .coordinator import (
PortainerStackData,
PortainerVolumeData,
)
from .util import sanitize_container_name
class PortainerCoordinatorEntity(CoordinatorEntity[PortainerCoordinator]):
@@ -95,7 +96,7 @@ class PortainerContainerEntity(PortainerCoordinatorEntity):
# According to Docker's API docs, the first name is unique
names = self._device_info.container.names
assert names, "Container names list unexpectedly empty"
self.device_name = names[0].replace("/", " ").strip()
self.device_name = sanitize_container_name(names[0])
self._attr_device_info = DeviceInfo(
identifiers={
@@ -0,0 +1,6 @@
"""Utility functions for the Portainer integration."""
def sanitize_container_name(container_name: str) -> str:
"""Sanitize to get a proper container name."""
return container_name.replace("/", " ").strip()
+4 -4
View File
@@ -32,8 +32,8 @@
"timeout": "Timeout for connection to website.",
"verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed."
},
"description": "Provide additional advanced settings for the resource.",
"name": "Advanced settings"
"description": "Provide additional settings for the resource.",
"name": "Additional settings"
},
"auth": {
"data": {
@@ -117,8 +117,8 @@
"unit_of_measurement": "Choose unit of measurement or create your own.",
"value_template": "Defines a template to get the state of the sensor."
},
"description": "Provide additional advanced settings for the sensor.",
"name": "Advanced settings"
"description": "Provide additional settings for the sensor.",
"name": "Additional settings"
}
}
}
+12 -3
View File
@@ -31,7 +31,7 @@ from homeassistant.helpers.trigger_template_entity import (
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_ADVANCED_OPTIONS,
CONF_ADDITIONAL_OPTIONS,
CONF_COLUMN_NAME,
CONF_QUERY,
DOMAIN,
@@ -120,7 +120,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
new_options[CONF_COLUMN_NAME] = old_options.get(CONF_COLUMN_NAME)
new_options[CONF_QUERY] = old_options.get(CONF_QUERY)
new_options[CONF_ADVANCED_OPTIONS] = {}
new_options[CONF_ADDITIONAL_OPTIONS] = {}
for key in (
CONF_VALUE_TEMPLATE,
@@ -129,12 +129,21 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
CONF_STATE_CLASS,
):
if (value := old_options.get(key)) is not None:
new_options[CONF_ADVANCED_OPTIONS][key] = value
new_options[CONF_ADDITIONAL_OPTIONS][key] = value
hass.config_entries.async_update_entry(
entry, data=new_data, options=new_options, version=2
)
if entry.version == 2 and entry.minor_version < 2:
new_options = {**entry.options}
# The "advanced_options" section was renamed to "additional_options"
if (additional := new_options.pop("advanced_options", None)) is not None:
new_options[CONF_ADDITIONAL_OPTIONS] = additional
hass.config_entries.async_update_entry(
entry, options=new_options, minor_version=2
)
_LOGGER.debug(
"Migration to version %s.%s successful",
entry.version,
+9 -8
View File
@@ -32,7 +32,7 @@ from homeassistant.data_entry_flow import section
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import selector
from .const import CONF_ADVANCED_OPTIONS, CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
from .const import CONF_ADDITIONAL_OPTIONS, CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
from .util import (
EmptyQueryError,
InvalidSqlQuery,
@@ -50,7 +50,7 @@ OPTIONS_SCHEMA: vol.Schema = vol.Schema(
{
vol.Required(CONF_QUERY): selector.TemplateSelector(),
vol.Required(CONF_COLUMN_NAME): selector.TextSelector(),
vol.Required(CONF_ADVANCED_OPTIONS): section(
vol.Required(CONF_ADDITIONAL_OPTIONS): section(
vol.Schema(
{
vol.Optional(CONF_VALUE_TEMPLATE): selector.TemplateSelector(),
@@ -165,6 +165,7 @@ class SQLConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for SQL integration."""
VERSION = 2
MINOR_VERSION = 2
data: dict[str, Any]
@@ -239,12 +240,12 @@ class SQLConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("Invalid query: %s", err)
errors["query"] = "query_invalid"
mod_advanced_options = {
mod_additional_options = {
k: v
for k, v in user_input[CONF_ADVANCED_OPTIONS].items()
for k, v in user_input[CONF_ADDITIONAL_OPTIONS].items()
if v is not None
}
user_input[CONF_ADVANCED_OPTIONS] = mod_advanced_options
user_input[CONF_ADDITIONAL_OPTIONS] = mod_additional_options
if not errors:
name = self.data[CONF_NAME]
@@ -305,12 +306,12 @@ class SQLOptionsFlowHandler(OptionsFlowWithReload):
recorder_db,
)
mod_advanced_options = {
mod_additional_options = {
k: v
for k, v in user_input[CONF_ADVANCED_OPTIONS].items()
for k, v in user_input[CONF_ADDITIONAL_OPTIONS].items()
if v is not None
}
user_input[CONF_ADVANCED_OPTIONS] = mod_advanced_options
user_input[CONF_ADDITIONAL_OPTIONS] = mod_additional_options
return self.async_create_entry(
data=user_input,
+1 -1
View File
@@ -9,5 +9,5 @@ PLATFORMS = [Platform.SENSOR]
CONF_COLUMN_NAME = "column"
CONF_QUERY = "query"
CONF_ADVANCED_OPTIONS = "advanced_options"
CONF_ADDITIONAL_OPTIONS = "additional_options"
DB_URL_RE = re.compile("//.*:.*@")
+6 -4
View File
@@ -36,7 +36,7 @@ from homeassistant.helpers.trigger_template_entity import (
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_ADVANCED_OPTIONS, CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
from .const import CONF_ADDITIONAL_OPTIONS, CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
from .util import (
InvalidSqlQuery,
async_create_sessionmaker,
@@ -115,7 +115,9 @@ async def async_setup_entry(
db_url: str = resolve_db_url(hass, entry.data.get(CONF_DB_URL))
name: str = entry.title
query_str: str = entry.options[CONF_QUERY]
template: str | None = entry.options[CONF_ADVANCED_OPTIONS].get(CONF_VALUE_TEMPLATE)
template: str | None = entry.options[CONF_ADDITIONAL_OPTIONS].get(
CONF_VALUE_TEMPLATE
)
column_name: str = entry.options[CONF_COLUMN_NAME]
query_template: ValueTemplate | None = None
@@ -136,9 +138,9 @@ async def async_setup_entry(
name_template = Template(name, hass)
trigger_entity_config = {CONF_NAME: name_template, CONF_UNIQUE_ID: entry.entry_id}
for key in TRIGGER_ENTITY_OPTIONS:
if key not in entry.options[CONF_ADVANCED_OPTIONS]:
if key not in entry.options[CONF_ADDITIONAL_OPTIONS]:
continue
trigger_entity_config[key] = entry.options[CONF_ADVANCED_OPTIONS][key]
trigger_entity_config[key] = entry.options[CONF_ADDITIONAL_OPTIONS][key]
await async_setup_sensor(
hass,
+12 -12
View File
@@ -21,7 +21,7 @@
"query": "Query to run, needs to start with 'SELECT'"
},
"sections": {
"advanced_options": {
"additional_options": {
"data": {
"device_class": "Device class",
"state_class": "State class",
@@ -87,21 +87,21 @@
"query": "[%key:component::sql::config::step::options::data_description::query%]"
},
"sections": {
"advanced_options": {
"additional_options": {
"data": {
"device_class": "[%key:component::sql::config::step::options::sections::advanced_options::data::device_class%]",
"state_class": "[%key:component::sql::config::step::options::sections::advanced_options::data::state_class%]",
"unit_of_measurement": "[%key:component::sql::config::step::options::sections::advanced_options::data::unit_of_measurement%]",
"value_template": "[%key:component::sql::config::step::options::sections::advanced_options::data::value_template%]"
"device_class": "[%key:component::sql::config::step::options::sections::additional_options::data::device_class%]",
"state_class": "[%key:component::sql::config::step::options::sections::additional_options::data::state_class%]",
"unit_of_measurement": "[%key:component::sql::config::step::options::sections::additional_options::data::unit_of_measurement%]",
"value_template": "[%key:component::sql::config::step::options::sections::additional_options::data::value_template%]"
},
"data_description": {
"device_class": "[%key:component::sql::config::step::options::sections::advanced_options::data_description::device_class%]",
"state_class": "[%key:component::sql::config::step::options::sections::advanced_options::data_description::state_class%]",
"unit_of_measurement": "[%key:component::sql::config::step::options::sections::advanced_options::data_description::unit_of_measurement%]",
"value_template": "[%key:component::sql::config::step::options::sections::advanced_options::data_description::value_template%]"
"device_class": "[%key:component::sql::config::step::options::sections::additional_options::data_description::device_class%]",
"state_class": "[%key:component::sql::config::step::options::sections::additional_options::data_description::state_class%]",
"unit_of_measurement": "[%key:component::sql::config::step::options::sections::additional_options::data_description::unit_of_measurement%]",
"value_template": "[%key:component::sql::config::step::options::sections::additional_options::data_description::value_template%]"
},
"description": "[%key:component::sql::config::step::options::sections::advanced_options::name%]",
"name": "[%key:component::sql::config::step::options::sections::advanced_options::name%]"
"description": "[%key:component::sql::config::step::options::sections::additional_options::name%]",
"name": "[%key:component::sql::config::step::options::sections::additional_options::name%]"
}
}
}
@@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["verisure"],
"requirements": ["vsure==2.7.1"]
"requirements": ["vsure==2.8.0"]
}
+2 -2
View File
@@ -55,7 +55,7 @@
"migration_strategy_recommended": "This is the quickest option to migrate to a new adapter."
},
"menu_options": {
"migration_strategy_advanced": "Advanced migration",
"migration_strategy_advanced": "Migrate manually",
"migration_strategy_recommended": "Migrate automatically (recommended)"
},
"title": "Migrate to a new adapter"
@@ -74,7 +74,7 @@
"setup_strategy_recommended": "This is the quickest option to create a new network and get started."
},
"menu_options": {
"setup_strategy_advanced": "Advanced setup",
"setup_strategy_advanced": "Set up manually",
"setup_strategy_recommended": "Set up automatically (recommended)"
},
"title": "Set up Zigbee"
+18 -176
View File
@@ -441,18 +441,6 @@ class EntityTriggerBase(Trigger):
"""
return True
def _is_valid_to_state(
self, to_state: State, report_not_triggered: _NotTriggeredReasonReporter
) -> bool:
"""Check if to_state can fire the trigger, optionally reporting why if it can't.
When the state cannot fire the trigger, subclasses may use
`report_not_triggered` to record an interesting reason - e.g. a
non-numeric value or an unsupported unit - in the automation trace.
The base implementation never reports.
"""
return self.is_valid_state(to_state)
def _should_include(self, state: State) -> bool:
"""Check if an entity should participate in all/count checks.
@@ -604,23 +592,11 @@ class EntityTriggerBase(Trigger):
if not from_state or not to_state:
return
@callback
def report_not_triggered(reason: str, /, **data: Any) -> None:
"""Report why this evaluated change did not fire the trigger."""
if did_not_trigger is None:
return
did_not_trigger(
NotTriggeredInfo(reason=reason, data=data), event.context
)
# The trigger should never fire if the new state is excluded.
if to_state.state in self._excluded_states:
return
# The trigger should never fire if the new state is not a target
# state. Interesting reasons (e.g. a non-numeric value or an
# unsupported unit) are reported for the trace.
if not self._is_valid_to_state(to_state, report_not_triggered):
# The trigger should never fire if the new state is excluded
# or not a target state.
if to_state.state in self._excluded_states or not self.is_valid_state(
to_state
):
return
# The trigger should never fire if the origin state is excluded
@@ -827,11 +803,7 @@ class EntityNumericalStateTriggerBase(EntityTriggerBase):
return True
return unit == self._valid_unit
def _get_threshold_value(
self,
threshold: ThresholdConfig | None,
report_not_triggered: _NotTriggeredReasonReporter | None = None,
) -> float | None:
def _get_threshold_value(self, threshold: ThresholdConfig | None) -> float | None:
"""Get threshold value from float or entity state."""
if threshold is None:
return None
@@ -840,32 +812,14 @@ class EntityNumericalStateTriggerBase(EntityTriggerBase):
if not (state := self._hass.states.get(threshold.entity)): # type: ignore[arg-type]
# Entity not found
if report_not_triggered is not None:
report_not_triggered(
"threshold_entity_not_found",
entity_id=threshold.entity,
)
return None
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if not self._is_valid_unit(unit):
if not self._is_valid_unit(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)):
# Entity unit does not match the expected unit
if report_not_triggered is not None:
report_not_triggered(
"threshold_unit_not_supported",
entity_id=threshold.entity,
unit=unit,
)
return None
try:
return float(state.state)
except TypeError, ValueError:
# Entity state is not a valid number
if report_not_triggered is not None:
report_not_triggered(
"threshold_value_not_numeric",
entity_id=threshold.entity,
value=state.state,
)
return None
@override
@@ -887,53 +841,10 @@ class EntityNumericalStateTriggerBase(EntityTriggerBase):
return None
@override
def _is_valid_to_state(
self, to_state: State, report_not_triggered: _NotTriggeredReasonReporter
) -> bool:
"""Check if to_state can fire, reporting non-numeric / unit reasons."""
return self.is_valid_state(to_state, report_not_triggered)
def _report_tracked_value_problem(
self, state: State, report_not_triggered: _NotTriggeredReasonReporter
) -> None:
"""Report why `_get_tracked_value` rejected this state.
Called only when the tracked value is invalid. It mirrors the failure
modes of `_get_tracked_value` - which integrations override, so the
reason is derived here rather than reported inline: a state-sourced
value with an unsupported unit, otherwise a value that is not a number.
"""
domain_spec = self._domain_specs[state.domain]
raw_value: Any
if domain_spec.value_source is None:
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if not self._is_valid_unit(unit):
report_not_triggered(
"entity_unit_not_supported",
entity_id=state.entity_id,
unit=unit,
)
return
raw_value = state.state
else:
raw_value = state.attributes.get(domain_spec.value_source)
report_not_triggered(
"entity_value_not_numeric",
entity_id=state.entity_id,
value=raw_value,
)
@override
def is_valid_state(
self,
state: State,
report_not_triggered: _NotTriggeredReasonReporter | None = None,
) -> bool:
def is_valid_state(self, state: State) -> bool:
"""Check if the new state or state attribute matches the expected one."""
# Handle missing or None value case first to avoid expensive exceptions
if (current_value := self._get_tracked_value(state)) is None:
if report_not_triggered is not None:
self._report_tracked_value_problem(state, report_not_triggered)
return False
if self._threshold_type == NumericThresholdType.ANY:
@@ -942,32 +853,20 @@ class EntityNumericalStateTriggerBase(EntityTriggerBase):
return True
if self._threshold_type == NumericThresholdType.ABOVE:
if (
limit := self._get_threshold_value(self.threshold, report_not_triggered)
) is None:
if (limit := self._get_threshold_value(self.threshold)) is None:
# Entity not found or invalid number, don't trigger
return False
return current_value > limit
if self._threshold_type == NumericThresholdType.BELOW:
if (
limit := self._get_threshold_value(self.threshold, report_not_triggered)
) is None:
if (limit := self._get_threshold_value(self.threshold)) is None:
# Entity not found or invalid number, don't trigger
return False
return current_value < limit
# Mode is BETWEEN or OUTSIDE. Evaluate the lower limit first so at most
# one not-triggered reason is reported per change.
lower_limit = self._get_threshold_value(
self.lower_threshold, report_not_triggered
)
if lower_limit is None:
# Entity not found or invalid number, don't trigger
return False
upper_limit = self._get_threshold_value(
self.upper_threshold, report_not_triggered
)
if upper_limit is None:
# Mode is BETWEEN or OUTSIDE
lower_limit = self._get_threshold_value(self.lower_threshold)
upper_limit = self._get_threshold_value(self.upper_threshold)
if lower_limit is None or upper_limit is None:
# Entity not found or invalid number, don't trigger
return False
between = lower_limit <= current_value <= upper_limit
@@ -987,41 +886,7 @@ class EntityNumericalStateTriggerWithUnitBase(EntityNumericalStateTriggerBase):
return state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@override
def _report_tracked_value_problem(
self, state: State, report_not_triggered: _NotTriggeredReasonReporter
) -> None:
"""Report why `_get_tracked_value` rejected this state.
Mirrors the with-unit failure modes: a value that is not a number,
otherwise a unit that cannot be converted to the base unit.
"""
domain_spec = self._domain_specs[state.domain]
raw_value: Any
if domain_spec.value_source is None:
raw_value = state.state
else:
raw_value = state.attributes.get(domain_spec.value_source)
try:
float(raw_value)
except TypeError, ValueError:
report_not_triggered(
"entity_value_not_numeric",
entity_id=state.entity_id,
value=raw_value,
)
return
report_not_triggered(
"entity_unit_not_supported",
entity_id=state.entity_id,
unit=self._get_entity_unit(state),
)
@override
def _get_threshold_value(
self,
threshold: ThresholdConfig | None,
report_not_triggered: _NotTriggeredReasonReporter | None = None,
) -> float | None:
def _get_threshold_value(self, threshold: ThresholdConfig | None) -> float | None:
"""Get threshold value from float or entity state."""
if threshold is None:
return None
@@ -1034,35 +899,19 @@ class EntityNumericalStateTriggerWithUnitBase(EntityNumericalStateTriggerBase):
if not (state := self._hass.states.get(threshold.entity)): # type: ignore[arg-type]
# Entity not found
if report_not_triggered is not None:
report_not_triggered(
"threshold_entity_not_found",
entity_id=threshold.entity,
)
return None
try:
value = float(state.state)
except TypeError, ValueError:
# Entity state is not a valid number
if report_not_triggered is not None:
report_not_triggered(
"threshold_value_not_numeric",
entity_id=threshold.entity,
value=state.state,
)
return None
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
try:
return self._unit_converter.convert(value, unit, self._base_unit)
return self._unit_converter.convert(
value, state.attributes.get(ATTR_UNIT_OF_MEASUREMENT), self._base_unit
)
except HomeAssistantError:
# Unit conversion failed (i.e. incompatible units), treat as invalid number
if report_not_triggered is not None:
report_not_triggered(
"threshold_unit_not_supported",
entity_id=threshold.entity,
unit=unit,
)
return None
@override
@@ -1439,13 +1288,6 @@ class TriggerNotTriggeredReporter(Protocol):
"""Report that the trigger did not fire."""
class _NotTriggeredReasonReporter(Protocol):
"""Reports why an evaluated change did not fire an entity trigger."""
def __call__(self, reason: str, /, **data: Any) -> None:
"""Report, with diagnostic data, why the change did not fire."""
class TriggerNotTriggeredAction(Protocol):
"""Protocol type for the did_not_trigger consumer callback.
+1 -1
View File
@@ -3313,7 +3313,7 @@ volkszaehler==0.4.0
volvocarsapi==0.4.3
# homeassistant.components.verisure
vsure==2.7.1
vsure==2.8.0
# homeassistant.components.vasttrafik
vtjp==0.2.1
+12 -12
View File
@@ -275,9 +275,9 @@ async def test_subentry_options_thinking_budget_more_than_max(
},
)
assert options["type"] is FlowResultType.FORM
assert options["step_id"] == "advanced"
assert options["step_id"] == "additional"
# Configure advanced step
# Configure additional step
options = await hass.config_entries.subentries.async_configure(
options["flow_id"],
{"chat_model": "claude-sonnet-4-5"},
@@ -330,9 +330,9 @@ async def test_subentry_web_search_user_location(
},
)
assert options["type"] is FlowResultType.FORM
assert options["step_id"] == "advanced"
assert options["step_id"] == "additional"
# Configure advanced step
# Configure additional step
options = await hass.config_entries.subentries.async_configure(
options["flow_id"],
{
@@ -424,7 +424,7 @@ async def test_model_list(
},
)
assert options["type"] is FlowResultType.FORM
assert options["step_id"] == "advanced"
assert options["step_id"] == "additional"
assert options["data_schema"].schema["chat_model"].config["options"] == snapshot
@@ -447,9 +447,9 @@ async def test_invalid_model(
},
)
assert options["type"] is FlowResultType.FORM
assert options["step_id"] == "advanced"
assert options["step_id"] == "additional"
# Configure advanced step but with api error
# Configure additional step but with api error
with patch(
"homeassistant.components.anthropic.config_flow.anthropic.resources.models.AsyncModels.retrieve",
new_callable=AsyncMock,
@@ -877,12 +877,12 @@ async def test_ai_task_subentry_not_loaded(
assert result.get("reason") == "entry_not_loaded"
async def test_creating_ai_task_subentry_advanced(
async def test_creating_ai_task_subentry_additional(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_init_component,
) -> None:
"""Test creating an AI task subentry with advanced settings."""
"""Test creating an AI task subentry with additional settings."""
result = await hass.config_entries.subentries.async_init(
(mock_config_entry.entry_id, "ai_task_data"),
context={"source": config_entries.SOURCE_USER},
@@ -891,7 +891,7 @@ async def test_creating_ai_task_subentry_advanced(
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "init"
# Go to advanced settings
# Go to additional settings
result2 = await hass.config_entries.subentries.async_configure(
result["flow_id"],
{
@@ -901,9 +901,9 @@ async def test_creating_ai_task_subentry_advanced(
)
assert result2.get("type") is FlowResultType.FORM
assert result2.get("step_id") == "advanced"
assert result2.get("step_id") == "additional"
# Configure advanced settings
# Configure additional settings
result3 = await hass.config_entries.subentries.async_configure(
result["flow_id"],
{
+6 -6
View File
@@ -8,7 +8,7 @@ import pytest
from homeassistant import config_entries
from homeassistant.components.dnsip.config_flow import DATA_SCHEMA
from homeassistant.components.dnsip.const import (
CONF_ADVANCED_OPTIONS,
CONF_ADDITIONAL_OPTIONS,
CONF_HOSTNAME,
CONF_IPV4,
CONF_IPV6,
@@ -50,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None:
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOSTNAME: "home-assistant.io", CONF_ADVANCED_OPTIONS: {}},
{CONF_HOSTNAME: "home-assistant.io", CONF_ADDITIONAL_OPTIONS: {}},
)
await hass.async_block_till_done()
@@ -71,7 +71,7 @@ async def test_form(hass: HomeAssistant) -> None:
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_with_advanced_options(hass: HomeAssistant) -> None:
async def test_form_with_additional_options(hass: HomeAssistant) -> None:
"""Test we can submit the form with custom resolver and port options."""
result = await hass.config_entries.flow.async_init(
@@ -95,7 +95,7 @@ async def test_form_with_advanced_options(hass: HomeAssistant) -> None:
result["flow_id"],
{
CONF_HOSTNAME: "home-assistant.io",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_RESOLVER: "8.8.8.8",
CONF_RESOLVER_IPV6: "2620:119:53::53",
CONF_PORT: 53,
@@ -136,7 +136,7 @@ async def test_form_error(hass: HomeAssistant) -> None:
result["flow_id"],
{
CONF_HOSTNAME: "home-assistant.io",
CONF_ADVANCED_OPTIONS: {},
CONF_ADDITIONAL_OPTIONS: {},
},
)
await hass.async_block_till_done()
@@ -185,7 +185,7 @@ async def test_flow_already_exist(hass: HomeAssistant) -> None:
result["flow_id"],
{
CONF_HOSTNAME: "home-assistant.io",
CONF_ADVANCED_OPTIONS: {},
CONF_ADDITIONAL_OPTIONS: {},
},
)
await hass.async_block_till_done()
@@ -246,9 +246,9 @@ async def test_subentry_unsupported_model(
)
await hass.async_block_till_done()
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
# Configure advanced step
# Configure additional step
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
{
@@ -300,9 +300,9 @@ async def test_subentry_reasoning_effort_list(
},
)
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
# Configure advanced step
# Configure additional step
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
{
@@ -354,9 +354,9 @@ async def test_subentry_reasoning_summary_visibility(
},
)
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
# Configure advanced step
# Configure additional step
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
{
@@ -403,7 +403,7 @@ async def test_subentry_reasoning_summary_options(
},
)
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
@@ -450,7 +450,7 @@ async def test_subentry_reasoning_summary_default_sanitized_on_model_switch(
CONF_LLM_HASS_API: ["assist"],
},
)
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
@@ -515,9 +515,9 @@ async def test_subentry_service_tier_list(
},
)
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
# Configure advanced step
# Configure additional step
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
{
@@ -561,9 +561,9 @@ async def test_subentry_unsupported_reasoning_effort(
},
)
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
# Configure advanced step
# Configure additional step
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
{
@@ -1144,9 +1144,9 @@ async def test_subentry_web_search_user_location(
},
)
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["step_id"] == "advanced"
assert subentry_flow["step_id"] == "additional"
# Configure advanced step
# Configure additional step
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
{
@@ -1292,12 +1292,12 @@ async def test_ai_task_subentry_not_loaded(
assert result.get("reason") == "entry_not_loaded"
async def test_creating_ai_task_subentry_advanced(
async def test_creating_ai_task_subentry_additional(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_init_component,
) -> None:
"""Test creating an AI task subentry with advanced settings."""
"""Test creating an AI task subentry with additional settings."""
result = await hass.config_entries.subentries.async_init(
(mock_config_entry.entry_id, "ai_task_data"),
context={"source": config_entries.SOURCE_USER},
@@ -1306,7 +1306,7 @@ async def test_creating_ai_task_subentry_advanced(
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "init"
# Go to advanced settings
# Go to additional settings
result2 = await hass.config_entries.subentries.async_configure(
result["flow_id"],
{
@@ -1316,9 +1316,9 @@ async def test_creating_ai_task_subentry_advanced(
)
assert result2.get("type") is FlowResultType.FORM
assert result2.get("step_id") == "advanced"
assert result2.get("step_id") == "additional"
# Configure advanced settings
# Configure additional settings
result3 = await hass.config_entries.subentries.async_configure(
result["flow_id"],
{
+26 -26
View File
@@ -9,7 +9,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.components.sql.const import (
CONF_ADVANCED_OPTIONS,
CONF_ADDITIONAL_OPTIONS,
CONF_COLUMN_NAME,
CONF_QUERY,
DOMAIN,
@@ -35,7 +35,7 @@ from tests.common import MockConfigEntry
ENTRY_CONFIG = {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
@@ -46,7 +46,7 @@ ENTRY_CONFIG_BLANK_QUERY = {
CONF_NAME: "Get Value",
CONF_QUERY: " ",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
@@ -56,7 +56,7 @@ ENTRY_CONFIG_BLANK_QUERY = {
ENTRY_CONFIG_WITH_VALUE_TEMPLATE = {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_VALUE_TEMPLATE: "{{ value }}",
},
@@ -68,7 +68,7 @@ ENTRY_CONFIG_WITH_QUERY_TEMPLATE = {
" 5 {% else %} 6 {% endif %} as value"
),
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_VALUE_TEMPLATE: "{{ value }}",
},
@@ -77,7 +77,7 @@ ENTRY_CONFIG_WITH_QUERY_TEMPLATE = {
ENTRY_CONFIG_WITH_BROKEN_QUERY_TEMPLATE = {
CONF_QUERY: "SELECT {{ 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_VALUE_TEMPLATE: "{{ value }}",
},
@@ -86,7 +86,7 @@ ENTRY_CONFIG_WITH_BROKEN_QUERY_TEMPLATE = {
ENTRY_CONFIG_WITH_BROKEN_QUERY_TEMPLATE_OPT = {
CONF_QUERY: "SELECT {{ 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_VALUE_TEMPLATE: "{{ value }}",
},
@@ -95,7 +95,7 @@ ENTRY_CONFIG_WITH_BROKEN_QUERY_TEMPLATE_OPT = {
ENTRY_CONFIG_INVALID_QUERY = {
CONF_QUERY: "SELECT 5 FROM as value",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -104,7 +104,7 @@ ENTRY_CONFIG_INVALID_QUERY = {
ENTRY_CONFIG_INVALID_QUERY_2 = {
CONF_QUERY: "SELECT5 FROM as value",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -113,7 +113,7 @@ ENTRY_CONFIG_INVALID_QUERY_2 = {
ENTRY_CONFIG_INVALID_QUERY_3 = {
CONF_QUERY: ";;",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -122,7 +122,7 @@ ENTRY_CONFIG_INVALID_QUERY_3 = {
ENTRY_CONFIG_INVALID_QUERY_OPT = {
CONF_QUERY: "SELECT 5 FROM as value",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -131,7 +131,7 @@ ENTRY_CONFIG_INVALID_QUERY_OPT = {
ENTRY_CONFIG_INVALID_QUERY_2_OPT = {
CONF_QUERY: "SELECT5 FROM as value",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -140,7 +140,7 @@ ENTRY_CONFIG_INVALID_QUERY_2_OPT = {
ENTRY_CONFIG_INVALID_QUERY_3_OPT = {
CONF_QUERY: ";;",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -152,7 +152,7 @@ ENTRY_CONFIG_QUERY_READ_ONLY_CTE = {
" SELECT state FROM test WHERE row_num = 1 LIMIT 1;"
),
CONF_COLUMN_NAME: "state",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -160,7 +160,7 @@ ENTRY_CONFIG_QUERY_READ_ONLY_CTE = {
ENTRY_CONFIG_QUERY_NO_READ_ONLY = {
CONF_QUERY: "UPDATE states SET state = 999999 WHERE state_id = 11125",
CONF_COLUMN_NAME: "state",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -171,7 +171,7 @@ ENTRY_CONFIG_QUERY_NO_READ_ONLY_CTE = {
" UPDATE states SET states.state = test.state;"
),
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -182,7 +182,7 @@ ENTRY_CONFIG_QUERY_READ_ONLY_CTE_OPT = {
" SELECT state FROM test WHERE row_num = 1 LIMIT 1;"
),
CONF_COLUMN_NAME: "state",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -190,7 +190,7 @@ ENTRY_CONFIG_QUERY_READ_ONLY_CTE_OPT = {
ENTRY_CONFIG_QUERY_NO_READ_ONLY_OPT = {
CONF_QUERY: "UPDATE 5 as value",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -201,7 +201,7 @@ ENTRY_CONFIG_QUERY_NO_READ_ONLY_CTE_OPT = {
" UPDATE states SET states.state = test.state;"
),
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -210,7 +210,7 @@ ENTRY_CONFIG_QUERY_NO_READ_ONLY_CTE_OPT = {
ENTRY_CONFIG_MULTIPLE_QUERIES = {
CONF_QUERY: "SELECT 5 as state; UPDATE states SET state = 10;",
CONF_COLUMN_NAME: "state",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -219,7 +219,7 @@ ENTRY_CONFIG_MULTIPLE_QUERIES = {
ENTRY_CONFIG_MULTIPLE_QUERIES_OPT = {
CONF_QUERY: "SELECT 5 as state; UPDATE states SET state = 10;",
CONF_COLUMN_NAME: "state",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -228,7 +228,7 @@ ENTRY_CONFIG_MULTIPLE_QUERIES_OPT = {
ENTRY_CONFIG_INVALID_COLUMN_NAME = {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -236,7 +236,7 @@ ENTRY_CONFIG_INVALID_COLUMN_NAME = {
ENTRY_CONFIG_INVALID_COLUMN_NAME_OPT = {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -244,7 +244,7 @@ ENTRY_CONFIG_INVALID_COLUMN_NAME_OPT = {
ENTRY_CONFIG_NO_RESULTS = {
CONF_QUERY: "SELECT kalle as value from no_table;",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -365,8 +365,8 @@ async def init_integration(
"""Set up the SQL integration in Home Assistant."""
if not options:
options = ENTRY_CONFIG
if CONF_ADVANCED_OPTIONS not in options:
options[CONF_ADVANCED_OPTIONS] = {}
if CONF_ADDITIONAL_OPTIONS not in options:
options[CONF_ADDITIONAL_OPTIONS] = {}
config_entry = MockConfigEntry(
title=title,
+33 -33
View File
@@ -16,7 +16,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.components.sql.const import (
CONF_ADVANCED_OPTIONS,
CONF_ADDITIONAL_OPTIONS,
CONF_COLUMN_NAME,
CONF_QUERY,
DOMAIN,
@@ -100,7 +100,7 @@ async def test_form_simple(
assert result["options"] == {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
@@ -141,7 +141,7 @@ async def test_form_with_query_template(
" 5 {% else %} 6 {% endif %} as value"
),
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_VALUE_TEMPLATE: "{{ value }}",
},
@@ -188,7 +188,7 @@ async def test_form_with_broken_query_template(
" 5 {% else %} 6 {% endif %} as value"
),
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_VALUE_TEMPLATE: "{{ value }}",
},
@@ -225,7 +225,7 @@ async def test_form_with_value_template(
assert result["options"] == {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_VALUE_TEMPLATE: "{{ value }}",
},
@@ -348,7 +348,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None:
assert result["options"] == {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
@@ -391,7 +391,7 @@ async def test_flow_fails_invalid_column_name(hass: HomeAssistant) -> None:
assert result["options"] == {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
@@ -407,7 +407,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
options={
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
@@ -430,7 +430,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
user_input={
CONF_QUERY: "SELECT 5 as size",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_VALUE_TEMPLATE: "{{ value }}",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
@@ -443,7 +443,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
assert result["data"] == {
CONF_QUERY: "SELECT 5 as size",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_VALUE_TEMPLATE: "{{ value }}",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
@@ -460,7 +460,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None
options={
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
@@ -482,7 +482,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None
user_input={
CONF_QUERY: "SELECT 5 as size",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
@@ -493,7 +493,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None
assert result["data"] == {
CONF_QUERY: "SELECT 5 as size",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -507,7 +507,7 @@ async def test_options_flow_fails_db_url(hass: HomeAssistant) -> None:
options={
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
@@ -529,7 +529,7 @@ async def test_options_flow_fails_db_url(hass: HomeAssistant) -> None:
user_input={
CONF_QUERY: "SELECT 5 as size",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
@@ -546,7 +546,7 @@ async def test_options_flow_fails_invalid_query(hass: HomeAssistant) -> None:
options={
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
@@ -630,7 +630,7 @@ async def test_options_flow_fails_invalid_query(hass: HomeAssistant) -> None:
user_input={
CONF_QUERY: "SELECT 5 as size",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
@@ -640,7 +640,7 @@ async def test_options_flow_fails_invalid_query(hass: HomeAssistant) -> None:
assert result["data"] == {
CONF_QUERY: "SELECT 5 as size",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -654,7 +654,7 @@ async def test_options_flow_fails_invalid_column_name(hass: HomeAssistant) -> No
options={
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
@@ -682,7 +682,7 @@ async def test_options_flow_fails_invalid_column_name(hass: HomeAssistant) -> No
user_input={
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
@@ -692,7 +692,7 @@ async def test_options_flow_fails_invalid_column_name(hass: HomeAssistant) -> No
assert result["data"] == {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -706,7 +706,7 @@ async def test_options_flow_db_url_empty(hass: HomeAssistant) -> None:
options={
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
@@ -727,7 +727,7 @@ async def test_options_flow_db_url_empty(hass: HomeAssistant) -> None:
user_input={
CONF_QUERY: "SELECT 5 as size",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
@@ -738,7 +738,7 @@ async def test_options_flow_db_url_empty(hass: HomeAssistant) -> None:
assert result["data"] == {
CONF_QUERY: "SELECT 5 as size",
CONF_COLUMN_NAME: "size",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -770,7 +770,7 @@ async def test_full_flow_not_recorder_db(
{
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {},
CONF_ADDITIONAL_OPTIONS: {},
},
)
await hass.async_block_till_done()
@@ -781,7 +781,7 @@ async def test_full_flow_not_recorder_db(
assert result["options"] == {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {},
CONF_ADDITIONAL_OPTIONS: {},
}
entry = hass.config_entries.async_entries(DOMAIN)[0]
@@ -796,7 +796,7 @@ async def test_full_flow_not_recorder_db(
user_input={
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
@@ -807,7 +807,7 @@ async def test_full_flow_not_recorder_db(
assert result["data"] == {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
@@ -822,7 +822,7 @@ async def test_device_state_class(hass: HomeAssistant) -> None:
options={
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
@@ -839,7 +839,7 @@ async def test_device_state_class(hass: HomeAssistant) -> None:
user_input={
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
@@ -852,7 +852,7 @@ async def test_device_state_class(hass: HomeAssistant) -> None:
assert result["data"] == {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
@@ -868,7 +868,7 @@ async def test_device_state_class(hass: HomeAssistant) -> None:
user_input={
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
@@ -881,7 +881,7 @@ async def test_device_state_class(hass: HomeAssistant) -> None:
assert result["data"] == {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
+50 -3
View File
@@ -12,7 +12,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.components.sql.const import (
CONF_ADVANCED_OPTIONS,
CONF_ADDITIONAL_OPTIONS,
CONF_COLUMN_NAME,
CONF_QUERY,
DOMAIN,
@@ -122,7 +122,7 @@ async def test_migration_from_future(
options={
CONF_QUERY: "SELECT 5.01 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {},
CONF_ADDITIONAL_OPTIONS: {},
},
entry_id="1",
version=3,
@@ -168,7 +168,54 @@ async def test_migration_from_v1_to_v2(
assert config_entry.options == {
CONF_QUERY: "SELECT 5.01 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_VALUE_TEMPLATE: "{{ value | int }}",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
}
state = hass.states.get("sensor.test_migration")
assert state.state == "5"
async def test_migration_from_v2_1_to_v2_2(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test migration from version 2.1 to 2.2 renames the options section."""
config_entry = MockConfigEntry(
title="Test migration",
domain=DOMAIN,
source=SOURCE_USER,
data={},
options={
CONF_QUERY: "SELECT 5.01 as value",
CONF_COLUMN_NAME: "value",
"advanced_options": {
CONF_VALUE_TEMPLATE: "{{ value | int }}",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
},
entry_id="1",
version=2,
minor_version=1,
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.minor_version == 2
assert "advanced_options" not in config_entry.options
assert config_entry.options == {
CONF_QUERY: "SELECT 5.01 as value",
CONF_COLUMN_NAME: "value",
CONF_ADDITIONAL_OPTIONS: {
CONF_VALUE_TEMPLATE: "{{ value | int }}",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
+6 -6
View File
@@ -18,7 +18,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.components.sql.const import (
CONF_ADVANCED_OPTIONS,
CONF_ADDITIONAL_OPTIONS,
CONF_COLUMN_NAME,
CONF_QUERY,
DOMAIN,
@@ -94,7 +94,7 @@ async def test_query_value_template(
options = {
CONF_QUERY: "SELECT 5.01 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_VALUE_TEMPLATE: "{{ value | int }}",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
@@ -126,7 +126,7 @@ async def test_template_query(
" 5 {% else %} 6 {% endif %} as value"
),
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_VALUE_TEMPLATE: "{{ value | int }}",
},
}
@@ -168,7 +168,7 @@ async def test_broken_template_query(
options = {
CONF_QUERY: "SELECT {{ 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_VALUE_TEMPLATE: "{{ value | int }}",
},
}
@@ -672,7 +672,7 @@ async def test_attributes_from_entry_config(
options={
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
@@ -694,7 +694,7 @@ async def test_attributes_from_entry_config(
options={
CONF_QUERY: "SELECT 6 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_ADDITIONAL_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
+26 -437
View File
@@ -84,7 +84,6 @@ from homeassistant.helpers.trigger import (
async_initialize_triggers,
async_validate_trigger_config,
make_entity_numerical_state_changed_trigger,
make_entity_numerical_state_changed_with_unit_trigger,
make_entity_numerical_state_crossed_threshold_trigger,
make_entity_origin_state_trigger,
make_entity_target_state_trigger,
@@ -106,13 +105,6 @@ from tests.common import (
)
def _reported_reasons(
did_not_trigger_reports: list[NotTriggeredInfo],
) -> list[tuple[str, Any]]:
"""Return the (reason, data) pair of each recorded did-not-trigger report."""
return [(report.reason, report.data) for report in did_not_trigger_reports]
async def _arm_numerical_trigger(
hass: HomeAssistant,
trigger_cls: type[Trigger],
@@ -1845,23 +1837,14 @@ async def test_numerical_state_attribute_changed_error_handling(
hass.states.async_set("test.test_entity", "on", {})
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
("entity_value_not_numeric", {"entity_id": "test.test_entity", "value": None})
]
did_not_trigger_reports.clear()
assert did_not_trigger_reports == []
# Test the trigger does not fire when the attribute value is invalid
for value in ("cat", None):
hass.states.async_set("test.test_entity", "on", {"test_attribute": value})
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
(
"entity_value_not_numeric",
{"entity_id": "test.test_entity", "value": value},
)
]
did_not_trigger_reports.clear()
assert did_not_trigger_reports == []
# Test the trigger does not fire when the above sensor does not exist
hass.states.async_remove("sensor.above")
@@ -1869,10 +1852,7 @@ async def test_numerical_state_attribute_changed_error_handling(
hass.states.async_set("test.test_entity", "on", {"test_attribute": 50})
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
("threshold_entity_not_found", {"entity_id": "sensor.above"})
]
did_not_trigger_reports.clear()
assert did_not_trigger_reports == []
# Test the trigger does not fire when the above sensor state is not numeric
for invalid_value in ("cat", None):
@@ -1881,17 +1861,7 @@ async def test_numerical_state_attribute_changed_error_handling(
hass.states.async_set("test.test_entity", "on", {"test_attribute": 50})
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
(
"entity_value_not_numeric",
{"entity_id": "test.test_entity", "value": None},
),
(
"threshold_value_not_numeric",
{"entity_id": "sensor.above", "value": str(invalid_value)},
),
]
did_not_trigger_reports.clear()
assert did_not_trigger_reports == []
# Reset the above sensor state to a valid numeric value
hass.states.async_set("sensor.above", "10")
@@ -1902,11 +1872,7 @@ async def test_numerical_state_attribute_changed_error_handling(
hass.states.async_set("test.test_entity", "on", {"test_attribute": 50})
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
("entity_value_not_numeric", {"entity_id": "test.test_entity", "value": None}),
("threshold_entity_not_found", {"entity_id": "sensor.below"}),
]
did_not_trigger_reports.clear()
assert did_not_trigger_reports == []
# Test the trigger does not fire when the below sensor state is not numeric
for invalid_value in ("cat", None):
@@ -1915,17 +1881,7 @@ async def test_numerical_state_attribute_changed_error_handling(
hass.states.async_set("test.test_entity", "on", {"test_attribute": 50})
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
(
"entity_value_not_numeric",
{"entity_id": "test.test_entity", "value": None},
),
(
"threshold_value_not_numeric",
{"entity_id": "sensor.below", "value": str(invalid_value)},
),
]
did_not_trigger_reports.clear()
assert did_not_trigger_reports == []
unsub()
@@ -2332,11 +2288,6 @@ async def test_numerical_state_attribute_changed_with_unit_error_handling(
entity_did_not_trigger_reports,
)
)
# Both triggers report a non-numeric tracked value identically.
entity_not_numeric = (
"entity_value_not_numeric",
{"entity_id": "test.test_entity", "value": None},
)
# 77°F = 25°C, within range (above 20, below 30) - should trigger numerical
# Entity automation won't trigger because sensor.above/below don't exist yet
@@ -2351,13 +2302,8 @@ async def test_numerical_state_attribute_changed_with_unit_error_handling(
await hass.async_block_till_done()
assert len(numeric_calls) == 1
assert entity_calls == []
# The entity-threshold trigger can't resolve its limits yet (sensors absent)
assert numeric_did_not_trigger_reports == []
assert _reported_reasons(entity_did_not_trigger_reports) == [
("threshold_entity_not_found", {"entity_id": "sensor.above"})
]
assert numeric_did_not_trigger_reports == entity_did_not_trigger_reports == []
numeric_calls.clear()
entity_did_not_trigger_reports.clear()
# 59°F = 15°C, below 20°C - should NOT trigger
hass.states.async_set(
@@ -2370,11 +2316,7 @@ async def test_numerical_state_attribute_changed_with_unit_error_handling(
)
await hass.async_block_till_done()
assert numeric_calls == entity_calls == []
assert numeric_did_not_trigger_reports == []
assert _reported_reasons(entity_did_not_trigger_reports) == [
("threshold_entity_not_found", {"entity_id": "sensor.above"})
]
entity_did_not_trigger_reports.clear()
assert numeric_did_not_trigger_reports == entity_did_not_trigger_reports == []
# 95°F = 35°C, above 30°C - should NOT trigger
hass.states.async_set(
@@ -2387,11 +2329,7 @@ async def test_numerical_state_attribute_changed_with_unit_error_handling(
)
await hass.async_block_till_done()
assert numeric_calls == entity_calls == []
assert numeric_did_not_trigger_reports == []
assert _reported_reasons(entity_did_not_trigger_reports) == [
("threshold_entity_not_found", {"entity_id": "sensor.above"})
]
entity_did_not_trigger_reports.clear()
assert numeric_did_not_trigger_reports == entity_did_not_trigger_reports == []
# Set up entity limits referencing sensors that report in °F
hass.states.async_set(
@@ -2425,10 +2363,7 @@ async def test_numerical_state_attribute_changed_with_unit_error_handling(
hass.states.async_set("test.test_entity", "on", {})
await hass.async_block_till_done()
assert numeric_calls == entity_calls == []
assert _reported_reasons(numeric_did_not_trigger_reports) == [entity_not_numeric]
assert _reported_reasons(entity_did_not_trigger_reports) == [entity_not_numeric]
numeric_did_not_trigger_reports.clear()
entity_did_not_trigger_reports.clear()
assert numeric_did_not_trigger_reports == entity_did_not_trigger_reports == []
# Test the trigger does not fire when the attribute value is invalid
for value in ("cat", None):
@@ -2442,20 +2377,7 @@ async def test_numerical_state_attribute_changed_with_unit_error_handling(
)
await hass.async_block_till_done()
assert numeric_calls == entity_calls == []
assert _reported_reasons(numeric_did_not_trigger_reports) == [
(
"entity_value_not_numeric",
{"entity_id": "test.test_entity", "value": value},
)
]
assert _reported_reasons(entity_did_not_trigger_reports) == [
(
"entity_value_not_numeric",
{"entity_id": "test.test_entity", "value": value},
)
]
numeric_did_not_trigger_reports.clear()
entity_did_not_trigger_reports.clear()
assert numeric_did_not_trigger_reports == entity_did_not_trigger_reports == []
# Test the trigger does not fire when the unit is incompatible
hass.states.async_set(
@@ -2468,20 +2390,7 @@ async def test_numerical_state_attribute_changed_with_unit_error_handling(
)
await hass.async_block_till_done()
assert numeric_calls == entity_calls == []
assert _reported_reasons(numeric_did_not_trigger_reports) == [
(
"entity_unit_not_supported",
{"entity_id": "test.test_entity", "unit": "invalid_unit"},
)
]
assert _reported_reasons(entity_did_not_trigger_reports) == [
(
"entity_unit_not_supported",
{"entity_id": "test.test_entity", "unit": "invalid_unit"},
)
]
numeric_did_not_trigger_reports.clear()
entity_did_not_trigger_reports.clear()
assert numeric_did_not_trigger_reports == entity_did_not_trigger_reports == []
# Test the trigger does not fire when the above sensor does not exist
hass.states.async_remove("sensor.above")
@@ -2500,15 +2409,7 @@ async def test_numerical_state_attribute_changed_with_unit_error_handling(
)
await hass.async_block_till_done()
assert numeric_calls == entity_calls == []
# The intermediate None reports a non-numeric value on both triggers; the
# missing threshold entity is reported only by the entity-threshold trigger.
assert _reported_reasons(numeric_did_not_trigger_reports) == [entity_not_numeric]
assert _reported_reasons(entity_did_not_trigger_reports) == [
entity_not_numeric,
("threshold_entity_not_found", {"entity_id": "sensor.above"}),
]
numeric_did_not_trigger_reports.clear()
entity_did_not_trigger_reports.clear()
assert numeric_did_not_trigger_reports == entity_did_not_trigger_reports == []
# Test the trigger does not fire when the above sensor state is not numeric
for invalid_value in ("cat", None):
@@ -2535,18 +2436,7 @@ async def test_numerical_state_attribute_changed_with_unit_error_handling(
)
await hass.async_block_till_done()
assert numeric_calls == entity_calls == []
assert _reported_reasons(numeric_did_not_trigger_reports) == [
entity_not_numeric
]
assert _reported_reasons(entity_did_not_trigger_reports) == [
entity_not_numeric,
(
"threshold_value_not_numeric",
{"entity_id": "sensor.above", "value": str(invalid_value)},
),
]
numeric_did_not_trigger_reports.clear()
entity_did_not_trigger_reports.clear()
assert numeric_did_not_trigger_reports == entity_did_not_trigger_reports == []
# Test the trigger does not fire when the above sensor's unit is incompatible
hass.states.async_set(
@@ -2569,16 +2459,7 @@ async def test_numerical_state_attribute_changed_with_unit_error_handling(
)
await hass.async_block_till_done()
assert numeric_calls == entity_calls == []
assert _reported_reasons(numeric_did_not_trigger_reports) == [entity_not_numeric]
assert _reported_reasons(entity_did_not_trigger_reports) == [
entity_not_numeric,
(
"threshold_unit_not_supported",
{"entity_id": "sensor.above", "unit": "invalid_unit"},
),
]
numeric_did_not_trigger_reports.clear()
entity_did_not_trigger_reports.clear()
assert numeric_did_not_trigger_reports == entity_did_not_trigger_reports == []
# Reset the above sensor state to a valid numeric value
hass.states.async_set(
@@ -2604,13 +2485,7 @@ async def test_numerical_state_attribute_changed_with_unit_error_handling(
)
await hass.async_block_till_done()
assert numeric_calls == entity_calls == []
assert _reported_reasons(numeric_did_not_trigger_reports) == [entity_not_numeric]
assert _reported_reasons(entity_did_not_trigger_reports) == [
entity_not_numeric,
("threshold_entity_not_found", {"entity_id": "sensor.below"}),
]
numeric_did_not_trigger_reports.clear()
entity_did_not_trigger_reports.clear()
assert numeric_did_not_trigger_reports == entity_did_not_trigger_reports == []
# Test the trigger does not fire when the below sensor state is not numeric
for invalid_value in ("cat", None):
@@ -2633,18 +2508,7 @@ async def test_numerical_state_attribute_changed_with_unit_error_handling(
)
await hass.async_block_till_done()
assert numeric_calls == entity_calls == []
assert _reported_reasons(numeric_did_not_trigger_reports) == [
entity_not_numeric
]
assert _reported_reasons(entity_did_not_trigger_reports) == [
entity_not_numeric,
(
"threshold_value_not_numeric",
{"entity_id": "sensor.below", "value": str(invalid_value)},
),
]
numeric_did_not_trigger_reports.clear()
entity_did_not_trigger_reports.clear()
assert numeric_did_not_trigger_reports == entity_did_not_trigger_reports == []
# Test the trigger does not fire when the below sensor's unit is incompatible
hass.states.async_set(
@@ -2667,242 +2531,12 @@ async def test_numerical_state_attribute_changed_with_unit_error_handling(
)
await hass.async_block_till_done()
assert numeric_calls == entity_calls == []
assert _reported_reasons(numeric_did_not_trigger_reports) == [entity_not_numeric]
assert _reported_reasons(entity_did_not_trigger_reports) == [
entity_not_numeric,
(
"threshold_unit_not_supported",
{"entity_id": "sensor.below", "unit": "invalid_unit"},
),
]
numeric_did_not_trigger_reports.clear()
entity_did_not_trigger_reports.clear()
assert numeric_did_not_trigger_reports == entity_did_not_trigger_reports == []
for unsub in unsubs:
unsub()
# State-sourced numerical triggers: brightness-style (percentage) and
# temperature-style (with unit conversion to a base unit).
_PERCENT_CHANGED_TRIGGER = make_entity_numerical_state_changed_trigger(
{"test": DomainSpec()}, "%"
)
_TEMPERATURE_CHANGED_TRIGGER = make_entity_numerical_state_changed_with_unit_trigger(
{"test": DomainSpec()}, UnitOfTemperature.CELSIUS, TemperatureConverter
)
@pytest.mark.parametrize(
(
"trigger_cls",
"good_unit",
"bad_state",
"bad_unit",
"expected_reason",
"expected_data",
),
[
pytest.param(
_PERCENT_CHANGED_TRIGGER,
"%",
"cat",
"%",
"entity_value_not_numeric",
{"entity_id": "test.test_entity", "value": "cat"},
id="non-numeric",
),
pytest.param(
_PERCENT_CHANGED_TRIGGER,
"%",
"50",
"kg",
"entity_unit_not_supported",
{"entity_id": "test.test_entity", "unit": "kg"},
id="unsupported-unit",
),
pytest.param(
_TEMPERATURE_CHANGED_TRIGGER,
"°C",
"cat",
"°C",
"entity_value_not_numeric",
{"entity_id": "test.test_entity", "value": "cat"},
id="with-unit-non-numeric",
),
pytest.param(
_TEMPERATURE_CHANGED_TRIGGER,
"°C",
"50",
"kg",
"entity_unit_not_supported",
{"entity_id": "test.test_entity", "unit": "kg"},
id="with-unit-incompatible-unit",
),
],
)
async def test_numerical_trigger_reports_invalid_tracked_value(
hass: HomeAssistant,
trigger_cls: type[Trigger],
good_unit: str,
bad_state: str,
bad_unit: str,
expected_reason: str,
expected_data: dict[str, Any],
) -> None:
"""Report a non-numeric value or unsupported unit on the tracked entity."""
calls: list[dict[str, Any]] = []
did_not_trigger_reports: list[NotTriggeredInfo] = []
hass.states.async_set(
"test.test_entity", "10", {ATTR_UNIT_OF_MEASUREMENT: good_unit}
)
await hass.async_block_till_done()
unsub = await _arm_numerical_trigger(
hass,
trigger_cls,
{"threshold": {"type": "any"}},
calls,
did_not_trigger_reports,
)
hass.states.async_set(
"test.test_entity", bad_state, {ATTR_UNIT_OF_MEASUREMENT: bad_unit}
)
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
(expected_reason, expected_data)
]
unsub()
@pytest.mark.parametrize(
(
"trigger_cls",
"good_unit",
"threshold_state",
"threshold_unit",
"expected_reason",
"expected_data",
),
[
pytest.param(
_PERCENT_CHANGED_TRIGGER,
"%",
"cat",
"%",
"threshold_value_not_numeric",
{"entity_id": "sensor.limit", "value": "cat"},
id="non-numeric",
),
pytest.param(
_PERCENT_CHANGED_TRIGGER,
"%",
"30",
"kg",
"threshold_unit_not_supported",
{"entity_id": "sensor.limit", "unit": "kg"},
id="unsupported-unit",
),
pytest.param(
_TEMPERATURE_CHANGED_TRIGGER,
"°C",
"cat",
"°C",
"threshold_value_not_numeric",
{"entity_id": "sensor.limit", "value": "cat"},
id="with-unit-non-numeric",
),
pytest.param(
_TEMPERATURE_CHANGED_TRIGGER,
"°C",
"30",
"kg",
"threshold_unit_not_supported",
{"entity_id": "sensor.limit", "unit": "kg"},
id="with-unit-incompatible-unit",
),
],
)
async def test_numerical_trigger_reports_invalid_threshold_entity(
hass: HomeAssistant,
trigger_cls: type[Trigger],
good_unit: str,
threshold_state: str,
threshold_unit: str,
expected_reason: str,
expected_data: dict[str, Any],
) -> None:
"""Report a non-numeric value or unsupported unit on a threshold entity."""
calls: list[dict[str, Any]] = []
did_not_trigger_reports: list[NotTriggeredInfo] = []
hass.states.async_set(
"sensor.limit", threshold_state, {ATTR_UNIT_OF_MEASUREMENT: threshold_unit}
)
hass.states.async_set(
"test.test_entity", "10", {ATTR_UNIT_OF_MEASUREMENT: good_unit}
)
await hass.async_block_till_done()
unsub = await _arm_numerical_trigger(
hass,
trigger_cls,
{"threshold": {"type": "above", "value": {"entity": "sensor.limit"}}},
calls,
did_not_trigger_reports,
)
hass.states.async_set(
"test.test_entity", "20", {ATTR_UNIT_OF_MEASUREMENT: good_unit}
)
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
(expected_reason, expected_data)
]
unsub()
async def test_numerical_trigger_reports_single_reason_for_between(
hass: HomeAssistant,
) -> None:
"""Two invalid between-thresholds yield a single diagnostic for the lower one."""
calls: list[dict[str, Any]] = []
did_not_trigger_reports: list[NotTriggeredInfo] = []
hass.states.async_set("sensor.low", "cat", {ATTR_UNIT_OF_MEASUREMENT: "%"})
hass.states.async_set("sensor.high", "dog", {ATTR_UNIT_OF_MEASUREMENT: "%"})
hass.states.async_set("test.test_entity", "10", {ATTR_UNIT_OF_MEASUREMENT: "%"})
await hass.async_block_till_done()
unsub = await _arm_numerical_trigger(
hass,
_PERCENT_CHANGED_TRIGGER,
{
"threshold": {
"type": "between",
"value_min": {"entity": "sensor.low"},
"value_max": {"entity": "sensor.high"},
}
},
calls,
did_not_trigger_reports,
)
hass.states.async_set("test.test_entity", "20", {ATTR_UNIT_OF_MEASUREMENT: "%"})
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
("threshold_value_not_numeric", {"entity_id": "sensor.low", "value": "cat"})
]
unsub()
@pytest.mark.parametrize(
("trigger_options", "expected_result"),
[
@@ -3345,11 +2979,8 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
hass.states.async_set("test.test_entity", "on", {"test_attribute": 50})
await hass.async_block_till_done()
assert len(calls) == 1
assert _reported_reasons(did_not_trigger_reports) == [
("entity_value_not_numeric", {"entity_id": "test.test_entity", "value": None})
]
assert did_not_trigger_reports == []
calls.clear()
did_not_trigger_reports.clear()
# Test the trigger does not fire when the attribute value is outside the limits
for value in (5, 95):
@@ -3362,23 +2993,14 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
hass.states.async_set("test.test_entity", "on", {})
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
("entity_value_not_numeric", {"entity_id": "test.test_entity", "value": None})
]
did_not_trigger_reports.clear()
assert did_not_trigger_reports == []
# Test the trigger does not fire when the attribute value is invalid
for value in ("cat", None):
hass.states.async_set("test.test_entity", "on", {"test_attribute": value})
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
(
"entity_value_not_numeric",
{"entity_id": "test.test_entity", "value": value},
)
]
did_not_trigger_reports.clear()
assert did_not_trigger_reports == []
# Test the trigger does not fire when the lower sensor does not exist
hass.states.async_remove("sensor.lower")
@@ -3386,10 +3008,7 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
hass.states.async_set("test.test_entity", "on", {"test_attribute": 50})
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
("threshold_entity_not_found", {"entity_id": "sensor.lower"})
]
did_not_trigger_reports.clear()
assert did_not_trigger_reports == []
# Test the trigger does not fire when the lower sensor state is not numeric
for invalid_value in ("cat", None):
@@ -3398,17 +3017,7 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
hass.states.async_set("test.test_entity", "on", {"test_attribute": 50})
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
(
"entity_value_not_numeric",
{"entity_id": "test.test_entity", "value": None},
),
(
"threshold_value_not_numeric",
{"entity_id": "sensor.lower", "value": str(invalid_value)},
),
]
did_not_trigger_reports.clear()
assert did_not_trigger_reports == []
# Reset the lower sensor state to a valid numeric value
hass.states.async_set("sensor.lower", "10")
@@ -3419,11 +3028,7 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
hass.states.async_set("test.test_entity", "on", {"test_attribute": 50})
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
("entity_value_not_numeric", {"entity_id": "test.test_entity", "value": None}),
("threshold_entity_not_found", {"entity_id": "sensor.upper"}),
]
did_not_trigger_reports.clear()
assert did_not_trigger_reports == []
# Test the trigger does not fire when the upper sensor state is not numeric
for invalid_value in ("cat", None):
@@ -3432,17 +3037,7 @@ async def test_numerical_state_attribute_crossed_threshold_error_handling(
hass.states.async_set("test.test_entity", "on", {"test_attribute": 50})
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
(
"entity_value_not_numeric",
{"entity_id": "test.test_entity", "value": None},
),
(
"threshold_value_not_numeric",
{"entity_id": "sensor.upper", "value": str(invalid_value)},
),
]
did_not_trigger_reports.clear()
assert did_not_trigger_reports == []
unsub()
@@ -3823,13 +3418,7 @@ async def test_numerical_state_attribute_crossed_threshold_with_unit_error_handl
)
await hass.async_block_till_done()
assert calls == []
assert _reported_reasons(did_not_trigger_reports) == [
(
"entity_unit_not_supported",
{"entity_id": "test.test_entity", "unit": "invalid_unit"},
)
]
did_not_trigger_reports.clear()
assert did_not_trigger_reports == []
unsub()