Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Introduce integration with TGE energy sensor #335

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ __pycache__

# Misc
issues
.DS_Store

# Home Assistant configuration
config/*
!config/configuration.yaml
custom_components/*
!custom_components/ev_smart_charging
!custom_components/ev_smart_charging
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@

![Icon](assets/icon.png)

The EV Smart Charging integration will automatically charge the electric vehicle (EV) when the electricity price is the lowest. The integration natively supports the [Nordpool](https://github.com/custom-components/nordpool), the [Energi Data Service](https://github.com/MTrab/energidataservice) and the [Entso-e](https://github.com/JaccoR/hass-entso-e) integrations. The integration also supports a [generic price format](https://github.com/jonasbkarlsson/ev_smart_charging/wiki/Price-sensor). Together with using a template sensor, many different price integrations should be possible to use.
The EV Smart Charging integration will automatically charge the electric vehicle (EV) when the electricity price is the lowest. The integration natively supports the [Nordpool](https://github.com/custom-components/nordpool), the [Energi Data Service](https://github.com/MTrab/energidataservice), the [Entso-e](https://github.com/JaccoR/hass-entso-e) and the [TGE](https://github.com/PiotrMachowski/Home-Assistant-custom-components-TGE) integrations. The integration also supports a [generic price format](https://github.com/jonasbkarlsson/ev_smart_charging/wiki/Price-sensor). Together with using a template sensor, many different price integrations should be possible to use.

The integration calculates the set of hours that will give the lowest price, by default restricted to a continuous set. This calculation is done when the electricity prices for tomorrow is available (typically between shortly after 13:00 CET/CEST and midnight) or when the time of the day is before the configured charge completion time. When the automatic charging has started, changes of settings will not have any effect.

## Requirements
- The [Nordpool](https://github.com/custom-components/nordpool), the [Energi Data Service](https://github.com/MTrab/energidataservice), the [Entso-e](https://github.com/JaccoR/hass-entso-e) integration or a template sensor that generates price data using the supported [generic price format](https://github.com/jonasbkarlsson/ev_smart_charging/wiki/Price-sensor).
- The [Nordpool](https://github.com/custom-components/nordpool), the [Energi Data Service](https://github.com/MTrab/energidataservice), the [Entso-e](https://github.com/JaccoR/hass-entso-e), the [TGE](https://github.com/PiotrMachowski/Home-Assistant-custom-components-TGE) integration or a template sensor that generates price data using the supported [generic price format](https://github.com/jonasbkarlsson/ev_smart_charging/wiki/Price-sensor).
- Home Assistant version 2023.4 or newer.

## Features
- Automatic EV charging control based on electricity prices.
- Native support of the [Nordpool](https://github.com/custom-components/nordpool), [Energi Data Service](https://github.com/MTrab/energidataservice) and [Entso-e](https://github.com/JaccoR/hass-entso-e) integrations.
- Native support of the [Nordpool](https://github.com/custom-components/nordpool), [Energi Data Service](https://github.com/MTrab/energidataservice), [Entso-e](https://github.com/JaccoR/hass-entso-e) and [TGE](https://github.com/PiotrMachowski/Home-Assistant-custom-components-TGE) integrations.
- Support of a [generic price format](https://github.com/jonasbkarlsson/ev_smart_charging/wiki/Price-sensor). A template sensor can be used to get price information from many price integrations.
- Configuration of the latest time of the day when the charging should be completed, and the earliest time the charging can start.
- Selection of preference between one continuous charging session or several (possibly more price optimized) non-continuous charging sessions.
Expand Down Expand Up @@ -63,7 +63,7 @@ The configuration form contains the entities that the integration is interacting
Parameter | Required | Description
-- | -- | --
Name | Yes | The name of the instance.
Electricity price entity | Yes | The Nordpool, the Energi Data Service, the Entso-e integration sensor entity or a template sensor providing the price in the [generic price format](https://github.com/jonasbkarlsson/ev_smart_charging/wiki/Price-sensor). For the Entso-e integration, the entity called `sensor.average_electricity_price` should be used.
Electricity price entity | Yes | The (HACS) Nordpool, the Energi Data Service, the Entso-e, the TGE integration sensor entity or a template sensor providing the price in the [generic price format](https://github.com/jonasbkarlsson/ev_smart_charging/wiki/Price-sensor). For the Entso-e integration, the entity called `sensor.average_electricity_price` should be used.
EV SOC entity | Yes | Entity with the car's State-of-Charge. A value between 0 and 100. Note that this entity is crucial for the integration. If live information about he SOC is not available, please carefully read the section below with more information about the EV SOC entity.
EV target SOC entity | No | Entity with the target value for the State-of-Charge. A value between 0 and 100. If not provided, 100 is assumed.
Charger control switch entity | No | If provided, the integration will directly control the charger by setting the state of this entity to 'on' or 'off'.
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 @@ -29,6 +29,7 @@
PLATFORM_NORDPOOL = "nordpool"
PLATFORM_ENERGIDATASERVICE = "energidataservice"
PLATFORM_ENTSOE = "entsoe"
PLATFORM_TGE = "tge"
PLATFORM_VW = "volkswagen_we_connect_id"
PLATFORM_OCPP = "ocpp"
PLATFORM_GENERIC = "generic"
Expand Down
15 changes: 15 additions & 0 deletions custom_components/ev_smart_charging/helpers/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
PLATFORM_ENTSOE,
PLATFORM_GENERIC,
PLATFORM_NORDPOOL,
PLATFORM_TGE,
PLATFORM_OCPP,
PLATFORM_VW,
SWITCH,
Expand Down Expand Up @@ -112,6 +113,8 @@ def find_price_sensor(hass: HomeAssistant) -> str:
sensor = FindEntity.find_nordpool_sensor(hass)
if len(sensor) == 0:
sensor = FindEntity.find_energidataservice_sensor(hass)
if len(sensor) == 0:
sensor = FindEntity.find_tge_sensor(hass)
if len(sensor) == 0:
sensor = FindEntity.find_entsoe_sensor(hass)
return sensor
Expand Down Expand Up @@ -168,6 +171,18 @@ def find_generic_sensor(hass: HomeAssistant) -> str:
return entity_id
return ""

@staticmethod
def find_tge_sensor(hass: HomeAssistant) -> str:
"""Search for TGE 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_TGE and entry[1].unique_id == 'tge_sensor_fixing1_rate':
return entry[1].entity_id
return ""

@staticmethod
def find_vw_soc_sensor(hass: HomeAssistant) -> str:
"""Search for Volkswagen SOC sensor"""
Expand Down
16 changes: 16 additions & 0 deletions custom_components/ev_smart_charging/helpers/coordinator.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from custom_components.ev_smart_charging.const import (
PLATFORM_ENERGIDATASERVICE,
PLATFORM_ENTSOE,
PLATFORM_TGE,
PLATFORM_GENERIC,
PLATFORM_NORDPOOL,
READY_HOUR_NONE,
Expand Down Expand Up @@ -67,6 +68,21 @@ def convert_raw_item(
item_new["end"] = item_new["start"] + timedelta(hours=1)
return item_new

# Array of item = {
# "time": datetime,
# "price": float,
# }
# {'time': datetime.datetime(2023, 3, 6, 0, 0,
# tzinfo=<DstTzInfo 'Europe/Stockholm' CET+1:00:00 STD>),
# 'price': 146.96}
if platform == PLATFORM_TGE:
if item["price"] is not None and isinstance(item["time"], datetime):
item_new = {}
item_new["value"] = item["price"]
item_new["start"] = item["time"]
item_new["end"] = item["time"] + timedelta(hours=1)
return item_new

# Array of item = {
# "time": string,
# "price": float,
Expand Down
9 changes: 5 additions & 4 deletions custom_components/ev_smart_charging/helpers/price_adaptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
CONF_PRICE_SENSOR,
PLATFORM_ENERGIDATASERVICE,
PLATFORM_ENTSOE,
PLATFORM_TGE,
PLATFORM_GENERIC,
PLATFORM_NORDPOOL,
)
Expand Down Expand Up @@ -65,7 +66,7 @@ def get_raw_today_local(self, state) -> Raw:
if self._price_platform in (PLATFORM_NORDPOOL, PLATFORM_ENERGIDATASERVICE):
return Raw(state.attributes["raw_today"], self._price_platform)

if self._price_platform in (PLATFORM_ENTSOE, PLATFORM_GENERIC):
if self._price_platform in (PLATFORM_ENTSOE, PLATFORM_TGE, PLATFORM_GENERIC):
return Raw(state.attributes["prices_today"], self._price_platform)

return Raw([])
Expand All @@ -76,7 +77,7 @@ def get_raw_tomorrow_local(self, state) -> Raw:
if self._price_platform in (PLATFORM_NORDPOOL, PLATFORM_ENERGIDATASERVICE):
return Raw(state.attributes["raw_tomorrow"], self._price_platform)

if self._price_platform in (PLATFORM_ENTSOE, PLATFORM_GENERIC):
if self._price_platform in (PLATFORM_ENTSOE, PLATFORM_TGE, PLATFORM_GENERIC):
return Raw(state.attributes["prices_tomorrow"], self._price_platform)

return Raw([])
Expand All @@ -87,7 +88,7 @@ def get_current_price(self, state) -> float:
if self._price_platform in (PLATFORM_NORDPOOL, PLATFORM_ENERGIDATASERVICE):
return state.attributes["current_price"]

if self._price_platform in (PLATFORM_ENTSOE, PLATFORM_GENERIC):
if self._price_platform in (PLATFORM_ENTSOE, PLATFORM_TGE, PLATFORM_GENERIC):
time_now = dt.now()
return self.get_raw_today_local(state).get_value(time_now)

Expand Down Expand Up @@ -117,7 +118,7 @@ def validate_price_entity(
_LOGGER.debug("No attribute raw_tomorrow in price sensor")
return ("base", "sensor_is_not_price")

if price_platform in (PLATFORM_ENTSOE, PLATFORM_GENERIC):
if price_platform in (PLATFORM_ENTSOE, PLATFORM_TGE, PLATFORM_GENERIC):
if not "prices_today" in price_state.attributes.keys():
_LOGGER.debug("No attribute prices today in price sensor")
return ("base", "sensor_is_not_price")
Expand Down
47 changes: 47 additions & 0 deletions tests/helpers/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
PLATFORM_GENERIC,
PLATFORM_NORDPOOL,
PLATFORM_OCPP,
PLATFORM_TGE,
PLATFORM_VW,
SENSOR,
SWITCH,
Expand Down Expand Up @@ -152,6 +153,52 @@ def set_state(
},
)

class MockPriceEntityTGE:
"""Mockup for price entity TGE"""

@staticmethod
def create(
hass: HomeAssistant, entity_registry: EntityRegistry, price: float = 123
):
"""Create a correct price entity"""
entity_registry.async_get_or_create(
domain=SENSOR,
platform=PLATFORM_TGE,
unique_id="tge_sensor_fixing1_rate",
)
MockPriceEntityTGE.set_state(hass, None, None, price)

@staticmethod
def set_state(
hass: HomeAssistant,
new_raw_today: list,
new_raw_tomorrow: list,
new_price: float = None,
):
"""Set state of MockPriceEntity"""

# Find current price
if new_price is None:
new_price = "unavailable"
if price := Raw(new_raw_today, PLATFORM_TGE).get_value(
dt_util.now()
):
new_price = price
if price := Raw(new_raw_tomorrow, PLATFORM_TGE).get_value(
dt_util.now()
):
new_price = price

# Set state
hass.states.async_set(
"sensor.tge_sensor_fixing1_rate",
f"{new_price}",
{
"prices_today": new_raw_today,
"prices_tomorrow": new_raw_tomorrow,
},
)

class MockPriceEntityGeneric:
"""Mockup for a generic price entity"""

Expand Down
5 changes: 5 additions & 0 deletions tests/helpers/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
MockPriceEntityEnergiDataService,
MockPriceEntityEntsoe,
MockPriceEntityGeneric,
MockPriceEntityTGE,
MockSOCEntity,
MockTargetSOCEntity,
)
Expand Down Expand Up @@ -299,6 +300,10 @@ async def test_find_entity(hass: HomeAssistant):
MockPriceEntityEntsoe.create(hass, entity_registry)
assert FindEntity.find_price_sensor(hass).startswith("sensor.entsoe")

assert FindEntity.find_tge_sensor(hass) == ""
MockPriceEntityTGE.create(hass, entity_registry)
assert FindEntity.find_price_sensor(hass).startswith("sensor.tge")

assert FindEntity.find_energidataservice_sensor(hass) == ""
MockPriceEntityEnergiDataService.create(hass, entity_registry)
assert FindEntity.find_price_sensor(hass).startswith("sensor.energidataservice")
Expand Down
54 changes: 54 additions & 0 deletions tests/helpers/test_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from custom_components.ev_smart_charging.const import (
PLATFORM_ENERGIDATASERVICE,
PLATFORM_ENTSOE,
PLATFORM_TGE,
READY_HOUR_NONE,
START_HOUR_NONE,
)
Expand All @@ -24,9 +25,11 @@
PRICE_20220930,
PRICE_20220930_ENERGIDATASERVICE,
PRICE_20220930_ENTSOE,
PRICE_20220930_TGE,
PRICE_20221001,
PRICE_20221001_ENERGIDATASERVICE,
PRICE_20221001_ENTSOE,
PRICE_20221001_TGE,
)
from tests.schedule import MOCK_SCHEDULE_20220930

Expand Down Expand Up @@ -203,6 +206,57 @@ async def test_raw_entsoe(hass, set_cet_timezone, freezer):
assert not price.is_valid()
assert price.last_value() is None

async def test_raw_tge(hass, set_cet_timezone):
"""Test Raw"""

price = Raw(PRICE_20220930_TGE, PLATFORM_TGE)
assert price.get_raw() == PRICE_20220930
assert price.is_valid()
assert price.copy().get_raw() == PRICE_20220930
assert price.max_value() == 388.65
assert price.last_value() == 49.64
assert price.number_of_nonzero() == 24

time = datetime(
2022, 9, 30, 8, 0, 0, tzinfo=dt_util.get_time_zone("Europe/Stockholm")
)
assert price.get_value(time) == 388.65
assert price.get_item(time) == {
"start": datetime(
2022, 9, 30, 8, 0, tzinfo=dt_util.get_time_zone("Europe/Stockholm")
),
"end": datetime(
2022, 9, 30, 9, 0, tzinfo=dt_util.get_time_zone("Europe/Stockholm")
),
"value": 388.65,
}
time = datetime(
2022, 9, 29, 8, 0, 0, tzinfo=dt_util.get_time_zone("Europe/Stockholm")
)
assert price.get_value(time) is None
assert price.get_item(time) is None

price2 = Raw(PRICE_20221001_TGE, PLATFORM_TGE)
price.extend(None)
assert price.get_raw() == PRICE_20220930
price.extend(price2)
assert price.number_of_nonzero() == 48

start = price.data[0]["start"]
assert start.tzinfo == dt_util.get_time_zone("Europe/Stockholm")
assert start.hour == 0
price_utc = price.copy().to_utc()
start = price_utc.data[0]["start"]
assert start.tzinfo == dt_util.UTC
assert start.hour == 22
price_local = price_utc.copy().to_local()
start = price_local.data[0]["start"]
assert start.tzinfo == dt_util.get_time_zone("Europe/Stockholm")
assert start.hour == 0

price = Raw([], PLATFORM_TGE)
assert not price.is_valid()
assert price.last_value() is None

async def test_get_lowest_hours_non_continuous(hass, set_cet_timezone, freezer):
"""Test get_lowest_hours()"""
Expand Down
Loading
Loading