Improve sql config flow (#150757)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
pull/152064/head
G Johansson 2025-09-11 12:27:48 +02:00 committed by GitHub
parent e65b4292b2
commit 1428b41a25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 964 additions and 754 deletions

View File

@ -3,6 +3,7 @@
from __future__ import annotations
import logging
from typing import Any
import sqlparse
import voluptuous as vol
@ -32,7 +33,13 @@ from homeassistant.helpers.trigger_template_entity import (
)
from homeassistant.helpers.typing import ConfigType
from .const import CONF_COLUMN_NAME, CONF_QUERY, DOMAIN, PLATFORMS
from .const import (
CONF_ADVANCED_OPTIONS,
CONF_COLUMN_NAME,
CONF_QUERY,
DOMAIN,
PLATFORMS,
)
from .util import redact_credentials
_LOGGER = logging.getLogger(__name__)
@ -75,18 +82,6 @@ CONFIG_SCHEMA = vol.Schema(
)
def remove_configured_db_url_if_not_needed(
hass: HomeAssistant, entry: ConfigEntry
) -> None:
"""Remove db url from config if it matches recorder database."""
hass.config_entries.async_update_entry(
entry,
options={
key: value for key, value in entry.options.items() if key != CONF_DB_URL
},
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up SQL from yaml config."""
if (conf := config.get(DOMAIN)) is None:
@ -107,8 +102,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
redact_credentials(entry.options.get(CONF_DB_URL)),
redact_credentials(get_instance(hass).db_url),
)
if entry.options.get(CONF_DB_URL) == get_instance(hass).db_url:
remove_configured_db_url_if_not_needed(hass, entry)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -119,3 +112,47 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload SQL config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
if entry.version > 1:
# This means the user has downgraded from a future version
return False
if entry.version == 1:
old_options = {**entry.options}
new_data = {}
new_options: dict[str, Any] = {}
if (db_url := old_options.get(CONF_DB_URL)) and db_url != get_instance(
hass
).db_url:
new_data[CONF_DB_URL] = db_url
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] = {}
for key in (
CONF_VALUE_TEMPLATE,
CONF_UNIT_OF_MEASUREMENT,
CONF_DEVICE_CLASS,
CONF_STATE_CLASS,
):
if (value := old_options.get(key)) is not None:
new_options[CONF_ADVANCED_OPTIONS][key] = value
hass.config_entries.async_update_entry(
entry, data=new_data, options=new_options, version=2
)
_LOGGER.debug(
"Migration to version %s.%s successful",
entry.version,
entry.minor_version,
)
return True

View File

@ -6,7 +6,7 @@ import logging
from typing import Any
import sqlalchemy
from sqlalchemy.engine import Result
from sqlalchemy.engine import Engine, Result
from sqlalchemy.exc import MultipleResultsFound, NoSuchColumnError, SQLAlchemyError
from sqlalchemy.orm import Session, scoped_session, sessionmaker
import sqlparse
@ -32,9 +32,10 @@ from homeassistant.const import (
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import section
from homeassistant.helpers import selector
from .const import CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
from .const import CONF_ADVANCED_OPTIONS, CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
from .util import resolve_db_url
_LOGGER = logging.getLogger(__name__)
@ -42,40 +43,38 @@ _LOGGER = logging.getLogger(__name__)
OPTIONS_SCHEMA: vol.Schema = vol.Schema(
{
vol.Optional(
CONF_DB_URL,
): selector.TextSelector(),
vol.Required(
CONF_COLUMN_NAME,
): selector.TextSelector(),
vol.Required(
CONF_QUERY,
): selector.TextSelector(selector.TextSelectorConfig(multiline=True)),
vol.Optional(
CONF_UNIT_OF_MEASUREMENT,
): selector.TextSelector(),
vol.Optional(
CONF_VALUE_TEMPLATE,
): selector.TemplateSelector(),
vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
cls.value
for cls in SensorDeviceClass
if cls != SensorDeviceClass.ENUM
],
mode=selector.SelectSelectorMode.DROPDOWN,
translation_key="device_class",
sort=True,
)
vol.Required(CONF_QUERY): selector.TextSelector(
selector.TextSelectorConfig(multiline=True)
),
vol.Optional(CONF_STATE_CLASS): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[cls.value for cls in SensorStateClass],
mode=selector.SelectSelectorMode.DROPDOWN,
translation_key="state_class",
sort=True,
)
vol.Required(CONF_COLUMN_NAME): selector.TextSelector(),
vol.Required(CONF_ADVANCED_OPTIONS): section(
vol.Schema(
{
vol.Optional(CONF_VALUE_TEMPLATE): selector.TemplateSelector(),
vol.Optional(CONF_UNIT_OF_MEASUREMENT): selector.TextSelector(),
vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
cls.value
for cls in SensorDeviceClass
if cls != SensorDeviceClass.ENUM
],
mode=selector.SelectSelectorMode.DROPDOWN,
translation_key="device_class",
sort=True,
)
),
vol.Optional(CONF_STATE_CLASS): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[cls.value for cls in SensorStateClass],
mode=selector.SelectSelectorMode.DROPDOWN,
translation_key="state_class",
sort=True,
)
),
}
),
{"collapsed": True},
),
}
)
@ -83,8 +82,9 @@ OPTIONS_SCHEMA: vol.Schema = vol.Schema(
CONFIG_SCHEMA: vol.Schema = vol.Schema(
{
vol.Required(CONF_NAME, default="Select SQL Query"): selector.TextSelector(),
vol.Optional(CONF_DB_URL): selector.TextSelector(),
}
).extend(OPTIONS_SCHEMA.schema)
)
def validate_sql_select(value: str) -> str:
@ -99,6 +99,31 @@ def validate_sql_select(value: str) -> str:
return str(query[0])
def validate_db_connection(db_url: str) -> bool:
"""Validate db connection."""
engine: Engine | None = None
sess: Session | None = None
try:
engine = sqlalchemy.create_engine(db_url, future=True)
sessmaker = scoped_session(sessionmaker(bind=engine, future=True))
sess = sessmaker()
sess.execute(sqlalchemy.text("select 1 as value"))
except SQLAlchemyError as error:
_LOGGER.debug("Execution error %s", error)
if sess:
sess.close()
if engine:
engine.dispose()
raise
if sess:
sess.close()
engine.dispose()
return True
def validate_query(db_url: str, query: str, column: str) -> bool:
"""Validate SQL query."""
@ -136,7 +161,9 @@ def validate_query(db_url: str, query: str, column: str) -> bool:
class SQLConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for SQL integration."""
VERSION = 1
VERSION = 2
data: dict[str, Any]
@staticmethod
@callback
@ -151,17 +178,46 @@ class SQLConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle the user step."""
errors = {}
description_placeholders = {}
if user_input is not None:
db_url = user_input.get(CONF_DB_URL)
try:
db_url_for_validation = resolve_db_url(self.hass, db_url)
await self.hass.async_add_executor_job(
validate_db_connection, db_url_for_validation
)
except SQLAlchemyError:
errors["db_url"] = "db_url_invalid"
if not errors:
self.data = {CONF_NAME: user_input[CONF_NAME]}
if db_url and db_url_for_validation != get_instance(self.hass).db_url:
self.data[CONF_DB_URL] = db_url
return await self.async_step_options()
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(CONFIG_SCHEMA, user_input),
errors=errors,
)
async def async_step_options(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the user step."""
errors = {}
description_placeholders = {}
if user_input is not None:
query = user_input[CONF_QUERY]
column = user_input[CONF_COLUMN_NAME]
db_url_for_validation = None
try:
query = validate_sql_select(query)
db_url_for_validation = resolve_db_url(self.hass, db_url)
db_url_for_validation = resolve_db_url(
self.hass, self.data.get(CONF_DB_URL)
)
await self.hass.async_add_executor_job(
validate_query, db_url_for_validation, query, column
)
@ -178,32 +234,25 @@ class SQLConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("Invalid query: %s", err)
errors["query"] = "query_invalid"
options = {
CONF_QUERY: query,
CONF_COLUMN_NAME: column,
CONF_NAME: user_input[CONF_NAME],
mod_advanced_options = {
k: v
for k, v in user_input[CONF_ADVANCED_OPTIONS].items()
if v is not None
}
if uom := user_input.get(CONF_UNIT_OF_MEASUREMENT):
options[CONF_UNIT_OF_MEASUREMENT] = uom
if value_template := user_input.get(CONF_VALUE_TEMPLATE):
options[CONF_VALUE_TEMPLATE] = value_template
if device_class := user_input.get(CONF_DEVICE_CLASS):
options[CONF_DEVICE_CLASS] = device_class
if state_class := user_input.get(CONF_STATE_CLASS):
options[CONF_STATE_CLASS] = state_class
if db_url_for_validation != get_instance(self.hass).db_url:
options[CONF_DB_URL] = db_url_for_validation
user_input[CONF_ADVANCED_OPTIONS] = mod_advanced_options
if not errors:
name = self.data[CONF_NAME]
self.data.pop(CONF_NAME)
return self.async_create_entry(
title=user_input[CONF_NAME],
data={},
options=options,
title=name,
data=self.data,
options=user_input,
)
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(CONFIG_SCHEMA, user_input),
step_id="options",
data_schema=self.add_suggested_values_to_schema(OPTIONS_SCHEMA, user_input),
errors=errors,
description_placeholders=description_placeholders,
)
@ -220,10 +269,9 @@ class SQLOptionsFlowHandler(OptionsFlowWithReload):
description_placeholders = {}
if user_input is not None:
db_url = user_input.get(CONF_DB_URL)
db_url = self.config_entry.data.get(CONF_DB_URL)
query = user_input[CONF_QUERY]
column = user_input[CONF_COLUMN_NAME]
name = self.config_entry.options.get(CONF_NAME, self.config_entry.title)
try:
query = validate_sql_select(query)
@ -252,24 +300,15 @@ class SQLOptionsFlowHandler(OptionsFlowWithReload):
recorder_db,
)
options = {
CONF_QUERY: query,
CONF_COLUMN_NAME: column,
CONF_NAME: name,
mod_advanced_options = {
k: v
for k, v in user_input[CONF_ADVANCED_OPTIONS].items()
if v is not None
}
if uom := user_input.get(CONF_UNIT_OF_MEASUREMENT):
options[CONF_UNIT_OF_MEASUREMENT] = uom
if value_template := user_input.get(CONF_VALUE_TEMPLATE):
options[CONF_VALUE_TEMPLATE] = value_template
if device_class := user_input.get(CONF_DEVICE_CLASS):
options[CONF_DEVICE_CLASS] = device_class
if state_class := user_input.get(CONF_STATE_CLASS):
options[CONF_STATE_CLASS] = state_class
if db_url_for_validation != get_instance(self.hass).db_url:
options[CONF_DB_URL] = db_url_for_validation
user_input[CONF_ADVANCED_OPTIONS] = mod_advanced_options
return self.async_create_entry(
data=options,
data=user_input,
)
return self.async_show_form(

View File

@ -9,4 +9,5 @@ PLATFORMS = [Platform.SENSOR]
CONF_COLUMN_NAME = "column"
CONF_QUERY = "query"
CONF_ADVANCED_OPTIONS = "advanced_options"
DB_URL_RE = re.compile("//.*:.*@")

View File

@ -49,7 +49,7 @@ from homeassistant.helpers.trigger_template_entity import (
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
from .const import CONF_ADVANCED_OPTIONS, CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
from .models import SQLData
from .util import redact_credentials, resolve_db_url
@ -111,10 +111,10 @@ async def async_setup_entry(
) -> None:
"""Set up the SQL sensor from config entry."""
db_url: str = resolve_db_url(hass, entry.options.get(CONF_DB_URL))
name: str = entry.options[CONF_NAME]
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.get(CONF_VALUE_TEMPLATE)
template: str | None = entry.options[CONF_ADVANCED_OPTIONS].get(CONF_VALUE_TEMPLATE)
column_name: str = entry.options[CONF_COLUMN_NAME]
value_template: ValueTemplate | None = None
@ -128,9 +128,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:
if key not in entry.options[CONF_ADVANCED_OPTIONS]:
continue
trigger_entity_config[key] = entry.options[key]
trigger_entity_config[key] = entry.options[CONF_ADVANCED_OPTIONS][key]
await async_setup_sensor(
hass,

View File

@ -14,23 +14,39 @@
"user": {
"data": {
"db_url": "Database URL",
"name": "[%key:common::config_flow::data::name%]",
"query": "Select query",
"column": "Column",
"unit_of_measurement": "Unit of measurement",
"value_template": "Value template",
"device_class": "Device class",
"state_class": "State class"
"name": "[%key:common::config_flow::data::name%]"
},
"data_description": {
"db_url": "Leave empty to use Home Assistant Recorder database",
"name": "Name that will be used for config entry and also the sensor",
"name": "Name that will be used for config entry and also the sensor"
}
},
"options": {
"data": {
"query": "Select query",
"column": "Column"
},
"data_description": {
"query": "Query to run, needs to start with 'SELECT'",
"column": "Column for returned query to present as state",
"unit_of_measurement": "The unit of measurement for the sensor (optional)",
"value_template": "Template to extract a value from the payload (optional)",
"device_class": "The type/class of the sensor to set the icon in the frontend",
"state_class": "The state class of the sensor"
"column": "Column for returned query to present as state"
},
"sections": {
"advanced_options": {
"name": "Advanced options",
"description": "Provide additional configuration to the sensor",
"data": {
"unit_of_measurement": "Unit of measurement",
"value_template": "Value template",
"device_class": "Device class",
"state_class": "State class"
},
"data_description": {
"unit_of_measurement": "The unit of measurement for the sensor (optional)",
"value_template": "Template to extract a value from the payload (optional)",
"device_class": "The type/class of the sensor to set the icon in the frontend",
"state_class": "The state class of the sensor"
}
}
}
}
}
@ -39,24 +55,30 @@
"step": {
"init": {
"data": {
"db_url": "[%key:component::sql::config::step::user::data::db_url%]",
"name": "[%key:common::config_flow::data::name%]",
"query": "[%key:component::sql::config::step::user::data::query%]",
"column": "[%key:component::sql::config::step::user::data::column%]",
"unit_of_measurement": "[%key:component::sql::config::step::user::data::unit_of_measurement%]",
"value_template": "[%key:component::sql::config::step::user::data::value_template%]",
"device_class": "[%key:component::sql::config::step::user::data::device_class%]",
"state_class": "[%key:component::sql::config::step::user::data::state_class%]"
"query": "[%key:component::sql::config::step::options::data::query%]",
"column": "[%key:component::sql::config::step::options::data::column%]"
},
"data_description": {
"db_url": "[%key:component::sql::config::step::user::data_description::db_url%]",
"name": "[%key:component::sql::config::step::user::data_description::name%]",
"query": "[%key:component::sql::config::step::user::data_description::query%]",
"column": "[%key:component::sql::config::step::user::data_description::column%]",
"unit_of_measurement": "[%key:component::sql::config::step::user::data_description::unit_of_measurement%]",
"value_template": "[%key:component::sql::config::step::user::data_description::value_template%]",
"device_class": "[%key:component::sql::config::step::user::data_description::device_class%]",
"state_class": "[%key:component::sql::config::step::user::data_description::state_class%]"
"query": "[%key:component::sql::config::step::options::data_description::query%]",
"column": "[%key:component::sql::config::step::options::data_description::column%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::sql::config::step::options::sections::advanced_options::name%]",
"description": "[%key:component::sql::config::step::options::sections::advanced_options::name%]",
"data": {
"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::advanced_options::data::device_class%]",
"state_class": "[%key:component::sql::config::step::options::sections::advanced_options::data::state_class%]"
},
"data_description": {
"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::advanced_options::data_description::device_class%]",
"state_class": "[%key:component::sql::config::step::options::sections::advanced_options::data_description::state_class%]"
}
}
}
}
},

View File

@ -10,7 +10,12 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.components.sql.const import CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
from homeassistant.components.sql.const import (
CONF_ADVANCED_OPTIONS,
CONF_COLUMN_NAME,
CONF_QUERY,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import (
CONF_DEVICE_CLASS,
@ -30,140 +35,167 @@ from homeassistant.helpers.trigger_template_entity import (
from tests.common import MockConfigEntry
ENTRY_CONFIG = {
CONF_NAME: "Get Value",
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
},
}
ENTRY_CONFIG_WITH_VALUE_TEMPLATE = {
CONF_NAME: "Get Value",
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_VALUE_TEMPLATE: "{{ value }}",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_VALUE_TEMPLATE: "{{ value }}",
},
}
ENTRY_CONFIG_INVALID_QUERY = {
CONF_NAME: "Get Value",
CONF_QUERY: "SELECT 5 FROM as value",
CONF_COLUMN_NAME: "size",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_INVALID_QUERY_2 = {
CONF_NAME: "Get Value",
CONF_QUERY: "SELECT5 FROM as value",
CONF_COLUMN_NAME: "size",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_INVALID_QUERY_3 = {
CONF_NAME: "Get Value",
CONF_QUERY: ";;",
CONF_COLUMN_NAME: "size",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_INVALID_QUERY_OPT = {
CONF_QUERY: "SELECT 5 FROM as value",
CONF_COLUMN_NAME: "size",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_INVALID_QUERY_2_OPT = {
CONF_QUERY: "SELECT5 FROM as value",
CONF_COLUMN_NAME: "size",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_INVALID_QUERY_3_OPT = {
CONF_QUERY: ";;",
CONF_COLUMN_NAME: "size",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_QUERY_READ_ONLY_CTE = {
CONF_NAME: "Get Value",
CONF_QUERY: "WITH test AS (SELECT 1 AS row_num, 10 AS state) SELECT state FROM test WHERE row_num = 1 LIMIT 1;",
CONF_COLUMN_NAME: "state",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_QUERY_NO_READ_ONLY = {
CONF_NAME: "Get Value",
CONF_QUERY: "UPDATE states SET state = 999999 WHERE state_id = 11125",
CONF_COLUMN_NAME: "state",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_QUERY_NO_READ_ONLY_CTE = {
CONF_NAME: "Get Value",
CONF_QUERY: "WITH test AS (SELECT state FROM states) UPDATE states SET states.state = test.state;",
CONF_COLUMN_NAME: "size",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_QUERY_READ_ONLY_CTE_OPT = {
CONF_QUERY: "WITH test AS (SELECT 1 AS row_num, 10 AS state) SELECT state FROM test WHERE row_num = 1 LIMIT 1;",
CONF_COLUMN_NAME: "state",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_QUERY_NO_READ_ONLY_OPT = {
CONF_QUERY: "UPDATE 5 as value",
CONF_COLUMN_NAME: "size",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_QUERY_NO_READ_ONLY_CTE_OPT = {
CONF_QUERY: "WITH test AS (SELECT state FROM states) UPDATE states SET states.state = test.state;",
CONF_COLUMN_NAME: "size",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_MULTIPLE_QUERIES = {
CONF_NAME: "Get Value",
CONF_QUERY: "SELECT 5 as state; UPDATE states SET state = 10;",
CONF_COLUMN_NAME: "state",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_MULTIPLE_QUERIES_OPT = {
CONF_QUERY: "SELECT 5 as state; UPDATE states SET state = 10;",
CONF_COLUMN_NAME: "state",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_INVALID_COLUMN_NAME = {
CONF_NAME: "Get Value",
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "size",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_INVALID_COLUMN_NAME_OPT = {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "size",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
ENTRY_CONFIG_NO_RESULTS = {
CONF_NAME: "Get Value",
CONF_QUERY: "SELECT kalle as value from no_table;",
CONF_COLUMN_NAME: "value",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
}
YAML_CONFIG = {
@ -260,22 +292,29 @@ YAML_CONFIG_ALL_TEMPLATES = {
}
async def init_integration(
async def init_integration( # pylint: disable=dangerous-default-value
hass: HomeAssistant,
config: dict[str, Any] | None = None,
*,
title: str = "Select value SQL query",
config: dict[str, Any] = {},
options: dict[str, Any] | None = None,
entry_id: str = "1",
source: str = SOURCE_USER,
) -> MockConfigEntry:
"""Set up the SQL integration in Home Assistant."""
if not config:
config = ENTRY_CONFIG
if not options:
options = ENTRY_CONFIG
if CONF_ADVANCED_OPTIONS not in options:
options[CONF_ADVANCED_OPTIONS] = {}
config_entry = MockConfigEntry(
title=title,
domain=DOMAIN,
source=source,
data={},
options=config,
data=config,
options=options,
entry_id=entry_id,
version=2,
)
config_entry.add_to_hass(hass)

View File

@ -0,0 +1,17 @@
"""Fixtures for the SQL integration."""
from __future__ import annotations
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.sql.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry

File diff suppressed because it is too large Load Diff

View File

@ -7,16 +7,33 @@ from unittest.mock import patch
import pytest
import voluptuous as vol
from homeassistant.components.recorder import Recorder
from homeassistant.components.recorder.util import get_instance
from homeassistant.components.recorder import CONF_DB_URL, Recorder
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.components.sql import validate_sql_select
from homeassistant.components.sql.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.components.sql.const import (
CONF_ADVANCED_OPTIONS,
CONF_COLUMN_NAME,
CONF_QUERY,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from . import YAML_CONFIG_INVALID, YAML_CONFIG_NO_DB, init_integration
from tests.common import MockConfigEntry
async def test_setup_entry(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test setup entry."""
@ -86,39 +103,71 @@ async def test_multiple_queries(hass: HomeAssistant) -> None:
validate_sql_select("SELECT 5 as value; UPDATE states SET state = 10;")
async def test_remove_configured_db_url_if_not_needed_when_not_needed(
recorder_mock: Recorder,
hass: HomeAssistant,
async def test_migration_from_future(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test configured db_url is replaced with None if matching the recorder db."""
recorder_db_url = get_instance(hass).db_url
"""Test migration from future version fails."""
config_entry = MockConfigEntry(
title="Test future",
domain=DOMAIN,
source=SOURCE_USER,
data={},
options={
CONF_QUERY: "SELECT 5.01 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {},
},
entry_id="1",
version=3,
)
config = {
"db_url": recorder_db_url,
"query": "SELECT 5 as value",
"column": "value",
"name": "count_tables",
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.MIGRATION_ERROR
async def test_migration_from_v1_to_v2(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test migration from version 1 to 2."""
config_entry = MockConfigEntry(
title="Test migration",
domain=DOMAIN,
source=SOURCE_USER,
data={},
options={
CONF_DB_URL: "sqlite://",
CONF_NAME: "Test migration",
CONF_QUERY: "SELECT 5.01 as value",
CONF_COLUMN_NAME: "value",
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=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.data == {}
assert config_entry.options == {
CONF_QUERY: "SELECT 5.01 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_VALUE_TEMPLATE: "{{ value | int }}",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
}
config_entry = await init_integration(hass, config)
assert config_entry.options.get("db_url") is None
async def test_remove_configured_db_url_if_not_needed_when_needed(
recorder_mock: Recorder,
hass: HomeAssistant,
) -> None:
"""Test configured db_url is not replaced if it differs from the recorder db."""
db_url = "mssql://"
config = {
"db_url": db_url,
"query": "SELECT 5 as value",
"column": "value",
"name": "count_tables",
}
config_entry = await init_integration(hass, config)
assert config_entry.options.get("db_url") == db_url
state = hass.states.get("sensor.test_migration")
assert state.state == "5"

View File

@ -12,14 +12,27 @@ from freezegun.api import FrozenDateTimeFactory
import pytest
from sqlalchemy.exc import SQLAlchemyError
from homeassistant.components.recorder import Recorder
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.components.sql.const import CONF_QUERY, DOMAIN
from homeassistant.components.recorder import CONF_DB_URL, Recorder
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.components.sql.const import (
CONF_ADVANCED_OPTIONS,
CONF_COLUMN_NAME,
CONF_QUERY,
DOMAIN,
)
from homeassistant.components.sql.sensor import _generate_lambda_stmt
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_ICON,
CONF_NAME,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfInformation,
@ -37,7 +50,6 @@ from . import (
YAML_CONFIG_FULL_TABLE_SCAN,
YAML_CONFIG_FULL_TABLE_SCAN_NO_UNIQUE_ID,
YAML_CONFIG_FULL_TABLE_SCAN_WITH_MULTIPLE_COLUMNS,
YAML_CONFIG_WITH_VIEW_THAT_CONTAINS_ENTITY_ID,
init_integration,
)
@ -46,14 +58,11 @@ from tests.common import MockConfigEntry, async_fire_time_changed
async def test_query_basic(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test the SQL sensor."""
config = {
"db_url": "sqlite://",
"query": "SELECT 5 as value",
"column": "value",
"name": "Select value SQL query",
"unique_id": "very_unique_id",
options = {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
}
await init_integration(hass, config)
await init_integration(hass, title="Select value SQL query", options=options)
state = hass.states.get("sensor.select_value_sql_query")
assert state.state == "5"
@ -62,14 +71,11 @@ async def test_query_basic(recorder_mock: Recorder, hass: HomeAssistant) -> None
async def test_query_cte(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test the SQL sensor with CTE."""
config = {
"db_url": "sqlite://",
"query": "WITH test AS (SELECT 1 AS row_num, 10 AS state) SELECT state FROM test WHERE row_num = 1 LIMIT 1;",
"column": "state",
"name": "Select value SQL query CTE",
"unique_id": "very_unique_id",
options = {
CONF_QUERY: "WITH test AS (SELECT 1 AS row_num, 10 AS state) SELECT state FROM test WHERE row_num = 1 LIMIT 1;",
CONF_COLUMN_NAME: "state",
}
await init_integration(hass, config)
await init_integration(hass, title="Select value SQL query CTE", options=options)
state = hass.states.get("sensor.select_value_sql_query_cte")
assert state.state == "10"
@ -80,31 +86,39 @@ async def test_query_value_template(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test the SQL sensor."""
config = {
"db_url": "sqlite://",
"query": "SELECT 5.01 as value",
"column": "value",
"name": "count_tables",
"value_template": "{{ value | int }}",
options = {
CONF_QUERY: "SELECT 5.01 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_VALUE_TEMPLATE: "{{ value | int }}",
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
}
await init_integration(hass, config)
await init_integration(hass, title="count_tables", options=options)
state = hass.states.get("sensor.count_tables")
assert state.state == "5"
assert state.attributes == {
"device_class": "data_size",
"friendly_name": "count_tables",
"state_class": "measurement",
"unit_of_measurement": "MiB",
"value": 5.01,
}
async def test_query_value_template_invalid(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test the SQL sensor."""
config = {
"db_url": "sqlite://",
"query": "SELECT 5.01 as value",
"column": "value",
"name": "count_tables",
"value_template": "{{ value | dontwork }}",
options = {
CONF_QUERY: "SELECT 5.01 as value",
CONF_COLUMN_NAME: "value",
CONF_VALUE_TEMPLATE: "{{ value | dontwork }}",
}
await init_integration(hass, config)
await init_integration(hass, title="count_tables", options=options)
state = hass.states.get("sensor.count_tables")
assert state.state == "5.01"
@ -112,13 +126,11 @@ async def test_query_value_template_invalid(
async def test_query_limit(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test the SQL sensor with a query containing 'LIMIT' in lowercase."""
config = {
"db_url": "sqlite://",
"query": "SELECT 5 as value limit 1",
"column": "value",
"name": "Select value SQL query",
options = {
CONF_QUERY: "SELECT 5 as value limit 1",
CONF_COLUMN_NAME: "value",
}
await init_integration(hass, config)
await init_integration(hass, options=options)
state = hass.states.get("sensor.select_value_sql_query")
assert state.state == "5"
@ -129,13 +141,11 @@ async def test_query_no_value(
recorder_mock: Recorder, hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test the SQL sensor with a query that returns no value."""
config = {
"db_url": "sqlite://",
"query": "SELECT 5 as value where 1=2",
"column": "value",
"name": "count_tables",
options = {
CONF_QUERY: "SELECT 5 as value where 1=2",
CONF_COLUMN_NAME: "value",
}
await init_integration(hass, config)
await init_integration(hass, title="count_tables", options=options)
state = hass.states.get("sensor.count_tables")
assert state.state == STATE_UNKNOWN
@ -163,13 +173,13 @@ async def test_query_on_disk_sqlite_no_result(
await hass.async_add_executor_job(make_test_db)
config = {
"db_url": db_path_str,
"query": "SELECT value from users",
"column": "value",
"name": "count_users",
config = {CONF_DB_URL: db_path_str}
options = {
CONF_QUERY: "SELECT value from users",
CONF_COLUMN_NAME: "value",
CONF_NAME: "count_users",
}
await init_integration(hass, config)
await init_integration(hass, title="count_users", options=options, config=config)
state = hass.states.get("sensor.count_users")
assert state.state == STATE_UNKNOWN
@ -203,17 +213,17 @@ async def test_invalid_url_setup(
) -> None:
"""Test invalid db url with redacted credentials."""
config = {
"db_url": url,
"query": "SELECT 5 as value",
"column": "value",
"name": "count_tables",
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
}
entry = MockConfigEntry(
title="count_tables",
domain=DOMAIN,
source=SOURCE_USER,
data={},
data={CONF_DB_URL: url},
options=config,
entry_id="1",
version=2,
)
entry.add_to_hass(hass)
@ -237,11 +247,9 @@ async def test_invalid_url_on_update(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test invalid db url with redacted credentials on retry."""
config = {
"db_url": "sqlite://",
"query": "SELECT 5 as value",
"column": "value",
"name": "count_tables",
options = {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
}
class MockSession:
@ -255,7 +263,7 @@ async def test_invalid_url_on_update(
"homeassistant.components.sql.sensor.scoped_session",
return_value=MockSession,
):
await init_integration(hass, config)
await init_integration(hass, title="count_tables", options=options)
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(minutes=1),
@ -343,12 +351,12 @@ async def test_config_from_old_yaml(
config = {
"sensor": {
"platform": "sql",
"db_url": "sqlite://",
CONF_DB_URL: "sqlite://",
"queries": [
{
"name": "count_tables",
"query": "SELECT 5 as value",
"column": "value",
CONF_NAME: "count_tables",
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
}
],
}
@ -386,10 +394,10 @@ async def test_invalid_url_setup_from_yaml(
"""Test invalid db url with redacted credentials from yaml setup."""
config = {
"sql": {
"db_url": url,
"query": "SELECT 5 as value",
"column": "value",
"name": "count_tables",
CONF_DB_URL: url,
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_NAME: "count_tables",
}
}
@ -417,9 +425,9 @@ async def test_attributes_from_yaml_setup(
state = hass.states.get("sensor.get_value")
assert state.state == "5"
assert state.attributes["device_class"] == SensorDeviceClass.DATA_SIZE
assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT
assert state.attributes["unit_of_measurement"] == UnitOfInformation.MEBIBYTES
assert state.attributes[CONF_DEVICE_CLASS] == SensorDeviceClass.DATA_SIZE
assert state.attributes[CONF_STATE_CLASS] == SensorStateClass.MEASUREMENT
assert state.attributes[CONF_UNIT_OF_MEASUREMENT] == UnitOfInformation.MEBIBYTES
async def test_binary_data_from_yaml_setup(
@ -455,7 +463,7 @@ async def test_issue_when_using_old_query(
issue = issue_registry.async_get_issue(
DOMAIN, f"entity_id_query_does_full_table_scan_{unique_id}"
)
assert issue.translation_placeholders == {"query": config[CONF_QUERY]}
assert issue.translation_placeholders == {CONF_QUERY: config[CONF_QUERY]}
@pytest.mark.parametrize(
@ -486,7 +494,7 @@ async def test_issue_when_using_old_query_without_unique_id(
issue = issue_registry.async_get_issue(
DOMAIN, f"entity_id_query_does_full_table_scan_{query}"
)
assert issue.translation_placeholders == {"query": query}
assert issue.translation_placeholders == {CONF_QUERY: query}
async def test_no_issue_when_view_has_the_text_entity_id_in_it(
@ -498,7 +506,12 @@ async def test_no_issue_when_view_has_the_text_entity_id_in_it(
"homeassistant.components.sql.sensor.scoped_session",
):
await init_integration(
hass, YAML_CONFIG_WITH_VIEW_THAT_CONTAINS_ENTITY_ID["sql"]
hass,
title="Get entity_id",
options={
CONF_QUERY: "SELECT value from view_sensor_db_unique_entity_ids;",
CONF_COLUMN_NAME: "value",
},
)
async_fire_time_changed(
hass,
@ -516,20 +529,18 @@ async def test_multiple_sensors_using_same_db(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test multiple sensors using the same db."""
config = {
"db_url": "sqlite:///",
"query": "SELECT 5 as value",
"column": "value",
"name": "Select value SQL query",
options = {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
}
config2 = {
"db_url": "sqlite:///",
"query": "SELECT 5 as value",
"column": "value",
"name": "Select value SQL query 2",
options2 = {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
}
await init_integration(hass, config)
await init_integration(hass, config2, entry_id="2")
await init_integration(hass, title="Select value SQL query", options=options)
await init_integration(
hass, title="Select value SQL query 2", options=options2, entry_id="2"
)
state = hass.states.get("sensor.select_value_sql_query")
assert state.state == "5"
@ -547,13 +558,14 @@ async def test_engine_is_disposed_at_stop(
recorder_mock: Recorder, hass: HomeAssistant
) -> None:
"""Test we dispose of the engine at stop."""
config = {
"db_url": "sqlite:///",
"query": "SELECT 5 as value",
"column": "value",
"name": "Select value SQL query",
config = {CONF_DB_URL: "sqlite:///"}
options = {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
}
await init_integration(hass, config)
await init_integration(
hass, title="Select value SQL query", config=config, options=options
)
state = hass.states.get("sensor.select_value_sql_query")
assert state.state == "5"
@ -572,13 +584,15 @@ async def test_attributes_from_entry_config(
await init_integration(
hass,
config={
"name": "Get Value - With",
"query": "SELECT 5 as value",
"column": "value",
"unit_of_measurement": "MiB",
"device_class": SensorDeviceClass.DATA_SIZE,
"state_class": SensorStateClass.TOTAL,
title="Get Value - With",
options={
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
},
},
entry_id="8693d4782ced4fb1ecca4743f29ab8f1",
)
@ -586,27 +600,29 @@ async def test_attributes_from_entry_config(
state = hass.states.get("sensor.get_value_with")
assert state.state == "5"
assert state.attributes["value"] == 5
assert state.attributes["unit_of_measurement"] == "MiB"
assert state.attributes["device_class"] == SensorDeviceClass.DATA_SIZE
assert state.attributes["state_class"] == SensorStateClass.TOTAL
assert state.attributes[CONF_UNIT_OF_MEASUREMENT] == "MiB"
assert state.attributes[CONF_DEVICE_CLASS] == SensorDeviceClass.DATA_SIZE
assert state.attributes[CONF_STATE_CLASS] == SensorStateClass.TOTAL
await init_integration(
hass,
config={
"name": "Get Value - Without",
"query": "SELECT 5 as value",
"column": "value",
"unit_of_measurement": "MiB",
title="Get Value - Without",
options={
CONF_QUERY: "SELECT 6 as value",
CONF_COLUMN_NAME: "value",
CONF_ADVANCED_OPTIONS: {
CONF_UNIT_OF_MEASUREMENT: "MiB",
},
},
entry_id="7aec7cd8045fba4778bb0621469e3cd9",
)
state = hass.states.get("sensor.get_value_without")
assert state.state == "5"
assert state.attributes["value"] == 5
assert state.attributes["unit_of_measurement"] == "MiB"
assert "device_class" not in state.attributes
assert "state_class" not in state.attributes
assert state.state == "6"
assert state.attributes["value"] == 6
assert state.attributes[CONF_UNIT_OF_MEASUREMENT] == "MiB"
assert CONF_DEVICE_CLASS not in state.attributes
assert CONF_STATE_CLASS not in state.attributes
async def test_query_recover_from_rollback(
@ -616,14 +632,12 @@ async def test_query_recover_from_rollback(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the SQL sensor."""
config = {
"db_url": "sqlite://",
"query": "SELECT 5 as value",
"column": "value",
"name": "Select value SQL query",
"unique_id": "very_unique_id",
options = {
CONF_QUERY: "SELECT 5 as value",
CONF_COLUMN_NAME: "value",
CONF_UNIQUE_ID: "very_unique_id",
}
await init_integration(hass, config)
await init_integration(hass, title="Select value SQL query", options=options)
platforms = async_get_platforms(hass, "sql")
sql_entity = platforms[0].entities["sensor.select_value_sql_query"]
@ -671,7 +685,7 @@ async def test_availability_blocks_value_template(
"""Test availability blocks value_template from rendering."""
error = "Error parsing value for sensor.get_value: 'x' is undefined"
config = YAML_CONFIG
config["sql"]["value_template"] = "{{ x - 0 }}"
config["sql"][CONF_VALUE_TEMPLATE] = "{{ x - 0 }}"
config["sql"]["availability"] = '{{ states("sensor.input1")=="on" }}'
hass.states.async_set("sensor.input1", "off")