From 1a1c573617efb10d8ac21112d34a402bf328a020 Mon Sep 17 00:00:00 2001 From: Jonas Karlsson Date: Sat, 31 Aug 2024 20:20:15 +0200 Subject: [PATCH] Solar power based charging. --- .../ev_smart_charging/__init__.py | 16 +- .../ev_smart_charging/config_flow.py | 128 ++++++++++- custom_components/ev_smart_charging/const.py | 36 +++- .../ev_smart_charging/coordinator.py | 140 ++++++++++-- .../ev_smart_charging/helpers/config_flow.py | 59 ++++-- .../helpers/solar_charging.py | 199 ++++++++++++++++++ custom_components/ev_smart_charging/number.py | 113 ++++++++++ custom_components/ev_smart_charging/sensor.py | 41 +++- custom_components/ev_smart_charging/switch.py | 82 ++++++++ .../ev_smart_charging/translations/en.json | 65 +++++- tests/const.py | 4 + tests/coordinator/test_coordinator.py | 5 + tests/coordinator/test_coordinator_keep_on.py | 8 + .../coordinator/test_coordinator_keep_on2.py | 5 + .../coordinator/test_coordinator_keep_on3.py | 1 + .../test_coordinator_late_ready.py | 4 + .../test_coordinator_low_price_charging.py | 1 + .../test_coordinator_low_soc_charging.py | 1 + .../test_coordinator_negative_price.py | 2 + .../coordinator/test_coordinator_no_ready.py | 3 + .../test_coordinator_reschedule.py | 2 + .../test_coordinator_start_hour_both_same.py | 3 + ...coordinator_start_hour_end_before_start.py | 4 + .../test_coordinator_start_hour_only_start.py | 3 + ...coordinator_start_hour_start_before_end.py | 4 + tests/coordinator/test_coordinator_status.py | 1 + tests/test_daylight_saving.py | 3 + 27 files changed, 872 insertions(+), 61 deletions(-) create mode 100644 custom_components/ev_smart_charging/helpers/solar_charging.py diff --git a/custom_components/ev_smart_charging/__init__.py b/custom_components/ev_smart_charging/__init__.py index 20a2b9a..0b60bd7 100644 --- a/custom_components/ev_smart_charging/__init__.py +++ b/custom_components/ev_smart_charging/__init__.py @@ -5,7 +5,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import MAJOR_VERSION, MINOR_VERSION -from homeassistant.core import Config, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import async_get as async_device_registry_get from homeassistant.helpers.device_registry import DeviceRegistry, DeviceEntry @@ -19,9 +19,12 @@ from .coordinator import EVSmartChargingCoordinator from .const import ( CONF_EV_CONTROLLED, + CONF_GRID_USAGE_SENSOR, + CONF_GRID_VOLTAGE, CONF_LOW_PRICE_CHARGING_LEVEL, CONF_LOW_SOC_CHARGING_LEVEL, CONF_OPPORTUNISTIC_LEVEL, + CONF_SOLAR_CHARGING_CONFIGURED, CONF_START_HOUR, DOMAIN, STARTUP_MESSAGE, @@ -105,9 +108,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unloaded + # Global lock ev_smart_charging_lock = asyncio.Lock() + async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Reload config entry.""" _LOGGER.debug("async_reload_entry") @@ -152,7 +157,14 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry): new[CONF_LOW_SOC_CHARGING_LEVEL] = 20.0 migration = True - if version > 6: + if version == 6: + version = 7 + new[CONF_SOLAR_CHARGING_CONFIGURED] = False + new[CONF_GRID_USAGE_SENSOR] = "" + new[CONF_GRID_VOLTAGE] = 230 # [V] + migration = True + + if version > 7: _LOGGER.error( "Migration from version %s to a lower version is not possible", version, diff --git a/custom_components/ev_smart_charging/config_flow.py b/custom_components/ev_smart_charging/config_flow.py index 675e905..4a60e34 100644 --- a/custom_components/ev_smart_charging/config_flow.py +++ b/custom_components/ev_smart_charging/config_flow.py @@ -1,10 +1,10 @@ """Adds config flow for EV Smart Charging.""" + import logging from typing import Any, Optional import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import MAJOR_VERSION, MINOR_VERSION from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv @@ -14,8 +14,11 @@ CONF_EV_CONTROLLED, CONF_EV_SOC_SENSOR, CONF_EV_TARGET_SOC_SENSOR, + CONF_GRID_USAGE_SENSOR, + CONF_GRID_VOLTAGE, CONF_PRICE_SENSOR, CONF_CHARGER_ENTITY, + CONF_SOLAR_CHARGING_CONFIGURED, DOMAIN, ) from .helpers.config_flow import DeviceNameCreator, FindEntity, FlowValidator @@ -27,7 +30,7 @@ class EVSmartChargingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow.""" - VERSION = 6 + VERSION = 7 user_input: Optional[dict[str, Any]] def __init__(self): @@ -58,11 +61,12 @@ async def async_step_user(self, user_input=None): user_input[CONF_DEVICE_NAME] = DeviceNameCreator.create(self.hass) user_input[CONF_PRICE_SENSOR] = FindEntity.find_price_sensor(self.hass) user_input[CONF_EV_SOC_SENSOR] = FindEntity.find_vw_soc_sensor(self.hass) - user_input[ - CONF_EV_TARGET_SOC_SENSOR - ] = FindEntity.find_vw_target_soc_sensor(self.hass) + user_input[CONF_EV_TARGET_SOC_SENSOR] = ( + FindEntity.find_vw_target_soc_sensor(self.hass) + ) user_input[CONF_CHARGER_ENTITY] = FindEntity.find_ocpp_device(self.hass) user_input[CONF_EV_CONTROLLED] = False + user_input[CONF_SOLAR_CHARGING_CONFIGURED] = False else: # process user_input @@ -72,9 +76,12 @@ async def async_step_user(self, user_input=None): if not self._errors: self.user_input = user_input - return self.async_create_entry( - title=user_input[CONF_DEVICE_NAME], data=self.user_input - ) + if user_input[CONF_SOLAR_CHARGING_CONFIGURED]: + return await self.async_step_solar() + else: + return self.async_create_entry( + title=user_input[CONF_DEVICE_NAME], data=self.user_input + ) return await self._show_config_form_user(user_input) @@ -100,12 +107,62 @@ async def _show_config_form_user(self, user_input): vol.Optional( CONF_EV_CONTROLLED, default=user_input[CONF_EV_CONTROLLED] ): cv.boolean, + vol.Optional( + CONF_SOLAR_CHARGING_CONFIGURED, + default=user_input[CONF_SOLAR_CHARGING_CONFIGURED], + ): cv.boolean, } return self.async_show_form( step_id="user", data_schema=vol.Schema(user_schema), errors=self._errors, + last_step=False, + ) + + async def async_step_solar(self, user_input=None): + """Configuraton of Solar charging""" + _LOGGER.debug("EVChargingControlConfigFlow.async_step_solar") + self._errors = {} + + if user_input is None: + user_input = {} + # Provide defaults for form + user_input[CONF_GRID_USAGE_SENSOR] = "" + user_input[CONF_GRID_VOLTAGE] = 230 # [V] + + else: + # process user_input + error = FlowValidator.validate_step_solar(self.hass, user_input) + if error is not None: + self._errors[error[0]] = error[1] + + if not self._errors: + self.user_input = self.user_input | user_input + return self.async_create_entry( + title=self.user_input[CONF_DEVICE_NAME], data=self.user_input + ) + + return await self._show_config_form_solar(user_input) + + async def _show_config_form_solar(self, user_input): + """Show the configuration form.""" + + positive_int = vol.All(vol.Coerce(int), vol.Range(min=1)) + + user_schema = { + vol.Required( + CONF_GRID_USAGE_SENSOR, default=user_input[CONF_GRID_USAGE_SENSOR] + ): cv.string, + vol.Required( + CONF_GRID_VOLTAGE, default=user_input[CONF_GRID_VOLTAGE] + ): positive_int, + } + + return self.async_show_form( + step_id="solar", + data_schema=vol.Schema(user_schema), + errors=self._errors, last_step=True, ) @@ -117,6 +174,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self._errors = {} + self.user_input = {} async def async_step_init(self, user_input) -> FlowResult: """Manage the options.""" @@ -131,9 +189,13 @@ async def async_step_init(self, user_input) -> FlowResult: self._errors[error[0]] = error[1] if not self._errors: - return self.async_create_entry( - title=self.config_entry.title, data=user_input - ) + self.user_input = user_input + if user_input[CONF_SOLAR_CHARGING_CONFIGURED]: + return await self.async_step_solar() + else: + return self.async_create_entry( + title=self.config_entry.title, data=self.user_input + ) user_schema = { vol.Required( @@ -156,11 +218,55 @@ async def async_step_init(self, user_input) -> FlowResult: CONF_EV_CONTROLLED, default=get_parameter(self.config_entry, CONF_EV_CONTROLLED), ): cv.boolean, + vol.Optional( + CONF_SOLAR_CHARGING_CONFIGURED, + default=get_parameter( + self.config_entry, CONF_SOLAR_CHARGING_CONFIGURED + ), + ): cv.boolean, } return self.async_show_form( step_id="init", data_schema=vol.Schema(user_schema), errors=self._errors, + last_step=False, + ) + + async def async_step_solar(self, user_input=None) -> FlowResult: + """Manage the options.""" + + positive_int = vol.All(vol.Coerce(int), vol.Range(min=1)) + + self._errors = {} + + if user_input is not None: + # process user_input + error = FlowValidator.validate_step_solar(self.hass, user_input) + + if error is not None: + self._errors[error[0]] = error[1] + + if not self._errors: + self.user_input = self.user_input | user_input + return self.async_create_entry( + title=self.config_entry.title, data=self.user_input + ) + + user_schema = { + vol.Required( + CONF_GRID_USAGE_SENSOR, + default=get_parameter(self.config_entry, CONF_GRID_USAGE_SENSOR), + ): cv.string, + vol.Required( + CONF_GRID_VOLTAGE, + default=get_parameter(self.config_entry, CONF_GRID_VOLTAGE), + ): positive_int, + } + + return self.async_show_form( + step_id="solar", + data_schema=vol.Schema(user_schema), + errors=self._errors, last_step=True, ) diff --git a/custom_components/ev_smart_charging/const.py b/custom_components/ev_smart_charging/const.py index 3b21ffc..9371107 100644 --- a/custom_components/ev_smart_charging/const.py +++ b/custom_components/ev_smart_charging/const.py @@ -18,6 +18,7 @@ ICON_START = "mdi:play-circle-outline" ICON_STOP = "mdi:stop-circle-outline" ICON_TIME = "mdi:clock-time-four-outline" +ICON_TIMER = "mdi:camera-timer" # Platforms SENSOR = Platform.SENSOR @@ -35,7 +36,9 @@ # Entity keys ENTITY_KEY_CHARGING_SENSOR = "charging" +ENTITY_KEY_CHARGING_CURRENT_SENSOR = "charging_current" ENTITY_KEY_STATUS_SENSOR = "status" +ENTITY_KEY_SOLAR_STATUS_SENSOR = "solar_status" ENTITY_KEY_ACTIVE_SWITCH = "smart_charging_activated" ENTITY_KEY_APPLY_LIMIT_SWITCH = "apply_price_limit" ENTITY_KEY_CONTINUOUS_SWITCH = "continuous_charging_preferred" @@ -44,6 +47,8 @@ ENTITY_KEY_OPPORTUNISTIC_SWITCH = "opportunistic_charging" ENTITY_KEY_LOW_PRICE_CHARGING_SWITCH = "low_price_charging" ENTITY_KEY_LOW_SOC_CHARGING_SWITCH = "low_soc_charging" +ENTITY_KEY_ACTIVE_PRICE_SWITCH = "price_charging_activated" +ENTITY_KEY_ACTIVE_SOLAR_SWITCH = "solar_charging_activated" ENTITY_KEY_START_BUTTON = "manually_start_charging" ENTITY_KEY_STOP_BUTTON = "manually_stop_charging" ENTITY_KEY_CONF_PCT_PER_HOUR_NUMBER = "charging_speed" @@ -52,8 +57,14 @@ ENTITY_KEY_CONF_LOW_PRICE_CHARGING_NUMBER = "low_price_charging_level" ENTITY_KEY_CONF_LOW_SOC_CHARGING_NUMBER = "low_soc_charging_level" ENTITY_KEY_CONF_MIN_SOC_NUMBER = "minimum_ev_soc" +ENTITY_KEY_CONF_MAX_CHARGING_CURRENT_NUMBER = "max_charging_current" +ENTITY_KEY_CONF_MIN_CHARGING_CURRENT_NUMBER = "min_charging_current" +ENTITY_KEY_CONF_DEFAULT_CHARGING_CURRENT_NUMBER = "default_charging_current" +ENTITY_KEY_CONF_SOLAR_CHARGING_OFF_DELAY_NUMBER = "solar_charging_off_delay" ENTITY_KEY_CONF_START_HOUR = "charge_start_time" ENTITY_KEY_CONF_READY_HOUR = "charge_completion_time" +ENTITY_KEY_CONF_THREE_PHASE_CHARGING = "three_phase_charging" + # Configuration and options CONF_DEVICE_NAME = "device_name" @@ -62,14 +73,17 @@ CONF_EV_TARGET_SOC_SENSOR = "ev_target_soc_sensor" CONF_CHARGER_ENTITY = "charger_entity" CONF_EV_CONTROLLED = "ev_controlled" -CONF_PCT_PER_HOUR = "pct_per_hour" -CONF_START_HOUR = "start_hour" -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" +CONF_PCT_PER_HOUR = "pct_per_hour" # Config entity +CONF_START_HOUR = "start_hour" # Config entity +CONF_READY_HOUR = "ready_hour" # Config entity +CONF_MAX_PRICE = "maximum_price" # Config entity +CONF_OPPORTUNISTIC_LEVEL = "opportunistic_level" # Config entity +CONF_LOW_PRICE_CHARGING_LEVEL = "low_price_charging_level" # Config entity +CONF_LOW_SOC_CHARGING_LEVEL = "low_soc_charging_level" # Config entity +CONF_MIN_SOC = "min_soc" # Config entity +CONF_SOLAR_CHARGING_CONFIGURED = "solar_charging_configured" +CONF_GRID_USAGE_SENSOR = "grid_usage_sensor" +CONF_GRID_VOLTAGE = "grid_voltage" HOURS = [ "None", @@ -107,10 +121,16 @@ CHARGING_STATUS_CHARGING = "charging" CHARGING_STATUS_KEEP_ON = "keeping_charger_on" CHARGING_STATUS_DISCONNECTED = "disconnected" +CHARGING_STATUS_PRICE_NOT_ACTIVE = "price_not_active" CHARGING_STATUS_NOT_ACTIVE = "smart_charging_not_active" CHARGING_STATUS_LOW_PRICE_CHARGING = "low_price_charging" CHARGING_STATUS_LOW_SOC_CHARGING = "low_soc_charging" +SOLAR_CHARGING_STATUS_NOT_ACTIVATED = "not_activated" +SOLAR_CHARGING_STATUS_WAITING = "waiting" +SOLAR_CHARGING_STATUS_CHARGING = "charging" +SOLAR_CHARGING_STATUS_CHARGING_COMPLETED = "charging_completed" + # Defaults DEFAULT_NAME = DOMAIN DEFAULT_TARGET_SOC = 100 diff --git a/custom_components/ev_smart_charging/coordinator.py b/custom_components/ev_smart_charging/coordinator.py index 6d91dfb..5b6de49 100644 --- a/custom_components/ev_smart_charging/coordinator.py +++ b/custom_components/ev_smart_charging/coordinator.py @@ -47,6 +47,7 @@ class EventStateChangedData: from homeassistant.util import dt from custom_components.ev_smart_charging.helpers.price_adaptor import PriceAdaptor +from custom_components.ev_smart_charging.helpers.solar_charging import SolarCharging from .const import ( CHARGING_STATUS_CHARGING, @@ -54,12 +55,14 @@ class EventStateChangedData: CHARGING_STATUS_KEEP_ON, CHARGING_STATUS_NO_PLAN, CHARGING_STATUS_NOT_ACTIVE, + CHARGING_STATUS_PRICE_NOT_ACTIVE, CHARGING_STATUS_WAITING_CHARGING, CHARGING_STATUS_WAITING_NEW_PRICE, CHARGING_STATUS_LOW_PRICE_CHARGING, CHARGING_STATUS_LOW_SOC_CHARGING, CONF_CHARGER_ENTITY, CONF_EV_CONTROLLED, + CONF_GRID_USAGE_SENSOR, CONF_LOW_PRICE_CHARGING_LEVEL, CONF_LOW_SOC_CHARGING_LEVEL, CONF_MAX_PRICE, @@ -70,6 +73,7 @@ class EventStateChangedData: CONF_PRICE_SENSOR, CONF_EV_SOC_SENSOR, CONF_EV_TARGET_SOC_SENSOR, + CONF_SOLAR_CHARGING_CONFIGURED, CONF_START_HOUR, DEFAULT_TARGET_SOC, READY_HOUR_NONE, @@ -87,6 +91,8 @@ class EventStateChangedData: from .sensor import ( EVSmartChargingSensor, EVSmartChargingSensorCharging, + EVSmartChargingSensorChargingCurrent, + EVSmartChargingSensorSolarStatus, EVSmartChargingSensorStatus, ) @@ -106,6 +112,8 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self.sensor = None self.sensor_status = None + self.sensor_charging_current = None + self.sensor_solar_status = None self.switch_active = None self.switch_apply_limit = None self.switch_apply_limit_entity_id = None @@ -122,6 +130,9 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self.switch_opportunistic_unique_id = None self.switch_low_price_charging = None self.switch_low_soc_charging = None + self.switch_three_phase_charging = None + self.switch_active_price_charging = None + self.switch_active_solar_charging = None self.price_entity_id = None self.price_adaptor = PriceAdaptor() self.ev_soc_entity_id = None @@ -179,6 +190,10 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self.low_soc_charging = float( get_parameter(self.config_entry, CONF_LOW_SOC_CHARGING_LEVEL, 20.0) ) + self.max_charging_current = 16.0 + self.min_charging_current = 6.0 + self.default_charging_current = 16.0 + self.solar_charging_off_delay = 5.0 self.auto_charging_state = STATE_OFF self.low_price_charging_state = STATE_OFF @@ -193,9 +208,34 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, self.device_updated) ) # Update state once after intitialization - self.listeners.append( - async_call_later(hass, 10.0, self.update_initial) - ) + self.listeners.append(async_call_later(hass, 10.0, self.update_initial)) + + # Solar charging + self.solar_charging = None + self.solar_grid_usage_entity_id = None + if get_parameter(self.config_entry, CONF_SOLAR_CHARGING_CONFIGURED, False): + self.solar_charging = SolarCharging(config_entry) + self.solar_grid_usage_entity_id = get_parameter( + self.config_entry, CONF_GRID_USAGE_SENSOR + ) + if MAJOR_VERSION <= 2023 or (MAJOR_VERSION == 2024 and MINOR_VERSION <= 5): + # Use for Home Assistant 2024.5 or older + self.listeners.append( + async_track_state_change( + self.hass, + [self.solar_grid_usage_entity_id], + self.update_sensors, + ) + ) + else: + # Use for Home Assistant 2024.6 or newer + self.listeners.append( + async_track_state_change_event( + self.hass, + [self.solar_grid_usage_entity_id], + self.update_sensors_new, + ) + ) def unsubscribe_listeners(self): """Unsubscribed to listeners""" @@ -270,11 +310,13 @@ async def update_state( or self.ev_soc < self.number_min_soc ) and self.switch_ev_connected is True + and self.switch_active_price_charging is True and self.switch_active is True ) if ( self.switch_active is True + and self.switch_active_price_charging is True and self.switch_ev_connected is True and self.switch_low_price_charging is True and self.sensor.current_price is not None @@ -285,6 +327,7 @@ async def update_state( else: self.low_price_charging_state = STATE_OFF + # Perform low SOC charging even if self.switch_active_price_charging is False if ( self.switch_active is True and self.switch_ev_connected is True @@ -395,10 +438,12 @@ async def update_state( if self.sensor_status: if not self.switch_active: self.sensor_status.set_status(CHARGING_STATUS_NOT_ACTIVE) - elif not self.switch_ev_connected: - self.sensor_status.set_status(CHARGING_STATUS_DISCONNECTED) elif self.low_soc_charging_state == STATE_ON: self.sensor_status.set_status(CHARGING_STATUS_LOW_SOC_CHARGING) + elif not self.switch_active_price_charging: + self.sensor_status.set_status(CHARGING_STATUS_PRICE_NOT_ACTIVE) + elif not self.switch_ev_connected: + self.sensor_status.set_status(CHARGING_STATUS_DISCONNECTED) elif self.low_price_charging_state == STATE_ON: self.sensor_status.set_status( CHARGING_STATUS_LOW_PRICE_CHARGING @@ -421,6 +466,10 @@ async def turn_on_charging(self, state: bool = True): if state is True: _LOGGER.debug("Turn on charging") self.sensor.set_state(STATE_ON) + if self.sensor_charging_current: + self.sensor_charging_current.set_charging_current( + self.default_charging_current + ) if self.charger_switch is not None: _LOGGER.debug( "Before service call switch.turn_on: %s", self.charger_switch @@ -433,6 +482,8 @@ async def turn_on_charging(self, state: bool = True): else: _LOGGER.debug("Turn off charging") self.sensor.set_state(STATE_OFF) + if self.sensor_charging_current: + self.sensor_charging_current.set_charging_current(0) if self.charger_switch is not None: _LOGGER.debug( "Before service call switch.turn_off: %s", self.charger_switch @@ -454,6 +505,14 @@ async def add_sensor(self, sensors: list[EVSmartChargingSensor]): self.sensor = sensor if isinstance(sensor, EVSmartChargingSensorStatus): self.sensor_status = sensor + if isinstance(sensor, EVSmartChargingSensorChargingCurrent): + self.sensor_charging_current = sensor + if self.solar_charging: + self.solar_charging.set_charging_current_sensor(sensor) + if isinstance(sensor, EVSmartChargingSensorSolarStatus): + self.sensor_solar_status = sensor + if self.solar_charging: + self.solar_charging.set_solar_status_sensor(sensor) self.price_entity_id = get_parameter(self.config_entry, CONF_PRICE_SENSOR) self.price_adaptor.set_price_platform( @@ -584,12 +643,12 @@ async def switch_ev_connected_update(self, state: bool): self.switch_keep_on_completion_time = None # Make sure the charger is turned off, when connected to charger # and the car is used to start/stop charging. - if self.switch_active is True: + if self.switch_active is True and self.switch_active_price_charging is True: if get_parameter(self.config_entry, CONF_EV_CONTROLLED): self.after_ev_connected = True else: # Make sure the charger is turned off, but only if smart charging is active. - if self.switch_active is True: + if self.switch_active is True and self.switch_active is True: await self.turn_off_charging() await self.update_configuration() @@ -667,9 +726,32 @@ async def switch_low_soc_charging_update(self, state: bool): _LOGGER.debug("switch_low_soc_charging_update = %s", state) await self.update_configuration() - async def update_configuration(self): + async def switch_active_price_charging_update(self, state: bool): + """Handle the active price charging switch""" + self.switch_active_price_charging = state + _LOGGER.debug("switch_active_price_charging_update = %s", state) + await self.update_configuration() + + async def switch_active_solar_charging_update(self, state: bool): + """Handle the active solar charging switch""" + self.switch_active_solar_charging = state + _LOGGER.debug("switch_active_solar_charging_update = %s", state) + await self.update_configuration() + + async def switch_three_phase_charging_update(self, state: bool): + """Handle the three phase charging switch""" + self.switch_three_phase_charging = state + _LOGGER.debug("switch_three_phase_charging_update = %s", state) + await self.update_configuration() + + async def update_configuration( + self, default_charging_current_updated: bool = False + ): """Called when the configuration has been updated""" - await self.update_sensors(configuration_updated=True) + await self.update_sensors( + configuration_updated=True, + default_charging_current_updated=default_charging_current_updated, + ) @callback async def update_sensors_new( @@ -690,6 +772,7 @@ async def update_sensors_new( old_state=old_state, new_state=new_state, configuration_updated=configuration_updated, + default_charging_current_updated=False, ) async def update_sensors( @@ -698,6 +781,7 @@ async def update_sensors( old_state: State = None, new_state: State = None, configuration_updated: bool = False, + default_charging_current_updated: bool = False, ): # pylint: disable=unused-argument """Price or EV sensors have been updated.""" @@ -706,6 +790,36 @@ async def update_sensors( # _LOGGER.debug("old_state = %s", old_state) _LOGGER.debug("new_state = %s", new_state) + if default_charging_current_updated: + self.sensor_charging_current.set_charging_current( + self.default_charging_current + ) + + # Handle Solar Charging + if self.solar_charging: + if configuration_updated: + if self.switch_three_phase_charging: + number_of_phases = 3 + else: + number_of_phases = 1 + self.solar_charging.update_configuration( + self.switch_active, + self.switch_active_solar_charging, + self.switch_ev_connected, + number_of_phases, + self.min_charging_current, + self.max_charging_current, + self.solar_charging_off_delay, + ) + if self.solar_charging and (entity_id == self.ev_soc_entity_id): + self.solar_charging.update_ev_soc(float(new_state.state)) + if self.solar_charging and (entity_id == self.ev_target_soc_entity_id): + self.solar_charging.update_target_ev_soc(float(new_state.state)) + + if self.solar_charging and (entity_id == self.solar_grid_usage_entity_id): + self.solar_charging.update_grid_usage(float(new_state.state)) + return + # Update schedule and reset keep_on if EV SOC Target is updated if self.ev_target_soc_entity_id and (entity_id == self.ev_target_soc_entity_id): configuration_updated = True @@ -758,8 +872,10 @@ async def update_sensors( # Most likely due to the price entity updating its state in multiple steps, # resulting in invalid information before all the updates have been done. if dt.now().hour == 0 and dt.now().minute < 10: - _LOGGER.debug("Price sensor not valid directly after midnight. " \ - "Can usually be ignored.") + _LOGGER.debug( + "Price sensor not valid directly after midnight. " + "Can usually be ignored." + ) _LOGGER.debug("Price state: %s", price_state) else: _LOGGER.error("Price sensor not valid") @@ -806,7 +922,7 @@ async def update_sensors( self.start_hour_local, self.ready_hour_local ), "ready_hour": get_ready_hour_utc(self.ready_hour_local), - "switch_active": self.switch_active, + "switch_active": self.switch_active and self.switch_active_price_charging, "switch_apply_limit": self.switch_apply_limit, "switch_continuous": self.switch_continuous, "max_price": max_price, diff --git a/custom_components/ev_smart_charging/helpers/config_flow.py b/custom_components/ev_smart_charging/helpers/config_flow.py index def143b..56c4ac0 100644 --- a/custom_components/ev_smart_charging/helpers/config_flow.py +++ b/custom_components/ev_smart_charging/helpers/config_flow.py @@ -19,6 +19,7 @@ CONF_CHARGER_ENTITY, CONF_EV_SOC_SENSOR, CONF_EV_TARGET_SOC_SENSOR, + CONF_GRID_USAGE_SENSOR, DOMAIN, NAME, PLATFORM_ENERGIDATASERVICE, @@ -100,6 +101,22 @@ def validate_step_user( return None + @staticmethod + def validate_step_solar( + hass: HomeAssistant, user_input: dict[str, Any] + ) -> list[str]: + """Validate step_solar""" + + # Validate grid_usage entity + entity = hass.states.get(user_input[CONF_GRID_USAGE_SENSOR]) + if entity is None: + return ("base", "grid_usage_not_found") + if not Validator.is_float(entity.state): + _LOGGER.debug("Grid usage state is not float") + return ("base", "grid_usage_invalid_data") + + return None + class FindEntity: """Find entities""" @@ -120,9 +137,9 @@ def find_price_sensor(hass: HomeAssistant) -> str: def find_nordpool_sensor(hass: HomeAssistant) -> str: """Find Nordpool sensor""" entity_registry: EntityRegistry = async_entity_registry_get(hass) - registry_entries: UserDict[ - str, RegistryEntry - ] = entity_registry.entities.items() + registry_entries: UserDict[str, RegistryEntry] = ( + entity_registry.entities.items() + ) for entry in registry_entries: if entry[1].platform == PLATFORM_NORDPOOL: return entry[1].entity_id @@ -132,9 +149,9 @@ def find_nordpool_sensor(hass: HomeAssistant) -> str: def find_energidataservice_sensor(hass: HomeAssistant) -> str: """Find Energi Data Service sensor""" entity_registry: EntityRegistry = async_entity_registry_get(hass) - registry_entries: UserDict[ - str, RegistryEntry - ] = entity_registry.entities.items() + registry_entries: UserDict[str, RegistryEntry] = ( + entity_registry.entities.items() + ) for entry in registry_entries: if entry[1].platform == PLATFORM_ENERGIDATASERVICE: return entry[1].entity_id @@ -144,9 +161,9 @@ def find_energidataservice_sensor(hass: HomeAssistant) -> str: def find_entsoe_sensor(hass: HomeAssistant) -> str: """Search for Entso-e sensor""" entity_registry: EntityRegistry = async_entity_registry_get(hass) - registry_entries: UserDict[ - str, RegistryEntry - ] = entity_registry.entities.items() + registry_entries: UserDict[str, RegistryEntry] = ( + entity_registry.entities.items() + ) for entry in registry_entries: if entry[1].platform == PLATFORM_ENTSOE: entity_id = entry[1].entity_id @@ -158,9 +175,9 @@ def find_entsoe_sensor(hass: HomeAssistant) -> str: def find_generic_sensor(hass: HomeAssistant) -> str: """Search for generic sensor""" entity_registry: EntityRegistry = async_entity_registry_get(hass) - registry_entries: UserDict[ - str, RegistryEntry - ] = entity_registry.entities.items() + registry_entries: UserDict[str, RegistryEntry] = ( + entity_registry.entities.items() + ) for entry in registry_entries: if entry[1].platform == PLATFORM_GENERIC: entity_id = entry[1].entity_id @@ -172,9 +189,9 @@ def find_generic_sensor(hass: HomeAssistant) -> str: def find_vw_soc_sensor(hass: HomeAssistant) -> str: """Search for Volkswagen SOC sensor""" entity_registry: EntityRegistry = async_entity_registry_get(hass) - registry_entries: UserDict[ - str, RegistryEntry - ] = entity_registry.entities.items() + registry_entries: UserDict[str, RegistryEntry] = ( + entity_registry.entities.items() + ) for entry in registry_entries: if entry[1].platform == PLATFORM_VW: entity_id = entry[1].entity_id @@ -187,9 +204,9 @@ def find_vw_soc_sensor(hass: HomeAssistant) -> str: def find_vw_target_soc_sensor(hass: HomeAssistant) -> str: """Search for Volkswagen Target SOC sensor""" entity_registry: EntityRegistry = async_entity_registry_get(hass) - registry_entries: UserDict[ - str, RegistryEntry - ] = entity_registry.entities.items() + registry_entries: UserDict[str, RegistryEntry] = ( + entity_registry.entities.items() + ) for entry in registry_entries: if entry[1].platform == PLATFORM_VW: entity_id = entry[1].entity_id @@ -201,9 +218,9 @@ def find_vw_target_soc_sensor(hass: HomeAssistant) -> str: def find_ocpp_device(hass: HomeAssistant) -> str: """Find OCPP entity""" entity_registry: EntityRegistry = async_entity_registry_get(hass) - registry_entries: UserDict[ - str, RegistryEntry - ] = entity_registry.entities.items() + registry_entries: UserDict[str, RegistryEntry] = ( + entity_registry.entities.items() + ) for entry in registry_entries: if entry[1].platform == PLATFORM_OCPP: entity_id = entry[1].entity_id diff --git a/custom_components/ev_smart_charging/helpers/solar_charging.py b/custom_components/ev_smart_charging/helpers/solar_charging.py new file mode 100644 index 0000000..e166468 --- /dev/null +++ b/custom_components/ev_smart_charging/helpers/solar_charging.py @@ -0,0 +1,199 @@ +"""SolarCharging class""" + +import logging +import math + +from homeassistant.config_entries import ConfigEntry +from homeassistant.util import dt + +from custom_components.ev_smart_charging.const import ( + CHARGING_STATUS_DISCONNECTED, + CHARGING_STATUS_NOT_ACTIVE, + CONF_GRID_VOLTAGE, + SOLAR_CHARGING_STATUS_CHARGING, + SOLAR_CHARGING_STATUS_CHARGING_COMPLETED, + SOLAR_CHARGING_STATUS_NOT_ACTIVATED, + SOLAR_CHARGING_STATUS_WAITING, +) +from custom_components.ev_smart_charging.helpers.general import get_parameter +from custom_components.ev_smart_charging.sensor import ( + EVSmartChargingSensorChargingCurrent, + EVSmartChargingSensorSolarStatus, +) + + +_LOGGER = logging.getLogger(__name__) + + +class SolarCharging: + """SolarCharging class""" + + def __init__( + self, + config_entry: ConfigEntry, + ) -> None: + self.grid_usage = 0 + self.grid_usage_timestamp = dt.now().timestamp() + self.grid_voltage = float(get_parameter(config_entry, CONF_GRID_VOLTAGE)) + self.charging_activated = False + self.solar_charging_activated = False + self.ev_connected = False + self.number_of_phases = 1 + self.min_charging_current = 6 + self.max_charging_current = 16 + self.solar_charging_off_delay = 5 + self.current_charging_amps = 0 + self.low_power_timestamp = dt.now().timestamp() - 100000 # Long time ago + self.sensor_charging_current = None + self.sensor_solar_status = None + self.pacing_time = 10 + self.ev_soc = 0 + self.target_ev_soc = 100 + self.solar_charging = False + + def set_charging_current_sensor( + self, sensor_charging_current: EVSmartChargingSensorChargingCurrent + ) -> None: + """Store sensor.""" + self.sensor_charging_current = sensor_charging_current + + def set_solar_status_sensor( + self, sensor_solar_status: EVSmartChargingSensorSolarStatus + ) -> None: + """Store sensor.""" + self.sensor_solar_status = sensor_solar_status + self.sensor_solar_status.set_status(SOLAR_CHARGING_STATUS_WAITING) + + def update_solar_status(self) -> None: + """Update solar charging status""" + if self.sensor_solar_status and self.sensor_charging_current: + current_solar_status = self.sensor_solar_status.state + new_solar_status = current_solar_status + if not self.charging_activated: + new_solar_status = CHARGING_STATUS_NOT_ACTIVE + else: + if not self.solar_charging_activated: + new_solar_status = SOLAR_CHARGING_STATUS_NOT_ACTIVATED + else: + if not self.ev_connected: + new_solar_status = CHARGING_STATUS_DISCONNECTED + else: + if self.ev_soc >= self.target_ev_soc: + new_solar_status = SOLAR_CHARGING_STATUS_CHARGING_COMPLETED + else: + if self.solar_charging: + new_solar_status = SOLAR_CHARGING_STATUS_CHARGING + else: + new_solar_status = SOLAR_CHARGING_STATUS_WAITING + + if new_solar_status != current_solar_status: + if new_solar_status in [ + SOLAR_CHARGING_STATUS_NOT_ACTIVATED, + CHARGING_STATUS_DISCONNECTED, + SOLAR_CHARGING_STATUS_CHARGING_COMPLETED, + CHARGING_STATUS_NOT_ACTIVE, + ]: + new_charging_amps = 0.0 + self.sensor_charging_current.set_charging_current(new_charging_amps) + self.current_charging_amps = new_charging_amps + self.solar_charging = False + self.sensor_solar_status.set_status(new_solar_status) + + def update_configuration( + self, + charging_activated: bool, + solar_charging_activated: bool, + ev_connected: bool, + number_of_phases: int, + min_charging_current: float, + max_charging_current: float, + solar_charging_off_delay: float, + ) -> None: + """Update configuration""" + _LOGGER.debug( + "update_configuration().= %s %s %s %s %s %s %s", + str(charging_activated), + str(solar_charging_activated), + str(ev_connected), + str(number_of_phases), + str(min_charging_current), + str(max_charging_current), + str(solar_charging_off_delay), + ) + self.charging_activated = charging_activated + self.solar_charging_activated = solar_charging_activated + self.ev_connected = ev_connected + self.number_of_phases = number_of_phases + self.min_charging_current = min_charging_current + self.max_charging_current = max_charging_current + self.solar_charging_off_delay = solar_charging_off_delay + self.update_solar_status() + + def update_ev_soc(self, ev_soc: float) -> None: + """Update EV SOC""" + self.ev_soc = ev_soc + self.update_solar_status() + + def update_target_ev_soc(self, target_ev_soc: float) -> None: + """Update target EV SOC""" + self.target_ev_soc = target_ev_soc + self.update_solar_status() + + def update_grid_usage(self, grid_usage: float) -> None: + """New value of grid usage received""" + timestamp = dt.now().timestamp() + # Don't update charging current more than once per 10 seconds + if ( + self.charging_activated + and self.solar_charging_activated + and self.ev_connected + and self.sensor_charging_current + and self.sensor_solar_status + and (timestamp - self.grid_usage_timestamp) >= self.pacing_time + and self.ev_soc < self.target_ev_soc + ): + + self.pacing_time = 10 + self.grid_usage_timestamp = timestamp + self.grid_usage = grid_usage + + available_amps = ( + -self.grid_usage / self.grid_voltage + ) / self.number_of_phases + proposed_charging_amps = available_amps + self.current_charging_amps + new_charging_amps = math.floor( + min( + max(proposed_charging_amps, self.min_charging_current), + self.max_charging_current, + ) + ) + + if proposed_charging_amps >= self.min_charging_current: + self.low_power_timestamp = None + + if proposed_charging_amps < self.min_charging_current: + timestamp = dt.now().timestamp() + if not self.low_power_timestamp: + self.low_power_timestamp = timestamp + if (timestamp - self.low_power_timestamp) > ( + 60 * self.solar_charging_off_delay + ): + # Too low solar power for too long time + _LOGGER.debug("Too low solar power for too long time.") + new_charging_amps = 0 + + if new_charging_amps != self.current_charging_amps: + _LOGGER.debug( + "set_charging_current(new_charging_amps) = %s", + new_charging_amps, + ) + self.sensor_charging_current.set_charging_current(new_charging_amps) + if new_charging_amps == 0: + self.solar_charging = False + else: + self.solar_charging = True + if self.current_charging_amps == 0: + # Wait longer to update after turning on charger. + self.pacing_time = 30 + self.current_charging_amps = new_charging_amps + self.update_solar_status() diff --git a/custom_components/ev_smart_charging/number.py b/custom_components/ev_smart_charging/number.py index 7fda539..50e1f04 100644 --- a/custom_components/ev_smart_charging/number.py +++ b/custom_components/ev_smart_charging/number.py @@ -1,4 +1,5 @@ """Number platform for EV Smart Charging.""" + import logging from typing import Union @@ -19,12 +20,17 @@ DOMAIN, ENTITY_KEY_CONF_LOW_PRICE_CHARGING_NUMBER, ENTITY_KEY_CONF_LOW_SOC_CHARGING_NUMBER, + ENTITY_KEY_CONF_MAX_CHARGING_CURRENT_NUMBER, + ENTITY_KEY_CONF_MIN_CHARGING_CURRENT_NUMBER, + ENTITY_KEY_CONF_DEFAULT_CHARGING_CURRENT_NUMBER, ENTITY_KEY_CONF_OPPORTUNISTIC_LEVEL_NUMBER, ENTITY_KEY_CONF_PCT_PER_HOUR_NUMBER, ENTITY_KEY_CONF_MAX_PRICE_NUMBER, ENTITY_KEY_CONF_MIN_SOC_NUMBER, + ENTITY_KEY_CONF_SOLAR_CHARGING_OFF_DELAY_NUMBER, ICON_BATTERY_50, ICON_CASH, + ICON_TIMER, NUMBER, ) from .coordinator import EVSmartChargingCoordinator @@ -47,9 +53,14 @@ async def async_setup_entry( numbers.append(EVSmartChargingNumberOpportunistic(entry, coordinator)) numbers.append(EVSmartChargingNumberLowPriceCharging(entry, coordinator)) numbers.append(EVSmartChargingNumberLowSocCharging(entry, coordinator)) + numbers.append(EVSmartChargingNumberMaxChargingCurrent(entry, coordinator)) + numbers.append(EVSmartChargingNumberMinChargingCurrent(entry, coordinator)) + numbers.append(EVSmartChargingNumberDefaultChargingCurrent(entry, coordinator)) + numbers.append(EVSmartChargingNumberSolarChargingOffDelay(entry, coordinator)) async_add_devices(numbers) +# pylint: disable=abstract-method class EVSmartChargingNumber(EVSmartChargingEntity, RestoreNumber): """EV Smart Charging number class.""" @@ -234,3 +245,105 @@ async def async_set_native_value(self, value: float) -> None: await super().async_set_native_value(value) self.coordinator.low_soc_charging = value await self.coordinator.update_configuration() + + +class EVSmartChargingNumberMaxChargingCurrent(EVSmartChargingNumber): + """EV Smart Charging maximum charging current number class.""" + + _entity_key = ENTITY_KEY_CONF_MAX_CHARGING_CURRENT_NUMBER + # _attr_icon = ICON_BATTERY_50 + _attr_entity_category = EntityCategory.CONFIG + _attr_native_min_value = 1.0 + _attr_native_max_value = 32.0 + _attr_native_step = 1.0 + _attr_native_unit_of_measurement = "A" + + def __init__(self, entry, coordinator: EVSmartChargingCoordinator): + _LOGGER.debug("EVSmartChargingNumberMaxChargingCurrent.__init__()") + super().__init__(entry, coordinator) + if self.value is None: + self._attr_native_value = 16.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.max_charging_current = value + await self.coordinator.update_configuration() + + +class EVSmartChargingNumberMinChargingCurrent(EVSmartChargingNumber): + """EV Smart Charging minimum charging current number class.""" + + _entity_key = ENTITY_KEY_CONF_MIN_CHARGING_CURRENT_NUMBER + # _attr_icon = ICON_BATTERY_50 + _attr_entity_category = EntityCategory.CONFIG + _attr_native_min_value = 1.0 + _attr_native_max_value = 32.0 + _attr_native_step = 1.0 + _attr_native_unit_of_measurement = "A" + + def __init__(self, entry, coordinator: EVSmartChargingCoordinator): + _LOGGER.debug("EVSmartChargingNumberMinChargingCurrent.__init__()") + super().__init__(entry, coordinator) + if self.value is None: + self._attr_native_value = 6.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.min_charging_current = value + await self.coordinator.update_configuration() + + +class EVSmartChargingNumberDefaultChargingCurrent(EVSmartChargingNumber): + """EV Smart Charging default charging current number class.""" + + _entity_key = ENTITY_KEY_CONF_DEFAULT_CHARGING_CURRENT_NUMBER + # _attr_icon = ICON_BATTERY_50 + _attr_entity_category = EntityCategory.CONFIG + _attr_native_min_value = 1.0 + _attr_native_max_value = 32.0 + _attr_native_step = 1.0 + _attr_native_unit_of_measurement = "A" + + def __init__(self, entry, coordinator: EVSmartChargingCoordinator): + _LOGGER.debug("EVSmartChargingNumberDefaultChargingCurrent.__init__()") + super().__init__(entry, coordinator) + if self.value is None: + self._attr_native_value = 16.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.default_charging_current = value + await self.coordinator.update_configuration( + default_charging_current_updated=True + ) + + +class EVSmartChargingNumberSolarChargingOffDelay(EVSmartChargingNumber): + """EV Smart Charging Solar Charging Off Delay number class.""" + + _entity_key = ENTITY_KEY_CONF_SOLAR_CHARGING_OFF_DELAY_NUMBER + _attr_icon = ICON_TIMER + _attr_entity_category = EntityCategory.CONFIG + _attr_native_min_value = 1.0 + _attr_native_max_value = 60.0 + _attr_native_step = 1.0 + _attr_native_unit_of_measurement = "minutes" + + def __init__(self, entry, coordinator: EVSmartChargingCoordinator): + _LOGGER.debug("EVSmartChargingNumberSolarChargingOffDelay.__init__()") + super().__init__(entry, coordinator) + if self.value is None: + self._attr_native_value = 5.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.solar_charging_off_delay = value + await self.coordinator.update_configuration() diff --git a/custom_components/ev_smart_charging/sensor.py b/custom_components/ev_smart_charging/sensor.py index 612464e..53d9db3 100644 --- a/custom_components/ev_smart_charging/sensor.py +++ b/custom_components/ev_smart_charging/sensor.py @@ -1,14 +1,17 @@ """Sensor platform for EV Smart Charging.""" + import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorDeviceClass from homeassistant.core import HomeAssistant from homeassistant.const import STATE_OFF from .const import ( DOMAIN, + ENTITY_KEY_CHARGING_CURRENT_SENSOR, ENTITY_KEY_CHARGING_SENSOR, + ENTITY_KEY_SOLAR_STATUS_SENSOR, ENTITY_KEY_STATUS_SENSOR, SENSOR, ) @@ -24,6 +27,8 @@ async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): sensors = [] sensors.append(EVSmartChargingSensorCharging(entry)) sensors.append(EVSmartChargingSensorStatus(entry)) + sensors.append(EVSmartChargingSensorChargingCurrent(entry)) + sensors.append(EVSmartChargingSensorSolarStatus(entry)) async_add_devices(sensors) await coordinator.add_sensor(sensors) @@ -38,6 +43,7 @@ def __init__(self, entry): self._attr_unique_id = ".".join([entry.entry_id, SENSOR, id_name]) self.set_entity_id(SENSOR, self._entity_key) + class EVSmartChargingSensorCharging(EVSmartChargingSensor): """EV Smart Charging sensor class.""" @@ -185,3 +191,36 @@ def set_status(self, new_status): """Set new status.""" self._attr_native_value = new_status self.update_ha_state() + + +class EVSmartChargingSensorChargingCurrent(EVSmartChargingSensor): + """EV Smart Charging sensor class.""" + + _entity_key = ENTITY_KEY_CHARGING_CURRENT_SENSOR + _attr_device_class = SensorDeviceClass.CURRENT + _attr_native_unit_of_measurement = "A" + + def __init__(self, entry): + _LOGGER.debug("EVSmartChargingSensorChargingCurrent.__init__()") + self._attr_native_value = 0 + super().__init__(entry) + + def set_charging_current(self, new_status): + """Set new current.""" + self._attr_native_value = new_status + self.update_ha_state() + + +class EVSmartChargingSensorSolarStatus(EVSmartChargingSensor): + """EV Smart Charging sensor class.""" + + _entity_key = ENTITY_KEY_SOLAR_STATUS_SENSOR + + def __init__(self, entry): + _LOGGER.debug("EVSmartChargingSensorSolarStatus.__init__()") + super().__init__(entry) + + def set_status(self, new_status): + """Set new status.""" + self._attr_native_value = new_status + self.update_ha_state() diff --git a/custom_components/ev_smart_charging/switch.py b/custom_components/ev_smart_charging/switch.py index e7c70d4..d008a7c 100644 --- a/custom_components/ev_smart_charging/switch.py +++ b/custom_components/ev_smart_charging/switch.py @@ -1,16 +1,21 @@ """Switch platform for EV Smart Charging.""" + import logging from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.restore_state import RestoreEntity from .const import ( DOMAIN, + ENTITY_KEY_ACTIVE_PRICE_SWITCH, + ENTITY_KEY_ACTIVE_SOLAR_SWITCH, ENTITY_KEY_ACTIVE_SWITCH, ENTITY_KEY_APPLY_LIMIT_SWITCH, + ENTITY_KEY_CONF_THREE_PHASE_CHARGING, ENTITY_KEY_CONTINUOUS_SWITCH, ENTITY_KEY_EV_CONNECTED_SWITCH, ENTITY_KEY_LOW_PRICE_CHARGING_SWITCH, @@ -41,9 +46,13 @@ async def async_setup_entry( switches.append(EVSmartChargingSwitchOpportunistic(entry, coordinator)) switches.append(EVSmartChargingSwitchLowPriceCharging(entry, coordinator)) switches.append(EVSmartChargingSwitchLowSocCharging(entry, coordinator)) + switches.append(EVSmartChargingSwitchThreePhaseCharging(entry, coordinator)) + switches.append(EVSmartChargingSwitchActivePriceCharging(entry, coordinator)) + switches.append(EVSmartChargingSwitchActiveSolarCharging(entry, coordinator)) async_add_devices(switches) +# pylint: disable=abstract-method class EVSmartChargingSwitch(EVSmartChargingEntity, SwitchEntity, RestoreEntity): """EV Smart Charging switch class.""" @@ -267,3 +276,76 @@ async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await super().async_turn_off(**kwargs) await self.coordinator.switch_low_soc_charging_update(False) + + +class EVSmartChargingSwitchThreePhaseCharging(EVSmartChargingSwitch): + """EV Smart Charging three phase charging switch class.""" + + _entity_key = ENTITY_KEY_CONF_THREE_PHASE_CHARGING + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, entry, coordinator: EVSmartChargingCoordinator): + _LOGGER.debug("EVSmartChargingSwitchThreePhaseCharging.__init__()") + super().__init__(entry, coordinator) + if self.is_on is None: + self._attr_is_on = False + self.update_ha_state() + self.coordinator.switch_three_phase_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_three_phase_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_three_phase_charging_update(False) + + +class EVSmartChargingSwitchActivePriceCharging(EVSmartChargingSwitch): + """EV Smart Charging active price charging switch class.""" + + _entity_key = ENTITY_KEY_ACTIVE_PRICE_SWITCH + + def __init__(self, entry, coordinator: EVSmartChargingCoordinator): + _LOGGER.debug("EVSmartChargingSwitchActivePriceCharging.__init__()") + super().__init__(entry, coordinator) + if self.is_on is None: + self._attr_is_on = True + self.update_ha_state() + self.coordinator.switch_active_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_active_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_active_price_charging_update(False) + + +class EVSmartChargingSwitchActiveSolarCharging(EVSmartChargingSwitch): + """EV Smart Charging active solar charging switch class.""" + + _entity_key = ENTITY_KEY_ACTIVE_SOLAR_SWITCH + + def __init__(self, entry, coordinator: EVSmartChargingCoordinator): + _LOGGER.debug("EVSmartChargingSwitchActiveSolarCharging.__init__()") + super().__init__(entry, coordinator) + if self.is_on is None: + self._attr_is_on = False + self.update_ha_state() + self.coordinator.switch_active_solar_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_active_solar_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_active_solar_charging_update(False) diff --git a/custom_components/ev_smart_charging/translations/en.json b/custom_components/ev_smart_charging/translations/en.json index a167a34..86c54c7 100644 --- a/custom_components/ev_smart_charging/translations/en.json +++ b/custom_components/ev_smart_charging/translations/en.json @@ -9,7 +9,20 @@ "ev_soc_sensor": "EV SOC entity", "ev_target_soc_sensor": "EV Target SOC entity (single space to remove)", "charger_entity": "Charger control switch (single space to remove)", - "ev_controlled": "An EV integration will control start/stop of charging" + "ev_controlled": "An EV integration will control start/stop of charging", + "solar_charging_configured": "Configure charging using solar power" + } + }, + "solar": { + "description": "Configuration of charging using solar power.", + "data": { + "grid_usage_sensor": "Grid usage sensor [W]", + "grid_voltage": "Grid voltage [V]", + "max_charging_current": "Maximum charging current [A]", + "min_charging_current": "Minimum charging current [A]", + "default_charging_current": "Default (non-solar) charging current [A]", + "three_phase_charging": "Three-phase charging", + "solar_charging_off_delay": "Time delay for ending solar power based charging" } } }, @@ -22,7 +35,9 @@ "ev_target_soc_not_found": "EV Target SOC entity not found.", "ev_target_soc_invalid_data": "The Target SOC entity gives invalid data.", "charger_control_switch_not_found": "Charger control switch entity not found.", - "charger_control_switch_not_switch": "Charger control switch entity is not a switch." + "charger_control_switch_not_switch": "Charger control switch entity is not a switch.", + "grid_usage_not_found": "Grid usage sensor not found.", + "grid_usage_invalid_data": "Grid usage entity gives invalid data." } }, "options": { @@ -34,7 +49,20 @@ "ev_soc_sensor": "EV SOC entity", "ev_target_soc_sensor": "EV Target SOC entity (single space to remove)", "charger_entity": "Charger control switch (single space to remove)", - "ev_controlled": "An EV integration will control start/stop of charging" + "ev_controlled": "An EV integration will control start/stop of charging", + "solar_charging_configured": "Configure charging using solar power" + } + }, + "solar": { + "description": "Configuration of charging using solar power.", + "data": { + "grid_usage_sensor": "Grid usage sensor [W]", + "grid_voltage": "Grid voltage [V]", + "max_charging_current": "Maximum charging current [A]", + "min_charging_current": "Minimum charging current [A]", + "default_charging_current": "Default (non-solar) charging current [A]", + "three_phase_charging": "Three-phase charging", + "solar_charging_off_delay": "Time delay for ending solar power based charging" } } }, @@ -47,7 +75,9 @@ "ev_target_soc_not_found": "EV Target SOC entity not found.", "ev_target_soc_invalid_data": "The Target SOC entity gives invalid data.", "charger_control_switch_not_found": "Charger control switch entity not found.", - "charger_control_switch_not_switch": "Charger control switch entity is not a switch." + "charger_control_switch_not_switch": "Charger control switch entity is not a switch.", + "grid_usage_not_found": "Grid usage sensor not found.", + "grid_usage_invalid_data": "Grid usage entity gives invalid data." } }, "entity": { @@ -61,7 +91,11 @@ "opportunistic_level": { "name": "Opportunistic level" }, "low_price_charging_level": { "name": "Low price charging level" }, "low_soc_charging_level": { "name": "Low SOC charging level" }, - "minimum_ev_soc": { "name": "Minimum EV SOC" } + "minimum_ev_soc": { "name": "Minimum EV SOC" }, + "max_charging_current": { "name": "Maximum charging current" }, + "min_charging_current": { "name": "Minimum charging current" }, + "default_charging_current": { "name": "Default charging current" }, + "solar_charging_off_delay": { "name": "Solar charging off delay" } }, "select": { "charge_start_time": { "name": "Charge start time" }, @@ -82,6 +116,9 @@ "charging_schedule": {"name": "Charging schedule"} } }, + "charging_current": { + "name": "Charging current" + }, "status": { "name": "Status", "state": { @@ -91,11 +128,24 @@ "charging": "Charging", "keeping_charger_on": "Keeping charger on", "disconnected": "Disconnected", + "price_not_active": "Price based charging not active", "smart_charging_not_active": "Smart charging not active", "low_price_charging": "Low price charging", "low_soc_charging": "Low SOC charging" } + }, + "solar_status": { + "name": "Solar charging status", + "state": { + "smart_charging_not_active": "Smart charging not active", + "not_activated": "Not activated", + "disconnected": "Disconnected", + "waiting": "Waiting for solar power", + "charging": "Charging with solar power", + "charging_completed": "Charging completed" + } } + }, "switch": { "smart_charging_activated": { "name": "Smart charging activated" }, @@ -105,7 +155,10 @@ "keep_charger_on": { "name": "Keep charger on" }, "opportunistic_charging": { "name": "Opportunistic charging" }, "low_price_charging": { "name": "Low price charging" }, - "low_soc_charging": { "name": "Low SOC charging" } + "low_soc_charging": { "name": "Low SOC charging" }, + "three_phase_charging": { "name": "Three phase charging"}, + "price_charging_activated": { "name": "Price charging activated"}, + "solar_charging_activated": { "name": "Solar charging activated"} } } } \ No newline at end of file diff --git a/tests/const.py b/tests/const.py index 2ceaa6c..d9ade89 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,4 +1,5 @@ """Constants for ev_smart_charging tests.""" + from custom_components.ev_smart_charging.const import ( CONF_CHARGER_ENTITY, CONF_DEVICE_NAME, @@ -13,6 +14,7 @@ CONF_EV_TARGET_SOC_SENSOR, CONF_PCT_PER_HOUR, CONF_READY_HOUR, + CONF_SOLAR_CHARGING_CONFIGURED, CONF_START_HOUR, NAME, ) @@ -24,6 +26,7 @@ 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_SOLAR_CHARGING_CONFIGURED: False, } MOCK_CONFIG_USER_WRONG_PRICE = { @@ -70,6 +73,7 @@ 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_SOLAR_CHARGING_CONFIGURED: False, } MOCK_CONFIG_CHARGER_NEW = { diff --git a/tests/coordinator/test_coordinator.py b/tests/coordinator/test_coordinator.py index e76f349..aad55f0 100644 --- a/tests/coordinator/test_coordinator.py +++ b/tests/coordinator/test_coordinator.py @@ -1,4 +1,5 @@ """Test ev_smart_charging coordinator.""" + from datetime import datetime from pytest_homeassistant_custom_component.common import MockConfigEntry @@ -80,6 +81,7 @@ async def test_coordinator( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -193,6 +195,7 @@ async def test_coordinator_min_soc1( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(True) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -258,6 +261,7 @@ async def test_coordinator_min_soc2( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(True) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -372,6 +376,7 @@ async def test_coordinator_fix_soc( # Turn on switches. Should give 2h schedule 05:00-07:00 await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) diff --git a/tests/coordinator/test_coordinator_keep_on.py b/tests/coordinator/test_coordinator_keep_on.py index 36aa5d0..c9f194b 100644 --- a/tests/coordinator/test_coordinator_keep_on.py +++ b/tests/coordinator/test_coordinator_keep_on.py @@ -62,6 +62,7 @@ async def test_coordinator_keep_on1( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -191,6 +192,7 @@ async def test_coordinator_keep_on2( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -266,6 +268,7 @@ async def test_coordinator_keep_on3( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(False) @@ -372,6 +375,7 @@ async def test_coordinator_keep_on4( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -494,6 +498,7 @@ async def test_coordinator_keep_on4b( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -596,6 +601,7 @@ async def test_coordinator_keep_on5( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(False) await coordinator.switch_ev_connected_update(True) @@ -726,6 +732,7 @@ async def test_coordinator_keep_on6( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(False) await coordinator.switch_ev_connected_update(True) @@ -812,6 +819,7 @@ async def test_coordinator_keep_on6b( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(False) await coordinator.switch_ev_connected_update(True) diff --git a/tests/coordinator/test_coordinator_keep_on2.py b/tests/coordinator/test_coordinator_keep_on2.py index a12aca5..7f84358 100644 --- a/tests/coordinator/test_coordinator_keep_on2.py +++ b/tests/coordinator/test_coordinator_keep_on2.py @@ -69,6 +69,7 @@ async def test_coordinator_keep_on_get_entities( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(False) await coordinator.switch_ev_connected_update(True) @@ -138,6 +139,7 @@ async def test_coordinator_keep_on_schedule( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(False) await coordinator.switch_ev_connected_update(True) @@ -240,6 +242,7 @@ async def test_coordinator_keep_on_connect( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(False) await coordinator.switch_ev_connected_update(True) @@ -398,6 +401,7 @@ async def test_coordinator_keep_on_connect2( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(False) await coordinator.switch_ev_connected_update(True) # Set to True and don't change @@ -517,6 +521,7 @@ async def test_coordinator_keep_on_connect3( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(False) await coordinator.switch_ev_connected_update(True) diff --git a/tests/coordinator/test_coordinator_keep_on3.py b/tests/coordinator/test_coordinator_keep_on3.py index 1a0c823..e63cd16 100644 --- a/tests/coordinator/test_coordinator_keep_on3.py +++ b/tests/coordinator/test_coordinator_keep_on3.py @@ -70,6 +70,7 @@ async def test_coordinator_keep_on_issue( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) diff --git a/tests/coordinator/test_coordinator_late_ready.py b/tests/coordinator/test_coordinator_late_ready.py index 4fb5012..37bd8da 100644 --- a/tests/coordinator/test_coordinator_late_ready.py +++ b/tests/coordinator/test_coordinator_late_ready.py @@ -1,4 +1,5 @@ """Test ev_smart_charging coordinator.""" + from datetime import datetime from pytest_homeassistant_custom_component.common import MockConfigEntry @@ -59,6 +60,7 @@ async def test_coordinator_late_ready( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -241,6 +243,7 @@ async def test_coordinator_late_ready2( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -311,6 +314,7 @@ async def test_coordinator_late_ready3( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) diff --git a/tests/coordinator/test_coordinator_low_price_charging.py b/tests/coordinator/test_coordinator_low_price_charging.py index 683e17f..1bda06e 100644 --- a/tests/coordinator/test_coordinator_low_price_charging.py +++ b/tests/coordinator/test_coordinator_low_price_charging.py @@ -66,6 +66,7 @@ async def test_coordinator_low_price_charging1( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) diff --git a/tests/coordinator/test_coordinator_low_soc_charging.py b/tests/coordinator/test_coordinator_low_soc_charging.py index 58d5f0b..933eff0 100644 --- a/tests/coordinator/test_coordinator_low_soc_charging.py +++ b/tests/coordinator/test_coordinator_low_soc_charging.py @@ -66,6 +66,7 @@ async def test_coordinator_low_soc_charging1( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(False) diff --git a/tests/coordinator/test_coordinator_negative_price.py b/tests/coordinator/test_coordinator_negative_price.py index af5a2b7..def45ed 100644 --- a/tests/coordinator/test_coordinator_negative_price.py +++ b/tests/coordinator/test_coordinator_negative_price.py @@ -62,6 +62,7 @@ async def test_coordinator_negative_price_5( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(True) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -114,6 +115,7 @@ async def test_coordinator_negative_price_0( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(True) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) diff --git a/tests/coordinator/test_coordinator_no_ready.py b/tests/coordinator/test_coordinator_no_ready.py index 81369aa..c457339 100644 --- a/tests/coordinator/test_coordinator_no_ready.py +++ b/tests/coordinator/test_coordinator_no_ready.py @@ -1,4 +1,5 @@ """Test ev_smart_charging coordinator.""" + from datetime import datetime from pytest_homeassistant_custom_component.common import MockConfigEntry @@ -59,6 +60,7 @@ async def test_coordinator_no_ready( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -144,6 +146,7 @@ async def test_coordinator_no_ready2( # Turn on switches await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) diff --git a/tests/coordinator/test_coordinator_reschedule.py b/tests/coordinator/test_coordinator_reschedule.py index b9eaab8..44c8b0b 100644 --- a/tests/coordinator/test_coordinator_reschedule.py +++ b/tests/coordinator/test_coordinator_reschedule.py @@ -1,4 +1,5 @@ """Test ev_smart_charging coordinator.""" + from datetime import datetime import asyncio from pytest_homeassistant_custom_component.common import MockConfigEntry @@ -55,6 +56,7 @@ async def test_coordinator_reschedule( coordinator.ready_hour_local = 8 await hass.async_block_till_done() await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) diff --git a/tests/coordinator/test_coordinator_start_hour_both_same.py b/tests/coordinator/test_coordinator_start_hour_both_same.py index 761bc9f..3596cb0 100644 --- a/tests/coordinator/test_coordinator_start_hour_both_same.py +++ b/tests/coordinator/test_coordinator_start_hour_both_same.py @@ -1,4 +1,5 @@ """Test ev_smart_charging coordinator.""" + from datetime import datetime from pytest_homeassistant_custom_component.common import MockConfigEntry @@ -61,6 +62,7 @@ async def test_coordinator_start_hour_both_same_3a( await coordinator.add_sensor([sensor]) await hass.async_block_till_done() await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -151,6 +153,7 @@ async def test_coordinator_start_hour_both_same_3b( await coordinator.add_sensor([sensor]) await hass.async_block_till_done() await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) diff --git a/tests/coordinator/test_coordinator_start_hour_end_before_start.py b/tests/coordinator/test_coordinator_start_hour_end_before_start.py index 36d007b..501221d 100644 --- a/tests/coordinator/test_coordinator_start_hour_end_before_start.py +++ b/tests/coordinator/test_coordinator_start_hour_end_before_start.py @@ -1,4 +1,5 @@ """Test ev_smart_charging coordinator.""" + from datetime import datetime from pytest_homeassistant_custom_component.common import MockConfigEntry @@ -64,6 +65,7 @@ async def test_coordinator_start_hour_end_before_start_2a( await coordinator.add_sensor([sensor]) await hass.async_block_till_done() await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -175,6 +177,7 @@ async def test_coordinator_start_hour_end_before_start_2b( await coordinator.add_sensor([sensor]) await hass.async_block_till_done() await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -286,6 +289,7 @@ async def test_coordinator_start_hour_end_before_start_2c( await coordinator.add_sensor([sensor]) await hass.async_block_till_done() await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) diff --git a/tests/coordinator/test_coordinator_start_hour_only_start.py b/tests/coordinator/test_coordinator_start_hour_only_start.py index 8168413..e58e525 100644 --- a/tests/coordinator/test_coordinator_start_hour_only_start.py +++ b/tests/coordinator/test_coordinator_start_hour_only_start.py @@ -1,4 +1,5 @@ """Test ev_smart_charging coordinator.""" + from datetime import datetime from pytest_homeassistant_custom_component.common import MockConfigEntry @@ -61,6 +62,7 @@ async def test_coordinator_start_hour_only_start_4a( await coordinator.add_sensor([sensor]) await hass.async_block_till_done() await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -155,6 +157,7 @@ async def test_coordinator_start_hour_only_start_4b( await coordinator.add_sensor([sensor]) await hass.async_block_till_done() await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) diff --git a/tests/coordinator/test_coordinator_start_hour_start_before_end.py b/tests/coordinator/test_coordinator_start_hour_start_before_end.py index 9a66fc6..af8cc09 100644 --- a/tests/coordinator/test_coordinator_start_hour_start_before_end.py +++ b/tests/coordinator/test_coordinator_start_hour_start_before_end.py @@ -1,4 +1,5 @@ """Test ev_smart_charging coordinator.""" + from datetime import datetime from pytest_homeassistant_custom_component.common import MockConfigEntry @@ -64,6 +65,7 @@ async def test_coordinator_start_hour_start_before_end_1a( await coordinator.add_sensor([sensor]) await hass.async_block_till_done() await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -175,6 +177,7 @@ async def test_coordinator_start_hour_start_before_end_1b( await coordinator.add_sensor([sensor]) await hass.async_block_till_done() await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) @@ -290,6 +293,7 @@ async def test_coordinator_start_hour_start_before_end_1c( await coordinator.add_sensor([sensor]) await hass.async_block_till_done() await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(True) diff --git a/tests/coordinator/test_coordinator_status.py b/tests/coordinator/test_coordinator_status.py index 3a3d670..87368aa 100644 --- a/tests/coordinator/test_coordinator_status.py +++ b/tests/coordinator/test_coordinator_status.py @@ -71,6 +71,7 @@ async def test_coordinator_status( await coordinator.add_sensor(sensors) await hass.async_block_till_done() await coordinator.switch_active_update(False) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) await coordinator.switch_ev_connected_update(False) diff --git a/tests/test_daylight_saving.py b/tests/test_daylight_saving.py index 8b45398..fa61c05 100644 --- a/tests/test_daylight_saving.py +++ b/tests/test_daylight_saving.py @@ -1,4 +1,5 @@ """Test ev_smart_charging coordinator.""" + from datetime import datetime from pytest_homeassistant_custom_component.common import MockConfigEntry @@ -54,6 +55,7 @@ async def test_to_daylight_saving_time( sensor: EVSmartChargingSensorCharging = EVSmartChargingSensorCharging(config_entry) assert sensor is not None await coordinator.add_sensor([sensor]) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_active_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True) @@ -181,6 +183,7 @@ async def test_from_daylight_saving_time( assert sensor is not None await coordinator.add_sensor([sensor]) await coordinator.switch_active_update(True) + await coordinator.switch_active_price_charging_update(True) await coordinator.switch_apply_limit_update(False) await coordinator.switch_continuous_update(True)