Skip to content

Commit afcd616

Browse files
committed
initial version
0 parents  commit afcd616

18 files changed

+971
-0
lines changed

.github/workflows/hacs.yaml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: HACS Action
2+
3+
on:
4+
push:
5+
pull_request:
6+
schedule:
7+
- cron: "0 0 * * *"
8+
9+
jobs:
10+
hacs:
11+
name: HACS Action
12+
runs-on: "ubuntu-latest"
13+
steps:
14+
- uses: "actions/checkout@v2"
15+
- name: HACS Action
16+
uses: "hacs/action@main"
17+
with:
18+
category: "integration"

.github/workflows/hassfest.yaml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: Validate with hassfest
2+
3+
on:
4+
push:
5+
pull_request:
6+
schedule:
7+
- cron: "0 0 * * *"
8+
9+
jobs:
10+
validate:
11+
runs-on: "ubuntu-latest"
12+
steps:
13+
- uses: "actions/checkout@v2"
14+
- uses: home-assistant/actions/hassfest@master

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
__pycache__
2+
*.swp
3+
*.swo
4+
.vscode/
5+
.mypy_cache/
6+
venv/

.pre-commit-config.yaml

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
repos:
2+
- repo: https://github.com/asottile/pyupgrade
3+
rev: v2.32.1
4+
hooks:
5+
- id: pyupgrade
6+
args: [--py37-plus]
7+
- repo: https://github.com/psf/black
8+
rev: 22.3.0
9+
hooks:
10+
- id: black
11+
args:
12+
- --safe
13+
- --quiet
14+
files: ^((custom_components|script|tests)/.+)?[^/]+\.py$
15+
- repo: https://github.com/codespell-project/codespell
16+
rev: v2.1.0
17+
hooks:
18+
- id: codespell
19+
args:
20+
- --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing, Adresse, termine, adresse
21+
- --skip="./.*,*.csv,*.json"
22+
- --quiet-level=2
23+
exclude_types: [csv, json]
24+
- repo: https://github.com/pycqa/flake8
25+
rev: 3.9.2
26+
hooks:
27+
- id: flake8
28+
args:
29+
- --ignore=D100,D101,D102,D103,D104,D105,D107,E501,W503
30+
additional_dependencies:
31+
- flake8-docstrings==1.5.0
32+
- pydocstyle==5.0.2
33+
files: ^(custom_components|script|tests)/.+\.py$
34+
- repo: https://github.com/PyCQA/bandit
35+
rev: 1.7.4
36+
hooks:
37+
- id: bandit
38+
args:
39+
- --quiet
40+
- --format=custom
41+
- --configfile=tests/bandit.yaml
42+
files: ^(custom_components|script|tests)/.+\.py$
43+
- repo: https://github.com/pre-commit/mirrors-isort
44+
rev: v5.10.1
45+
hooks:
46+
- id: isort
47+
- repo: https://github.com/pre-commit/pre-commit-hooks
48+
rev: v4.2.0
49+
hooks:
50+
- id: check-executables-have-shebangs
51+
stages: [manual]
52+
- id: check-json
53+
- repo: https://github.com/pre-commit/mirrors-mypy
54+
rev: v0.960
55+
hooks:
56+
- id: mypy
57+
args:
58+
- --pretty
59+
- --show-error-codes
60+
- --show-error-context
61+
- --ignore-missing-imports

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 Steffen Zimmermann
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.

README.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# EPEX Spot
2+
3+
This component adds electricity prices from stock exchange [EPEX Spot](https://www.epexspot.com) to Home Assistant. [EPEX Spot](https://www.epexspot.com) does not provide free access to the data, so this component uses different ways to retrieve the data.
4+
5+
You can chose between multiple sources:
6+
7+
1. Awattar
8+
9+
[Awattar](https://www.awattar.de/services/api) provides a free of charge service for their customers. Market price data is available for Germany and Austria. So far no user identifiation is required.
10+
11+
2. EPEX Spot Web Scraper
12+
13+
This source uses web scraping technologies to retrieve publicly available data from its [website](https://www.epexspot.com/en/market-data).
14+
15+
If you like this component, please give it a star on [github](https://github.com/mampfes/hacs_epex_spot_awattar).
16+
17+
## Installation
18+
19+
1. Ensure that [HACS](https://hacs.xyz) is installed.
20+
2. Install **EPEX Spot** integration via HACS.
21+
3. Add **EPEX Spot** integration to Home Assistant:
22+
23+
[![badge](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start?domain=epex_spot)
24+
25+
In case you would like to install manually:
26+
27+
1. Copy the folder `custom_components/epex_spot` to `custom_components` in your Home Assistant `config` folder.
28+
2. Add **EPEX Spot** integration to Home Assistant:
29+
30+
[![badge](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start?domain=epex_spot)
31+
32+
## Sensors
33+
34+
This component provides one sensor for market prices. The sensor state is the current price in EUR/MWh.
35+
36+
Some sources (like EPEX Spot Web Scraper) provide additional sensors like buy volume, sell volume or volume.
37+
38+
### Sensor Attributes
39+
40+
In addition to the current market price, the price sensor also provides a list of upcoming prices per hour:
41+
42+
```yaml
43+
unit_of_measurement: EUR/MWh
44+
icon: mdi:currency-eur
45+
friendly_name: EPEX Spot DE-LU Price
46+
data:
47+
- start_time: '2022-12-15T23:00:00+00:00'
48+
end_time: '2022-12-16T00:00:00+00:00'
49+
price_eur_per_mwh: 296.3
50+
- start_time: '2022-12-16T00:00:00+00:00'
51+
end_time: '2022-12-16T01:00:00+00:00'
52+
price_eur_per_mwh: 288.12
53+
- start_time: '2022-12-16T01:00:00+00:00'
54+
end_time: '2022-12-16T02:00:00+00:00'
55+
price_eur_per_mwh: 280.19
56+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import logging
2+
from datetime import datetime, timedelta, timezone
3+
4+
import requests
5+
6+
_LOGGER = logging.getLogger(__name__)
7+
8+
9+
class Marketprice:
10+
UOM_EUR_PER_MWh = "EUR/MWh"
11+
UOM_CT_PER_KWh = "ct/KWh"
12+
13+
def __init__(self, data):
14+
assert data["unit"].lower() == self.UOM_EUR_PER_MWh.lower()
15+
self._start_time = datetime.fromtimestamp(
16+
data["start_timestamp"] / 1000, tz=timezone.utc
17+
)
18+
self._end_time = datetime.fromtimestamp(
19+
data["end_timestamp"] / 1000, tz=timezone.utc
20+
)
21+
self._price_eur_per_mwh = float(data["marketprice"])
22+
23+
def __repr__(self):
24+
return f"{self.__class__.__name__}(start: {self._start_time.isoformat()}, end: {self._end_time.isoformat()}, marketprice: {self._price_eur_per_mwh} {self.UOM_EUR_PER_MWh})"
25+
26+
@property
27+
def start_time(self):
28+
return self._start_time
29+
30+
@property
31+
def end_time(self):
32+
return self._end_time
33+
34+
@property
35+
def price_eur_per_mwh(self):
36+
return self._price_eur_per_mwh
37+
38+
@property
39+
def price_ct_per_kwh(self):
40+
return self._price_eur_per_mwh / 10
41+
42+
43+
class Awattar:
44+
URL = "https://api.awattar.{market_area}/v1/marketdata"
45+
46+
MARKET_AREAS = ("at", "de")
47+
48+
def __init__(self, market_area):
49+
self._market_area = market_area
50+
self._url = self.URL.format(market_area=market_area)
51+
self._marketprices = []
52+
53+
@property
54+
def name(self):
55+
return "Awattar API V1"
56+
57+
@property
58+
def market_area(self):
59+
return self._market_area
60+
61+
@property
62+
def marketprices(self):
63+
return self._marketprices
64+
65+
def fetch(self):
66+
data = self._fetch_data(self._url)
67+
self._marketprices = self._extract_marketprices(data["data"])
68+
69+
def _fetch_data(self, url):
70+
start = datetime.now(tz=timezone.utc).replace(
71+
hour=0, minute=0, second=0, microsecond=0
72+
)
73+
end = start + timedelta(days=2)
74+
r = requests.get(url, params={"start": start, "end": end})
75+
r.raise_for_status()
76+
return r.json()
77+
78+
def _extract_marketprices(self, data):
79+
entries = []
80+
for entry in data:
81+
entries.append(Marketprice(entry))
82+
return entries

0 commit comments

Comments
 (0)