Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added low price charging. #199

Merged
merged 6 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The integration calculates the set of hours that will give the lowest price, by
- Optional setting of minimum SOC level that should be reached indepently of the electrity price.
- Optional setting to only charge when the electricty price is lower than a specified level (will be ignored if needed by the minimum SOC setting).
- Optional setting to lower the level of maximum electricity price even further if the electricity price is very low at the end of the day tomorrow.
- Optional setting to start charging immediately if the electricity price is lower than a configured level.
- Optional possibility to provide information to the integration about when the EV is connected to the charger.
- Optional possibility to keep the charger on after completed charging, to enable preconditioning before departure, i.e., preheating/cooling can be done from the power grid instead of the battery.
- Service calls to dynamically control all configuation parameters that affect charging.
Expand Down Expand Up @@ -77,6 +78,7 @@ Entity | Type | Descriptions, valid value ranges and service calls
`select.ev_smart_charging_charge_completion_time` | Select | The lastest time of the day for charging to reach the target State-of-Charge. If `None` is selected, charging will be optimized using all hours with available price information, including before tomorrow's prices are available. Valid options, "00:00", "01:00", ..., "23:00" and "None". Can be set by service call `select.select_option`.
`number.ev_smart_charging_charging_speed` | Number | The charging speed expressed as percent per hour. For example, if the EV has a 77 kWh battery and the charger can deliver 11 kW (3-phase 16 A), then set this parameter to 14.3 (11/77*100). If there are limitations in the charging power, it is preferred to choose a smaller number. Try and see what works for you! Valid values min=0.1, step=0.1, max=100. Can be set by service call `number.set_value`.
`number.ev_smart_charging_electricity_price_limit` | Number | If the `apply_price_limit` switch is activated, charging will not be performed during hours when the electricity price is above this limit. NOTE that this might lead to that the EV will not be charged to the target State-of-Charge. Valid values min=-10000, step=0.01, max=10000. Can be set by service call `number.set_value`.
`number.ev_smart_charging_low_price_charging_level` | Number | If the `low_price_charging` switch is activated, charging will be done immediately if the electricity price is below this level. Valid values min=-10000, step=0.01, max=10000. Can be set by service call `number.set_value`.
`number.ev_smart_charging_opportunistic_level` | Number | If the `switch.ev_smart_charging_opportunistic_charging` switch is activated, the price limit will be even further reduced if the price at the end of the day is lower than `Electricity price limit * Opportunistic level / 100`. For example, if the `Opportunistic level` is set to 50, the price limit will be set to 50% of the `Electricity price limit`. If the prices for tomorrow are available, the price at the end of the day tomorrow will be used as trigger. Valid values min=0, step=1, max=100. Can be set by service call `number.set_value`.
`number.ev_smart_charging_minimum_ev_soc` | Number | The minimum State-of-Charge that should be charged, independently of the electricity price. Valid values min=0, step=1, max=100. Can be set by service call `number.set_value`.

Expand All @@ -89,6 +91,7 @@ Entity | Type | Description
`switch.ev_smart_charging_smart_charging_activated` | Switch | Turns the EV Smart Charging integration on and off.
`switch.ev_smart_charging_apply_price_limit` | Switch | Applies the price limit.
`switch.ev_smart_charging_opportunistic_charging` | Switch | Activates opportunistic charging. See the desciption of the configuration entity`number.ev_smart_charging_opportunistic_level`. This feature requires the feature `Electricity price limit` to be on.
`switch.ev_smart_charging_low_price_charging` | Switch | Activates charging immediately if the electricity price is lower than a configured level.
`switch.ev_smart_charging_continuous_charging_preferred` | Switch | If turned on, will as basis schedule one continuous charging session. If turned off, will schedule charging on the hours with lowest electricity price, even if they are not continuous.
`switch.ev_smart_charging_keep_charger_on` | Switch | If "on", the `sensor.ev_smart_charging_charging` will not turn off after completed charge cycle. The feature is intended to enable preconditioning before departure, i.e., preheating/cooling can be done from the power grid instead of the battery. If this option is used, the feature `Electricity price limit` will be turned off, and vice versa. *NOTE* It is required that `switch.ev_smart_charging_ev_connected` is controlled in a proper way in order for this feature to work. Also, there is an assumption made that the EV itself will stop its charging when reaching the target SOC.
`switch.ev_smart_charging_ev_connected` | Switch | Tells the integration that the EV is connected to the charger. Is preferable controlled by automations (see example below). Can avoid problems occuring when the EV is not connected to the charger at the time the charging should start. Using it will also ensure that the `sensor.ev_smart_charging_charging` is set to "off" when the EV is not connected to the charger.
Expand Down
10 changes: 9 additions & 1 deletion custom_components/ev_smart_charging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from .coordinator import EVSmartChargingCoordinator
from .const import (
CONF_EV_CONTROLLED,
CONF_LOW_PRICE_CHARGING_LEVEL,
CONF_LOW_SOC_CHARGING_LEVEL,
CONF_OPPORTUNISTIC_LEVEL,
CONF_START_HOUR,
DOMAIN,
Expand Down Expand Up @@ -138,7 +140,13 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry):
new[CONF_EV_CONTROLLED] = False
migration = True

if config_entry.version > 5:
if config_entry.version == 5:
config_entry.version = 6
new[CONF_LOW_PRICE_CHARGING_LEVEL] = 0.0
new[CONF_LOW_SOC_CHARGING_LEVEL] = 0.0
migration = True

if config_entry.version > 6:
_LOGGER.error(
"Migration from version %s to a lower version is not possible",
config_entry.version,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ev_smart_charging/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
class EVSmartChargingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow."""

VERSION = 5
VERSION = 6
user_input: Optional[dict[str, Any]]

def __init__(self):
Expand Down
5 changes: 5 additions & 0 deletions custom_components/ev_smart_charging/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@
ENTITY_NAME_EV_CONNECTED_SWITCH = "EV connected"
ENTITY_NAME_KEEP_ON_SWITCH = "Keep charger on"
ENTITY_NAME_OPPORTUNISTIC_SWITCH = "Opportunistic charging"
ENTITY_NAME_LOW_PRICE_CHARGING_SWITCH = "Low price charging"
ENTITY_NAME_START_BUTTON = "Manually start charging"
ENTITY_NAME_STOP_BUTTON = "Manually stop charging"
ENTITY_NAME_CONF_PCT_PER_HOUR_NUMBER = "Charging speed"
ENTITY_NAME_CONF_MAX_PRICE_NUMBER = "Electricity price limit"
ENTITY_NAME_CONF_OPPORTUNISTIC_LEVEL_NUMBER = "Opportunistic level"
ENTITY_NAME_CONF_LOW_PRICE_CHARGING_NUMBER = "Low price charging level"
ENTITY_NAME_CONF_MIN_SOC_NUMBER = "Minimum EV SOC"
ENTITY_NAME_CONF_START_HOUR = "Charge start time"
ENTITY_NAME_CONF_READY_HOUR = "Charge completion time"
Expand All @@ -62,6 +64,8 @@
CONF_READY_HOUR = "ready_hour"
CONF_MAX_PRICE = "maximum_price"
CONF_OPPORTUNISTIC_LEVEL = "opportunistic_level"
CONF_LOW_PRICE_CHARGING_LEVEL = "low_price_charging_level"
CONF_LOW_SOC_CHARGING_LEVEL = "low_soc_charging_level"
CONF_MIN_SOC = "min_soc"

HOURS = [
Expand Down Expand Up @@ -101,6 +105,7 @@
CHARGING_STATUS_KEEP_ON = "Keeping charger on"
CHARGING_STATUS_DISCONNECTED = "Disconnected"
CHARGING_STATUS_NOT_ACTIVE = "Smart charging not active"
CHARGING_STATUS_LOW_PRICE_CHARGING = "Low price charging"

# Defaults
DEFAULT_NAME = DOMAIN
Expand Down
31 changes: 30 additions & 1 deletion custom_components/ev_smart_charging/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
CHARGING_STATUS_NOT_ACTIVE,
CHARGING_STATUS_WAITING_CHARGING,
CHARGING_STATUS_WAITING_NEW_PRICE,
CHARGING_STATUS_LOW_PRICE_CHARGING,
CONF_CHARGER_ENTITY,
CONF_EV_CONTROLLED,
CONF_LOW_PRICE_CHARGING_LEVEL,
CONF_MAX_PRICE,
CONF_MIN_SOC,
CONF_OPPORTUNISTIC_LEVEL,
Expand Down Expand Up @@ -91,6 +93,7 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
self.switch_opportunistic = None
self.switch_opportunistic_entity_id = None
self.switch_opportunistic_unique_id = None
self.switch_low_price_charging = None
self.price_entity_id = None
self.price_adaptor = PriceAdaptor()
self.ev_soc_entity_id = None
Expand Down Expand Up @@ -142,8 +145,12 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
self.number_opportunistic_level = int(
get_parameter(self.config_entry, CONF_OPPORTUNISTIC_LEVEL, 50.0)
)
self.low_price_charging = float(
get_parameter(self.config_entry, CONF_LOW_PRICE_CHARGING_LEVEL, 0.0)
)

self.auto_charging_state = STATE_OFF
self.low_price_charging_state = STATE_OFF

# Update state once per hour.
self.listeners.append(
Expand Down Expand Up @@ -214,13 +221,25 @@ async def update_state(
and self.switch_ev_connected is True
and self.switch_active is True
)

if (
self.ev_soc is not None
and self.ev_target_soc is not None
and self.ev_soc >= self.ev_target_soc
):
turn_on_charging = False

if (
self.switch_active is True
and self.switch_low_price_charging is True
and self.sensor.current_price is not None
and self.sensor.current_price <= self.low_price_charging
):
turn_on_charging = True
self.low_price_charging_state = STATE_ON
else:
self.low_price_charging_state = STATE_OFF

time_now = dt.now()
current_value = self.auto_charging_state == STATE_ON

Expand Down Expand Up @@ -300,6 +319,10 @@ async def update_state(
self.sensor_status.native_value = CHARGING_STATUS_NOT_ACTIVE
elif not self.switch_ev_connected:
self.sensor_status.native_value = CHARGING_STATUS_DISCONNECTED
elif self.low_price_charging_state == STATE_ON:
self.sensor_status.native_value = (
CHARGING_STATUS_LOW_PRICE_CHARGING
)
elif self.switch_keep_on and self.auto_charging_state == STATE_ON:
self.sensor_status.native_value = CHARGING_STATUS_KEEP_ON
elif (
Expand Down Expand Up @@ -526,6 +549,12 @@ async def switch_opportunistic_update(self, state: bool):
)
await self.update_configuration()

async def switch_low_price_charging_update(self, state: bool):
"""Handle the low price charging switch"""
self.switch_low_price_charging = state
_LOGGER.debug("switch_low_price_charging_update = %s", state)
await self.update_configuration()

async def update_configuration(self):
"""Called when the configuration has been updated"""
await self.update_sensors(configuration_updated=True)
Expand Down Expand Up @@ -679,7 +708,7 @@ async def update_sensors(
(self.ev_soc >= self.ev_target_soc)
or (
(self.tomorrow_valid or time_now_hour_local < self.ready_hour_local)
and not_charging
and (not_charging or configuration_updated)
)
)
):
Expand Down
29 changes: 29 additions & 0 deletions custom_components/ev_smart_charging/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
from homeassistant.helpers.entity import EntityCategory

from .const import (
CONF_LOW_PRICE_CHARGING_LEVEL,
CONF_MAX_PRICE,
CONF_MIN_SOC,
CONF_OPPORTUNISTIC_LEVEL,
CONF_PCT_PER_HOUR,
DOMAIN,
ENTITY_NAME_CONF_LOW_PRICE_CHARGING_NUMBER,
ENTITY_NAME_CONF_OPPORTUNISTIC_LEVEL_NUMBER,
ENTITY_NAME_CONF_PCT_PER_HOUR_NUMBER,
ENTITY_NAME_CONF_MAX_PRICE_NUMBER,
Expand All @@ -41,6 +43,7 @@ async def async_setup_entry(
numbers.append(EVSmartChargingNumberPriceLimit(entry, coordinator))
numbers.append(EVSmartChargingNumberMinSOC(entry, coordinator))
numbers.append(EVSmartChargingNumberOpportunistic(entry, coordinator))
numbers.append(EVSmartChargingNumberLowPriceCharging(entry, coordinator))
async_add_devices(numbers)


Expand Down Expand Up @@ -171,3 +174,29 @@ async def async_set_native_value(self, value: float) -> None:
await super().async_set_native_value(value)
self.coordinator.number_opportunistic_level = value
await self.coordinator.update_configuration()


class EVSmartChargingNumberLowPriceCharging(EVSmartChargingNumber):
"""EV Smart Charging low price charging number class."""

_attr_name = ENTITY_NAME_CONF_LOW_PRICE_CHARGING_NUMBER
_attr_icon = ICON_CASH
_attr_entity_category = EntityCategory.CONFIG
_attr_native_min_value = -10000.0
_attr_native_max_value = 10000.0
_attr_native_step = 0.01

def __init__(self, entry, coordinator: EVSmartChargingCoordinator):
_LOGGER.debug("EVSmartChargingNumberLowPriceCharging.__init__()")
super().__init__(entry, coordinator)
if self.value is None:
self._attr_native_value = get_parameter(
entry, CONF_LOW_PRICE_CHARGING_LEVEL, 0.0
)
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.low_price_charging = value
await self.coordinator.update_configuration()
26 changes: 26 additions & 0 deletions custom_components/ev_smart_charging/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
ENTITY_NAME_APPLY_LIMIT_SWITCH,
ENTITY_NAME_CONTINUOUS_SWITCH,
ENTITY_NAME_EV_CONNECTED_SWITCH,
ENTITY_NAME_LOW_PRICE_CHARGING_SWITCH,
ENTITY_NAME_KEEP_ON_SWITCH,
ENTITY_NAME_OPPORTUNISTIC_SWITCH,
ICON_CONNECTION,
Expand All @@ -37,6 +38,7 @@ async def async_setup_entry(
switches.append(EVSmartChargingSwitchEVConnected(entry, coordinator))
switches.append(EVSmartChargingSwitchKeepOn(entry, coordinator))
switches.append(EVSmartChargingSwitchOpportunistic(entry, coordinator))
switches.append(EVSmartChargingSwitchLowPriceCharging(entry, coordinator))
async_add_devices(switches)


Expand Down Expand Up @@ -214,3 +216,27 @@ async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
await super().async_turn_off(**kwargs)
await self.coordinator.switch_opportunistic_update(False)


class EVSmartChargingSwitchLowPriceCharging(EVSmartChargingSwitch):
"""EV Smart Charging low price charging switch class."""

_attr_name = ENTITY_NAME_LOW_PRICE_CHARGING_SWITCH

def __init__(self, entry, coordinator: EVSmartChargingCoordinator):
_LOGGER.debug("EVSmartChargingSwitchLowPriceCharging.__init__()")
super().__init__(entry, coordinator)
if self.is_on is None:
self._attr_is_on = False
self.update_ha_state()
self.coordinator.switch_low_price_charging = self.is_on

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
await super().async_turn_on(**kwargs)
await self.coordinator.switch_low_price_charging_update(True)

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
await super().async_turn_off(**kwargs)
await self.coordinator.switch_low_price_charging_update(False)
36 changes: 36 additions & 0 deletions tests/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
CONF_CHARGER_ENTITY,
CONF_DEVICE_NAME,
CONF_EV_CONTROLLED,
CONF_LOW_PRICE_CHARGING_LEVEL,
CONF_LOW_SOC_CHARGING_LEVEL,
CONF_MAX_PRICE,
CONF_MIN_SOC,
CONF_OPPORTUNISTIC_LEVEL,
Expand Down Expand Up @@ -115,6 +117,21 @@
CONF_MIN_SOC: 30.0,
}

MOCK_CONFIG_ALL_V5 = {
CONF_DEVICE_NAME: "EV Smart Charging",
CONF_PRICE_SENSOR: "sensor.nordpool_kwh_se3_sek_2_10_0",
CONF_EV_SOC_SENSOR: "sensor.volkswagen_we_connect_id_state_of_charge",
CONF_EV_TARGET_SOC_SENSOR: "sensor.volkswagen_we_connect_id_target_state_of_charge",
CONF_CHARGER_ENTITY: "switch.ocpp_charge_control",
CONF_EV_CONTROLLED: False,
CONF_PCT_PER_HOUR: 6.0,
CONF_START_HOUR: "None",
CONF_READY_HOUR: "08:00",
CONF_MAX_PRICE: 0.0,
CONF_OPPORTUNISTIC_LEVEL: 50.0,
CONF_MIN_SOC: 30.0,
}

MOCK_CONFIG_ALL = {
CONF_DEVICE_NAME: "EV Smart Charging",
CONF_PRICE_SENSOR: "sensor.nordpool_kwh_se3_sek_2_10_0",
Expand All @@ -128,6 +145,8 @@
CONF_MAX_PRICE: 0.0,
CONF_OPPORTUNISTIC_LEVEL: 50.0,
CONF_MIN_SOC: 30.0,
CONF_LOW_PRICE_CHARGING_LEVEL: 0.0,
CONF_LOW_SOC_CHARGING_LEVEL: 0.0,
}

MOCK_CONFIG_LATE = {
Expand Down Expand Up @@ -208,6 +227,23 @@
CONF_MIN_SOC: 40.0,
}

MOCK_CONFIG_LOW_PRICE_CHARGING = {
CONF_DEVICE_NAME: "EV Smart Charging",
CONF_PRICE_SENSOR: "sensor.nordpool_kwh_se3_sek_2_10_0",
CONF_EV_SOC_SENSOR: "sensor.volkswagen_we_connect_id_state_of_charge",
CONF_EV_TARGET_SOC_SENSOR: "sensor.volkswagen_we_connect_id_target_state_of_charge",
CONF_CHARGER_ENTITY: "switch.ocpp_charge_control",
CONF_EV_CONTROLLED: False,
CONF_PCT_PER_HOUR: 6.0,
CONF_START_HOUR: "None",
CONF_READY_HOUR: "08:00",
CONF_MAX_PRICE: 0.0,
CONF_OPPORTUNISTIC_LEVEL: 50.0,
CONF_MIN_SOC: 30.0,
CONF_LOW_PRICE_CHARGING_LEVEL: 150.0,
CONF_LOW_SOC_CHARGING_LEVEL: 0.0,
}

MOCK_CONFIG_TIME1 = {
CONF_PRICE_SENSOR: "sensor.nordpool_kwh_se3_sek_2_10_0",
CONF_EV_SOC_SENSOR: "sensor.volkswagen_we_connect_id_state_of_charge",
Expand Down
Loading
Loading