Skip to content

Commit

Permalink
This is a combination of 2 commits.
Browse files Browse the repository at this point in the history
This is a combination of 2 commits.

This is a combination of 2 commits.

Avoid multiple threads on the same scene by
using change all eval related methods to async

remove unused import

Statisfy Ruff for switch import platform schema

Add async_cancel_if_active to avoid calling
async_executor_job
  • Loading branch information
cayossarian committed Dec 31, 2024
1 parent 6232651 commit eecba61
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 30 deletions.
60 changes: 42 additions & 18 deletions custom_components/stateful_scenes/StatefulScenes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any

from homeassistant.core import Event, EventStateChangedData, HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.template import area_id, area_name

Expand Down Expand Up @@ -57,7 +58,7 @@ def __init__(

async def async_start(self, callback) -> None:
"""Start a new timer if we have a duration."""
await self._hass.async_add_executor_job(self.cancel_if_active)
await self.async_cancel_if_active()
if self.transition_time > 0 and self._hass is not None:
_LOGGER.debug(
"Starting scene evaluation timer for %s seconds",
Expand Down Expand Up @@ -88,14 +89,17 @@ def set_debounce_time(self, time: float) -> None:
"""Set the timer duration."""
self._debounce_time = time or 0.0

def set(self, cancel_callback) -> None:
"""Store new timer's cancel callback."""
self._cancel_callback = cancel_callback

def cancel_if_active(self) -> None:
"""Cancel current timer if active."""
if self._cancel_callback:
_LOGGER.debug("Cancelling active scene evaluation timer")
_LOGGER.debug("Sync cancelling active scene evaluation timer")
self._cancel_callback()
self._cancel_callback = None

async def async_cancel_if_active(self) -> None:
"""Cancel current timer if active."""
if self._cancel_callback:
_LOGGER.debug("Async cancelling active scene evaluation timer")
self._cancel_callback()
self._cancel_callback = None

Expand Down Expand Up @@ -324,9 +328,10 @@ def update_callback(self, event: Event[EventStateChangedData]):

async def async_evaluate_scene_state(self):
"""Evaluate scene state immediately."""
await self.hass.async_add_executor_job(self.check_all_states)
_LOGGER.debug("[Scene: %s] Starting scene evaluation", self.name)
await self.async_check_all_states()
if self.schedule_update:
await self.hass.async_add_executor_job(self.schedule_update, True)
self.schedule_update(True)

async def async_timer_evaluate_scene_state(self, _now):
"""Handle Callback from HA after expiration of SceneEvaluationTimer."""
Expand All @@ -337,6 +342,8 @@ async def async_timer_evaluate_scene_state(self, _now):
def is_interesting_update(self, old_state, new_state):
"""Check if the state change is interesting."""
if old_state is None:
if new_state is None:
_LOGGER.warning("New State is None and Old State is None")
return True
if not self.compare_values(old_state.state, new_state.state):
return True
Expand All @@ -354,11 +361,31 @@ def is_interesting_update(self, old_state, new_state):
return True
return False

def check_state(self, entity_id, new_state):
async def async_check_state(self, entity_id, new_state):
"""Check if entity's current state matches the scene's defined state."""
if new_state is None:
_LOGGER.warning("Entity not found: %s", entity_id)
return False
# Check if entity exists in registry
# Get entity registry directly
registry = er.async_get(self.hass)
entry = registry.async_get(entity_id)

if entry is None:
_LOGGER.debug(
"[Scene: %s] Entity %s not found in registry.",
self.name,
entity_id,
)
return False

# Check if entity exists in state
new_state = self.hass.states.get(entity_id)
if new_state is None:
_LOGGER.debug(
"[Scene: %s] Entity %s not found in state.",
self.name,
entity_id,
)
return False

if self.ignore_unavailable and new_state.state == "unavailable":
return None
Expand Down Expand Up @@ -407,7 +434,7 @@ def check_state(self, entity_id, new_state):
)
return True

def check_all_states(self):
async def async_check_all_states(self):
"""Check the state of the scene.
If all entities are in the desired state, the scene is on. If any entity is not
Expand All @@ -416,14 +443,11 @@ def check_all_states(self):
"""
for entity_id in self.entities:
state = self.hass.states.get(entity_id)
self.states[entity_id] = self.check_state(entity_id, state)
self.states[entity_id] = await self.async_check_state(entity_id, state)

states = [state for state in self.states.values() if state is not None]

if not states:
self._is_on = False
else:
self._is_on = all(states)
result = all(states) if states else False
self._is_on = result

def store_entity_state(self, entity_id, state=None):
"""Store the state of an entity.
Expand Down
29 changes: 17 additions & 12 deletions custom_components/stateful_scenes/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@

import logging

# Import the device class from the component that you want to support
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity

from homeassistant.components.switch import (
PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA,
SwitchEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory, STATE_ON
from homeassistant.const import STATE_ON, EntityCategory
from homeassistant.core import HomeAssistant

# Import the device class from the component that you want to support
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from . import StatefulScenes
from .const import (
Expand All @@ -30,7 +35,7 @@
_LOGGER = logging.getLogger(__name__)

# Validation of the user's configuration
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
SWITCH_PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_SCENE_PATH, default=DEFAULT_SCENE_PATH): cv.string,
vol.Optional(
Expand Down Expand Up @@ -164,19 +169,19 @@ async def async_added_to_hass(self) -> None:
"""Validate and set the actual scene state on restart."""
await super().async_added_to_hass()

def _validate_scene_state():
self._scene.check_all_states()
async def async_validate_scene_state(_now=None):
await self._scene.async_check_all_states()
self._is_on = self._scene.is_on
self.schedule_update_ha_state()
self.async_write_ha_state()

self.hass.loop.call_later(1, _validate_scene_state)
await async_validate_scene_state()

def update(self) -> None:
async def async_update(self) -> None:
"""Fetch new state data for this light.
This is the only method that should fetch new data for Home Assistant.
"""
self._scene.check_all_states()
await self._scene.async_check_all_states()
self._is_on = self._scene.is_on

def register_callback(self) -> None:
Expand Down

0 comments on commit eecba61

Please sign in to comment.