Add config flow to InfluxDB integration (#134463)

Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
This commit is contained in:
Rob Bierbooms
2026-02-19 10:33:32 +01:00
committed by GitHub
parent 3abaa99706
commit 7914ebe54e
12 changed files with 1942 additions and 270 deletions

4
CODEOWNERS generated
View File

@@ -792,8 +792,8 @@ build.json @home-assistant/supervisor
/tests/components/indevolt/ @xirtnl /tests/components/indevolt/ @xirtnl
/homeassistant/components/inels/ @epdevlab /homeassistant/components/inels/ @epdevlab
/tests/components/inels/ @epdevlab /tests/components/inels/ @epdevlab
/homeassistant/components/influxdb/ @mdegat01 /homeassistant/components/influxdb/ @mdegat01 @Robbie1221
/tests/components/influxdb/ @mdegat01 /tests/components/influxdb/ @mdegat01 @Robbie1221
/homeassistant/components/inkbird/ @bdraco /homeassistant/components/inkbird/ @bdraco
/tests/components/inkbird/ @bdraco /tests/components/inkbird/ @bdraco
/homeassistant/components/input_boolean/ @home-assistant/core /homeassistant/components/input_boolean/ @home-assistant/core

View File

@@ -20,10 +20,14 @@ import requests.exceptions
import urllib3.exceptions import urllib3.exceptions
import voluptuous as vol import voluptuous as vol
from homeassistant import config as conf_util
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_DOMAIN, CONF_DOMAIN,
CONF_ENTITY_ID, CONF_ENTITY_ID,
CONF_EXCLUDE,
CONF_HOST, CONF_HOST,
CONF_INCLUDE,
CONF_PASSWORD, CONF_PASSWORD,
CONF_PATH, CONF_PATH,
CONF_PORT, CONF_PORT,
@@ -34,17 +38,13 @@ from homeassistant.const import (
CONF_URL, CONF_URL,
CONF_USERNAME, CONF_USERNAME,
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
EVENT_HOMEASSISTANT_STOP,
EVENT_STATE_CHANGED, EVENT_STATE_CHANGED,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.helpers import ( from homeassistant.exceptions import ConfigEntryNotReady
config_validation as cv, from homeassistant.helpers import config_validation as cv, state as state_helper
event as event_helper,
state as state_helper,
)
from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.entity_values import EntityValues
from homeassistant.helpers.entityfilter import ( from homeassistant.helpers.entityfilter import (
INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA,
@@ -79,6 +79,7 @@ from .const import (
CONF_TAGS_ATTRIBUTES, CONF_TAGS_ATTRIBUTES,
CONNECTION_ERROR, CONNECTION_ERROR,
DEFAULT_API_VERSION, DEFAULT_API_VERSION,
DEFAULT_HOST,
DEFAULT_HOST_V2, DEFAULT_HOST_V2,
DEFAULT_MEASUREMENT_ATTR, DEFAULT_MEASUREMENT_ATTR,
DEFAULT_SSL_V2, DEFAULT_SSL_V2,
@@ -97,8 +98,6 @@ from .const import (
RE_DIGIT_TAIL, RE_DIGIT_TAIL,
RESUMED_MESSAGE, RESUMED_MESSAGE,
RETRY_DELAY, RETRY_DELAY,
RETRY_INTERVAL,
RETRY_MESSAGE,
TEST_QUERY_V1, TEST_QUERY_V1,
TEST_QUERY_V2, TEST_QUERY_V2,
TIMEOUT, TIMEOUT,
@@ -108,6 +107,8 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
type InfluxDBConfigEntry = ConfigEntry[InfluxThread]
def create_influx_url(conf: dict) -> dict: def create_influx_url(conf: dict) -> dict:
"""Build URL used from config inputs and default when necessary.""" """Build URL used from config inputs and default when necessary."""
@@ -198,8 +199,26 @@ INFLUX_SCHEMA = vol.All(
create_influx_url, create_influx_url,
) )
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{DOMAIN: INFLUX_SCHEMA}, {
DOMAIN: vol.All(
cv.deprecated(CONF_API_VERSION),
cv.deprecated(CONF_HOST),
cv.deprecated(CONF_PATH),
cv.deprecated(CONF_PORT),
cv.deprecated(CONF_SSL),
cv.deprecated(CONF_VERIFY_SSL),
cv.deprecated(CONF_SSL_CA_CERT),
cv.deprecated(CONF_USERNAME),
cv.deprecated(CONF_PASSWORD),
cv.deprecated(CONF_DB_NAME),
cv.deprecated(CONF_TOKEN),
cv.deprecated(CONF_ORG),
cv.deprecated(CONF_BUCKET),
INFLUX_SCHEMA,
)
},
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
@@ -349,8 +368,8 @@ def get_influx_connection( # noqa: C901
kwargs[CONF_TOKEN] = conf[CONF_TOKEN] kwargs[CONF_TOKEN] = conf[CONF_TOKEN]
kwargs[INFLUX_CONF_ORG] = conf[CONF_ORG] kwargs[INFLUX_CONF_ORG] = conf[CONF_ORG]
kwargs[CONF_VERIFY_SSL] = conf[CONF_VERIFY_SSL] kwargs[CONF_VERIFY_SSL] = conf[CONF_VERIFY_SSL]
if CONF_SSL_CA_CERT in conf: if (cert := conf.get(CONF_SSL_CA_CERT)) is not None:
kwargs[CONF_SSL_CA_CERT] = conf[CONF_SSL_CA_CERT] kwargs[CONF_SSL_CA_CERT] = cert
bucket = conf.get(CONF_BUCKET) bucket = conf.get(CONF_BUCKET)
influx = InfluxDBClientV2(**kwargs) influx = InfluxDBClientV2(**kwargs)
query_api = influx.query_api() query_api = influx.query_api()
@@ -406,31 +425,31 @@ def get_influx_connection( # noqa: C901
return InfluxClient(buckets, write_v2, query_v2, close_v2) return InfluxClient(buckets, write_v2, query_v2, close_v2)
# Else it's a V1 client # Else it's a V1 client
if CONF_SSL_CA_CERT in conf and conf[CONF_VERIFY_SSL]: if (cert := conf.get(CONF_SSL_CA_CERT)) is not None and conf[CONF_VERIFY_SSL]:
kwargs[CONF_VERIFY_SSL] = conf[CONF_SSL_CA_CERT] kwargs[CONF_VERIFY_SSL] = cert
else: else:
kwargs[CONF_VERIFY_SSL] = conf[CONF_VERIFY_SSL] kwargs[CONF_VERIFY_SSL] = conf[CONF_VERIFY_SSL]
if CONF_DB_NAME in conf: if (db_name := conf.get(CONF_DB_NAME)) is not None:
kwargs[CONF_DB_NAME] = conf[CONF_DB_NAME] kwargs[CONF_DB_NAME] = db_name
if CONF_USERNAME in conf: if (user_name := conf.get(CONF_USERNAME)) is not None:
kwargs[CONF_USERNAME] = conf[CONF_USERNAME] kwargs[CONF_USERNAME] = user_name
if CONF_PASSWORD in conf: if (password := conf.get(CONF_PASSWORD)) is not None:
kwargs[CONF_PASSWORD] = conf[CONF_PASSWORD] kwargs[CONF_PASSWORD] = password
if CONF_HOST in conf: if CONF_HOST in conf:
kwargs[CONF_HOST] = conf[CONF_HOST] kwargs[CONF_HOST] = conf[CONF_HOST]
if CONF_PATH in conf: if (path := conf.get(CONF_PATH)) is not None:
kwargs[CONF_PATH] = conf[CONF_PATH] kwargs[CONF_PATH] = path
if CONF_PORT in conf: if (port := conf.get(CONF_PORT)) is not None:
kwargs[CONF_PORT] = conf[CONF_PORT] kwargs[CONF_PORT] = port
if CONF_SSL in conf: if (ssl := conf.get(CONF_SSL)) is not None:
kwargs[CONF_SSL] = conf[CONF_SSL] kwargs[CONF_SSL] = ssl
influx = InfluxDBClient(**kwargs) influx = InfluxDBClient(**kwargs)
@@ -478,34 +497,79 @@ def get_influx_connection( # noqa: C901
return InfluxClient(databases, write_v1, query_v1, close_v1) return InfluxClient(databases, write_v1, query_v1, close_v1)
def _retry_setup(hass: HomeAssistant, config: ConfigType) -> None: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
setup(hass, config)
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the InfluxDB component.""" """Set up the InfluxDB component."""
conf = config[DOMAIN] conf = config.get(DOMAIN)
try:
influx = get_influx_connection(conf, test_write=True) if conf is not None:
except ConnectionError as exc: if CONF_HOST not in conf and conf[CONF_API_VERSION] == DEFAULT_API_VERSION:
_LOGGER.error(RETRY_MESSAGE, exc) conf[CONF_HOST] = DEFAULT_HOST
event_helper.call_later(
hass, RETRY_INTERVAL, lambda _: _retry_setup(hass, config) hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=conf,
)
) )
return True
event_to_json = _generate_event_to_json(conf) return True
max_tries = conf.get(CONF_RETRY_COUNT)
instance = hass.data[DOMAIN] = InfluxThread(hass, influx, event_to_json, max_tries)
instance.start()
def shutdown(event):
"""Shut down the thread."""
instance.queue.put(None)
instance.join()
influx.close()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) async def async_setup_entry(hass: HomeAssistant, entry: InfluxDBConfigEntry) -> bool:
"""Set up InfluxDB from a config entry."""
data = entry.data
hass_config = await conf_util.async_hass_config_yaml(hass)
influx_yaml = CONFIG_SCHEMA(hass_config).get(DOMAIN, {})
default_filter_settings: dict[str, Any] = {
"entity_globs": [],
"entities": [],
"domains": [],
}
options = {
CONF_RETRY_COUNT: influx_yaml.get(CONF_RETRY_COUNT, 0),
CONF_PRECISION: influx_yaml.get(CONF_PRECISION),
CONF_MEASUREMENT_ATTR: influx_yaml.get(
CONF_MEASUREMENT_ATTR, DEFAULT_MEASUREMENT_ATTR
),
CONF_DEFAULT_MEASUREMENT: influx_yaml.get(CONF_DEFAULT_MEASUREMENT),
CONF_OVERRIDE_MEASUREMENT: influx_yaml.get(CONF_OVERRIDE_MEASUREMENT),
CONF_INCLUDE: influx_yaml.get(CONF_INCLUDE, default_filter_settings),
CONF_EXCLUDE: influx_yaml.get(CONF_EXCLUDE, default_filter_settings),
CONF_TAGS: influx_yaml.get(CONF_TAGS, {}),
CONF_TAGS_ATTRIBUTES: influx_yaml.get(CONF_TAGS_ATTRIBUTES, []),
CONF_IGNORE_ATTRIBUTES: influx_yaml.get(CONF_IGNORE_ATTRIBUTES, []),
CONF_COMPONENT_CONFIG: influx_yaml.get(CONF_COMPONENT_CONFIG, {}),
CONF_COMPONENT_CONFIG_DOMAIN: influx_yaml.get(CONF_COMPONENT_CONFIG_DOMAIN, {}),
CONF_COMPONENT_CONFIG_GLOB: influx_yaml.get(CONF_COMPONENT_CONFIG_GLOB, {}),
}
config = data | options
try:
influx = await hass.async_add_executor_job(get_influx_connection, config, True)
except ConnectionError as err:
raise ConfigEntryNotReady(err) from err
influx_thread = InfluxThread(
hass, entry, influx, _generate_event_to_json(config), config[CONF_RETRY_COUNT]
)
await hass.async_add_executor_job(influx_thread.start)
entry.runtime_data = influx_thread
return True
async def async_unload_entry(hass: HomeAssistant, entry: InfluxDBConfigEntry) -> bool:
"""Unload a config entry."""
influx_thread = entry.runtime_data
# Run shutdown in the executor so the event loop isn't blocked
await hass.async_add_executor_job(influx_thread.shutdown)
return True return True
@@ -513,7 +577,14 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
class InfluxThread(threading.Thread): class InfluxThread(threading.Thread):
"""A threaded event handler class.""" """A threaded event handler class."""
def __init__(self, hass, influx, event_to_json, max_tries): def __init__(
self,
hass: HomeAssistant,
entry: InfluxDBConfigEntry,
influx: InfluxClient,
event_to_json: Callable[[Event], dict[str, Any] | None],
max_tries: int,
) -> None:
"""Initialize the listener.""" """Initialize the listener."""
threading.Thread.__init__(self, name=DOMAIN) threading.Thread.__init__(self, name=DOMAIN)
self.queue: queue.SimpleQueue[threading.Event | tuple[float, Event] | None] = ( self.queue: queue.SimpleQueue[threading.Event | tuple[float, Event] | None] = (
@@ -523,8 +594,16 @@ class InfluxThread(threading.Thread):
self.event_to_json = event_to_json self.event_to_json = event_to_json
self.max_tries = max_tries self.max_tries = max_tries
self.write_errors = 0 self.write_errors = 0
self.shutdown = False self._shutdown = False
hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener) entry.async_on_unload(
hass.bus.async_listen(EVENT_STATE_CHANGED, self._event_listener)
)
def shutdown(self) -> None:
"""Shutdown the influx thread."""
self.queue.put(None)
self.join()
self.influx.close()
@callback @callback
def _event_listener(self, event): def _event_listener(self, event):
@@ -547,13 +626,13 @@ class InfluxThread(threading.Thread):
dropped = 0 dropped = 0
with suppress(queue.Empty): with suppress(queue.Empty):
while len(json) < BATCH_BUFFER_SIZE and not self.shutdown: while len(json) < BATCH_BUFFER_SIZE and not self._shutdown:
timeout = None if count == 0 else self.batch_timeout() timeout = None if count == 0 else self.batch_timeout()
item = self.queue.get(timeout=timeout) item = self.queue.get(timeout=timeout)
count += 1 count += 1
if item is None: if item is None:
self.shutdown = True self._shutdown = True
elif type(item) is tuple: elif type(item) is tuple:
timestamp, event = item timestamp, event = item
age = time.monotonic() - timestamp age = time.monotonic() - timestamp
@@ -596,7 +675,7 @@ class InfluxThread(threading.Thread):
def run(self): def run(self):
"""Process incoming events.""" """Process incoming events."""
while not self.shutdown: while not self._shutdown:
_, json = self.get_events_json() _, json = self.get_events_json()
if json: if json:
self.write_to_influxdb(json) self.write_to_influxdb(json)

View File

@@ -0,0 +1,281 @@
"""Config flow for InfluxDB integration."""
import logging
from pathlib import Path
import shutil
from typing import Any
import voluptuous as vol
from yarl import URL
from homeassistant.components.file_upload import process_uploaded_file
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
CONF_PORT,
CONF_SSL,
CONF_TOKEN,
CONF_URL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.selector import (
FileSelector,
FileSelectorConfig,
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from homeassistant.helpers.storage import STORAGE_DIR
from . import DOMAIN, get_influx_connection
from .const import (
API_VERSION_2,
CONF_API_VERSION,
CONF_BUCKET,
CONF_DB_NAME,
CONF_ORG,
CONF_SSL_CA_CERT,
DEFAULT_API_VERSION,
DEFAULT_HOST,
DEFAULT_PORT,
)
_LOGGER = logging.getLogger(__name__)
INFLUXDB_V1_SCHEMA = vol.Schema(
{
vol.Required(
CONF_URL, default=f"http://{DEFAULT_HOST}:{DEFAULT_PORT}"
): TextSelector(
TextSelectorConfig(
type=TextSelectorType.URL,
autocomplete="url",
),
),
vol.Required(CONF_VERIFY_SSL, default=False): bool,
vol.Required(CONF_DB_NAME): TextSelector(
TextSelectorConfig(
type=TextSelectorType.TEXT,
),
),
vol.Optional(CONF_USERNAME): TextSelector(
TextSelectorConfig(
type=TextSelectorType.TEXT,
autocomplete="username",
),
),
vol.Optional(CONF_PASSWORD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD,
autocomplete="current-password",
),
),
vol.Optional(CONF_SSL_CA_CERT): FileSelector(
FileSelectorConfig(accept=".pem,.crt,.cer,.der")
),
}
)
INFLUXDB_V2_SCHEMA = vol.Schema(
{
vol.Required(CONF_URL, default="https://"): TextSelector(
TextSelectorConfig(
type=TextSelectorType.URL,
autocomplete="url",
),
),
vol.Required(CONF_VERIFY_SSL, default=False): bool,
vol.Required(CONF_ORG): TextSelector(
TextSelectorConfig(
type=TextSelectorType.TEXT,
),
),
vol.Required(CONF_BUCKET): TextSelector(
TextSelectorConfig(
type=TextSelectorType.TEXT,
),
),
vol.Required(CONF_TOKEN): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD,
),
),
vol.Optional(CONF_SSL_CA_CERT): FileSelector(
FileSelectorConfig(accept=".pem,.crt,.cer,.der")
),
}
)
async def _validate_influxdb_connection(
hass: HomeAssistant, data: dict[str, Any]
) -> dict[str, str]:
"""Validate connection to influxdb."""
def _test_connection() -> None:
influx = get_influx_connection(data, test_write=True)
influx.close()
errors = {}
try:
await hass.async_add_executor_job(_test_connection)
except ConnectionError as ex:
_LOGGER.error(ex)
if "SSLError" in ex.args[0]:
errors = {"base": "ssl_error"}
elif "database not found" in ex.args[0]:
errors = {"base": "invalid_database"}
elif "authorization failed" in ex.args[0]:
errors = {"base": "invalid_auth"}
elif "token" in ex.args[0]:
errors = {"base": "invalid_config"}
else:
errors = {"base": "cannot_connect"}
except Exception:
_LOGGER.exception("Unknown error")
errors = {"base": "unknown"}
return errors
async def _save_uploaded_cert_file(hass: HomeAssistant, uploaded_file_id: str) -> Path:
"""Move the uploaded file to storage directory."""
def _process_upload() -> Path:
with process_uploaded_file(hass, uploaded_file_id) as file_path:
dest_path = Path(hass.config.path(STORAGE_DIR, DOMAIN))
dest_path.mkdir(exist_ok=True)
file_name = f"influxdb{file_path.suffix}"
dest_file = dest_path / file_name
shutil.move(file_path, dest_file)
return dest_file
return await hass.async_add_executor_job(_process_upload)
class InfluxDBConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for InfluxDB."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Step when user initializes an integration."""
return self.async_show_menu(
step_id="user",
menu_options=["configure_v1", "configure_v2"],
)
async def async_step_configure_v1(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Step when user configures InfluxDB v1."""
errors: dict[str, str] = {}
if user_input is not None:
url = URL(user_input[CONF_URL])
data = {
CONF_API_VERSION: DEFAULT_API_VERSION,
CONF_HOST: url.host,
CONF_PORT: url.port,
CONF_USERNAME: user_input.get(CONF_USERNAME),
CONF_PASSWORD: user_input.get(CONF_PASSWORD),
CONF_DB_NAME: user_input[CONF_DB_NAME],
CONF_SSL: url.scheme == "https",
CONF_PATH: url.path,
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
}
if (cert := user_input.get(CONF_SSL_CA_CERT)) is not None:
path = await _save_uploaded_cert_file(self.hass, cert)
data[CONF_SSL_CA_CERT] = str(path)
errors = await _validate_influxdb_connection(self.hass, data)
if not errors:
title = f"{data[CONF_DB_NAME]} ({data[CONF_HOST]})"
return self.async_create_entry(title=title, data=data)
schema = INFLUXDB_V1_SCHEMA
return self.async_show_form(
step_id="configure_v1",
data_schema=self.add_suggested_values_to_schema(schema, user_input),
errors=errors,
)
async def async_step_configure_v2(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Step when user configures InfluxDB v2."""
errors: dict[str, str] = {}
if user_input is not None:
data = {
CONF_API_VERSION: API_VERSION_2,
CONF_URL: user_input[CONF_URL],
CONF_TOKEN: user_input[CONF_TOKEN],
CONF_ORG: user_input[CONF_ORG],
CONF_BUCKET: user_input[CONF_BUCKET],
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
}
if (cert := user_input.get(CONF_SSL_CA_CERT)) is not None:
path = await _save_uploaded_cert_file(self.hass, cert)
data[CONF_SSL_CA_CERT] = str(path)
errors = await _validate_influxdb_connection(self.hass, data)
if not errors:
title = f"{data[CONF_BUCKET]} ({data[CONF_URL]})"
return self.async_create_entry(title=title, data=data)
schema = INFLUXDB_V2_SCHEMA
return self.async_show_form(
step_id="configure_v2",
data_schema=self.add_suggested_values_to_schema(schema, user_input),
errors=errors,
)
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Handle the initial step."""
host = import_data.get(CONF_HOST)
database = import_data.get(CONF_DB_NAME)
bucket = import_data.get(CONF_BUCKET)
api_version = import_data.get(CONF_API_VERSION)
ssl = import_data.get(CONF_SSL)
if api_version == DEFAULT_API_VERSION:
title = f"{database} ({host})"
data = {
CONF_API_VERSION: api_version,
CONF_HOST: host,
CONF_PORT: import_data.get(CONF_PORT),
CONF_USERNAME: import_data.get(CONF_USERNAME),
CONF_PASSWORD: import_data.get(CONF_PASSWORD),
CONF_DB_NAME: database,
CONF_SSL: ssl,
CONF_PATH: import_data.get(CONF_PATH),
CONF_VERIFY_SSL: import_data.get(CONF_VERIFY_SSL),
CONF_SSL_CA_CERT: import_data.get(CONF_SSL_CA_CERT),
}
else:
url = import_data.get(CONF_URL)
title = f"{bucket} ({url})"
data = {
CONF_API_VERSION: api_version,
CONF_URL: import_data.get(CONF_URL),
CONF_TOKEN: import_data.get(CONF_TOKEN),
CONF_ORG: import_data.get(CONF_ORG),
CONF_BUCKET: bucket,
CONF_VERIFY_SSL: import_data.get(CONF_VERIFY_SSL),
CONF_SSL_CA_CERT: import_data.get(CONF_SSL_CA_CERT),
}
errors = await _validate_influxdb_connection(self.hass, data)
if errors:
return self.async_abort(reason=errors["base"])
return self.async_create_entry(title=title, data=data)

View File

@@ -48,7 +48,9 @@ CONF_QUERY = "query"
CONF_IMPORTS = "imports" CONF_IMPORTS = "imports"
DEFAULT_DATABASE = "home_assistant" DEFAULT_DATABASE = "home_assistant"
DEFAULT_HOST = "localhost"
DEFAULT_HOST_V2 = "us-west-2-1.aws.cloud2.influxdata.com" DEFAULT_HOST_V2 = "us-west-2-1.aws.cloud2.influxdata.com"
DEFAULT_PORT = 8086
DEFAULT_SSL_V2 = True DEFAULT_SSL_V2 = True
DEFAULT_BUCKET = "Home Assistant" DEFAULT_BUCKET = "Home Assistant"
DEFAULT_VERIFY_SSL = True DEFAULT_VERIFY_SSL = True
@@ -130,8 +132,8 @@ RENDERING_QUERY_ERROR_MESSAGE = "Could not render query template: %s."
RENDERING_WHERE_MESSAGE = "Rendering where: %s." RENDERING_WHERE_MESSAGE = "Rendering where: %s."
RENDERING_WHERE_ERROR_MESSAGE = "Could not render where template: %s." RENDERING_WHERE_ERROR_MESSAGE = "Could not render where template: %s."
COMPONENT_CONFIG_SCHEMA_CONNECTION = { COMPONENT_CONFIG_SCHEMA_CONNECTION = {
# Connection config for V1 and V2 APIs.
vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): vol.All( vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): vol.All(
vol.Coerce(str), vol.Coerce(str),
vol.In([DEFAULT_API_VERSION, API_VERSION_2]), vol.In([DEFAULT_API_VERSION, API_VERSION_2]),

View File

@@ -1,10 +1,13 @@
{ {
"domain": "influxdb", "domain": "influxdb",
"name": "InfluxDB", "name": "InfluxDB",
"codeowners": ["@mdegat01"], "codeowners": ["@mdegat01", "@Robbie1221"],
"config_flow": true,
"dependencies": ["file_upload"],
"documentation": "https://www.home-assistant.io/integrations/influxdb", "documentation": "https://www.home-assistant.io/integrations/influxdb",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["influxdb", "influxdb_client"], "loggers": ["influxdb", "influxdb_client"],
"quality_scale": "legacy", "quality_scale": "legacy",
"requirements": ["influxdb==5.3.1", "influxdb-client==1.50.0"] "requirements": ["influxdb==5.3.1", "influxdb-client==1.50.0"],
"single_config_entry": true
} }

View File

@@ -0,0 +1,58 @@
{
"common": {
"ssl_ca_cert": "SSL CA certificate (Optional)"
},
"config": {
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_config": "Invalid organization, bucket or token",
"invalid_database": "Invalid database",
"ssl_error": "SSL certificate error",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"configure_v1": {
"data": {
"database": "Database",
"password": "[%key:common::config_flow::data::password%]",
"ssl_ca_cert": "[%key:component::influxdb::common::ssl_ca_cert%]",
"url": "[%key:common::config_flow::data::url%]",
"username": "[%key:common::config_flow::data::username%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"database": "The name of the database.",
"ssl_ca_cert": "Path to the SSL certificate"
},
"title": "InfluxDB configuration"
},
"configure_v2": {
"data": {
"bucket": "Bucket",
"organization": "Organization",
"ssl_ca_cert": "[%key:component::influxdb::common::ssl_ca_cert%]",
"token": "[%key:common::config_flow::data::api_token%]",
"url": "[%key:common::config_flow::data::url%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"bucket": "The name of the bucket.",
"organization": "The name of the organization.",
"ssl_ca_cert": "Path to the SSL certificate"
},
"title": "InfluxDB configuration"
},
"import": {
"title": "Import configuration"
},
"user": {
"menu_options": {
"configure_v1": "InfluxDB v1.x",
"configure_v2": "InfluxDB v2.x / v3"
},
"title": "Choose InfluxDB version"
}
}
}
}

View File

@@ -331,6 +331,7 @@ FLOWS = {
"incomfort", "incomfort",
"indevolt", "indevolt",
"inels", "inels",
"influxdb",
"inkbird", "inkbird",
"insteon", "insteon",
"intelliclima", "intelliclima",

View File

@@ -3137,8 +3137,9 @@
"influxdb": { "influxdb": {
"name": "InfluxDB", "name": "InfluxDB",
"integration_type": "hub", "integration_type": "hub",
"config_flow": false, "config_flow": true,
"iot_class": "local_push" "iot_class": "local_push",
"single_config_entry": true
}, },
"inkbird": { "inkbird": {
"name": "INKBIRD", "name": "INKBIRD",

View File

@@ -1 +1,92 @@
"""Tests for the influxdb component.""" """Tests for the influxdb component."""
from homeassistant.components import influxdb
from homeassistant.components.influxdb import (
CONF_API_VERSION,
CONF_BUCKET,
CONF_COMPONENT_CONFIG,
CONF_COMPONENT_CONFIG_DOMAIN,
CONF_COMPONENT_CONFIG_GLOB,
CONF_DB_NAME,
CONF_IGNORE_ATTRIBUTES,
CONF_MEASUREMENT_ATTR,
CONF_ORG,
CONF_RETRY_COUNT,
CONF_SSL_CA_CERT,
CONF_TAGS,
CONF_TAGS_ATTRIBUTES,
)
from homeassistant.const import (
CONF_EXCLUDE,
CONF_HOST,
CONF_INCLUDE,
CONF_PASSWORD,
CONF_PATH,
CONF_PORT,
CONF_SSL,
CONF_TOKEN,
CONF_URL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.helpers.entityfilter import (
CONF_DOMAINS,
CONF_ENTITIES,
CONF_ENTITY_GLOBS,
)
BASE_V1_CONFIG = {
CONF_API_VERSION: influxdb.DEFAULT_API_VERSION,
CONF_HOST: "localhost",
CONF_PORT: None,
CONF_USERNAME: None,
CONF_PASSWORD: None,
CONF_SSL: None,
CONF_PATH: None,
CONF_DB_NAME: "home_assistant",
CONF_VERIFY_SSL: True,
CONF_SSL_CA_CERT: None,
}
BASE_V2_CONFIG = {
CONF_API_VERSION: influxdb.API_VERSION_2,
CONF_URL: "https://us-west-2-1.aws.cloud2.influxdata.com",
CONF_TOKEN: "token",
CONF_ORG: "org",
CONF_BUCKET: "Home Assistant",
CONF_VERIFY_SSL: True,
CONF_SSL_CA_CERT: None,
}
BASE_OPTIONS = {
CONF_RETRY_COUNT: 0,
CONF_INCLUDE: {
CONF_ENTITY_GLOBS: [],
CONF_ENTITIES: [],
CONF_DOMAINS: [],
},
CONF_EXCLUDE: {
CONF_ENTITY_GLOBS: [],
CONF_ENTITIES: [],
CONF_DOMAINS: [],
},
CONF_TAGS: {},
CONF_TAGS_ATTRIBUTES: [],
CONF_MEASUREMENT_ATTR: "unit_of_measurement",
CONF_IGNORE_ATTRIBUTES: [],
CONF_COMPONENT_CONFIG: {},
CONF_COMPONENT_CONFIG_GLOB: {},
CONF_COMPONENT_CONFIG_DOMAIN: {},
CONF_BUCKET: "Home Assistant",
}
INFLUX_PATH = "homeassistant.components.influxdb"
INFLUX_CLIENT_PATH = f"{INFLUX_PATH}.InfluxDBClient"
def _get_write_api_mock_v1(mock_influx_client):
"""Return the write api mock for the V1 client."""
return mock_influx_client.return_value.write_points
def _get_write_api_mock_v2(mock_influx_client):
"""Return the write api mock for the V2 client."""
return mock_influx_client.return_value.write_api.return_value.write

View File

@@ -0,0 +1,719 @@
"""Test the influxdb config flow."""
from collections.abc import Generator
from contextlib import contextmanager
from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
from influxdb.exceptions import InfluxDBClientError, InfluxDBServerError
import pytest
from homeassistant import config_entries
from homeassistant.components.influxdb import (
API_VERSION_2,
CONF_API_VERSION,
CONF_BUCKET,
CONF_DB_NAME,
CONF_ORG,
CONF_SSL_CA_CERT,
DEFAULT_API_VERSION,
DOMAIN,
ApiException,
)
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
CONF_PORT,
CONF_SSL,
CONF_TOKEN,
CONF_URL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from . import (
BASE_V1_CONFIG,
BASE_V2_CONFIG,
INFLUX_CLIENT_PATH,
_get_write_api_mock_v1,
_get_write_api_mock_v2,
)
from tests.common import MockConfigEntry
PATH_FIXTURE = Path("/influxdb.crt")
FIXTURE_UPLOAD_UUID = "0123456789abcdef0123456789abcdef"
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.influxdb.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture(name="mock_client")
def mock_client_fixture(
request: pytest.FixtureRequest,
) -> Generator[MagicMock]:
"""Patch the InfluxDBClient object with mock for version under test."""
if request.param == API_VERSION_2:
client_target = f"{INFLUX_CLIENT_PATH}V2"
else:
client_target = INFLUX_CLIENT_PATH
with patch(client_target) as client:
yield client
@contextmanager
def patch_file_upload(return_value=PATH_FIXTURE, side_effect=None):
"""Patch file upload. Yields the Path (return_value)."""
with (
patch(
"homeassistant.components.influxdb.config_flow.process_uploaded_file"
) as file_upload_mock,
patch("homeassistant.core_config.Config.path", return_value="/.storage"),
patch(
"pathlib.Path.mkdir",
) as mkdir_mock,
patch(
"shutil.move",
) as shutil_move_mock,
):
file_upload_mock.return_value.__enter__.return_value = PATH_FIXTURE
yield return_value
if side_effect:
mkdir_mock.assert_not_called()
shutil_move_mock.assert_not_called()
else:
mkdir_mock.assert_called_once()
shutil_move_mock.assert_called_once()
@pytest.mark.parametrize(
("mock_client", "config_base", "config_url", "get_write_api"),
[
(
DEFAULT_API_VERSION,
{
CONF_URL: "http://localhost:8086",
CONF_VERIFY_SSL: False,
CONF_DB_NAME: "home_assistant",
CONF_USERNAME: "user",
CONF_PASSWORD: "pass",
},
{
CONF_HOST: "localhost",
CONF_PORT: 8086,
CONF_SSL: False,
CONF_PATH: "/",
},
_get_write_api_mock_v1,
),
(
DEFAULT_API_VERSION,
{
CONF_URL: "http://localhost:8086",
CONF_VERIFY_SSL: False,
CONF_DB_NAME: "home_assistant",
},
{
CONF_HOST: "localhost",
CONF_PORT: 8086,
CONF_SSL: False,
CONF_PATH: "/",
},
_get_write_api_mock_v1,
),
(
DEFAULT_API_VERSION,
{
CONF_URL: "https://influxdb.mydomain.com",
CONF_VERIFY_SSL: True,
CONF_DB_NAME: "home_assistant",
CONF_USERNAME: "user",
CONF_PASSWORD: "pass",
},
{
CONF_HOST: "influxdb.mydomain.com",
CONF_PORT: 443,
CONF_SSL: True,
CONF_PATH: "/",
},
_get_write_api_mock_v1,
),
],
indirect=["mock_client"],
)
async def test_setup_v1(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_client: MagicMock,
config_base: dict[str, Any],
config_url: dict[str, Any],
get_write_api: Any,
) -> None:
"""Test we can setup an InfluxDB v1."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"next_step_id": "configure_v1"},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "configure_v1"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
config_base,
)
data = {
CONF_API_VERSION: "1",
CONF_HOST: config_url[CONF_HOST],
CONF_PORT: config_url[CONF_PORT],
CONF_USERNAME: config_base.get(CONF_USERNAME),
CONF_PASSWORD: config_base.get(CONF_PASSWORD),
CONF_DB_NAME: config_base[CONF_DB_NAME],
CONF_SSL: config_url[CONF_SSL],
CONF_PATH: config_url[CONF_PATH],
CONF_VERIFY_SSL: config_base[CONF_VERIFY_SSL],
}
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"{config_base['database']} ({config_url['host']})"
assert result["data"] == data
@pytest.mark.parametrize(
("mock_client", "config_base", "config_url", "get_write_api"),
[
(
DEFAULT_API_VERSION,
{
CONF_URL: "https://influxdb.mydomain.com",
CONF_VERIFY_SSL: True,
CONF_DB_NAME: "home_assistant",
CONF_USERNAME: "user",
CONF_PASSWORD: "pass",
CONF_SSL_CA_CERT: FIXTURE_UPLOAD_UUID,
},
{
CONF_HOST: "influxdb.mydomain.com",
CONF_PORT: 443,
CONF_SSL: True,
CONF_PATH: "/",
},
_get_write_api_mock_v1,
),
],
indirect=["mock_client"],
)
async def test_setup_v1_ssl_cert(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_client: MagicMock,
config_base: dict[str, Any],
config_url: dict[str, Any],
get_write_api: Any,
) -> None:
"""Test we can setup an InfluxDB v1 with SSL Certificate."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"next_step_id": "configure_v1"},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "configure_v1"
assert result["errors"] == {}
with (
patch_file_upload(),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
config_base,
)
data = {
CONF_API_VERSION: "1",
CONF_HOST: config_url[CONF_HOST],
CONF_PORT: config_url[CONF_PORT],
CONF_USERNAME: config_base.get(CONF_USERNAME),
CONF_PASSWORD: config_base.get(CONF_PASSWORD),
CONF_DB_NAME: config_base[CONF_DB_NAME],
CONF_SSL: config_url[CONF_SSL],
CONF_PATH: config_url[CONF_PATH],
CONF_VERIFY_SSL: config_base[CONF_VERIFY_SSL],
CONF_SSL_CA_CERT: "/.storage/influxdb.crt",
}
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"{config_base['database']} ({config_url['host']})"
assert result["data"] == data
@pytest.mark.parametrize(
("mock_client", "config_base", "get_write_api"),
[
(
API_VERSION_2,
{
CONF_URL: "http://localhost:8086",
CONF_VERIFY_SSL: True,
CONF_ORG: "my_org",
CONF_BUCKET: "home_assistant",
CONF_TOKEN: "token",
},
_get_write_api_mock_v2,
),
],
indirect=["mock_client"],
)
async def test_setup_v2(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_client: MagicMock,
config_base: dict[str, Any],
get_write_api: Any,
) -> None:
"""Test we can setup an InfluxDB v2."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"next_step_id": "configure_v2"},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "configure_v2"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
config_base,
)
data = {
CONF_API_VERSION: "2",
CONF_URL: config_base[CONF_URL],
CONF_ORG: config_base[CONF_ORG],
CONF_BUCKET: config_base.get(CONF_BUCKET),
CONF_TOKEN: config_base.get(CONF_TOKEN),
CONF_VERIFY_SSL: config_base[CONF_VERIFY_SSL],
}
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"{config_base['bucket']} ({config_base['url']})"
assert result["data"] == data
@pytest.mark.parametrize(
("mock_client", "config_base", "get_write_api"),
[
(
API_VERSION_2,
{
CONF_URL: "http://localhost:8086",
CONF_VERIFY_SSL: True,
CONF_ORG: "my_org",
CONF_BUCKET: "home_assistant",
CONF_TOKEN: "token",
CONF_SSL_CA_CERT: FIXTURE_UPLOAD_UUID,
},
_get_write_api_mock_v2,
),
],
indirect=["mock_client"],
)
async def test_setup_v2_ssl_cert(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_client: MagicMock,
config_base: dict[str, Any],
get_write_api: Any,
) -> None:
"""Test we can setup an InfluxDB v2 with SSL Certificate."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"next_step_id": "configure_v2"},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "configure_v2"
assert result["errors"] == {}
with (
patch_file_upload(),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
config_base,
)
data = {
CONF_API_VERSION: "2",
CONF_URL: config_base[CONF_URL],
CONF_ORG: config_base[CONF_ORG],
CONF_BUCKET: config_base.get(CONF_BUCKET),
CONF_TOKEN: config_base.get(CONF_TOKEN),
CONF_VERIFY_SSL: config_base[CONF_VERIFY_SSL],
CONF_SSL_CA_CERT: "/.storage/influxdb.crt",
}
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"{config_base['bucket']} ({config_base['url']})"
assert result["data"] == data
@pytest.mark.parametrize(
(
"mock_client",
"config_base",
"api_version",
"get_write_api",
"test_exception",
"reason",
),
[
(
DEFAULT_API_VERSION,
{
CONF_URL: "http://localhost:8086",
CONF_VERIFY_SSL: False,
CONF_DB_NAME: "home_assistant",
CONF_USERNAME: "user",
CONF_PASSWORD: "pass",
},
DEFAULT_API_VERSION,
_get_write_api_mock_v1,
InfluxDBClientError("SSLError"),
"ssl_error",
),
(
DEFAULT_API_VERSION,
{
CONF_URL: "http://localhost:8086",
CONF_VERIFY_SSL: False,
CONF_DB_NAME: "home_assistant",
CONF_USERNAME: "user",
CONF_PASSWORD: "pass",
},
DEFAULT_API_VERSION,
_get_write_api_mock_v1,
InfluxDBClientError("database not found"),
"invalid_database",
),
(
DEFAULT_API_VERSION,
{
CONF_URL: "http://localhost:8086",
CONF_VERIFY_SSL: False,
CONF_DB_NAME: "home_assistant",
CONF_USERNAME: "user",
CONF_PASSWORD: "pass",
},
DEFAULT_API_VERSION,
_get_write_api_mock_v1,
InfluxDBClientError("authorization failed"),
"invalid_auth",
),
(
API_VERSION_2,
{
CONF_URL: "http://localhost:8086",
CONF_VERIFY_SSL: True,
CONF_ORG: "my_org",
CONF_BUCKET: "home_assistant",
CONF_TOKEN: "token",
},
API_VERSION_2,
_get_write_api_mock_v2,
ApiException("SSLError"),
"ssl_error",
),
(
API_VERSION_2,
{
CONF_URL: "http://localhost:8086",
CONF_VERIFY_SSL: True,
CONF_ORG: "my_org",
CONF_BUCKET: "home_assistant",
CONF_TOKEN: "token",
},
API_VERSION_2,
_get_write_api_mock_v2,
ApiException("token"),
"invalid_config",
),
],
indirect=["mock_client"],
)
async def test_setup_connection_error(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_client: MagicMock,
config_base: dict[str, Any],
api_version: str,
get_write_api: Any,
test_exception: Exception,
reason: str,
) -> None:
"""Test connection error during setup of InfluxDB v2."""
write_api = get_write_api(mock_client)
write_api.side_effect = test_exception
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"next_step_id": f"configure_v{api_version}"},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == f"configure_v{api_version}"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
config_base,
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": reason}
write_api.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
config_base,
)
assert result["type"] is FlowResultType.CREATE_ENTRY
@pytest.mark.parametrize(
("mock_client", "config_base", "get_write_api"),
[
(
DEFAULT_API_VERSION,
BASE_V1_CONFIG,
_get_write_api_mock_v1,
),
],
indirect=["mock_client"],
)
async def test_single_instance(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_client: MagicMock,
config_base: dict[str, Any],
get_write_api: Any,
) -> None:
"""Test we cannot setup a second entry for InfluxDB."""
mock_entry = MockConfigEntry(
domain="influxdb",
data=config_base,
)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed"
@pytest.mark.parametrize(
("mock_client", "config_base", "get_write_api", "db_name", "host"),
[
(
DEFAULT_API_VERSION,
BASE_V1_CONFIG,
_get_write_api_mock_v1,
BASE_V1_CONFIG[CONF_DB_NAME],
BASE_V1_CONFIG[CONF_HOST],
),
(
API_VERSION_2,
BASE_V2_CONFIG,
_get_write_api_mock_v2,
BASE_V2_CONFIG[CONF_BUCKET],
BASE_V2_CONFIG[CONF_URL],
),
],
indirect=["mock_client"],
)
async def test_import(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_client: MagicMock,
config_base: dict[str, Any],
get_write_api: Any,
db_name: str,
host: str,
) -> None:
"""Test we can import."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=config_base,
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"{db_name} ({host})"
assert result["data"] == config_base
assert get_write_api(mock_client).call_count == 1
@pytest.mark.parametrize(
("mock_client", "config_base", "get_write_api", "test_exception", "reason"),
[
(
DEFAULT_API_VERSION,
BASE_V1_CONFIG,
_get_write_api_mock_v1,
ConnectionError("fail"),
"cannot_connect",
),
(
DEFAULT_API_VERSION,
BASE_V1_CONFIG,
_get_write_api_mock_v1,
InfluxDBClientError("fail"),
"cannot_connect",
),
(
DEFAULT_API_VERSION,
BASE_V1_CONFIG,
_get_write_api_mock_v1,
InfluxDBServerError("fail"),
"cannot_connect",
),
(
API_VERSION_2,
BASE_V2_CONFIG,
_get_write_api_mock_v2,
ConnectionError("fail"),
"cannot_connect",
),
(
API_VERSION_2,
BASE_V2_CONFIG,
_get_write_api_mock_v2,
ApiException(http_resp=MagicMock()),
"invalid_config",
),
(
API_VERSION_2,
BASE_V2_CONFIG,
_get_write_api_mock_v2,
Exception(),
"unknown",
),
],
indirect=["mock_client"],
)
async def test_import_connection_error(
hass: HomeAssistant,
mock_client: MagicMock,
config_base: dict[str, Any],
get_write_api: Any,
test_exception: Exception,
reason: str,
) -> None:
"""Test abort on connection error."""
write_api = get_write_api(mock_client)
write_api.side_effect = test_exception
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=config_base,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == reason
@pytest.mark.parametrize(
("mock_client", "config_base", "get_write_api"),
[
(
DEFAULT_API_VERSION,
BASE_V1_CONFIG,
_get_write_api_mock_v1,
),
],
indirect=["mock_client"],
)
async def test_single_instance_import(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_client: MagicMock,
config_base: dict[str, Any],
get_write_api: Any,
) -> None:
"""Test we cannot setup a second entry for InfluxDB."""
mock_entry = MockConfigEntry(
domain="influxdb",
data=config_base,
)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=config_base,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed"

File diff suppressed because it is too large Load Diff

View File

@@ -195,7 +195,6 @@ async def _setup(
) -> list[State]: ) -> list[State]:
"""Create client and test expected sensors.""" """Create client and test expected sensors."""
config = { config = {
DOMAIN: config_ext,
sensor.DOMAIN: {"platform": DOMAIN}, sensor.DOMAIN: {"platform": DOMAIN},
} }
influx_config = config[sensor.DOMAIN] influx_config = config[sensor.DOMAIN]
@@ -217,8 +216,18 @@ async def _setup(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mock_client", "config_ext", "queries", "set_query_mock"), ("mock_client", "config_ext", "queries", "set_query_mock"),
[ [
(DEFAULT_API_VERSION, BASE_V1_CONFIG, BASE_V1_QUERY, _set_query_mock_v1), (
(API_VERSION_2, BASE_V2_CONFIG, BASE_V2_QUERY, _set_query_mock_v2), DEFAULT_API_VERSION,
BASE_V1_CONFIG,
BASE_V1_QUERY,
_set_query_mock_v1,
),
(
API_VERSION_2,
BASE_V2_CONFIG,
BASE_V2_QUERY,
_set_query_mock_v2,
),
], ],
indirect=["mock_client"], indirect=["mock_client"],
) )
@@ -313,7 +322,13 @@ async def test_config_failure(hass: HomeAssistant, config_ext) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mock_client", "config_ext", "queries", "set_query_mock", "make_resultset"), (
"mock_client",
"config_ext",
"queries",
"set_query_mock",
"make_resultset",
),
[ [
( (
DEFAULT_API_VERSION, DEFAULT_API_VERSION,
@@ -349,7 +364,13 @@ async def test_state_matches_query_result(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mock_client", "config_ext", "queries", "set_query_mock", "make_resultset"), (
"mock_client",
"config_ext",
"queries",
"set_query_mock",
"make_resultset",
),
[ [
( (
DEFAULT_API_VERSION, DEFAULT_API_VERSION,
@@ -396,7 +417,12 @@ async def test_state_matches_first_query_result_for_multiple_return(
BASE_V1_QUERY, BASE_V1_QUERY,
_set_query_mock_v1, _set_query_mock_v1,
), ),
(API_VERSION_2, BASE_V2_CONFIG, BASE_V2_QUERY, _set_query_mock_v2), (
API_VERSION_2,
BASE_V2_CONFIG,
BASE_V2_QUERY,
_set_query_mock_v2,
),
], ],
indirect=["mock_client"], indirect=["mock_client"],
) )
@@ -419,7 +445,13 @@ async def test_state_for_no_results(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mock_client", "config_ext", "queries", "set_query_mock", "query_exception"), (
"mock_client",
"config_ext",
"queries",
"set_query_mock",
"query_exception",
),
[ [
( (
DEFAULT_API_VERSION, DEFAULT_API_VERSION,
@@ -486,7 +518,14 @@ async def test_error_querying_influx(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mock_client", "config_ext", "queries", "set_query_mock", "make_resultset", "key"), (
"mock_client",
"config_ext",
"queries",
"set_query_mock",
"make_resultset",
"key",
),
[ [
( (
DEFAULT_API_VERSION, DEFAULT_API_VERSION,