-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into valetudo-room-preferences
- Loading branch information
Showing
10 changed files
with
1,637 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,314 @@ | ||
"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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
"definition of constants" | ||
|
||
from homeassistant.const import Platform | ||
|
||
DOMAIN = "watchman" | ||
DOMAIN_DATA = "watchman_data" | ||
VERSION = "0.6.3" | ||
|
||
DEFAULT_REPORT_FILENAME = "watchman_report.txt" | ||
DEFAULT_HEADER = "-== WATCHMAN REPORT ==- " | ||
DEFAULT_CHUNK_SIZE = 3500 | ||
|
||
CONF_IGNORED_FILES = "ignored_files" | ||
CONF_HEADER = "report_header" | ||
CONF_REPORT_PATH = "report_path" | ||
CONF_IGNORED_ITEMS = "ignored_items" | ||
CONF_SERVICE_NAME = "service" | ||
CONF_SERVICE_DATA = "data" | ||
CONF_SERVICE_DATA2 = "service_data" | ||
CONF_INCLUDED_FOLDERS = "included_folders" | ||
CONF_CHECK_LOVELACE = "check_lovelace" | ||
CONF_IGNORED_STATES = "ignored_states" | ||
CONF_CHUNK_SIZE = "chunk_size" | ||
CONF_CREATE_FILE = "create_file" | ||
CONF_SEND_NOTIFICATION = "send_notification" | ||
CONF_PARSE_CONFIG = "parse_config" | ||
CONF_COLUMNS_WIDTH = "columns_width" | ||
CONF_STARTUP_DELAY = "startup_delay" | ||
CONF_FRIENDLY_NAMES = "friendly_names" | ||
CONF_TEST_MODE = "test_mode" | ||
# configuration parameters allowed in watchman.report service data | ||
CONF_ALLOWED_SERVICE_PARAMS = [ | ||
CONF_SERVICE_NAME, | ||
CONF_CHUNK_SIZE, | ||
CONF_CREATE_FILE, | ||
CONF_SEND_NOTIFICATION, | ||
CONF_PARSE_CONFIG, | ||
CONF_SERVICE_DATA, | ||
CONF_TEST_MODE, | ||
] | ||
|
||
EVENT_AUTOMATION_RELOADED = "automation_reloaded" | ||
EVENT_SCENE_RELOADED = "scene_reloaded" | ||
|
||
SENSOR_LAST_UPDATE = "watchman_last_updated" | ||
SENSOR_MISSING_ENTITIES = "watchman_missing_entities" | ||
SENSOR_MISSING_SERVICES = "watchman_missing_services" | ||
MONITORED_STATES = ["unavailable", "unknown", "missing"] | ||
|
||
TRACKED_EVENT_DOMAINS = [ | ||
"homeassistant", | ||
"input_boolean", | ||
"input_button", | ||
"input_select", | ||
"input_number", | ||
"input_datetime", | ||
"person", | ||
"input_text", | ||
"script", | ||
"timer", | ||
"zone", | ||
] | ||
|
||
BUNDLED_IGNORED_ITEMS = [ | ||
"timer.cancelled", | ||
"timer.finished", | ||
"timer.started", | ||
"timer.restarted", | ||
"timer.paused", | ||
] | ||
|
||
# Platforms | ||
PLATFORMS = [Platform.SENSOR] |
Oops, something went wrong.