-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20 from dala318/tests
Add some basic but relevant tests
- Loading branch information
Showing
5 changed files
with
350 additions
and
48 deletions.
There are no files selected for viewing
144 changes: 144 additions & 0 deletions
144
custom_components/ev_load_balancing/chargers/virtual.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
"""Handling Virtual Charger.""" | ||
|
||
import logging | ||
|
||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.event import async_track_state_change_event | ||
from homeassistant.helpers.template import device_entities | ||
|
||
from ..const import Phases | ||
from ..helpers.entity_value import get_sensor_entity_attribute_value | ||
from . import Charger, ChargerPhase, ChargingState | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class ChargerPhaseVirtual(ChargerPhase): | ||
"""A data class for a charger phase.""" | ||
|
||
def __init__(self, hass: HomeAssistant, entity_id: str, attribute: str) -> None: | ||
"""Initialize object.""" | ||
self._hass = hass | ||
self._entity = entity_id | ||
self._attribute = attribute | ||
self._value = None | ||
|
||
def set_expected(self, value: float) -> None: | ||
"""Fake update mathod.""" | ||
self._value = value | ||
|
||
def update(self) -> None: | ||
"""Do nothing, but exists to conform to standard.""" | ||
|
||
def current_limit(self) -> float: | ||
"""Get set current limit on phase.""" | ||
return self._value | ||
|
||
|
||
class ChargerVirtual(Charger): | ||
"""Slimmelezer mains extractor.""" | ||
|
||
_state_change_listeners = [] | ||
|
||
def __init__( | ||
self, hass: HomeAssistant, update_callback, device_id: str, ttl: int | ||
) -> None: | ||
"""Initilalize Slimmelezer extractor.""" | ||
super().__init__(hass, update_callback) | ||
self._id = device_id | ||
self._ttl = ttl | ||
|
||
# entities = device_entities(hass, device_id) | ||
|
||
# self._ent_status = [e for e in entities if e.endswith("_status")][0] | ||
# self._state_change_listeners.append( | ||
# async_track_state_change_event( | ||
# self._hass, | ||
# [self._ent_status], | ||
# self._async_input_changed, | ||
# ) | ||
# ) | ||
|
||
# self._ent_circuit_limit = [ | ||
# e for e in entities if e.endswith("_dynamic_circuit_limit") | ||
# ][0] | ||
# self._state_change_listeners.append( | ||
# async_track_state_change_event( | ||
# self._hass, | ||
# [self._ent_circuit_limit], | ||
# self._async_input_changed, | ||
# ) | ||
# ) | ||
|
||
# self._phase1 = ChargerPhaseVirtual( | ||
# self._hass, self._ent_circuit_limit, "state_dynamicCircuitCurrentP1" | ||
# ) | ||
# self._phase2 = ChargerPhaseVirtual( | ||
# self._hass, self._ent_circuit_limit, "state_dynamicCircuitCurrentP2" | ||
# ) | ||
# self._phase3 = ChargerPhaseVirtual( | ||
# self._hass, self._ent_circuit_limit, "state_dynamicCircuitCurrentP3" | ||
# ) | ||
|
||
def set_expected(self, current_limits, set_limits) -> None: | ||
"""Set expected values.""" | ||
|
||
async def async_set_limits( | ||
self, phase1: float, phase2: float, phase3: float | ||
) -> bool: | ||
"""Set charger limits.""" | ||
_LOGGER.debug( | ||
"Setting limits: phase 1 %f, phase 2 %f, phase 3 %f", phase1, phase2, phase3 | ||
) | ||
domain = "easee" | ||
service = "set_circuit_dynamic_limit" | ||
service_data = { | ||
"device_id": self._id, | ||
"current_p1": phase1, | ||
"current_p2": phase2, | ||
"current_p3": phase3, | ||
"time_to_live": self._ttl, | ||
} | ||
# await self._hass.services.async_call(domain, service, service_data) | ||
|
||
return True | ||
|
||
def update(self) -> None: | ||
"""Update measuremetns.""" | ||
self._phase1.update() | ||
self._phase2.update() | ||
self._phase3.update() | ||
|
||
def cleanup(self): | ||
"""Cleanup by removing event listeners.""" | ||
# for listner in self._state_change_listeners: | ||
# listner() | ||
|
||
@property | ||
def charging_state(self) -> ChargingState: | ||
"""Return if charging state.""" | ||
if self._hass.states.get(self._ent_status).state in ["charging"]: | ||
return ChargingState.CHARGING | ||
if self._hass.states.get(self._ent_status).state in ["awaiting_start"]: | ||
return ChargingState.PENDING | ||
return ChargingState.OFF | ||
|
||
def get_phase(self, phase: Phases) -> ChargerPhase: | ||
"""Return phase X data.""" | ||
if phase == Phases.PHASE1: | ||
return self._phase1 | ||
if phase == Phases.PHASE2: | ||
return self._phase2 | ||
if phase == Phases.PHASE3: | ||
return self._phase3 | ||
return None | ||
|
||
def get_rated_limit(self) -> int: | ||
"""Return overall limit per phase on charger circuit.""" | ||
limit = get_sensor_entity_attribute_value( | ||
self._hass, _LOGGER, self._ent_circuit_limit, "circuit_ratedCurrent" | ||
) | ||
if limit is not None: | ||
limit = int(limit) | ||
_LOGGER.debug("Returning rated limit %d for charger circuit", limit) | ||
return limit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
"""Handling Virtual mains currents input.""" | ||
|
||
from datetime import UTC, datetime, timedelta | ||
import logging | ||
import statistics | ||
|
||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.event import async_track_state_change_event | ||
from homeassistant.helpers.template import device_entities | ||
|
||
from ..const import Phases | ||
from ..helpers.entity_value import get_sensor_entity_value | ||
from . import Mains, MainsPhase | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class MainsPhaseVirtual(MainsPhase): | ||
"""A data class for a mains phase.""" | ||
|
||
_stddev_min_num = 10 | ||
_stddev_max_age = timedelta(minutes=2) | ||
|
||
def __init__(self, hass: HomeAssistant, entity_id: str) -> None: | ||
"""Initialize object.""" | ||
self._hass = hass | ||
self._entity = entity_id | ||
self._value = None | ||
self._history_values = {} | ||
|
||
def set_expected(self): | ||
"""Fake update mathod.""" | ||
pass | ||
|
||
def update(self) -> None: | ||
"""Update measuremetns.""" | ||
now = datetime.now(UTC) | ||
self._value = get_sensor_entity_value( | ||
self._hass, | ||
_LOGGER, | ||
self._entity, | ||
) | ||
|
||
if self._value is None: | ||
_LOGGER.debug("Skipping history since None value") | ||
return | ||
|
||
self._history_values[now] = self._value | ||
|
||
# Find and drop old values if enough in dict | ||
drop_keys = [] | ||
keep_count = 0 | ||
for k in sorted(self._history_values.keys(), reverse=True): | ||
if keep_count < self._stddev_min_num or k > now - self._stddev_max_age: | ||
keep_count += 1 | ||
else: | ||
drop_keys.append(k) | ||
for k in drop_keys: | ||
self._history_values.pop(k) | ||
_LOGGER.debug("Dropping measurement with key %s", k) | ||
|
||
def actual_current(self) -> float: | ||
"""Get actual current on phase.""" | ||
return self._value | ||
|
||
def stddev_current(self) -> float: | ||
"""Get standard deviation of current on phase.""" | ||
if len(self._history_values) > self._stddev_min_num / 2: | ||
return statistics.pstdev(self._history_values.values()) | ||
_LOGGER.debug( | ||
"Not enough values for stddev (%d), returning 0", len(self._history_values) | ||
) | ||
return 0 | ||
|
||
|
||
class MainsVirtual(Mains): | ||
"""Virtual mains extractor.""" | ||
|
||
_state_change_listeners = [] | ||
|
||
def __init__( | ||
self, hass: HomeAssistant, update_callback, device_id: str, mains_limit: int | ||
) -> None: | ||
"""Initilalize Virtual extractor.""" | ||
super().__init__(hass, update_callback) | ||
self._id = device_id | ||
self._mains_limit = mains_limit | ||
|
||
entities = device_entities(hass, device_id) | ||
used_entities = [] | ||
|
||
entity_phase1 = [e for e in entities if "_current" in e and e.endswith("1")][0] | ||
self._phase1 = MainsPhaseVirtual(self._hass, entity_phase1) | ||
used_entities.append(entity_phase1) | ||
|
||
entity_phase2 = [e for e in entities if "_current" in e and e.endswith("2")][0] | ||
self._phase2 = MainsPhaseVirtual(self._hass, entity_phase2) | ||
used_entities.append(entity_phase2) | ||
|
||
entity_phase3 = [e for e in entities if "_current" in e and e.endswith("3")][0] | ||
self._phase3 = MainsPhaseVirtual(self._hass, entity_phase3) | ||
used_entities.append(entity_phase3) | ||
|
||
self._state_change_listeners.append( | ||
async_track_state_change_event( | ||
self._hass, | ||
used_entities, | ||
self._async_input_changed, | ||
) | ||
) | ||
|
||
def get_phase(self, phase: Phases) -> MainsPhase: | ||
"""Return phase X data.""" | ||
if phase == Phases.PHASE1: | ||
return self._phase1 | ||
if phase == Phases.PHASE2: | ||
return self._phase2 | ||
if phase == Phases.PHASE3: | ||
return self._phase3 | ||
return None | ||
|
||
def get_rated_limit(self) -> int: | ||
"""Return main limit per phase.""" | ||
return self._mains_limit | ||
|
||
def update(self) -> None: | ||
"""Update measuremetns.""" | ||
self._phase1.update() | ||
self._phase2.update() | ||
self._phase3.update() | ||
|
||
def cleanup(self): | ||
"""Cleanup by removing event listeners.""" | ||
# for listner in self._state_change_listeners: | ||
# listner() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.