├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── custom_components └── nordpool_diff │ ├── __init__.py │ ├── manifest.json │ └── sensor.py ├── diff_example.png ├── hacs.json └── middle_change.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Joonas Pulakka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is Nordpool diff? 2 | 3 | Nordpool Diff enables you to make smarter use of hourly electricity prices by considering how the price will change over the coming hours. For example; the hours before a price increase it might be smart to run the water heater or turn up the thermostat, and during the expensive hours turn the thermostat down, thereby shifting your electricity consumption from expensive hours to cheaper hours. 4 | 5 | As the share of renewables in our electricity grids increases, the price variations during each 24 hours will be greater and greater, and also negative prices. This means that not only will you save some money by using electricity smarter, but you will also help the electricity grid. High energy prices most often mean a higher degree of dirty energy sources, so by using electricity smarter, you also reduce your carbon footprint. 6 | 7 | What Nordpool Diff does is add a sensor to Home Assistant that will tell you how the current price relates to the prices in the coming hours. Based on that, you can make automations in Home Assistant that take the upcoming electricity prices into account. 8 | 9 | # What is Nordpool Diff suitable for? 10 | Nordpool Diff is intended to optimise heating. Heating can be divided in two types: 11 | 1. Thermostats and other continuously adjustable appliances 12 | 2. Water heaters and other on/off appliances. 13 | 14 | Nordpool Diff is not suitable for controlling washing machines, dishwashers and other things that needs to run for a number of hours in a stretch to work. Neither is it suitable for things that require to be on a minimum number of hours per day, as you won't know how for sure many hours per day a Nordpool Diff sensor will stay over some specific threshold. If you are looking for smart charging of your EV, use something like [EV Smart Charging](https://github.com/jonasbkarlsson/ev_smart_charging). 15 | 16 | # Installation 17 | 18 | 1. Integrate electricity prices into Home Assistant, if you haven't already. You have two choices: 19 | 1. [Nordpool integration](https://www.home-assistant.io/integrations/nordpool/) This is the only built-in integration in Home Assistant that can import electricity prices. 20 | 2. [Entso-e](https://github.com/JaccoR/hass-entso-e). It has the benefit of being fully in accordance with the Terms and conditions. It also covers a few more markets than Nordpool. 21 | 2. Install `nordpool_diff`, either using HACS or manually 22 | 1. HACS (recommended) 23 | 1. Go to [HACS](https://hacs.xyz) in your Home Assistant instance and open `Custom Repositories` 24 | 2. Add this repository `https://github.com/jpulakka/nordpool_diff` as an `Integration` 25 | 3. Restart Home Assistant 26 | 1. Manually 27 | 1. Copy the `nordpool_diff` folder to HA `/custom_components/nordpool_diff/` 28 | 2. Restart HA. (Skipping restarting before modifying configuration would give "Integration 'nordpool_diff' not found" error message from the configuration.) 29 | 3. Create a first `nordpool_diff` sensor. Add the following to your `configuration.yaml` file: 30 | ```yaml 31 | sensor: 32 | - platform: nordpool_diff 33 | nordpool_entity: [your nordpool price sensor, for example sensor.nordpool_kwh_fi_eur_3_095_024] 34 | filter_length: 10 35 | normalize: max_min_sqrt_max 36 | ``` 37 | 4. Restart Home Assistant again 38 | 5. You should now find a new `sensor.nordpool_diff_triangle_10_normalize_max_min_sqrt_max` sensor that varies with the electricity price. 39 | 40 | # Configuring Nordpool Diff 41 | 42 | The following parameters can be added to the sensor: 43 | 44 | | Name | Possible values | Default | Description | 45 | | ---- | :-----: | :---: | ----------- | 46 | |`nordpool_entity`| | | Your Nordpool sensor, for example `sensor.nordpool_kwh_fi_eur_3_095_024`. Mandatory if you use Nordpool. | 47 | |`entsoe_entity`| | `sensor.average_electricity_price_today` | Your Entso-E sensor, if you've named it during installation| 48 | |`filter_type`| `triangle`, `rectangle`, `rank`, `interval` | `triangle`| See next chapters| 49 | |`filter_length`| 2...20 | `10`| Defines how many hours into the future to consider| 50 | |`normalize`| `no`, `max`, `max_min`, `sqrt_max`, `max_min_sqrt_max`| `no`| How to normalize the output of `triangle` and `rectangle` filters. See [`Normalization` option](#normalization).| 51 | |`unit`| | `EUR/kWh/h`| The unit of the sensor. Loosely speaking reflects rate of change (1/h) of hourly price (EUR/kWh)| 52 | 53 | You can create multiple Nordpool_diff sensors, with different parameters. This can be a good practice when you get familiar each with different parameter, plot them in the dashboard, and pick what you like best. 54 | 55 | Note that you need to restart Home Assistant for each new sensor you add to `configuration.yaml`, reloading YAML isn't enough. 56 | 57 | # How does Nordpool Diff work? (`filter_type: rectangle` and `filter_type: triangle`) 58 | The basics is that the price of the current hour is compared to the upcoming hours. The number of hours is defined by `filter_length`. The output from the Nordpool diff sensor is a scoring in relation to the upcoming hours. The scoring can be done in different ways, and this is where `filter_type` comes into play. The most basic variant is that each of the upcoming hours is given equal weight. For example; 59 | 60 | If you have the following Nordpool Diff sensor: 61 | ```yaml 62 | sensor: 63 | - platform: nordpool_diff 64 | filter_type: rectangle 65 | filter_length: 5 66 | ``` 67 | This means that the output of the sensor will be calculated as: 68 | 69 | | Hour | Price | Multiplier | Sum per hour | 70 | |:-------|:-----:|:-------------:|:------------:| 71 | | 11-12 | 0.1 | -1 | -0.1 | 72 | | 12-13 | 0.2 | 1/4 | 0.05 | 73 | | 13-14 | 0.1 | 1/4 | 0.025 | 74 | | 14-15 | 0.4 | 1/4 | 0.1 | 75 | | 15-16 | 0.4 | 1/4 | 0.1 | 76 | | Sum (value of sensor) | | | **0.175** | 77 | 78 | So the scoring in this case is 0.175 EUR/kWh/h. 79 | 80 | This number can be thought of as an indication of how much the price is changing per hour during the number of hours (given by the filter length). Mathematically, this is called a FIR-filter. This FIR-filter always has -1 as the first value, and then followed by the multipliers that depend on the filter length, as follows: 81 | 82 | | `filter_length` | FIR-filter | 83 | |:-------|:-----:| 84 | | 2 | `[-1, 1]` | 85 | | 3 | `[-1, 1/2, 1/2]` | 86 | | 4 | `[-1, 1/3, 1/3, 1/3]` | 87 | | 5 | `[-1, 1/4, 1/4, 1/4, 1/4]` | 88 | 89 | ...and so on... 90 | 91 | As you see, the (rectangle) filter will weight all hours equally. It will basically tell: how does the current electricity price compare to the average of the hours within the filter length? 92 | 93 | ## `filter_type: triangle` 94 | In addition to `rectangle`, Nordpool Diff also supports `triangle` as filter type. While the `rectangle` filter puts equal weight on all future electricity price, `triangle` put greater weight on the hours closeser in time. This can be illustrated as: 95 | 96 | | `filter_length` | FIR-filter | 97 | |:-------|:-----:| 98 | | 2 | `[-1, 1]` | 99 | | 3 | `[-1, 2/3, 1/3]` | 100 | | 4 | `[-1, 3/6, 2/6, 1/6]` | 101 | | 5 | `[-1, 4/10, 3/10, 2/10, 1/10]` | 102 | 103 | ...and so on... 104 | 105 | Looking at the numbers in the FIR-filter, you might be able to spot why it's called `triangle`. 106 | 107 | This filter type can be used to more aggressively turn up a thermostat in preparation of a price hike. 108 | 109 | The difference between rectangle and triangle can be better understood with a visual example. Consider these two sensors: 110 | 111 | ```yaml 112 | sensor: 113 | - platform: nordpool_diff 114 | filter_type: rectangle 115 | filter_length: 2 116 | - platform: nordpool_diff 117 | filter_type: triangle 118 | filter_length: 10 119 | ``` 120 | They yield the following graph: 121 | 122 | ![Diff example](diff_example.png) 123 | 124 | ## Normalize 125 | Sensors with `filter_type: rectangle` or `filter_type: triangle` have an output that is proportional to the variations in of the electricity price. The greater variation, the greater the output. If a thermostat is intended to vary +/-2 degrees under normal price variations, it may start varying much more if the price variations are larger than normal. It doesn't make sense for a thermostat to be adjusted with +/-20 deg C, no matter how the electricity prices varies. 126 | 127 | `normalize` addresses this problem. Options include: 128 | * `normalize: max_min_sqrt_max`: **Recommended**. Output of the filter is multiplied by square root of maximum price of the next `filter_length` hours and divided by maximum minus minimum price of the next `filter_length` hours. Think about it this way: 129 | * Raw output of the FIR differentiator is proportional to price *variation*. 130 | * Divide by maximum minus minimum price (= price variation; could also use e.g. standard deviation), to get scale-free output. 131 | * Multiply by square root of maximum price (could also use e.g. average, but max is good enough and besides less likely negative), to introduce scale. So now 9x price gives 3x output. 132 | * `normalize: max`: Output of the filter is divided by maximum price of the next `filter_length` hours. 133 | * `normalize: max_min`: Output of the filter is divided by maximum minus minimum price of the next `filter_length` hours. 134 | * `normalize: sqrt_max`: Output of the filter is divided by square root of maximum price of the next `filter_length` hours. This provides "somewhat scale-free normalization" where the output magnitude depends on price magnitude, but not linearly so. 135 | * `normalize: no`: No normalization. Not recommended if you are controlling a thermostat. 136 | 137 | Possible edge cases of price staying exactly constant, zero or negative for long time are handled gracefully. 138 | 139 | In addition to this, you should use a filter length of 10 or more for `normalize` to work well. 140 | 141 | The `normalize` parameter has no effect on `rank` or `interval`. 142 | 143 | 144 | # Optimizing water heaters (`filter_type` `rank` and `interval`) 145 | 146 | With `filter_type: rank`, the current price is ranked amongst the next `filter_length` prices. The lowest price is given a value of `1`, the highest price is given the value of `-1`, and the other prices are equally distributed in this interval. 147 | 148 | With `filter_type: interval`, the current price is placed inside the interval of the next `filter_length` prices. The lowest price is given a value of `1`, the highest price is given the value of `-1`, and the current price is linearly placed inside this interval. 149 | 150 | If the current price is the lowest or highest price for the next `filter_length` prices, both filter types will output `1` or `-1`, respectively. If the next three prices are `1.4`, `1` and `2`, the `rank` filter will output `0` and the `interval` filter will output `0.2`. 151 | 152 | Since the output magnitude of the `rank` and `interval` filters are always between -1 and +1, independent of magnitude of price variation, it may be more appropriate (than the linear FIR filters) for simple thresholding and controlling binary things can only be turned on/off, such as water heaters. 153 | 154 | Example: 155 | 156 | ```yaml 157 | sensor: 158 | - platform: nordpool_diff 159 | nordpool_entity: sensor.nordpool_kwh_fi_eur_5_10_0 160 | filter_type: interval 161 | filter_length: 12 162 | ``` 163 | 164 | # Various comments 165 | 166 | ## Avoiding short cycles 167 | If you have an appliance such as a heat pump, for which you would like to avoid short "on" cycles, you might get such a short cycle if you're at the end of an hour (for example 11:59) and your automation will turn the appliance off when the next hour starts due to the new value of the Nordpool Diff sensor, causing the appliance to only run for 1 minute. 168 | If you have such an automation, you can benefit from the sensor attribute `next_hour` that indicates what the value of the sensor will be next hour. 169 | 170 | ## ENTSO-E vs. Nord Pool 171 | 172 | This component was initially (in 2021) created to support https://github.com/custom-components/nordpool, hence the name. But after that (in 2022) https://github.com/JaccoR/hass-entso-e became available. Besides being 100 % legal to use, ENTSO-E also covers a wider range of markets than Nord Pool. 173 | [Nord Pool API documentation](https://www.nordpoolgroup.com/en/trading/api/) states: 174 | _If you are a Nord Pool customer, using our trading APIs is free. All others must become a customer to use our APIs._ 175 | Which apparently means that almost nobody should be using it, even though the API is technically public and appears to work without any tokens. 176 | It's more correct to use [ENTSO-E](https://transparency.entsoe.eu/) which is intended to be used by anyone. 177 | 178 | Nordpool Diff supports both Entso-E and Nordpool, and if you have _both_ integrated, Nordpool Diff will use both for redundancy as follows: 179 | 180 | 1. In first hand, use prices from Entso-E. 181 | 2. If less than N upcoming hours are available, then look up prices from Nordpool too. 182 | 3. Use whichever (Entso-E or Nordpool) provided more upcoming hours. 183 | 184 | ## Known limitations 185 | 186 | ### If price sensor is not found, no error will be provided 187 | If you forget to point out the price sensor in `configuration.yaml`, you will just get a Nordpool Diff sensor value of 0 but no error. 188 | 189 | ### Adding new sensors requires a restart 190 | If you add a new Nordpool Diff sensor, Home Assistant must be restarted. Reloading the YAML configuration is not sufficient. 191 | 192 | ### Changes to sensor value in the middle of the hour 193 | As you know, a new set of prices for the next day is published about 10-11 hours before they start to come into effect. If you have a filter length of 11 or larger, you might notice that the filter for the current hour changes in the middle of an hour during the afternoon. 194 | ![Example](middle_change.png) 195 | 196 | This is since the filter suddenly has new hours to take into account, meaning it immediately makes use of new information and hence technically not an error. It might however become a problem if you are looking to avoid short on-cycles or similar. 197 | 198 | ## Debug logging 199 | Add the following to `configuration.yaml`: 200 | 201 | ```yaml 202 | logger: 203 | default: info 204 | logs: 205 | custom_components.nordpool_diff.sensor: debug 206 | ``` 207 | -------------------------------------------------------------------------------- /custom_components/nordpool_diff/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from homeassistant.config_entries import ConfigEntry 4 | from homeassistant.core_config import Config 5 | from homeassistant.core import HomeAssistant 6 | 7 | DOMAIN = "nordpool_diff" 8 | PLATFORMS = ["sensor"] 9 | 10 | 11 | async def async_setup(hass: HomeAssistant, config: Config) -> bool: 12 | hass.data.setdefault(DOMAIN, {}) 13 | return True 14 | 15 | 16 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 17 | entry_data = dict(entry.data) 18 | hass.data[DOMAIN][entry.entry_id] = entry_data 19 | hass.config_entries.async_setup_platforms(entry, PLATFORMS) 20 | return True 21 | 22 | 23 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 24 | unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) 25 | if unload_ok: 26 | hass.data[DOMAIN].pop(entry.entry_id) 27 | return unload_ok 28 | -------------------------------------------------------------------------------- /custom_components/nordpool_diff/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "nordpool_diff", 3 | "name": "nordpool_diff", 4 | "documentation": "https://github.com/jpulakka/nordpool_diff", 5 | "issue_tracker": "https://github.com/jpulakka/nordpool_diff/issues", 6 | "requirements": [], 7 | "dependencies": [], 8 | "codeowners": ["@jpulakka"], 9 | "iot_class": "calculated", 10 | "version": "0.2.2" 11 | } 12 | -------------------------------------------------------------------------------- /custom_components/nordpool_diff/sensor.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import voluptuous as vol 5 | import homeassistant.helpers.config_validation as cv 6 | from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity 7 | from homeassistant.const import STATE_UNKNOWN 8 | from homeassistant.core import HomeAssistant 9 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 10 | from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType 11 | from homeassistant.util import dt 12 | from datetime import datetime, timedelta 13 | from math import sqrt 14 | 15 | _LOGGER = logging.getLogger(__name__) 16 | 17 | NORDPOOL_ENTITY = "nordpool_entity" 18 | ENTSOE_ENTITY = "entsoe_entity" 19 | FILTER_LENGTH = "filter_length" 20 | FILTER_TYPE = "filter_type" 21 | RECTANGLE = "rectangle" 22 | TRIANGLE = "triangle" 23 | RANK = "rank" 24 | INTERVAL = "interval" 25 | NORMALIZE = "normalize" 26 | NO = "no" 27 | MAX = "max" 28 | MAX_MIN = "max_min" 29 | SQRT_MAX = "sqrt_max" 30 | MAX_MIN_SQRT_MAX = "max_min_sqrt_max" 31 | UNIT = "unit" 32 | 33 | # https://developers.home-assistant.io/docs/development_validation/ 34 | # https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/config_validation.py 35 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 36 | vol.Optional(NORDPOOL_ENTITY, default=""): cv.string, # Is there a way to require EITHER nordpool OR entsoe being valid cv.entity_id? 37 | vol.Optional(ENTSOE_ENTITY, default="sensor.average_electricity_price_today"): cv.string, # hass-entso-e's default entity id 38 | vol.Optional(FILTER_LENGTH, default=10): vol.All(vol.Coerce(int), vol.Range(min=2, max=20)), 39 | vol.Optional(FILTER_TYPE, default=TRIANGLE): vol.In([RECTANGLE, TRIANGLE, INTERVAL, RANK]), 40 | vol.Optional(NORMALIZE, default=NO): vol.In([NO, MAX, MAX_MIN, SQRT_MAX, MAX_MIN_SQRT_MAX]), 41 | vol.Optional(UNIT, default="EUR/kWh/h"): cv.string 42 | }) 43 | 44 | 45 | def setup_platform( 46 | hass: HomeAssistant, 47 | config: ConfigType, 48 | add_entities: AddEntitiesCallback, 49 | discovery_info: DiscoveryInfoType | None = None 50 | ) -> None: 51 | nordpool_entity_id = config[NORDPOOL_ENTITY] 52 | entsoe_entity_id = config[ENTSOE_ENTITY] 53 | filter_length = config[FILTER_LENGTH] 54 | filter_type = config[FILTER_TYPE] 55 | normalize = config[NORMALIZE] 56 | unit = config[UNIT] 57 | 58 | add_entities([NordpoolDiffSensor(nordpool_entity_id, entsoe_entity_id, filter_length, filter_type, normalize, unit)]) 59 | 60 | def _with_interval(prices): 61 | p_min = min(prices) 62 | p_max = max(prices) 63 | if not p_max > p_min: 64 | return 0 65 | return 1 - 2 * (prices[0]-p_min)/(p_max-p_min) 66 | 67 | def _with_rank(prices): 68 | return 1 - 2 * sorted(prices).index(prices[0]) / (len(prices) - 1) 69 | 70 | def _with_filter(filter, normalize): 71 | return lambda prices : sum([a * b for a, b in zip(prices, filter)]) * normalize(prices) 72 | 73 | def _get_next_n_hours_from_nordpool(n, np): 74 | prices = np.attributes["today"] 75 | hour = dt.now().hour 76 | # Get tomorrow if needed: 77 | if len(prices) < hour + n and np.attributes["tomorrow_valid"]: 78 | prices = prices + np.attributes["tomorrow"] 79 | # Nordpool sometimes returns null prices, https://github.com/custom-components/nordpool/issues/125 80 | # The nulls are typically at (tail of) "tomorrow", so simply removing them is reasonable: 81 | prices = [x for x in prices if x is not None] 82 | return prices[hour: hour + n] 83 | 84 | def _get_next_n_hours_from_entsoe(n, e): 85 | prices = [] 86 | if p := e.attributes.get("prices"): 87 | hour_before_now = dt.utcnow() - timedelta(hours=1) 88 | for item in p: 89 | if prices or hour_before_now <= datetime.fromisoformat(item["time"]): 90 | prices.append(item["price"]) 91 | if len(prices) == n: 92 | break 93 | return prices 94 | 95 | class NordpoolDiffSensor(SensorEntity): 96 | _attr_icon = "mdi:flash" 97 | 98 | def __init__(self, nordpool_entity_id, entsoe_entity_id, filter_length, filter_type, normalize, unit): 99 | self._nordpool_entity_id = nordpool_entity_id 100 | self._entsoe_entity_id = entsoe_entity_id 101 | self._filter_length = filter_length 102 | if normalize == MAX: 103 | normalize = lambda prices : 1 / (max(prices) if max(prices) > 0 else 1) 104 | normalize_suffix = "_normalize_max" 105 | elif normalize == MAX_MIN: 106 | normalize = lambda prices : 1 / (max(prices) - min(prices) if max(prices) - min(prices) > 0 else 1) 107 | normalize_suffix = "_normalize_max_min" 108 | elif normalize == SQRT_MAX: 109 | normalize = lambda prices: 1 / sqrt(max(prices) if max(prices) > 0 else 1) 110 | normalize_suffix = "_normalize_sqrt_max" 111 | elif normalize == MAX_MIN_SQRT_MAX: 112 | normalize = lambda prices: sqrt(max(prices) if max(prices) > 0 else 0) \ 113 | / (max(prices) - min(prices) if max(prices) - min(prices) > 0 else 1) 114 | normalize_suffix = "_normalize_max_min_sqrt_max" 115 | else: # NO 116 | normalize = lambda prices : 1 117 | normalize_suffix = "" 118 | if filter_type == RANK: 119 | self._compute = _with_rank 120 | elif filter_type == INTERVAL: 121 | self._compute = _with_interval 122 | elif filter_type == TRIANGLE: 123 | filter = [-1] 124 | triangular_number = (filter_length * (filter_length - 1)) / 2 125 | for i in range(filter_length - 1, 0, -1): 126 | filter += [i / triangular_number] 127 | self._compute = _with_filter(filter, normalize) 128 | else: # RECTANGLE 129 | filter = [-1] 130 | filter += [1 / (filter_length - 1)] * (filter_length - 1) 131 | self._compute = _with_filter(filter, normalize) 132 | self._attr_native_unit_of_measurement = unit 133 | self._attr_name = f"nordpool_diff_{filter_type}_{filter_length}{normalize_suffix}" 134 | # https://developers.home-assistant.io/docs/entity_registry_index/ : Entities should not include the domain in 135 | # their Unique ID as the system already accounts for these identifiers: 136 | self._attr_unique_id = f"{filter_type}_{filter_length}_{unit}{normalize_suffix}" 137 | self._state = self._next_hour = STATE_UNKNOWN 138 | 139 | @property 140 | def state(self): 141 | return self._state 142 | 143 | @property 144 | def extra_state_attributes(self): 145 | # TODO could also add self._nordpool_entity_id etc. useful properties here. 146 | return {"next_hour": self._next_hour} 147 | 148 | def update(self): 149 | prices = self._get_next_n_hours(self._filter_length + 1) # +1 to calculate next hour 150 | self._state = round(self._compute(prices[:-1]), 3) 151 | self._next_hour = round(self._compute(prices[1:]), 3) 152 | # TODO here could add caching, this really needs to be recalculated only each xx:00 if successful. 153 | 154 | def _get_next_n_hours(self, n): 155 | prices = [] 156 | # Prefer entsoe: 157 | if e := self.hass.states.get(self._entsoe_entity_id): 158 | try: 159 | prices = _get_next_n_hours_from_entsoe(n, e) 160 | _LOGGER.debug(f"{n} prices from entsoe {prices}") 161 | except: 162 | _LOGGER.exception("_get_next_n_hours_from_entsoe") 163 | # Fall back to nordpool: 164 | if (len(prices) < n) and (np := self.hass.states.get(self._nordpool_entity_id)): 165 | try: 166 | np_prices = _get_next_n_hours_from_nordpool(n, np) 167 | _LOGGER.debug(f"{n} prices from nordpool {np_prices}") 168 | if len(np_prices) > len(prices): 169 | prices = np_prices 170 | except: 171 | _LOGGER.exception("_get_next_n_hours_from_nordpool") 172 | # Fail gracefully if nothing works: 173 | if not prices: 174 | return n * [0] 175 | # Pad if needed, using last element. 176 | prices = prices + (n - len(prices)) * [prices[-1]] 177 | _LOGGER.debug(f"{n} prices after padding {prices}") 178 | return prices 179 | -------------------------------------------------------------------------------- /diff_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpulakka/nordpool_diff/e2ff6348d01f92432f92cc6f0b2bc9763d391a50/diff_example.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nordpool_diff", 3 | "render_readme": true 4 | } -------------------------------------------------------------------------------- /middle_change.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpulakka/nordpool_diff/e2ff6348d01f92432f92cc6f0b2bc9763d391a50/middle_change.png --------------------------------------------------------------------------------