Coverage for custom_components/autoarm/const.py: 100%
78 statements
« prev ^ index » next coverage.py v7.10.6, created at 2026-01-26 21:24 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2026-01-26 21:24 +0000
1"""The Auto Arm integration"""
3import logging
4from dataclasses import dataclass
5from enum import StrEnum, auto
7import voluptuous as vol
8from homeassistant.components.alarm_control_panel.const import AlarmControlPanelState
9from homeassistant.components.calendar import CalendarEvent
10from homeassistant.const import (
11 CONF_ALIAS,
12 CONF_CONDITIONS,
13 CONF_DELAY_TIME,
14 CONF_ENTITY_ID,
15 CONF_SERVICE,
16 STATE_HOME,
17 STATE_NOT_HOME,
18)
19from homeassistant.helpers import config_validation as cv
20from homeassistant.helpers.typing import ConfigType
22_LOGGER = logging.getLogger(__name__)
24DOMAIN = "autoarm"
26ATTR_ACTION = "action"
27ATTR_RESET = "reset"
28CONF_DATA = "data"
29CONF_NOTIFY = "notify"
30CONF_ALARM_PANEL = "alarm_panel"
31CONF_ALARM_STATES = "alarm_states"
33ALARM_STATES = [k.lower() for k in AlarmControlPanelState.__members__]
35NO_CAL_EVENT_MODE_AUTO = "auto"
36NO_CAL_EVENT_MODE_MANUAL = "manual"
37NO_CAL_EVENT_OPTIONS: list[str] = [NO_CAL_EVENT_MODE_AUTO, NO_CAL_EVENT_MODE_MANUAL, *ALARM_STATES]
39NOTIFY_COMMON = "common"
40NOTIFY_QUIET = "quiet"
41NOTIFY_NORMAL = "normal"
42NOTIFY_CATEGORIES = [NOTIFY_COMMON, NOTIFY_QUIET, NOTIFY_NORMAL]
44NOTIFY_DEF_SCHEMA = vol.Schema({vol.Optional(CONF_SERVICE): cv.service, vol.Optional(CONF_DATA): dict})
46NOTIFY_SCHEMA = vol.Schema({
47 vol.Optional(NOTIFY_COMMON): {vol.Required(CONF_SERVICE): cv.service, vol.Optional(CONF_DATA): dict},
48 vol.Optional(NOTIFY_QUIET): NOTIFY_DEF_SCHEMA,
49 vol.Optional(NOTIFY_NORMAL): NOTIFY_DEF_SCHEMA,
50})
52DEFAULT_CALENDAR_MAPPINGS = {
53 AlarmControlPanelState.ARMED_AWAY: "Away",
54 AlarmControlPanelState.DISARMED: "Disarmed",
55 AlarmControlPanelState.ARMED_HOME: "Home",
56 AlarmControlPanelState.ARMED_VACATION: "Vacation",
57 AlarmControlPanelState.ARMED_VACATION: "Night",
58}
60CONF_CALENDAR_CONTROL = "calendar_control"
61CONF_CALENDARS = "calendars"
62CONF_CALENDAR_POLL_INTERVAL = "poll_interval"
63CONF_CALENDAR_EVENT_STATES = "state_patterns"
64CONF_CALENDAR_NO_EVENT = "no_event_mode"
65CALENDAR_SCHEMA = vol.Schema({
66 vol.Required(CONF_ENTITY_ID): cv.entity_id,
67 vol.Optional(CONF_ALIAS): cv.string,
68 vol.Optional(CONF_CALENDAR_POLL_INTERVAL, default=30): cv.positive_int,
69 vol.Optional(CONF_CALENDAR_EVENT_STATES, default=DEFAULT_CALENDAR_MAPPINGS): {
70 vol.In(ALARM_STATES): vol.All(cv.ensure_list, [cv.string])
71 },
72})
73CALENDAR_CONTROL_SCHEMA = vol.Schema({
74 vol.Optional(CONF_CALENDAR_NO_EVENT, default=NO_CAL_EVENT_MODE_AUTO): vol.All(vol.Lower, vol.In(NO_CAL_EVENT_OPTIONS)),
75 vol.Optional(CONF_CALENDARS, default=[]): vol.All(cv.ensure_list, [CALENDAR_SCHEMA]),
76})
78CONF_TRANSITIONS = "transitions"
79TRANSITION_SCHEMA = vol.Schema({vol.Optional(CONF_ALIAS): cv.string, vol.Required(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA})
81CONF_BUTTONS = "buttons"
82BUTTON_OPTIONS = [ATTR_RESET, *ALARM_STATES]
83BUTTON_SCHEMA = vol.Schema({
84 vol.Optional(CONF_ALIAS): cv.string,
85 vol.Optional(CONF_DELAY_TIME): vol.All(cv.time_period, cv.positive_timedelta),
86 vol.Required(CONF_ENTITY_ID): vol.All(cv.ensure_list, [cv.entity_id]),
87})
89CONF_RATE_LIMIT = "rate_limit"
90CONF_RATE_LIMIT_CALLS = "max_calls"
91CONF_RATE_LIMIT_PERIOD = "period"
92RATE_LIMIT_SCHEMA = vol.Schema({
93 vol.Optional(CONF_RATE_LIMIT_PERIOD, default=60): vol.All(cv.time_period, cv.positive_timedelta),
94 vol.Optional(CONF_RATE_LIMIT_CALLS, default=6): cv.positive_int,
95})
97CONF_OCCUPANCY = "occupancy"
98CONF_DAY = "day"
99CONF_NIGHT = "night"
100CONF_OCCUPANCY_DEFAULT = "default_state"
101OCCUPANCY_SCHEMA = vol.Schema({
102 vol.Required(CONF_ENTITY_ID, default=[]): vol.All(cv.ensure_list, [cv.entity_id]),
103 vol.Optional(CONF_OCCUPANCY_DEFAULT, default={CONF_DAY: AlarmControlPanelState.ARMED_HOME}): {
104 vol.In([CONF_DAY, CONF_NIGHT]): vol.In(ALARM_STATES)
105 },
106 vol.Optional(CONF_DELAY_TIME): {vol.In([STATE_HOME, STATE_NOT_HOME]): vol.All(cv.time_period, cv.positive_timedelta)},
107})
109CONF_DIURNAL = "diurnal"
110CONF_SUNRISE = "sunrise"
111CONF_EARLIEST = "earliest"
113CONFIG_SCHEMA = vol.Schema(
114 {
115 DOMAIN: vol.Schema({
116 vol.Required(CONF_ALARM_PANEL): vol.Schema({
117 vol.Optional(CONF_ALIAS): cv.string,
118 vol.Required(CONF_ENTITY_ID): cv.entity_id,
119 }),
120 vol.Optional(CONF_DIURNAL): vol.Schema({
121 vol.Optional(CONF_SUNRISE): vol.Schema({vol.Optional(CONF_EARLIEST): cv.time})
122 }),
123 vol.Optional(CONF_TRANSITIONS): {vol.In(ALARM_STATES): TRANSITION_SCHEMA},
124 vol.Optional(CONF_CALENDAR_CONTROL): CALENDAR_CONTROL_SCHEMA,
125 vol.Optional(CONF_BUTTONS): {vol.In(BUTTON_OPTIONS): BUTTON_SCHEMA},
126 vol.Optional(CONF_OCCUPANCY, default={}): OCCUPANCY_SCHEMA,
127 vol.Optional(CONF_NOTIFY, default={}): NOTIFY_SCHEMA,
128 vol.Optional(CONF_RATE_LIMIT, default={}): RATE_LIMIT_SCHEMA,
129 })
130 },
131 extra=vol.ALLOW_EXTRA, # validation fails without this by trying to include all of HASS config
132)
134DEFAULT_TRANSITIONS: dict[str, str | list[str]] = {
135 "armed_home": [
136 "{{ autoarm.occupied and not autoarm.night }}",
137 "{{ autoarm.computed and autoarm.occupied_daytime_state == 'armed_home'}}",
138 ],
139 "armed_away": "{{ not autoarm.occupied and autoarm.computed}}",
140 "disarmed": [
141 "{{ autoarm.occupied and not autoarm.night }}",
142 "{{ autoarm.computed and autoarm.occupied_daytime_state == 'disarmed'}}",
143 ],
144 "armed_night": "{{ autoarm.occupied and autoarm.night and autoarm.computed}}",
145 "armed_vacation": "{{ autoarm.vacation }}",
146}
149@dataclass
150class ConditionVariables:
151 """Field with sub-fields added to the template context of Transition Conditions"""
153 occupied: bool
154 night: bool
155 state: AlarmControlPanelState
156 occupied_defaults: dict[str, AlarmControlPanelState]
157 calendar_event: CalendarEvent | None = None
158 at_home: list[str] | None = None
159 not_home: list[str] | None = None
161 def as_dict(self) -> ConfigType:
162 """Generate the field to be exposed in the context, stringifying alarm states"""
163 return {
164 "daytime": not self.night,
165 "occupied": self.occupied,
166 "at_home": self.at_home or [],
167 "not_home": self.at_home or [],
168 "vacation": self.state == AlarmControlPanelState.ARMED_VACATION,
169 "night": self.night,
170 "bypass": self.state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
171 "manual": self.state in (AlarmControlPanelState.ARMED_VACATION, AlarmControlPanelState.ARMED_CUSTOM_BYPASS),
172 "calendar_event": self.calendar_event,
173 "state": str(self.state),
174 "occupied_daytime_state": self.occupied_defaults.get(CONF_DAY, AlarmControlPanelState.ARMED_HOME),
175 "disarmed": self.state == AlarmControlPanelState.DISARMED,
176 "computed": not self.calendar_event
177 and self.state not in (AlarmControlPanelState.ARMED_VACATION, AlarmControlPanelState.ARMED_CUSTOM_BYPASS),
178 }
181class ChangeSource(StrEnum):
182 """Enumeration of all the known ways to trigger a state change"""
184 CALENDAR = auto()
185 MOBILE = auto()
186 OCCUPANCY = auto()
187 INTERNAL = auto()
188 ALARM_PANEL = auto()
189 BUTTON = auto()
190 ACTION = auto()
191 NOTIFY = auto()
192 SUNRISE = auto()
193 SUNSET = auto()
194 ZOMBIFICATION = auto()
195 STARTUP = auto()