Skip to content

Commit

Permalink
Enable ENTSO-e
Browse files Browse the repository at this point in the history
  • Loading branch information
dala318 committed Sep 25, 2024
1 parent 10805d8 commit 5169d82
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 48 deletions.
130 changes: 107 additions & 23 deletions custom_components/nordpool_planner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@
import logging

from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.util import dt as dt_util

from .config_flow import NordpoolPlannerConfigFlow
from .const import (
CONF_ACCEPT_COST_ENTITY,
CONF_ACCEPT_RATE_ENTITY,
Expand Down Expand Up @@ -67,6 +73,58 @@ async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
await async_setup_entry(hass, config_entry)


async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug(
"Migrating configuration from version %s.%s",
config_entry.version,
config_entry.minor_version,
)
installed_version = NordpoolPlannerConfigFlow.VERSION
installed_minor_version = NordpoolPlannerConfigFlow.MINOR_VERSION

if config_entry.version > installed_version:
# Downgraded from a future version
return False

if config_entry.version == 1:
new_data = {**config_entry.data}
new_options = {**config_entry.options}

np_entity = hass.states.get(new_data[CONF_NP_ENTITY])
try:
uom = np_entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
new_options.pop("currency")
new_options[ATTR_UNIT_OF_MEASUREMENT] = uom

except (IndexError, KeyError):
_LOGGER.warning("Could not extract currency from Nordpool entity")
return False

# if config_entry.minor_version < 2:
# # TODO: modify Config Entry data with changes in version 1.2
# pass
# if config_entry.minor_version < 3:
# # TODO: modify Config Entry data with changes in version 1.3
# pass

hass.config_entries.async_update_entry(
config_entry,
data=new_data,
options=new_options,
minor_version=installed_minor_version,
version=installed_version,
)

_LOGGER.debug(
"Migration to configuration version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)

return True


class NordpoolPlanner:
"""Planner base class."""

Expand Down Expand Up @@ -106,6 +164,16 @@ def name(self) -> str:
"""Name of planner."""
return self._config.data["name"]

@property
def price_sensor_id(self) -> str:
"""Entity id of source sensor."""
return self._np_entity.unique_id

@property
def price_now(self) -> str:
"""Current price from source sensor."""
return self._np_entity.current_price_attr

@property
def _duration(self) -> int:
"""Get duration parameter."""
Expand Down Expand Up @@ -296,6 +364,7 @@ def update(self):
end_time,
duration,
)
return

_LOGGER.debug(
"Processing %s prices_groups found in range %s to %s",
Expand All @@ -310,19 +379,19 @@ def update(self):
for p in prices_groups:
if accept_cost and p.average < accept_cost:
_LOGGER.debug("Accept cost fulfilled")
self.set_lowest_cost_state(p, now)
self.set_lowest_cost_state(p)
break
if accept_rate:
if self._np_entity.average_attr == 0:
if p.average <= 0 and accept_rate <= 0:
_LOGGER.debug(
"Accept rate indirectly fulfilled (NP average 0 but cost and accept rate <= 0)"
)
self.set_lowest_cost_state(p, now)
self.set_lowest_cost_state(p)
break
elif (p.average / self._np_entity.average_attr) < accept_rate:
_LOGGER.debug("Accept rate fulfilled")
self.set_lowest_cost_state(p, now)
self.set_lowest_cost_state(p)
break
if p.average < lowest_cost_group.average:
lowest_cost_group = p
Expand Down Expand Up @@ -395,39 +464,54 @@ def valid(self) -> bool:
# TODO: Add more checks, make function of those in update()
return self._np is not None

@property
def prices(self):
"""Get the prices."""
np_prices = self._np.attributes["today"]
if self._np.attributes["tomorrow_valid"]:
np_prices += self._np.attributes["tomorrow"]
return np_prices
# @property
# def prices(self):
# """Get the prices."""
# np_prices = self._np.attributes["today"]
# if self._np.attributes["tomorrow_valid"]:
# np_prices += self._np.attributes["tomorrow"]
# return np_prices

@property
def _prices_raw(self):
np_prices = self._np.attributes["raw_today"]
if self._np.attributes["tomorrow_valid"]:
np_prices += self._np.attributes["raw_tomorrow"]
return np_prices
def _all_prices(self):
if np_prices := self._np.attributes.get("raw_today"):
if self._np.attributes["tomorrow_valid"]:
np_prices += self._np.attributes["raw_tomorrow"]
return np_prices
elif e_prices := self._np.attributes.get("prices"): # noqa: RET505
e_prices = [
{"start": dt_util.parse_datetime(ep["time"]), "value": ep["price"]}
for ep in e_prices
]
return e_prices
return []

@property
def average_attr(self):
"""Get the average price attribute."""
return self._np.attributes["average"]
if self._np is not None:
return self._np.attributes["average"]
return None

@property
def current_price_attr(self):
"""Get the curent price attribute."""
return self._np.attributes["current_price"]
"""Get the current price attribute."""
if self._np is not None:
return self._np.attributes["current_price"]
return None

# @property
# def price_value(self):
# self._np.state

def update(self, hass: HomeAssistant) -> bool:
"""Update price in storage."""
np = hass.states.get(self._unique_id)
if np is None:
_LOGGER.warning("Got empty data from Norpool entity %s ", self._unique_id)
elif "today" not in np.attributes:
_LOGGER.warning("Got empty data from Nordpool entity %s ", self._unique_id)
elif "today" not in np.attributes and "prices_today" not in np.attributes:
_LOGGER.warning(
"No values for today in Norpool entity %s ", self._unique_id
"No values for today in Nordpool entity %s ", self._unique_id
)
else:
_LOGGER.debug(
Expand All @@ -451,7 +535,7 @@ def get_prices_group(
"""
started = False
selected = []
for p in self._prices_raw:
for p in self._all_prices:
if p["start"] > start - dt.timedelta(hours=1):
started = True
if p["start"] > end:
Expand Down
14 changes: 10 additions & 4 deletions custom_components/nordpool_planner/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,26 @@ def extra_state_attributes(self):
state_attributes = {
"starts_at": STATE_UNKNOWN,
"cost_at": STATE_UNKNOWN,
"now_cost_rate": STATE_UNKNOWN,
"current_cost": self._planner.price_now,
"current_cost_rate": STATE_UNKNOWN,
"price_sensor": self._planner.price_sensor_id,
}
# TODO: This can be made nicer to get value from states in dictionary in planner
if self.entity_description.key == CONF_LOW_COST_ENTITY:
state_attributes = {
"starts_at": self._planner.low_cost_state.starts_at,
"cost_at": self._planner.low_cost_state.cost_at,
"now_cost_rate": self._planner.low_cost_state.now_cost_rate,
"current_cost": self._planner.price_now,
"current_cost_rate": self._planner.low_cost_state.now_cost_rate,
"price_sensor": self._planner.price_sensor_id,
}
elif self.entity_description.key == CONF_HIGH_COST_ENTITY:
state_attributes = {
"starts_at": self._planner.high_cost_state.starts_at,
"cost_at": self._planner.high_cost_state.cost_at,
"now_cost_rate": self._planner.high_cost_state.now_cost_rate,
"current_cost": self._planner.price_now,
"current_cost_rate": self._planner.high_cost_state.now_cost_rate,
"price_sensor": self._planner.price_sensor_id,
}
_LOGGER.debug(
'Returning extra state attributes "%s" of binary sensor "%s"',
Expand All @@ -141,7 +147,7 @@ async def async_added_to_hass(self) -> None:
self._planner.register_output_listener_entity(self, self.entity_description.key)

def update_callback(self) -> None:
"""Call from planner that new data avaialble."""
"""Call from planner that new data available."""
self.schedule_update_ha_state()

# async def async_update(self):
Expand Down
26 changes: 14 additions & 12 deletions custom_components/nordpool_planner/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,17 @@
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import ATTR_NAME, ATTR_UNIT_OF_MEASUREMENT
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import selector

from .const import (
CONF_ACCEPT_COST_ENTITY,
CONF_ACCEPT_RATE_ENTITY,
CONF_CURRENCY,
CONF_DURATION_ENTITY,
CONF_END_TIME_ENTITY,
CONF_HIGH_COST_ENTITY,
CONF_LOW_COST_ENTITY,
CONF_NAME,
CONF_NP_ENTITY,
CONF_SEARCH_LENGTH_ENTITY,
CONF_STARTS_AT_ENTITY,
Expand All @@ -36,7 +35,8 @@
class NordpoolPlannerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Nordpool Planner config flow."""

VERSION = 1
VERSION = 2
MINOR_VERSION = 0
data = None
options = None
_reauth_entry: config_entries.ConfigEntry | None = None
Expand All @@ -60,12 +60,14 @@ async def async_step_user(
self.options = {}
np_entity = self.hass.states.get(self.data[CONF_NP_ENTITY])
try:
self.options[CONF_CURRENCY] = np_entity.attributes.get(CONF_CURRENCY)
self.options[ATTR_UNIT_OF_MEASUREMENT] = np_entity.attributes.get(
ATTR_UNIT_OF_MEASUREMENT
)
except (IndexError, KeyError):
_LOGGER.warning("Could not extract currency from Nordpool entity")

await self.async_set_unique_id(
self.data[CONF_NAME]
self.data[ATTR_NAME]
+ "_"
+ self.data[CONF_NP_ENTITY]
+ "_"
Expand All @@ -79,22 +81,22 @@ async def async_step_user(
self.data,
)
return self.async_create_entry(
title=self.data[CONF_NAME], data=self.data, options=self.options
title=self.data[ATTR_NAME], data=self.data, options=self.options
)

sensor_entities = self.hass.states.async_entity_ids(domain_filter="sensor")
selected_entities = [s for s in sensor_entities if "nordpool" in s]
# TODO: Enable usage for ENTSO-E prices integration
# selected_entities += [
# s for s in sensor_entities if "current_electricity_market_price" in s
# ]
selected_entities = [
s
for s in sensor_entities
if "nordpool" in s or "average_electricity_price_today" in s
]

if len(selected_entities) == 0:
errors["base"] = "No Nordpool entity found"

schema = vol.Schema(
{
vol.Required(CONF_NAME): str,
vol.Required(ATTR_NAME): str,
vol.Required(CONF_TYPE): selector.SelectSelector(
selector.SelectSelectorConfig(options=CONF_TYPE_LIST),
),
Expand Down
3 changes: 0 additions & 3 deletions custom_components/nordpool_planner/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

DOMAIN = "nordpool_planner"

CONF_NAME = "name"
CONF_TYPE = "type"
CONF_TYPE_MOVING = "moving"
CONF_TYPE_STATIC = "static"
Expand All @@ -21,5 +20,3 @@
CONF_SEARCH_LENGTH_ENTITY = "search_length_entity"
# CONF_END_TIME = "end_time"
CONF_END_TIME_ENTITY = "end_time_entity"

CONF_CURRENCY = "currency"
15 changes: 10 additions & 5 deletions custom_components/nordpool_planner/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
RestoreNumber,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfTime
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfTime,
)
from homeassistant.core import HomeAssistant

from . import (
Expand All @@ -22,7 +27,7 @@
NordpoolPlanner,
NordpoolPlannerEntity,
)
from .const import CONF_CURRENCY, DOMAIN
from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -87,14 +92,14 @@ async def async_setup_entry(
if config_entry.data.get(CONF_ACCEPT_COST_ENTITY):
entity_description = ACCEPT_COST_ENTITY_DESCRIPTION
# Override if currency option is set
if currency := config_entry.options.get(CONF_CURRENCY):
if unit_of_measurement := config_entry.options.get(ATTR_UNIT_OF_MEASUREMENT):
entity_description = NumberEntityDescription(
key=ACCEPT_COST_ENTITY_DESCRIPTION.key,
device_class=ACCEPT_COST_ENTITY_DESCRIPTION.device_class,
native_min_value=ACCEPT_COST_ENTITY_DESCRIPTION.native_min_value,
native_max_value=ACCEPT_COST_ENTITY_DESCRIPTION.native_max_value,
native_step=ACCEPT_COST_ENTITY_DESCRIPTION.step,
native_unit_of_measurement=currency,
native_step=ACCEPT_COST_ENTITY_DESCRIPTION.native_step,
native_unit_of_measurement=unit_of_measurement,
)
entities.append(
NordpoolPlannerNumber(
Expand Down
2 changes: 1 addition & 1 deletion custom_components/nordpool_planner/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ async def async_added_to_hass(self) -> None:
self._planner.register_output_listener_entity(self, self.entity_description.key)

def update_callback(self) -> None:
"""Call from planner that new data avaialble."""
"""Call from planner that new data available."""
self.schedule_update_ha_state()

# async def async_update(self):
Expand Down

0 comments on commit 5169d82

Please sign in to comment.