Skip to content

Commit

Permalink
Fixed reschedule issue.
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasbkarlsson authored Feb 25, 2023
1 parent 390e8d4 commit 5917908
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 182 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pull.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
run: |
pytest \
-qq \
--timeout=9 \
--timeout=45 \
--durations=10 \
-n auto \
--cov-report=xml \
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
run: |
pytest \
-qq \
--timeout=9 \
--timeout=45 \
--durations=10 \
-n auto \
--cov-report=xml \
Expand Down
2 changes: 0 additions & 2 deletions custom_components/ev_smart_charging/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,6 @@
CHARGING_STATUS_DISCONNECTED = "Disconnected"
CHARGING_STATUS_NOT_ACTIVE = "Smart charging not active"

DEBOUNCE_TIME = 1.0

# Defaults
DEFAULT_NAME = DOMAIN
DEFAULT_TARGET_SOC = 100
Expand Down
38 changes: 32 additions & 6 deletions custom_components/ev_smart_charging/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
CONF_EV_SOC_SENSOR,
CONF_EV_TARGET_SOC_SENSOR,
CONF_START_HOUR,
DEBOUNCE_TIME,
DEFAULT_TARGET_SOC,
READY_HOUR_NONE,
START_HOUR_NONE,
Expand All @@ -49,7 +48,7 @@
get_ready_hour_utc,
get_start_hour_utc,
)
from .helpers.general import Validator, debounce_async, get_parameter
from .helpers.general import Validator, get_parameter
from .sensor import EVSmartChargingSensor

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -255,12 +254,11 @@ async def update_state(
self._charging_schedule = Scheduler.get_empty_schedule()
self.sensor.charging_schedule = self._charging_schedule

@debounce_async(DEBOUNCE_TIME)
async def turn_on_charging(self, state: bool = True):
"""Turn on charging"""

if state is True:
_LOGGER.info("Turn on charging")
_LOGGER.debug("Turn on charging")
self.sensor.native_value = STATE_ON
if self.charger_switch is not None:
_LOGGER.debug(
Expand All @@ -272,7 +270,7 @@ async def turn_on_charging(self, state: bool = True):
target={"entity_id": self.charger_switch},
)
else:
_LOGGER.info("Turn off charging")
_LOGGER.debug("Turn off charging")
self.sensor.native_value = STATE_OFF
if self.charger_switch is not None:
_LOGGER.debug(
Expand Down Expand Up @@ -558,6 +556,7 @@ async def update_sensors(
"max_price": max_price,
}

time_now_local = dt.now()
time_now_hour_local = dt.now().hour

# To handle non-live SOC
Expand All @@ -569,14 +568,41 @@ async def update_sensors(
else:
self.ready_hour_first = True

not_charging = True
if self._charging_schedule is not None:
not_charging = (
get_charging_value(self._charging_schedule) is None
or get_charging_value(self._charging_schedule) == 0
)
# Handle self.switch_keep_on
if self.switch_keep_on:
# Only if price limit is not used and the EV is connected
if (
self.switch_apply_limit is False or self.max_price == 0.0
) and self.switch_ev_connected:
# Only if SOC has reached Target SOC or there are no more scheduled charging
if (
self.ev_soc is not None
and self.ev_target_soc is not None
and self.ev_soc >= self.ev_target_soc
):
# Don't reschedule due to keep_on
not_charging = False

if self.switch_keep_on_completion_time is not None and (
time_now_local >= self.switch_keep_on_completion_time
):
# Don't reschedule due to keep_on
not_charging = False

if (
(self.ev_soc is not None and self.ev_target_soc is not None)
and (self.ev_soc > self.ev_soc_before_last_charging)
and (
(self.ev_soc >= self.ev_target_soc)
or (
(self.tomorrow_valid or time_now_hour_local < self.ready_hour_local)
and self.auto_charging_state == STATE_OFF
and not_charging
)
)
):
Expand Down
44 changes: 0 additions & 44 deletions custom_components/ev_smart_charging/helpers/general.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"""General helpers"""

# pylint: disable=relative-beyond-top-level
import asyncio
import logging
import threading
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import State
Expand Down Expand Up @@ -75,45 +73,3 @@ def get_parameter(config_entry: ConfigEntry, parameter: str, default_val: Any =
if parameter in config_entry.data.keys():
return config_entry.data.get(parameter)
return default_val


def get_wait_time(time):
"""Function to be patched during tests"""
return time


# pylint: disable=unused-argument
def debounce_async(wait_time):
"""
Decorator that will debounce an async function so that it is called
after wait_time seconds. If it is called multiple times, will wait
for the last call to be debounced and run only this one.
"""

def decorator(function):
async def debounced(*args, **kwargs):
nonlocal wait_time

def call_function():
_LOGGER.info("debounced.call_function()")
debounced.timer = None
return asyncio.run_coroutine_threadsafe(
function(*args, **kwargs), loop=debounced.loop
)

# Used for patching during testing to disable debounce
wait_time = get_wait_time(wait_time)
_LOGGER.info("debounced.wait_time= %s", wait_time)
if wait_time == 0.0:
return await function(*args, **kwargs)

debounced.loop = asyncio.get_running_loop()
if debounced.timer is not None:
debounced.timer.cancel()
debounced.timer = threading.Timer(wait_time, call_function)
debounced.timer.start()

debounced.timer = None
return debounced

return decorator
24 changes: 0 additions & 24 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from unittest.mock import patch
import pytest
from homeassistant.util import dt as dt_util
from custom_components.ev_smart_charging.const import DEBOUNCE_TIME


# pylint: disable=invalid-name
Expand Down Expand Up @@ -99,26 +98,3 @@ def skip_update_hourly_fixture():
"custom_components.ev_smart_charging.coordinator.EVSmartChargingCoordinator.update_hourly"
):
yield


def pytest_configure(config):
"""Register a new marker"""
config.addinivalue_line("markers", "ensure_debounce")


# This fixture is used to skip debounce.
# Decorate test case with @pytest.mark.ensure_debounce if debounce should be done.
@pytest.fixture(autouse=True)
def skip_debounce_fixture(request):
"""Skip debounce"""

debounce_time = 0.0

if "ensure_debounce" in request.keywords:
debounce_time = DEBOUNCE_TIME

with patch(
"custom_components.ev_smart_charging.helpers.general.get_wait_time",
return_value=debounce_time,
):
yield
13 changes: 10 additions & 3 deletions tests/coordinator/test_coordinator_opportunistic2.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,16 @@ async def test_coordinator_opportunistic_1(
assert coordinator.sensor.charging_number_of_hours == 8

# Move time to next day
freezer.move_to("2022-10-01T02:00:00+02:00")
freezer.move_to("2022-10-01T00:30:00+02:00")
MockPriceEntity.set_state(hass, PRICE_20221001A, None)
MockSOCEntity.set_state(hass, "72")
await coordinator.update_sensors()
await hass.async_block_till_done()

# Ready_hour = 08:00, Max price 200. Opportunistic level 50%
# 3 hours => 02:00-05:00
assert coordinator.auto_charging_state == STATE_ON
assert coordinator.sensor.state == STATE_ON
assert coordinator.auto_charging_state == STATE_OFF
assert coordinator.sensor.state == STATE_OFF
assert coordinator.sensor.charging_is_planned is True
assert coordinator.sensor.charging_start_time == datetime(
2022, 10, 1, 2, 0, tzinfo=dt_util.get_time_zone("Europe/Stockholm")
Expand All @@ -130,3 +131,9 @@ async def test_coordinator_opportunistic_1(
2022, 10, 1, 5, 0, tzinfo=dt_util.get_time_zone("Europe/Stockholm")
)
assert coordinator.sensor.charging_number_of_hours == 3

freezer.move_to("2022-10-01T02:00:00+02:00")
await coordinator.update_sensors()
await hass.async_block_till_done()
assert coordinator.auto_charging_state == STATE_ON
assert coordinator.sensor.state == STATE_ON
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""Test ev_smart_charging coordinator."""

from datetime import datetime
import asyncio
import pytest
from pytest_homeassistant_custom_component.common import MockConfigEntry
from freezegun import freeze_time

from homeassistant.core import HomeAssistant
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.helpers.entity_registry import async_get as async_entity_registry_get
from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.util import dt as dt_util

from custom_components.ev_smart_charging.coordinator import (
EVSmartChargingCoordinator,
Expand All @@ -25,65 +25,72 @@
MockSOCEntity,
MockTargetSOCEntity,
)
from tests.price import PRICE_20220930
from tests.price import PRICE_20220930, PRICE_20221001

# pylint: disable=unused-argument
@pytest.mark.ensure_debounce
@freeze_time("2022-09-30T02:00:00+02:00", tick=True)
async def test_coordinator_debounce(
@freeze_time("2022-09-30T05:59:50+02:00", tick=True)
async def test_coordinator_reschedule(
hass: HomeAssistant,
skip_service_calls,
set_cet_timezone,
freezer,
skip_update_hourly,
):
"""Test Coordinator debounce."""
"""Test Coordinator reschedule."""

entity_registry: EntityRegistry = async_entity_registry_get(hass)
MockSOCEntity.create(hass, entity_registry, "67")
MockSOCEntity.create(hass, entity_registry, "75")
MockTargetSOCEntity.create(hass, entity_registry, "80")
MockPriceEntity.create(hass, entity_registry, 123)
MockChargerEntity.create(hass, entity_registry, STATE_OFF)
MockPriceEntity.set_state(hass, PRICE_20220930, None)
MockPriceEntity.set_state(hass, PRICE_20220930, PRICE_20221001)
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_ALL, entry_id="test")
coordinator = EVSmartChargingCoordinator(hass, config_entry)
assert coordinator is not None
sensor: EVSmartChargingSensor = EVSmartChargingSensor(config_entry)
assert sensor is not None
await coordinator.add_sensor(sensor)
coordinator.ready_hour_local = 8
await hass.async_block_till_done()
await coordinator.switch_active_update(False)
await coordinator.switch_active_update(True)
await coordinator.switch_apply_limit_update(False)
await coordinator.switch_continuous_update(True)
await coordinator.switch_ev_connected_update(False)
await coordinator.switch_ev_connected_update(True)
await coordinator.switch_keep_on_update(False)
await hass.async_block_till_done()
assert coordinator.sensor.state == STATE_OFF

await coordinator.turn_on_charging()
await asyncio.sleep(2)
await hass.async_block_till_done()
# Schedule 05-06
await coordinator.update_configuration()
await asyncio.sleep(5)
assert coordinator.sensor.state == STATE_ON
assert coordinator.sensor.charging_is_planned is True
assert coordinator.sensor.charging_start_time == datetime(
2022, 9, 30, 5, 0, tzinfo=dt_util.get_time_zone("Europe/Stockholm")
)
assert coordinator.sensor.charging_stop_time == datetime(
2022, 9, 30, 6, 0, tzinfo=dt_util.get_time_zone("Europe/Stockholm")
)
assert coordinator.sensor.charging_number_of_hours == 1

await coordinator.turn_off_charging()
await asyncio.sleep(0.1)
await hass.async_block_till_done()
assert coordinator.sensor.state == STATE_ON
await coordinator.turn_on_charging()
await asyncio.sleep(2)
await hass.async_block_till_done()
# Schedule 06-07
await asyncio.sleep(5)
await coordinator.update_sensors()
await asyncio.sleep(5)
assert coordinator.sensor.state == STATE_ON
assert coordinator.sensor.charging_is_planned is True
assert coordinator.sensor.charging_start_time == datetime(
2022, 9, 30, 6, 0, tzinfo=dt_util.get_time_zone("Europe/Stockholm")
)
assert coordinator.sensor.charging_stop_time == datetime(
2022, 9, 30, 7, 0, tzinfo=dt_util.get_time_zone("Europe/Stockholm")
)
assert coordinator.sensor.charging_number_of_hours == 1

await coordinator.turn_off_charging()
await asyncio.sleep(2)
await hass.async_block_till_done()
# No schedule
MockSOCEntity.set_state(hass, "80")
await coordinator.update_sensors()
await asyncio.sleep(5)
assert coordinator.sensor.state == STATE_OFF
assert coordinator.sensor.charging_is_planned is False

await coordinator.turn_on_charging()
await asyncio.sleep(0.1)
await hass.async_block_till_done()
assert coordinator.sensor.state == STATE_OFF
await coordinator.turn_off_charging()
await asyncio.sleep(2)
await hass.async_block_till_done()
assert coordinator.sensor.state == STATE_OFF
Loading

0 comments on commit 5917908

Please sign in to comment.