Updated apps

This commit is contained in:
2026-07-20 22:52:35 -04:00
parent 28a8cb98f6
commit a0c3271743
1164 changed files with 94781 additions and 6892 deletions
+245
View File
@@ -0,0 +1,245 @@
"""Miscellaneous support functions for Watchman."""
from collections.abc import AsyncGenerator
import fnmatch
import os
import re
from types import MappingProxyType
from typing import Any
import anyio
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.helpers import entity_registry as er
from ..const import (
CONF_COLUMNS_WIDTH,
CONF_EXCLUDE_DISABLED_AUTOMATION,
CONF_FRIENDLY_NAMES,
CONF_HEADER,
CONF_IGNORED_FILES,
CONF_IGNORED_ITEMS,
CONF_IGNORED_STATES,
CONF_INCLUDED_FOLDERS,
CONF_LOG_OBFUSCATE,
CONF_REPORT_PATH,
CONF_SECTION_APPEARANCE_LOCATION,
CONF_STARTUP_DELAY,
DEFAULT_OPTIONS,
DOMAIN_DATA,
)
from .logger import _LOGGER, INDENT
_OBFUSCATE_ENABLED = True
def set_obfuscation_config(enabled: bool) -> None:
"""Set the global obfuscation enabled state."""
global _OBFUSCATE_ENABLED
_OBFUSCATE_ENABLED = enabled
def get_val(
options: MappingProxyType[str, Any], key: str, section: str | None = None
) -> Any:
"""Return value of a key."""
val = None
if section:
try:
val = options[section][key]
except KeyError:
_LOGGER.error(
"Key %s is missing in secion %s, return default value", key, section
)
val = DEFAULT_OPTIONS[section][key]
else:
val = options.get(key, DEFAULT_OPTIONS[key])
return val
def to_lists(
options: MappingProxyType[str, Any] | dict[str, Any], key: str, section: str | None = None
) -> list[str]:
"""Transform configuration value to the list of strings."""
val = get_val(options, key, section)
if isinstance(val, list):
return val
if not val:
return []
return [x.strip() for x in val.split(",") if x.strip()]
def to_listi(
options: MappingProxyType[str, Any] | dict[str, Any], key: str, section: str | None = None
) -> list[int]:
"""Transform configuration value to the list of integers."""
val = get_val(options, key, section)
return [int(x) for x in val.split(",") if x.strip()]
def get_entry(hass: HomeAssistant) -> Any:
"""Return Watchman's ConfigEntry instance."""
if DOMAIN_DATA not in hass.data:
return None
return hass.config_entries.async_get_entry(
hass.data[DOMAIN_DATA]["config_entry_id"]
)
def get_config(hass: HomeAssistant, key: str, default: Any | None = None) -> Any: # noqa: PLR0911
"""Get configuration value from ConfigEntry."""
if DOMAIN_DATA not in hass.data:
return default
entry = hass.config_entries.async_get_entry(
hass.data[DOMAIN_DATA]["config_entry_id"]
)
if not isinstance(entry, ConfigEntry):
return default
if key in [
CONF_INCLUDED_FOLDERS,
CONF_IGNORED_ITEMS,
CONF_IGNORED_FILES,
]:
return to_lists(entry.data, key)
if key in [
CONF_IGNORED_STATES,
CONF_EXCLUDE_DISABLED_AUTOMATION,
CONF_STARTUP_DELAY,
CONF_LOG_OBFUSCATE,
]:
return get_val(entry.data, key)
if key in [CONF_HEADER, CONF_REPORT_PATH, CONF_COLUMNS_WIDTH, CONF_FRIENDLY_NAMES]:
section_name = CONF_SECTION_APPEARANCE_LOCATION
if key == CONF_COLUMNS_WIDTH:
return to_listi(entry.data, CONF_COLUMNS_WIDTH, section_name)
return get_val(entry.data, key, section_name)
return default
async def async_is_valid_path(path: str) -> bool:
"""Validate the report path."""
folder, f_name = os.path.split(path)
if is_valid := (
folder.strip() and f_name.strip() and await anyio.Path(folder).exists()
):
is_valid = not await anyio.Path(path).is_dir()
return is_valid
async def async_get_next_file(
folder_tuples: list[tuple[str, str]], ignored_files: list[str]
) -> AsyncGenerator[tuple[str, bool]]:
"""Return next file from scan queue."""
if not ignored_files:
ignored_files = ""
else:
ignored_files = "|".join([f"({fnmatch.translate(f)})" for f in ignored_files])
ignored_files_re = re.compile(ignored_files)
for folder_name, glob_pattern in folder_tuples:
_LOGGER.debug(
f"{INDENT}Scan folder {folder_name} with pattern {glob_pattern} for configuration files"
)
async for filename in anyio.Path(folder_name).glob(glob_pattern):
yield (
str(filename),
(ignored_files and ignored_files_re.match(str(filename))),
)
def get_included_folders(hass: HomeAssistant) -> list[tuple[str, str]]:
"""Gather the list of folders to parse."""
folders = []
included = get_config(hass, CONF_INCLUDED_FOLDERS, None)
if not included:
# Default to config dir if nothing specified
folders.append((hass.config.config_dir, "**"))
else:
for fld in included:
folders.append((fld, "**"))
return folders
def is_action(hass: HomeAssistant, entry: str) -> bool:
"""Check whether config entry is an action."""
if not isinstance(entry, str):
return False
try:
domain, service = split_entity_id(entry)
except ValueError:
return False
return bool(service) and hass.services.has_service(domain, service)
def get_entity_state(
hass: HomeAssistant,
entry: str,
*,
friendly_names: bool = False,
registry_entry: er.RegistryEntry | None = None,
) -> tuple[str, str | None]:
"""Return entity state or 'missing' if entity does not exist."""
entity_state = hass.states.get(entry)
name = None
if entity_state and entity_state.attributes.get("friendly_name", None):
if friendly_names:
name = entity_state.name
if not entity_state:
state = "missing"
if registry_entry is None:
entity_registry = er.async_get(hass)
registry_entry = entity_registry.async_get(entry)
if registry_entry and registry_entry.disabled_by:
state = "disabled"
else:
state = str(entity_state.state).replace("unavailable", "unavail")
if split_entity_id(entry)[0] == "input_button" and state == "unknown":
state = "available"
return state, name
def obfuscate_id(item_id: Any) -> Any:
"""Obfuscate entity or action ID for logging."""
if not _OBFUSCATE_ENABLED:
return item_id
if isinstance(item_id, (list, tuple, set)):
return ", ".join([str(obfuscate_id(x)) for x in item_id])
if not isinstance(item_id, str) or "." not in item_id:
return item_id
parts = item_id.split(".", 1)
domain = parts[0]
name = parts[1]
if len(name) <= 3:
return f"{domain}.{name}"
if len(name) > 15:
# Truncate to 15 chars: 3 visible + 11 stars + '~'
return f"{domain}.{name[:3]}***********~"
prefix = name[:3]
suffix = name[3:]
masked_suffix = ""
for char in suffix:
if char.isalnum():
masked_suffix += "*"
else:
masked_suffix += char
return f"{domain}.{prefix}{masked_suffix}"