Skip to content

Commit

Permalink
Dynamic configuration parameters.
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasbkarlsson authored Dec 29, 2022
1 parent 8810dcc commit ca42249
Show file tree
Hide file tree
Showing 5 changed files with 368 additions and 3 deletions.
14 changes: 11 additions & 3 deletions custom_components/ev_smart_charging/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,21 @@

# Icons
ICON = "mdi:flash"
ICON_BATTERY_50 = "mdi:battery-50"
ICON_CASH = "mdi:cash"
ICON_CONNECTION = "mdi:connection"
ICON_MIN_SOC = "mdi:battery-charging-30"
ICON_START = "mdi:play-circle-outline"
ICON_STOP = "mdi:stop-circle-outline"
ICON_CONNECTION = "mdi:connection"
ICON_TIME = "mdi:clock-time-four-outline"

# Platforms
SENSOR = Platform.SENSOR
SWITCH = Platform.SWITCH
BUTTON = Platform.BUTTON
PLATFORMS = [SWITCH, SENSOR, BUTTON]
NUMBER = Platform.NUMBER
SELECT = Platform.SELECT
PLATFORMS = [SWITCH, SENSOR, BUTTON, NUMBER, SELECT]
PLATFORM_NORDPOOL = "nordpool"
PLATFORM_VW = "volkswagen_we_connect_id"
PLATFORM_OCPP = "ocpp"
Expand All @@ -34,7 +39,10 @@
ENTITY_NAME_KEEP_ON_SWITCH = "Keep charger on"
ENTITY_NAME_START_BUTTON = "Manually start charging"
ENTITY_NAME_STOP_BUTTON = "Manually stop charging"
ENTITY_NAME_MIN_SOC_NUMBER = "Minimum SOC"
ENTITY_NAME_CONF_PCT_PER_HOUR_NUMBER = "Charging speed"
ENTITY_NAME_CONF_MAX_PRICE_NUMBER = "Electricity price limit"
ENTITY_NAME_CONF_MIN_SOC_NUMBER = "Minimum EV SOC"
ENTITY_NAME_CONF_READY_HOUR = "Charge completion time"

# Configuration and options
CONF_DEVICE_NAME = "device_name"
Expand Down
125 changes: 125 additions & 0 deletions custom_components/ev_smart_charging/number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""Number platform for EV Smart Charging."""
import logging

from homeassistant.components.number import NumberEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory

from .const import (
CONF_MAX_PRICE,
CONF_MIN_SOC,
CONF_PCT_PER_HOUR,
DOMAIN,
ENTITY_NAME_CONF_PCT_PER_HOUR_NUMBER,
ENTITY_NAME_CONF_MAX_PRICE_NUMBER,
ENTITY_NAME_CONF_MIN_SOC_NUMBER,
ICON_BATTERY_50,
ICON_CASH,
NUMBER,
)
from .coordinator import EVSmartChargingCoordinator
from .entity import EVSmartChargingEntity
from .helpers.general import get_parameter

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant, entry, async_add_devices
): # pylint: disable=unused-argument
"""Setup number platform."""
_LOGGER.debug("EVSmartCharging.number.py")
coordinator = hass.data[DOMAIN][entry.entry_id]
numbers = []
numbers.append(EVSmartChargingNumberChargingSpeed(entry, coordinator))
numbers.append(EVSmartChargingNumberPriceLimit(entry, coordinator))
numbers.append(EVSmartChargingNumberMinSOC(entry, coordinator))
async_add_devices(numbers)


class EVSmartChargingNumber(EVSmartChargingEntity, NumberEntity):
"""EV Smart Charging switch class."""

_attr_native_value: float | None = None # To support HA 2022.7

def __init__(self, entry, coordinator: EVSmartChargingCoordinator):
_LOGGER.debug("EVSmartChargingNumber.__init__()")
super().__init__(entry)
self.coordinator = coordinator
id_name = self._attr_name.replace(" ", "").lower()
self._attr_unique_id = ".".join([entry.entry_id, NUMBER, id_name])

async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
self._attr_native_value = value


class EVSmartChargingNumberChargingSpeed(EVSmartChargingNumber):
"""EV Smart Charging active switch class."""

_attr_name = ENTITY_NAME_CONF_PCT_PER_HOUR_NUMBER
_attr_entity_category = EntityCategory.CONFIG
_attr_native_min_value = 0.1
_attr_native_max_value = 100.0
_attr_native_step = 0.1

def __init__(self, entry, coordinator: EVSmartChargingCoordinator):
_LOGGER.debug("EVSmartChargingNumberChargingSpeed.__init__()")
super().__init__(entry, coordinator)
if self.value is None:
self._attr_native_value = get_parameter(entry, CONF_PCT_PER_HOUR)
self.update_ha_state()

async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
await super().async_set_native_value(value)
self.coordinator.charging_pct_per_hour = value
await self.coordinator.update_sensors()


class EVSmartChargingNumberPriceLimit(EVSmartChargingNumber):
"""EV Smart Charging apply limit switch class."""

_attr_name = ENTITY_NAME_CONF_MAX_PRICE_NUMBER
_attr_icon = ICON_CASH
_attr_entity_category = EntityCategory.CONFIG
_attr_native_min_value = 0.0
_attr_native_max_value = 10000.0
_attr_native_step = 0.01

def __init__(self, entry, coordinator: EVSmartChargingCoordinator):
_LOGGER.debug("EVSmartChargingNumberPriceLimit.__init__()")
super().__init__(entry, coordinator)
if self.value is None:
self._attr_native_value = get_parameter(entry, CONF_MAX_PRICE)
self.update_ha_state()

async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
await super().async_set_native_value(value)
self.coordinator.max_price = value
await self.coordinator.update_sensors()


class EVSmartChargingNumberMinSOC(EVSmartChargingNumber):
"""EV Smart Charging continuous switch class."""

_attr_name = ENTITY_NAME_CONF_MIN_SOC_NUMBER
_attr_icon = ICON_BATTERY_50
_attr_entity_category = EntityCategory.CONFIG
_attr_native_min_value = 0.0
_attr_native_max_value = 100.0
_attr_native_step = 1.0

def __init__(self, entry, coordinator: EVSmartChargingCoordinator):
_LOGGER.debug("EVSmartChargingNumberMinSOC.__init__()")
super().__init__(entry, coordinator)
if self.value is None:
self._attr_native_value = get_parameter(entry, CONF_MIN_SOC)
self.update_ha_state()

async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
await super().async_set_native_value(value)
self.coordinator.number_min_soc = value
await self.coordinator.update_sensors()
78 changes: 78 additions & 0 deletions custom_components/ev_smart_charging/select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Select platform for EV Smart Charging."""
import logging

from homeassistant.components.select import SelectEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory

from .const import (
CONF_READY_HOUR,
DOMAIN,
ENTITY_NAME_CONF_READY_HOUR,
HOURS,
ICON_TIME,
SELECT,
)
from .coordinator import EVSmartChargingCoordinator
from .entity import EVSmartChargingEntity
from .helpers.general import get_parameter

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant, entry, async_add_devices
): # pylint: disable=unused-argument
"""Setup select platform."""
_LOGGER.debug("EVSmartCharging.select.py")
coordinator = hass.data[DOMAIN][entry.entry_id]
selects = []
selects.append(EVSmartChargingSelectReadyHour(entry, coordinator))
async_add_devices(selects)


class EVSmartChargingSelect(EVSmartChargingEntity, SelectEntity):
"""EV Smart Charging switch class."""

_attr_current_option: str | None = None

def __init__(self, entry, coordinator: EVSmartChargingCoordinator):
_LOGGER.debug("EVSmartChargingSelect.__init__()")
super().__init__(entry)
self.coordinator = coordinator
id_name = self._attr_name.replace(" ", "").lower()
self._attr_unique_id = ".".join([entry.entry_id, SELECT, id_name])

async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
self._attr_current_option = option


class EVSmartChargingSelectReadyHour(EVSmartChargingSelect):
"""EV Smart Charging active switch class."""

_attr_name = ENTITY_NAME_CONF_READY_HOUR
_attr_icon = ICON_TIME
_attr_entity_category = EntityCategory.CONFIG
_attr_options = HOURS

def __init__(self, entry, coordinator: EVSmartChargingCoordinator):
_LOGGER.debug("EVSmartChargingSelectReadyHour.__init__()")
super().__init__(entry, coordinator)
if self.state is None:
self._attr_current_option = get_parameter(entry, CONF_READY_HOUR)
self.update_ha_state()

async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await super().async_select_option(option)
if self.state:
try:
self.coordinator.ready_hour_local = int(self.state[0:2])
except ValueError:
# Don't use ready_hour. Select a time in the far future.
self.coordinator.ready_hour_local = 72
if self.coordinator.ready_hour_local == 0:
# Treat 00:00 as 24:00
self.coordinator.ready_hour_local = 24
await self.coordinator.update_sensors()
86 changes: 86 additions & 0 deletions tests/test_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Test ev_smart_charging number."""
from pytest_homeassistant_custom_component.common import MockConfigEntry

from custom_components.ev_smart_charging import (
async_setup_entry,
async_unload_entry,
)
from custom_components.ev_smart_charging.const import (
CONF_MAX_PRICE,
CONF_MIN_SOC,
CONF_PCT_PER_HOUR,
DOMAIN,
NUMBER,
)
from custom_components.ev_smart_charging.coordinator import (
EVSmartChargingCoordinator,
)
from custom_components.ev_smart_charging.number import (
EVSmartChargingNumberChargingSpeed,
EVSmartChargingNumberPriceLimit,
EVSmartChargingNumberMinSOC,
)

from .const import MOCK_CONFIG_MIN_SOC

# 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.
# Assertions allow you to verify that the return value of whatever is on the left
# side of the assertion matches with the right side.

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

# Set up the entry and assert that the values set during setup are where we expect
# them to be. Because we have patched the BlueprintDataUpdateCoordinator.async_get_data
# call, no code from custom_components/integration_blueprint/api.py actually runs.
assert await async_setup_entry(hass, config_entry)
await hass.async_block_till_done()

assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN]
assert isinstance(
hass.data[DOMAIN][config_entry.entry_id], EVSmartChargingCoordinator
)
coordinator = hass.data[DOMAIN][config_entry.entry_id]

# Get the numbers
number_charging_speed: EVSmartChargingNumberChargingSpeed = hass.data[
"entity_components"
][NUMBER].get_entity("number.none_charging_speed")
number_price_limit: EVSmartChargingNumberPriceLimit = hass.data[
"entity_components"
][NUMBER].get_entity("number.none_electricity_price_limit")
number_min_soc: EVSmartChargingNumberMinSOC = hass.data["entity_components"][
NUMBER
].get_entity("number.none_minimum_ev_soc")
assert number_charging_speed
assert number_price_limit
assert number_min_soc
assert isinstance(number_charging_speed, EVSmartChargingNumberChargingSpeed)
assert isinstance(number_price_limit, EVSmartChargingNumberPriceLimit)
assert isinstance(number_min_soc, EVSmartChargingNumberMinSOC)

# Test the numbers

assert number_charging_speed.native_value == MOCK_CONFIG_MIN_SOC[CONF_PCT_PER_HOUR]
assert number_price_limit.native_value == MOCK_CONFIG_MIN_SOC[CONF_MAX_PRICE]
assert number_min_soc.native_value == MOCK_CONFIG_MIN_SOC[CONF_MIN_SOC]

await number_charging_speed.async_set_native_value(3.2)
assert coordinator.charging_pct_per_hour == 3.2

await number_price_limit.async_set_native_value(123)
assert coordinator.max_price == 123

await number_min_soc.async_set_native_value(33)
assert coordinator.number_min_soc == 33

# Unload the entry and verify that the data has been removed
assert await async_unload_entry(hass, config_entry)
assert config_entry.entry_id not in hass.data[DOMAIN]
68 changes: 68 additions & 0 deletions tests/test_select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Test ev_smart_charging select."""
from pytest_homeassistant_custom_component.common import MockConfigEntry

from custom_components.ev_smart_charging import (
async_setup_entry,
async_unload_entry,
)
from custom_components.ev_smart_charging.const import (
CONF_READY_HOUR,
DOMAIN,
SELECT,
)
from custom_components.ev_smart_charging.coordinator import (
EVSmartChargingCoordinator,
)
from custom_components.ev_smart_charging.select import (
EVSmartChargingSelectReadyHour,
)

from .const import MOCK_CONFIG_MIN_SOC

# 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.
# Assertions allow you to verify that the return value of whatever is on the left
# side of the assertion matches with the right side.

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

# Set up the entry and assert that the values set during setup are where we expect
# them to be. Because we have patched the BlueprintDataUpdateCoordinator.async_get_data
# call, no code from custom_components/integration_blueprint/api.py actually runs.
assert await async_setup_entry(hass, config_entry)
await hass.async_block_till_done()

assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN]
assert isinstance(
hass.data[DOMAIN][config_entry.entry_id], EVSmartChargingCoordinator
)
coordinator = hass.data[DOMAIN][config_entry.entry_id]

# Get the selects
select_ready_hour: EVSmartChargingSelectReadyHour = hass.data["entity_components"][
SELECT
].get_entity("select.none_charge_completion_time")
assert select_ready_hour
assert isinstance(select_ready_hour, EVSmartChargingSelectReadyHour)

# Test the selects

assert select_ready_hour.state == MOCK_CONFIG_MIN_SOC[CONF_READY_HOUR]

await select_ready_hour.async_select_option("00:00")
assert coordinator.ready_hour_local == 24
await select_ready_hour.async_select_option("13:00")
assert coordinator.ready_hour_local == 13
await select_ready_hour.async_select_option("None")
assert coordinator.ready_hour_local == 72

# Unload the entry and verify that the data has been removed
assert await async_unload_entry(hass, config_entry)
assert config_entry.entry_id not in hass.data[DOMAIN]

0 comments on commit ca42249

Please sign in to comment.