Files
homeassistant_config/config/custom_components/watchman/config_flow.py
2024-08-09 06:45:02 +02:00

315 lines
12 KiB
Python

"ConfigFlow definition for watchman"
from typing import Dict
import json
from json.decoder import JSONDecodeError
import logging
from homeassistant.config_entries import ConfigFlow, OptionsFlow, ConfigEntry
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv, selector
import voluptuous as vol
from .utils import is_service, get_columns_width, async_get_report_path
from .const import (
DOMAIN,
CONF_IGNORED_FILES,
CONF_HEADER,
CONF_REPORT_PATH,
CONF_IGNORED_ITEMS,
CONF_SERVICE_NAME,
CONF_SERVICE_DATA,
CONF_SERVICE_DATA2,
CONF_INCLUDED_FOLDERS,
CONF_CHECK_LOVELACE,
CONF_IGNORED_STATES,
CONF_CHUNK_SIZE,
CONF_COLUMNS_WIDTH,
CONF_STARTUP_DELAY,
CONF_FRIENDLY_NAMES,
)
DEFAULT_DATA = {
CONF_SERVICE_NAME: "",
CONF_SERVICE_DATA2: "{}",
CONF_INCLUDED_FOLDERS: ["/config"],
CONF_HEADER: "-== Watchman Report ==-",
CONF_REPORT_PATH: "",
CONF_IGNORED_ITEMS: [],
CONF_IGNORED_STATES: [],
CONF_CHUNK_SIZE: 3500,
CONF_IGNORED_FILES: [],
CONF_CHECK_LOVELACE: False,
CONF_COLUMNS_WIDTH: [30, 7, 60],
CONF_STARTUP_DELAY: 0,
CONF_FRIENDLY_NAMES: False,
}
INCLUDED_FOLDERS_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
IGNORED_ITEMS_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
IGNORED_STATES_SCHEMA = vol.Schema(["missing", "unavailable", "unknown"])
IGNORED_FILES_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
COLUMNS_WIDTH_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.positive_int]))
_LOGGER = logging.getLogger(__name__)
class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow"""
async def async_step_user(self, user_input=None):
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(title="Watchman", data={}, options=DEFAULT_DATA)
async def async_step_import(self, import_data):
"""Import configuration.yaml settings as OptionsEntry"""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
# change "data" key from configuration.yaml to "service_data" as "data" is reserved by
# OptionsFlow
import_data[CONF_SERVICE_DATA2] = import_data.get(CONF_SERVICE_DATA, {})
if CONF_SERVICE_DATA in import_data:
import_data.pop(CONF_SERVICE_DATA)
_LOGGER.info(
"watchman settings imported successfully and can be removed from "
"configuration.yaml"
)
_LOGGER.debug("configuration.yaml settings successfully imported to UI options")
return self.async_create_entry(
title="configuration.yaml", data={}, options=import_data
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Handles options flow for the component."""
def __init__(self, config_entry: ConfigEntry) -> None:
self.config_entry = config_entry
async def async_default(self, key, uinput=None):
"""provide default value for an OptionsFlow field"""
if uinput and key in uinput:
# supply last entered value to display an error during form validation
result = uinput[key]
else:
# supply last saved value or default one
result = self.config_entry.options.get(key, DEFAULT_DATA[key])
if result == "":
# some default values cannot be empty
if DEFAULT_DATA[key]:
result = DEFAULT_DATA[key]
elif key == CONF_REPORT_PATH:
result = await async_get_report_path(self.hass, None)
if isinstance(result, list):
return ", ".join([str(i) for i in result])
if isinstance(result, dict):
return json.dumps(result)
if isinstance(result, bool):
return result
return str(result)
def to_list(self, user_input, key):
"""validate user input against list requirements"""
errors: Dict[str, str] = {}
if key not in user_input:
return DEFAULT_DATA[key], errors
val = user_input[key]
val = [x.strip() for x in val.split(",") if x.strip()]
try:
val = INCLUDED_FOLDERS_SCHEMA(val)
except vol.Invalid:
errors[key] = f"invalid_{key}"
return val, errors
async def _show_options_form(self, uinput=None, errors=None, placehoders=None): # pylint: disable=unused-argument
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_SERVICE_NAME,
description={
"suggested_value": await self.async_default(
CONF_SERVICE_NAME, uinput
)
},
): cv.string,
vol.Optional(
CONF_SERVICE_DATA2,
description={
"suggested_value": await self.async_default(
CONF_SERVICE_DATA2, uinput
)
},
): selector.TemplateSelector(),
vol.Optional(
CONF_INCLUDED_FOLDERS,
description={
"suggested_value": await self.async_default(
CONF_INCLUDED_FOLDERS, uinput
)
},
): selector.TextSelector(
selector.TextSelectorConfig(multiline=True)
),
vol.Optional(
CONF_HEADER,
description={
"suggested_value": await self.async_default(
CONF_HEADER, uinput
)
},
): cv.string,
vol.Optional(
CONF_REPORT_PATH,
description={
"suggested_value": await self.async_default(
CONF_REPORT_PATH, uinput
)
},
): cv.string,
vol.Optional(
CONF_IGNORED_ITEMS,
description={
"suggested_value": await self.async_default(
CONF_IGNORED_ITEMS, uinput
)
},
): selector.TextSelector(
selector.TextSelectorConfig(multiline=True)
),
vol.Optional(
CONF_IGNORED_STATES,
description={
"suggested_value": await self.async_default(
CONF_IGNORED_STATES, uinput
)
},
): selector.TextSelector(
selector.TextSelectorConfig(multiline=True)
),
vol.Optional(
CONF_CHUNK_SIZE,
description={
"suggested_value": await self.async_default(
CONF_CHUNK_SIZE, uinput
)
},
): cv.positive_int,
vol.Optional(
CONF_IGNORED_FILES,
description={
"suggested_value": await self.async_default(
CONF_IGNORED_FILES, uinput
)
},
): selector.TextSelector(
selector.TextSelectorConfig(multiline=True)
),
vol.Optional(
CONF_COLUMNS_WIDTH,
description={
"suggested_value": await self.async_default(
CONF_COLUMNS_WIDTH, uinput
)
},
): cv.string,
vol.Optional(
CONF_STARTUP_DELAY,
description={
"suggested_value": await self.async_default(
CONF_STARTUP_DELAY, uinput
)
},
): cv.positive_int,
vol.Optional(
CONF_FRIENDLY_NAMES,
description={
"suggested_value": await self.async_default(
CONF_FRIENDLY_NAMES, uinput
)
},
): cv.boolean,
vol.Optional(
CONF_CHECK_LOVELACE,
description={
"suggested_value": await self.async_default(
CONF_CHECK_LOVELACE, uinput
)
},
): cv.boolean,
}
),
errors=errors or {},
description_placeholders=placehoders or {},
)
async def async_step_init(self, user_input=None):
"""Manage the options"""
errors: Dict[str, str] = {}
placehoders: Dict[str, str] = {}
if user_input is not None:
user_input[CONF_INCLUDED_FOLDERS], err = self.to_list(
user_input, CONF_INCLUDED_FOLDERS
)
errors |= err
user_input[CONF_IGNORED_ITEMS], err = self.to_list(
user_input, CONF_IGNORED_ITEMS
)
errors |= err
ignored_states, err = self.to_list(user_input, CONF_IGNORED_STATES)
errors |= err
try:
user_input[CONF_IGNORED_STATES] = IGNORED_STATES_SCHEMA(ignored_states)
except vol.Invalid:
errors[CONF_IGNORED_STATES] = "wrong_value_ignored_states"
user_input[CONF_IGNORED_FILES], err = self.to_list(
user_input, CONF_IGNORED_FILES
)
errors |= err
if CONF_COLUMNS_WIDTH in user_input:
columns_width = user_input[CONF_COLUMNS_WIDTH]
try:
columns_width = [
int(x) for x in columns_width.split(",") if x.strip()
]
if len(columns_width) != 3:
raise ValueError()
columns_width = COLUMNS_WIDTH_SCHEMA(columns_width)
user_input[CONF_COLUMNS_WIDTH] = get_columns_width(columns_width)
except (ValueError, vol.Invalid):
errors[CONF_COLUMNS_WIDTH] = "invalid_columns_width"
if CONF_SERVICE_DATA2 in user_input:
try:
result = json.loads(user_input[CONF_SERVICE_DATA2])
if not isinstance(result, dict):
errors[CONF_SERVICE_DATA2] = "malformed_json"
except JSONDecodeError:
errors[CONF_SERVICE_DATA2] = "malformed_json"
if CONF_SERVICE_NAME in user_input:
if not is_service(self.hass, user_input[CONF_SERVICE_NAME]):
errors[CONF_SERVICE_NAME] = "unknown_service"
placehoders["service"] = user_input[CONF_SERVICE_NAME]
if not errors:
return self.async_create_entry(title="", data=user_input)
else:
# provide last entered values to display error
return await self._show_options_form(user_input, errors, placehoders)
# provide default values
return await self._show_options_form()