Improve sql config flow (#150757)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>pull/152064/head
parent
e65b4292b2
commit
1428b41a25
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -9,4 +9,5 @@ PLATFORMS = [Platform.SENSOR]
|
|||
|
||||
CONF_COLUMN_NAME = "column"
|
||||
CONF_QUERY = "query"
|
||||
CONF_ADVANCED_OPTIONS = "advanced_options"
|
||||
DB_URL_RE = re.compile("//.*:.*@")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue