Coverage for custom_components/autoarm/hass_api.py: 93%
96 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-26 20:57 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-26 20:57 +0000
1import logging
2from functools import partial
3from typing import TYPE_CHECKING, Any, cast
5from homeassistant.components.alarm_control_panel.const import AlarmControlPanelState
6from homeassistant.exceptions import ConditionError, ConditionErrorContainer
7from homeassistant.helpers import condition as condition_helper
8from homeassistant.helpers import issue_registry as ir
9from homeassistant.helpers.template import Template
11from .const import DOMAIN, ConditionVariables
13if TYPE_CHECKING:
14 from collections.abc import Callable
16 from homeassistant.core import Context, HomeAssistant
17 from homeassistant.helpers.typing import ConfigType, TemplateVarsType
20_LOGGER = logging.getLogger(__name__)
23class HomeAssistantAPI:
24 def __init__(self, hass: HomeAssistant | None = None) -> None:
25 self._hass = hass
27 def raise_issue(
28 self,
29 issue_id: str,
30 issue_key: str,
31 issue_map: dict[str, str],
32 severity: ir.IssueSeverity = ir.IssueSeverity.WARNING,
33 learn_more_url: str = "https://autoarm.rhizomatics.org.uk",
34 is_fixable: bool = False,
35 ) -> None:
36 if not self._hass:
37 return
38 ir.async_create_issue(
39 self._hass,
40 DOMAIN,
41 issue_id,
42 translation_key=issue_key,
43 translation_placeholders=issue_map,
44 severity=severity,
45 learn_more_url=learn_more_url,
46 is_fixable=is_fixable,
47 )
49 async def build_condition(
50 self, condition_config: list[ConfigType], strict: bool = False, validate: bool = False, name: str = DOMAIN
51 ) -> Callable[[TemplateVarsType], bool] | None:
52 if self._hass is None:
53 raise ValueError("HomeAssistant not available")
54 capturing_logger: ConditionErrorLoggingAdaptor = ConditionErrorLoggingAdaptor(_LOGGER)
55 condition_variables: ConditionVariables = ConditionVariables(None, None, False, AlarmControlPanelState.PENDING, {})
56 cond_list: list[ConfigType]
57 try:
58 if validate:
59 cond_list = cast(
60 "list[ConfigType]", await condition_helper.async_validate_conditions_config(self._hass, condition_config)
61 )
62 else:
63 cond_list = condition_config
64 except Exception:
65 _LOGGER.exception("AUTOARM Condition validation failed")
66 raise
67 try:
68 if strict:
69 force_strict_template_mode(cond_list, undo=False)
71 test: Callable[[TemplateVarsType], bool] = await condition_helper.async_conditions_from_config(
72 self._hass, cond_list, cast("logging.Logger", capturing_logger), name
73 )
74 if test is None:
75 raise ValueError(f"Invalid condition {condition_config}")
76 test({DOMAIN: condition_variables.as_dict()})
77 if strict and capturing_logger.condition_errors:
78 for exception in capturing_logger.condition_errors:
79 _LOGGER.warning("AUTOARM Invalid condition %s:%s", condition_config, exception)
80 raise capturing_logger.condition_errors[0]
81 return test
82 except Exception:
83 _LOGGER.exception("AUTOARM Condition eval failed")
84 raise
85 finally:
86 if strict:
87 force_strict_template_mode(condition_config, undo=True)
89 def evaluate_condition(
90 self,
91 checker: Callable[[TemplateVarsType], bool],
92 condition_variables: ConditionVariables | None = None,
93 ) -> bool | None:
94 if self._hass is None:
95 raise ValueError("HomeAssistant not available")
96 try:
97 return checker({DOMAIN: condition_variables.as_dict()} if condition_variables else None)
98 except Exception as e:
99 _LOGGER.error("AUTOARM Condition eval failed: %s", e)
100 raise
102 def fire_event(self, event_name: str, event_data: dict[str, Any] | None = None, context: Context | None = None) -> None:
103 if self._hass is not None:
104 _LOGGER.debug("AUTOARM Firing %s event: %s", event_name, event_data)
105 self._hass.bus.async_fire(f"{DOMAIN}_{event_name}", event_data, context=context)
108class ConditionErrorLoggingAdaptor(logging.LoggerAdapter["logging.Logger"]):
109 def __init__(self, *args: Any, **kwargs: Any) -> None:
110 super().__init__(*args, **kwargs)
111 self.condition_errors: list[ConditionError] = []
113 def capture(self, args: Any) -> None:
114 if args and isinstance(args, (list, tuple)):
115 for arg in args:
116 if isinstance(arg, ConditionErrorContainer):
117 self.condition_errors.extend(arg.errors)
118 elif isinstance(arg, ConditionError):
119 self.condition_errors.append(arg)
121 def error(self, msg: Any, *args: object, **kwargs: Any) -> None:
122 self.capture(args)
123 self.logger.error(msg, args, kwargs)
125 def warning(self, msg: Any, *args: Any, **kwargs: Any) -> None:
126 self.capture(args)
127 self.logger.warning(msg, args, kwargs)
130def force_strict_template_mode(conditions: list[ConfigType], undo: bool = False) -> None:
131 class TemplateWrapper:
132 def __init__(self, obj: Template) -> None:
133 self._obj = obj
135 def __getattr__(self, name: str) -> Any:
136 if name == "async_render_to_info":
137 return partial(self._obj.async_render_to_info, strict=True)
138 return getattr(self._obj, name)
140 def __setattr__(self, name: str, value: Any) -> None:
141 super().__setattr__(name, value)
143 def wrap_template(cond: ConfigType, undo: bool) -> ConfigType:
144 for key, val in cond.items():
145 if not undo and isinstance(val, Template) and hasattr(val, "_env"):
146 cond[key] = TemplateWrapper(val)
147 elif undo and isinstance(val, TemplateWrapper):
148 cond[key] = val._obj
149 elif isinstance(val, dict):
150 wrap_template(val, undo)
151 return cond
153 if conditions is not None:
154 conditions = [wrap_template(condition, undo) for condition in conditions]