315 lines
12 KiB
Python
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()
|