1 """Script to check the configuration file."""
3 from __future__
import annotations
7 from collections
import OrderedDict
8 from collections.abc
import Callable, Mapping, Sequence
12 from typing
import Any
13 from unittest.mock
import patch
15 from homeassistant
import core, loader
21 device_registry
as dr,
22 entity_registry
as er,
31 REQUIREMENTS = (
"colorlog==6.8.2",)
33 _LOGGER = logging.getLogger(__name__)
34 MOCKS: dict[str, tuple[str, Callable]] = {
35 "load": (
"homeassistant.util.yaml.loader.load_yaml", yaml_loader.load_yaml),
36 "load*": (
"homeassistant.config.load_yaml_dict", yaml_loader.load_yaml_dict),
37 "secrets": (
"homeassistant.util.yaml.loader.secret_yaml", yaml_loader.secret_yaml),
40 PATCHES: dict[str, Any] = {}
43 ERROR_STR =
"General Errors"
44 WARNING_STR =
"General Warnings"
47 def color(the_color, *args, reset=None):
50 from colorlog.escape_codes
import escape_codes, parse_colors
54 assert reset
is None,
"You cannot reset if nothing being printed"
55 return parse_colors(the_color)
56 return parse_colors(the_color) +
" ".join(args) + escape_codes[reset
or "reset"]
58 raise ValueError(f
"Invalid color {k!s} in {the_color}")
from k
61 def run(script_args: list) -> int:
62 """Handle check config commandline script."""
63 parser = argparse.ArgumentParser(description=
"Check Home Assistant configuration.")
64 parser.add_argument(
"--script", choices=[
"check_config"])
69 help=
"Directory that contains the Home Assistant configuration",
77 help=
"Show a portion of the config",
80 "-f",
"--files", action=
"store_true", help=
"Show used configuration files"
83 "-s",
"--secrets", action=
"store_true", help=
"Show secret information"
86 args, unknown = parser.parse_known_args()
88 print(
color(
"red",
"Unknown arguments:",
", ".join(unknown)))
90 config_dir = os.path.join(os.getcwd(), args.config)
92 print(
color(
"bold",
"Testing configuration at", config_dir))
94 res =
check(config_dir, args.secrets)
96 domain_info: list[str] = []
98 domain_info = args.info.split(
",")
101 print(
color(C_HEAD,
"yaml files"),
"(used /",
color(
"red",
"not used") +
")")
102 deps = os.path.join(config_dir,
"deps")
105 for f
in glob(os.path.join(config_dir,
"**/*.yaml"), recursive=
True)
106 if not f.startswith(deps)
109 for yfn
in sorted(yaml_files):
110 the_color =
"" if yfn
in res[
"yaml_files"]
else "red"
111 print(
color(the_color,
"-", yfn))
114 print(
color(
"bold_white",
"Failed config"))
115 for domain, config
in res[
"except"].items():
116 domain_info.append(domain)
117 print(
" ",
color(
"bold_red", domain +
":"),
color(
"red",
"", reset=
"red"))
119 print(
color(
"reset"))
122 print(
color(
"bold_white",
"Incorrect config"))
123 for domain, config
in res[
"warn"].items():
124 domain_info.append(domain)
127 color(
"bold_yellow", domain +
":"),
128 color(
"yellow",
"", reset=
"yellow"),
131 print(
color(
"reset"))
134 if "all" in domain_info:
135 print(
color(
"bold_white",
"Successful config (all)"))
136 for domain, config
in res[
"components"].items():
137 print(
" ",
color(C_HEAD, domain +
":"))
140 print(
color(
"bold_white",
"Successful config (partial)"))
141 for domain
in domain_info:
142 if domain == ERROR_STR:
144 print(
" ",
color(C_HEAD, domain +
":"))
148 flatsecret: dict[str, str] = {}
150 for sfn, sdict
in res[
"secret_cache"].items():
153 if skey
in flatsecret:
155 "Duplicated secrets in files %s and %s", flatsecret[skey], sfn
157 flatsecret[skey] = sfn
158 sss.append(
color(
"green", skey)
if skey
in res[
"secrets"]
else skey)
159 print(
color(C_HEAD,
"Secrets from", sfn +
":"),
", ".join(sss))
161 print(
color(C_HEAD,
"Used Secrets:"))
162 for skey, sval
in res[
"secrets"].items():
164 print(
" -", skey +
":",
color(
"red",
"not found"))
166 print(
" -", skey +
":", sval)
168 return len(res[
"except"])
171 def check(config_dir, secrets=False):
172 """Perform a check by mocking hass load functions."""
173 logging.getLogger(
"homeassistant.loader").setLevel(logging.CRITICAL)
174 res: dict[str, Any] = {
175 "yaml_files": OrderedDict(),
176 "secrets": OrderedDict(),
177 "except": OrderedDict(),
178 "warn": OrderedDict(),
184 def mock_load(filename, secrets=None):
185 """Mock hass.util.load_yaml to save config file names."""
186 res[
"yaml_files"][filename] =
True
187 return MOCKS[
"load"][1](filename, secrets)
190 def mock_secrets(ldr, node):
191 """Mock _get_secrets."""
193 val = MOCKS[
"secrets"][1](ldr, node)
194 except HomeAssistantError:
196 res[
"secrets"][node.value] = val
200 for key, val
in MOCKS.items():
201 if not secrets
and key ==
"secrets":
205 mock_function = locals()[f
"mock_{key.replace('*', '')}"]
206 PATCHES[key] = patch(val[0], side_effect=mock_function)
209 for pat
in PATCHES.values():
214 yaml_loader.add_constructor(
"!secret", yaml_loader.secret_yaml)
216 def secrets_proxy(*args):
217 secrets = Secrets(*args)
218 res[
"secret_cache"] = secrets._cache
222 with patch.object(yaml_loader,
"Secrets", secrets_proxy):
224 res[
"secret_cache"] = {
225 str(key): val
for key, val
in res[
"secret_cache"].items()
227 for err
in res[
"components"].errors:
228 domain = err.domain
or ERROR_STR
229 res[
"except"].setdefault(domain, []).append(err.message)
231 res[
"except"].setdefault(domain, []).append(err.config)
233 for err
in res[
"components"].warnings:
234 domain = err.domain
or WARNING_STR
235 res[
"warn"].setdefault(domain, []).append(err.message)
237 res[
"warn"].setdefault(domain, []).append(err.config)
239 except Exception
as err:
240 print(
color(
"red",
"Fatal error while loading config:"),
str(err))
241 res[
"except"].setdefault(ERROR_STR, []).append(
str(err))
244 for pat
in PATCHES.values():
248 yaml_loader.add_constructor(
"!secret", yaml_loader.secret_yaml)
254 """Check the HA config."""
256 loader.async_setup(hass)
258 await ar.async_load(hass)
259 await dr.async_load(hass)
260 await er.async_load(hass)
261 await ir.async_load(hass, read_only=
True)
263 await hass.async_stop(force=
True)
268 """Display line config source."""
269 if hasattr(obj,
"__config_file__"):
271 "cyan", f
"[source {obj.__config_file__}:{obj.__line__ or '?'}]", **kwargs
276 def dump_dict(layer, indent_count=3, listi=False, **kwargs):
279 A friendly version of print yaml_loader.yaml.dump(config).
282 def sort_dict_key(val):
283 """Return the dict key for sorting."""
284 key =
str(val[0]).lower()
285 return "0" if key ==
"platform" else key
287 indent_str = indent_count *
" "
288 if listi
or isinstance(layer, list):
289 indent_str = indent_str[:-1] +
"-"
290 if isinstance(layer, Mapping):
291 for key, value
in sorted(layer.items(), key=sort_dict_key):
292 if isinstance(value, (dict, list)):
293 print(indent_str,
str(key) +
":",
line_info(value, **kwargs))
294 dump_dict(value, indent_count + 2, **kwargs)
296 print(indent_str,
str(key) +
":", value,
line_info(key, **kwargs))
297 indent_str = indent_count *
" "
298 if isinstance(layer, Sequence):
300 if isinstance(i, dict):
301 dump_dict(i, indent_count + 2,
True, **kwargs)
303 print(
" ", indent_str, i)
web.Response get(self, web.Request request, str config_key)
str get_default_config_dir()
HomeAssistantConfig async_check_ha_config_file(HomeAssistant hass)
def color(the_color, *args, reset=None)
def dump_dict(layer, indent_count=3, listi=False, **kwargs)
def async_check_config(config_dir)
def check(config_dir, secrets=False)
int run(list script_args)
def line_info(obj, **kwargs)