elec-tariffs 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- elec_tariffs-0.1.0/.github/dependabot.yml +18 -0
- elec_tariffs-0.1.0/.github/pull_request_template.md +9 -0
- elec_tariffs-0.1.0/.github/release.yml +12 -0
- elec_tariffs-0.1.0/.github/workflows/ci.yml +70 -0
- elec_tariffs-0.1.0/.github/workflows/release.yml +55 -0
- elec_tariffs-0.1.0/.gitignore +14 -0
- elec_tariffs-0.1.0/CLAUDE.md +25 -0
- elec_tariffs-0.1.0/LICENSE +21 -0
- elec_tariffs-0.1.0/PKG-INFO +149 -0
- elec_tariffs-0.1.0/README.md +126 -0
- elec_tariffs-0.1.0/SECURITY.md +6 -0
- elec_tariffs-0.1.0/pyproject.toml +80 -0
- elec_tariffs-0.1.0/requirements-dev.txt +8 -0
- elec_tariffs-0.1.0/src/elec_tariffs/__init__.py +87 -0
- elec_tariffs-0.1.0/src/elec_tariffs/_version.py +3 -0
- elec_tariffs-0.1.0/src/elec_tariffs/engine.py +140 -0
- elec_tariffs-0.1.0/src/elec_tariffs/errors.py +57 -0
- elec_tariffs-0.1.0/src/elec_tariffs/models.py +104 -0
- elec_tariffs-0.1.0/src/elec_tariffs/py.typed +0 -0
- elec_tariffs-0.1.0/src/elec_tariffs/sources/__init__.py +22 -0
- elec_tariffs-0.1.0/src/elec_tariffs/sources/tempo.py +394 -0
- elec_tariffs-0.1.0/src/elec_tariffs/sources/trv.py +121 -0
- elec_tariffs-0.1.0/tests/__init__.py +1 -0
- elec_tariffs-0.1.0/tests/conftest.py +104 -0
- elec_tariffs-0.1.0/tests/fixtures/rte_tempo_2024-2025.json +1 -0
- elec_tariffs-0.1.0/tests/fixtures/rte_tempo_2025-2026.json +1 -0
- elec_tariffs-0.1.0/tests/fixtures/rte_tempo_light.json +1 -0
- elec_tariffs-0.1.0/tests/fixtures/trv_base.csv +116 -0
- elec_tariffs-0.1.0/tests/fixtures/trv_hphc.csv +186 -0
- elec_tariffs-0.1.0/tests/fixtures/trv_tempo.csv +135 -0
- elec_tariffs-0.1.0/tests/test_engine.py +180 -0
- elec_tariffs-0.1.0/tests/test_models.py +54 -0
- elec_tariffs-0.1.0/tests/test_tempo.py +547 -0
- elec_tariffs-0.1.0/tests/test_trv.py +141 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: github-actions
|
|
4
|
+
directory: /
|
|
5
|
+
schedule:
|
|
6
|
+
interval: weekly
|
|
7
|
+
groups:
|
|
8
|
+
actions:
|
|
9
|
+
patterns: ["*"]
|
|
10
|
+
- package-ecosystem: pip
|
|
11
|
+
directory: /
|
|
12
|
+
schedule:
|
|
13
|
+
interval: weekly
|
|
14
|
+
cooldown:
|
|
15
|
+
default-days: 7
|
|
16
|
+
groups:
|
|
17
|
+
python:
|
|
18
|
+
patterns: ["*"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
lint:
|
|
18
|
+
name: Lint and types
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
timeout-minutes: 10
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
23
|
+
with:
|
|
24
|
+
persist-credentials: false
|
|
25
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.14"
|
|
28
|
+
cache: pip
|
|
29
|
+
cache-dependency-path: requirements-dev.txt
|
|
30
|
+
- run: python -m pip install --upgrade pip
|
|
31
|
+
- run: python -m pip install -r requirements-dev.txt -e .
|
|
32
|
+
- run: ruff check
|
|
33
|
+
- run: ruff format --check
|
|
34
|
+
- run: mypy
|
|
35
|
+
|
|
36
|
+
test:
|
|
37
|
+
name: Tests
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
timeout-minutes: 10
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
42
|
+
with:
|
|
43
|
+
persist-credentials: false
|
|
44
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
45
|
+
with:
|
|
46
|
+
python-version: "3.14"
|
|
47
|
+
cache: pip
|
|
48
|
+
cache-dependency-path: requirements-dev.txt
|
|
49
|
+
- run: python -m pip install --upgrade pip
|
|
50
|
+
- run: python -m pip install -r requirements-dev.txt -e .
|
|
51
|
+
- run: pytest --cov --cov-report=term-missing
|
|
52
|
+
|
|
53
|
+
build:
|
|
54
|
+
name: Build
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
timeout-minutes: 10
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
59
|
+
with:
|
|
60
|
+
persist-credentials: false
|
|
61
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
62
|
+
with:
|
|
63
|
+
python-version: "3.14"
|
|
64
|
+
- run: python -m pip install build==1.6.1
|
|
65
|
+
- run: python -m build
|
|
66
|
+
- name: Install the wheel in a clean environment
|
|
67
|
+
run: |
|
|
68
|
+
python -m venv /tmp/check
|
|
69
|
+
/tmp/check/bin/pip install dist/*.whl
|
|
70
|
+
/tmp/check/bin/python -c "import elec_tariffs; print(elec_tariffs.__version__)"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions: {}
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
name: Build
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
timeout-minutes: 10
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
18
|
+
with:
|
|
19
|
+
persist-credentials: false
|
|
20
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.14"
|
|
23
|
+
- name: Check that the tag matches the version
|
|
24
|
+
env:
|
|
25
|
+
TAG: ${{ github.event.release.tag_name }}
|
|
26
|
+
run: |
|
|
27
|
+
version=$(python -c 'import runpy; print(runpy.run_path("src/elec_tariffs/_version.py")["__version__"])')
|
|
28
|
+
if [ "$TAG" != "v$version" ]; then
|
|
29
|
+
echo "Tag $TAG does not match version $version" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
- run: python -m pip install build==1.6.1
|
|
33
|
+
- run: python -m build
|
|
34
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
35
|
+
with:
|
|
36
|
+
name: dist
|
|
37
|
+
path: dist/
|
|
38
|
+
if-no-files-found: error
|
|
39
|
+
|
|
40
|
+
publish:
|
|
41
|
+
name: Publish to PyPI
|
|
42
|
+
needs: build
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
timeout-minutes: 10
|
|
45
|
+
environment:
|
|
46
|
+
name: pypi
|
|
47
|
+
url: https://pypi.org/p/elec-tariffs
|
|
48
|
+
permissions:
|
|
49
|
+
id-token: write
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
52
|
+
with:
|
|
53
|
+
name: dist
|
|
54
|
+
path: dist/
|
|
55
|
+
- uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# elec-tariffs
|
|
2
|
+
|
|
3
|
+
Library behind the Electricity Contracts Home Assistant integration (damou8/electricity_contracts). No Home Assistant import here.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
.venv/bin/pip install -r requirements-dev.txt -e .
|
|
9
|
+
.venv/bin/ruff check && .venv/bin/ruff format --check && .venv/bin/mypy
|
|
10
|
+
.venv/bin/pytest --cov # fails under 95 % coverage
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Conventions
|
|
14
|
+
|
|
15
|
+
- Python 3.14, `mypy --strict`, ruff `ALL`. Annotations are lazy: no `TYPE_CHECKING` blocks.
|
|
16
|
+
- Keep `aiohttp` in `requirements-dev.txt` at Home Assistant's pin (`homeassistant/package_constraints.txt`).
|
|
17
|
+
- The version lives in `src/elec_tariffs/_version.py`; the release tag must be `v` + that version.
|
|
18
|
+
- Tests make no network request: recorded answers in `tests/fixtures/`, HTTP through the `server` fixture (a local aiohttp server) and the clients' `base_url` / `urls` arguments.
|
|
19
|
+
|
|
20
|
+
## Pitfalls
|
|
21
|
+
|
|
22
|
+
- aioresponses 0.7.9 doesn't work with aiohttp 3.14 (`ClientResponse` needs `stream_writer`): don't bring it back.
|
|
23
|
+
- aiohttp 3.14 deprecates `BasicAuth`; the official RTE client builds the header itself.
|
|
24
|
+
- aiohttp retries a GET once when a reused connection drops: a test of a dropped connection must queue the failure twice.
|
|
25
|
+
- The public RTE endpoint is undocumented. Keep its parsing strict (`TempoFormatError` on anything unexpected) rather than lenient.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 damou8
|
|
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.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: elec-tariffs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cost French electricity consumption hour by hour under Base, HP/HC and Tempo contracts
|
|
5
|
+
Project-URL: Homepage, https://github.com/damou8/elec-tariffs
|
|
6
|
+
Project-URL: Issues, https://github.com/damou8/elec-tariffs/issues
|
|
7
|
+
Author: damou8
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: edf,electricity,home-assistant,linky,rte,tariff,tempo
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Home Automation
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.14
|
|
21
|
+
Requires-Dist: aiohttp>=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# elec-tariffs
|
|
25
|
+
|
|
26
|
+
Cost French electricity consumption hour by hour under Base, HP/HC and Tempo contracts.
|
|
27
|
+
|
|
28
|
+
`elec-tariffs` takes hourly consumption (as a Linky meter reports it) and works out what it cost under one or more contracts: which tranche each hour falls into, its price on that day, and its share of the yearly subscription. It also downloads the data that needs:
|
|
29
|
+
|
|
30
|
+
- the regulated prices (Tarif Bleu) of every period since 2012, from [data.gouv.fr](https://www.data.gouv.fr/fr/datasets/historique-des-tarifs-reglementes-de-vente-delectricite-pour-les-consommateurs-residentiels/);
|
|
31
|
+
- the color of each Tempo day, from RTE.
|
|
32
|
+
|
|
33
|
+
It is async (aiohttp), fully typed and has no dependency on Home Assistant. It was written for the [Electricity Contracts](https://github.com/damou8/electricity_contracts) Home Assistant integration.
|
|
34
|
+
|
|
35
|
+
It is not affiliated with EDF, RTE or Enedis.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install elec-tariffs
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Python 3.14 or later.
|
|
44
|
+
|
|
45
|
+
## Use
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import asyncio
|
|
49
|
+
from datetime import date, datetime
|
|
50
|
+
from zoneinfo import ZoneInfo
|
|
51
|
+
|
|
52
|
+
import aiohttp
|
|
53
|
+
|
|
54
|
+
from elec_tariffs import (
|
|
55
|
+
ContractKind,
|
|
56
|
+
ContractRules,
|
|
57
|
+
RtePublicTempoClient,
|
|
58
|
+
TrvClient,
|
|
59
|
+
cost_hour,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
PARIS = ZoneInfo("Europe/Paris")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def main() -> None:
|
|
66
|
+
async with aiohttp.ClientSession() as session:
|
|
67
|
+
prices = await TrvClient(session).async_price_periods(ContractKind.TEMPO, 6)
|
|
68
|
+
colors = await RtePublicTempoClient(session).async_calendar(
|
|
69
|
+
date(2026, 1, 4), date(2026, 1, 5)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# 1.8 kWh used from 19:00 to 20:00 on 5 January 2026.
|
|
73
|
+
cost = cost_hour(
|
|
74
|
+
datetime(2026, 1, 5, 19, tzinfo=PARIS),
|
|
75
|
+
1.8,
|
|
76
|
+
ContractRules(kind=ContractKind.TEMPO),
|
|
77
|
+
prices,
|
|
78
|
+
colors,
|
|
79
|
+
)
|
|
80
|
+
print(cost.energy, cost.variable, cost.fixed, cost.total)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
An HP/HC contract takes its own off-peak ranges, to the half hour, and can make weekends off-peak:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from datetime import time
|
|
90
|
+
|
|
91
|
+
from elec_tariffs import ContractKind, ContractRules, TimeRange
|
|
92
|
+
|
|
93
|
+
rules = ContractRules(
|
|
94
|
+
kind=ContractKind.HPHC,
|
|
95
|
+
off_peak=(
|
|
96
|
+
TimeRange(start=time(12, 30), end=time(14)),
|
|
97
|
+
TimeRange(start=time(22), end=time(6, 30)),
|
|
98
|
+
),
|
|
99
|
+
weekend_off_peak=True,
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Prices you set yourself are `PricePeriod` objects: a first and last day (`None` while in force), the yearly fixed part and a price per tranche, all with taxes.
|
|
104
|
+
|
|
105
|
+
## How an hour is costed
|
|
106
|
+
|
|
107
|
+
- **Tranches.** Each hour is split into two half hours with half the energy each, so an off-peak range that starts or ends on a half hour is followed. This assumes the consumption is even within the hour.
|
|
108
|
+
- **Tempo.** A Tempo day runs from 6:00 to 6:00, so a night takes the color of the day that started the evening before. Off-peak hours are 22:00 to 6:00.
|
|
109
|
+
- **Prices** change at midnight on the first day of a price period (1 February and 1 August for the regulated prices). An hour uses the prices of its calendar day.
|
|
110
|
+
- **Subscription.** The yearly fixed part is spread evenly over the hours of the year (8,760, or 8,784 in a leap year).
|
|
111
|
+
- **Missing data.** An hour whose Tempo color or price isn't known raises `MissingColorError` or `MissingPriceError` instead of guessing.
|
|
112
|
+
|
|
113
|
+
## Tempo calendar sources
|
|
114
|
+
|
|
115
|
+
| Source | Account | Notes |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| `RtePublicTempoClient` | none | The backend of RTE's public [Tempo calendar page](https://www.services-rte.com/fr/visualisez-les-donnees-publiees-par-rte/calendrier-des-offres-de-fourniture-de-type-tempo.html). It isn't documented and RTE can change it without notice, so its answers are checked strictly: an unexpected format raises `TempoFormatError` instead of giving wrong colors. |
|
|
118
|
+
| `RteOfficialTempoClient` | free [data.rte-france.com](https://data.rte-france.com/catalog/-/api/consumption/Tempo-Like-Supply-Contract/v1.1) application | RTE's documented "Tempo Like Supply Contract" API, with OAuth2 client credentials. The token is kept until it expires and renewed once if refused. Long ranges are split into requests of 366 days at most. **Not yet tested against the live API**: its tests follow the format used by other clients. |
|
|
119
|
+
|
|
120
|
+
`FallbackTempoSource` asks each source in turn, for example the public one first and the official one when it fails. It records which source answered and the errors of the others.
|
|
121
|
+
|
|
122
|
+
Past colors never change, so callers should cache them and only ask for the current season and the next day.
|
|
123
|
+
|
|
124
|
+
To create the official API application: sign up on data.rte-france.com, open the "Tempo Like Supply Contract" API, choose "S'abonner à l'API" (this creates an application), then copy its client ID and secret.
|
|
125
|
+
|
|
126
|
+
## Errors
|
|
127
|
+
|
|
128
|
+
Every error derives from `ElecTariffsError`:
|
|
129
|
+
|
|
130
|
+
- `MissingColorError`, `MissingPriceError` (with the `day` concerned);
|
|
131
|
+
- `TrvServerError`, `TrvFormatError` for data.gouv.fr;
|
|
132
|
+
- `TempoServerError` (network or 5xx), `TempoRateLimitError` (429), `TempoAuthError` (credentials refused) and `TempoFormatError` for the Tempo sources.
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
python3.14 -m venv .venv
|
|
138
|
+
.venv/bin/pip install -r requirements-dev.txt -e .
|
|
139
|
+
.venv/bin/ruff check && .venv/bin/ruff format --check && .venv/bin/mypy
|
|
140
|
+
.venv/bin/pytest --cov
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The tests use recorded answers in `tests/fixtures/` and a local HTTP server; they make no network requests.
|
|
144
|
+
|
|
145
|
+
Releases are published to PyPI by GitHub Actions when a GitHub release is published; the tag (`vX.Y.Z`) must match `src/elec_tariffs/_version.py`.
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# elec-tariffs
|
|
2
|
+
|
|
3
|
+
Cost French electricity consumption hour by hour under Base, HP/HC and Tempo contracts.
|
|
4
|
+
|
|
5
|
+
`elec-tariffs` takes hourly consumption (as a Linky meter reports it) and works out what it cost under one or more contracts: which tranche each hour falls into, its price on that day, and its share of the yearly subscription. It also downloads the data that needs:
|
|
6
|
+
|
|
7
|
+
- the regulated prices (Tarif Bleu) of every period since 2012, from [data.gouv.fr](https://www.data.gouv.fr/fr/datasets/historique-des-tarifs-reglementes-de-vente-delectricite-pour-les-consommateurs-residentiels/);
|
|
8
|
+
- the color of each Tempo day, from RTE.
|
|
9
|
+
|
|
10
|
+
It is async (aiohttp), fully typed and has no dependency on Home Assistant. It was written for the [Electricity Contracts](https://github.com/damou8/electricity_contracts) Home Assistant integration.
|
|
11
|
+
|
|
12
|
+
It is not affiliated with EDF, RTE or Enedis.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install elec-tariffs
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Python 3.14 or later.
|
|
21
|
+
|
|
22
|
+
## Use
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import asyncio
|
|
26
|
+
from datetime import date, datetime
|
|
27
|
+
from zoneinfo import ZoneInfo
|
|
28
|
+
|
|
29
|
+
import aiohttp
|
|
30
|
+
|
|
31
|
+
from elec_tariffs import (
|
|
32
|
+
ContractKind,
|
|
33
|
+
ContractRules,
|
|
34
|
+
RtePublicTempoClient,
|
|
35
|
+
TrvClient,
|
|
36
|
+
cost_hour,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
PARIS = ZoneInfo("Europe/Paris")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def main() -> None:
|
|
43
|
+
async with aiohttp.ClientSession() as session:
|
|
44
|
+
prices = await TrvClient(session).async_price_periods(ContractKind.TEMPO, 6)
|
|
45
|
+
colors = await RtePublicTempoClient(session).async_calendar(
|
|
46
|
+
date(2026, 1, 4), date(2026, 1, 5)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# 1.8 kWh used from 19:00 to 20:00 on 5 January 2026.
|
|
50
|
+
cost = cost_hour(
|
|
51
|
+
datetime(2026, 1, 5, 19, tzinfo=PARIS),
|
|
52
|
+
1.8,
|
|
53
|
+
ContractRules(kind=ContractKind.TEMPO),
|
|
54
|
+
prices,
|
|
55
|
+
colors,
|
|
56
|
+
)
|
|
57
|
+
print(cost.energy, cost.variable, cost.fixed, cost.total)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
asyncio.run(main())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
An HP/HC contract takes its own off-peak ranges, to the half hour, and can make weekends off-peak:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from datetime import time
|
|
67
|
+
|
|
68
|
+
from elec_tariffs import ContractKind, ContractRules, TimeRange
|
|
69
|
+
|
|
70
|
+
rules = ContractRules(
|
|
71
|
+
kind=ContractKind.HPHC,
|
|
72
|
+
off_peak=(
|
|
73
|
+
TimeRange(start=time(12, 30), end=time(14)),
|
|
74
|
+
TimeRange(start=time(22), end=time(6, 30)),
|
|
75
|
+
),
|
|
76
|
+
weekend_off_peak=True,
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Prices you set yourself are `PricePeriod` objects: a first and last day (`None` while in force), the yearly fixed part and a price per tranche, all with taxes.
|
|
81
|
+
|
|
82
|
+
## How an hour is costed
|
|
83
|
+
|
|
84
|
+
- **Tranches.** Each hour is split into two half hours with half the energy each, so an off-peak range that starts or ends on a half hour is followed. This assumes the consumption is even within the hour.
|
|
85
|
+
- **Tempo.** A Tempo day runs from 6:00 to 6:00, so a night takes the color of the day that started the evening before. Off-peak hours are 22:00 to 6:00.
|
|
86
|
+
- **Prices** change at midnight on the first day of a price period (1 February and 1 August for the regulated prices). An hour uses the prices of its calendar day.
|
|
87
|
+
- **Subscription.** The yearly fixed part is spread evenly over the hours of the year (8,760, or 8,784 in a leap year).
|
|
88
|
+
- **Missing data.** An hour whose Tempo color or price isn't known raises `MissingColorError` or `MissingPriceError` instead of guessing.
|
|
89
|
+
|
|
90
|
+
## Tempo calendar sources
|
|
91
|
+
|
|
92
|
+
| Source | Account | Notes |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `RtePublicTempoClient` | none | The backend of RTE's public [Tempo calendar page](https://www.services-rte.com/fr/visualisez-les-donnees-publiees-par-rte/calendrier-des-offres-de-fourniture-de-type-tempo.html). It isn't documented and RTE can change it without notice, so its answers are checked strictly: an unexpected format raises `TempoFormatError` instead of giving wrong colors. |
|
|
95
|
+
| `RteOfficialTempoClient` | free [data.rte-france.com](https://data.rte-france.com/catalog/-/api/consumption/Tempo-Like-Supply-Contract/v1.1) application | RTE's documented "Tempo Like Supply Contract" API, with OAuth2 client credentials. The token is kept until it expires and renewed once if refused. Long ranges are split into requests of 366 days at most. **Not yet tested against the live API**: its tests follow the format used by other clients. |
|
|
96
|
+
|
|
97
|
+
`FallbackTempoSource` asks each source in turn, for example the public one first and the official one when it fails. It records which source answered and the errors of the others.
|
|
98
|
+
|
|
99
|
+
Past colors never change, so callers should cache them and only ask for the current season and the next day.
|
|
100
|
+
|
|
101
|
+
To create the official API application: sign up on data.rte-france.com, open the "Tempo Like Supply Contract" API, choose "S'abonner à l'API" (this creates an application), then copy its client ID and secret.
|
|
102
|
+
|
|
103
|
+
## Errors
|
|
104
|
+
|
|
105
|
+
Every error derives from `ElecTariffsError`:
|
|
106
|
+
|
|
107
|
+
- `MissingColorError`, `MissingPriceError` (with the `day` concerned);
|
|
108
|
+
- `TrvServerError`, `TrvFormatError` for data.gouv.fr;
|
|
109
|
+
- `TempoServerError` (network or 5xx), `TempoRateLimitError` (429), `TempoAuthError` (credentials refused) and `TempoFormatError` for the Tempo sources.
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python3.14 -m venv .venv
|
|
115
|
+
.venv/bin/pip install -r requirements-dev.txt -e .
|
|
116
|
+
.venv/bin/ruff check && .venv/bin/ruff format --check && .venv/bin/mypy
|
|
117
|
+
.venv/bin/pytest --cov
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The tests use recorded answers in `tests/fixtures/` and a local HTTP server; they make no network requests.
|
|
121
|
+
|
|
122
|
+
Releases are published to PyPI by GitHub Actions when a GitHub release is published; the tag (`vX.Y.Z`) must match `src/elec_tariffs/_version.py`.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "elec-tariffs"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Cost French electricity consumption hour by hour under Base, HP/HC and Tempo contracts"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "damou8" }]
|
|
13
|
+
requires-python = ">=3.14"
|
|
14
|
+
dependencies = ["aiohttp>=3.10"]
|
|
15
|
+
keywords = ["electricity", "tariff", "tempo", "edf", "rte", "linky", "home-assistant"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Framework :: AsyncIO",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Natural Language :: English",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Topic :: Home Automation",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/damou8/elec-tariffs"
|
|
30
|
+
Issues = "https://github.com/damou8/elec-tariffs/issues"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.version]
|
|
33
|
+
path = "src/elec_tariffs/_version.py"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/elec_tariffs"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
asyncio_mode = "auto"
|
|
40
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
41
|
+
filterwarnings = ["error"]
|
|
42
|
+
testpaths = ["tests"]
|
|
43
|
+
addopts = "--strict-markers"
|
|
44
|
+
|
|
45
|
+
[tool.coverage.run]
|
|
46
|
+
source = ["elec_tariffs"]
|
|
47
|
+
branch = true
|
|
48
|
+
|
|
49
|
+
[tool.coverage.report]
|
|
50
|
+
fail_under = 95
|
|
51
|
+
show_missing = true
|
|
52
|
+
exclude_also = ["if TYPE_CHECKING:", "raise NotImplementedError", "\\.\\.\\."]
|
|
53
|
+
|
|
54
|
+
[tool.mypy]
|
|
55
|
+
strict = true
|
|
56
|
+
python_version = "3.14"
|
|
57
|
+
files = ["src", "tests"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
target-version = "py314"
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
select = ["ALL"]
|
|
64
|
+
ignore = [
|
|
65
|
+
"ANN401", # Any is needed for JSON payloads
|
|
66
|
+
"COM812", # incompatible with the formatter
|
|
67
|
+
"CPY001", # the license is in LICENSE
|
|
68
|
+
"D203", # incompatible with D211
|
|
69
|
+
"D213", # incompatible with D212
|
|
70
|
+
"ISC001", # incompatible with the formatter
|
|
71
|
+
"TC", # annotations are lazy since Python 3.14, no TYPE_CHECKING blocks
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint.per-file-ignores]
|
|
75
|
+
"tests/**" = [
|
|
76
|
+
"S101", # pytest asserts
|
|
77
|
+
"PLR2004", # magic values are the expected results
|
|
78
|
+
"D103", # test names say what they check
|
|
79
|
+
"SLF001", # tests read private state on purpose
|
|
80
|
+
]
|