Skip to content

Commit

Permalink
Solar power based charging.
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasbkarlsson authored Aug 31, 2024
1 parent 7b5face commit 1a1c573
Show file tree
Hide file tree
Showing 27 changed files with 872 additions and 61 deletions.
16 changes: 14 additions & 2 deletions custom_components/ev_smart_charging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down
128 changes: 117 additions & 11 deletions custom_components/ev_smart_charging/config_flow.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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,
)

Expand All @@ -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."""
Expand All @@ -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(
Expand All @@ -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,
)
36 changes: 28 additions & 8 deletions custom_components/ev_smart_charging/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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",
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 1a1c573

Please sign in to comment.