myelectricaldatapy 2.2.6__tar.gz → 2.2.8__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.6 → myelectricaldatapy-2.2.8}/.github/workflows/auto-approve.yml +1 -1
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/.github/workflows/python-lint.yml +1 -1
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/.github/workflows/release.yml +2 -2
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/PKG-INFO +8 -4
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/example.py +4 -3
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy/__init__.py +1 -1
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy/analytics.py +39 -46
- myelectricaldatapy-2.2.6/myelectricaldatapy/myelectricaldata.py → myelectricaldatapy-2.2.8/myelectricaldatapy/api.py +34 -38
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy/auth.py +5 -6
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy/const.py +2 -1
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy/mypdl.py +54 -38
- myelectricaldatapy-2.2.8/myelectricaldatapy/tz.py +43 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy.egg-info/PKG-INFO +8 -4
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy.egg-info/SOURCES.txt +2 -1
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy.egg-info/requires.txt +5 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy.egg-info/scm_file_list.json +2 -1
- myelectricaldatapy-2.2.8/myelectricaldatapy.egg-info/scm_version.json +8 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy.egg-info/top_level.txt +1 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/pyproject.toml +10 -6
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/tests/test_analytics.py +11 -8
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/tests/test_load_data.py +57 -3
- myelectricaldatapy-2.2.6/myelectricaldatapy.egg-info/scm_version.json +0 -8
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/.devcontainer/devcontainer-lock.json +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/.devcontainer/devcontainer.json +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/.github/dependabot.yml +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/.github/workflows/python-publish.yml +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/.gitignore +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/.pre-commit-config.yaml +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/.vscode/settings.json +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/LICENSE +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/README.md +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/_secrets.yaml +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy/exceptions.py +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy/py.typed +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy.egg-info/dependency_links.txt +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/setup.cfg +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/tests/__init__.py +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/tests/conftest.py +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/tests/consts.py +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/tests/fixtures/access.json +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/tests/fixtures/address.json +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/tests/fixtures/contract.json +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/tests/fixtures/daily.json +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/tests/fixtures/detail.json +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/tests/fixtures/ecowatt.json +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/tests/fixtures/invalid_access.json +0 -0
- {myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/tests/fixtures/tempo.json +0 -0
|
@@ -8,7 +8,7 @@ permissions:
|
|
|
8
8
|
jobs:
|
|
9
9
|
dependency:
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
|
-
if: ${{ github.
|
|
11
|
+
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.actor == 'dependabot[bot]' }}
|
|
12
12
|
steps:
|
|
13
13
|
- name: Approve a PR
|
|
14
14
|
run: gh pr review --approve "$PR_URL"
|
|
@@ -13,7 +13,7 @@ jobs:
|
|
|
13
13
|
|
|
14
14
|
- name: Generate changelog
|
|
15
15
|
id: changelog
|
|
16
|
-
uses: metcalfc/changelog-generator@v4.
|
|
16
|
+
uses: metcalfc/changelog-generator@v4.8.0
|
|
17
17
|
with:
|
|
18
18
|
myToken: ${{ secrets.GITHUB_TOKEN }}
|
|
19
19
|
|
|
@@ -24,7 +24,7 @@ jobs:
|
|
|
24
24
|
body: ${{ steps.changelog.outputs.changelog }}
|
|
25
25
|
draft: false
|
|
26
26
|
prerelease: ${{ contains(github.ref_name, '-') }}
|
|
27
|
-
token: ${{ secrets.
|
|
27
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
28
28
|
publish:
|
|
29
29
|
needs: release
|
|
30
30
|
permissions:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: myelectricaldatapy
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.8
|
|
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
|
|
@@ -10,14 +10,13 @@ Platform: any
|
|
|
10
10
|
Classifier: Programming Language :: Python
|
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
15
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
16
|
Classifier: Framework :: AsyncIO
|
|
18
17
|
Classifier: Intended Audience :: Developers
|
|
19
18
|
Classifier: Topic :: Home Automation
|
|
20
|
-
Requires-Python: >=3.
|
|
19
|
+
Requires-Python: >=3.12
|
|
21
20
|
Description-Content-Type: text/markdown
|
|
22
21
|
License-File: LICENSE
|
|
23
22
|
Requires-Dist: aiohttp>=3.8.1
|
|
@@ -27,6 +26,11 @@ Provides-Extra: dev
|
|
|
27
26
|
Requires-Dist: prek; extra == "dev"
|
|
28
27
|
Requires-Dist: ruff; extra == "dev"
|
|
29
28
|
Requires-Dist: yamllint; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
31
|
+
Requires-Dist: python-typing-update; extra == "dev"
|
|
32
|
+
Requires-Dist: typing_extensions; extra == "dev"
|
|
33
|
+
Requires-Dist: freezegun; extra == "dev"
|
|
30
34
|
Dynamic: license-file
|
|
31
35
|
|
|
32
36
|
# myelectricaldatapy
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
"""Example code."""
|
|
3
3
|
|
|
4
4
|
import asyncio
|
|
5
|
-
from datetime import
|
|
5
|
+
from datetime import timedelta
|
|
6
6
|
import logging
|
|
7
7
|
|
|
8
8
|
import yaml
|
|
9
9
|
|
|
10
10
|
from myelectricaldatapy import Enedis, EnedisByPDL, EnedisException
|
|
11
|
+
from myelectricaldatapy.tz import local_now
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger()
|
|
13
14
|
logger.setLevel(logging.INFO)
|
|
@@ -32,8 +33,8 @@ async def async_main() -> None:
|
|
|
32
33
|
api = Enedis(token=TOKEN)
|
|
33
34
|
|
|
34
35
|
try:
|
|
35
|
-
start =
|
|
36
|
-
end =
|
|
36
|
+
start = local_now() - timedelta(days=7)
|
|
37
|
+
end = local_now()
|
|
37
38
|
data = await api.async_fetch_datas("consumption_load_curve", PDL, start, end)
|
|
38
39
|
logger.info(data)
|
|
39
40
|
data = await api.async_get_contract(PDL)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""myelectricaldatapy package."""
|
|
2
2
|
|
|
3
|
+
from .api import Enedis
|
|
3
4
|
from .exceptions import (
|
|
4
5
|
EnedisException,
|
|
5
6
|
HttpRequestError,
|
|
6
7
|
LimitReached,
|
|
7
8
|
TimeoutExceededError,
|
|
8
9
|
)
|
|
9
|
-
from .myelectricaldata import Enedis
|
|
10
10
|
from .mypdl import EnedisByPDL
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
@@ -1,38 +1,21 @@
|
|
|
1
1
|
"""Class for analytics."""
|
|
2
2
|
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from collections.abc import Collection
|
|
6
3
|
from datetime import datetime as dt, timedelta
|
|
7
|
-
import os
|
|
8
4
|
import re
|
|
9
5
|
from typing import Any
|
|
10
|
-
from zoneinfo import ZoneInfo
|
|
11
6
|
|
|
12
7
|
import pandas as pd
|
|
13
8
|
|
|
14
|
-
from .const import ATTR_OFFPEAK, ATTR_STANDARD
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _local_timezone() -> Any:
|
|
18
|
-
"""Resolve the local IANA timezone so DST offsets stay correct per-date.
|
|
19
|
-
|
|
20
|
-
Falls back to the fixed UTC offset captured at call time when the
|
|
21
|
-
system timezone database cannot be resolved (e.g. non-Linux hosts).
|
|
22
|
-
"""
|
|
23
|
-
try:
|
|
24
|
-
tz_path = os.path.realpath("/etc/localtime")
|
|
25
|
-
return ZoneInfo(tz_path.split("zoneinfo/")[-1])
|
|
26
|
-
except Exception: # noqa: BLE001
|
|
27
|
-
return dt.now().astimezone().tzinfo
|
|
9
|
+
from .const import ATTR_OFFPEAK, ATTR_STANDARD, TEMPO_DAYS
|
|
10
|
+
from .tz import LOCAL_TIMEZONE
|
|
28
11
|
|
|
29
12
|
|
|
30
13
|
class EnedisAnalytics:
|
|
31
|
-
"""Data
|
|
14
|
+
"""Data analytics."""
|
|
32
15
|
|
|
33
|
-
local_timezone =
|
|
16
|
+
local_timezone = LOCAL_TIMEZONE
|
|
34
17
|
|
|
35
|
-
def __init__(self, data:
|
|
18
|
+
def __init__(self, data: Any) -> None:
|
|
36
19
|
"""Initialize Dataframe."""
|
|
37
20
|
self.df = pd.DataFrame(data)
|
|
38
21
|
|
|
@@ -50,20 +33,25 @@ class EnedisAnalytics:
|
|
|
50
33
|
tempo: dict[str, str] | None = None,
|
|
51
34
|
) -> Any:
|
|
52
35
|
"""Convert data to analyze."""
|
|
36
|
+
|
|
53
37
|
cum_value = cum_value or {}
|
|
54
38
|
cum_price = cum_price or {}
|
|
55
39
|
step_hour = False
|
|
40
|
+
|
|
56
41
|
if not self.df.empty:
|
|
57
42
|
# Convert str to datetime
|
|
58
43
|
try:
|
|
59
|
-
self.df
|
|
44
|
+
self.df["date"] = pd.to_datetime(
|
|
45
|
+
self.df["date"], format="%Y-%m-%d %H:%M:%S"
|
|
46
|
+
)
|
|
60
47
|
except ValueError:
|
|
61
|
-
self.df
|
|
62
|
-
|
|
48
|
+
self.df["date"] = pd.to_datetime(self.df["date"], format="%Y-%m-%d")
|
|
49
|
+
|
|
50
|
+
self.df["date"] = self.df["date"].dt.tz_localize(self.local_timezone)
|
|
63
51
|
|
|
64
52
|
if convertUTC:
|
|
65
|
-
self.df
|
|
66
|
-
self.df
|
|
53
|
+
self.df["date"] = pd.to_datetime(
|
|
54
|
+
self.df["date"], utc=True, format="%Y-%m-%d %H:%M:%S"
|
|
67
55
|
)
|
|
68
56
|
|
|
69
57
|
# Subtract 1 minute at hour
|
|
@@ -72,16 +60,17 @@ class EnedisAnalytics:
|
|
|
72
60
|
if "interval_length" in self.df:
|
|
73
61
|
step_hour = True
|
|
74
62
|
self.df.loc[
|
|
75
|
-
(self.df
|
|
63
|
+
(self.df["date"].dt.minute == 0),
|
|
76
64
|
"date",
|
|
77
|
-
] = self.df
|
|
65
|
+
] = self.df["date"] - timedelta(minutes=1)
|
|
78
66
|
|
|
79
67
|
if start_date:
|
|
80
68
|
dt_start_date = pd.to_datetime(start_date, format="%Y-%m-%d %H:%M:%S")
|
|
81
|
-
dt_start_date
|
|
82
|
-
|
|
69
|
+
if dt_start_date.tzinfo is None:
|
|
70
|
+
dt_start_date = dt_start_date.tz_localize(self.local_timezone)
|
|
71
|
+
self.df = self.df[(self.df["date"] > dt_start_date)]
|
|
83
72
|
|
|
84
|
-
self.df.index = self.df
|
|
73
|
+
self.df.index = self.df["date"]
|
|
85
74
|
|
|
86
75
|
# Add mark
|
|
87
76
|
self.df["notes"] = ATTR_STANDARD
|
|
@@ -89,18 +78,20 @@ class EnedisAnalytics:
|
|
|
89
78
|
if self.df.empty:
|
|
90
79
|
return self.df.to_dict(orient="records")
|
|
91
80
|
|
|
92
|
-
self.df
|
|
93
|
-
self.df
|
|
81
|
+
self.df["interval_length"] = (
|
|
82
|
+
self.df["interval_length"].transform(self._weighted_interval)
|
|
94
83
|
if step_hour
|
|
95
84
|
else 1
|
|
96
85
|
)
|
|
97
86
|
|
|
98
87
|
if convertKwh:
|
|
99
|
-
self.df
|
|
100
|
-
pd.to_numeric(self.df
|
|
88
|
+
self.df["value"] = (
|
|
89
|
+
pd.to_numeric(self.df["value"]) / 1000 * self.df["interval_length"]
|
|
101
90
|
)
|
|
102
91
|
else:
|
|
103
|
-
self.df
|
|
92
|
+
self.df["value"] = (
|
|
93
|
+
pd.to_numeric(self.df["value"]) * self.df["interval_length"]
|
|
94
|
+
)
|
|
104
95
|
|
|
105
96
|
if intervals:
|
|
106
97
|
self._get_data_interval(intervals)
|
|
@@ -121,7 +112,7 @@ class EnedisAnalytics:
|
|
|
121
112
|
for mode, values in prices.items():
|
|
122
113
|
if isinstance(values, dict):
|
|
123
114
|
for offset, price in values.items():
|
|
124
|
-
if tempo and offset in
|
|
115
|
+
if tempo and offset in TEMPO_DAYS:
|
|
125
116
|
self.df.loc[
|
|
126
117
|
(self.df.notes == mode) & (self.df.tempo == offset),
|
|
127
118
|
"price",
|
|
@@ -149,7 +140,7 @@ class EnedisAnalytics:
|
|
|
149
140
|
|
|
150
141
|
def _weighted_interval(self, interval: str) -> float | int:
|
|
151
142
|
"""Compute weighted."""
|
|
152
|
-
if interval and len(rslt := re.findall("PT([0-9]{2})M", interval)) == 1:
|
|
143
|
+
if interval and len(rslt := re.findall("PT([0-9]{1,2})M", interval)) == 1:
|
|
153
144
|
return int(rslt[0]) / 60
|
|
154
145
|
return 1
|
|
155
146
|
|
|
@@ -159,16 +150,18 @@ class EnedisAnalytics:
|
|
|
159
150
|
# Convert str to datetime
|
|
160
151
|
start = pd.to_datetime(interval[0], format="%H:%M:%S").time()
|
|
161
152
|
end = pd.to_datetime(interval[1], format="%H:%M:%S").time()
|
|
162
|
-
# Mark
|
|
163
|
-
self.df.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
153
|
+
# Mark (handle ranges spanning midnight, e.g. 22:00:00 -> 06:00:00)
|
|
154
|
+
date_time = self.df["date"].dt.time
|
|
155
|
+
if start <= end:
|
|
156
|
+
mask = (date_time > start) & (date_time <= end)
|
|
157
|
+
else:
|
|
158
|
+
mask = (date_time > start) | (date_time <= end)
|
|
159
|
+
self.df.loc[mask, "notes"] = ATTR_OFFPEAK
|
|
167
160
|
|
|
168
161
|
return self.df
|
|
169
162
|
|
|
170
|
-
def _set_tempo_days(self, tempo: dict[str, str]) ->
|
|
163
|
+
def _set_tempo_days(self, tempo: dict[str, str]) -> None:
|
|
171
164
|
"""Add columns with tempo day."""
|
|
172
165
|
for str_date, value in tempo.items():
|
|
173
166
|
dt_date = pd.to_datetime(str_date, format="%Y-%m-%d")
|
|
174
|
-
self.df.loc[(self.df
|
|
167
|
+
self.df.loc[(self.df["date"].dt.date == dt_date.date()), "tempo"] = value
|
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
"""Class for Enedis Gateway (http://www.myelectricaldata.fr)."""
|
|
2
2
|
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
3
|
from collections.abc import Generator
|
|
6
4
|
from datetime import date, datetime as dt, timedelta
|
|
7
5
|
import logging
|
|
8
6
|
import re
|
|
9
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
10
8
|
|
|
11
9
|
from aiohttp import ClientSession
|
|
12
10
|
|
|
13
11
|
from .auth import EnedisAuth
|
|
14
12
|
from .const import DAILY_CONSUM, DAILY_PROD, DETAIL_CONSUM, DETAIL_PROD, TIMEOUT
|
|
15
13
|
from .exceptions import EnedisException
|
|
14
|
+
from .tz import LOCAL_TIMEZONE, as_local, local_now
|
|
16
15
|
|
|
17
16
|
_LOGGER = logging.getLogger(__name__)
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
class Enedis:
|
|
21
|
-
"""
|
|
20
|
+
"""Enedis API."""
|
|
22
21
|
|
|
23
22
|
def __init__(
|
|
24
23
|
self, token: str, session: ClientSession | None = None, timeout: int = TIMEOUT
|
|
@@ -28,7 +27,6 @@ class Enedis:
|
|
|
28
27
|
self.auth = EnedisAuth(session, token, timeout)
|
|
29
28
|
self.async_request = self.auth.async_request
|
|
30
29
|
self.offpeaks: list[str] = []
|
|
31
|
-
self.dt_offpeak: list[dt] = []
|
|
32
30
|
self.last_access: date | None = None
|
|
33
31
|
|
|
34
32
|
async def async_fetch_datas(
|
|
@@ -41,7 +39,7 @@ class Enedis:
|
|
|
41
39
|
daily_consumption, daily_production,
|
|
42
40
|
consumption_load_curve, production_load_curve
|
|
43
41
|
"""
|
|
44
|
-
self.last_access =
|
|
42
|
+
self.last_access = local_now()
|
|
45
43
|
path_range = ""
|
|
46
44
|
if start and end:
|
|
47
45
|
start_date = start.strftime("%Y-%m-%d")
|
|
@@ -69,13 +67,6 @@ class Enedis:
|
|
|
69
67
|
contract = usage_point.get("contracts", {})
|
|
70
68
|
if offpeak_hours := contract.get("offpeak_hours"):
|
|
71
69
|
self.offpeaks = re.findall("(?:(\\w+)-(\\w+))+", offpeak_hours)
|
|
72
|
-
self.dt_offpeak = [
|
|
73
|
-
( # type: ignore
|
|
74
|
-
dt.strptime(offpeak[0], "%HH%M"),
|
|
75
|
-
dt.strptime(offpeak[1], "%HH%M"),
|
|
76
|
-
)
|
|
77
|
-
for offpeak in self.offpeaks
|
|
78
|
-
]
|
|
79
70
|
return contract
|
|
80
71
|
|
|
81
72
|
async def async_get_contracts(self, pdl: str) -> Any:
|
|
@@ -101,12 +92,12 @@ class Enedis:
|
|
|
101
92
|
) -> Any:
|
|
102
93
|
"""Return Tempo Day."""
|
|
103
94
|
str_start = (
|
|
104
|
-
start.strftime("%Y-%m-%d") if start else
|
|
95
|
+
start.strftime("%Y-%m-%d") if start else local_now().strftime("%Y-%m-%d")
|
|
105
96
|
)
|
|
106
97
|
str_end = (
|
|
107
98
|
end.strftime("%Y-%m-%d")
|
|
108
99
|
if end
|
|
109
|
-
else (
|
|
100
|
+
else (local_now() + timedelta(days=1)).strftime("%Y-%m-%d")
|
|
110
101
|
)
|
|
111
102
|
return await self.auth.async_request(path=f"rte/tempo/{str_start}/{str_end}")
|
|
112
103
|
|
|
@@ -115,12 +106,12 @@ class Enedis:
|
|
|
115
106
|
) -> Any:
|
|
116
107
|
"""Return Ecowatt information."""
|
|
117
108
|
str_start = (
|
|
118
|
-
start.strftime("%Y-%m-%d") if start else
|
|
109
|
+
start.strftime("%Y-%m-%d") if start else local_now().strftime("%Y-%m-%d")
|
|
119
110
|
)
|
|
120
111
|
str_end = (
|
|
121
112
|
end.strftime("%Y-%m-%d")
|
|
122
113
|
if end
|
|
123
|
-
else (
|
|
114
|
+
else (local_now() + timedelta(days=1)).strftime("%Y-%m-%d")
|
|
124
115
|
)
|
|
125
116
|
return await self.async_request(path=f"rte/ecowatt/{str_start}/{str_end}")
|
|
126
117
|
|
|
@@ -133,11 +124,21 @@ class Enedis:
|
|
|
133
124
|
async def async_check_offpeak(self, pdl: str, start: dt) -> bool:
|
|
134
125
|
"""Return offpeak status."""
|
|
135
126
|
if await self.async_has_offpeak(pdl) is True:
|
|
136
|
-
|
|
127
|
+
# Off-peak windows are defined in local wall-clock time, so a
|
|
128
|
+
# start given in another timezone must be converted first.
|
|
129
|
+
start_time = as_local(start).astimezone(LOCAL_TIMEZONE).time()
|
|
137
130
|
for range_time in self.offpeaks:
|
|
138
|
-
starting =
|
|
139
|
-
|
|
140
|
-
|
|
131
|
+
starting = (
|
|
132
|
+
dt.strptime(range_time[0], "%HH%M")
|
|
133
|
+
.replace(tzinfo=LOCAL_TIMEZONE)
|
|
134
|
+
.time()
|
|
135
|
+
)
|
|
136
|
+
ending = (
|
|
137
|
+
dt.strptime(range_time[1], "%HH%M")
|
|
138
|
+
.replace(tzinfo=LOCAL_TIMEZONE)
|
|
139
|
+
.time()
|
|
140
|
+
)
|
|
141
|
+
if starting < start_time <= ending:
|
|
141
142
|
return True
|
|
142
143
|
return False
|
|
143
144
|
|
|
@@ -170,7 +171,7 @@ class Enedis:
|
|
|
170
171
|
async def _async_get_details(self, mode: str, pdl: str, start: dt, end: dt) -> Any:
|
|
171
172
|
"""Get production details. (max: 7 days)."""
|
|
172
173
|
data = None
|
|
173
|
-
response =
|
|
174
|
+
response: dict[str, Any] | None = None
|
|
174
175
|
raise_error = False
|
|
175
176
|
for interval in list(self.date_range(start, end, 7)):
|
|
176
177
|
start, end = interval
|
|
@@ -179,15 +180,18 @@ class Enedis:
|
|
|
179
180
|
response = await self.async_fetch_datas(mode, pdl, start, end)
|
|
180
181
|
except EnedisException as error:
|
|
181
182
|
raise_error = True
|
|
183
|
+
response = None
|
|
182
184
|
_LOGGER.error(error)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
185
|
+
|
|
186
|
+
if response is None:
|
|
187
|
+
continue
|
|
188
|
+
new_data = response.get("meter_reading", {}).get("interval_reading")
|
|
189
|
+
if new_data is None:
|
|
190
|
+
continue
|
|
191
|
+
elif data is None:
|
|
192
|
+
data = response
|
|
193
|
+
else:
|
|
194
|
+
data["meter_reading"]["interval_reading"].extend(new_data)
|
|
191
195
|
|
|
192
196
|
return data
|
|
193
197
|
|
|
@@ -205,14 +209,6 @@ class Enedis:
|
|
|
205
209
|
start = s_end
|
|
206
210
|
yield (start, end)
|
|
207
211
|
|
|
208
|
-
async def __aenter__(self) -> Enedis:
|
|
209
|
-
"""Asynchronous enter."""
|
|
210
|
-
return self
|
|
211
|
-
|
|
212
|
-
async def __aexit__(self, *_exc_info: object) -> None:
|
|
213
|
-
"""Async exit."""
|
|
214
|
-
await self.async_close()
|
|
215
|
-
|
|
216
212
|
async def async_close(self) -> None:
|
|
217
213
|
"""Close the session."""
|
|
218
214
|
if self.auth.session:
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Class for Enedis Authentication (http://www.myelectricaldata.fr)."""
|
|
2
2
|
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
3
|
import asyncio
|
|
6
4
|
import json
|
|
7
5
|
import logging
|
|
@@ -38,6 +36,7 @@ class EnedisAuth:
|
|
|
38
36
|
kwargs["headers"].update(
|
|
39
37
|
{"Content-Type": "application/json", "Authorization": self.token}
|
|
40
38
|
)
|
|
39
|
+
contents = b""
|
|
41
40
|
|
|
42
41
|
try:
|
|
43
42
|
async with asyncio.timeout(self.timeout):
|
|
@@ -45,15 +44,15 @@ class EnedisAuth:
|
|
|
45
44
|
response = await self.session.request(method, f"{URL}/{path}", **kwargs)
|
|
46
45
|
contents = await response.read()
|
|
47
46
|
response.raise_for_status()
|
|
48
|
-
except (
|
|
47
|
+
except (TimeoutError, asyncio.CancelledError) as error:
|
|
49
48
|
raise TimeoutExceededError(
|
|
50
49
|
"Timeout occurred while connecting to MyElectricalData."
|
|
51
50
|
) from error
|
|
52
|
-
except ClientResponseError:
|
|
51
|
+
except ClientResponseError as error:
|
|
53
52
|
message = contents.decode("utf8")
|
|
54
|
-
if "application/json" in
|
|
53
|
+
if "application/json" in (error.headers or {}).get("Content-Type", ""):
|
|
55
54
|
msg = json.loads(message)
|
|
56
|
-
if
|
|
55
|
+
if error.status == 409:
|
|
57
56
|
raise LimitReached(msg.get("detail", msg))
|
|
58
57
|
raise EnedisException(msg.get("detail", msg))
|
|
59
58
|
raise EnedisException({"message": message})
|
|
@@ -6,10 +6,11 @@ ATTR_END = "end"
|
|
|
6
6
|
ATTR_INTERVALS = "intervals"
|
|
7
7
|
ATTR_OFFPEAK = "offpeak"
|
|
8
8
|
ATTR_PRICES = "prices"
|
|
9
|
-
ATTR_SERVICE = "service"
|
|
10
9
|
ATTR_STANDARD = "standard"
|
|
11
10
|
ATTR_START = "start"
|
|
12
11
|
ATTR_FN = "function"
|
|
12
|
+
ATTR_PRICE = "price"
|
|
13
|
+
TEMPO_DAYS = ("blue", "white", "red")
|
|
13
14
|
CONSUMPTION = "consumption"
|
|
14
15
|
DAILY_CONSUM = "daily_consumption"
|
|
15
16
|
DAILY_PROD = "daily_production"
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
"""Class for my PDL."""
|
|
2
2
|
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
3
|
from collections.abc import Callable
|
|
6
4
|
from datetime import date, datetime as dt, timedelta
|
|
7
5
|
import logging
|
|
8
|
-
from typing import Any
|
|
6
|
+
from typing import Any, Literal
|
|
9
7
|
|
|
10
8
|
from aiohttp import ClientSession
|
|
11
9
|
import voluptuous as vol
|
|
12
10
|
|
|
13
|
-
from myelectricaldatapy import Enedis, EnedisException, LimitReached
|
|
14
|
-
|
|
15
11
|
from .analytics import EnedisAnalytics
|
|
12
|
+
from .api import Enedis
|
|
16
13
|
from .const import (
|
|
17
14
|
ATTR_CUM_PRICE,
|
|
18
15
|
ATTR_CUM_VALUE,
|
|
@@ -20,8 +17,8 @@ from .const import (
|
|
|
20
17
|
ATTR_FN,
|
|
21
18
|
ATTR_INTERVALS,
|
|
22
19
|
ATTR_OFFPEAK,
|
|
20
|
+
ATTR_PRICE,
|
|
23
21
|
ATTR_PRICES,
|
|
24
|
-
ATTR_SERVICE,
|
|
25
22
|
ATTR_STANDARD,
|
|
26
23
|
ATTR_START,
|
|
27
24
|
CONSUMPTION,
|
|
@@ -32,31 +29,18 @@ from .const import (
|
|
|
32
29
|
PRODUCTION,
|
|
33
30
|
TIMEOUT,
|
|
34
31
|
)
|
|
32
|
+
from .exceptions import EnedisException, LimitReached
|
|
33
|
+
from .tz import as_local, local_now
|
|
35
34
|
|
|
36
35
|
_LOGGER = logging.getLogger(__name__)
|
|
37
36
|
|
|
38
|
-
MODES_SCH = vol.Schema(
|
|
39
|
-
{
|
|
40
|
-
vol.Optional(CONSUMPTION): {
|
|
41
|
-
vol.Required(ATTR_SERVICE): str,
|
|
42
|
-
vol.Optional(ATTR_START): dt,
|
|
43
|
-
vol.Optional(ATTR_END): dt,
|
|
44
|
-
},
|
|
45
|
-
vol.Optional(PRODUCTION): {
|
|
46
|
-
vol.Required(ATTR_SERVICE): str,
|
|
47
|
-
vol.Optional(ATTR_START): dt,
|
|
48
|
-
vol.Optional(ATTR_END): dt,
|
|
49
|
-
},
|
|
50
|
-
}
|
|
51
|
-
)
|
|
52
|
-
|
|
53
37
|
PRICE_SCH = vol.Schema(
|
|
54
38
|
{
|
|
55
39
|
vol.Required(ATTR_STANDARD): {
|
|
56
|
-
vol.Required(
|
|
40
|
+
vol.Required(ATTR_PRICE): vol.Any(int, float),
|
|
57
41
|
},
|
|
58
42
|
vol.Optional(ATTR_OFFPEAK): {
|
|
59
|
-
vol.Required(
|
|
43
|
+
vol.Required(ATTR_PRICE): vol.Any(int, float),
|
|
60
44
|
},
|
|
61
45
|
}
|
|
62
46
|
)
|
|
@@ -118,7 +102,7 @@ class EnedisByPDL:
|
|
|
118
102
|
self.has_collected: bool = False
|
|
119
103
|
self.has_parameters: bool = False
|
|
120
104
|
self.intervals: list[tuple[str, str]] = []
|
|
121
|
-
self.last_access: dt =
|
|
105
|
+
self.last_access: dt = local_now()
|
|
122
106
|
self.last_refresh: date | None = None
|
|
123
107
|
self.max_power: dict[str, Any] = {}
|
|
124
108
|
self.tempo: dict[str, Any] = {}
|
|
@@ -133,16 +117,44 @@ class EnedisByPDL:
|
|
|
133
117
|
"""Intervals exist."""
|
|
134
118
|
return len(self.intervals) > 0
|
|
135
119
|
|
|
120
|
+
@property
|
|
121
|
+
def has_tempo_subscription(self) -> bool:
|
|
122
|
+
"""Tempo subscription status."""
|
|
123
|
+
return self._tempo_subs
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def has_offpeak_hours_subscription(self) -> bool:
|
|
127
|
+
"""Offpeak hours subscription status."""
|
|
128
|
+
return self._off_subs
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def has_ecowatt_subscription(self) -> bool:
|
|
132
|
+
"""Ecowatt subscription status."""
|
|
133
|
+
return self._ecowatt_subs
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def has_maxpower_subscription(self) -> bool:
|
|
137
|
+
"""Max power subscription status."""
|
|
138
|
+
return self._maxpower_subs
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def subscription(self) -> Literal["hphc", "tempo", "standard"]:
|
|
142
|
+
if self.has_tempo_subscription:
|
|
143
|
+
return "tempo"
|
|
144
|
+
if self.has_offpeak_hours_subscription:
|
|
145
|
+
return "hphc"
|
|
146
|
+
return "standard"
|
|
147
|
+
|
|
136
148
|
@property
|
|
137
149
|
def ecowatt_day(self) -> Any:
|
|
138
150
|
"""ecowatt."""
|
|
139
|
-
str_date =
|
|
151
|
+
str_date = local_now().strftime("%Y-%m-%d")
|
|
140
152
|
return self.ecowatt.get(str_date, {})
|
|
141
153
|
|
|
142
154
|
@property
|
|
143
155
|
def tempo_day(self) -> str | None:
|
|
144
156
|
"""Tempo day."""
|
|
145
|
-
str_date =
|
|
157
|
+
str_date = local_now().strftime("%Y-%m-%d")
|
|
146
158
|
return self.tempo.get(str_date)
|
|
147
159
|
|
|
148
160
|
@property
|
|
@@ -179,14 +191,18 @@ class EnedisByPDL:
|
|
|
179
191
|
|
|
180
192
|
async def async_update(self, force_refresh: bool = False) -> None:
|
|
181
193
|
"""Update data."""
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
self.
|
|
188
|
-
self.
|
|
194
|
+
|
|
195
|
+
start = local_now() - timedelta(days=1095)
|
|
196
|
+
end = local_now() + timedelta(days=1)
|
|
197
|
+
|
|
198
|
+
if force_refresh or (self.last_access.date() != local_now().date()):
|
|
199
|
+
self.contract.clear()
|
|
200
|
+
self.contract.clear()
|
|
201
|
+
self.address.clear()
|
|
202
|
+
self.ecowatt.clear()
|
|
203
|
+
self.max_power.clear()
|
|
189
204
|
self.has_collected = False
|
|
205
|
+
|
|
190
206
|
try:
|
|
191
207
|
self.access = await self._api.async_valid_access(self.pdl)
|
|
192
208
|
if self.access.get("quota_reached", False):
|
|
@@ -218,11 +234,11 @@ class EnedisByPDL:
|
|
|
218
234
|
|
|
219
235
|
if self.has_parameters and self.has_collected is False:
|
|
220
236
|
await self.async_update_collects()
|
|
221
|
-
self.last_refresh =
|
|
237
|
+
self.last_refresh = local_now()
|
|
222
238
|
except EnedisException as error:
|
|
223
239
|
raise error from error
|
|
224
240
|
finally:
|
|
225
|
-
self.last_access =
|
|
241
|
+
self.last_access = local_now()
|
|
226
242
|
|
|
227
243
|
def tempo_subscription(self, activate: bool = False) -> None:
|
|
228
244
|
"""Enable or Disable Tempo Subscription."""
|
|
@@ -327,8 +343,8 @@ class EnedisByPDL:
|
|
|
327
343
|
days = 1095 if service in [DAILY_PROD, DAILY_CONSUM] else 7
|
|
328
344
|
mode = CONSUMPTION if service in [DAILY_CONSUM, DETAIL_CONSUM] else PRODUCTION
|
|
329
345
|
func = funcs[service]
|
|
330
|
-
dt_start = start if start else
|
|
331
|
-
dt_end = end if end else
|
|
346
|
+
dt_start = as_local(start) if start else local_now() - timedelta(days=days)
|
|
347
|
+
dt_end = as_local(end) if end else local_now() + timedelta(days=1)
|
|
332
348
|
self._params[mode] = {ATTR_FN: func, ATTR_START: dt_start, ATTR_END: dt_end}
|
|
333
349
|
if intervals:
|
|
334
350
|
self._set_intervals(mode, intervals)
|
|
@@ -337,7 +353,7 @@ class EnedisByPDL:
|
|
|
337
353
|
if cum_value:
|
|
338
354
|
self._set_cumsum(mode, "value", cum_value)
|
|
339
355
|
if cum_price:
|
|
340
|
-
self._set_cumsum(mode,
|
|
356
|
+
self._set_cumsum(mode, ATTR_PRICE, cum_price)
|
|
341
357
|
self.has_parameters = True
|
|
342
358
|
|
|
343
359
|
async def async_update_collects(self) -> None:
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Local timezone helpers.
|
|
2
|
+
|
|
3
|
+
The Enedis/RTE APIs return naive timestamps that represent local wall-clock
|
|
4
|
+
time (see tests/fixtures, e.g. access.json's "last_call"), so this library
|
|
5
|
+
treats "naive datetime" as "local time" throughout. These helpers make that
|
|
6
|
+
convention explicit and DST-safe, instead of relying on a fixed UTC offset
|
|
7
|
+
captured once at import time.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from datetime import UTC, datetime as dt, tzinfo as _tzinfo
|
|
11
|
+
import os
|
|
12
|
+
from typing import cast
|
|
13
|
+
from zoneinfo import ZoneInfo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _resolve_local_timezone() -> _tzinfo:
|
|
17
|
+
"""Resolve the local IANA timezone, DST-aware.
|
|
18
|
+
|
|
19
|
+
Falls back to the fixed UTC offset captured at call time when the
|
|
20
|
+
system timezone database cannot be resolved (e.g. non-Linux hosts).
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
tz_path = os.path.realpath("/etc/localtime")
|
|
24
|
+
return ZoneInfo(tz_path.split("zoneinfo/")[-1])
|
|
25
|
+
except Exception: # noqa: BLE001
|
|
26
|
+
# dt.now().astimezone() always sets tzinfo when called without
|
|
27
|
+
# arguments; typeshed just can't express that.
|
|
28
|
+
return cast(_tzinfo, dt.now().astimezone().tzinfo) or UTC
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
LOCAL_TIMEZONE = _resolve_local_timezone()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def local_now() -> dt:
|
|
35
|
+
"""Return the current time, timezone-aware in the local zone."""
|
|
36
|
+
return dt.now(tz=LOCAL_TIMEZONE)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def as_local(value: dt) -> dt:
|
|
40
|
+
"""Attach the local timezone to a naive datetime; pass aware ones through."""
|
|
41
|
+
if value.tzinfo is None:
|
|
42
|
+
return value.replace(tzinfo=LOCAL_TIMEZONE)
|
|
43
|
+
return value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: myelectricaldatapy
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.8
|
|
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
|
|
@@ -10,14 +10,13 @@ Platform: any
|
|
|
10
10
|
Classifier: Programming Language :: Python
|
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
15
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
16
|
Classifier: Framework :: AsyncIO
|
|
18
17
|
Classifier: Intended Audience :: Developers
|
|
19
18
|
Classifier: Topic :: Home Automation
|
|
20
|
-
Requires-Python: >=3.
|
|
19
|
+
Requires-Python: >=3.12
|
|
21
20
|
Description-Content-Type: text/markdown
|
|
22
21
|
License-File: LICENSE
|
|
23
22
|
Requires-Dist: aiohttp>=3.8.1
|
|
@@ -27,6 +26,11 @@ Provides-Extra: dev
|
|
|
27
26
|
Requires-Dist: prek; extra == "dev"
|
|
28
27
|
Requires-Dist: ruff; extra == "dev"
|
|
29
28
|
Requires-Dist: yamllint; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
31
|
+
Requires-Dist: python-typing-update; extra == "dev"
|
|
32
|
+
Requires-Dist: typing_extensions; extra == "dev"
|
|
33
|
+
Requires-Dist: freezegun; extra == "dev"
|
|
30
34
|
Dynamic: license-file
|
|
31
35
|
|
|
32
36
|
# myelectricaldatapy
|
{myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy.egg-info/SOURCES.txt
RENAMED
|
@@ -15,12 +15,13 @@ pyproject.toml
|
|
|
15
15
|
.vscode/settings.json
|
|
16
16
|
myelectricaldatapy/__init__.py
|
|
17
17
|
myelectricaldatapy/analytics.py
|
|
18
|
+
myelectricaldatapy/api.py
|
|
18
19
|
myelectricaldatapy/auth.py
|
|
19
20
|
myelectricaldatapy/const.py
|
|
20
21
|
myelectricaldatapy/exceptions.py
|
|
21
|
-
myelectricaldatapy/myelectricaldata.py
|
|
22
22
|
myelectricaldatapy/mypdl.py
|
|
23
23
|
myelectricaldatapy/py.typed
|
|
24
|
+
myelectricaldatapy/tz.py
|
|
24
25
|
myelectricaldatapy.egg-info/PKG-INFO
|
|
25
26
|
myelectricaldatapy.egg-info/SOURCES.txt
|
|
26
27
|
myelectricaldatapy.egg-info/dependency_links.txt
|
{myelectricaldatapy-2.2.6 → myelectricaldatapy-2.2.8}/myelectricaldatapy.egg-info/scm_file_list.json
RENAMED
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
"example.py",
|
|
17
17
|
"myelectricaldatapy/__init__.py",
|
|
18
18
|
"myelectricaldatapy/analytics.py",
|
|
19
|
+
"myelectricaldatapy/api.py",
|
|
19
20
|
"myelectricaldatapy/auth.py",
|
|
20
21
|
"myelectricaldatapy/const.py",
|
|
21
22
|
"myelectricaldatapy/exceptions.py",
|
|
22
|
-
"myelectricaldatapy/myelectricaldata.py",
|
|
23
23
|
"myelectricaldatapy/mypdl.py",
|
|
24
24
|
"myelectricaldatapy/py.typed",
|
|
25
|
+
"myelectricaldatapy/tz.py",
|
|
25
26
|
"pyproject.toml",
|
|
26
27
|
"tests/__init__.py",
|
|
27
28
|
"tests/conftest.py",
|
|
@@ -16,15 +16,14 @@ classifiers = [
|
|
|
16
16
|
"Programming Language :: Python",
|
|
17
17
|
"Development Status :: 5 - Production/Stable",
|
|
18
18
|
"Programming Language :: Python :: 3",
|
|
19
|
-
"Programming Language :: Python :: 3.10",
|
|
20
|
-
"Programming Language :: Python :: 3.11",
|
|
21
19
|
"Programming Language :: Python :: 3.12",
|
|
22
20
|
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
23
22
|
"Framework :: AsyncIO",
|
|
24
23
|
"Intended Audience :: Developers",
|
|
25
24
|
"Topic :: Home Automation",
|
|
26
25
|
]
|
|
27
|
-
requires-python = ">=3.
|
|
26
|
+
requires-python = ">=3.12"
|
|
28
27
|
dependencies = [
|
|
29
28
|
"aiohttp>=3.8.1",
|
|
30
29
|
"pandas>=2.0.2",
|
|
@@ -36,6 +35,11 @@ dev = [
|
|
|
36
35
|
"prek",
|
|
37
36
|
"ruff",
|
|
38
37
|
"yamllint",
|
|
38
|
+
"pytest",
|
|
39
|
+
"pytest-asyncio",
|
|
40
|
+
"python-typing-update",
|
|
41
|
+
"typing_extensions",
|
|
42
|
+
"freezegun"
|
|
39
43
|
]
|
|
40
44
|
|
|
41
45
|
[project.urls]
|
|
@@ -46,12 +50,12 @@ Homepage = "https://github.com/cyr-ius/myelectricaldatapy"
|
|
|
46
50
|
[tool.setuptools]
|
|
47
51
|
platforms = ["any"]
|
|
48
52
|
|
|
53
|
+
[tool.setuptools.packages.find]
|
|
54
|
+
exclude = ["node_modules*", "tests*"]
|
|
55
|
+
|
|
49
56
|
[tool.pytest.ini_options]
|
|
50
57
|
asyncio_mode = "auto"
|
|
51
58
|
|
|
52
|
-
[tool.ruff.lint.flake8-pytest-style]
|
|
53
|
-
fixture-parentheses = false
|
|
54
|
-
|
|
55
59
|
[tool.ruff.lint.isort]
|
|
56
60
|
force-sort-within-sections = true
|
|
57
61
|
combine-as-imports = true
|
|
@@ -12,6 +12,7 @@ import pytest
|
|
|
12
12
|
|
|
13
13
|
import myelectricaldatapy
|
|
14
14
|
from myelectricaldatapy import EnedisByPDL, LimitReached
|
|
15
|
+
from myelectricaldatapy.tz import LOCAL_TIMEZONE
|
|
15
16
|
|
|
16
17
|
from .consts import PDL, TOKEN
|
|
17
18
|
|
|
@@ -203,8 +204,8 @@ async def test_cumsums(mock_enedis: Mock) -> None: # pylint: disable=unused-arg
|
|
|
203
204
|
api = EnedisByPDL(pdl=PDL, token=TOKEN, session=ClientSession())
|
|
204
205
|
api.set_collects(
|
|
205
206
|
"consumption_load_curve",
|
|
206
|
-
start=dt.strptime("2023-03-01", "%Y-%m-%d"),
|
|
207
|
-
end=dt.strptime("2023-03-08", "%Y-%m-%d"),
|
|
207
|
+
start=dt.strptime("2023-03-01", "%Y-%m-%d").replace(tzinfo=LOCAL_TIMEZONE),
|
|
208
|
+
end=dt.strptime("2023-03-08", "%Y-%m-%d").replace(tzinfo=LOCAL_TIMEZONE),
|
|
208
209
|
prices=prices,
|
|
209
210
|
intervals=intervals,
|
|
210
211
|
cum_value=cumsum_value,
|
|
@@ -228,8 +229,8 @@ async def test_extra_date(mock_base: Mock, mock_detail) -> None: # pylint: disa
|
|
|
228
229
|
api = EnedisByPDL(pdl=PDL, token=TOKEN, session=ClientSession())
|
|
229
230
|
api.set_collects(
|
|
230
231
|
"consumption_load_curve",
|
|
231
|
-
start=dt.strptime("2023-03-01", "%Y-%m-%d"),
|
|
232
|
-
end=dt.strptime("2023-03-28", "%Y-%m-%d"),
|
|
232
|
+
start=dt.strptime("2023-03-01", "%Y-%m-%d").replace(tzinfo=LOCAL_TIMEZONE),
|
|
233
|
+
end=dt.strptime("2023-03-28", "%Y-%m-%d").replace(tzinfo=LOCAL_TIMEZONE),
|
|
233
234
|
prices=prices,
|
|
234
235
|
intervals=intervals,
|
|
235
236
|
)
|
|
@@ -243,8 +244,8 @@ async def test_extra_date(mock_base: Mock, mock_detail) -> None: # pylint: disa
|
|
|
243
244
|
|
|
244
245
|
api.set_collects(
|
|
245
246
|
"consumption_load_curve",
|
|
246
|
-
start=dt.strptime("2023-03-01", "%Y-%m-%d"),
|
|
247
|
-
end=dt.strptime("2023-03-28", "%Y-%m-%d"),
|
|
247
|
+
start=dt.strptime("2023-03-01", "%Y-%m-%d").replace(tzinfo=LOCAL_TIMEZONE),
|
|
248
|
+
end=dt.strptime("2023-03-28", "%Y-%m-%d").replace(tzinfo=LOCAL_TIMEZONE),
|
|
248
249
|
prices=prices,
|
|
249
250
|
intervals=intervals,
|
|
250
251
|
)
|
|
@@ -326,14 +327,16 @@ async def test_start_date(mock_enedis: Mock) -> None: # pylint: disable=unused-
|
|
|
326
327
|
"""Test with start_date."""
|
|
327
328
|
api = EnedisByPDL(pdl=PDL, token=TOKEN, session=ClientSession())
|
|
328
329
|
api.set_collects(
|
|
329
|
-
"consumption_load_curve",
|
|
330
|
+
"consumption_load_curve",
|
|
331
|
+
start=dt.strptime("2023-3-7", "%Y-%m-%d").replace(tzinfo=LOCAL_TIMEZONE),
|
|
330
332
|
)
|
|
331
333
|
await api.async_update_collects()
|
|
332
334
|
resultat = api.stats["consumption"]
|
|
333
335
|
assert len(resultat) == 0
|
|
334
336
|
|
|
335
337
|
api.set_collects(
|
|
336
|
-
"consumption_load_curve",
|
|
338
|
+
"consumption_load_curve",
|
|
339
|
+
start=dt.strptime("2023-3-7", "%Y-%m-%d").replace(tzinfo=LOCAL_TIMEZONE),
|
|
337
340
|
)
|
|
338
341
|
await api.async_update_collects()
|
|
339
342
|
resultat = api.stats["consumption"]
|
|
@@ -11,6 +11,7 @@ import pytest
|
|
|
11
11
|
|
|
12
12
|
import myelectricaldatapy
|
|
13
13
|
from myelectricaldatapy import Enedis, EnedisByPDL, EnedisException, LimitReached
|
|
14
|
+
from myelectricaldatapy.tz import LOCAL_TIMEZONE, local_now
|
|
14
15
|
|
|
15
16
|
from .consts import PDL, TOKEN
|
|
16
17
|
|
|
@@ -54,6 +55,59 @@ async def test_tempoday(mock_enedis: Mock) -> None: # pylint: disable=unused-ar
|
|
|
54
55
|
assert mypdl.tempo_day == "blue"
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
@freeze_time("2023-03-01")
|
|
59
|
+
async def test_check_offpeak(mock_contract) -> None:
|
|
60
|
+
"""Test off-peak hour detection against the real contract's schedule.
|
|
61
|
+
|
|
62
|
+
contract.json's offpeak_hours is "HC (1H30-8H00;12H30-14H00)". Patches
|
|
63
|
+
the HTTP layer (not async_get_contract itself) so the real parsing in
|
|
64
|
+
Enedis.async_get_contract runs and populates self.offpeaks.
|
|
65
|
+
"""
|
|
66
|
+
with patch.object(
|
|
67
|
+
myelectricaldatapy.auth.EnedisAuth,
|
|
68
|
+
"async_request",
|
|
69
|
+
return_value=mock_contract,
|
|
70
|
+
):
|
|
71
|
+
api = Enedis(token=TOKEN, session=ClientSession())
|
|
72
|
+
assert await api.async_has_offpeak(PDL) is True
|
|
73
|
+
|
|
74
|
+
# 03:00 falls inside the 01:30-08:00 off-peak window
|
|
75
|
+
assert (
|
|
76
|
+
await api.async_check_offpeak(
|
|
77
|
+
PDL, dt(2023, 3, 1, 3, 0, tzinfo=LOCAL_TIMEZONE)
|
|
78
|
+
)
|
|
79
|
+
is True
|
|
80
|
+
)
|
|
81
|
+
# exactly on the lower bound is excluded (start, end] convention
|
|
82
|
+
assert (
|
|
83
|
+
await api.async_check_offpeak(
|
|
84
|
+
PDL, dt(2023, 3, 1, 1, 30, tzinfo=LOCAL_TIMEZONE)
|
|
85
|
+
)
|
|
86
|
+
is False
|
|
87
|
+
)
|
|
88
|
+
# exactly on the upper bound is included
|
|
89
|
+
assert (
|
|
90
|
+
await api.async_check_offpeak(
|
|
91
|
+
PDL, dt(2023, 3, 1, 8, 0, tzinfo=LOCAL_TIMEZONE)
|
|
92
|
+
)
|
|
93
|
+
is True
|
|
94
|
+
)
|
|
95
|
+
# standard hours, not off-peak
|
|
96
|
+
assert (
|
|
97
|
+
await api.async_check_offpeak(
|
|
98
|
+
PDL, dt(2023, 3, 1, 10, 0, tzinfo=LOCAL_TIMEZONE)
|
|
99
|
+
)
|
|
100
|
+
is False
|
|
101
|
+
)
|
|
102
|
+
# second window 12:30-14:00
|
|
103
|
+
assert (
|
|
104
|
+
await api.async_check_offpeak(
|
|
105
|
+
PDL, dt(2023, 3, 1, 13, 0, tzinfo=LOCAL_TIMEZONE)
|
|
106
|
+
)
|
|
107
|
+
is True
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
57
111
|
async def test_valid_access(mock_enedis: Mock) -> None: # pylint: disable=unused-argument
|
|
58
112
|
"""Test access."""
|
|
59
113
|
api = Enedis(token=TOKEN, session=ClientSession())
|
|
@@ -87,8 +141,8 @@ async def test_fetch_data(mock_detail) -> None:
|
|
|
87
141
|
resultat = await api.async_fetch_datas(
|
|
88
142
|
service="comsumption_load_curve",
|
|
89
143
|
pdl=PDL,
|
|
90
|
-
start=dt.strptime("2022-12-30", "%Y-%m-%d"),
|
|
91
|
-
end=dt.strptime("2022-12-31", "%Y-%m-%d"),
|
|
144
|
+
start=dt.strptime("2022-12-30", "%Y-%m-%d").replace(tzinfo=LOCAL_TIMEZONE),
|
|
145
|
+
end=dt.strptime("2022-12-31", "%Y-%m-%d").replace(tzinfo=LOCAL_TIMEZONE),
|
|
92
146
|
)
|
|
93
147
|
assert (
|
|
94
148
|
resultat["meter_reading"]["interval_reading"]
|
|
@@ -100,7 +154,7 @@ async def test_force_refresh(
|
|
|
100
154
|
mock_enedis: Mock, # pylint: disable=unused-argument
|
|
101
155
|
) -> None:
|
|
102
156
|
"""Test refresh object."""
|
|
103
|
-
last_call =
|
|
157
|
+
last_call = local_now().strftime("%Y-%m-%dT%H:%M:%S.%f")
|
|
104
158
|
access = {
|
|
105
159
|
"valid": True,
|
|
106
160
|
"quota_reached": False,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|