mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 00:03:16 +01:00
Add config flow to InfluxDB integration (#134463)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
This commit is contained in:
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -792,8 +792,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/indevolt/ @xirtnl
|
||||
/homeassistant/components/inels/ @epdevlab
|
||||
/tests/components/inels/ @epdevlab
|
||||
/homeassistant/components/influxdb/ @mdegat01
|
||||
/tests/components/influxdb/ @mdegat01
|
||||
/homeassistant/components/influxdb/ @mdegat01 @Robbie1221
|
||||
/tests/components/influxdb/ @mdegat01 @Robbie1221
|
||||
/homeassistant/components/inkbird/ @bdraco
|
||||
/tests/components/inkbird/ @bdraco
|
||||
/homeassistant/components/input_boolean/ @home-assistant/core
|
||||
|
||||
@@ -20,10 +20,14 @@ import requests.exceptions
|
||||
import urllib3.exceptions
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_DOMAIN,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_EXCLUDE,
|
||||
CONF_HOST,
|
||||
CONF_INCLUDE,
|
||||
CONF_PASSWORD,
|
||||
CONF_PATH,
|
||||
CONF_PORT,
|
||||
@@ -34,17 +38,13 @@ from homeassistant.const import (
|
||||
CONF_URL,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_STATE_CHANGED,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
event as event_helper,
|
||||
state as state_helper,
|
||||
)
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, state as state_helper
|
||||
from homeassistant.helpers.entity_values import EntityValues
|
||||
from homeassistant.helpers.entityfilter import (
|
||||
INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA,
|
||||
@@ -79,6 +79,7 @@ from .const import (
|
||||
CONF_TAGS_ATTRIBUTES,
|
||||
CONNECTION_ERROR,
|
||||
DEFAULT_API_VERSION,
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_HOST_V2,
|
||||
DEFAULT_MEASUREMENT_ATTR,
|
||||
DEFAULT_SSL_V2,
|
||||
@@ -97,8 +98,6 @@ from .const import (
|
||||
RE_DIGIT_TAIL,
|
||||
RESUMED_MESSAGE,
|
||||
RETRY_DELAY,
|
||||
RETRY_INTERVAL,
|
||||
RETRY_MESSAGE,
|
||||
TEST_QUERY_V1,
|
||||
TEST_QUERY_V2,
|
||||
TIMEOUT,
|
||||
@@ -108,6 +107,8 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type InfluxDBConfigEntry = ConfigEntry[InfluxThread]
|
||||
|
||||
|
||||
def create_influx_url(conf: dict) -> dict:
|
||||
"""Build URL used from config inputs and default when necessary."""
|
||||
@@ -198,8 +199,26 @@ INFLUX_SCHEMA = vol.All(
|
||||
create_influx_url,
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -349,8 +368,8 @@ def get_influx_connection( # noqa: C901
|
||||
kwargs[CONF_TOKEN] = conf[CONF_TOKEN]
|
||||
kwargs[INFLUX_CONF_ORG] = conf[CONF_ORG]
|
||||
kwargs[CONF_VERIFY_SSL] = conf[CONF_VERIFY_SSL]
|
||||
if CONF_SSL_CA_CERT in conf:
|
||||
kwargs[CONF_SSL_CA_CERT] = conf[CONF_SSL_CA_CERT]
|
||||
if (cert := conf.get(CONF_SSL_CA_CERT)) is not None:
|
||||
kwargs[CONF_SSL_CA_CERT] = cert
|
||||
bucket = conf.get(CONF_BUCKET)
|
||||
influx = InfluxDBClientV2(**kwargs)
|
||||
query_api = influx.query_api()
|
||||
@@ -406,31 +425,31 @@ def get_influx_connection( # noqa: C901
|
||||
return InfluxClient(buckets, write_v2, query_v2, close_v2)
|
||||
|
||||
# Else it's a V1 client
|
||||
if CONF_SSL_CA_CERT in conf and conf[CONF_VERIFY_SSL]:
|
||||
kwargs[CONF_VERIFY_SSL] = conf[CONF_SSL_CA_CERT]
|
||||
if (cert := conf.get(CONF_SSL_CA_CERT)) is not None and conf[CONF_VERIFY_SSL]:
|
||||
kwargs[CONF_VERIFY_SSL] = cert
|
||||
else:
|
||||
kwargs[CONF_VERIFY_SSL] = conf[CONF_VERIFY_SSL]
|
||||
|
||||
if CONF_DB_NAME in conf:
|
||||
kwargs[CONF_DB_NAME] = conf[CONF_DB_NAME]
|
||||
if (db_name := conf.get(CONF_DB_NAME)) is not None:
|
||||
kwargs[CONF_DB_NAME] = db_name
|
||||
|
||||
if CONF_USERNAME in conf:
|
||||
kwargs[CONF_USERNAME] = conf[CONF_USERNAME]
|
||||
if (user_name := conf.get(CONF_USERNAME)) is not None:
|
||||
kwargs[CONF_USERNAME] = user_name
|
||||
|
||||
if CONF_PASSWORD in conf:
|
||||
kwargs[CONF_PASSWORD] = conf[CONF_PASSWORD]
|
||||
if (password := conf.get(CONF_PASSWORD)) is not None:
|
||||
kwargs[CONF_PASSWORD] = password
|
||||
|
||||
if CONF_HOST in conf:
|
||||
kwargs[CONF_HOST] = conf[CONF_HOST]
|
||||
|
||||
if CONF_PATH in conf:
|
||||
kwargs[CONF_PATH] = conf[CONF_PATH]
|
||||
if (path := conf.get(CONF_PATH)) is not None:
|
||||
kwargs[CONF_PATH] = path
|
||||
|
||||
if CONF_PORT in conf:
|
||||
kwargs[CONF_PORT] = conf[CONF_PORT]
|
||||
if (port := conf.get(CONF_PORT)) is not None:
|
||||
kwargs[CONF_PORT] = port
|
||||
|
||||
if CONF_SSL in conf:
|
||||
kwargs[CONF_SSL] = conf[CONF_SSL]
|
||||
if (ssl := conf.get(CONF_SSL)) is not None:
|
||||
kwargs[CONF_SSL] = ssl
|
||||
|
||||
influx = InfluxDBClient(**kwargs)
|
||||
|
||||
@@ -478,34 +497,79 @@ def get_influx_connection( # noqa: C901
|
||||
return InfluxClient(databases, write_v1, query_v1, close_v1)
|
||||
|
||||
|
||||
def _retry_setup(hass: HomeAssistant, config: ConfigType) -> None:
|
||||
setup(hass, config)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the InfluxDB component."""
|
||||
conf = config[DOMAIN]
|
||||
try:
|
||||
influx = get_influx_connection(conf, test_write=True)
|
||||
except ConnectionError as exc:
|
||||
_LOGGER.error(RETRY_MESSAGE, exc)
|
||||
event_helper.call_later(
|
||||
hass, RETRY_INTERVAL, lambda _: _retry_setup(hass, config)
|
||||
conf = config.get(DOMAIN)
|
||||
|
||||
if conf is not None:
|
||||
if CONF_HOST not in conf and conf[CONF_API_VERSION] == DEFAULT_API_VERSION:
|
||||
conf[CONF_HOST] = DEFAULT_HOST
|
||||
|
||||
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)
|
||||
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()
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: InfluxDBConfigEntry) -> bool:
|
||||
"""Set up InfluxDB from a config entry."""
|
||||
data = entry.data
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
|
||||
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
|
||||
|
||||
@@ -513,7 +577,14 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
class InfluxThread(threading.Thread):
|
||||
"""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."""
|
||||
threading.Thread.__init__(self, name=DOMAIN)
|
||||
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.max_tries = max_tries
|
||||
self.write_errors = 0
|
||||
self.shutdown = False
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener)
|
||||
self._shutdown = False
|
||||
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
|
||||
def _event_listener(self, event):
|
||||
@@ -547,13 +626,13 @@ class InfluxThread(threading.Thread):
|
||||
dropped = 0
|
||||
|
||||
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()
|
||||
item = self.queue.get(timeout=timeout)
|
||||
count += 1
|
||||
|
||||
if item is None:
|
||||
self.shutdown = True
|
||||
self._shutdown = True
|
||||
elif type(item) is tuple:
|
||||
timestamp, event = item
|
||||
age = time.monotonic() - timestamp
|
||||
@@ -596,7 +675,7 @@ class InfluxThread(threading.Thread):
|
||||
|
||||
def run(self):
|
||||
"""Process incoming events."""
|
||||
while not self.shutdown:
|
||||
while not self._shutdown:
|
||||
_, json = self.get_events_json()
|
||||
if json:
|
||||
self.write_to_influxdb(json)
|
||||
|
||||
281
homeassistant/components/influxdb/config_flow.py
Normal file
281
homeassistant/components/influxdb/config_flow.py
Normal 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)
|
||||
@@ -48,7 +48,9 @@ CONF_QUERY = "query"
|
||||
CONF_IMPORTS = "imports"
|
||||
|
||||
DEFAULT_DATABASE = "home_assistant"
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_HOST_V2 = "us-west-2-1.aws.cloud2.influxdata.com"
|
||||
DEFAULT_PORT = 8086
|
||||
DEFAULT_SSL_V2 = True
|
||||
DEFAULT_BUCKET = "Home Assistant"
|
||||
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_ERROR_MESSAGE = "Could not render where template: %s."
|
||||
|
||||
|
||||
COMPONENT_CONFIG_SCHEMA_CONNECTION = {
|
||||
# Connection config for V1 and V2 APIs.
|
||||
vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): vol.All(
|
||||
vol.Coerce(str),
|
||||
vol.In([DEFAULT_API_VERSION, API_VERSION_2]),
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
{
|
||||
"domain": "influxdb",
|
||||
"name": "InfluxDB",
|
||||
"codeowners": ["@mdegat01"],
|
||||
"codeowners": ["@mdegat01", "@Robbie1221"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["file_upload"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/influxdb",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["influxdb", "influxdb_client"],
|
||||
"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
|
||||
}
|
||||
|
||||
58
homeassistant/components/influxdb/strings.json
Normal file
58
homeassistant/components/influxdb/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -331,6 +331,7 @@ FLOWS = {
|
||||
"incomfort",
|
||||
"indevolt",
|
||||
"inels",
|
||||
"influxdb",
|
||||
"inkbird",
|
||||
"insteon",
|
||||
"intelliclima",
|
||||
|
||||
@@ -3137,8 +3137,9 @@
|
||||
"influxdb": {
|
||||
"name": "InfluxDB",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push"
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"single_config_entry": true
|
||||
},
|
||||
"inkbird": {
|
||||
"name": "INKBIRD",
|
||||
|
||||
@@ -1 +1,92 @@
|
||||
"""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
|
||||
|
||||
719
tests/components/influxdb/test_config_flow.py
Normal file
719
tests/components/influxdb/test_config_flow.py
Normal 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
@@ -195,7 +195,6 @@ async def _setup(
|
||||
) -> list[State]:
|
||||
"""Create client and test expected sensors."""
|
||||
config = {
|
||||
DOMAIN: config_ext,
|
||||
sensor.DOMAIN: {"platform": DOMAIN},
|
||||
}
|
||||
influx_config = config[sensor.DOMAIN]
|
||||
@@ -217,8 +216,18 @@ async def _setup(
|
||||
@pytest.mark.parametrize(
|
||||
("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"],
|
||||
)
|
||||
@@ -313,7 +322,13 @@ async def test_config_failure(hass: HomeAssistant, config_ext) -> None:
|
||||
|
||||
|
||||
@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,
|
||||
@@ -349,7 +364,13 @@ async def test_state_matches_query_result(
|
||||
|
||||
|
||||
@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,
|
||||
@@ -396,7 +417,12 @@ async def test_state_matches_first_query_result_for_multiple_return(
|
||||
BASE_V1_QUERY,
|
||||
_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"],
|
||||
)
|
||||
@@ -419,7 +445,13 @@ async def test_state_for_no_results(
|
||||
|
||||
|
||||
@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,
|
||||
@@ -486,7 +518,14 @@ async def test_error_querying_influx(
|
||||
|
||||
|
||||
@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,
|
||||
|
||||
Reference in New Issue
Block a user