diff --git a/custom_components/nordpool_planner/__init__.py b/custom_components/nordpool_planner/__init__.py index a214473..1276aa7 100644 --- a/custom_components/nordpool_planner/__init__.py +++ b/custom_components/nordpool_planner/__init__.py @@ -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, @@ -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.""" @@ -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.""" @@ -296,6 +364,7 @@ def update(self): end_time, duration, ) + return _LOGGER.debug( "Processing %s prices_groups found in range %s to %s", @@ -310,7 +379,7 @@ 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: @@ -318,11 +387,11 @@ def update(self): _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 @@ -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( @@ -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: diff --git a/custom_components/nordpool_planner/binary_sensor.py b/custom_components/nordpool_planner/binary_sensor.py index 642a86c..6a10a75 100644 --- a/custom_components/nordpool_planner/binary_sensor.py +++ b/custom_components/nordpool_planner/binary_sensor.py @@ -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"', @@ -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): diff --git a/custom_components/nordpool_planner/config_flow.py b/custom_components/nordpool_planner/config_flow.py index 418d475..d96be37 100644 --- a/custom_components/nordpool_planner/config_flow.py +++ b/custom_components/nordpool_planner/config_flow.py @@ -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, @@ -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 @@ -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] + "_" @@ -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), ), diff --git a/custom_components/nordpool_planner/const.py b/custom_components/nordpool_planner/const.py index ae74aca..83e8741 100644 --- a/custom_components/nordpool_planner/const.py +++ b/custom_components/nordpool_planner/const.py @@ -2,7 +2,6 @@ DOMAIN = "nordpool_planner" -CONF_NAME = "name" CONF_TYPE = "type" CONF_TYPE_MOVING = "moving" CONF_TYPE_STATIC = "static" @@ -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" diff --git a/custom_components/nordpool_planner/number.py b/custom_components/nordpool_planner/number.py index a1b64fa..55ac599 100644 --- a/custom_components/nordpool_planner/number.py +++ b/custom_components/nordpool_planner/number.py @@ -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 ( @@ -22,7 +27,7 @@ NordpoolPlanner, NordpoolPlannerEntity, ) -from .const import CONF_CURRENCY, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -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( diff --git a/custom_components/nordpool_planner/sensor.py b/custom_components/nordpool_planner/sensor.py index 42c93c3..44d1614 100644 --- a/custom_components/nordpool_planner/sensor.py +++ b/custom_components/nordpool_planner/sensor.py @@ -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):