Initil after Upgrade
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -46,7 +46,7 @@ DEFAULT_MODE = CompositeMode.STOP_AT_FIRST
|
||||
def make_entity_id_optional(schema: vol.Schema) -> vol.Schema:
|
||||
"""Make entity_id optional in schema."""
|
||||
schema = schema.schema
|
||||
schema[vol.Optional(CONF_ENTITY_ID)] = schema.pop(vol.Required(CONF_ENTITY_ID)) # type: ignore
|
||||
schema[vol.Optional(CONF_ENTITY_ID)] = schema.pop(vol.Required(CONF_ENTITY_ID)) # type: ignore[index, attr-defined]
|
||||
return vol.Schema(schema)
|
||||
|
||||
|
||||
@@ -55,17 +55,17 @@ def get_numeric_state_schema() -> vol.Schema:
|
||||
return make_entity_id_optional(cv.NUMERIC_STATE_CONDITION_SCHEMA.validators[0])
|
||||
|
||||
|
||||
def get_state_condition_attribute_schema(value: Any) -> dict[str, Any]: # noqa: ANN401
|
||||
def get_state_condition_attribute_schema(value: object) -> dict[str, Any]:
|
||||
"""Return the state attribute condition schema. We need to modify it to make entity_id optional."""
|
||||
return make_entity_id_optional(cv.STATE_CONDITION_ATTRIBUTE_SCHEMA)(value) # type: ignore
|
||||
return make_entity_id_optional(cv.STATE_CONDITION_ATTRIBUTE_SCHEMA)(value) # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def get_state_condition_state_schema(value: Any) -> dict[str, Any]: # noqa: ANN401
|
||||
def get_state_condition_state_schema(value: object) -> dict[str, Any]:
|
||||
"""Return the state condition schema. We need to modify it to make entity_id optional."""
|
||||
return make_entity_id_optional(cv.STATE_CONDITION_STATE_SCHEMA)(value) # type: ignore
|
||||
return make_entity_id_optional(cv.STATE_CONDITION_STATE_SCHEMA)(value) # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def get_state_schema(value: Any) -> dict[str, Any]: # noqa: ANN401
|
||||
def get_state_schema(value: object) -> dict[str, Any]:
|
||||
"""Validate a state condition."""
|
||||
if not isinstance(value, dict):
|
||||
raise vol.Invalid("Expected a dictionary") # pragma: no cover
|
||||
@@ -158,7 +158,9 @@ class CompositeStrategy(PowerCalculationStrategyInterface):
|
||||
if isinstance(strategy, PlaybookStrategy):
|
||||
await self.activate_playbook(strategy)
|
||||
|
||||
if (entity_state.state == STATE_OFF and strategy.can_calculate_standby()) or entity_state.state != STATE_OFF:
|
||||
if (
|
||||
entity_state.state == STATE_OFF and strategy.can_calculate_standby()
|
||||
) or entity_state.state != STATE_OFF:
|
||||
value = await strategy.calculate(entity_state)
|
||||
if value is not None:
|
||||
if self.mode == CompositeMode.STOP_AT_FIRST:
|
||||
@@ -186,8 +188,7 @@ class CompositeStrategy(PowerCalculationStrategyInterface):
|
||||
and manipulate the state
|
||||
"""
|
||||
for sub_strategy in self.strategies:
|
||||
if hasattr(sub_strategy.strategy, "set_update_callback"):
|
||||
sub_strategy.strategy.set_update_callback(update_callback)
|
||||
sub_strategy.strategy.set_update_callback(update_callback)
|
||||
|
||||
async def validate_config(self) -> None:
|
||||
"""Validate correct setup of the strategy."""
|
||||
@@ -204,7 +205,9 @@ class CompositeStrategy(PowerCalculationStrategyInterface):
|
||||
track_templates,
|
||||
)
|
||||
|
||||
track_entities = [entity for sub_strategy in self.strategies for entity in sub_strategy.strategy.get_entities_to_track()]
|
||||
track_entities = [
|
||||
entity for sub_strategy in self.strategies for entity in sub_strategy.strategy.get_entities_to_track()
|
||||
]
|
||||
return track_templates + track_entities
|
||||
|
||||
def can_calculate_standby(self) -> bool:
|
||||
|
||||
@@ -171,11 +171,13 @@ class PowerCalculatorStrategyFactory:
|
||||
else:
|
||||
raise StrategyConfigurationError("No composite configuration supplied")
|
||||
|
||||
sub_strategies = composite_config
|
||||
sub_strategies: list[ConfigType]
|
||||
mode = DEFAULT_MODE
|
||||
if isinstance(composite_config, dict):
|
||||
mode = composite_config.get(CONF_MODE, DEFAULT_MODE)
|
||||
sub_strategies = composite_config.get(CONF_STRATEGIES) # type: ignore
|
||||
sub_strategies = composite_config.get(CONF_STRATEGIES, [])
|
||||
else:
|
||||
sub_strategies = composite_config
|
||||
|
||||
async def _create_sub_strategy(strategy_config: ConfigType) -> SubStrategy:
|
||||
condition_instance = None
|
||||
@@ -200,6 +202,8 @@ class PowerCalculatorStrategyFactory:
|
||||
)
|
||||
return SubStrategy(condition_config, condition_instance, strategy_instance) # type: ignore
|
||||
|
||||
if not sub_strategies:
|
||||
raise StrategyConfigurationError("No strategies configured for composite strategy")
|
||||
strategies = [await _create_sub_strategy(config) for config in sub_strategies]
|
||||
return CompositeStrategy(self._hass, strategies, mode)
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class FixedStrategy(PowerCalculationStrategyInterface):
|
||||
if self._per_state_power is not None:
|
||||
# Lookup by state
|
||||
if entity_state.state in self._per_state_power:
|
||||
return await evaluate_power(
|
||||
return evaluate_power(
|
||||
self._per_state_power.get(entity_state.state) or 0,
|
||||
)
|
||||
|
||||
@@ -55,12 +55,12 @@ class FixedStrategy(PowerCalculationStrategyInterface):
|
||||
if "|" in state_key:
|
||||
attribute, value = state_key.split("|", 2)
|
||||
if str(entity_state.attributes.get(attribute)) == value:
|
||||
return await evaluate_power(power)
|
||||
return evaluate_power(power)
|
||||
|
||||
if self._power is None:
|
||||
return None
|
||||
|
||||
return await evaluate_power(self._power)
|
||||
return evaluate_power(self._power)
|
||||
|
||||
async def validate_config(self) -> None:
|
||||
"""Validate correct setup of the strategy."""
|
||||
@@ -84,8 +84,10 @@ class FixedStrategy(PowerCalculationStrategyInterface):
|
||||
track_templates.append(TrackTemplate(self._power, None, None))
|
||||
|
||||
if self._per_state_power:
|
||||
for power in list(self._per_state_power.values()):
|
||||
if isinstance(power, Template):
|
||||
track_templates.append(TrackTemplate(power, None, None)) # noqa: PERF401
|
||||
track_templates.extend(
|
||||
TrackTemplate(power, None, None)
|
||||
for power in self._per_state_power.values()
|
||||
if isinstance(power, Template)
|
||||
)
|
||||
|
||||
return track_templates
|
||||
|
||||
@@ -25,6 +25,8 @@ from custom_components.powercalc.const import (
|
||||
CONF_GAMMA_CURVE,
|
||||
CONF_MAX_POWER,
|
||||
CONF_MIN_POWER,
|
||||
CONF_POWER,
|
||||
CONF_VALUE,
|
||||
)
|
||||
from custom_components.powercalc.errors import StrategyConfigurationError
|
||||
from custom_components.powercalc.helpers import get_related_entity_by_device_class
|
||||
@@ -78,6 +80,8 @@ class LinearStrategy(PowerCalculationStrategyInterface):
|
||||
|
||||
async def calculate(self, entity_state: State) -> Decimal | None:
|
||||
"""Calculate the current power consumption."""
|
||||
value_entity = self.get_initialized_value_entity()
|
||||
|
||||
if not self._initialized:
|
||||
self._attribute = self.get_attribute(entity_state)
|
||||
self._initialized = True
|
||||
@@ -93,7 +97,7 @@ class LinearStrategy(PowerCalculationStrategyInterface):
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s: Linear mode state value: %d range(%d-%d)",
|
||||
self._value_entity.entity_id, # type: ignore
|
||||
value_entity.entity_id,
|
||||
value,
|
||||
min_value,
|
||||
max_value,
|
||||
@@ -115,9 +119,7 @@ class LinearStrategy(PowerCalculationStrategyInterface):
|
||||
|
||||
def is_enabled(self, entity_state: State) -> bool:
|
||||
"""Return if this strategy is enabled based on entity state."""
|
||||
if self._source_entity.domain == media_player.DOMAIN and entity_state.state is not STATE_PLAYING: # noqa: SIM103
|
||||
return False
|
||||
return True
|
||||
return not (self._source_entity.domain == media_player.DOMAIN and entity_state.state is not STATE_PLAYING)
|
||||
|
||||
def get_min_calibrate(self, value: int) -> tuple[int, float]:
|
||||
"""Get closest lower value from calibration table."""
|
||||
@@ -134,16 +136,19 @@ class LinearStrategy(PowerCalculationStrategyInterface):
|
||||
calibrate = self._config.get(CONF_CALIBRATE)
|
||||
if isinstance(calibrate, dict):
|
||||
calibrate = [f"{key} -> {value}" for key, value in calibrate.items()]
|
||||
elif isinstance(calibrate, list) and calibrate and isinstance(calibrate[0], dict):
|
||||
calibrate = [f"{item[CONF_VALUE]} -> {item[CONF_POWER]}" for item in calibrate]
|
||||
|
||||
if calibrate is None or len(calibrate) == 0:
|
||||
full_range = self.get_entity_value_range()
|
||||
min_value = full_range[0]
|
||||
max_value = full_range[1]
|
||||
min_power = self._config.get(CONF_MIN_POWER) or self._standby_power or 0
|
||||
max_power = self._config.get(CONF_MAX_POWER)
|
||||
if max_power is None: # pragma: no cover
|
||||
raise StrategyConfigurationError("Linear strategy must have max power defined")
|
||||
calibration_list.append((min_value, float(min_power)))
|
||||
calibration_list.append(
|
||||
(max_value, float(self._config.get(CONF_MAX_POWER))), # type: ignore
|
||||
)
|
||||
calibration_list.append((max_value, float(max_power)))
|
||||
return calibration_list
|
||||
|
||||
for line in calibrate:
|
||||
@@ -154,23 +159,30 @@ class LinearStrategy(PowerCalculationStrategyInterface):
|
||||
|
||||
def get_entity_value_range(self) -> tuple:
|
||||
"""Get the min/max range for a given entity domain."""
|
||||
if self._value_entity.domain == light.DOMAIN: # type: ignore
|
||||
if self.get_initialized_value_entity().domain == light.DOMAIN:
|
||||
return 0, 255
|
||||
|
||||
return 0, 100
|
||||
|
||||
def get_initialized_value_entity(self) -> SourceEntity:
|
||||
"""Return the initialized value entity."""
|
||||
if self._value_entity is None: # pragma: no cover
|
||||
raise StrategyConfigurationError("Linear strategy has not been initialized")
|
||||
return self._value_entity
|
||||
|
||||
def get_current_state_value(self, entity_state: State) -> int | None:
|
||||
"""Get the current entity state, i.e. selected brightness."""
|
||||
if self._attribute:
|
||||
return self.get_value_from_attribute(entity_state)
|
||||
|
||||
if self._value_entity.entity_id is not self._source_entity.entity_id: # type: ignore
|
||||
value_entity = self.get_initialized_value_entity()
|
||||
if value_entity.entity_id is not self._source_entity.entity_id:
|
||||
# If the value entity is different from the source entity, we need to fetch the state of the value entity
|
||||
entity_state = self._hass.states.get(self._value_entity.entity_id) # type: ignore
|
||||
entity_state = self._hass.states.get(value_entity.entity_id)
|
||||
if not entity_state:
|
||||
_LOGGER.error(
|
||||
"Value entity %s not found",
|
||||
self._value_entity.entity_id, # type: ignore
|
||||
value_entity.entity_id,
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -184,7 +196,10 @@ class LinearStrategy(PowerCalculationStrategyInterface):
|
||||
return None
|
||||
|
||||
def get_value_from_attribute(self, entity_state: State) -> int | None:
|
||||
value: int | None = entity_state.attributes.get(self._attribute) # type: ignore[arg-type]
|
||||
if self._attribute is None: # pragma: no cover
|
||||
return None
|
||||
|
||||
value = entity_state.attributes.get(self._attribute)
|
||||
if value is None:
|
||||
_LOGGER.warning(
|
||||
"No %s attribute for entity: %s",
|
||||
@@ -192,13 +207,15 @@ class LinearStrategy(PowerCalculationStrategyInterface):
|
||||
entity_state.entity_id,
|
||||
)
|
||||
return None
|
||||
if self._attribute == ATTR_BRIGHTNESS and value > 255:
|
||||
value = 255
|
||||
# Convert volume level to 0-100 range
|
||||
if self._attribute == ATTR_MEDIA_VOLUME_LEVEL:
|
||||
if entity_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is True:
|
||||
value = 0
|
||||
value *= 100
|
||||
return 0
|
||||
return int(float(value) * 100)
|
||||
|
||||
value = int(value)
|
||||
if self._attribute == ATTR_BRIGHTNESS and value > 255:
|
||||
value = 255
|
||||
return value
|
||||
|
||||
def get_attribute(self, entity_state: State) -> str | None:
|
||||
@@ -214,9 +231,8 @@ class LinearStrategy(PowerCalculationStrategyInterface):
|
||||
if not self._config.get(CONF_CALIBRATE):
|
||||
if self._source_entity.domain not in ALLOWED_DOMAINS:
|
||||
raise StrategyConfigurationError(
|
||||
"Entity domain not supported for linear mode. Must be one of: {}, or use the calibrate option".format(
|
||||
",".join(ALLOWED_DOMAINS),
|
||||
),
|
||||
"Entity domain not supported for linear mode. "
|
||||
f"Must be one of: {','.join(ALLOWED_DOMAINS)}, or use the calibrate option",
|
||||
"linear_unsupported_domain",
|
||||
)
|
||||
if CONF_MAX_POWER not in self._config:
|
||||
@@ -235,7 +251,11 @@ class LinearStrategy(PowerCalculationStrategyInterface):
|
||||
|
||||
async def get_value_entity(self) -> SourceEntity:
|
||||
"""Set the value entity based on the current state."""
|
||||
if self._source_entity.domain in (vacuum.DOMAIN, lawn_mower.DOMAIN) and self._attribute is None and self._source_entity.entity_entry:
|
||||
if (
|
||||
self._source_entity.domain in (vacuum.DOMAIN, lawn_mower.DOMAIN)
|
||||
and self._attribute is None
|
||||
and self._source_entity.entity_entry
|
||||
):
|
||||
# For vacuum cleaner and lawn mower, battery level is a separate entity
|
||||
related_entity = get_related_entity_by_device_class(
|
||||
self._hass,
|
||||
@@ -247,7 +267,7 @@ class LinearStrategy(PowerCalculationStrategyInterface):
|
||||
"No battery entity found for vacuum cleaner",
|
||||
"linear_no_battery_entity",
|
||||
)
|
||||
return await create_source_entity(related_entity, self._hass)
|
||||
return create_source_entity(related_entity, self._hass)
|
||||
|
||||
return self._value_entity or self._source_entity
|
||||
|
||||
|
||||
@@ -109,7 +109,11 @@ class LutRegistry:
|
||||
supported_modes = self._supported_modes.get(cache_key)
|
||||
if supported_modes is None:
|
||||
supported_modes = set()
|
||||
for filename in await self._hass.async_add_executor_job(os.listdir, power_profile.get_model_directory()):
|
||||
filenames = cast(
|
||||
list[str],
|
||||
await self._hass.async_add_executor_job(os.listdir, power_profile.get_model_directory()),
|
||||
)
|
||||
for filename in filenames:
|
||||
if filename.endswith((".csv.gz", ".csv")):
|
||||
base_name = filename.split(".", 1)[0]
|
||||
supported_modes.add(LookupMode(base_name))
|
||||
@@ -290,7 +294,8 @@ class LutStrategy(PowerCalculationStrategyInterface):
|
||||
color_temp = attrs.get(ATTR_COLOR_TEMP_KELVIN)
|
||||
if color_temp is None:
|
||||
_LOGGER.error(
|
||||
"%s: Could not calculate power. no color temp set. Please check the attributes of your light in the developer tools.",
|
||||
"%s: Could not calculate power. no color temp set. "
|
||||
"Please check the attributes of your light in the developer tools.",
|
||||
entity_state.entity_id,
|
||||
)
|
||||
return None
|
||||
@@ -300,12 +305,17 @@ class LutStrategy(PowerCalculationStrategyInterface):
|
||||
if color_mode == ColorMode.HS:
|
||||
try:
|
||||
original_color_mode = attrs.get(ATTR_COLOR_MODE)
|
||||
hs = color_temperature_to_hs(attrs[ATTR_COLOR_TEMP_KELVIN]) if original_color_mode == ColorMode.COLOR_TEMP else attrs[ATTR_HS_COLOR]
|
||||
hs = (
|
||||
color_temperature_to_hs(attrs[ATTR_COLOR_TEMP_KELVIN])
|
||||
if original_color_mode == ColorMode.COLOR_TEMP
|
||||
else attrs[ATTR_HS_COLOR]
|
||||
)
|
||||
light_setting.hue = int(hs[0] / 360 * 65535)
|
||||
light_setting.saturation = int(hs[1] / 100 * 255)
|
||||
except Exception: # noqa: BLE001
|
||||
except (KeyError, TypeError, ValueError):
|
||||
_LOGGER.error(
|
||||
"%s: Could not calculate power. no hue/sat set. Please check the attributes of your light in the developer tools.",
|
||||
"%s: Could not calculate power. no hue/sat set. "
|
||||
"Please check the attributes of your light in the developer tools.",
|
||||
entity_state.entity_id,
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -44,7 +44,8 @@ class MultiSwitchStrategy(PowerCalculationStrategyInterface):
|
||||
async def calculate(self, entity_state: State) -> Decimal | None:
|
||||
if self.known_states is None:
|
||||
self.known_states = {
|
||||
entity_id: (state.state if (state := self.hass.states.get(entity_id)) else STATE_UNAVAILABLE) for entity_id in self.switch_entities
|
||||
entity_id: (state.state if (state := self.hass.states.get(entity_id)) else STATE_UNAVAILABLE)
|
||||
for entity_id in self.switch_entities
|
||||
}
|
||||
|
||||
if entity_state.entity_id != DUMMY_ENTITY_ID and entity_state.entity_id in self.switch_entities:
|
||||
@@ -60,7 +61,7 @@ class MultiSwitchStrategy(PowerCalculationStrategyInterface):
|
||||
return Decimal(sum(_get_power(state) for state in self.known_states.values()))
|
||||
|
||||
def get_entities_to_track(self) -> list[str | TrackTemplate]:
|
||||
return self.switch_entities # type: ignore
|
||||
return [*self.switch_entities]
|
||||
|
||||
def can_calculate_standby(self) -> bool:
|
||||
return self.off_power is not None
|
||||
|
||||
@@ -10,8 +10,8 @@ import gzip
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.const import STATE_OFF
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_OFF
|
||||
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, State, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_point_in_time
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -64,6 +64,7 @@ class PlaybookStrategy(PowerCalculationStrategyInterface):
|
||||
self._update_callback: Callable[[Decimal], None] = lambda power: None
|
||||
self._start_time: datetime = dt.utcnow()
|
||||
self._cancel_timer: CALLBACK_TYPE | None = None
|
||||
self._cancel_stop_listener: CALLBACK_TYPE | None = None
|
||||
self._config = config
|
||||
self._repeat: bool = bool(config.get(CONF_REPEAT))
|
||||
self._autostart: str | None = config.get(CONF_AUTOSTART)
|
||||
@@ -116,6 +117,9 @@ class PlaybookStrategy(PowerCalculationStrategyInterface):
|
||||
if self._cancel_timer is not None:
|
||||
self._cancel_timer()
|
||||
self._cancel_timer = None
|
||||
if self._cancel_stop_listener is not None:
|
||||
self._cancel_stop_listener()
|
||||
self._cancel_stop_listener = None
|
||||
|
||||
def get_active_playbook(self) -> Playbook | None:
|
||||
"""Get running playbook"""
|
||||
@@ -124,41 +128,79 @@ class PlaybookStrategy(PowerCalculationStrategyInterface):
|
||||
@callback
|
||||
def _execute_playbook_entry(self) -> None:
|
||||
"""Execute one step of the playbook"""
|
||||
if self._cancel_timer is not None:
|
||||
self._cancel_timer()
|
||||
self._cancel_timer = None
|
||||
self._cancel_pending_timer()
|
||||
|
||||
if not self._active_playbook: # pragma: no cover
|
||||
_LOGGER.error("Could not execute next playbook entry. No active playbook")
|
||||
return
|
||||
|
||||
queue = self._active_playbook.queue
|
||||
if len(queue) == 0:
|
||||
if self._repeat:
|
||||
_LOGGER.debug("Playbook %s repeating", self._active_playbook.key)
|
||||
self._start_time = dt.utcnow()
|
||||
queue.reset()
|
||||
self._execute_playbook_entry()
|
||||
return
|
||||
|
||||
_LOGGER.debug("Playbook %s completed", self._active_playbook.key)
|
||||
self._active_playbook = None
|
||||
playbook = self._active_playbook
|
||||
if self._complete_or_repeat_playbook(playbook):
|
||||
return
|
||||
|
||||
entry = queue.dequeue()
|
||||
entry = playbook.queue.dequeue()
|
||||
self._schedule_playbook_entry_update(playbook, entry)
|
||||
|
||||
@callback
|
||||
def _cancel_pending_timer(self) -> None:
|
||||
if self._cancel_timer is not None:
|
||||
self._cancel_timer()
|
||||
self._cancel_timer = None
|
||||
|
||||
@callback
|
||||
def _complete_or_repeat_playbook(self, playbook: Playbook) -> bool:
|
||||
queue = playbook.queue
|
||||
if len(queue) != 0:
|
||||
return False
|
||||
|
||||
if self._repeat:
|
||||
_LOGGER.debug("Playbook %s repeating", playbook.key)
|
||||
self._start_time = dt.utcnow()
|
||||
queue.reset()
|
||||
self._execute_playbook_entry()
|
||||
return True
|
||||
|
||||
_LOGGER.debug("Playbook %s completed", playbook.key)
|
||||
self._active_playbook = None
|
||||
return True
|
||||
|
||||
@callback
|
||||
def _schedule_playbook_entry_update(self, playbook: Playbook, entry: PlaybookEntry) -> None:
|
||||
"""Schedule the next playbook power update."""
|
||||
|
||||
@callback
|
||||
def _update_power(date_time: datetime) -> None:
|
||||
active_playbook = self._active_playbook
|
||||
if active_playbook is None: # pragma: no cover
|
||||
return
|
||||
self._power = entry.power
|
||||
_LOGGER.debug("playbook %s: Update power %.2f", self._active_playbook.key, self._power) # type: ignore
|
||||
_LOGGER.debug("playbook %s: Update power %.2f", active_playbook.key, self._power)
|
||||
self._update_callback(self._power)
|
||||
# Schedule next update
|
||||
self._execute_playbook_entry()
|
||||
|
||||
@callback
|
||||
def _cancel_pending_updates_on_stop(_: datetime) -> None:
|
||||
self._cancel_pending_timer()
|
||||
if self._cancel_stop_listener is not None:
|
||||
self._cancel_stop_listener()
|
||||
self._cancel_stop_listener = None
|
||||
self._active_playbook = None
|
||||
|
||||
if self._cancel_stop_listener is not None:
|
||||
self._cancel_stop_listener()
|
||||
self._cancel_stop_listener = self._hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
_cancel_pending_updates_on_stop,
|
||||
)
|
||||
|
||||
# Schedule update in the future
|
||||
self._cancel_timer = async_track_point_in_time(
|
||||
self._hass,
|
||||
_update_power,
|
||||
HassJob(
|
||||
_update_power,
|
||||
name=f"powercalc playbook {playbook.key}",
|
||||
cancel_on_shutdown=True,
|
||||
),
|
||||
self._start_time + timedelta(seconds=entry.time),
|
||||
)
|
||||
|
||||
@@ -167,20 +209,20 @@ class PlaybookStrategy(PowerCalculationStrategyInterface):
|
||||
if playbook_id in self._loaded_playbooks:
|
||||
return self._loaded_playbooks[playbook_id]
|
||||
|
||||
playbooks: dict[str, str] = self._config.get(CONF_PLAYBOOKS) # type: ignore
|
||||
playbooks: dict[str, str] = dict(self._config.get(CONF_PLAYBOOKS) or {})
|
||||
if playbook_id not in playbooks:
|
||||
raise StrategyConfigurationError(
|
||||
f"Playbook with id {playbook_id} not defined in playbooks config",
|
||||
)
|
||||
|
||||
file_path = os.path.join(self._playbook_directory, playbooks[playbook_id])
|
||||
if not (os.path.exists(file_path) or os.path.exists(f"{file_path}.gz")):
|
||||
raise StrategyConfigurationError(
|
||||
f"Playbook file '{file_path}' does not exist",
|
||||
)
|
||||
|
||||
def _load_playbook_entries() -> list[PlaybookEntry]:
|
||||
"""Load playbook entries from a CSV file, with support for gzipped files"""
|
||||
if not (os.path.exists(file_path) or os.path.exists(f"{file_path}.gz")):
|
||||
raise StrategyConfigurationError(
|
||||
f"Playbook file '{file_path}' does not exist",
|
||||
)
|
||||
actual_path = file_path if os.path.exists(file_path) else f"{file_path}.gz"
|
||||
open_func = gzip.open if actual_path.endswith(".gz") else open
|
||||
with open_func(actual_path, mode="rt") as csv_file:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from decimal import Decimal
|
||||
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
@@ -28,5 +29,8 @@ class PowerCalculationStrategyInterface:
|
||||
"""Return if this strategy is enabled based on entity state."""
|
||||
return True
|
||||
|
||||
def set_update_callback(self, update_callback: Callable[[Decimal], None]) -> None:
|
||||
"""Register update callback to allow strategy to push power updates."""
|
||||
|
||||
async def on_start(self, hass: HomeAssistant) -> None:
|
||||
"""Called after HA has started"""
|
||||
|
||||
@@ -48,7 +48,11 @@ class WledStrategy(PowerCalculationStrategyInterface):
|
||||
self._estimated_current_entity: str | None = None
|
||||
|
||||
async def calculate(self, entity_state: State) -> Decimal | None:
|
||||
light_state = entity_state if entity_state.entity_id == self._light_entity.entity_id else self._hass.states.get(self._light_entity.entity_id)
|
||||
light_state = (
|
||||
entity_state
|
||||
if entity_state.entity_id == self._light_entity.entity_id
|
||||
else self._hass.states.get(self._light_entity.entity_id)
|
||||
)
|
||||
|
||||
if light_state.state in OFF_STATES and self._standby_power:
|
||||
return self._standby_power
|
||||
@@ -72,7 +76,7 @@ class WledStrategy(PowerCalculationStrategyInterface):
|
||||
self._power_factor,
|
||||
)
|
||||
power = float(entity_state.state) / 1000 * self._voltage * self._power_factor
|
||||
return await evaluate_power(power)
|
||||
return evaluate_power(power)
|
||||
|
||||
async def find_estimated_current_entity(self) -> str:
|
||||
entity_reg = entity_registry.async_get(self._hass)
|
||||
@@ -86,7 +90,9 @@ class WledStrategy(PowerCalculationStrategyInterface):
|
||||
if entity:
|
||||
return entity
|
||||
|
||||
raise StrategyConfigurationError("No estimated current entity found. Probably brightness limiter not enabled. See documentation")
|
||||
raise StrategyConfigurationError(
|
||||
"No estimated current entity found. Probably brightness limiter not enabled. See documentation",
|
||||
)
|
||||
|
||||
def get_entities_to_track(self) -> list[str | TrackTemplate]:
|
||||
if self._estimated_current_entity:
|
||||
|
||||
Reference in New Issue
Block a user