myelectricaldatapy 2.2.8__tar.gz → 2.3.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.
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.github/workflows/release.yml +0 -8
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/PKG-INFO +25 -2
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/README.md +24 -0
- myelectricaldatapy-2.3.0/myelectricaldatapy/__init__.py +48 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/analytics.py +7 -5
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/api.py +14 -16
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/auth.py +5 -5
- myelectricaldatapy-2.3.0/myelectricaldatapy/const.py +25 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/mypdl.py +165 -163
- myelectricaldatapy-2.3.0/myelectricaldatapy/types.py +71 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/tz.py +23 -2
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy.egg-info/PKG-INFO +25 -2
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy.egg-info/SOURCES.txt +2 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy.egg-info/requires.txt +0 -1
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy.egg-info/scm_file_list.json +3 -1
- myelectricaldatapy-2.3.0/myelectricaldatapy.egg-info/scm_version.json +8 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/pyproject.toml +0 -1
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/conftest.py +2 -28
- myelectricaldatapy-2.3.0/tests/test_analytics.py +346 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/test_load_data.py +21 -14
- myelectricaldatapy-2.3.0/uv.lock +1008 -0
- myelectricaldatapy-2.2.8/myelectricaldatapy/__init__.py +0 -19
- myelectricaldatapy-2.2.8/myelectricaldatapy/const.py +0 -21
- myelectricaldatapy-2.2.8/myelectricaldatapy.egg-info/scm_version.json +0 -8
- myelectricaldatapy-2.2.8/tests/test_analytics.py +0 -361
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.devcontainer/devcontainer-lock.json +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.devcontainer/devcontainer.json +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.github/dependabot.yml +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.github/workflows/auto-approve.yml +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.github/workflows/python-lint.yml +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.github/workflows/python-publish.yml +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.gitignore +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.pre-commit-config.yaml +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.vscode/settings.json +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/LICENSE +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/_secrets.yaml +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/example.py +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/exceptions.py +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/py.typed +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy.egg-info/dependency_links.txt +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy.egg-info/top_level.txt +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/setup.cfg +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/__init__.py +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/consts.py +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/access.json +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/address.json +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/contract.json +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/daily.json +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/detail.json +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/ecowatt.json +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/invalid_access.json +0 -0
- {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/tempo.json +0 -0
|
@@ -25,11 +25,3 @@ jobs:
|
|
|
25
25
|
draft: false
|
|
26
26
|
prerelease: ${{ contains(github.ref_name, '-') }}
|
|
27
27
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
28
|
-
publish:
|
|
29
|
-
needs: release
|
|
30
|
-
permissions:
|
|
31
|
-
contents: write
|
|
32
|
-
packages: write
|
|
33
|
-
id-token: write
|
|
34
|
-
if: ${{ ! contains(github.ref_name, '-') }}
|
|
35
|
-
uses: ./.github/workflows/python-publish.yml
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: myelectricaldatapy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Fetch Linky data from myelectricaldata.fr
|
|
5
5
|
Author-email: Cyr-ius <cyr-ius@ipocus.net>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -21,7 +21,6 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: aiohttp>=3.8.1
|
|
23
23
|
Requires-Dist: pandas>=2.0.2
|
|
24
|
-
Requires-Dist: voluptuous>=0.13.1
|
|
25
24
|
Provides-Extra: dev
|
|
26
25
|
Requires-Dist: prek; extra == "dev"
|
|
27
26
|
Requires-Dist: ruff; extra == "dev"
|
|
@@ -76,6 +75,30 @@ $ python setup.py install
|
|
|
76
75
|
- async_load (start: datetime, end: datetime) Return None - Load Data in power_Data attribute
|
|
77
76
|
- async_refresh Return None - Refresh power_Data , tempo_day and ecowatt attributes.
|
|
78
77
|
|
|
78
|
+
## Timezone
|
|
79
|
+
|
|
80
|
+
Enedis/RTE APIs return naive timestamps that represent local wall-clock time. This
|
|
81
|
+
library needs to know which timezone that is in order to compute things like
|
|
82
|
+
off-peak hours or hourly statistics correctly.
|
|
83
|
+
|
|
84
|
+
By default it falls back to the timezone of the host machine running the code
|
|
85
|
+
(`/etc/localtime`), but that has no reason to match the timezone your
|
|
86
|
+
application is actually configured for (e.g. Home Assistant's
|
|
87
|
+
`hass.config.time_zone`). Pass it explicitly to avoid a fixed-offset shift in
|
|
88
|
+
the reported times:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
api = EnedisByPDL(token=TOKEN, pdl=PDL, timezone=ZoneInfo("Europe/Paris"))
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Alternatively, set it once globally for every instance:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from myelectricaldatapy import set_local_timezone
|
|
98
|
+
|
|
99
|
+
set_local_timezone(ZoneInfo("Europe/Paris"))
|
|
100
|
+
```
|
|
101
|
+
|
|
79
102
|
## Get started
|
|
80
103
|
|
|
81
104
|
```python
|
|
@@ -41,6 +41,30 @@ $ python setup.py install
|
|
|
41
41
|
- async_load (start: datetime, end: datetime) Return None - Load Data in power_Data attribute
|
|
42
42
|
- async_refresh Return None - Refresh power_Data , tempo_day and ecowatt attributes.
|
|
43
43
|
|
|
44
|
+
## Timezone
|
|
45
|
+
|
|
46
|
+
Enedis/RTE APIs return naive timestamps that represent local wall-clock time. This
|
|
47
|
+
library needs to know which timezone that is in order to compute things like
|
|
48
|
+
off-peak hours or hourly statistics correctly.
|
|
49
|
+
|
|
50
|
+
By default it falls back to the timezone of the host machine running the code
|
|
51
|
+
(`/etc/localtime`), but that has no reason to match the timezone your
|
|
52
|
+
application is actually configured for (e.g. Home Assistant's
|
|
53
|
+
`hass.config.time_zone`). Pass it explicitly to avoid a fixed-offset shift in
|
|
54
|
+
the reported times:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
api = EnedisByPDL(token=TOKEN, pdl=PDL, timezone=ZoneInfo("Europe/Paris"))
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Alternatively, set it once globally for every instance:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from myelectricaldatapy import set_local_timezone
|
|
64
|
+
|
|
65
|
+
set_local_timezone(ZoneInfo("Europe/Paris"))
|
|
66
|
+
```
|
|
67
|
+
|
|
44
68
|
## Get started
|
|
45
69
|
|
|
46
70
|
```python
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""myelectricaldatapy package."""
|
|
2
|
+
|
|
3
|
+
from .api import Enedis
|
|
4
|
+
from .const import *
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
EnedisException,
|
|
7
|
+
HttpRequestError,
|
|
8
|
+
LimitReached,
|
|
9
|
+
TimeoutExceededError,
|
|
10
|
+
)
|
|
11
|
+
from .mypdl import EnedisByPDL, FormatError, validate_cumsum, validate_prices
|
|
12
|
+
from .types import (
|
|
13
|
+
ConsumptionCollect,
|
|
14
|
+
Cum,
|
|
15
|
+
EnergyCollect,
|
|
16
|
+
Mode,
|
|
17
|
+
Prices,
|
|
18
|
+
ProductionCollect,
|
|
19
|
+
Service,
|
|
20
|
+
StandardPrice,
|
|
21
|
+
Subscription,
|
|
22
|
+
TempoPrice,
|
|
23
|
+
)
|
|
24
|
+
from .tz import get_local_timezone, set_local_timezone
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"ConsumptionCollect",
|
|
28
|
+
"Cum",
|
|
29
|
+
"Enedis",
|
|
30
|
+
"EnedisByPDL",
|
|
31
|
+
"EnedisException",
|
|
32
|
+
"EnergyCollect",
|
|
33
|
+
"FormatError",
|
|
34
|
+
"HttpRequestError",
|
|
35
|
+
"LimitReached",
|
|
36
|
+
"Mode",
|
|
37
|
+
"Prices",
|
|
38
|
+
"ProductionCollect",
|
|
39
|
+
"Service",
|
|
40
|
+
"StandardPrice",
|
|
41
|
+
"Subscription",
|
|
42
|
+
"TempoPrice",
|
|
43
|
+
"TimeoutExceededError",
|
|
44
|
+
"get_local_timezone",
|
|
45
|
+
"set_local_timezone",
|
|
46
|
+
"validate_cumsum",
|
|
47
|
+
"validate_prices",
|
|
48
|
+
]
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
"""Class for analytics."""
|
|
2
2
|
|
|
3
|
-
from datetime import datetime as dt, timedelta
|
|
3
|
+
from datetime import datetime as dt, timedelta, tzinfo as _tzinfo
|
|
4
4
|
import re
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
import pandas as pd
|
|
8
8
|
|
|
9
9
|
from .const import ATTR_OFFPEAK, ATTR_STANDARD, TEMPO_DAYS
|
|
10
|
-
from .tz import
|
|
10
|
+
from .tz import get_local_timezone
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class EnedisAnalytics:
|
|
14
14
|
"""Data analytics."""
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def __init__(self, data: Any) -> None:
|
|
16
|
+
def __init__(self, data: Any, timezone: _tzinfo | None = None) -> None:
|
|
19
17
|
"""Initialize Dataframe."""
|
|
20
18
|
self.df = pd.DataFrame(data)
|
|
19
|
+
# Resolved per instance (not at import/class-definition time) so a
|
|
20
|
+
# timezone set later via tz.set_local_timezone(), or passed here
|
|
21
|
+
# explicitly, is honored.
|
|
22
|
+
self.local_timezone = timezone or get_local_timezone()
|
|
21
23
|
|
|
22
24
|
def get_data_analytics(
|
|
23
25
|
self,
|
|
@@ -9,9 +9,10 @@ from typing import Any
|
|
|
9
9
|
from aiohttp import ClientSession
|
|
10
10
|
|
|
11
11
|
from .auth import EnedisAuth
|
|
12
|
-
from .const import DAILY_CONSUM, DAILY_PROD, DETAIL_CONSUM, DETAIL_PROD
|
|
12
|
+
from .const import DAILY_CONSUM, DAILY_PROD, DETAIL_CONSUM, DETAIL_PROD
|
|
13
13
|
from .exceptions import EnedisException
|
|
14
|
-
from .
|
|
14
|
+
from .types import Service
|
|
15
|
+
from .tz import as_local, get_local_timezone, local_now
|
|
15
16
|
|
|
16
17
|
_LOGGER = logging.getLogger(__name__)
|
|
17
18
|
|
|
@@ -20,7 +21,7 @@ class Enedis:
|
|
|
20
21
|
"""Enedis API."""
|
|
21
22
|
|
|
22
23
|
def __init__(
|
|
23
|
-
self, token: str, session: ClientSession | None = None, timeout: int =
|
|
24
|
+
self, token: str, session: ClientSession | None = None, timeout: int = 30
|
|
24
25
|
) -> None:
|
|
25
26
|
"""Initialize."""
|
|
26
27
|
session = session or ClientSession()
|
|
@@ -30,15 +31,9 @@ class Enedis:
|
|
|
30
31
|
self.last_access: date | None = None
|
|
31
32
|
|
|
32
33
|
async def async_fetch_datas(
|
|
33
|
-
self, service:
|
|
34
|
+
self, service: Service, pdl: str, start: dt | None = None, end: dt | None = None
|
|
34
35
|
) -> Any:
|
|
35
|
-
"""Retrieve date from service.
|
|
36
|
-
|
|
37
|
-
service: contracts, identity, contact, addresses,
|
|
38
|
-
daily_consumption_max_power,
|
|
39
|
-
daily_consumption, daily_production,
|
|
40
|
-
consumption_load_curve, production_load_curve
|
|
41
|
-
"""
|
|
36
|
+
"""Retrieve date from service."""
|
|
42
37
|
self.last_access = local_now()
|
|
43
38
|
path_range = ""
|
|
44
39
|
if start and end:
|
|
@@ -126,16 +121,17 @@ class Enedis:
|
|
|
126
121
|
if await self.async_has_offpeak(pdl) is True:
|
|
127
122
|
# Off-peak windows are defined in local wall-clock time, so a
|
|
128
123
|
# start given in another timezone must be converted first.
|
|
129
|
-
|
|
124
|
+
local_timezone = get_local_timezone()
|
|
125
|
+
start_time = as_local(start).astimezone(local_timezone).time()
|
|
130
126
|
for range_time in self.offpeaks:
|
|
131
127
|
starting = (
|
|
132
128
|
dt.strptime(range_time[0], "%HH%M")
|
|
133
|
-
.replace(tzinfo=
|
|
129
|
+
.replace(tzinfo=local_timezone)
|
|
134
130
|
.time()
|
|
135
131
|
)
|
|
136
132
|
ending = (
|
|
137
133
|
dt.strptime(range_time[1], "%HH%M")
|
|
138
|
-
.replace(tzinfo=
|
|
134
|
+
.replace(tzinfo=local_timezone)
|
|
139
135
|
.time()
|
|
140
136
|
)
|
|
141
137
|
if starting < start_time <= ending:
|
|
@@ -168,7 +164,9 @@ class Enedis:
|
|
|
168
164
|
"daily_consumption_max_power", pdl, start, end
|
|
169
165
|
)
|
|
170
166
|
|
|
171
|
-
async def _async_get_details(
|
|
167
|
+
async def _async_get_details(
|
|
168
|
+
self, service: Service, pdl: str, start: dt, end: dt
|
|
169
|
+
) -> Any:
|
|
172
170
|
"""Get production details. (max: 7 days)."""
|
|
173
171
|
data = None
|
|
174
172
|
response: dict[str, Any] | None = None
|
|
@@ -177,7 +175,7 @@ class Enedis:
|
|
|
177
175
|
start, end = interval
|
|
178
176
|
try:
|
|
179
177
|
if raise_error is False:
|
|
180
|
-
response = await self.async_fetch_datas(
|
|
178
|
+
response = await self.async_fetch_datas(service, pdl, start, end)
|
|
181
179
|
except EnedisException as error:
|
|
182
180
|
raise_error = True
|
|
183
181
|
response = None
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Class for Enedis Authentication (http://www.myelectricaldata.fr)."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
from collections.abc import Mapping
|
|
4
5
|
import json
|
|
5
6
|
import logging
|
|
6
7
|
import socket
|
|
@@ -8,7 +9,6 @@ from typing import Any
|
|
|
8
9
|
|
|
9
10
|
from aiohttp import ClientError, ClientResponseError, ClientSession
|
|
10
11
|
|
|
11
|
-
from .const import TIMEOUT, URL
|
|
12
12
|
from .exceptions import (
|
|
13
13
|
EnedisException,
|
|
14
14
|
HttpRequestError,
|
|
@@ -17,14 +17,13 @@ from .exceptions import (
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
_LOGGER = logging.getLogger(__name__)
|
|
20
|
+
URL = "https://myelectricaldata.fr"
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class EnedisAuth:
|
|
23
24
|
"""Class for Enedis Auth API."""
|
|
24
25
|
|
|
25
|
-
def __init__(
|
|
26
|
-
self, session: ClientSession, token: str, timeout: int = TIMEOUT
|
|
27
|
-
) -> None:
|
|
26
|
+
def __init__(self, session: ClientSession, token: str, timeout: int = 30) -> None:
|
|
28
27
|
"""Init."""
|
|
29
28
|
self.token = token
|
|
30
29
|
self.timeout = timeout
|
|
@@ -50,7 +49,8 @@ class EnedisAuth:
|
|
|
50
49
|
) from error
|
|
51
50
|
except ClientResponseError as error:
|
|
52
51
|
message = contents.decode("utf8")
|
|
53
|
-
|
|
52
|
+
headers: Mapping[str, str] = error.headers or {}
|
|
53
|
+
if "application/json" in headers.get("Content-Type", ""):
|
|
54
54
|
msg = json.loads(message)
|
|
55
55
|
if error.status == 409:
|
|
56
56
|
raise LimitReached(msg.get("detail", msg))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Constants."""
|
|
2
|
+
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
ATTR_CUM_PRICE = "cum_price"
|
|
6
|
+
ATTR_CUM_VALUE = "cum_value"
|
|
7
|
+
ATTR_END = "end"
|
|
8
|
+
ATTR_FN = "function"
|
|
9
|
+
ATTR_HPHC = "hphc"
|
|
10
|
+
ATTR_INTERVALS = "intervals"
|
|
11
|
+
ATTR_OFFPEAK = "offpeak"
|
|
12
|
+
ATTR_PRICE: Final = "price"
|
|
13
|
+
ATTR_PRICES = "prices"
|
|
14
|
+
ATTR_STANDARD = "standard"
|
|
15
|
+
ATTR_START = "start"
|
|
16
|
+
ATTR_TEMPO = "tempo"
|
|
17
|
+
TEMPO_DAYS = ("blue", "white", "red")
|
|
18
|
+
SUBSCRIPTIONS = (ATTR_STANDARD, ATTR_HPHC, ATTR_TEMPO)
|
|
19
|
+
DEFAULT_SUBSCRIPTION = ATTR_STANDARD
|
|
20
|
+
CONSUMPTION: Final = "consumption"
|
|
21
|
+
DAILY_CONSUM: Final = "daily_consumption"
|
|
22
|
+
DAILY_PROD: Final = "daily_production"
|
|
23
|
+
DETAIL_CONSUM: Final = "consumption_load_curve"
|
|
24
|
+
DETAIL_PROD: Final = "production_load_curve"
|
|
25
|
+
PRODUCTION: Final = "production"
|