Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The integration calculates the set of 15-minute intervals that will give the low
- 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 configuration parameters that affect charging.
- Automatically detects and connects to the integrations [Volkswagen We Connect ID](https://github.com/mitch-dc/volkswagen_we_connect_id) and [OCPP](https://github.com/lbbrhzn/ocpp). Connections to other EV and charger integrations can be configured manually.
- Automatically detects and connects to the integrations [Volkswagen We Connect ID](https://github.com/mitch-dc/volkswagen_we_connect_id), [Tesla Fleet](https://www.home-assistant.io/integrations/tesla_fleet/) and [OCPP](https://github.com/lbbrhzn/ocpp). Connections to other EV and charger integrations can be configured manually.

## Installation

Expand Down
4 changes: 2 additions & 2 deletions custom_components/ev_smart_charging/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ async def async_step_user(self, user_input=None):
# Provide defaults for form
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_SOC_SENSOR] = FindEntity.find_ev_soc_sensor(self.hass)
user_input[
CONF_EV_TARGET_SOC_SENSOR
] = FindEntity.find_vw_target_soc_sensor(self.hass)
] = FindEntity.find_ev_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
Expand Down
1 change: 1 addition & 0 deletions custom_components/ev_smart_charging/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
PLATFORM_TGE = "tge"
PLATFORM_GESPOT = "ge_spot"
PLATFORM_VW = "volkswagen_we_connect_id"
PLATFORM_TESLA_FLEET = "tesla_fleet"
PLATFORM_OCPP = "ocpp"
PLATFORM_GENERIC = "generic"

Expand Down
50 changes: 50 additions & 0 deletions custom_components/ev_smart_charging/helpers/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
PLATFORM_NORDPOOL,
PLATFORM_TGE,
PLATFORM_OCPP,
PLATFORM_TESLA_FLEET,
PLATFORM_VW,
SWITCH,
)
Expand Down Expand Up @@ -227,6 +228,35 @@ def find_vw_target_soc_sensor(hass: HomeAssistant) -> str:
return entity_id
return ""

@staticmethod
def find_tesla_fleet_soc_sensor(hass: HomeAssistant) -> str:
"""Search for Tesla Fleet SOC sensor"""
entity_registry: EntityRegistry = async_entity_registry_get(hass)
registry_entries: UserDict[str, RegistryEntry] = (
entity_registry.entities.items()
)
for entry in registry_entries:
if entry[1].platform == PLATFORM_TESLA_FLEET:
entity_id = entry[1].entity_id
if "battery_level" in entity_id:
if "usable" not in entity_id:
return entity_id
return ""

@staticmethod
def find_tesla_fleet_target_soc_sensor(hass: HomeAssistant) -> str:
"""Search for Tesla Fleet Target SOC (Charge Limit) sensor"""
entity_registry: EntityRegistry = async_entity_registry_get(hass)
registry_entries: UserDict[str, RegistryEntry] = (
entity_registry.entities.items()
)
for entry in registry_entries:
if entry[1].platform == PLATFORM_TESLA_FLEET:
entity_id = entry[1].entity_id
if "charge_limit" in entity_id:
return entity_id
return ""

@staticmethod
def find_ocpp_device(hass: HomeAssistant) -> str:
"""Find OCPP entity"""
Expand All @@ -242,6 +272,26 @@ def find_ocpp_device(hass: HomeAssistant) -> str:
return entity_id
return ""

@staticmethod
def find_ev_soc_sensor(hass: HomeAssistant) -> str:
"""Search for EV SOC sensor across supported platforms"""
sensor = ""
if len(sensor) == 0:
sensor = FindEntity.find_tesla_fleet_soc_sensor(hass)
if len(sensor) == 0:
sensor = FindEntity.find_vw_soc_sensor(hass)
return sensor

@staticmethod
def find_ev_target_soc_sensor(hass: HomeAssistant) -> str:
"""Search for EV Target SOC sensor across supported platforms"""
sensor = ""
if len(sensor) == 0:
sensor = FindEntity.find_tesla_fleet_target_soc_sensor(hass)
if len(sensor) == 0:
sensor = FindEntity.find_vw_target_soc_sensor(hass)
return sensor


class DeviceNameCreator:
"""Class that creates the name of the new device"""
Expand Down
43 changes: 43 additions & 0 deletions tests/helpers/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
PLATFORM_GESPOT,
PLATFORM_NORDPOOL,
PLATFORM_OCPP,
PLATFORM_TESLA_FLEET,
PLATFORM_TGE,
PLATFORM_VW,
SENSOR,
Expand Down Expand Up @@ -379,3 +380,45 @@ def create(
def set_state(hass: HomeAssistant, new_state: str):
"""Set state"""
hass.states.async_set("switch.ocpp_charge_control", new_state)


class MockTeslaFleetSOCEntity:
"""Mockup for Tesla Fleet SOC entity"""

@staticmethod
def create(hass: HomeAssistant, entity_registry: EntityRegistry, value: str = "55"):
"""Create a correct Tesla Fleet SOC entity"""
entity_registry.async_get_or_create(
domain=SENSOR,
platform=PLATFORM_TESLA_FLEET,
unique_id="battery_level",
)
MockTeslaFleetSOCEntity.set_state(hass, value)

@staticmethod
def set_state(hass: HomeAssistant, new_state: str):
"""Set state"""
hass.states.async_set(
"sensor.tesla_fleet_battery_level", new_state
)


class MockTeslaFleetTargetSOCEntity:
"""Mockup for Tesla Fleet Target SOC (Charge Limit) entity"""

@staticmethod
def create(hass: HomeAssistant, entity_registry: EntityRegistry, value: str = "80"):
"""Create a correct Tesla Fleet target SOC entity"""
entity_registry.async_get_or_create(
domain=SENSOR,
platform=PLATFORM_TESLA_FLEET,
unique_id="charge_limit",
)
MockTeslaFleetTargetSOCEntity.set_state(hass, value)

@staticmethod
def set_state(hass: HomeAssistant, new_state: str):
"""Set state"""
hass.states.async_set(
"sensor.tesla_fleet_charge_limit", new_state
)
65 changes: 65 additions & 0 deletions tests/helpers/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
NAME,
PLATFORM_NORDPOOL,
PLATFORM_OCPP,
PLATFORM_TESLA_FLEET,
PLATFORM_VW,
SENSOR,
SWITCH,
Expand All @@ -43,6 +44,8 @@
MockPriceEntityTGE,
MockSOCEntity,
MockTargetSOCEntity,
MockTeslaFleetSOCEntity,
MockTeslaFleetTargetSOCEntity,
)
from tests.price import PRICE_THIRTEEN_LIST

Expand Down Expand Up @@ -390,3 +393,65 @@ async def test_device_name_creator(hass: HomeAssistant):
)
assert (name4 := DeviceNameCreator.create(hass)) not in names
assert NAME in name4


async def test_find_tesla_fleet_entity(hass: HomeAssistant):
"""Test the FindEntity for Tesla Fleet."""

entity_registry: EntityRegistry = async_entity_registry_get(hass)

# Initially no Tesla Fleet entities should be found
assert FindEntity.find_tesla_fleet_soc_sensor(hass) == ""
assert FindEntity.find_tesla_fleet_target_soc_sensor(hass) == ""

# Create Tesla Fleet SOC entity
MockTeslaFleetSOCEntity.create(hass, entity_registry)
assert FindEntity.find_tesla_fleet_soc_sensor(hass) != ""
assert "battery_level" in FindEntity.find_tesla_fleet_soc_sensor(hass)

# Create Tesla Fleet Target SOC entity
MockTeslaFleetTargetSOCEntity.create(hass, entity_registry)
assert FindEntity.find_tesla_fleet_target_soc_sensor(hass) != ""
assert "charge_limit" in FindEntity.find_tesla_fleet_target_soc_sensor(hass)


async def test_find_ev_soc_sensor_priority(hass: HomeAssistant):
"""Test that find_ev_soc_sensor prefers Tesla Fleet over VW."""

entity_registry: EntityRegistry = async_entity_registry_get(hass)

# No EV entities -> empty string
assert FindEntity.find_ev_soc_sensor(hass) == ""
assert FindEntity.find_ev_target_soc_sensor(hass) == ""

# Only VW entities -> returns VW
MockSOCEntity.create(hass, entity_registry)
MockTargetSOCEntity.create(hass, entity_registry)
assert "volkswagen" in FindEntity.find_ev_soc_sensor(hass)
assert "volkswagen" in FindEntity.find_ev_target_soc_sensor(hass)

# Add Tesla Fleet entities -> now prefers Tesla Fleet
MockTeslaFleetSOCEntity.create(hass, entity_registry)
MockTeslaFleetTargetSOCEntity.create(hass, entity_registry)
assert "tesla_fleet" in FindEntity.find_ev_soc_sensor(hass)
assert "tesla_fleet" in FindEntity.find_ev_target_soc_sensor(hass)


async def test_find_tesla_fleet_soc_excludes_usable(hass: HomeAssistant):
"""Test that find_tesla_fleet_soc_sensor excludes usable_battery_level."""

entity_registry: EntityRegistry = async_entity_registry_get(hass)

# Create a usable_battery_level entity (should be excluded)
entity_registry.async_get_or_create(
domain=SENSOR,
platform=PLATFORM_TESLA_FLEET,
unique_id="usable_battery_level",
)
hass.states.async_set("sensor.tesla_fleet_usable_battery_level", "50")
assert FindEntity.find_tesla_fleet_soc_sensor(hass) == ""

# Create the correct battery_level entity
MockTeslaFleetSOCEntity.create(hass, entity_registry)
assert FindEntity.find_tesla_fleet_soc_sensor(hass) != ""
assert "usable" not in FindEntity.find_tesla_fleet_soc_sensor(hass)
Loading