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.
Files changed (47) hide show
  1. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.github/workflows/auto-approve.yml +1 -1
  2. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.github/workflows/python-lint.yml +1 -1
  3. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.github/workflows/release.yml +2 -2
  4. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/PKG-INFO +8 -4
  5. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/example.py +4 -3
  6. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/__init__.py +4 -1
  7. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/analytics.py +43 -48
  8. myelectricaldatapy-2.2.7/myelectricaldatapy/myelectricaldata.py → myelectricaldatapy-2.2.9/myelectricaldatapy/api.py +34 -29
  9. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/auth.py +5 -6
  10. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/const.py +2 -1
  11. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/mypdl.py +68 -48
  12. myelectricaldatapy-2.2.9/myelectricaldatapy/tz.py +64 -0
  13. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy.egg-info/PKG-INFO +8 -4
  14. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy.egg-info/SOURCES.txt +2 -1
  15. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy.egg-info/requires.txt +5 -0
  16. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy.egg-info/scm_file_list.json +2 -1
  17. myelectricaldatapy-2.2.9/myelectricaldatapy.egg-info/scm_version.json +8 -0
  18. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy.egg-info/top_level.txt +1 -0
  19. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/pyproject.toml +10 -6
  20. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/test_analytics.py +11 -8
  21. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/test_load_data.py +34 -8
  22. myelectricaldatapy-2.2.7/myelectricaldatapy.egg-info/scm_version.json +0 -8
  23. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.devcontainer/devcontainer-lock.json +0 -0
  24. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.devcontainer/devcontainer.json +0 -0
  25. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.github/dependabot.yml +0 -0
  26. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.github/workflows/python-publish.yml +0 -0
  27. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.gitignore +0 -0
  28. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.pre-commit-config.yaml +0 -0
  29. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/.vscode/settings.json +0 -0
  30. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/LICENSE +0 -0
  31. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/README.md +0 -0
  32. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/_secrets.yaml +0 -0
  33. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/exceptions.py +0 -0
  34. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy/py.typed +0 -0
  35. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/myelectricaldatapy.egg-info/dependency_links.txt +0 -0
  36. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/setup.cfg +0 -0
  37. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/__init__.py +0 -0
  38. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/conftest.py +0 -0
  39. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/consts.py +0 -0
  40. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/access.json +0 -0
  41. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/address.json +0 -0
  42. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/contract.json +0 -0
  43. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/daily.json +0 -0
  44. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/detail.json +0 -0
  45. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/ecowatt.json +0 -0
  46. {myelectricaldatapy-2.2.7 → myelectricaldatapy-2.2.9}/tests/fixtures/invalid_access.json +0 -0
  47. {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.actor == 'dependabot[bot]' || github.actor == 'pre-commit-ci[bot]' }}
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"
@@ -9,7 +9,7 @@ jobs:
9
9
  strategy:
10
10
  max-parallel: 4
11
11
  matrix:
12
- python-version: ["3.10", "3.11", "3.12"]
12
+ python-version: ["3.12", "3.13", "3.14"]
13
13
 
14
14
  steps:
15
15
  - name: 📥 Checkout the repository
@@ -13,7 +13,7 @@ jobs:
13
13
 
14
14
  - name: Generate changelog
15
15
  id: changelog
16
- uses: metcalfc/changelog-generator@v4.7.0
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.RELEASE_TOKEN }}
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.7
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.10.0
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 datetime, timedelta
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 = datetime.now() - timedelta(days=7)
36
- end = datetime.now()
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 __future__ import annotations
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 analaytics."""
32
-
33
- local_timezone = _local_timezone()
14
+ """Data analytics."""
34
15
 
35
- def __init__(self, data: Collection[Collection[str]]) -> None:
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.date = pd.to_datetime(self.df.date, format="%Y-%m-%d %H:%M:%S")
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.date = pd.to_datetime(self.df.date, format="%Y-%m-%d")
62
- self.df.date = self.df.date.dt.tz_localize(self.local_timezone)
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.date = pd.to_datetime(
66
- self.df.date, utc=True, format="%Y-%m-%d %H:%M:%S"
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.date.dt.minute == 0),
65
+ (self.df["date"].dt.minute == 0),
76
66
  "date",
77
- ] = self.df.date - timedelta(minutes=1)
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 = dt_start_date.tz_localize(self.local_timezone)
82
- self.df = self.df[(self.df.date > dt_start_date)]
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.date
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.interval_length = (
93
- self.df.interval_length.transform(self._weighted_interval)
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.value = (
100
- pd.to_numeric(self.df.value) / 1000 * self.df.interval_length
90
+ self.df["value"] = (
91
+ pd.to_numeric(self.df["value"]) / 1000 * self.df["interval_length"]
101
92
  )
102
93
  else:
103
- self.df.value = pd.to_numeric(self.df.value) * self.df.interval_length
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 ["blue", "white", "red"]:
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.loc[
164
- (self.df.date.dt.time > start) & (self.df.date.dt.time <= end),
165
- "notes",
166
- ] = ATTR_OFFPEAK
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]) -> pd.DataFrame:
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.date.dt.date == dt_date.date()), "tempo"] = value
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, cast
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
- """Get data of pdl."""
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 = dt.now()
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 dt.now().strftime("%Y-%m-%d")
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 (dt.now() + timedelta(days=1)).strftime("%Y-%m-%d")
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 dt.now().strftime("%Y-%m-%d")
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 (dt.now() + timedelta(days=1)).strftime("%Y-%m-%d")
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
- start_time = start.time()
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 = dt.strptime(range_time[0], "%HH%M").time()
131
- ending = dt.strptime(range_time[1], "%HH%M").time()
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
- finally:
176
- new_data = response.get("meter_reading", {}).get("interval_reading")
177
- if response is None or new_data is None:
178
- continue
179
- elif data is None:
180
- data = cast(dict[str, Any], response)
181
- else:
182
- data["meter_reading"]["interval_reading"].extend(new_data)
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 (asyncio.CancelledError, asyncio.TimeoutError) as error:
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 response.headers.get("Content-Type", ""):
53
+ if "application/json" in (error.headers or {}).get("Content-Type", ""):
55
54
  msg = json.loads(message)
56
- if response.status == 409:
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("price"): vol.Any(int, float),
40
+ vol.Required(ATTR_PRICE): vol.Any(int, float),
57
41
  },
58
42
  vol.Optional(ATTR_OFFPEAK): {
59
- vol.Required("price"): vol.Any(int, float),
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 = dt.now()
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 = dt.now().strftime("%Y-%m-%d")
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 = dt.now().strftime("%Y-%m-%d")
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
- start = dt.now() - timedelta(days=1095)
183
- end = dt.now() + timedelta(days=1)
184
- if force_refresh or self.last_access.date() != dt.now().date():
185
- self.access = {}
186
- self.contract = {}
187
- self.address = {}
188
- self.ecowatt = {}
189
- self.max_power = {}
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 = dt.now()
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 = dt.now()
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 dt.now() - timedelta(days=days)
337
- dt_end = end if end else dt.now() + timedelta(days=1)
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, "price", cum_price)
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.7
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.10.0
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
@@ -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
@@ -6,3 +6,8 @@ voluptuous>=0.13.1
6
6
  prek
7
7
  ruff
8
8
  yamllint
9
+ pytest
10
+ pytest-asyncio
11
+ python-typing-update
12
+ typing_extensions
13
+ freezegun
@@ -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",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.2.9",
3
+ "distance": 0,
4
+ "node": "gd64089c5c53ebbcc9f4ffe8a04ed5c1abc20a527",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-24"
8
+ }
@@ -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.10.0"
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", start=dt.strptime("2023-3-7", "%Y-%m-%d")
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", start=dt.strptime("2023-3-7", "%Y-%m-%d")
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 await api.async_check_offpeak(PDL, dt(2023, 3, 1, 3, 0)) is True
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 await api.async_check_offpeak(PDL, dt(2023, 3, 1, 1, 30)) is False
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 await api.async_check_offpeak(PDL, dt(2023, 3, 1, 8, 0)) is True
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 await api.async_check_offpeak(PDL, dt(2023, 3, 1, 10, 0)) is False
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 await api.async_check_offpeak(PDL, dt(2023, 3, 1, 13, 0)) is True
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 = dt.now().strftime("%Y-%m-%dT%H:%M:%S.%f")
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,
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.2.7",
3
- "distance": 0,
4
- "node": "g3fb21c9929da8ebafc38ec5cfef31d7ccd6ac826",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-16"
8
- }