Skip to content

Commit

Permalink
feat: add option to ignore unavailable entities (#136)
Browse files Browse the repository at this point in the history
  • Loading branch information
hugobloem authored Nov 15, 2024
1 parent 06c72ae commit dff74e5
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 22 deletions.
52 changes: 38 additions & 14 deletions custom_components/stateful_scenes/StatefulScenes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@

import asyncio
import logging
import os

import yaml
import os
from homeassistant.core import HomeAssistant, Event, EventStateChangedData
from homeassistant.core import Event, EventStateChangedData, HomeAssistant
from homeassistant.helpers.template import area_id, area_name
from .helpers import (
get_name_from_entity_id,
get_icon_from_entity_id,
get_id_from_entity_id,
)

from .const import (
ATTRIBUTES_TO_CHECK,
CONF_SCENE_NAME,
CONF_SCENE_LEARN,
CONF_SCENE_NUMBER_TOLERANCE,
CONF_SCENE_ENTITY_ID,
CONF_SCENE_ID,
CONF_SCENE_AREA,
CONF_SCENE_ENTITIES,
CONF_SCENE_ENTITY_ID,
CONF_SCENE_ICON,
CONF_SCENE_ID,
CONF_SCENE_LEARN,
CONF_SCENE_NAME,
CONF_SCENE_NUMBER_TOLERANCE,
)
from .helpers import (
get_icon_from_entity_id,
get_id_from_entity_id,
get_name_from_entity_id,
)

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -217,6 +217,7 @@ def __init__(self, hass: HomeAssistant, scene_conf: dict) -> None:
self._transition_time = 0.0
self._restore_on_deactivate = True
self._debounce_time: float = 0
self._ignore_unavailable = False

self.callback = None
self.callback_funcs = {}
Expand Down Expand Up @@ -300,6 +301,15 @@ def set_restore_on_deactivate(self, restore_on_deactivate):
"""Set the restore on deactivate flag."""
self._restore_on_deactivate = restore_on_deactivate

@property
def ignore_unavailable(self) -> bool:
"""Get the ignore unavailable flag."""
return self._ignore_unavailable

def set_ignore_unavailable(self, ignore_unavailable):
"""Set the ignore unavailable flag."""
self._ignore_unavailable = ignore_unavailable

def register_callback(self):
"""Register callback."""
schedule_update_func = self.callback_funcs.get("schedule_update_func", None)
Expand Down Expand Up @@ -354,6 +364,9 @@ def check_state(self, entity_id, new_state):
_LOGGER.warning(f"Entity not found: {entity_id}")
return False

if self.ignore_unavailable and new_state.state == "unavailable":
return None

# Check state
if not self.compare_values(self.entities[entity_id]["state"], new_state.state):
_LOGGER.debug(
Expand Down Expand Up @@ -394,11 +407,22 @@ def check_state(self, entity_id, new_state):
return True

def check_all_states(self):
"""Check the state of the scene."""
"""Check the state of the scene.
If all entities are in the desired state, the scene is on. If any entity is not
in the desired state, the scene is off. Unavaiblable entities are ignored, but
if all entities are unavailable, the scene is off.
"""
for entity_id in self.entities:
state = self.hass.states.get(entity_id)
self.states[entity_id] = self.check_state(entity_id, state)
self._is_on = all(self.states.values())

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

if not states:
self._is_on = False

self._is_on = all(states)

def store_entity_state(self, entity_id, state):
"""Store the state of an entity."""
Expand Down
5 changes: 5 additions & 0 deletions custom_components/stateful_scenes/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
CONF_DEBOUNCE_TIME,
CONF_ENABLE_DISCOVERY,
CONF_EXTERNAL_SCENE_ACTIVE,
CONF_IGNORE_UNAVAILABLE,
CONF_NUMBER_TOLERANCE,
CONF_RESTORE_STATES_ON_DEACTIVATE,
CONF_SCENE_ENTITIES,
Expand All @@ -25,6 +26,7 @@
DEFAULT_DEBOUNCE_TIME,
DEFAULT_ENABLE_DISCOVERY,
DEFAULT_EXTERNAL_SCENE_ACTIVE,
DEFAULT_IGNORE_UNAVAILABLE,
DEFAULT_NUMBER_TOLERANCE,
DEFAULT_RESTORE_STATES_ON_DEACTIVATE,
DEFAULT_SCENE_PATH,
Expand Down Expand Up @@ -134,6 +136,9 @@ async def async_step_configure_internal_scenes(
min=DEBOUNCE_MIN, max=DEBOUNCE_MAX, step=DEBOUNCE_STEP
)
),
vol.Optional(
CONF_IGNORE_UNAVAILABLE, default=DEFAULT_IGNORE_UNAVAILABLE
): selector.BooleanSelector(),
vol.Optional(
CONF_ENABLE_DISCOVERY, default=DEFAULT_ENABLE_DISCOVERY
): selector.BooleanSelector(),
Expand Down
2 changes: 2 additions & 0 deletions custom_components/stateful_scenes/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
CONF_TRANSITION_TIME = "transition_time"
CONF_EXTERNAL_SCENE_ACTIVE = "external_scene_active"
CONF_DEBOUNCE_TIME = "debounce_time"
CONF_IGNORE_UNAVAILABLE = "ignore_unavailable"
CONF_ENABLE_DISCOVERY = "enable_discovery"

DEFAULT_SCENE_PATH = "scenes.yaml"
Expand All @@ -17,6 +18,7 @@
DEFAULT_TRANSITION_TIME = 1
DEFAULT_EXTERNAL_SCENE_ACTIVE = False
DEFAULT_DEBOUNCE_TIME = 0.0
DEFAULT_IGNORE_UNAVAILABLE = False
DEFAULT_ENABLE_DISCOVERY = True

DEBOUNCE_MIN = 0
Expand Down
87 changes: 81 additions & 6 deletions custom_components/stateful_scenes/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,19 @@ async def async_setup_entry(
if isinstance(data[entry.entry_id], StatefulScenes.Hub):
hub = data[entry.entry_id]
for scene in hub.scenes:
entities += [StatefulSceneSwitch(scene), RestoreOnDeactivate(scene)]
entities += [
StatefulSceneSwitch(scene),
RestoreOnDeactivate(scene),
IgnoreUnavailable(scene),
]

elif isinstance(data[entry.entry_id], StatefulScenes.Scene):
scene = data[entry.entry_id]
entities += [StatefulSceneSwitch(scene), RestoreOnDeactivate(scene)]
entities += [
StatefulSceneSwitch(scene),
RestoreOnDeactivate(scene),
IgnoreUnavailable(scene),
]

else:
_LOGGER.error("Invalid entity type for %s", entry.entry_id)
Expand Down Expand Up @@ -106,9 +114,9 @@ def __init__(self, scene) -> None:
self._attr_unique_id = f"stateful_{scene.id}"

self._scene.callback_funcs = {
"state_change_func":async_track_state_change_event,
"schedule_update_func":self.schedule_update_ha_state
}
"state_change_func": async_track_state_change_event,
"schedule_update_func": self.schedule_update_ha_state,
}
self.register_callback()

@property
Expand Down Expand Up @@ -173,7 +181,7 @@ class RestoreOnDeactivate(SwitchEntity, RestoreEntity):
_attr_name = "Restore On Deactivate"
_attr_entity_category = EntityCategory.CONFIG
_attr_should_poll = True
_attr_assumed_state = True
_attr_assumed_state = False

def __init__(self, scene: StatefulScenes.Scene) -> None:
"""Initialize."""
Expand All @@ -182,6 +190,7 @@ def __init__(self, scene: StatefulScenes.Scene) -> None:
self._attr_unique_id = f"{scene.id}_restore_on_deactivate"
self._scene.set_restore_on_deactivate(scene.restore_on_deactivate)
self._is_on = scene.restore_on_deactivate
self._is_on = None

@property
def name(self) -> str:
Expand Down Expand Up @@ -231,3 +240,69 @@ async def async_added_to_hass(self):
return
self._scene.set_restore_on_deactivate(state.state == STATE_ON)
self._is_on = state.state == STATE_ON


class IgnoreUnavailable(SwitchEntity, RestoreEntity):
"""Switch entity to ignore unavailable entities."""

_attr_name = "Ignore unavailable entities"
_attr_entity_category = EntityCategory.CONFIG
_attr_should_poll = True
_attr_assumed_state = False

def __init__(self, scene: StatefulScenes.Scene) -> None:
"""Initialize."""
self._scene = scene
self._name = f"{scene.name} Ignore Unavailable"
self._attr_unique_id = f"{scene.id}_ignore_unavailable"
self._scene.set_ignore_unavailable(scene.ignore_unavailable)
self._is_on = scene.ignore_unavailable

@property
def name(self) -> str:
"""Return the display name of this light."""
return self._name

@property
def device_info(self) -> DeviceInfo | None:
"""Return the device info."""
return DeviceInfo(
identifiers={(self._scene.id,)},
name=self._scene.name,
manufacturer=DEVICE_INFO_MANUFACTURER,
suggested_area=self._scene.area_id,
)

@property
def is_on(self) -> bool:
"""Return true if light is on."""
return self._is_on

def turn_on(self, **kwargs) -> None:
"""Instruct the light to turn on.
You can skip the brightness part if your light does not support
brightness control.
"""
self._scene.set_ignore_unavailable(True)
self._is_on = self._scene.restore_on_deactivate

def turn_off(self, **kwargs) -> None:
"""Instruct the light to turn off."""
self._scene.set_ignore_unavailable(False)
self._is_on = self._scene.restore_on_deactivate

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

async def async_added_to_hass(self):
"""Handle entity which will be added."""
state = await self.async_get_last_state()
if not state:
return
self._scene.set_ignore_unavailable(state.state == STATE_ON)
self._is_on = state.state == STATE_ON
1 change: 1 addition & 0 deletions custom_components/stateful_scenes/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"restore_states_on_deactivate": "Restore states on deactivation",
"transition_time": "Transition time",
"debounce_time": "Debounce time",
"ignore_unavailable": "Ignore entities in unavailable state",
"enable_discovery": "Enable discovery"
}
},
Expand Down
3 changes: 2 additions & 1 deletion custom_components/stateful_scenes/translations/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"number_tolerance": "Afrondingstolerantie",
"restore_states_on_deactivate": "Status herstellen bij deactivering",
"transition_time": "Transitie tijd",
"enable_discovery": "Ontdekking inschakelen"
"enable_discovery": "Ontdekking inschakelen",
"ignore_unavailable": "Negeer entiteiten in niet-beschikbare staat"
}
},
"select_external_scenes": {
Expand Down
3 changes: 2 additions & 1 deletion custom_components/stateful_scenes/translations/sk.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"restore_states_on_deactivate": "Obnovte stavy pri deaktivácii",
"transition_time": "Čas prechodu",
"debounce_time": "Čas odskoku",
"enable_discovery": "Povoliť objavovanie"
"enable_discovery": "Povoliť objavovanie",
"ignore_unavailable": "Ignorovať entity v nedostupnom stave"
}
},
"select_external_scenes": {
Expand Down

0 comments on commit dff74e5

Please sign in to comment.