Make manual and manual_mqtt fully asynchronous (#83436)

* manual: make it completely async

Restoring the timers on startup cannot use track_point_in_time,
because the restoring code runs from the async_added_to_hass
method.  Rewrite everything to run in the event loop instead
of using threaded wrappers, this way the code can be reused in
async_added_to_hass.

* manual_mqtt: replace async function with @callback

* manual_mqtt: make it completely async

Restoring the timers on startup cannot use track_point_in_time,
because the restoring code runs from the async_added_to_hass
method.  Rewrite everything to run in the event loop instead
of using threaded wrappers, this way the code can be reused in
async_added_to_hass when state restore is added.

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
This commit is contained in:
Paolo Bonzini 2022-12-17 19:31:17 +01:00 committed by GitHub
parent 5c272583e7
commit 02fa5656bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 59 additions and 54 deletions

View File

@ -32,7 +32,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
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 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.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
@ -284,61 +284,61 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity):
return alarm.CodeFormat.NUMBER return alarm.CodeFormat.NUMBER
return alarm.CodeFormat.TEXT return alarm.CodeFormat.TEXT
def alarm_disarm(self, code: str | None = None) -> None: async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command.""" """Send disarm command."""
if not self._validate_code(code, STATE_ALARM_DISARMED): if not self._async_validate_code(code, STATE_ALARM_DISARMED):
return return
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
self._state_ts = dt_util.utcnow() self._state_ts = dt_util.utcnow()
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
def alarm_arm_home(self, code: str | None = None) -> None: async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command.""" """Send arm home command."""
if self.code_arm_required and not self._validate_code( if self.code_arm_required and not self._async_validate_code(
code, STATE_ALARM_ARMED_HOME code, STATE_ALARM_ARMED_HOME
): ):
return return
self._update_state(STATE_ALARM_ARMED_HOME) self._async_update_state(STATE_ALARM_ARMED_HOME)
def alarm_arm_away(self, code: str | None = None) -> None: async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command.""" """Send arm away command."""
if self.code_arm_required and not self._validate_code( if self.code_arm_required and not self._async_validate_code(
code, STATE_ALARM_ARMED_AWAY code, STATE_ALARM_ARMED_AWAY
): ):
return return
self._update_state(STATE_ALARM_ARMED_AWAY) self._async_update_state(STATE_ALARM_ARMED_AWAY)
def alarm_arm_night(self, code: str | None = None) -> None: async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command.""" """Send arm night command."""
if self.code_arm_required and not self._validate_code( if self.code_arm_required and not self._async_validate_code(
code, STATE_ALARM_ARMED_NIGHT code, STATE_ALARM_ARMED_NIGHT
): ):
return return
self._update_state(STATE_ALARM_ARMED_NIGHT) self._async_update_state(STATE_ALARM_ARMED_NIGHT)
def alarm_arm_vacation(self, code: str | None = None) -> None: async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
"""Send arm vacation command.""" """Send arm vacation command."""
if self.code_arm_required and not self._validate_code( if self.code_arm_required and not self._async_validate_code(
code, STATE_ALARM_ARMED_VACATION code, STATE_ALARM_ARMED_VACATION
): ):
return return
self._update_state(STATE_ALARM_ARMED_VACATION) self._async_update_state(STATE_ALARM_ARMED_VACATION)
def alarm_arm_custom_bypass(self, code: str | None = None) -> None: async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None:
"""Send arm custom bypass command.""" """Send arm custom bypass command."""
if self.code_arm_required and not self._validate_code( if self.code_arm_required and not self._async_validate_code(
code, STATE_ALARM_ARMED_CUSTOM_BYPASS code, STATE_ALARM_ARMED_CUSTOM_BYPASS
): ):
return return
self._update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS) self._async_update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS)
def alarm_trigger(self, code: str | None = None) -> None: async def async_alarm_trigger(self, code: str | None = None) -> None:
""" """
Send alarm trigger command. Send alarm trigger command.
@ -347,9 +347,9 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity):
""" """
if not self._trigger_time_by_state[self._active_state]: if not self._trigger_time_by_state[self._active_state]:
return return
self._update_state(STATE_ALARM_TRIGGERED) self._async_update_state(STATE_ALARM_TRIGGERED)
def _update_state(self, state: str) -> None: def _async_update_state(self, state: str) -> None:
"""Update the state.""" """Update the state."""
if self._state == state: if self._state == state:
return return
@ -357,16 +357,16 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity):
self._previous_state = self._state self._previous_state = self._state
self._state = state self._state = state
self._state_ts = dt_util.utcnow() self._state_ts = dt_util.utcnow()
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
if state == STATE_ALARM_TRIGGERED: if state == STATE_ALARM_TRIGGERED:
pending_time = self._pending_time(state) pending_time = self._pending_time(state)
track_point_in_time( async_track_point_in_time(
self._hass, self.async_scheduled_update, self._state_ts + pending_time self._hass, self.async_scheduled_update, self._state_ts + pending_time
) )
trigger_time = self._trigger_time_by_state[self._previous_state] trigger_time = self._trigger_time_by_state[self._previous_state]
track_point_in_time( async_track_point_in_time(
self._hass, self._hass,
self.async_scheduled_update, self.async_scheduled_update,
self._state_ts + pending_time + trigger_time, self._state_ts + pending_time + trigger_time,
@ -374,20 +374,20 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity):
elif state in SUPPORTED_ARMING_STATES: elif state in SUPPORTED_ARMING_STATES:
arming_time = self._arming_time(state) arming_time = self._arming_time(state)
if arming_time: if arming_time:
track_point_in_time( async_track_point_in_time(
self._hass, self._hass,
self.async_scheduled_update, self.async_scheduled_update,
self._state_ts + arming_time, self._state_ts + arming_time,
) )
def _validate_code(self, code, state): def _async_validate_code(self, code, state):
"""Validate given code.""" """Validate given code."""
if self._code is None: if self._code is None:
return True return True
if isinstance(self._code, str): if isinstance(self._code, str):
alarm_code = self._code alarm_code = self._code
else: else:
alarm_code = self._code.render( alarm_code = self._code.async_render(
parse_result=False, from_state=self._state, to_state=state parse_result=False, from_state=self._state, to_state=state
) )
check = not alarm_code or code == alarm_code check = not alarm_code or code == alarm_code

View File

@ -27,12 +27,12 @@ from homeassistant.const import (
STATE_ALARM_PENDING, STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED, STATE_ALARM_TRIGGERED,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
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 ( from homeassistant.helpers.event import (
async_track_point_in_time,
async_track_state_change_event, async_track_state_change_event,
track_point_in_time,
) )
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
@ -314,43 +314,43 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity):
return alarm.CodeFormat.NUMBER return alarm.CodeFormat.NUMBER
return alarm.CodeFormat.TEXT return alarm.CodeFormat.TEXT
def alarm_disarm(self, code: str | None = None) -> None: async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command.""" """Send disarm command."""
if not self._validate_code(code, STATE_ALARM_DISARMED): if not self._async_validate_code(code, STATE_ALARM_DISARMED):
return return
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
self._state_ts = dt_util.utcnow() self._state_ts = dt_util.utcnow()
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
def alarm_arm_home(self, code: str | None = None) -> None: async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command.""" """Send arm home command."""
if self.code_arm_required and not self._validate_code( if self.code_arm_required and not self._async_validate_code(
code, STATE_ALARM_ARMED_HOME code, STATE_ALARM_ARMED_HOME
): ):
return return
self._update_state(STATE_ALARM_ARMED_HOME) self._async_update_state(STATE_ALARM_ARMED_HOME)
def alarm_arm_away(self, code: str | None = None) -> None: async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command.""" """Send arm away command."""
if self.code_arm_required and not self._validate_code( if self.code_arm_required and not self._async_validate_code(
code, STATE_ALARM_ARMED_AWAY code, STATE_ALARM_ARMED_AWAY
): ):
return return
self._update_state(STATE_ALARM_ARMED_AWAY) self._async_update_state(STATE_ALARM_ARMED_AWAY)
def alarm_arm_night(self, code: str | None = None) -> None: async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command.""" """Send arm night command."""
if self.code_arm_required and not self._validate_code( if self.code_arm_required and not self._async_validate_code(
code, STATE_ALARM_ARMED_NIGHT code, STATE_ALARM_ARMED_NIGHT
): ):
return return
self._update_state(STATE_ALARM_ARMED_NIGHT) self._async_update_state(STATE_ALARM_ARMED_NIGHT)
def alarm_trigger(self, code: str | None = None) -> None: async def async_alarm_trigger(self, code: str | None = None) -> None:
""" """
Send alarm trigger command. Send alarm trigger command.
@ -359,9 +359,9 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity):
""" """
if not self._trigger_time_by_state[self._active_state]: if not self._trigger_time_by_state[self._active_state]:
return return
self._update_state(STATE_ALARM_TRIGGERED) self._async_update_state(STATE_ALARM_TRIGGERED)
def _update_state(self, state): def _async_update_state(self, state: str) -> None:
"""Update the state.""" """Update the state."""
if self._state == state: if self._state == state:
return return
@ -369,33 +369,33 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity):
self._previous_state = self._state self._previous_state = self._state
self._state = state self._state = state
self._state_ts = dt_util.utcnow() self._state_ts = dt_util.utcnow()
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
pending_time = self._pending_time(state) pending_time = self._pending_time(state)
if state == STATE_ALARM_TRIGGERED: if state == STATE_ALARM_TRIGGERED:
track_point_in_time( async_track_point_in_time(
self._hass, self.async_update_ha_state, self._state_ts + pending_time self._hass, self.async_scheduled_update, self._state_ts + pending_time
) )
trigger_time = self._trigger_time_by_state[self._previous_state] trigger_time = self._trigger_time_by_state[self._previous_state]
track_point_in_time( async_track_point_in_time(
self._hass, self._hass,
self.async_update_ha_state, self.async_scheduled_update,
self._state_ts + pending_time + trigger_time, self._state_ts + pending_time + trigger_time,
) )
elif state in SUPPORTED_PENDING_STATES and pending_time: elif state in SUPPORTED_PENDING_STATES and pending_time:
track_point_in_time( async_track_point_in_time(
self._hass, self.async_update_ha_state, self._state_ts + pending_time self._hass, self.async_scheduled_update, self._state_ts + pending_time
) )
def _validate_code(self, code, state): def _async_validate_code(self, code, state):
"""Validate given code.""" """Validate given code."""
if self._code is None: if self._code is None:
return True return True
if isinstance(self._code, str): if isinstance(self._code, str):
alarm_code = self._code alarm_code = self._code
else: else:
alarm_code = self._code.render( alarm_code = self._code.async_render(
from_state=self._state, to_state=state, parse_result=False from_state=self._state, to_state=state, parse_result=False
) )
check = not alarm_code or code == alarm_code check = not alarm_code or code == alarm_code
@ -413,6 +413,11 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity):
ATTR_POST_PENDING_STATE: self._state, ATTR_POST_PENDING_STATE: self._state,
} }
@callback
def async_scheduled_update(self, now):
"""Update state at a scheduled point in time."""
self.async_write_ha_state()
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Subscribe to MQTT events.""" """Subscribe to MQTT events."""
async_track_state_change_event( async_track_state_change_event(