Improve code quality in Manual alarm (#123142)

* Improve code quality in Manual alarm

* Review

* Remove helper

* Remove unique id

* Reset demo and fix unique id

* next_state variable

* Fixes

* Is helper

* Fix unique id

* exception message

* Fix mypy
This commit is contained in:
G Johansson 2024-08-18 21:31:44 +02:00 committed by GitHub
parent 14c2ca85ec
commit 11d2258afc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 128 additions and 100 deletions

View File

@ -295,6 +295,7 @@ homeassistant.components.lookin.*
homeassistant.components.luftdaten.* homeassistant.components.luftdaten.*
homeassistant.components.madvr.* homeassistant.components.madvr.*
homeassistant.components.mailbox.* homeassistant.components.mailbox.*
homeassistant.components.manual.*
homeassistant.components.map.* homeassistant.components.map.*
homeassistant.components.mastodon.* homeassistant.components.mastodon.*
homeassistant.components.matrix.* homeassistant.components.matrix.*

View File

@ -30,7 +30,7 @@ async def async_setup_entry(
"""Set up the Demo config entry.""" """Set up the Demo config entry."""
async_add_entities( async_add_entities(
[ [
ManualAlarm( # type:ignore[no-untyped-call] ManualAlarm(
hass, hass,
"Security", "Security",
"demo_alarm_control_panel", "demo_alarm_control_panel",

View File

@ -3,12 +3,12 @@
from __future__ import annotations from __future__ import annotations
import datetime import datetime
import logging
from typing import Any from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant.components.alarm_control_panel import ( from homeassistant.components.alarm_control_panel import (
PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA,
AlarmControlPanelEntity, AlarmControlPanelEntity,
AlarmControlPanelEntityFeature, AlarmControlPanelEntityFeature,
CodeFormat, CodeFormat,
@ -19,7 +19,6 @@ from homeassistant.const import (
CONF_DELAY_TIME, CONF_DELAY_TIME,
CONF_DISARM_AFTER_TRIGGER, CONF_DISARM_AFTER_TRIGGER,
CONF_NAME, CONF_NAME,
CONF_PLATFORM,
CONF_TRIGGER_TIME, CONF_TRIGGER_TIME,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_AWAY,
@ -33,15 +32,16 @@ from homeassistant.const import (
STATE_ALARM_TRIGGERED, STATE_ALARM_TRIGGERED,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import ServiceValidationError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_point_in_time from homeassistant.helpers.event import async_track_point_in_time
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__) DOMAIN = "manual"
CONF_ARMING_STATES = "arming_states" CONF_ARMING_STATES = "arming_states"
CONF_CODE_TEMPLATE = "code_template" CONF_CODE_TEMPLATE = "code_template"
@ -85,7 +85,7 @@ ATTR_PREVIOUS_STATE = "previous_state"
ATTR_NEXT_STATE = "next_state" ATTR_NEXT_STATE = "next_state"
def _state_validator(config): def _state_validator(config: dict[str, Any]) -> dict[str, Any]:
"""Validate the state.""" """Validate the state."""
for state in SUPPORTED_PRETRIGGER_STATES: for state in SUPPORTED_PRETRIGGER_STATES:
if CONF_DELAY_TIME not in config[state]: if CONF_DELAY_TIME not in config[state]:
@ -101,7 +101,7 @@ def _state_validator(config):
return config return config
def _state_schema(state): def _state_schema(state: str) -> vol.Schema:
"""Validate the state.""" """Validate the state."""
schema = {} schema = {}
if state in SUPPORTED_PRETRIGGER_STATES: if state in SUPPORTED_PRETRIGGER_STATES:
@ -120,63 +120,64 @@ def _state_schema(state):
PLATFORM_SCHEMA = vol.Schema( PLATFORM_SCHEMA = vol.Schema(
vol.All( vol.All(
{ ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend(
vol.Required(CONF_PLATFORM): "manual", {
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Exclusive(CONF_CODE, "code validation"): cv.string, vol.Exclusive(CONF_CODE, "code validation"): cv.string,
vol.Exclusive(CONF_CODE_TEMPLATE, "code validation"): cv.template, vol.Exclusive(CONF_CODE_TEMPLATE, "code validation"): cv.template,
vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All( vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All(
cv.time_period, cv.positive_timedelta cv.time_period, cv.positive_timedelta
), ),
vol.Optional(CONF_ARMING_TIME, default=DEFAULT_ARMING_TIME): vol.All( vol.Optional(CONF_ARMING_TIME, default=DEFAULT_ARMING_TIME): vol.All(
cv.time_period, cv.positive_timedelta cv.time_period, cv.positive_timedelta
), ),
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All( vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(
cv.time_period, cv.positive_timedelta cv.time_period, cv.positive_timedelta
), ),
vol.Optional( vol.Optional(
CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER
): cv.boolean, ): cv.boolean,
vol.Optional(CONF_ARMING_STATES, default=SUPPORTED_ARMING_STATES): vol.All( vol.Optional(
cv.ensure_list, [vol.In(SUPPORTED_ARMING_STATES)] CONF_ARMING_STATES, default=SUPPORTED_ARMING_STATES
), ): vol.All(cv.ensure_list, [vol.In(SUPPORTED_ARMING_STATES)]),
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema( vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema(
STATE_ALARM_ARMED_AWAY STATE_ALARM_ARMED_AWAY
), ),
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): _state_schema( vol.Optional(STATE_ALARM_ARMED_HOME, default={}): _state_schema(
STATE_ALARM_ARMED_HOME STATE_ALARM_ARMED_HOME
), ),
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): _state_schema( vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): _state_schema(
STATE_ALARM_ARMED_NIGHT STATE_ALARM_ARMED_NIGHT
), ),
vol.Optional(STATE_ALARM_ARMED_VACATION, default={}): _state_schema( vol.Optional(STATE_ALARM_ARMED_VACATION, default={}): _state_schema(
STATE_ALARM_ARMED_VACATION STATE_ALARM_ARMED_VACATION
), ),
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}): _state_schema( vol.Optional(
STATE_ALARM_ARMED_CUSTOM_BYPASS STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}
), ): _state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS),
vol.Optional(STATE_ALARM_DISARMED, default={}): _state_schema( vol.Optional(STATE_ALARM_DISARMED, default={}): _state_schema(
STATE_ALARM_DISARMED STATE_ALARM_DISARMED
), ),
vol.Optional(STATE_ALARM_TRIGGERED, default={}): _state_schema( vol.Optional(STATE_ALARM_TRIGGERED, default={}): _state_schema(
STATE_ALARM_TRIGGERED STATE_ALARM_TRIGGERED
), ),
}, },
),
_state_validator, _state_validator,
) )
) )
def setup_platform( async def async_setup_platform(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config: ConfigType,
add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the manual alarm platform.""" """Set up the manual alarm platform."""
add_entities( async_add_entities(
[ [
ManualAlarm( ManualAlarm(
hass, hass,
@ -184,8 +185,8 @@ def setup_platform(
config.get(CONF_UNIQUE_ID), config.get(CONF_UNIQUE_ID),
config.get(CONF_CODE), config.get(CONF_CODE),
config.get(CONF_CODE_TEMPLATE), config.get(CONF_CODE_TEMPLATE),
config.get(CONF_CODE_ARM_REQUIRED), config[CONF_CODE_ARM_REQUIRED],
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER), config[CONF_DISARM_AFTER_TRIGGER],
config, config,
) )
] ]
@ -206,28 +207,25 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
def __init__( def __init__(
self, self,
hass, hass: HomeAssistant,
name, name: str,
unique_id, unique_id: str | None,
code, code: str | None,
code_template, code_template: Template | None,
code_arm_required, code_arm_required: bool,
disarm_after_trigger, disarm_after_trigger: bool,
config, config: dict[str, Any],
): ) -> None:
"""Init the manual alarm panel.""" """Init the manual alarm panel."""
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
self._hass = hass self._hass = hass
self._attr_name = name self._attr_name = name
self._attr_unique_id = unique_id self._attr_unique_id = unique_id
if code_template: self._code = code_template or code or None
self._code = code_template
else:
self._code = code or None
self._attr_code_arm_required = code_arm_required self._attr_code_arm_required = code_arm_required
self._disarm_after_trigger = disarm_after_trigger self._disarm_after_trigger = disarm_after_trigger
self._previous_state = self._state self._previous_state = self._state
self._state_ts = None self._state_ts: datetime.datetime = dt_util.utcnow()
self._delay_time_by_state = { self._delay_time_by_state = {
state: config[state][CONF_DELAY_TIME] state: config[state][CONF_DELAY_TIME]
@ -253,7 +251,9 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
if self._state == STATE_ALARM_TRIGGERED: if self._state == STATE_ALARM_TRIGGERED:
if self._within_pending_time(self._state): if self._within_pending_time(self._state):
return STATE_ALARM_PENDING return STATE_ALARM_PENDING
trigger_time = self._trigger_time_by_state[self._previous_state] trigger_time: datetime.timedelta = self._trigger_time_by_state[
self._previous_state
]
if ( if (
self._state_ts + self._pending_time(self._state) + trigger_time self._state_ts + self._pending_time(self._state) + trigger_time
) < dt_util.utcnow(): ) < dt_util.utcnow():
@ -270,25 +270,27 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
return self._state return self._state
@property @property
def _active_state(self): def _active_state(self) -> str:
"""Get the current state.""" """Get the current state."""
if self.state in (STATE_ALARM_PENDING, STATE_ALARM_ARMING): if self.state in (STATE_ALARM_PENDING, STATE_ALARM_ARMING):
return self._previous_state return self._previous_state
return self._state return self._state
def _arming_time(self, state): def _arming_time(self, state: str) -> datetime.timedelta:
"""Get the arming time.""" """Get the arming time."""
return self._arming_time_by_state[state] arming_time: datetime.timedelta = self._arming_time_by_state[state]
return arming_time
def _pending_time(self, state): def _pending_time(self, state: str) -> datetime.timedelta:
"""Get the pending time.""" """Get the pending time."""
return self._delay_time_by_state[self._previous_state] delay_time: datetime.timedelta = self._delay_time_by_state[self._previous_state]
return delay_time
def _within_arming_time(self, state): def _within_arming_time(self, state: str) -> bool:
"""Get if the action is in the arming time window.""" """Get if the action is in the arming time window."""
return self._state_ts + self._arming_time(state) > dt_util.utcnow() return self._state_ts + self._arming_time(state) > dt_util.utcnow()
def _within_pending_time(self, state): def _within_pending_time(self, state: str) -> bool:
"""Get if the action is in the pending time window.""" """Get if the action is in the pending time window."""
return self._state_ts + self._pending_time(state) > dt_util.utcnow() return self._state_ts + self._pending_time(state) > dt_util.utcnow()
@ -377,7 +379,7 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
self._state_ts + arming_time, self._state_ts + arming_time,
) )
def _async_validate_code(self, code, state): def _async_validate_code(self, code: str | None, state: str) -> None:
"""Validate given code.""" """Validate given code."""
if ( if (
state != STATE_ALARM_DISARMED and not self.code_arm_required state != STATE_ALARM_DISARMED and not self.code_arm_required
@ -394,24 +396,28 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
if not alarm_code or code == alarm_code: if not alarm_code or code == alarm_code:
return return
raise HomeAssistantError("Invalid alarm code provided") raise ServiceValidationError(
"Invalid alarm code provided",
translation_domain=DOMAIN,
translation_key="invalid_code",
)
@property @property
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes.""" """Return the state attributes."""
if self.state in (STATE_ALARM_PENDING, STATE_ALARM_ARMING): if self.state in (STATE_ALARM_PENDING, STATE_ALARM_ARMING):
return { prev_state: str | None = self._previous_state
ATTR_PREVIOUS_STATE: self._previous_state, state: str | None = self._state
ATTR_NEXT_STATE: self._state, elif self.state == STATE_ALARM_TRIGGERED:
} prev_state = self._previous_state
if self.state == STATE_ALARM_TRIGGERED: state = None
return { else:
ATTR_PREVIOUS_STATE: self._previous_state, prev_state = None
} state = None
return {} return {ATTR_PREVIOUS_STATE: prev_state, ATTR_NEXT_STATE: state}
@callback @callback
def async_scheduled_update(self, now): def async_scheduled_update(self, now: datetime.datetime) -> None:
"""Update state at a scheduled point in time.""" """Update state at a scheduled point in time."""
self.async_write_ha_state() self.async_write_ha_state()
@ -420,13 +426,13 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
await super().async_added_to_hass() await super().async_added_to_hass()
if state := await self.async_get_last_state(): if state := await self.async_get_last_state():
self._state_ts = state.last_updated self._state_ts = state.last_updated
if hasattr(state, "attributes") and ATTR_NEXT_STATE in state.attributes: if next_state := state.attributes.get(ATTR_NEXT_STATE):
# If in arming or pending state we record the transition, # If in arming or pending state we record the transition,
# not the current state # not the current state
self._state = state.attributes[ATTR_NEXT_STATE] self._state = next_state
else: else:
self._state = state.state self._state = state.state
if hasattr(state, "attributes") and ATTR_PREVIOUS_STATE in state.attributes: if prev_state := state.attributes.get(ATTR_PREVIOUS_STATE):
self._previous_state = state.attributes[ATTR_PREVIOUS_STATE] self._previous_state = prev_state
self._async_set_state_update_events() self._async_set_state_update_events()

View File

@ -0,0 +1,7 @@
{
"exceptions": {
"invalid_code": {
"message": "Invalid alarm code provided"
}
}
}

View File

@ -2706,6 +2706,16 @@ disallow_untyped_defs = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.manual.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.map.*] [mypy-homeassistant.components.map.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true

View File

@ -9,6 +9,10 @@ import pytest
from homeassistant.components import alarm_control_panel from homeassistant.components import alarm_control_panel
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature
from homeassistant.components.demo import alarm_control_panel as demo from homeassistant.components.demo import alarm_control_panel as demo
from homeassistant.components.manual.alarm_control_panel import (
ATTR_NEXT_STATE,
ATTR_PREVIOUS_STATE,
)
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_CODE,
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -28,7 +32,7 @@ from homeassistant.const import (
STATE_ALARM_TRIGGERED, STATE_ALARM_TRIGGERED,
) )
from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.core import CoreState, HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import ServiceValidationError
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -227,7 +231,7 @@ async def test_with_invalid_code(hass: HomeAssistant, service, expected_state) -
assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
with pytest.raises(HomeAssistantError, match=r"^Invalid alarm code provided$"): with pytest.raises(ServiceValidationError, match=r"^Invalid alarm code provided$"):
await hass.services.async_call( await hass.services.async_call(
alarm_control_panel.DOMAIN, alarm_control_panel.DOMAIN,
service, service,
@ -1089,7 +1093,7 @@ async def test_disarm_during_trigger_with_invalid_code(hass: HomeAssistant) -> N
assert hass.states.get(entity_id).state == STATE_ALARM_PENDING assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
with pytest.raises(HomeAssistantError, match=r"^Invalid alarm code provided$"): with pytest.raises(ServiceValidationError, match=r"^Invalid alarm code provided$"):
await common.async_alarm_disarm(hass, entity_id=entity_id) await common.async_alarm_disarm(hass, entity_id=entity_id)
assert hass.states.get(entity_id).state == STATE_ALARM_PENDING assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
@ -1133,7 +1137,7 @@ async def test_disarm_with_template_code(hass: HomeAssistant) -> None:
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.state == STATE_ALARM_ARMED_HOME assert state.state == STATE_ALARM_ARMED_HOME
with pytest.raises(HomeAssistantError, match=r"^Invalid alarm code provided$"): with pytest.raises(ServiceValidationError, match=r"^Invalid alarm code provided$"):
await common.async_alarm_disarm(hass, "def") await common.async_alarm_disarm(hass, "def")
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
@ -1411,8 +1415,8 @@ async def test_restore_state_triggered(hass: HomeAssistant, previous_state) -> N
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.attributes["previous_state"] == previous_state assert state.attributes[ATTR_PREVIOUS_STATE] == previous_state
assert "next_state" not in state.attributes assert state.attributes[ATTR_NEXT_STATE] is None
assert state.state == STATE_ALARM_TRIGGERED assert state.state == STATE_ALARM_TRIGGERED
future = time + timedelta(seconds=121) future = time + timedelta(seconds=121)