Home Assistant Git Exporter

This commit is contained in:
root
2024-08-09 06:45:02 +02:00
parent 60abdd866c
commit 80fc630f5e
624 changed files with 27739 additions and 4497 deletions

View File

@@ -0,0 +1,479 @@
""" A ManagedDevice represent a device than can be managed by the optimisatiion algorithm"""
import logging
from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
from .const import (
get_tz,
name_to_unique_id,
CONF_ACTION_MODE_SERVICE,
CONF_ACTION_MODE_EVENT,
CONF_ACTION_MODES,
ConfigurationError,
EVENT_TYPE_SOLAR_OPTIMIZER_CHANGE_POWER,
EVENT_TYPE_SOLAR_OPTIMIZER_STATE_CHANGE,
EVENT_TYPE_SOLAR_OPTIMIZER_ENABLE_STATE_CHANGE,
)
ACTION_ACTIVATE = "Activate"
ACTION_DEACTIVATE = "Deactivate"
ACTION_CHANGE_POWER = "ChangePower"
_LOGGER = logging.getLogger(__name__)
async def do_service_action(
hass: HomeAssistant,
entity_id,
action_type,
service_name,
current_power,
requested_power,
convert_power_divide_factor,
):
"""Activate an entity via a service call"""
_LOGGER.info("Calling service %s for entity %s", service_name, entity_id)
parties = service_name.split("/")
if len(parties) != 2:
raise ConfigurationError(
f"Incorrect service declaration for entity {entity_id}. Service {service_name} should be formatted with: 'domain/service'"
)
if action_type == ACTION_CHANGE_POWER:
value = round(requested_power / convert_power_divide_factor)
service_data = {"value": value}
else:
service_data = {}
target = {
"entity_id": entity_id,
}
await hass.services.async_call(
parties[0], parties[1], service_data=service_data, target=target
)
# Also send an event to inform
do_event_action(
hass,
entity_id,
action_type,
current_power,
requested_power,
EVENT_TYPE_SOLAR_OPTIMIZER_STATE_CHANGE,
)
def do_event_action(
hass: HomeAssistant,
entity_id,
action_type,
current_power,
requested_power,
event_type: str,
):
"""Activate an entity via an event"""
_LOGGER.info(
"Sending event %s with action %s for entity %s with requested_power %s and current_power %s",
event_type,
action_type,
entity_id,
requested_power,
current_power,
)
hass.bus.fire(
event_type=event_type,
event_data={
"action_type": action_type,
"requested_power": requested_power,
"current_power": current_power,
"entity_id": entity_id,
},
)
class ManagedDevice:
"""A Managed device representation"""
_name: str
_unique_id: str
_entity_id: str
_power_entity_id: str
_power_max: int
_power_min: int
_power_step: int
_can_change_power: bool
_current_power: int
_requested_power: int
_duration_sec: int
_duration_stop_sec: int
_duration_power_sec: int
_check_usable_template: Template
_check_active_template: Template
_next_date_available: datetime
_next_date_available_power: datetime
_action_mode: str
_activation_service: str
_deactivation_service: str
_change_power_service: str
_convert_power_divide_factor: int
_battery_soc: float
_battery_soc_threshold: float
def __init__(self, hass: HomeAssistant, device_config):
"""Initialize a manageable device"""
self._hass = hass
self._name = device_config.get("name")
self._unique_id = name_to_unique_id(self._name)
self._entity_id = device_config.get("entity_id")
self._power_entity_id = device_config.get("power_entity_id")
self._power_max = int(device_config.get("power_max"))
self._power_min = int(device_config.get("power_min") or -1)
self._power_step = int(device_config.get("power_step") or 0)
self._can_change_power = self._power_min >= 0
self._convert_power_divide_factor = int(
device_config.get("convert_power_divide_factor") or 1
)
self._current_power = self._requested_power = 0
duration_min = float(device_config.get("duration_min"))
self._duration_sec = round(duration_min * 60)
self._duration_power_sec = round(
float(device_config.get("duration_power_min") or duration_min) * 60
)
self._duration_stop_sec = round(
float(device_config.get("duration_stop_min") or duration_min) * 60
)
if device_config.get("check_usable_template"):
self._check_usable_template = Template(
device_config.get("check_usable_template"), hass
)
else:
# If no template for usability, the device is supposed to be always usable
self._check_usable_template = Template("{{ True }}", hass)
if device_config.get("check_active_template"):
self._check_active_template = Template(
device_config.get("check_active_template"), hass
)
else:
template_string = (
"{{ is_state('" + self._entity_id + "', '" + STATE_ON + "') }}"
)
self._check_active_template = Template(template_string, hass)
self._next_date_available_power = self._next_date_available = datetime.now(
get_tz(hass)
)
self._action_mode = device_config.get("action_mode")
self._activation_service = device_config.get("activation_service")
self._deactivation_service = device_config.get("deactivation_service")
self._change_power_service = device_config.get("change_power_service")
self._battery_soc = None
self._battery_soc_threshold = float(device_config.get("battery_soc_threshold") or 0)
if self.is_active:
self._requested_power = self._current_power = (
self._power_max if self._can_change_power else self._power_min
)
self._enable = True
async def _apply_action(self, action_type: str, requested_power=None):
"""Apply an action to a managed device.
This method is a generical method for activate, deactivate, change_requested_power
"""
_LOGGER.debug(
"Applying action %s for entity %s. requested_power=%s",
action_type,
self._entity_id,
requested_power,
)
if requested_power is not None:
self._requested_power = requested_power
if self._action_mode == CONF_ACTION_MODE_SERVICE:
method = None
entity_id = self._entity_id
if action_type == ACTION_ACTIVATE:
method = self._activation_service
self.reset_next_date_available(action_type)
if self._can_change_power:
self.reset_next_date_available_power()
elif action_type == ACTION_DEACTIVATE:
method = self._deactivation_service
self.reset_next_date_available(action_type)
elif action_type == ACTION_CHANGE_POWER:
assert (
self._can_change_power
), f"Equipment {self._name} cannot change its power. We should not be there."
method = self._change_power_service
entity_id = self._power_entity_id
self.reset_next_date_available_power()
await do_service_action(
self._hass,
entity_id,
action_type,
method,
self._current_power,
self._requested_power,
self._convert_power_divide_factor,
)
elif self._action_mode == CONF_ACTION_MODE_EVENT:
do_event_action(
self._hass,
self._entity_id,
action_type,
self._current_power,
self._requested_power,
EVENT_TYPE_SOLAR_OPTIMIZER_CHANGE_POWER,
)
else:
raise ConfigurationError(
f"Incorrect action_mode declaration for entity '{self._entity_id}'. Action_mode '{self._action_mode}' is not supported. Use one of {CONF_ACTION_MODES}"
)
self._current_power = self._requested_power
async def activate(self, requested_power=None):
"""Use this method to activate this ManagedDevice"""
return await self._apply_action(ACTION_ACTIVATE, requested_power)
async def deactivate(self):
"""Use this method to deactivate this ManagedDevice"""
return await self._apply_action(ACTION_DEACTIVATE, 0)
async def change_requested_power(self, requested_power):
"""Use this method to change the requested power of this ManagedDevice"""
return await self._apply_action(ACTION_CHANGE_POWER, requested_power)
def reset_next_date_available(self, action_type):
"""Incremente the next availability date to now + _duration_sec"""
if action_type == ACTION_ACTIVATE:
self._next_date_available = datetime.now(get_tz(self._hass)) + timedelta(
seconds=self._duration_sec
)
else:
self._next_date_available = datetime.now(get_tz(self._hass)) + timedelta(
seconds=self._duration_stop_sec
)
_LOGGER.debug(
"Next availability date for %s is %s", self._name, self._next_date_available
)
def reset_next_date_available_power(self):
"""Incremente the next availability date for power change to now + _duration_power_sec"""
self._next_date_available_power = datetime.now(get_tz(self._hass)) + timedelta(
seconds=self._duration_power_sec
)
_LOGGER.debug(
"Next availability date for power change for %s is %s",
self._name,
self._next_date_available_power,
)
# def init_power(self, power: int):
# """Initialise current_power and requested_power to the given value"""
# _LOGGER.debug(
# "Initializing power for entity '%s' with %s value", self._name, power
# )
# self._requested_power = self._current_power = power
def set_current_power_with_device_state(self):
"""Set the current power according to the real device state"""
if not self.is_active:
self._current_power = 0
_LOGGER.debug(
"Set current_power to 0 for device %s cause not active", self._name
)
return
if not self._can_change_power:
self._current_power = self._power_max
_LOGGER.debug(
"Set current_power to %s for device %s cause active and not can_change_power",
self._current_power,
self._name,
)
return
amps = self._hass.states.get(self._power_entity_id)
if not amps or amps.state in [None, STATE_UNKNOWN, STATE_UNAVAILABLE]:
self._current_power = self._power_min
_LOGGER.debug(
"Set current_power to %s for device %s cause can_change_power but amps is %s",
self._current_power,
self._name,
amps,
)
return
self._current_power = round(
float(amps.state) * self._convert_power_divide_factor
)
_LOGGER.debug(
"Set current_power to %s for device %s cause can_change_power and amps is %s",
self._current_power,
self._name,
amps.state,
)
def set_enable(self, enable: bool):
"""Enable or disable the ManagedDevice for Solar Optimizer"""
_LOGGER.info("%s - Set enable=%s", self.name, enable)
self._enable = enable
self.publish_enable_state_change()
@property
def is_enabled(self) -> bool:
"""return true if the managed device is enabled for solar optimisation"""
return self._enable
@property
def is_active(self) -> bool:
"""Check if device is active by getting the underlying state of the device"""
result = self._check_active_template.async_render(context={})
if result:
_LOGGER.debug("%s is active", self._name)
return result
@property
def is_usable(self) -> bool:
"""A device is usable for optimisation if the check_usable_template returns true and
if the device is not waiting for the end of its cycle and if the battery_soc_threshold is >= battery_soc"""
context = {}
now = datetime.now(get_tz(self._hass))
result = self._check_usable_template.async_render(context) and (
now >= self._next_date_available
or (self._can_change_power and now >= self._next_date_available_power)
)
if not result:
_LOGGER.debug("%s is not usable", self._name)
if result and self._battery_soc is not None and self._battery_soc_threshold is not None:
if self._battery_soc < self._battery_soc_threshold:
result = False
_LOGGER.debug("%s is not usable due to battery soc threshold (%s < %s)", self._name, self._battery_soc, self._battery_soc_threshold)
return result
@property
def is_waiting(self):
"""A device is waiting if the device is waiting for the end of its cycle"""
now = datetime.now(get_tz(self._hass))
result = now < self._next_date_available
if result:
_LOGGER.debug("%s is waiting", self._name)
return result
@property
def name(self):
"""The name of the ManagedDevice"""
return self._name
@property
def unique_id(self):
"""The id of the ManagedDevice"""
return self._unique_id
@property
def power_max(self):
"""The power max of the managed device"""
return self._power_max
@property
def power_min(self):
"""The power min of the managed device"""
return self._power_min
@property
def power_step(self):
"""The power step of the managed device"""
return self._power_step
@property
def duration_sec(self) -> int:
"""The duration a device is not available after a change of the managed device"""
return self._duration_sec
@property
def duration_stop_sec(self) -> int:
"""The duration a device is not available after a change of the managed device to stop"""
return self._duration_stop_sec
@property
def duration_power_sec(self) -> int:
"""The duration a device is not available after a change of the managed device for power change"""
return self._duration_power_sec
@property
def entity_id(self) -> str:
"""The entity_id of the device"""
return self._entity_id
@property
def power_entity_id(self) -> str:
"""The entity_id of the device which gives the current power"""
return self._power_entity_id
@property
def current_power(self) -> int:
"""The current_power of the device"""
return self._current_power
@property
def requested_power(self) -> int:
"""The requested_power of the device"""
return self._requested_power
@property
def can_change_power(self) -> bool:
"""true is the device can change its power"""
return self._can_change_power
@property
def next_date_available(self) -> datetime:
"""returns the next available date for state change"""
return self._next_date_available
@property
def next_date_available_power(self) -> datetime:
"""return the next available date for power change"""
return self._next_date_available_power
@property
def convert_power_divide_factor(self) -> int:
"""return"""
return self._convert_power_divide_factor
def set_battery_soc(self, battery_soc):
"""Define the battery soc. This is used with is_usable
to determine if the device is usable"""
self._battery_soc = battery_soc
def publish_enable_state_change(self) -> None:
"""Publish an event when the state is changed"""
self._hass.bus.fire(
event_type=EVENT_TYPE_SOLAR_OPTIMIZER_ENABLE_STATE_CHANGE,
event_data={
"device_unique_id": self._unique_id,
"is_enabled": self.is_enabled,
"is_active": self.is_active,
"is_usable": self.is_usable,
"is_waiting": self.is_waiting,
},
)