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
|
/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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
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"
|
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]),
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
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",
|
"incomfort",
|
||||||
"indevolt",
|
"indevolt",
|
||||||
"inels",
|
"inels",
|
||||||
|
"influxdb",
|
||||||
"inkbird",
|
"inkbird",
|
||||||
"insteon",
|
"insteon",
|
||||||
"intelliclima",
|
"intelliclima",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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]:
|
) -> 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user