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
67 changes: 61 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,24 +219,79 @@ Add 21% tax and overhead cost stored in a helper
- ```price_in_cents```: Boolean if prices is in cents

## Actions
Actions has recently been added. The action will just forward the raw response from the Nordpool API so you can capture the value your are interested in.

Example for an automation that get the last months averge price.
The following actions are available for fetching Nordpool price data:

| Action | Description |
|--------|-------------|
| `nordpool.hourly` | Hourly prices (today + tomorrow if available) |
| `nordpool.daily` | Daily average prices for the current month |
| `nordpool.weekly` | Weekly average prices |
| `nordpool.monthly` | Monthly average prices |
| `nordpool.yearly` | Yearly average prices |

All actions accept `currency`, `area` and optionally `year` as parameters.
The `nordpool.hourly` action also accepts an optional `date` parameter.

### Response structure

All actions return data in the following format:

```yaml
start: "2025-12-31T23:00:00+00:00"
end: "2021-12-30T23:00:00+00:00"
updated: "2024-03-26T13:32:39.733019+00:00"
currency: NOK
areas:
NO2:
values:
- start: "2025-12-31T23:00:00+00:00"
end: "2026-01-30T23:00:00+00:00"
value: 1230.05
- start: "2024-12-31T23:00:00+00:00"
end: "2025-12-30T23:00:00+00:00"
value: 767.23
```

To access a value in a template, use: `{{ np_result.areas.<AREA>["values"][<index>].value }}`

> **Note:** Use bracket notation `["values"]` instead of `.values` to avoid conflicts with Jinja2's built-in `dict.values()` method.

### Example: Store yearly average price

```yaml
alias: Example automation action call with storing with parsing and storing result
alias: Get yearly average price from Nordpool
triggers: null
actions:
- action: nordpool.yearly
data:
currency: NOK
area: NO2
year: "2024"
response_variable: np_result
- action: input_text.set_value
target:
entity_id: input_text.test
entity_id: input_text.yearly_price
data:
value: "{{ np_result.areas.NO2[\"values\"][0].value | float }}"
mode: single
```

### Example: Store this month's average price

```yaml
alias: Get monthly average price from Nordpool
triggers: null
actions:
- action: nordpool.monthly
data:
currency: EUR
area: FI
response_variable: np_result
- action: input_text.set_value
target:
entity_id: input_text.monthly_price
data:
value: "{{np_result.prices[0].averagePerArea.NO2 | float}}"
value: "{{ np_result.areas.FI[\"values\"][0].value | float }}"
mode: single
```

Expand Down
46 changes: 24 additions & 22 deletions custom_components/nordpool/aio_price.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def _parse_dt(self, time_str):
return timezone("Europe/Stockholm").localize(time).astimezone(utc)
return time.astimezone(utc)

def _parse_json(self, data, areas=None, data_type=None):
def _parse_json(self, data, areas=None, data_type=None, aggregation=None):
"""
Parse json response from fetcher.
Returns dictionary with
Expand All @@ -142,16 +142,16 @@ def _parse_json(self, data, areas=None, data_type=None):

_LOGGER.debug("data type in _parser %s, areas %s", data_type, areas)

# Ripped from Kipe's nordpool
if data_type == self.HOURLY:
# Select the correct data source based on aggregation level
if aggregation == "hourly":
data_source = ("multiAreaEntries", "entryPerArea")
elif data_type == self.DAILY:
elif aggregation == "daily":
data_source = ("multiAreaDailyAggregates", "averagePerArea")
elif data_type == self.WEEKLY:
elif aggregation == "weekly":
data_source = ("multiAreaWeeklyAggregates", "averagePerArea")
elif data_type == self.MONTHLY:
elif aggregation == "monthly":
data_source = ("multiAreaMonthlyAggregates", "averagePerArea")
elif data_type == self.YEARLY:
elif aggregation == "yearly":
data_source = ("prices", "averagePerArea")
else:
data_source = ("multiAreaEntries", "entryPerArea")
Expand Down Expand Up @@ -249,7 +249,7 @@ async def _fetch_json(self, data_type, end_date=None, areas=None):
# @backoff.on_exception(
# backoff.expo, (aiohttp.ClientError, KeyError), logger=_LOGGER, max_value=20
# )
async def fetch(self, data_type, end_date=None, areas=None, raw=False):
async def fetch(self, data_type, end_date=None, areas=None, raw=False, aggregation=None):
"""
Fetch data from API.
Inputs:
Expand Down Expand Up @@ -294,63 +294,65 @@ async def fetch(self, data_type, end_date=None, areas=None, raw=False):
self._fetch_json(data_type, tomorrow, areas),
]
else:
# This is really not today but a year..
# All except from hourly returns the raw values
return await self._fetch_json(data_type, today, areas)
# All except from hourly returns a single response
raw_data = await self._fetch_json(data_type, today, areas)
if raw_data is None:
return None
return await self._async_parse_json(raw_data, areas, data_type=data_type, aggregation=aggregation)

res = await asyncio.gather(*jobs)
raw = [
await self._async_parse_json(i, areas, data_type=data_type)
await self._async_parse_json(i, areas, data_type=data_type, aggregation=aggregation)
for i in res
if i
]

return await join_result_for_correct_time(raw, end_date)

async def _async_parse_json(self, data, areas, data_type):
async def _async_parse_json(self, data, areas, data_type, aggregation=None):
"""
Async version of _parse_json to prevent blocking calls inside the event loop.
"""
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
None, self._parse_json, data, areas, data_type
None, self._parse_json, data, areas, data_type, aggregation
)

async def hourly(self, end_date=None, areas=None, raw=False):
"""Helper to fetch hourly data, see Prices.fetch()"""
if areas is None:
areas = []
return await self.fetch(self.HOURLY, end_date, areas, raw=raw)
return await self.fetch(self.HOURLY, end_date, areas, raw=raw, aggregation="hourly")

async def daily(self, end_date=None, areas=None):
"""Helper to fetch daily data, see Prices.fetch()"""
if areas is None:
areas = []
return await self.fetch(self.DAILY, end_date, areas)
return await self.fetch(self.DAILY, end_date, areas, aggregation="daily")

async def weekly(self, end_date=None, areas=None):
"""Helper to fetch weekly data, see Prices.fetch()"""
if areas is None:
areas = []
return await self.fetch(self.WEEKLY, end_date, areas)
return await self.fetch(self.WEEKLY, end_date, areas, aggregation="weekly")

async def monthly(self, end_date=None, areas=None):
"""Helper to fetch monthly data, see Prices.fetch()"""
if areas is None:
areas = []
return await self.fetch(self.MONTHLY, end_date, areas)
return await self.fetch(self.MONTHLY, end_date, areas, aggregation="monthly")

async def yearly(self, end_date=None, areas=None):
"""Helper to fetch yearly data, see Prices.fetch()"""
if areas is None:
areas = []
return await self.fetch(self.YEARLY, end_date, areas)
return await self.fetch(self.YEARLY, end_date, areas, aggregation="yearly")

def _conv_to_float(self, s):
"""Convert numbers to float. Return infinity, if conversion fails."""
# Skip if already float
if isinstance(s, float):
return s
# Skip if already numeric
if isinstance(s, (int, float)):
return float(s)
try:
return float(s.replace(",", ".").replace(" ", ""))
except ValueError:
Expand Down
2 changes: 1 addition & 1 deletion custom_components/nordpool/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"DE-LU": "DE-LU",
"FR": "FR",
"NL": "NL",
"PL ": "PL",
"PL": "PL",
}

INVALID_VALUES = frozenset((None, float("inf")))
Expand Down
25 changes: 12 additions & 13 deletions custom_components/nordpool/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,33 @@
_LOGGER = logging.getLogger(__name__)


def check_setting(value):
def validator(value):
c = any([i for i in value if i in list(_REGIONS.keys())])
if c is not True:
vol.Invalid(
f"{value} in not in on of the supported areas {','.join(_REGIONS.keys())}"
def _validate_areas(value):
"""Validate area codes and ensure list format."""
areas = cv.ensure_list(value)
for area in areas:
if area not in _REGIONS:
raise vol.Invalid(
f"{area} is not in one of the supported areas {','.join(_REGIONS.keys())}"
)
return value

return validator
return areas


HOURLY_SCHEMA = vol.Schema(
{
vol.Required("currency"): str,
vol.Required("date"): cv.date,
vol.Required("area"): check_setting(cv.ensure_list),
vol.Required("area"): _validate_areas,
}
)


YEAR_SCHEMA = vol.Schema(
{
vol.Required("currency"): str,
vol.Required("year", default=dt_util.now().strftime("Y")): cv.matches_regex(
vol.Required("year", default=dt_util.now().strftime("%Y")): cv.matches_regex(
r"^[1|2]\d{3}$"
),
vol.Required("area"): check_setting(cv.ensure_list),
vol.Required("area"): _validate_areas,
}
)

Expand Down Expand Up @@ -85,7 +84,7 @@ async def weekly(service_call: ServiceCall):
sc = service_call.data
_LOGGER.debug("called weekly with %r", sc)

value = await AioPrices(sc["currency"], client).yearly(
value = await AioPrices(sc["currency"], client).weekly(
areas=sc["area"], end_date=sc["year"]
)

Expand Down