Added VT Thermometer to HACS
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# pylint: disable=broad-exception-caught
|
||||
|
||||
"""Underlying state manager for versatile_thermostat.
|
||||
|
||||
This module provides `UnderlyingStateManager` which listens to state change
|
||||
events for a list of entity_ids and stores the last known `State` for each
|
||||
entity.
|
||||
"""
|
||||
from typing import List, Optional, Callable, Any
|
||||
import logging
|
||||
from vtherm_api.log_collector import get_vtherm_logger
|
||||
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
|
||||
_LOGGER = get_vtherm_logger(__name__)
|
||||
|
||||
|
||||
class UnknownEntity(Exception):
|
||||
"""Raised when an entity_id is not managed by the manager."""
|
||||
|
||||
|
||||
class UnderlyingStateManager:
|
||||
"""Manage states of underlying entities.
|
||||
|
||||
Accepts a list of `entity_id` strings to monitor, initializes an
|
||||
internal array of states (`State | None`) and stores the last received
|
||||
state for each entity when state change events occur.
|
||||
|
||||
Optionally accepts `on_change` callback with signature
|
||||
`async def on_change(entity_id: str, new_state: Optional[State])` that
|
||||
will be scheduled when a state change is received.
|
||||
"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, on_change: Optional[Callable[[str, Optional[State], Optional[State]], Any]] = None) -> None:
|
||||
"""Create the manager without any monitored entity IDs.
|
||||
|
||||
Entities can be added later with `add_underlying_entities`.
|
||||
"""
|
||||
self._hass: HomeAssistant = hass
|
||||
self._entity_ids: List[str] = []
|
||||
self._on_change = on_change
|
||||
|
||||
# no states initially; entities added at runtime
|
||||
self._states: List[Optional[State]] = []
|
||||
|
||||
# no listener registered until entities are added
|
||||
self._remove_callback: Optional[Callable[[], None]] = None
|
||||
|
||||
async def _state_changed(self, event) -> None:
|
||||
"""Internal callback invoked on each state change event."""
|
||||
new_state: Optional[State] = event.data.get("new_state", None)
|
||||
old_state: Optional[State] = event.data.get("old_state", None)
|
||||
# Retrieve the entity_id from the event (sometimes in data or via new_state)
|
||||
entity_id = event.data.get("entity_id") if event.data.get("entity_id") else (
|
||||
new_state.entity_id if new_state else None
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"UnderlyingStateManager - State change event received: %s for entity_id: %s",
|
||||
new_state,
|
||||
entity_id,
|
||||
)
|
||||
if entity_id is None:
|
||||
return
|
||||
|
||||
if not self._set_state(entity_id, new_state):
|
||||
return
|
||||
|
||||
if self._on_change:
|
||||
try:
|
||||
self._hass.async_create_task(self._on_change(entity_id, new_state, old_state))
|
||||
except Exception as err: # pragma: no cover - defensive
|
||||
_LOGGER.exception("Error %s scheduling on_change for %s", err, entity_id)
|
||||
|
||||
def get_state(self, entity_id: str) -> Optional[State]:
|
||||
"""Return the last known `State` for `entity_id`, or `None` if unknown."""
|
||||
idx = self._index_of(entity_id)
|
||||
if idx is None:
|
||||
state = self._hass.states.get(entity_id)
|
||||
if state is not None:
|
||||
_LOGGER.debug(
|
||||
"UnderlyingStateManager - Requested state for unknown entity_id: %s, found in HA",
|
||||
entity_id,
|
||||
)
|
||||
return state
|
||||
_LOGGER.error("UnderlyingStateManager - Requested state for unknown entity_id: %s", entity_id)
|
||||
return None
|
||||
return self._states[idx]
|
||||
|
||||
def _set_state(self, entity_id: str, state: Optional[State]) -> bool:
|
||||
"""Set the cached state for an entity and schedule `on_change` if present.
|
||||
Return True if the state was updated, False if ignored.
|
||||
States that are `None`, `STATE_UNAVAILABLE`, or `STATE_UNKNOWN` are ignored
|
||||
"""
|
||||
idx = self._index_of(entity_id)
|
||||
if idx is None:
|
||||
raise UnknownEntity(f"Entity ID {entity_id} is not managed by UnderlyingStateManager")
|
||||
|
||||
# if state is None or state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN, None]:
|
||||
# _LOGGER.debug("UnderlyingStateManager - Ignoring state change to unavailable/unknown for entity_id: %s", entity_id)
|
||||
# return False
|
||||
|
||||
self._states[idx] = state
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_all_states_initialized(self) -> bool:
|
||||
"""Return True if all monitored states are initialized and available.
|
||||
|
||||
A state is considered initialized if it is not None and its
|
||||
`.state` is not `STATE_UNAVAILABLE` or `STATE_UNKNOWN`.
|
||||
"""
|
||||
for st in self._states:
|
||||
if st is None or st.state in [STATE_UNAVAILABLE, STATE_UNKNOWN]:
|
||||
return False
|
||||
return len(self._states) == len(self._entity_ids)
|
||||
|
||||
def get_uninitialized_entities(self) -> List[str]:
|
||||
"""Return a list of entity_ids that are not initialized."""
|
||||
uninitialized = []
|
||||
for entity_id, state in zip(self._entity_ids, self._states):
|
||||
if state is None or state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN]:
|
||||
uninitialized.append(entity_id)
|
||||
return uninitialized
|
||||
|
||||
def _index_of(self, entity_id: str) -> Optional[int]:
|
||||
"""Return the index of `entity_id` in the monitored list, or None."""
|
||||
try:
|
||||
return self._entity_ids.index(entity_id)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def add_underlying_entities(self, entity_ids: List[str]) -> None:
|
||||
"""Add multiple entity_ids to the monitored list at runtime.
|
||||
|
||||
Existing entities are ignored. The state change listener is (re)
|
||||
registered for the updated entity list. If any added entity has an
|
||||
initial state and `on_change` is set, the callback is scheduled for
|
||||
that entity.
|
||||
"""
|
||||
added = []
|
||||
for entity_id in entity_ids:
|
||||
if entity_id in self._entity_ids:
|
||||
continue
|
||||
self._entity_ids.append(entity_id)
|
||||
state = self._hass.states.get(entity_id)
|
||||
# should always add an initial state even if None
|
||||
self._states.append(state)
|
||||
if state and state.state not in [STATE_UNAVAILABLE, STATE_UNKNOWN, None]:
|
||||
# but don't notify if not a real state
|
||||
added.append(entity_id)
|
||||
|
||||
# re-register the state change listener for the new set of entity_ids
|
||||
if self._remove_callback:
|
||||
try:
|
||||
self._remove_callback()
|
||||
except Exception:
|
||||
_LOGGER.exception("Error removing previous callback while adding %s", added)
|
||||
|
||||
self._remove_callback = async_track_state_change_event(
|
||||
self._hass, self._entity_ids, self._state_changed
|
||||
)
|
||||
|
||||
# schedule on_change for each newly added entity if initial state exists
|
||||
if self._on_change:
|
||||
for eid in added:
|
||||
st = self._states[self._entity_ids.index(eid)]
|
||||
if st is not None:
|
||||
try:
|
||||
self._hass.async_create_task(self._on_change(eid, st, None))
|
||||
except Exception as error: # pragma: no cover - defensive
|
||||
_LOGGER.exception("Error scheduling on_change for %s. error is %s", eid, error)
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop listening to state changes and remove the callback."""
|
||||
if self._remove_callback:
|
||||
try:
|
||||
self._remove_callback()
|
||||
except Exception:
|
||||
pass
|
||||
self._remove_callback = None
|
||||
Reference in New Issue
Block a user