myelectricaldatapy 2.2.5__tar.gz → 2.2.7__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 (52) hide show
  1. myelectricaldatapy-2.2.7/.devcontainer/devcontainer-lock.json +14 -0
  2. myelectricaldatapy-2.2.7/.devcontainer/devcontainer.json +60 -0
  3. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/.github/workflows/python-lint.yml +6 -9
  4. myelectricaldatapy-2.2.7/.github/workflows/python-publish.yml +31 -0
  5. myelectricaldatapy-2.2.7/.github/workflows/release.yml +35 -0
  6. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/.gitignore +4 -2
  7. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/.pre-commit-config.yaml +6 -6
  8. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/PKG-INFO +9 -4
  9. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/myelectricaldatapy/__init__.py +1 -3
  10. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/myelectricaldatapy/analytics.py +25 -10
  11. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/myelectricaldatapy/myelectricaldata.py +3 -10
  12. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/myelectricaldatapy/mypdl.py +13 -6
  13. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/myelectricaldatapy.egg-info/PKG-INFO +9 -4
  14. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/myelectricaldatapy.egg-info/SOURCES.txt +4 -4
  15. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/myelectricaldatapy.egg-info/requires.txt +5 -0
  16. myelectricaldatapy-2.2.7/myelectricaldatapy.egg-info/scm_file_list.json +40 -0
  17. myelectricaldatapy-2.2.7/myelectricaldatapy.egg-info/scm_version.json +8 -0
  18. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/pyproject.toml +13 -3
  19. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/tests/conftest.py +1 -1
  20. myelectricaldatapy-2.2.7/tests/fixtures/tempo.json +1 -0
  21. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/tests/test_load_data.py +30 -2
  22. myelectricaldatapy-2.2.5/.flake8 +0 -3
  23. myelectricaldatapy-2.2.5/.github/workflows/python-publish.yml +0 -36
  24. myelectricaldatapy-2.2.5/.github/workflows/release.yml +0 -59
  25. myelectricaldatapy-2.2.5/MANIFEST.in +0 -6
  26. myelectricaldatapy-2.2.5/requirements-dev.txt +0 -14
  27. myelectricaldatapy-2.2.5/requirements.txt +0 -3
  28. myelectricaldatapy-2.2.5/tests/fixtures/tempo.json +0 -1
  29. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/.github/dependabot.yml +0 -0
  30. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/.github/workflows/auto-approve.yml +0 -0
  31. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/.vscode/settings.json +0 -0
  32. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/LICENSE +0 -0
  33. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/README.md +0 -0
  34. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/_secrets.yaml +0 -0
  35. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/example.py +0 -0
  36. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/myelectricaldatapy/auth.py +0 -0
  37. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/myelectricaldatapy/const.py +0 -0
  38. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/myelectricaldatapy/exceptions.py +0 -0
  39. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/myelectricaldatapy/py.typed +0 -0
  40. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/myelectricaldatapy.egg-info/dependency_links.txt +0 -0
  41. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/myelectricaldatapy.egg-info/top_level.txt +0 -0
  42. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/setup.cfg +0 -0
  43. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/tests/__init__.py +0 -0
  44. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/tests/consts.py +0 -0
  45. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/tests/fixtures/access.json +0 -0
  46. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/tests/fixtures/address.json +0 -0
  47. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/tests/fixtures/contract.json +0 -0
  48. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/tests/fixtures/daily.json +0 -0
  49. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/tests/fixtures/detail.json +0 -0
  50. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/tests/fixtures/ecowatt.json +0 -0
  51. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/tests/fixtures/invalid_access.json +0 -0
  52. {myelectricaldatapy-2.2.5 → myelectricaldatapy-2.2.7}/tests/test_analytics.py +0 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "features": {
3
+ "ghcr.io/devcontainers-extra/features/prek:1": {
4
+ "version": "1.0.0",
5
+ "resolved": "ghcr.io/devcontainers-extra/features/prek@sha256:eb55e266252d8560cf726f27c5675e397bb833067a21428c50b273a59842f649",
6
+ "integrity": "sha256:eb55e266252d8560cf726f27c5675e397bb833067a21428c50b273a59842f649"
7
+ },
8
+ "ghcr.io/devcontainers-extra/features/uv:1": {
9
+ "version": "1.0.2",
10
+ "resolved": "ghcr.io/devcontainers-extra/features/uv@sha256:1ac5b9f17a9e9e745933d0ac2ecf758e06ed3da4423cd22c94cce4d482fd2dd8",
11
+ "integrity": "sha256:1ac5b9f17a9e9e745933d0ac2ecf758e06ed3da4423cd22c94cce4d482fd2dd8"
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,60 @@
1
+ // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2
+ // README at: https://github.com/devcontainers/templates/tree/main/src/python
3
+ {
4
+ "name": "Python 3",
5
+ "image": "mcr.microsoft.com/devcontainers/python:3-3.14-trixie",
6
+ "features": {
7
+ "ghcr.io/devcontainers-extra/features/prek:1": {},
8
+ "ghcr.io/devcontainers-extra/features/uv:1": {}
9
+ },
10
+ "postCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && prek install --overwrite && uv venv --seed --clear && uv pip install --no-cache-dir --link-mode=copy -e '.[dev]'",
11
+ "customizations": {
12
+ "vscode": {
13
+ "extensions": [
14
+ "ms-python.python",
15
+ "ms-python.vscode-pylance",
16
+ "charliermarsh.ruff",
17
+ "esbenp.prettier-vscode",
18
+ "dbaeumer.vscode-eslint",
19
+ "ms-azuretools.vscode-docker",
20
+ "ms-vscode-remote.remote-containers",
21
+ "streetsidesoftware.code-spell-checker",
22
+ "streetsidesoftware.code-spell-checker-french",
23
+ "anthropic.claude-code"
24
+ ],
25
+ "settings": {
26
+ // ── Python ──────────────────────────────────────────────────────
27
+ "python.terminal.activateEnvInCurrentTerminal": true,
28
+ "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
29
+ "[python]": {
30
+ "pythonPath": "${workspaceFolder}/.venv/bin/python",
31
+ "editor.defaultFormatter": "charliermarsh.ruff",
32
+ "editor.formatOnSave": true,
33
+ "editor.formatOnPaste": false,
34
+ "editor.formatOnType": true,
35
+ "editor.codeActionsOnSave": {
36
+ "source.fixAll": "explicit",
37
+ "source.organizeImports": "explicit"
38
+ }
39
+ },
40
+ "ruff.enable": true,
41
+ "ruff.organizeImports": true,
42
+ "[json]": {
43
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
44
+ },
45
+ "[yaml]": {
46
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
47
+ },
48
+ // ── Général ──────────────────────────────────────────────────────
49
+ "editor.tabSize": 2,
50
+ "editor.insertSpaces": true,
51
+ "files.eol": "\n",
52
+ "files.insertFinalNewline": true,
53
+ "files.trimTrailingWhitespace": true
54
+ }
55
+ }
56
+ },
57
+ "mounts": [
58
+ "source=${env:HOME}/.claude,target=/home/vscode/.claude,type=bind,consistency=cached"
59
+ ]
60
+ }
@@ -13,18 +13,15 @@ jobs:
13
13
 
14
14
  steps:
15
15
  - name: 📥 Checkout the repository
16
- uses: actions/checkout@v4
16
+ uses: actions/checkout@v7
17
17
 
18
- - name: Set up Python ${{ matrix.python-version }}
19
- uses: actions/setup-python@v5
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v7
20
20
  with:
21
21
  python-version: ${{ matrix.python-version }}
22
22
 
23
23
  - name: 📦 Install dependencies
24
- run: |
25
- python -m pip install --upgrade pip
26
- pip install -r requirements.txt
27
- pip install pylint
24
+ run: uv sync --extra dev
28
25
 
29
- - name: Run pre-commit
30
- uses: pre-commit/action@v3.0.1
26
+ - name: Run prek
27
+ run: uv run prek run --all-files
@@ -0,0 +1,31 @@
1
+ name: Upload Python Package
2
+
3
+ on:
4
+ release:
5
+ types: ["published"]
6
+ workflow_call:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ deploy:
11
+ runs-on: ubuntu-latest
12
+ name: Deploy to PyPi
13
+ environment: release
14
+ permissions:
15
+ id-token: write
16
+ steps:
17
+ - name: 📥 Checkout the repository
18
+ uses: actions/checkout@v7
19
+ with:
20
+ fetch-depth: 0
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v7
24
+ with:
25
+ python-version: "3.13"
26
+
27
+ - name: Build package
28
+ run: uv build
29
+
30
+ - name: 🚀 Publish to PyPi
31
+ uses: pypa/gh-action-pypi-publish@v1.14.2
@@ -0,0 +1,35 @@
1
+ name: Create release
2
+ on:
3
+ push:
4
+ tags:
5
+ - "*.*.*" # Push events to matching i.e. 1.0.0, 4.0.1
6
+ jobs:
7
+ release:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ # To use this repository's private action, you must check out the repository
11
+ - name: 📥 Checkout the repository
12
+ uses: actions/checkout@v7
13
+
14
+ - name: Generate changelog
15
+ id: changelog
16
+ uses: metcalfc/changelog-generator@v4.7.0
17
+ with:
18
+ myToken: ${{ secrets.GITHUB_TOKEN }}
19
+
20
+ - name: Create Release
21
+ uses: softprops/action-gh-release@v3
22
+ with:
23
+ name: ${{ github.ref_name }}
24
+ body: ${{ steps.changelog.outputs.changelog }}
25
+ draft: false
26
+ prerelease: ${{ contains(github.ref_name, '-') }}
27
+ token: ${{ secrets.RELEASE_TOKEN }}
28
+ publish:
29
+ needs: release
30
+ permissions:
31
+ contents: write
32
+ packages: write
33
+ id-token: write
34
+ if: ${{ ! contains(github.ref_name, '-') }}
35
+ uses: ./.github/workflows/python-publish.yml
@@ -2,7 +2,6 @@
2
2
  __pycache__/
3
3
  *.py[cod]
4
4
  *$py.class
5
- example_home.py
6
5
 
7
6
  # C extensions
8
7
  *.so
@@ -111,4 +110,7 @@ dmypy.json
111
110
  type_info.json
112
111
 
113
112
  # Secret files
114
- secrets.yaml
113
+ secrets.yaml
114
+
115
+ # Code
116
+ *.code-workpace
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  repos:
3
3
  - repo: https://github.com/astral-sh/ruff-pre-commit
4
- rev: v0.11.8
4
+ rev: v0.16.2
5
5
  hooks:
6
6
  - id: ruff
7
7
  args:
8
8
  - --fix
9
9
  - id: ruff-format
10
10
  - repo: https://github.com/codespell-project/codespell
11
- rev: v2.4.1
11
+ rev: v2.4.3
12
12
  hooks:
13
13
  - id: codespell
14
14
  args:
@@ -17,14 +17,14 @@ repos:
17
17
  - --quiet-level=2
18
18
  exclude_types: [csv, json]
19
19
  - repo: https://github.com/pre-commit/pre-commit-hooks
20
- rev: v5.0.0
20
+ rev: v6.0.0
21
21
  hooks:
22
22
  - id: check-executables-have-shebangs
23
23
  stages: [manual]
24
24
  - id: check-json
25
25
  exclude: (.vscode|.devcontainer)
26
26
  - repo: https://github.com/adrienverge/yamllint.git
27
- rev: v1.37.1
27
+ rev: v1.38.0
28
28
  hooks:
29
29
  - id: yamllint
30
30
  exclude: (.github|.vscode|.devcontainer)
@@ -33,7 +33,7 @@ repos:
33
33
  hooks:
34
34
  - id: prettier
35
35
  - repo: https://github.com/cdce8p/python-typing-update
36
- rev: v0.7.1
36
+ rev: v0.8.1
37
37
  hooks:
38
38
  # Run `python-typing-update` hook manually from time to time
39
39
  # to update python typing syntax.
@@ -46,7 +46,7 @@ repos:
46
46
  - --force
47
47
  - --keep-updates
48
48
  - repo: https://github.com/pre-commit/mirrors-mypy
49
- rev: v1.15.0
49
+ rev: v2.3.0
50
50
  hooks:
51
51
  - id: mypy
52
52
  args: [--strict, --ignore-missing-imports]
@@ -1,20 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: myelectricaldatapy
3
- Version: 2.2.5
3
+ Version: 2.2.7
4
4
  Summary: Fetch Linky data from myelectricaldata.fr
5
5
  Author-email: Cyr-ius <cyr-ius@ipocus.net>
6
- License: GPL-3
6
+ License-Expression: GPL-3.0-or-later
7
7
  Project-URL: Homepage, https://github.com/cyr-ius/myelectricaldatapy
8
8
  Keywords: enedis,linky,async,myelectricaldata
9
+ Platform: any
9
10
  Classifier: Programming Language :: Python
10
11
  Classifier: Development Status :: 5 - Production/Stable
11
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
15
17
  Classifier: Framework :: AsyncIO
16
18
  Classifier: Intended Audience :: Developers
17
- Classifier: Intended Audience :: System Administrators
18
19
  Classifier: Topic :: Home Automation
19
20
  Requires-Python: >=3.10.0
20
21
  Description-Content-Type: text/markdown
@@ -22,6 +23,10 @@ License-File: LICENSE
22
23
  Requires-Dist: aiohttp>=3.8.1
23
24
  Requires-Dist: pandas>=2.0.2
24
25
  Requires-Dist: voluptuous>=0.13.1
26
+ Provides-Extra: dev
27
+ Requires-Dist: prek; extra == "dev"
28
+ Requires-Dist: ruff; extra == "dev"
29
+ Requires-Dist: yamllint; extra == "dev"
25
30
  Dynamic: license-file
26
31
 
27
32
  # myelectricaldatapy
@@ -1,5 +1,3 @@
1
- # -*- coding:utf-8 -*-
2
-
3
1
  """myelectricaldatapy package."""
4
2
 
5
3
  from .exceptions import (
@@ -13,8 +11,8 @@ from .mypdl import EnedisByPDL
13
11
 
14
12
  __all__ = [
15
13
  "Enedis",
16
- "EnedisException",
17
14
  "EnedisByPDL",
15
+ "EnedisException",
18
16
  "HttpRequestError",
19
17
  "LimitReached",
20
18
  "TimeoutExceededError",
@@ -2,19 +2,35 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Collection
5
6
  from datetime import datetime as dt, timedelta
7
+ import os
6
8
  import re
7
- from typing import Any, Collection, Tuple
9
+ from typing import Any
10
+ from zoneinfo import ZoneInfo
8
11
 
9
12
  import pandas as pd
10
13
 
11
14
  from .const import ATTR_OFFPEAK, ATTR_STANDARD
12
15
 
13
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
28
+
29
+
14
30
  class EnedisAnalytics:
15
31
  """Data analaytics."""
16
32
 
17
- local_timezone = dt.now().astimezone().tzinfo
33
+ local_timezone = _local_timezone()
18
34
 
19
35
  def __init__(self, data: Collection[Collection[str]]) -> None:
20
36
  """Initialize Dataframe."""
@@ -25,15 +41,17 @@ class EnedisAnalytics:
25
41
  convertKwh: bool = False,
26
42
  convertUTC: bool = False,
27
43
  start_date: dt | None = None,
28
- intervals: list[Tuple[str, str]] | None = None,
44
+ intervals: list[tuple[str, str]] | None = None,
29
45
  groupby: bool = False,
30
46
  summary: bool = False,
31
- cum_value: dict[str, Any] = {},
32
- cum_price: dict[str, Any] = {},
47
+ cum_value: dict[str, Any] | None = None,
48
+ cum_price: dict[str, Any] | None = None,
33
49
  prices: dict[str, Any] | None = None,
34
50
  tempo: dict[str, str] | None = None,
35
51
  ) -> Any:
36
52
  """Convert data to analyze."""
53
+ cum_value = cum_value or {}
54
+ cum_price = cum_price or {}
37
55
  step_hour = False
38
56
  if not self.df.empty:
39
57
  # Convert str to datetime
@@ -54,10 +72,7 @@ class EnedisAnalytics:
54
72
  if "interval_length" in self.df:
55
73
  step_hour = True
56
74
  self.df.loc[
57
- (
58
- self.df.date.dt.minute
59
- == dt.strptime("00:00:00", "%H:%M:%S").minute
60
- ),
75
+ (self.df.date.dt.minute == 0),
61
76
  "date",
62
77
  ] = self.df.date - timedelta(minutes=1)
63
78
 
@@ -138,7 +153,7 @@ class EnedisAnalytics:
138
153
  return int(rslt[0]) / 60
139
154
  return 1
140
155
 
141
- def _get_data_interval(self, intervals: list[Tuple[str, str]]) -> pd.DataFrame:
156
+ def _get_data_interval(self, intervals: list[tuple[str, str]]) -> pd.DataFrame:
142
157
  """Group date from range time."""
143
158
  for interval in intervals:
144
159
  # Convert str to datetime
@@ -2,10 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Generator
5
6
  from datetime import date, datetime as dt, timedelta
6
7
  import logging
7
8
  import re
8
- from typing import Any, Generator, cast
9
+ from typing import Any, cast
9
10
 
10
11
  from aiohttp import ClientSession
11
12
 
@@ -27,7 +28,6 @@ class Enedis:
27
28
  self.auth = EnedisAuth(session, token, timeout)
28
29
  self.async_request = self.auth.async_request
29
30
  self.offpeaks: list[str] = []
30
- self.dt_offpeak: list[dt] = []
31
31
  self.last_access: date | None = None
32
32
 
33
33
  async def async_fetch_datas(
@@ -68,13 +68,6 @@ class Enedis:
68
68
  contract = usage_point.get("contracts", {})
69
69
  if offpeak_hours := contract.get("offpeak_hours"):
70
70
  self.offpeaks = re.findall("(?:(\\w+)-(\\w+))+", offpeak_hours)
71
- self.dt_offpeak = [
72
- ( # type: ignore
73
- dt.strptime(offpeak[0], "%HH%M"),
74
- dt.strptime(offpeak[1], "%HH%M"),
75
- )
76
- for offpeak in self.offpeaks
77
- ]
78
71
  return contract
79
72
 
80
73
  async def async_get_contracts(self, pdl: str) -> Any:
@@ -136,7 +129,7 @@ class Enedis:
136
129
  for range_time in self.offpeaks:
137
130
  starting = dt.strptime(range_time[0], "%HH%M").time()
138
131
  ending = dt.strptime(range_time[1], "%HH%M").time()
139
- if ending <= start_time > starting:
132
+ if starting < start_time <= ending:
140
133
  return True
141
134
  return False
142
135
 
@@ -2,9 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Callable
5
6
  from datetime import date, datetime as dt, timedelta
6
7
  import logging
7
- from typing import Any, Callable, Tuple
8
+ from typing import Any
8
9
 
9
10
  from aiohttp import ClientSession
10
11
  import voluptuous as vol
@@ -116,7 +117,7 @@ class EnedisByPDL:
116
117
  self.ecowatt: dict[str, Any] = {}
117
118
  self.has_collected: bool = False
118
119
  self.has_parameters: bool = False
119
- self.intervals: list[Tuple[str, str]] = []
120
+ self.intervals: list[tuple[str, str]] = []
120
121
  self.last_access: dt = dt.now()
121
122
  self.last_refresh: date | None = None
122
123
  self.max_power: dict[str, Any] = {}
@@ -141,7 +142,7 @@ class EnedisByPDL:
141
142
  @property
142
143
  def tempo_day(self) -> str | None:
143
144
  """Tempo day."""
144
- str_date = dt.now().strftime("%Y-%-m-%-d")
145
+ str_date = dt.now().strftime("%Y-%m-%d")
145
146
  return self.tempo.get(str_date)
146
147
 
147
148
  @property
@@ -181,13 +182,19 @@ class EnedisByPDL:
181
182
  start = dt.now() - timedelta(days=1095)
182
183
  end = dt.now() + timedelta(days=1)
183
184
  if force_refresh or self.last_access.date() != dt.now().date():
185
+ self.access = {}
184
186
  self.contract = {}
185
187
  self.address = {}
186
188
  self.ecowatt = {}
187
189
  self.max_power = {}
188
190
  self.has_collected = False
189
191
  try:
190
- self.access = await self._api.async_valid_access(self.pdl)
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
+
191
198
  if self.access.get("quota_reached", False):
192
199
  detail = self.access.get("information", "Quota reached")
193
200
  raise LimitReached(409, {"detail": detail})
@@ -241,7 +248,7 @@ class EnedisByPDL:
241
248
  """Enable or Disable Max power Subscription."""
242
249
  self._maxpower_subs = activate is True
243
250
 
244
- def _set_intervals(self, mode: str, intervals: list[Tuple[str, str]]) -> None:
251
+ def _set_intervals(self, mode: str, intervals: list[tuple[str, str]]) -> None:
245
252
  """Set intervals."""
246
253
  if isinstance(intervals, list):
247
254
  self.intervals = intervals
@@ -293,7 +300,7 @@ class EnedisByPDL:
293
300
  service: str,
294
301
  start: dt | None = None,
295
302
  end: dt | None = None,
296
- intervals: list[Tuple[str, str]] | None = None,
303
+ intervals: list[tuple[str, str]] | None = None,
297
304
  prices: dict[str, Any] | None = None,
298
305
  cum_value: dict[str, Any] | None = None,
299
306
  cum_price: dict[str, Any] | None = None,
@@ -1,20 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: myelectricaldatapy
3
- Version: 2.2.5
3
+ Version: 2.2.7
4
4
  Summary: Fetch Linky data from myelectricaldata.fr
5
5
  Author-email: Cyr-ius <cyr-ius@ipocus.net>
6
- License: GPL-3
6
+ License-Expression: GPL-3.0-or-later
7
7
  Project-URL: Homepage, https://github.com/cyr-ius/myelectricaldatapy
8
8
  Keywords: enedis,linky,async,myelectricaldata
9
+ Platform: any
9
10
  Classifier: Programming Language :: Python
10
11
  Classifier: Development Status :: 5 - Production/Stable
11
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
15
17
  Classifier: Framework :: AsyncIO
16
18
  Classifier: Intended Audience :: Developers
17
- Classifier: Intended Audience :: System Administrators
18
19
  Classifier: Topic :: Home Automation
19
20
  Requires-Python: >=3.10.0
20
21
  Description-Content-Type: text/markdown
@@ -22,6 +23,10 @@ License-File: LICENSE
22
23
  Requires-Dist: aiohttp>=3.8.1
23
24
  Requires-Dist: pandas>=2.0.2
24
25
  Requires-Dist: voluptuous>=0.13.1
26
+ Provides-Extra: dev
27
+ Requires-Dist: prek; extra == "dev"
28
+ Requires-Dist: ruff; extra == "dev"
29
+ Requires-Dist: yamllint; extra == "dev"
25
30
  Dynamic: license-file
26
31
 
27
32
  # myelectricaldatapy
@@ -1,14 +1,12 @@
1
- .flake8
2
1
  .gitignore
3
2
  .pre-commit-config.yaml
4
3
  LICENSE
5
- MANIFEST.in
6
4
  README.md
7
5
  _secrets.yaml
8
6
  example.py
9
7
  pyproject.toml
10
- requirements-dev.txt
11
- requirements.txt
8
+ .devcontainer/devcontainer-lock.json
9
+ .devcontainer/devcontainer.json
12
10
  .github/dependabot.yml
13
11
  .github/workflows/auto-approve.yml
14
12
  .github/workflows/python-lint.yml
@@ -27,6 +25,8 @@ myelectricaldatapy.egg-info/PKG-INFO
27
25
  myelectricaldatapy.egg-info/SOURCES.txt
28
26
  myelectricaldatapy.egg-info/dependency_links.txt
29
27
  myelectricaldatapy.egg-info/requires.txt
28
+ myelectricaldatapy.egg-info/scm_file_list.json
29
+ myelectricaldatapy.egg-info/scm_version.json
30
30
  myelectricaldatapy.egg-info/top_level.txt
31
31
  tests/__init__.py
32
32
  tests/conftest.py
@@ -1,3 +1,8 @@
1
1
  aiohttp>=3.8.1
2
2
  pandas>=2.0.2
3
3
  voluptuous>=0.13.1
4
+
5
+ [dev]
6
+ prek
7
+ ruff
8
+ yamllint
@@ -0,0 +1,40 @@
1
+ {
2
+ "files": [
3
+ ".devcontainer/devcontainer-lock.json",
4
+ ".devcontainer/devcontainer.json",
5
+ ".github/dependabot.yml",
6
+ ".github/workflows/auto-approve.yml",
7
+ ".github/workflows/python-lint.yml",
8
+ ".github/workflows/python-publish.yml",
9
+ ".github/workflows/release.yml",
10
+ ".gitignore",
11
+ ".pre-commit-config.yaml",
12
+ ".vscode/settings.json",
13
+ "LICENSE",
14
+ "README.md",
15
+ "_secrets.yaml",
16
+ "example.py",
17
+ "myelectricaldatapy/__init__.py",
18
+ "myelectricaldatapy/analytics.py",
19
+ "myelectricaldatapy/auth.py",
20
+ "myelectricaldatapy/const.py",
21
+ "myelectricaldatapy/exceptions.py",
22
+ "myelectricaldatapy/myelectricaldata.py",
23
+ "myelectricaldatapy/mypdl.py",
24
+ "myelectricaldatapy/py.typed",
25
+ "pyproject.toml",
26
+ "tests/__init__.py",
27
+ "tests/conftest.py",
28
+ "tests/consts.py",
29
+ "tests/fixtures/access.json",
30
+ "tests/fixtures/address.json",
31
+ "tests/fixtures/contract.json",
32
+ "tests/fixtures/daily.json",
33
+ "tests/fixtures/detail.json",
34
+ "tests/fixtures/ecowatt.json",
35
+ "tests/fixtures/invalid_access.json",
36
+ "tests/fixtures/tempo.json",
37
+ "tests/test_analytics.py",
38
+ "tests/test_load_data.py"
39
+ ]
40
+ }
@@ -0,0 +1,8 @@
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
+ }
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "myelectricaldatapy"
7
7
  dynamic = ["version"]
8
- license = {text = "GPL-3"}
8
+ license = "GPL-3.0-or-later"
9
9
  description = "Fetch Linky data from myelectricaldata.fr"
10
10
  readme = "README.md"
11
11
  authors = [
@@ -15,13 +15,13 @@ keywords = ["enedis", "linky", "async", "myelectricaldata"]
15
15
  classifiers = [
16
16
  "Programming Language :: Python",
17
17
  "Development Status :: 5 - Production/Stable",
18
- "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
19
18
  "Programming Language :: Python :: 3",
20
19
  "Programming Language :: Python :: 3.10",
21
20
  "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
22
23
  "Framework :: AsyncIO",
23
24
  "Intended Audience :: Developers",
24
- "Intended Audience :: System Administrators",
25
25
  "Topic :: Home Automation",
26
26
  ]
27
27
  requires-python = ">=3.10.0"
@@ -31,11 +31,21 @@ dependencies = [
31
31
  "voluptuous>=0.13.1",
32
32
  ]
33
33
 
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "prek",
37
+ "ruff",
38
+ "yamllint",
39
+ ]
40
+
34
41
  [project.urls]
35
42
  Homepage = "https://github.com/cyr-ius/myelectricaldatapy"
36
43
 
37
44
  [tool.setuptools_scm]
38
45
 
46
+ [tool.setuptools]
47
+ platforms = ["any"]
48
+
39
49
  [tool.pytest.ini_options]
40
50
  asyncio_mode = "auto"
41
51
 
@@ -48,7 +48,7 @@ def mock_tempo(return_invalid: bool = False) -> dict[str, Any]:
48
48
  def mock_ecowatt(request) -> dict[str, Any]:
49
49
  if hasattr(request, "param") and request.param:
50
50
  return {
51
- "detail": "Pas de données disponible entre la date du 2023-01-22 00:00:00 et 2023-01-22 00:00:00" # noqa
51
+ "detail": "Pas de données disponible entre la date du 2023-01-22 00:00:00 et 2023-01-22 00:00:00"
52
52
  }
53
53
  return json.loads(load_fixture("ecowatt.json"))
54
54
 
@@ -0,0 +1 @@
1
+ { "2023-03-01": "blue", "2023-03-02": "red", "2023-03-03": "blue" }
@@ -44,7 +44,7 @@ async def test_tempoday(mock_enedis: Mock) -> None: # pylint: disable=unused-ar
44
44
  """Test get tempo day."""
45
45
  api = Enedis(token=TOKEN, session=ClientSession())
46
46
  resultat = await api.async_get_tempo()
47
- assert resultat["2023-3-1"] == "blue"
47
+ assert resultat["2023-03-01"] == "blue"
48
48
 
49
49
  mypdl = EnedisByPDL(pdl=PDL, token=TOKEN, session=ClientSession())
50
50
  mypdl.set_collects("consumption_load_curve")
@@ -54,6 +54,34 @@ async def test_tempoday(mock_enedis: Mock) -> None: # pylint: disable=unused-ar
54
54
  assert mypdl.tempo_day == "blue"
55
55
 
56
56
 
57
+ @freeze_time("2023-03-01")
58
+ async def test_check_offpeak(mock_contract) -> None:
59
+ """Test off-peak hour detection against the real contract's schedule.
60
+
61
+ contract.json's offpeak_hours is "HC (1H30-8H00;12H30-14H00)". Patches
62
+ the HTTP layer (not async_get_contract itself) so the real parsing in
63
+ Enedis.async_get_contract runs and populates self.offpeaks.
64
+ """
65
+ with patch.object(
66
+ myelectricaldatapy.auth.EnedisAuth,
67
+ "async_request",
68
+ return_value=mock_contract,
69
+ ):
70
+ api = Enedis(token=TOKEN, session=ClientSession())
71
+ assert await api.async_has_offpeak(PDL) is True
72
+
73
+ # 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
+ # 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
77
+ # exactly on the upper bound is included
78
+ assert await api.async_check_offpeak(PDL, dt(2023, 3, 1, 8, 0)) is True
79
+ # standard hours, not off-peak
80
+ assert await api.async_check_offpeak(PDL, dt(2023, 3, 1, 10, 0)) is False
81
+ # second window 12:30-14:00
82
+ assert await api.async_check_offpeak(PDL, dt(2023, 3, 1, 13, 0)) is True
83
+
84
+
57
85
  async def test_valid_access(mock_enedis: Mock) -> None: # pylint: disable=unused-argument
58
86
  """Test access."""
59
87
  api = Enedis(token=TOKEN, session=ClientSession())
@@ -92,7 +120,7 @@ async def test_fetch_data(mock_detail) -> None:
92
120
  )
93
121
  assert (
94
122
  resultat["meter_reading"]["interval_reading"]
95
- == mock_detail["meter_reading"]["interval_reading"] # noqa
123
+ == mock_detail["meter_reading"]["interval_reading"]
96
124
  )
97
125
 
98
126
 
@@ -1,3 +0,0 @@
1
- [flake8]
2
- max-line-length = 88
3
- extend-ignore = E203
@@ -1,36 +0,0 @@
1
- name: Upload Python Package
2
-
3
- on:
4
- release:
5
- types: ["published"]
6
- workflow_call:
7
- workflow_dispatch:
8
-
9
- jobs:
10
- deploy:
11
- runs-on: ubuntu-latest
12
- name: Deploy to PyPi
13
- environment: release
14
- permissions:
15
- id-token: write
16
- steps:
17
- - name: 📥 Checkout the repository
18
- uses: actions/checkout@v4
19
-
20
- - name: Set up Python
21
- uses: actions/setup-python@v5
22
- with:
23
- python-version: "3.x"
24
-
25
- - name: 📦 Install dependencies
26
- run: |
27
- python -m pip install --upgrade pip
28
- pip install setuptools wheel twine build
29
-
30
- - name: Build package
31
- shell: bash
32
- run: |
33
- python -m build
34
-
35
- - name: 🚀 Publish to PyPi
36
- uses: pypa/gh-action-pypi-publish@release/v1
@@ -1,59 +0,0 @@
1
- name: Create release
2
- on:
3
- push:
4
- tags:
5
- - "*.*.*" # Push events to matching i.e. 1.0.0, 4.0.1
6
- jobs:
7
- release:
8
- runs-on: ubuntu-latest
9
- steps:
10
- # To use this repository's private action, you must check out the repository
11
- - name: 📥 Checkout the repository
12
- uses: actions/checkout@v4
13
-
14
- - name: Generate changelog
15
- id: changelog
16
- uses: metcalfc/changelog-generator@v4.6.2
17
- with:
18
- myToken: ${{ secrets.GITHUB_TOKEN }}
19
-
20
- - name: Create Release
21
- id: create_release
22
- uses: actions/create-release@latest
23
- env:
24
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25
- with:
26
- tag_name: ${{ github.ref }}
27
- release_name: ${{ github.ref }}
28
- body: ${{ steps.changelog.outputs.changelog }}
29
- draft: false
30
- prerelease: ${{ contains(github.ref_name,'-') }}
31
- publish:
32
- runs-on: ubuntu-latest
33
- needs: release
34
- permissions:
35
- contents: write
36
- packages: write
37
- id-token: write
38
- if: ${{ ! contains(github.ref_name, '-') }}
39
- steps:
40
- - name: 📥 Checkout the repository
41
- uses: actions/checkout@v4
42
-
43
- - name: Set up Python
44
- uses: actions/setup-python@v5
45
- with:
46
- python-version: "3.x"
47
-
48
- - name: 📦 Install dependencies
49
- run: |
50
- python -m pip install --upgrade pip
51
- pip install setuptools wheel twine build
52
-
53
- - name: Build package
54
- shell: bash
55
- run: |
56
- python -m build
57
-
58
- - name: 🚀 Publish to PyPi
59
- uses: pypa/gh-action-pypi-publish@release/v1
@@ -1,6 +0,0 @@
1
- include LICENSE
2
- exclude example_home.py
3
- recursive-exclude * __pycache__
4
- recursive-exclude * *.pyc
5
- recursive-exclude * *.pyo
6
- recursive-exclude * *.orig
@@ -1,14 +0,0 @@
1
- # AIOSysbus
2
- -r requirements.txt
3
-
4
- codespell
5
- mypy
6
- pre-commit
7
- pytest
8
- pytest-asyncio
9
- python-typing-update
10
- ruff
11
- setuptools_scm
12
- types-requests
13
- yamllint
14
- freezegun
@@ -1,3 +0,0 @@
1
- aiohttp>=3.8.1
2
- pandas>=2.0.2
3
- voluptuous>=0.13.1
@@ -1 +0,0 @@
1
- { "2023-3-1": "blue", "2023-3-2": "red", "2023-3-3": "blue" }