myelectricaldatapy 2.2.7__tar.gz → 2.2.9__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.7 → myelectricaldatapy-2.2.9}/.github/workflows/auto-approve.yml +1 -1
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.github/workflows/python-lint.yml +1 -1
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.github/workflows/release.yml +2 -2
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/PKG-INFO +8 -4
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/example.py +4 -3
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/__init__.py +4 -1
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/analytics.py +43 -48
- myelectricaldatapy-2.2.7/myelectricaldatapy/myelectricaldata.py → myelectricaldatapy-2.2.9/myelectricaldatapy/api.py +34 -29
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/auth.py +5 -6
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/const.py +2 -1
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/mypdl.py +68 -48
- myelectricaldatapy-2.2.9/myelectricaldatapy/tz.py +64 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy.egg-info/PKG-INFO +8 -4
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy.egg-info/SOURCES.txt +2 -1
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy.egg-info/requires.txt +5 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy.egg-info/scm_file_list.json +2 -1
- myelectricaldatapy-2.2.9/myelectricaldatapy.egg-info/scm_version.json +8 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy.egg-info/top_level.txt +1 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/pyproject.toml +10 -6
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/test_analytics.py +11 -8
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/test_load_data.py +34 -8
- myelectricaldatapy-2.2.7/myelectricaldatapy.egg-info/scm_version.json +0 -8
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.devcontainer/devcontainer-lock.json +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.devcontainer/devcontainer.json +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.github/dependabot.yml +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.github/workflows/python-publish.yml +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.gitignore +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.pre-commit-config.yaml +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.vscode/settings.json +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/LICENSE +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/README.md +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/_secrets.yaml +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/exceptions.py +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/py.typed +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy.egg-info/dependency_links.txt +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/setup.cfg +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/__init__.py +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/conftest.py +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/consts.py +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/access.json +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/address.json +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/contract.json +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/daily.json +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/detail.json +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/ecowatt.json +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/invalid_access.json +0 -0
- {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/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.9
|
|
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,13 +1,14 @@
|
|
|
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
|
+
from .tz import get_local_timezone, set_local_timezone
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
13
14
|
"Enedis",
|
|
@@ -16,4 +17,6 @@ __all__ = [
|
|
|
16
17
|
"HttpRequestError",
|
|
17
18
|
"LimitReached",
|
|
18
19
|
"TimeoutExceededError",
|
|
20
|
+
"get_local_timezone",
|
|
21
|
+
"set_local_timezone",
|
|
19
22
|
]
|
|
@@ -1,40 +1,25 @@
|
|
|
1
1
|
"""Class for analytics."""
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
from collections.abc import Collection
|
|
6
|
-
from datetime import datetime as dt, timedelta
|
|
7
|
-
import os
|
|
3
|
+
from datetime import datetime as dt, timedelta, tzinfo as _tzinfo
|
|
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 get_local_timezone
|
|
28
11
|
|
|
29
12
|
|
|
30
13
|
class EnedisAnalytics:
|
|
31
|
-
"""Data
|
|
32
|
-
|
|
33
|
-
local_timezone = _local_timezone()
|
|
14
|
+
"""Data analytics."""
|
|
34
15
|
|
|
35
|
-
def __init__(self, data:
|
|
16
|
+
def __init__(self, data: Any, timezone: _tzinfo | None = None) -> None:
|
|
36
17
|
"""Initialize Dataframe."""
|
|
37
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()
|
|
38
23
|
|
|
39
24
|
def get_data_analytics(
|
|
40
25
|
self,
|
|
@@ -50,20 +35,25 @@ class EnedisAnalytics:
|
|
|
50
35
|
tempo: dict[str, str] | None = None,
|
|
51
36
|
) -> Any:
|
|
52
37
|
"""Convert data to analyze."""
|
|
38
|
+
|
|
53
39
|
cum_value = cum_value or {}
|
|
54
40
|
cum_price = cum_price or {}
|
|
55
41
|
step_hour = False
|
|
42
|
+
|
|
56
43
|
if not self.df.empty:
|
|
57
44
|
# Convert str to datetime
|
|
58
45
|
try:
|
|
59
|
-
self.df
|
|
46
|
+
self.df["date"] = pd.to_datetime(
|
|
47
|
+
self.df["date"], format="%Y-%m-%d %H:%M:%S"
|
|
48
|
+
)
|
|
60
49
|
except ValueError:
|
|
61
|
-
self.df
|
|
62
|
-
|
|
50
|
+
self.df["date"] = pd.to_datetime(self.df["date"], format="%Y-%m-%d")
|
|
51
|
+
|
|
52
|
+
self.df["date"] = self.df["date"].dt.tz_localize(self.local_timezone)
|
|
63
53
|
|
|
64
54
|
if convertUTC:
|
|
65
|
-
self.df
|
|
66
|
-
self.df
|
|
55
|
+
self.df["date"] = pd.to_datetime(
|
|
56
|
+
self.df["date"], utc=True, format="%Y-%m-%d %H:%M:%S"
|
|
67
57
|
)
|
|
68
58
|
|
|
69
59
|
# Subtract 1 minute at hour
|
|
@@ -72,16 +62,17 @@ class EnedisAnalytics:
|
|
|
72
62
|
if "interval_length" in self.df:
|
|
73
63
|
step_hour = True
|
|
74
64
|
self.df.loc[
|
|
75
|
-
(self.df
|
|
65
|
+
(self.df["date"].dt.minute == 0),
|
|
76
66
|
"date",
|
|
77
|
-
] = self.df
|
|
67
|
+
] = self.df["date"] - timedelta(minutes=1)
|
|
78
68
|
|
|
79
69
|
if start_date:
|
|
80
70
|
dt_start_date = pd.to_datetime(start_date, format="%Y-%m-%d %H:%M:%S")
|
|
81
|
-
dt_start_date
|
|
82
|
-
|
|
71
|
+
if dt_start_date.tzinfo is None:
|
|
72
|
+
dt_start_date = dt_start_date.tz_localize(self.local_timezone)
|
|
73
|
+
self.df = self.df[(self.df["date"] > dt_start_date)]
|
|
83
74
|
|
|
84
|
-
self.df.index = self.df
|
|
75
|
+
self.df.index = self.df["date"]
|
|
85
76
|
|
|
86
77
|
# Add mark
|
|
87
78
|
self.df["notes"] = ATTR_STANDARD
|
|
@@ -89,18 +80,20 @@ class EnedisAnalytics:
|
|
|
89
80
|
if self.df.empty:
|
|
90
81
|
return self.df.to_dict(orient="records")
|
|
91
82
|
|
|
92
|
-
self.df
|
|
93
|
-
self.df
|
|
83
|
+
self.df["interval_length"] = (
|
|
84
|
+
self.df["interval_length"].transform(self._weighted_interval)
|
|
94
85
|
if step_hour
|
|
95
86
|
else 1
|
|
96
87
|
)
|
|
97
88
|
|
|
98
89
|
if convertKwh:
|
|
99
|
-
self.df
|
|
100
|
-
pd.to_numeric(self.df
|
|
90
|
+
self.df["value"] = (
|
|
91
|
+
pd.to_numeric(self.df["value"]) / 1000 * self.df["interval_length"]
|
|
101
92
|
)
|
|
102
93
|
else:
|
|
103
|
-
self.df
|
|
94
|
+
self.df["value"] = (
|
|
95
|
+
pd.to_numeric(self.df["value"]) * self.df["interval_length"]
|
|
96
|
+
)
|
|
104
97
|
|
|
105
98
|
if intervals:
|
|
106
99
|
self._get_data_interval(intervals)
|
|
@@ -121,7 +114,7 @@ class EnedisAnalytics:
|
|
|
121
114
|
for mode, values in prices.items():
|
|
122
115
|
if isinstance(values, dict):
|
|
123
116
|
for offset, price in values.items():
|
|
124
|
-
if tempo and offset in
|
|
117
|
+
if tempo and offset in TEMPO_DAYS:
|
|
125
118
|
self.df.loc[
|
|
126
119
|
(self.df.notes == mode) & (self.df.tempo == offset),
|
|
127
120
|
"price",
|
|
@@ -149,7 +142,7 @@ class EnedisAnalytics:
|
|
|
149
142
|
|
|
150
143
|
def _weighted_interval(self, interval: str) -> float | int:
|
|
151
144
|
"""Compute weighted."""
|
|
152
|
-
if interval and len(rslt := re.findall("PT([0-9]{2})M", interval)) == 1:
|
|
145
|
+
if interval and len(rslt := re.findall("PT([0-9]{1,2})M", interval)) == 1:
|
|
153
146
|
return int(rslt[0]) / 60
|
|
154
147
|
return 1
|
|
155
148
|
|
|
@@ -159,16 +152,18 @@ class EnedisAnalytics:
|
|
|
159
152
|
# Convert str to datetime
|
|
160
153
|
start = pd.to_datetime(interval[0], format="%H:%M:%S").time()
|
|
161
154
|
end = pd.to_datetime(interval[1], format="%H:%M:%S").time()
|
|
162
|
-
# Mark
|
|
163
|
-
self.df.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
155
|
+
# Mark (handle ranges spanning midnight, e.g. 22:00:00 -> 06:00:00)
|
|
156
|
+
date_time = self.df["date"].dt.time
|
|
157
|
+
if start <= end:
|
|
158
|
+
mask = (date_time > start) & (date_time <= end)
|
|
159
|
+
else:
|
|
160
|
+
mask = (date_time > start) | (date_time <= end)
|
|
161
|
+
self.df.loc[mask, "notes"] = ATTR_OFFPEAK
|
|
167
162
|
|
|
168
163
|
return self.df
|
|
169
164
|
|
|
170
|
-
def _set_tempo_days(self, tempo: dict[str, str]) ->
|
|
165
|
+
def _set_tempo_days(self, tempo: dict[str, str]) -> None:
|
|
171
166
|
"""Add columns with tempo day."""
|
|
172
167
|
for str_date, value in tempo.items():
|
|
173
168
|
dt_date = pd.to_datetime(str_date, format="%Y-%m-%d")
|
|
174
|
-
self.df.loc[(self.df
|
|
169
|
+
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 as_local, get_local_timezone, 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
|
|
@@ -40,7 +39,7 @@ class Enedis:
|
|
|
40
39
|
daily_consumption, daily_production,
|
|
41
40
|
consumption_load_curve, production_load_curve
|
|
42
41
|
"""
|
|
43
|
-
self.last_access =
|
|
42
|
+
self.last_access = local_now()
|
|
44
43
|
path_range = ""
|
|
45
44
|
if start and end:
|
|
46
45
|
start_date = start.strftime("%Y-%m-%d")
|
|
@@ -93,12 +92,12 @@ class Enedis:
|
|
|
93
92
|
) -> Any:
|
|
94
93
|
"""Return Tempo Day."""
|
|
95
94
|
str_start = (
|
|
96
|
-
start.strftime("%Y-%m-%d") if start else
|
|
95
|
+
start.strftime("%Y-%m-%d") if start else local_now().strftime("%Y-%m-%d")
|
|
97
96
|
)
|
|
98
97
|
str_end = (
|
|
99
98
|
end.strftime("%Y-%m-%d")
|
|
100
99
|
if end
|
|
101
|
-
else (
|
|
100
|
+
else (local_now() + timedelta(days=1)).strftime("%Y-%m-%d")
|
|
102
101
|
)
|
|
103
102
|
return await self.auth.async_request(path=f"rte/tempo/{str_start}/{str_end}")
|
|
104
103
|
|
|
@@ -107,12 +106,12 @@ class Enedis:
|
|
|
107
106
|
) -> Any:
|
|
108
107
|
"""Return Ecowatt information."""
|
|
109
108
|
str_start = (
|
|
110
|
-
start.strftime("%Y-%m-%d") if start else
|
|
109
|
+
start.strftime("%Y-%m-%d") if start else local_now().strftime("%Y-%m-%d")
|
|
111
110
|
)
|
|
112
111
|
str_end = (
|
|
113
112
|
end.strftime("%Y-%m-%d")
|
|
114
113
|
if end
|
|
115
|
-
else (
|
|
114
|
+
else (local_now() + timedelta(days=1)).strftime("%Y-%m-%d")
|
|
116
115
|
)
|
|
117
116
|
return await self.async_request(path=f"rte/ecowatt/{str_start}/{str_end}")
|
|
118
117
|
|
|
@@ -125,10 +124,21 @@ class Enedis:
|
|
|
125
124
|
async def async_check_offpeak(self, pdl: str, start: dt) -> bool:
|
|
126
125
|
"""Return offpeak status."""
|
|
127
126
|
if await self.async_has_offpeak(pdl) is True:
|
|
128
|
-
|
|
127
|
+
# Off-peak windows are defined in local wall-clock time, so a
|
|
128
|
+
# start given in another timezone must be converted first.
|
|
129
|
+
local_timezone = get_local_timezone()
|
|
130
|
+
start_time = as_local(start).astimezone(local_timezone).time()
|
|
129
131
|
for range_time in self.offpeaks:
|
|
130
|
-
starting =
|
|
131
|
-
|
|
132
|
+
starting = (
|
|
133
|
+
dt.strptime(range_time[0], "%HH%M")
|
|
134
|
+
.replace(tzinfo=local_timezone)
|
|
135
|
+
.time()
|
|
136
|
+
)
|
|
137
|
+
ending = (
|
|
138
|
+
dt.strptime(range_time[1], "%HH%M")
|
|
139
|
+
.replace(tzinfo=local_timezone)
|
|
140
|
+
.time()
|
|
141
|
+
)
|
|
132
142
|
if starting < start_time <= ending:
|
|
133
143
|
return True
|
|
134
144
|
return False
|
|
@@ -162,7 +172,7 @@ class Enedis:
|
|
|
162
172
|
async def _async_get_details(self, mode: str, pdl: str, start: dt, end: dt) -> Any:
|
|
163
173
|
"""Get production details. (max: 7 days)."""
|
|
164
174
|
data = None
|
|
165
|
-
response =
|
|
175
|
+
response: dict[str, Any] | None = None
|
|
166
176
|
raise_error = False
|
|
167
177
|
for interval in list(self.date_range(start, end, 7)):
|
|
168
178
|
start, end = interval
|
|
@@ -171,15 +181,18 @@ class Enedis:
|
|
|
171
181
|
response = await self.async_fetch_datas(mode, pdl, start, end)
|
|
172
182
|
except EnedisException as error:
|
|
173
183
|
raise_error = True
|
|
184
|
+
response = None
|
|
174
185
|
_LOGGER.error(error)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
|
|
187
|
+
if response is None:
|
|
188
|
+
continue
|
|
189
|
+
new_data = response.get("meter_reading", {}).get("interval_reading")
|
|
190
|
+
if new_data is None:
|
|
191
|
+
continue
|
|
192
|
+
elif data is None:
|
|
193
|
+
data = response
|
|
194
|
+
else:
|
|
195
|
+
data["meter_reading"]["interval_reading"].extend(new_data)
|
|
183
196
|
|
|
184
197
|
return data
|
|
185
198
|
|
|
@@ -197,14 +210,6 @@ class Enedis:
|
|
|
197
210
|
start = s_end
|
|
198
211
|
yield (start, end)
|
|
199
212
|
|
|
200
|
-
async def __aenter__(self) -> Enedis:
|
|
201
|
-
"""Asynchronous enter."""
|
|
202
|
-
return self
|
|
203
|
-
|
|
204
|
-
async def __aexit__(self, *_exc_info: object) -> None:
|
|
205
|
-
"""Async exit."""
|
|
206
|
-
await self.async_close()
|
|
207
|
-
|
|
208
213
|
async def async_close(self) -> None:
|
|
209
214
|
"""Close the session."""
|
|
210
215
|
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
|
-
from datetime import date, datetime as dt, timedelta
|
|
4
|
+
from datetime import date, datetime as dt, timedelta, tzinfo as _tzinfo
|
|
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, set_local_timezone
|
|
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
|
)
|
|
@@ -100,11 +84,21 @@ class EnedisByPDL:
|
|
|
100
84
|
token: str,
|
|
101
85
|
session: ClientSession | None = None,
|
|
102
86
|
timeout: int = TIMEOUT,
|
|
87
|
+
timezone: _tzinfo | None = None,
|
|
103
88
|
) -> None:
|
|
104
|
-
"""Initialize.
|
|
89
|
+
"""Initialize.
|
|
90
|
+
|
|
91
|
+
timezone: the timezone to interpret naive datetimes in (e.g. Home
|
|
92
|
+
Assistant's hass.config.time_zone / dt_util.get_default_time_zone()).
|
|
93
|
+
Defaults to the host machine's timezone, which has no reason to
|
|
94
|
+
match the consuming application's configured timezone.
|
|
95
|
+
"""
|
|
105
96
|
session = ClientSession() if session is None else session
|
|
106
97
|
self._api: Enedis = Enedis(token, session, timeout)
|
|
107
98
|
self.pdl = pdl
|
|
99
|
+
self._timezone = timezone
|
|
100
|
+
if timezone is not None:
|
|
101
|
+
set_local_timezone(timezone)
|
|
108
102
|
self._connected: bool = False
|
|
109
103
|
self._ecowatt_subs: bool = False
|
|
110
104
|
self._maxpower_subs: bool = False
|
|
@@ -118,7 +112,7 @@ class EnedisByPDL:
|
|
|
118
112
|
self.has_collected: bool = False
|
|
119
113
|
self.has_parameters: bool = False
|
|
120
114
|
self.intervals: list[tuple[str, str]] = []
|
|
121
|
-
self.last_access: dt =
|
|
115
|
+
self.last_access: dt = local_now()
|
|
122
116
|
self.last_refresh: date | None = None
|
|
123
117
|
self.max_power: dict[str, Any] = {}
|
|
124
118
|
self.tempo: dict[str, Any] = {}
|
|
@@ -133,16 +127,44 @@ class EnedisByPDL:
|
|
|
133
127
|
"""Intervals exist."""
|
|
134
128
|
return len(self.intervals) > 0
|
|
135
129
|
|
|
130
|
+
@property
|
|
131
|
+
def has_tempo_subscription(self) -> bool:
|
|
132
|
+
"""Tempo subscription status."""
|
|
133
|
+
return self._tempo_subs
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def has_offpeak_hours_subscription(self) -> bool:
|
|
137
|
+
"""Offpeak hours subscription status."""
|
|
138
|
+
return self._off_subs
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def has_ecowatt_subscription(self) -> bool:
|
|
142
|
+
"""Ecowatt subscription status."""
|
|
143
|
+
return self._ecowatt_subs
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def has_maxpower_subscription(self) -> bool:
|
|
147
|
+
"""Max power subscription status."""
|
|
148
|
+
return self._maxpower_subs
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def subscription(self) -> Literal["hphc", "tempo", "standard"]:
|
|
152
|
+
if self.has_tempo_subscription:
|
|
153
|
+
return "tempo"
|
|
154
|
+
if self.has_offpeak_hours_subscription:
|
|
155
|
+
return "hphc"
|
|
156
|
+
return "standard"
|
|
157
|
+
|
|
136
158
|
@property
|
|
137
159
|
def ecowatt_day(self) -> Any:
|
|
138
160
|
"""ecowatt."""
|
|
139
|
-
str_date =
|
|
161
|
+
str_date = local_now().strftime("%Y-%m-%d")
|
|
140
162
|
return self.ecowatt.get(str_date, {})
|
|
141
163
|
|
|
142
164
|
@property
|
|
143
165
|
def tempo_day(self) -> str | None:
|
|
144
166
|
"""Tempo day."""
|
|
145
|
-
str_date =
|
|
167
|
+
str_date = local_now().strftime("%Y-%m-%d")
|
|
146
168
|
return self.tempo.get(str_date)
|
|
147
169
|
|
|
148
170
|
@property
|
|
@@ -161,7 +183,7 @@ class EnedisByPDL:
|
|
|
161
183
|
stats = {}
|
|
162
184
|
for mode, params in self._params.items():
|
|
163
185
|
data = params.get("data", {})
|
|
164
|
-
analytics = EnedisAnalytics(data)
|
|
186
|
+
analytics = EnedisAnalytics(data, timezone=self._timezone)
|
|
165
187
|
resultat = analytics.get_data_analytics(
|
|
166
188
|
convertKwh=True,
|
|
167
189
|
convertUTC=False,
|
|
@@ -179,22 +201,20 @@ class EnedisByPDL:
|
|
|
179
201
|
|
|
180
202
|
async def async_update(self, force_refresh: bool = False) -> None:
|
|
181
203
|
"""Update data."""
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
self.
|
|
188
|
-
self.
|
|
189
|
-
self.
|
|
204
|
+
|
|
205
|
+
start = local_now() - timedelta(days=1095)
|
|
206
|
+
end = local_now() + timedelta(days=1)
|
|
207
|
+
|
|
208
|
+
if force_refresh or (self.last_access.date() != local_now().date()):
|
|
209
|
+
self.contract.clear()
|
|
210
|
+
self.contract.clear()
|
|
211
|
+
self.address.clear()
|
|
212
|
+
self.ecowatt.clear()
|
|
213
|
+
self.max_power.clear()
|
|
190
214
|
self.has_collected = False
|
|
191
|
-
try:
|
|
192
|
-
if not self.access:
|
|
193
|
-
# Quota is scarce (ex: 50 calls/day on Enedis API), so the
|
|
194
|
-
# access/quota check is only refreshed once a day (or on
|
|
195
|
-
# force_refresh) instead of on every update cycle.
|
|
196
|
-
self.access = await self._api.async_valid_access(self.pdl)
|
|
197
215
|
|
|
216
|
+
try:
|
|
217
|
+
self.access = await self._api.async_valid_access(self.pdl)
|
|
198
218
|
if self.access.get("quota_reached", False):
|
|
199
219
|
detail = self.access.get("information", "Quota reached")
|
|
200
220
|
raise LimitReached(409, {"detail": detail})
|
|
@@ -224,11 +244,11 @@ class EnedisByPDL:
|
|
|
224
244
|
|
|
225
245
|
if self.has_parameters and self.has_collected is False:
|
|
226
246
|
await self.async_update_collects()
|
|
227
|
-
self.last_refresh =
|
|
247
|
+
self.last_refresh = local_now()
|
|
228
248
|
except EnedisException as error:
|
|
229
249
|
raise error from error
|
|
230
250
|
finally:
|
|
231
|
-
self.last_access =
|
|
251
|
+
self.last_access = local_now()
|
|
232
252
|
|
|
233
253
|
def tempo_subscription(self, activate: bool = False) -> None:
|
|
234
254
|
"""Enable or Disable Tempo Subscription."""
|
|
@@ -333,8 +353,8 @@ class EnedisByPDL:
|
|
|
333
353
|
days = 1095 if service in [DAILY_PROD, DAILY_CONSUM] else 7
|
|
334
354
|
mode = CONSUMPTION if service in [DAILY_CONSUM, DETAIL_CONSUM] else PRODUCTION
|
|
335
355
|
func = funcs[service]
|
|
336
|
-
dt_start = start if start else
|
|
337
|
-
dt_end = end if end else
|
|
356
|
+
dt_start = as_local(start) if start else local_now() - timedelta(days=days)
|
|
357
|
+
dt_end = as_local(end) if end else local_now() + timedelta(days=1)
|
|
338
358
|
self._params[mode] = {ATTR_FN: func, ATTR_START: dt_start, ATTR_END: dt_end}
|
|
339
359
|
if intervals:
|
|
340
360
|
self._set_intervals(mode, intervals)
|
|
@@ -343,7 +363,7 @@ class EnedisByPDL:
|
|
|
343
363
|
if cum_value:
|
|
344
364
|
self._set_cumsum(mode, "value", cum_value)
|
|
345
365
|
if cum_price:
|
|
346
|
-
self._set_cumsum(mode,
|
|
366
|
+
self._set_cumsum(mode, ATTR_PRICE, cum_price)
|
|
347
367
|
self.has_parameters = True
|
|
348
368
|
|
|
349
369
|
async def async_update_collects(self) -> None:
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
_override_timezone: _tzinfo | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_local_timezone() -> _tzinfo:
|
|
37
|
+
"""Return the timezone to use for interpreting naive datetimes.
|
|
38
|
+
|
|
39
|
+
Returns the timezone set via set_local_timezone(), if any, otherwise
|
|
40
|
+
falls back to LOCAL_TIMEZONE (the host machine's /etc/localtime). The
|
|
41
|
+
host's timezone has no reason to match the application embedding this
|
|
42
|
+
library, so a caller such as a Home Assistant integration should call
|
|
43
|
+
set_local_timezone() at startup with its own configured timezone (e.g.
|
|
44
|
+
hass.config.time_zone).
|
|
45
|
+
"""
|
|
46
|
+
return _override_timezone or LOCAL_TIMEZONE
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def set_local_timezone(tz: _tzinfo) -> None:
|
|
50
|
+
"""Override the timezone used to interpret naive datetimes."""
|
|
51
|
+
global _override_timezone
|
|
52
|
+
_override_timezone = tz
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def local_now() -> dt:
|
|
56
|
+
"""Return the current time, timezone-aware in the local zone."""
|
|
57
|
+
return dt.now(tz=get_local_timezone())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def as_local(value: dt) -> dt:
|
|
61
|
+
"""Attach the local timezone to a naive datetime; pass aware ones through."""
|
|
62
|
+
if value.tzinfo is None:
|
|
63
|
+
return value.replace(tzinfo=get_local_timezone())
|
|
64
|
+
return value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: myelectricaldatapy
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.9
|
|
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.7 → myelectricaldatapy-2.2.9}/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.7 → myelectricaldatapy-2.2.9}/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
|
|
|
@@ -71,15 +72,40 @@ async def test_check_offpeak(mock_contract) -> None:
|
|
|
71
72
|
assert await api.async_has_offpeak(PDL) is True
|
|
72
73
|
|
|
73
74
|
# 03:00 falls inside the 01:30-08:00 off-peak window
|
|
74
|
-
assert
|
|
75
|
+
assert (
|
|
76
|
+
await api.async_check_offpeak(
|
|
77
|
+
PDL, dt(2023, 3, 1, 3, 0, tzinfo=LOCAL_TIMEZONE)
|
|
78
|
+
)
|
|
79
|
+
is True
|
|
80
|
+
)
|
|
75
81
|
# exactly on the lower bound is excluded (start, end] convention
|
|
76
|
-
assert
|
|
82
|
+
assert (
|
|
83
|
+
await api.async_check_offpeak(
|
|
84
|
+
PDL, dt(2023, 3, 1, 1, 30, tzinfo=LOCAL_TIMEZONE)
|
|
85
|
+
)
|
|
86
|
+
is False
|
|
87
|
+
)
|
|
77
88
|
# exactly on the upper bound is included
|
|
78
|
-
assert
|
|
89
|
+
assert (
|
|
90
|
+
await api.async_check_offpeak(
|
|
91
|
+
PDL, dt(2023, 3, 1, 8, 0, tzinfo=LOCAL_TIMEZONE)
|
|
92
|
+
)
|
|
93
|
+
is True
|
|
94
|
+
)
|
|
79
95
|
# standard hours, not off-peak
|
|
80
|
-
assert
|
|
96
|
+
assert (
|
|
97
|
+
await api.async_check_offpeak(
|
|
98
|
+
PDL, dt(2023, 3, 1, 10, 0, tzinfo=LOCAL_TIMEZONE)
|
|
99
|
+
)
|
|
100
|
+
is False
|
|
101
|
+
)
|
|
81
102
|
# second window 12:30-14:00
|
|
82
|
-
assert
|
|
103
|
+
assert (
|
|
104
|
+
await api.async_check_offpeak(
|
|
105
|
+
PDL, dt(2023, 3, 1, 13, 0, tzinfo=LOCAL_TIMEZONE)
|
|
106
|
+
)
|
|
107
|
+
is True
|
|
108
|
+
)
|
|
83
109
|
|
|
84
110
|
|
|
85
111
|
async def test_valid_access(mock_enedis: Mock) -> None: # pylint: disable=unused-argument
|
|
@@ -115,8 +141,8 @@ async def test_fetch_data(mock_detail) -> None:
|
|
|
115
141
|
resultat = await api.async_fetch_datas(
|
|
116
142
|
service="comsumption_load_curve",
|
|
117
143
|
pdl=PDL,
|
|
118
|
-
start=dt.strptime("2022-12-30", "%Y-%m-%d"),
|
|
119
|
-
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),
|
|
120
146
|
)
|
|
121
147
|
assert (
|
|
122
148
|
resultat["meter_reading"]["interval_reading"]
|
|
@@ -128,7 +154,7 @@ async def test_force_refresh(
|
|
|
128
154
|
mock_enedis: Mock, # pylint: disable=unused-argument
|
|
129
155
|
) -> None:
|
|
130
156
|
"""Test refresh object."""
|
|
131
|
-
last_call =
|
|
157
|
+
last_call = local_now().strftime("%Y-%m-%dT%H:%M:%S.%f")
|
|
132
158
|
access = {
|
|
133
159
|
"valid": True,
|
|
134
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
|