Skip to content

Commit

Permalink
Added debounce for turning on and off the charger.
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasbkarlsson authored Feb 17, 2023
1 parent 6adf664 commit a0c4a06
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 26 deletions.
2 changes: 2 additions & 0 deletions custom_components/ev_smart_charging/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
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
52 changes: 30 additions & 22 deletions custom_components/ev_smart_charging/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
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 @@ -48,7 +49,7 @@
get_ready_hour_utc,
get_start_hour_utc,
)
from .helpers.general import Validator, get_parameter
from .helpers.general import Validator, debounce_async, get_parameter
from .sensor import EVSmartChargingSensor

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

async def turn_on_charging(self):
@debounce_async(DEBOUNCE_TIME)
async def turn_on_charging(self, state: bool = True):
"""Turn on charging"""
_LOGGER.debug("Turn on charging")
self.sensor.native_value = STATE_ON
if self.charger_switch is not None:
_LOGGER.debug("Before service call switch.turn_on: %s", self.charger_switch)
await self.hass.services.async_call(
domain=SWITCH,
service=SERVICE_TURN_ON,
target={"entity_id": self.charger_switch},
)

if state is True:
_LOGGER.debug("Turn on charging")
self.sensor.native_value = STATE_ON
if self.charger_switch is not None:
_LOGGER.debug(
"Before service call switch.turn_on: %s", self.charger_switch
)
await self.hass.services.async_call(
domain=SWITCH,
service=SERVICE_TURN_ON,
target={"entity_id": self.charger_switch},
)
else:
_LOGGER.debug("Turn off charging")
self.sensor.native_value = STATE_OFF
if self.charger_switch is not None:
_LOGGER.debug(
"Before service call switch.turn_off: %s", self.charger_switch
)
await self.hass.services.async_call(
domain=SWITCH,
service=SERVICE_TURN_OFF,
target={"entity_id": self.charger_switch},
)

async def turn_off_charging(self):
"""Turn off charging"""
_LOGGER.debug("Turn off charging")
self.sensor.native_value = STATE_OFF
if self.charger_switch is not None:
_LOGGER.debug(
"Before service call switch.turn_off: %s", self.charger_switch
)
await self.hass.services.async_call(
domain=SWITCH,
service=SERVICE_TURN_OFF,
target={"entity_id": self.charger_switch},
)
await self.turn_on_charging(False)

async def add_sensor(self, sensor: EVSmartChargingSensor):
"""Set up sensor"""
Expand Down
42 changes: 42 additions & 0 deletions custom_components/ev_smart_charging/helpers/general.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""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 @@ -73,3 +75,43 @@ 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():
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)
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
26 changes: 25 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
# See here for more info: https://docs.pytest.org/en/latest/fixture.html (note that
# pytest includes fixtures OOB which you can use as defined on this page)
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

import pytest

# pylint: disable=invalid-name
pytest_plugins = "pytest_homeassistant_custom_component"
Expand Down Expand Up @@ -88,3 +89,26 @@ def skip_service_calls_fixture():
"""Skip service calls."""
with patch("homeassistant.core.ServiceRegistry.async_call"):
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
85 changes: 85 additions & 0 deletions tests/coordinator/test_coordinator_debounce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Test ev_smart_charging coordinator."""

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 custom_components.ev_smart_charging.coordinator import (
EVSmartChargingCoordinator,
)
from custom_components.ev_smart_charging.const import (
DOMAIN,
)
from custom_components.ev_smart_charging.sensor import EVSmartChargingSensor
from tests.const import MOCK_CONFIG_ALL

from tests.helpers.helpers import (
MockChargerEntity,
MockPriceEntity,
MockSOCEntity,
MockTargetSOCEntity,
)
from tests.price import PRICE_20220930

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

entity_registry: EntityRegistry = async_entity_registry_get(hass)
MockSOCEntity.create(hass, entity_registry, "67")
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)
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)
await hass.async_block_till_done()
await coordinator.switch_active_update(False)
await coordinator.switch_apply_limit_update(False)
await coordinator.switch_continuous_update(True)
await coordinator.switch_ev_connected_update(False)
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()
assert coordinator.sensor.state == STATE_ON

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()
assert coordinator.sensor.state == STATE_ON

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

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
73 changes: 72 additions & 1 deletion tests/helpers/test_general.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""Test ev_smart_charging/helpers/general.py"""

import asyncio
from datetime import datetime
from freezegun import freeze_time
from homeassistant.core import State
import pytest

from pytest_homeassistant_custom_component.common import MockConfigEntry

Expand All @@ -10,7 +13,12 @@
CONF_PCT_PER_HOUR,
CONF_READY_HOUR,
)
from custom_components.ev_smart_charging.helpers.general import Validator, get_parameter
from custom_components.ev_smart_charging.helpers.general import (
Validator,
debounce_async,
get_parameter,
get_wait_time,
)

from .const import MOCK_CONFIG_DATA, MOCK_CONFIG_OPTIONS

Expand Down Expand Up @@ -195,3 +203,66 @@ async def test_get_parameter(hass):
assert get_parameter(config_entry, CONF_MIN_SOC) == 30.0
assert get_parameter(config_entry, CONF_READY_HOUR) is None
assert get_parameter(config_entry, CONF_READY_HOUR, 12) is 12


async def test_get_wait_time(hass):
"""Test get_wait_time"""

assert get_wait_time(1.0) == 1.0
assert get_wait_time(2.0) == 2.0


TEST_DEBOUNCE_ASYNC_VALUE = 0


# pylint: disable=global-statement
@debounce_async(1.0)
async def mock_function(new_value):
"""Function to be used for testing"""
global TEST_DEBOUNCE_ASYNC_VALUE
TEST_DEBOUNCE_ASYNC_VALUE = new_value


@pytest.mark.ensure_debounce
@freeze_time("2022-09-30T02:00:00+02:00", tick=True)
async def test_debounce_async(hass):
"""Test debounce_async"""

global TEST_DEBOUNCE_ASYNC_VALUE
TEST_DEBOUNCE_ASYNC_VALUE = 0

await mock_function(1)
await asyncio.sleep(2)
await hass.async_block_till_done()
assert TEST_DEBOUNCE_ASYNC_VALUE == 1

await mock_function(2)
await asyncio.sleep(0.1)
await hass.async_block_till_done()
assert TEST_DEBOUNCE_ASYNC_VALUE == 1
await mock_function(3)
await asyncio.sleep(2)
await hass.async_block_till_done()
assert TEST_DEBOUNCE_ASYNC_VALUE == 3


@freeze_time("2022-09-30T02:00:00+02:00", tick=True)
async def test_debounce_async2(hass):
"""Test debounce_async"""

global TEST_DEBOUNCE_ASYNC_VALUE
TEST_DEBOUNCE_ASYNC_VALUE = 0

await mock_function(1)
await asyncio.sleep(2)
await hass.async_block_till_done()
assert TEST_DEBOUNCE_ASYNC_VALUE == 1

await mock_function(2)
await asyncio.sleep(0.1)
await hass.async_block_till_done()
assert TEST_DEBOUNCE_ASYNC_VALUE == 2
await mock_function(3)
await asyncio.sleep(2)
await hass.async_block_till_done()
assert TEST_DEBOUNCE_ASYNC_VALUE == 3
3 changes: 1 addition & 2 deletions tests/test_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

from .const import MOCK_CONFIG_USER_NO_CHARGER


# We can pass fixtures as defined in conftest.py to tell pytest to use the fixture
# for a given test. We can also leverage fixtures and mocks that are available in
# Home Assistant using the pytest_homeassistant_custom_component plugin.
Expand All @@ -27,7 +26,7 @@

# pylint: disable=unused-argument
async def test_button(hass, bypass_validate_input_sensors):
"""Test sensor properties."""
"""Test buttons."""
# Create a mock entry so we don't have to go through config flow
config_entry = MockConfigEntry(
domain=DOMAIN, data=MOCK_CONFIG_USER_NO_CHARGER, entry_id="test"
Expand Down

0 comments on commit a0c4a06

Please sign in to comment.