leneda-client 0.1.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- leneda_client-0.3.0/LICENSE +21 -0
- {leneda_client-0.1.1 → leneda_client-0.3.0}/PKG-INFO +37 -4
- leneda_client-0.3.0/README.md +52 -0
- {leneda_client-0.1.1 → leneda_client-0.3.0}/examples/advanced_usage.py +10 -13
- {leneda_client-0.1.1 → leneda_client-0.3.0}/examples/basic_usage.py +5 -6
- {leneda_client-0.1.1 → leneda_client-0.3.0}/src/leneda/__init__.py +2 -4
- {leneda_client-0.1.1 → leneda_client-0.3.0}/src/leneda/client.py +105 -39
- leneda_client-0.3.0/src/leneda/exceptions.py +27 -0
- {leneda_client-0.1.1 → leneda_client-0.3.0}/src/leneda/models.py +16 -12
- {leneda_client-0.1.1 → leneda_client-0.3.0}/src/leneda/obis_codes.py +60 -63
- {leneda_client-0.1.1 → leneda_client-0.3.0}/src/leneda/version.py +1 -1
- {leneda_client-0.1.1 → leneda_client-0.3.0}/src/leneda_client.egg-info/PKG-INFO +37 -4
- {leneda_client-0.1.1 → leneda_client-0.3.0}/src/leneda_client.egg-info/SOURCES.txt +2 -0
- {leneda_client-0.1.1 → leneda_client-0.3.0}/tests/test_client.py +112 -12
- leneda_client-0.1.1/README.md +0 -21
- {leneda_client-0.1.1 → leneda_client-0.3.0}/MANIFEST.in +0 -0
- {leneda_client-0.1.1 → leneda_client-0.3.0}/pyproject.toml +0 -0
- {leneda_client-0.1.1 → leneda_client-0.3.0}/requirements.txt +0 -0
- {leneda_client-0.1.1 → leneda_client-0.3.0}/setup.cfg +0 -0
- {leneda_client-0.1.1 → leneda_client-0.3.0}/setup.py +0 -0
- {leneda_client-0.1.1 → leneda_client-0.3.0}/src/leneda_client.egg-info/dependency_links.txt +0 -0
- {leneda_client-0.1.1 → leneda_client-0.3.0}/src/leneda_client.egg-info/requires.txt +0 -0
- {leneda_client-0.1.1 → leneda_client-0.3.0}/src/leneda_client.egg-info/top_level.txt +0 -0
- {leneda_client-0.1.1 → leneda_client-0.3.0}/tests/__init__.py +0 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 fedus
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: leneda-client
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: Python client for the Leneda energy data platform
|
5
5
|
Home-page: https://github.com/fedus/leneda-client
|
6
6
|
Author: fedus
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
21
|
Classifier: Topic :: Utilities
|
22
22
|
Requires-Python: >=3.8
|
23
23
|
Description-Content-Type: text/markdown
|
24
|
+
License-File: LICENSE
|
24
25
|
Requires-Dist: requests>=2.25.0
|
25
26
|
Requires-Dist: python-dateutil>=2.8.2
|
26
27
|
Dynamic: author
|
@@ -30,6 +31,7 @@ Dynamic: description
|
|
30
31
|
Dynamic: description-content-type
|
31
32
|
Dynamic: home-page
|
32
33
|
Dynamic: keywords
|
34
|
+
Dynamic: license-file
|
33
35
|
Dynamic: project-url
|
34
36
|
Dynamic: requires-dist
|
35
37
|
Dynamic: requires-python
|
@@ -37,9 +39,10 @@ Dynamic: summary
|
|
37
39
|
|
38
40
|
# Leneda API Client
|
39
41
|
|
40
|
-
[![PyPI version]](https://pypi.org/project/leneda-client/)
|
41
|
-
[![Python
|
42
|
-
[![License]](https://github.com/fedus/leneda-client/blob/main/LICENSE)
|
42
|
+
[](https://pypi.org/project/leneda-client/)
|
43
|
+
[](https://pypi.org/project/leneda-client/)
|
44
|
+
[](https://github.com/fedus/leneda-client/blob/main/LICENSE)
|
45
|
+
|
43
46
|
|
44
47
|
A Python client for interacting with the Leneda energy data platform API.
|
45
48
|
|
@@ -56,3 +59,33 @@ This client provides a simple interface to the Leneda API, which allows users to
|
|
56
59
|
|
57
60
|
```bash
|
58
61
|
pip install leneda-client
|
62
|
+
```
|
63
|
+
|
64
|
+
## Trying it out
|
65
|
+
|
66
|
+
```bash
|
67
|
+
$ export LENEDA_ENERGY_ID='LUXE-xx-yy-1234'
|
68
|
+
$ export LENEDA_API_KEY='YOUR-API-KEY'
|
69
|
+
$ python examples/basic_usage.py --metering-point LU0000012345678901234000000000000
|
70
|
+
Example 1: Getting hourly electricity consumption data for the last 7 days
|
71
|
+
Retrieved 514 consumption measurements
|
72
|
+
Unit: kW
|
73
|
+
Interval length: PT15M
|
74
|
+
Metering point: LU0000012345678901234000000000000
|
75
|
+
OBIS code: ObisCode.ELEC_CONSUMPTION_ACTIVE
|
76
|
+
|
77
|
+
First 3 measurements:
|
78
|
+
Time: 2025-04-18T13:30:00+00:00, Value: 0.048 kW, Type: Actual, Version: 2, Calculated: False
|
79
|
+
Time: 2025-04-18T13:45:00+00:00, Value: 0.08 kW, Type: Actual, Version: 2, Calculated: False
|
80
|
+
Time: 2025-04-18T14:00:00+00:00, Value: 0.08 kW, Type: Actual, Version: 2, Calculated: False
|
81
|
+
|
82
|
+
Example 2: Getting monthly aggregated electricity consumption for 2025
|
83
|
+
Retrieved 4 monthly aggregations
|
84
|
+
Unit: kWh
|
85
|
+
|
86
|
+
Monthly consumption:
|
87
|
+
Period: 2024-12 to 2025-01, Value: 30.858 kWh, Calculated: False
|
88
|
+
Period: 2025-01 to 2025-02, Value: 148.985 kWh, Calculated: False
|
89
|
+
Period: 2025-02 to 2025-03, Value: 44.619 kWh, Calculated: False
|
90
|
+
Period: 2025-03 to 2025-04, Value: 29.662 kWh, Calculated: False
|
91
|
+
```
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# Leneda API Client
|
2
|
+
|
3
|
+
[](https://pypi.org/project/leneda-client/)
|
4
|
+
[](https://pypi.org/project/leneda-client/)
|
5
|
+
[](https://github.com/fedus/leneda-client/blob/main/LICENSE)
|
6
|
+
|
7
|
+
|
8
|
+
A Python client for interacting with the Leneda energy data platform API.
|
9
|
+
|
10
|
+
## Overview
|
11
|
+
|
12
|
+
This client provides a simple interface to the Leneda API, which allows users to:
|
13
|
+
|
14
|
+
- Retrieve metering data for specific time ranges
|
15
|
+
- Get aggregated metering data (hourly, daily, weekly, monthly, or total)
|
16
|
+
- Create metering data access requests
|
17
|
+
- Use predefined OBIS code constants for easy reference
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
```bash
|
22
|
+
pip install leneda-client
|
23
|
+
```
|
24
|
+
|
25
|
+
## Trying it out
|
26
|
+
|
27
|
+
```bash
|
28
|
+
$ export LENEDA_ENERGY_ID='LUXE-xx-yy-1234'
|
29
|
+
$ export LENEDA_API_KEY='YOUR-API-KEY'
|
30
|
+
$ python examples/basic_usage.py --metering-point LU0000012345678901234000000000000
|
31
|
+
Example 1: Getting hourly electricity consumption data for the last 7 days
|
32
|
+
Retrieved 514 consumption measurements
|
33
|
+
Unit: kW
|
34
|
+
Interval length: PT15M
|
35
|
+
Metering point: LU0000012345678901234000000000000
|
36
|
+
OBIS code: ObisCode.ELEC_CONSUMPTION_ACTIVE
|
37
|
+
|
38
|
+
First 3 measurements:
|
39
|
+
Time: 2025-04-18T13:30:00+00:00, Value: 0.048 kW, Type: Actual, Version: 2, Calculated: False
|
40
|
+
Time: 2025-04-18T13:45:00+00:00, Value: 0.08 kW, Type: Actual, Version: 2, Calculated: False
|
41
|
+
Time: 2025-04-18T14:00:00+00:00, Value: 0.08 kW, Type: Actual, Version: 2, Calculated: False
|
42
|
+
|
43
|
+
Example 2: Getting monthly aggregated electricity consumption for 2025
|
44
|
+
Retrieved 4 monthly aggregations
|
45
|
+
Unit: kWh
|
46
|
+
|
47
|
+
Monthly consumption:
|
48
|
+
Period: 2024-12 to 2025-01, Value: 30.858 kWh, Calculated: False
|
49
|
+
Period: 2025-01 to 2025-02, Value: 148.985 kWh, Calculated: False
|
50
|
+
Period: 2025-02 to 2025-03, Value: 44.619 kWh, Calculated: False
|
51
|
+
Period: 2025-03 to 2025-04, Value: 29.662 kWh, Calculated: False
|
52
|
+
```
|
@@ -10,8 +10,8 @@ LENEDA_API_KEY: Your Leneda API key
|
|
10
10
|
LENEDA_ENERGY_ID: Your Energy ID
|
11
11
|
|
12
12
|
Usage:
|
13
|
-
python advanced_usage.py --api-key YOUR_API_KEY --energy-id YOUR_ENERGY_ID
|
14
|
-
python advanced_usage.py --metering-point LU-METERING_POINT1 --example 2
|
13
|
+
python advanced_usage.py --api-key YOUR_API_KEY --energy-id YOUR_ENERGY_ID --metering-point LU-METERING_POINT1
|
14
|
+
python advanced_usage.py --api-key YOUR_API_KEY --energy-id YOUR_ENERGY_ID --metering-point LU-METERING_POINT1 --example 2
|
15
15
|
"""
|
16
16
|
|
17
17
|
import argparse
|
@@ -24,12 +24,9 @@ from typing import Optional
|
|
24
24
|
import matplotlib.pyplot as plt
|
25
25
|
import pandas as pd
|
26
26
|
|
27
|
-
from leneda import
|
28
|
-
|
29
|
-
|
30
|
-
LenedaClient,
|
31
|
-
MeteringData,
|
32
|
-
)
|
27
|
+
from leneda import LenedaClient
|
28
|
+
from leneda.models import AggregatedMeteringData, MeteringData
|
29
|
+
from leneda.obis_codes import ObisCode
|
33
30
|
|
34
31
|
# Set up logging
|
35
32
|
logging.basicConfig(
|
@@ -193,7 +190,7 @@ def compare_consumption_periods(
|
|
193
190
|
# Get data for period 1
|
194
191
|
period1_data = client.get_metering_data(
|
195
192
|
metering_point_code=metering_point,
|
196
|
-
obis_code=
|
193
|
+
obis_code=ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
197
194
|
start_date_time=period1_start,
|
198
195
|
end_date_time=period1_end,
|
199
196
|
)
|
@@ -201,7 +198,7 @@ def compare_consumption_periods(
|
|
201
198
|
# Get data for period 2
|
202
199
|
period2_data = client.get_metering_data(
|
203
200
|
metering_point_code=metering_point,
|
204
|
-
obis_code=
|
201
|
+
obis_code=ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
205
202
|
start_date_time=period2_start,
|
206
203
|
end_date_time=period2_end,
|
207
204
|
)
|
@@ -290,7 +287,7 @@ def analyze_monthly_trends(
|
|
290
287
|
|
291
288
|
monthly_data = client.get_aggregated_metering_data(
|
292
289
|
metering_point_code=metering_point,
|
293
|
-
obis_code=
|
290
|
+
obis_code=ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
294
291
|
start_date=start_date,
|
295
292
|
end_date=end_date,
|
296
293
|
aggregation_level="Month",
|
@@ -355,7 +352,7 @@ def detect_consumption_anomalies(
|
|
355
352
|
# Get hourly consumption data
|
356
353
|
consumption_data = client.get_metering_data(
|
357
354
|
metering_point_code=metering_point,
|
358
|
-
obis_code=
|
355
|
+
obis_code=ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
359
356
|
start_date_time=start_date,
|
360
357
|
end_date_time=end_date,
|
361
358
|
)
|
@@ -473,7 +470,7 @@ def main():
|
|
473
470
|
)
|
474
471
|
consumption_data = client.get_metering_data(
|
475
472
|
metering_point_code=metering_point,
|
476
|
-
obis_code=
|
473
|
+
obis_code=ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
477
474
|
start_date_time=start_date,
|
478
475
|
end_date_time=end_date,
|
479
476
|
)
|
@@ -9,8 +9,7 @@ Environment variables:
|
|
9
9
|
LENEDA_ENERGY_ID: Your Energy ID
|
10
10
|
|
11
11
|
Usage:
|
12
|
-
python basic_usage.py --api-key YOUR_API_KEY --energy-id YOUR_ENERGY_ID
|
13
|
-
python basic_usage.py --metering-point LU-METERING_POINT1
|
12
|
+
python basic_usage.py --api-key YOUR_API_KEY --energy-id YOUR_ENERGY_ID --metering-point LU-METERING_POINT1
|
14
13
|
"""
|
15
14
|
|
16
15
|
import argparse
|
@@ -19,7 +18,8 @@ import os
|
|
19
18
|
import sys
|
20
19
|
from datetime import datetime, timedelta
|
21
20
|
|
22
|
-
from leneda import
|
21
|
+
from leneda import LenedaClient
|
22
|
+
from leneda.obis_codes import ObisCode
|
23
23
|
|
24
24
|
# Set up logging
|
25
25
|
logging.basicConfig(
|
@@ -45,7 +45,6 @@ def parse_arguments():
|
|
45
45
|
# Other parameters
|
46
46
|
parser.add_argument(
|
47
47
|
"--metering-point",
|
48
|
-
default="LU-METERING_POINT1",
|
49
48
|
help="Metering point code (default: LU-METERING_POINT1)",
|
50
49
|
)
|
51
50
|
parser.add_argument(
|
@@ -107,7 +106,7 @@ def main():
|
|
107
106
|
print(f"\nExample 1: Getting hourly electricity consumption data for the last {days} days")
|
108
107
|
consumption_data = client.get_metering_data(
|
109
108
|
metering_point_code=metering_point,
|
110
|
-
obis_code=
|
109
|
+
obis_code=ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
111
110
|
start_date_time=start_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
112
111
|
end_date_time=end_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
113
112
|
)
|
@@ -136,7 +135,7 @@ def main():
|
|
136
135
|
print(f"\nExample 2: Getting monthly aggregated electricity consumption for {today.year}")
|
137
136
|
monthly_consumption = client.get_aggregated_metering_data(
|
138
137
|
metering_point_code=metering_point,
|
139
|
-
obis_code=
|
138
|
+
obis_code=ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
140
139
|
start_date=first_day.strftime("%Y-%m-%d"),
|
141
140
|
end_date=last_day.strftime("%Y-%m-%d"),
|
142
141
|
aggregation_level="Month",
|
@@ -17,7 +17,7 @@ from .models import (
|
|
17
17
|
)
|
18
18
|
|
19
19
|
# Import the OBIS code constants
|
20
|
-
from .obis_codes import
|
20
|
+
from .obis_codes import ObisCode
|
21
21
|
|
22
22
|
# Import the version
|
23
23
|
from .version import __version__
|
@@ -25,9 +25,7 @@ from .version import __version__
|
|
25
25
|
# Define what's available when using "from leneda import *"
|
26
26
|
__all__ = [
|
27
27
|
"LenedaClient",
|
28
|
-
"
|
29
|
-
"ElectricityProduction",
|
30
|
-
"GasConsumption",
|
28
|
+
"ObisCode",
|
31
29
|
"MeteringValue",
|
32
30
|
"MeteringData",
|
33
31
|
"AggregatedMeteringValue",
|
@@ -7,12 +7,17 @@ energy consumption and production data for electricity and gas.
|
|
7
7
|
|
8
8
|
import json
|
9
9
|
import logging
|
10
|
-
from datetime import datetime
|
11
|
-
from typing import Any, Dict, Union
|
10
|
+
from datetime import datetime, timedelta
|
11
|
+
from typing import Any, Dict, List, Optional, Union
|
12
12
|
|
13
13
|
import requests
|
14
14
|
|
15
|
-
from .
|
15
|
+
from .exceptions import ForbiddenException, InvalidMeteringPointException, UnauthorizedException
|
16
|
+
from .models import (
|
17
|
+
AggregatedMeteringData,
|
18
|
+
MeteringData,
|
19
|
+
)
|
20
|
+
from .obis_codes import ObisCode
|
16
21
|
|
17
22
|
# Set up logging
|
18
23
|
logger = logging.getLogger("leneda.client")
|
@@ -50,22 +55,28 @@ class LenedaClient:
|
|
50
55
|
|
51
56
|
def _make_request(
|
52
57
|
self,
|
58
|
+
method: str,
|
53
59
|
endpoint: str,
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
) -> Dict[str, Any]:
|
60
|
+
params: Optional[dict] = None,
|
61
|
+
json_data: Optional[dict] = None,
|
62
|
+
) -> dict:
|
58
63
|
"""
|
59
64
|
Make a request to the Leneda API.
|
60
65
|
|
61
66
|
Args:
|
62
|
-
|
63
|
-
|
64
|
-
params:
|
65
|
-
|
67
|
+
method: The HTTP method to use
|
68
|
+
endpoint: The API endpoint to call
|
69
|
+
params: Optional query parameters
|
70
|
+
json_data: Optional JSON data to send in the request body
|
66
71
|
|
67
72
|
Returns:
|
68
|
-
|
73
|
+
The JSON response from the API
|
74
|
+
|
75
|
+
Raises:
|
76
|
+
UnauthorizedException: If the API returns a 401 status code
|
77
|
+
ForbiddenException: If the API returns a 403 status code
|
78
|
+
requests.exceptions.RequestException: For other request errors
|
79
|
+
json.JSONDecodeError: If the response cannot be parsed as JSON
|
69
80
|
"""
|
70
81
|
url = f"{self.BASE_URL}/{endpoint}"
|
71
82
|
|
@@ -73,16 +84,24 @@ class LenedaClient:
|
|
73
84
|
logger.debug(f"Making {method} request to {url}")
|
74
85
|
if params:
|
75
86
|
logger.debug(f"Query parameters: {params}")
|
76
|
-
if
|
77
|
-
logger.debug(f"Request data: {
|
87
|
+
if json_data:
|
88
|
+
logger.debug(f"Request data: {json.dumps(json_data, indent=2)}")
|
78
89
|
|
79
90
|
try:
|
80
91
|
# Make the request
|
81
92
|
response = requests.request(
|
82
|
-
method=method, url=url, headers=self.headers, params=params, json=
|
93
|
+
method=method, url=url, headers=self.headers, params=params, json=json_data
|
83
94
|
)
|
84
95
|
|
85
96
|
# Check for HTTP errors
|
97
|
+
if response.status_code == 401:
|
98
|
+
raise UnauthorizedException(
|
99
|
+
"API authentication failed. Please check your API key and energy ID."
|
100
|
+
)
|
101
|
+
if response.status_code == 403:
|
102
|
+
raise ForbiddenException(
|
103
|
+
"Access forbidden. This may be due to Leneda's geoblocking or other access restrictions."
|
104
|
+
)
|
86
105
|
response.raise_for_status()
|
87
106
|
|
88
107
|
# Parse the response
|
@@ -117,7 +136,7 @@ class LenedaClient:
|
|
117
136
|
def get_metering_data(
|
118
137
|
self,
|
119
138
|
metering_point_code: str,
|
120
|
-
obis_code:
|
139
|
+
obis_code: ObisCode,
|
121
140
|
start_date_time: Union[str, datetime],
|
122
141
|
end_date_time: Union[str, datetime],
|
123
142
|
) -> MeteringData:
|
@@ -126,7 +145,7 @@ class LenedaClient:
|
|
126
145
|
|
127
146
|
Args:
|
128
147
|
metering_point_code: The metering point code
|
129
|
-
obis_code: The OBIS code
|
148
|
+
obis_code: The OBIS code (from ElectricityConsumption, ElectricityProduction, or GasConsumption)
|
130
149
|
start_date_time: Start date and time (ISO format string or datetime object)
|
131
150
|
end_date_time: End date and time (ISO format string or datetime object)
|
132
151
|
|
@@ -142,23 +161,21 @@ class LenedaClient:
|
|
142
161
|
# Set up the endpoint and parameters
|
143
162
|
endpoint = f"metering-points/{metering_point_code}/time-series"
|
144
163
|
params = {
|
145
|
-
"obisCode": obis_code,
|
164
|
+
"obisCode": obis_code.value, # Use enum value for API request
|
146
165
|
"startDateTime": start_date_time,
|
147
166
|
"endDateTime": end_date_time,
|
148
167
|
}
|
149
168
|
|
150
169
|
# Make the request
|
151
|
-
response_data = self._make_request(endpoint, params=params)
|
170
|
+
response_data = self._make_request(method="GET", endpoint=endpoint, params=params)
|
152
171
|
|
153
172
|
# Parse the response into a MeteringData object
|
154
|
-
return MeteringData.from_dict(
|
155
|
-
response_data, metering_point_code=metering_point_code, obis_code=obis_code
|
156
|
-
)
|
173
|
+
return MeteringData.from_dict(response_data)
|
157
174
|
|
158
175
|
def get_aggregated_metering_data(
|
159
176
|
self,
|
160
177
|
metering_point_code: str,
|
161
|
-
obis_code:
|
178
|
+
obis_code: ObisCode,
|
162
179
|
start_date: Union[str, datetime],
|
163
180
|
end_date: Union[str, datetime],
|
164
181
|
aggregation_level: str = "Day",
|
@@ -169,11 +186,11 @@ class LenedaClient:
|
|
169
186
|
|
170
187
|
Args:
|
171
188
|
metering_point_code: The metering point code
|
172
|
-
obis_code: The OBIS code
|
189
|
+
obis_code: The OBIS code (from ElectricityConsumption, ElectricityProduction, or GasConsumption)
|
173
190
|
start_date: Start date (ISO format string or datetime object)
|
174
191
|
end_date: End date (ISO format string or datetime object)
|
175
|
-
aggregation_level: Aggregation level (Day, Week, Month,
|
176
|
-
transformation_mode: Transformation mode (Accumulation
|
192
|
+
aggregation_level: Aggregation level (Hour, Day, Week, Month, Infinite)
|
193
|
+
transformation_mode: Transformation mode (Accumulation)
|
177
194
|
|
178
195
|
Returns:
|
179
196
|
AggregatedMeteringData object containing the aggregated time series data
|
@@ -187,7 +204,7 @@ class LenedaClient:
|
|
187
204
|
# Set up the endpoint and parameters
|
188
205
|
endpoint = f"metering-points/{metering_point_code}/time-series/aggregated"
|
189
206
|
params = {
|
190
|
-
"obisCode": obis_code,
|
207
|
+
"obisCode": obis_code.value, # Use enum value for API request
|
191
208
|
"startDate": start_date,
|
192
209
|
"endDate": end_date,
|
193
210
|
"aggregationLevel": aggregation_level,
|
@@ -195,32 +212,81 @@ class LenedaClient:
|
|
195
212
|
}
|
196
213
|
|
197
214
|
# Make the request
|
198
|
-
response_data = self._make_request(endpoint, params=params)
|
215
|
+
response_data = self._make_request(method="GET", endpoint=endpoint, params=params)
|
199
216
|
|
200
217
|
# Parse the response into an AggregatedMeteringData object
|
201
|
-
return AggregatedMeteringData.from_dict(
|
202
|
-
response_data,
|
203
|
-
metering_point_code=metering_point_code,
|
204
|
-
obis_code=obis_code,
|
205
|
-
aggregation_level=aggregation_level,
|
206
|
-
transformation_mode=transformation_mode,
|
207
|
-
)
|
218
|
+
return AggregatedMeteringData.from_dict(response_data)
|
208
219
|
|
209
|
-
def request_metering_data_access(
|
220
|
+
def request_metering_data_access(
|
221
|
+
self,
|
222
|
+
from_energy_id: str,
|
223
|
+
from_name: str,
|
224
|
+
metering_point_codes: List[str],
|
225
|
+
obis_codes: List[ObisCode],
|
226
|
+
) -> Dict[str, Any]:
|
210
227
|
"""
|
211
228
|
Request access to metering data for a specific metering point.
|
212
229
|
|
213
230
|
Args:
|
214
|
-
|
231
|
+
from_energy_id: The energy ID of the requester
|
232
|
+
from_name: The name of the requester
|
233
|
+
metering_point_codes: The metering point codes to access
|
234
|
+
obis_point_codes: The OBIS point codes to access (from ElectricityConsumption, ElectricityProduction, or GasConsumption)
|
215
235
|
|
216
236
|
Returns:
|
217
237
|
Response data from the API
|
218
238
|
"""
|
219
239
|
# Set up the endpoint and data
|
220
240
|
endpoint = "metering-data-access-request"
|
221
|
-
data = {
|
241
|
+
data = {
|
242
|
+
"from": from_energy_id,
|
243
|
+
"fromName": from_name,
|
244
|
+
"meteringPointCodes": metering_point_codes,
|
245
|
+
"obisCodes": [code.value for code in obis_codes], # Use enum values for API request
|
246
|
+
}
|
222
247
|
|
223
248
|
# Make the request
|
224
|
-
response_data = self._make_request(
|
249
|
+
response_data = self._make_request(method="POST", endpoint=endpoint, json_data=data)
|
225
250
|
|
226
251
|
return response_data
|
252
|
+
|
253
|
+
def test_metering_point(self, metering_point_code: str) -> bool:
|
254
|
+
"""
|
255
|
+
Test if a metering point code is valid and accessible.
|
256
|
+
|
257
|
+
This method checks if a metering point code is valid by making a request
|
258
|
+
for aggregated metering data. If the unit property in the response is null,
|
259
|
+
it indicates that the metering point is invalid or not accessible.
|
260
|
+
|
261
|
+
Args:
|
262
|
+
metering_point_code: The metering point code to test
|
263
|
+
|
264
|
+
Returns:
|
265
|
+
bool: True if the metering point is valid and accessible, False otherwise
|
266
|
+
|
267
|
+
Raises:
|
268
|
+
UnauthorizedException: If the API returns a 401 status code
|
269
|
+
ForbiddenException: If the API returns a 403 status code
|
270
|
+
requests.exceptions.RequestException: For other request errors
|
271
|
+
"""
|
272
|
+
# Use arbitrary time window
|
273
|
+
end_date = datetime.now()
|
274
|
+
start_date = end_date - timedelta(weeks=4)
|
275
|
+
|
276
|
+
# Try to get aggregated data for electricity consumption
|
277
|
+
result = self.get_aggregated_metering_data(
|
278
|
+
metering_point_code=metering_point_code,
|
279
|
+
obis_code=ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
280
|
+
start_date=start_date,
|
281
|
+
end_date=end_date,
|
282
|
+
aggregation_level="Month",
|
283
|
+
transformation_mode="Accumulation",
|
284
|
+
)
|
285
|
+
|
286
|
+
# If we get here and the unit is None, the metering point is invalid
|
287
|
+
if result.unit is None:
|
288
|
+
raise InvalidMeteringPointException(
|
289
|
+
f"Metering point {metering_point_code} is invalid or not accessible"
|
290
|
+
)
|
291
|
+
|
292
|
+
return True
|
@@ -0,0 +1,27 @@
|
|
1
|
+
"""
|
2
|
+
Custom exceptions for the Leneda API client.
|
3
|
+
"""
|
4
|
+
|
5
|
+
|
6
|
+
class LenedaException(Exception):
|
7
|
+
"""Base exception for all Leneda API client exceptions."""
|
8
|
+
|
9
|
+
pass
|
10
|
+
|
11
|
+
|
12
|
+
class UnauthorizedException(LenedaException):
|
13
|
+
"""Raised when API authentication fails (401 Unauthorized)."""
|
14
|
+
|
15
|
+
pass
|
16
|
+
|
17
|
+
|
18
|
+
class ForbiddenException(LenedaException):
|
19
|
+
"""Raised when access is forbidden (403 Forbidden), typically due to geoblocking or other access restrictions."""
|
20
|
+
|
21
|
+
pass
|
22
|
+
|
23
|
+
|
24
|
+
class InvalidMeteringPointException(LenedaException):
|
25
|
+
"""Raised when a metering point code is invalid or not accessible."""
|
26
|
+
|
27
|
+
pass
|
@@ -12,6 +12,8 @@ from typing import Any, Dict, List
|
|
12
12
|
|
13
13
|
from dateutil import parser
|
14
14
|
|
15
|
+
from .obis_codes import ObisCode
|
16
|
+
|
15
17
|
# Set up logging
|
16
18
|
logger = logging.getLogger("leneda.models")
|
17
19
|
|
@@ -64,23 +66,21 @@ class MeteringData:
|
|
64
66
|
"""Metering data for a specific metering point and OBIS code."""
|
65
67
|
|
66
68
|
metering_point_code: str
|
67
|
-
obis_code:
|
69
|
+
obis_code: ObisCode
|
68
70
|
interval_length: str
|
69
71
|
unit: str
|
70
72
|
items: List[MeteringValue] = field(default_factory=list)
|
71
73
|
|
72
74
|
@classmethod
|
73
|
-
def from_dict(
|
74
|
-
cls, data: Dict[str, Any], metering_point_code: str = "", obis_code: str = ""
|
75
|
-
) -> "MeteringData":
|
75
|
+
def from_dict(cls, data: Dict[str, Any]) -> "MeteringData":
|
76
76
|
"""Create a MeteringData from a dictionary."""
|
77
77
|
try:
|
78
78
|
# Log the raw data for debugging
|
79
79
|
logger.debug(f"Creating MeteringData from: {data}")
|
80
80
|
|
81
|
-
# Use values from the response
|
82
|
-
metering_point_code_value = data
|
83
|
-
obis_code_value = data
|
81
|
+
# Use values from the response
|
82
|
+
metering_point_code_value = data["meteringPointCode"]
|
83
|
+
obis_code_value = ObisCode(data["obisCode"])
|
84
84
|
|
85
85
|
# Extract items safely
|
86
86
|
items_data = data.get("items", [])
|
@@ -101,6 +101,10 @@ class MeteringData:
|
|
101
101
|
unit=data.get("unit", ""),
|
102
102
|
items=items,
|
103
103
|
)
|
104
|
+
except KeyError as e:
|
105
|
+
logger.error(f"Missing key in API response: {e}")
|
106
|
+
logger.debug(f"API response data: {data}")
|
107
|
+
raise
|
104
108
|
except Exception as e:
|
105
109
|
logger.error(f"Error creating MeteringData: {e}")
|
106
110
|
logger.debug(f"API response data: {data}")
|
@@ -184,10 +188,6 @@ class AggregatedMeteringData:
|
|
184
188
|
def from_dict(
|
185
189
|
cls,
|
186
190
|
data: Dict[str, Any],
|
187
|
-
metering_point_code: str = "",
|
188
|
-
obis_code: str = "",
|
189
|
-
aggregation_level: str = "",
|
190
|
-
transformation_mode: str = "",
|
191
191
|
) -> "AggregatedMeteringData":
|
192
192
|
"""Create an AggregatedMeteringData from a dictionary."""
|
193
193
|
try:
|
@@ -206,7 +206,11 @@ class AggregatedMeteringData:
|
|
206
206
|
logger.warning(f"Skipping invalid aggregated item: {e}")
|
207
207
|
logger.debug(f"Invalid item data: {item_data}")
|
208
208
|
|
209
|
-
return cls(unit=data
|
209
|
+
return cls(unit=data["unit"], aggregated_time_series=time_series)
|
210
|
+
except KeyError as e:
|
211
|
+
logger.error(f"Missing key in API response: {e}")
|
212
|
+
logger.debug(f"API response data: {data}")
|
213
|
+
raise
|
210
214
|
except Exception as e:
|
211
215
|
logger.error(f"Error creating AggregatedMeteringData: {e}")
|
212
216
|
logger.debug(f"API response data: {data}")
|
@@ -20,139 +20,136 @@ class ObisCodeInfo:
|
|
20
20
|
description: str
|
21
21
|
|
22
22
|
|
23
|
-
class
|
24
|
-
"""OBIS codes for
|
23
|
+
class ObisCode(str, Enum):
|
24
|
+
"""OBIS codes for all energy types."""
|
25
25
|
|
26
|
-
|
27
|
-
|
26
|
+
# Electricity Consumption
|
27
|
+
ELEC_CONSUMPTION_ACTIVE = "1-1:1.29.0" # Measured active consumption (kW)
|
28
|
+
ELEC_CONSUMPTION_REACTIVE = "1-1:3.29.0" # Measured reactive consumption (kVAR)
|
28
29
|
|
29
30
|
# Consumption covered by production sharing groups
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
31
|
+
ELEC_CONSUMPTION_COVERED_LAYER1 = "1-65:1.29.1" # Layer 1 sharing Group (AIR)
|
32
|
+
ELEC_CONSUMPTION_COVERED_LAYER2 = "1-65:1.29.3" # Layer 2 sharing Group (ACR/ACF/AC1)
|
33
|
+
ELEC_CONSUMPTION_COVERED_LAYER3 = "1-65:1.29.2" # Layer 3 sharing Group (CEL)
|
34
|
+
ELEC_CONSUMPTION_COVERED_LAYER4 = "1-65:1.29.4" # Layer 4 sharing Group (APS/CER/CEN)
|
35
|
+
ELEC_CONSUMPTION_REMAINING = (
|
36
|
+
"1-65:1.29.9" # Remaining consumption after sharing invoiced by supplier
|
37
|
+
)
|
36
38
|
|
37
|
-
|
38
|
-
""
|
39
|
-
|
40
|
-
ACTIVE = "1-1:2.29.0" # Measured active production (kW)
|
41
|
-
REACTIVE = "1-1:4.29.0" # Measured reactive production (kVAR)
|
39
|
+
# Electricity Production
|
40
|
+
ELEC_PRODUCTION_ACTIVE = "1-1:2.29.0" # Measured active production (kW)
|
41
|
+
ELEC_PRODUCTION_REACTIVE = "1-1:4.29.0" # Measured reactive production (kVAR)
|
42
42
|
|
43
43
|
# Production shared within sharing groups
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
44
|
+
ELEC_PRODUCTION_SHARED_LAYER1 = "1-65:2.29.1" # Layer 1 sharing Group (AIR)
|
45
|
+
ELEC_PRODUCTION_SHARED_LAYER2 = "1-65:2.29.3" # Layer 2 sharing Group (ACR/ACF/AC1)
|
46
|
+
ELEC_PRODUCTION_SHARED_LAYER3 = "1-65:2.29.2" # Layer 3 sharing Group (CEL)
|
47
|
+
ELEC_PRODUCTION_SHARED_LAYER4 = "1-65:2.29.4" # Layer 4 sharing Group (APS/CER/CEN)
|
48
|
+
ELEC_PRODUCTION_REMAINING = "1-65:2.29.9" # Remaining production after sharing sold to market
|
49
49
|
|
50
|
-
|
51
|
-
|
52
|
-
""
|
53
|
-
|
54
|
-
VOLUME = "7-1:99.23.15" # Measured consumed volume (m³)
|
55
|
-
STANDARD_VOLUME = "7-1:99.23.17" # Measured consumed standard volume (Nm³)
|
56
|
-
ENERGY = "7-20:99.33.17" # Measured consumed energy (kWh)
|
50
|
+
# Gas Consumption
|
51
|
+
GAS_CONSUMPTION_VOLUME = "7-1:99.23.15" # Measured consumed volume (m³)
|
52
|
+
GAS_CONSUMPTION_STANDARD_VOLUME = "7-1:99.23.17" # Measured consumed standard volume (Nm³)
|
53
|
+
GAS_CONSUMPTION_ENERGY = "7-20:99.33.17" # Measured consumed energy (kWh)
|
57
54
|
|
58
55
|
|
59
56
|
# Complete mapping of all OBIS codes to their details
|
60
57
|
OBIS_CODES: Dict[str, ObisCodeInfo] = {
|
61
58
|
# Electricity Consumption
|
62
|
-
|
63
|
-
|
59
|
+
ObisCode.ELEC_CONSUMPTION_ACTIVE: ObisCodeInfo(
|
60
|
+
ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
64
61
|
"kW",
|
65
62
|
"Consumption",
|
66
63
|
"Measured active consumption",
|
67
64
|
),
|
68
|
-
|
69
|
-
|
65
|
+
ObisCode.ELEC_CONSUMPTION_REACTIVE: ObisCodeInfo(
|
66
|
+
ObisCode.ELEC_CONSUMPTION_REACTIVE,
|
70
67
|
"kVAR",
|
71
68
|
"Consumption",
|
72
69
|
"Measured reactive consumption",
|
73
70
|
),
|
74
|
-
|
75
|
-
|
71
|
+
ObisCode.ELEC_CONSUMPTION_COVERED_LAYER1: ObisCodeInfo(
|
72
|
+
ObisCode.ELEC_CONSUMPTION_COVERED_LAYER1,
|
76
73
|
"kW",
|
77
74
|
"Consumption",
|
78
75
|
"Consumption covered by production of layer 1 sharing Group (AIR)",
|
79
76
|
),
|
80
|
-
|
81
|
-
|
77
|
+
ObisCode.ELEC_CONSUMPTION_COVERED_LAYER2: ObisCodeInfo(
|
78
|
+
ObisCode.ELEC_CONSUMPTION_COVERED_LAYER2,
|
82
79
|
"kW",
|
83
80
|
"Consumption",
|
84
81
|
"Consumption covered by production of layer 2 sharing Group (ACR/ACF/AC1)",
|
85
82
|
),
|
86
|
-
|
87
|
-
|
83
|
+
ObisCode.ELEC_CONSUMPTION_COVERED_LAYER3: ObisCodeInfo(
|
84
|
+
ObisCode.ELEC_CONSUMPTION_COVERED_LAYER3,
|
88
85
|
"kW",
|
89
86
|
"Consumption",
|
90
87
|
"Consumption covered by production of layer 3 sharing Group (CEL)",
|
91
88
|
),
|
92
|
-
|
93
|
-
|
89
|
+
ObisCode.ELEC_CONSUMPTION_COVERED_LAYER4: ObisCodeInfo(
|
90
|
+
ObisCode.ELEC_CONSUMPTION_COVERED_LAYER4,
|
94
91
|
"kW",
|
95
92
|
"Consumption",
|
96
93
|
"Consumption covered by production of layer 4 sharing Group (APS/CER/CEN)",
|
97
94
|
),
|
98
|
-
|
99
|
-
|
95
|
+
ObisCode.ELEC_CONSUMPTION_REMAINING: ObisCodeInfo(
|
96
|
+
ObisCode.ELEC_CONSUMPTION_REMAINING,
|
100
97
|
"kW",
|
101
98
|
"Consumption",
|
102
99
|
"Remaining consumption after sharing invoiced by supplier",
|
103
100
|
),
|
104
101
|
# Electricity Production
|
105
|
-
|
106
|
-
|
102
|
+
ObisCode.ELEC_PRODUCTION_ACTIVE: ObisCodeInfo(
|
103
|
+
ObisCode.ELEC_PRODUCTION_ACTIVE, "kW", "Production", "Measured active production"
|
107
104
|
),
|
108
|
-
|
109
|
-
|
105
|
+
ObisCode.ELEC_PRODUCTION_REACTIVE: ObisCodeInfo(
|
106
|
+
ObisCode.ELEC_PRODUCTION_REACTIVE,
|
110
107
|
"kVAR",
|
111
108
|
"Consumption",
|
112
109
|
"Measured reactive production",
|
113
110
|
),
|
114
|
-
|
115
|
-
|
111
|
+
ObisCode.ELEC_PRODUCTION_SHARED_LAYER1: ObisCodeInfo(
|
112
|
+
ObisCode.ELEC_PRODUCTION_SHARED_LAYER1,
|
116
113
|
"kW",
|
117
114
|
"Production",
|
118
115
|
"Production shared within layer 1 sharing Group (AIR)",
|
119
116
|
),
|
120
|
-
|
121
|
-
|
117
|
+
ObisCode.ELEC_PRODUCTION_SHARED_LAYER2: ObisCodeInfo(
|
118
|
+
ObisCode.ELEC_PRODUCTION_SHARED_LAYER2,
|
122
119
|
"kW",
|
123
120
|
"Production",
|
124
121
|
"Production shared within layer 2 sharing Group (ACR/ACF/AC1)",
|
125
122
|
),
|
126
|
-
|
127
|
-
|
123
|
+
ObisCode.ELEC_PRODUCTION_SHARED_LAYER3: ObisCodeInfo(
|
124
|
+
ObisCode.ELEC_PRODUCTION_SHARED_LAYER3,
|
128
125
|
"kW",
|
129
126
|
"Production",
|
130
127
|
"Production shared within layer 3 sharing Group (CEL)",
|
131
128
|
),
|
132
|
-
|
133
|
-
|
129
|
+
ObisCode.ELEC_PRODUCTION_SHARED_LAYER4: ObisCodeInfo(
|
130
|
+
ObisCode.ELEC_PRODUCTION_SHARED_LAYER4,
|
134
131
|
"kW",
|
135
132
|
"Production",
|
136
133
|
"Production shared within layer 4 sharing Group (APS/CER/CEN)",
|
137
134
|
),
|
138
|
-
|
139
|
-
|
135
|
+
ObisCode.ELEC_PRODUCTION_REMAINING: ObisCodeInfo(
|
136
|
+
ObisCode.ELEC_PRODUCTION_REMAINING,
|
140
137
|
"kW",
|
141
138
|
"Production",
|
142
139
|
"Remaining production after sharing sold to market",
|
143
140
|
),
|
144
141
|
# Gas Consumption
|
145
|
-
|
146
|
-
|
142
|
+
ObisCode.GAS_CONSUMPTION_VOLUME: ObisCodeInfo(
|
143
|
+
ObisCode.GAS_CONSUMPTION_VOLUME, "m³", "Consumption", "Measured consumed volume"
|
147
144
|
),
|
148
|
-
|
149
|
-
|
145
|
+
ObisCode.GAS_CONSUMPTION_STANDARD_VOLUME: ObisCodeInfo(
|
146
|
+
ObisCode.GAS_CONSUMPTION_STANDARD_VOLUME,
|
150
147
|
"Nm³",
|
151
148
|
"Consumption",
|
152
149
|
"Measured consumed standard volume",
|
153
150
|
),
|
154
|
-
|
155
|
-
|
151
|
+
ObisCode.GAS_CONSUMPTION_ENERGY: ObisCodeInfo(
|
152
|
+
ObisCode.GAS_CONSUMPTION_ENERGY, "kWh", "Consumption", "Measured consumed energy"
|
156
153
|
),
|
157
154
|
}
|
158
155
|
|
@@ -171,7 +168,7 @@ def get_obis_info(obis_code: str) -> ObisCodeInfo:
|
|
171
168
|
KeyError: If the OBIS code is not found
|
172
169
|
|
173
170
|
Example:
|
174
|
-
>>> info = get_obis_info(
|
171
|
+
>>> info = get_obis_info(ObisCode.ELEC_CONSUMPTION_ACTIVE)
|
175
172
|
>>> print(info.unit)
|
176
173
|
'kW'
|
177
174
|
"""
|
@@ -192,7 +189,7 @@ def get_unit(obis_code: str) -> str:
|
|
192
189
|
KeyError: If the OBIS code is not found
|
193
190
|
|
194
191
|
Example:
|
195
|
-
>>> unit = get_unit(
|
192
|
+
>>> unit = get_unit(ObisCode.ELEC_CONSUMPTION_ACTIVE)
|
196
193
|
>>> print(unit)
|
197
194
|
'kW'
|
198
195
|
"""
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: leneda-client
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: Python client for the Leneda energy data platform
|
5
5
|
Home-page: https://github.com/fedus/leneda-client
|
6
6
|
Author: fedus
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
21
|
Classifier: Topic :: Utilities
|
22
22
|
Requires-Python: >=3.8
|
23
23
|
Description-Content-Type: text/markdown
|
24
|
+
License-File: LICENSE
|
24
25
|
Requires-Dist: requests>=2.25.0
|
25
26
|
Requires-Dist: python-dateutil>=2.8.2
|
26
27
|
Dynamic: author
|
@@ -30,6 +31,7 @@ Dynamic: description
|
|
30
31
|
Dynamic: description-content-type
|
31
32
|
Dynamic: home-page
|
32
33
|
Dynamic: keywords
|
34
|
+
Dynamic: license-file
|
33
35
|
Dynamic: project-url
|
34
36
|
Dynamic: requires-dist
|
35
37
|
Dynamic: requires-python
|
@@ -37,9 +39,10 @@ Dynamic: summary
|
|
37
39
|
|
38
40
|
# Leneda API Client
|
39
41
|
|
40
|
-
[![PyPI version]](https://pypi.org/project/leneda-client/)
|
41
|
-
[![Python
|
42
|
-
[![License]](https://github.com/fedus/leneda-client/blob/main/LICENSE)
|
42
|
+
[](https://pypi.org/project/leneda-client/)
|
43
|
+
[](https://pypi.org/project/leneda-client/)
|
44
|
+
[](https://github.com/fedus/leneda-client/blob/main/LICENSE)
|
45
|
+
|
43
46
|
|
44
47
|
A Python client for interacting with the Leneda energy data platform API.
|
45
48
|
|
@@ -56,3 +59,33 @@ This client provides a simple interface to the Leneda API, which allows users to
|
|
56
59
|
|
57
60
|
```bash
|
58
61
|
pip install leneda-client
|
62
|
+
```
|
63
|
+
|
64
|
+
## Trying it out
|
65
|
+
|
66
|
+
```bash
|
67
|
+
$ export LENEDA_ENERGY_ID='LUXE-xx-yy-1234'
|
68
|
+
$ export LENEDA_API_KEY='YOUR-API-KEY'
|
69
|
+
$ python examples/basic_usage.py --metering-point LU0000012345678901234000000000000
|
70
|
+
Example 1: Getting hourly electricity consumption data for the last 7 days
|
71
|
+
Retrieved 514 consumption measurements
|
72
|
+
Unit: kW
|
73
|
+
Interval length: PT15M
|
74
|
+
Metering point: LU0000012345678901234000000000000
|
75
|
+
OBIS code: ObisCode.ELEC_CONSUMPTION_ACTIVE
|
76
|
+
|
77
|
+
First 3 measurements:
|
78
|
+
Time: 2025-04-18T13:30:00+00:00, Value: 0.048 kW, Type: Actual, Version: 2, Calculated: False
|
79
|
+
Time: 2025-04-18T13:45:00+00:00, Value: 0.08 kW, Type: Actual, Version: 2, Calculated: False
|
80
|
+
Time: 2025-04-18T14:00:00+00:00, Value: 0.08 kW, Type: Actual, Version: 2, Calculated: False
|
81
|
+
|
82
|
+
Example 2: Getting monthly aggregated electricity consumption for 2025
|
83
|
+
Retrieved 4 monthly aggregations
|
84
|
+
Unit: kWh
|
85
|
+
|
86
|
+
Monthly consumption:
|
87
|
+
Period: 2024-12 to 2025-01, Value: 30.858 kWh, Calculated: False
|
88
|
+
Period: 2025-01 to 2025-02, Value: 148.985 kWh, Calculated: False
|
89
|
+
Period: 2025-02 to 2025-03, Value: 44.619 kWh, Calculated: False
|
90
|
+
Period: 2025-03 to 2025-04, Value: 29.662 kWh, Calculated: False
|
91
|
+
```
|
@@ -1,3 +1,4 @@
|
|
1
|
+
LICENSE
|
1
2
|
MANIFEST.in
|
2
3
|
README.md
|
3
4
|
pyproject.toml
|
@@ -7,6 +8,7 @@ examples/advanced_usage.py
|
|
7
8
|
examples/basic_usage.py
|
8
9
|
src/leneda/__init__.py
|
9
10
|
src/leneda/client.py
|
11
|
+
src/leneda/exceptions.py
|
10
12
|
src/leneda/models.py
|
11
13
|
src/leneda/obis_codes.py
|
12
14
|
src/leneda/version.py
|
@@ -14,12 +14,18 @@ import requests
|
|
14
14
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
15
15
|
|
16
16
|
from src.leneda import LenedaClient
|
17
|
+
from src.leneda.exceptions import (
|
18
|
+
ForbiddenException,
|
19
|
+
InvalidMeteringPointException,
|
20
|
+
UnauthorizedException,
|
21
|
+
)
|
17
22
|
from src.leneda.models import (
|
18
23
|
AggregatedMeteringData,
|
19
24
|
AggregatedMeteringValue,
|
20
25
|
MeteringData,
|
21
26
|
MeteringValue,
|
22
27
|
)
|
28
|
+
from src.leneda.obis_codes import ObisCode
|
23
29
|
|
24
30
|
|
25
31
|
class TestLenedaClient(unittest.TestCase):
|
@@ -34,7 +40,7 @@ class TestLenedaClient(unittest.TestCase):
|
|
34
40
|
# Sample response data
|
35
41
|
self.sample_metering_data = {
|
36
42
|
"meteringPointCode": "LU-METERING_POINT1",
|
37
|
-
"obisCode":
|
43
|
+
"obisCode": ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
38
44
|
"intervalLength": "PT15M",
|
39
45
|
"unit": "kWh",
|
40
46
|
"items": [
|
@@ -86,7 +92,7 @@ class TestLenedaClient(unittest.TestCase):
|
|
86
92
|
# Call the method
|
87
93
|
result = self.client.get_metering_data(
|
88
94
|
"LU-METERING_POINT1",
|
89
|
-
|
95
|
+
ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
90
96
|
"2023-01-01T00:00:00Z",
|
91
97
|
"2023-01-02T00:00:00Z",
|
92
98
|
)
|
@@ -94,7 +100,7 @@ class TestLenedaClient(unittest.TestCase):
|
|
94
100
|
# Check the result
|
95
101
|
self.assertIsInstance(result, MeteringData)
|
96
102
|
self.assertEqual(result.metering_point_code, "LU-METERING_POINT1")
|
97
|
-
self.assertEqual(result.obis_code,
|
103
|
+
self.assertEqual(result.obis_code, ObisCode.ELEC_CONSUMPTION_ACTIVE)
|
98
104
|
self.assertEqual(result.unit, "kWh")
|
99
105
|
self.assertEqual(len(result.items), 2)
|
100
106
|
|
@@ -116,7 +122,7 @@ class TestLenedaClient(unittest.TestCase):
|
|
116
122
|
"Content-Type": "application/json",
|
117
123
|
},
|
118
124
|
params={
|
119
|
-
"obisCode":
|
125
|
+
"obisCode": ObisCode.ELEC_CONSUMPTION_ACTIVE.value,
|
120
126
|
"startDateTime": "2023-01-01T00:00:00Z",
|
121
127
|
"endDateTime": "2023-01-02T00:00:00Z",
|
122
128
|
},
|
@@ -136,7 +142,7 @@ class TestLenedaClient(unittest.TestCase):
|
|
136
142
|
# Call the method
|
137
143
|
result = self.client.get_aggregated_metering_data(
|
138
144
|
"LU-METERING_POINT1",
|
139
|
-
|
145
|
+
ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
140
146
|
"2023-01-01",
|
141
147
|
"2023-01-31",
|
142
148
|
"Day",
|
@@ -171,7 +177,7 @@ class TestLenedaClient(unittest.TestCase):
|
|
171
177
|
"Content-Type": "application/json",
|
172
178
|
},
|
173
179
|
params={
|
174
|
-
"obisCode":
|
180
|
+
"obisCode": ObisCode.ELEC_CONSUMPTION_ACTIVE.value,
|
175
181
|
"startDate": "2023-01-01",
|
176
182
|
"endDate": "2023-01-31",
|
177
183
|
"aggregationLevel": "Day",
|
@@ -186,12 +192,20 @@ class TestLenedaClient(unittest.TestCase):
|
|
186
192
|
# Set up the mock response
|
187
193
|
mock_response = MagicMock()
|
188
194
|
mock_response.status_code = 200
|
189
|
-
mock_response.json.return_value =
|
190
|
-
mock_response.content = "{}"
|
195
|
+
mock_response.json.return_value = {"requestId": "test-request-id", "status": "PENDING"}
|
191
196
|
mock_request.return_value = mock_response
|
192
197
|
|
193
198
|
# Call the method
|
194
|
-
self.client.request_metering_data_access(
|
199
|
+
result = self.client.request_metering_data_access(
|
200
|
+
from_energy_id="test_energy_id",
|
201
|
+
from_name="Test User",
|
202
|
+
metering_point_codes=["LU-METERING_POINT1"],
|
203
|
+
obis_codes=[ObisCode.ELEC_CONSUMPTION_ACTIVE],
|
204
|
+
)
|
205
|
+
|
206
|
+
# Check the result
|
207
|
+
self.assertEqual(result["requestId"], "test-request-id")
|
208
|
+
self.assertEqual(result["status"], "PENDING")
|
195
209
|
|
196
210
|
# Check that the request was made correctly
|
197
211
|
mock_request.assert_called_once_with(
|
@@ -203,12 +217,59 @@ class TestLenedaClient(unittest.TestCase):
|
|
203
217
|
"Content-Type": "application/json",
|
204
218
|
},
|
205
219
|
params=None,
|
206
|
-
json={
|
220
|
+
json={
|
221
|
+
"from": "test_energy_id",
|
222
|
+
"fromName": "Test User",
|
223
|
+
"meteringPointCodes": ["LU-METERING_POINT1"],
|
224
|
+
"obisCodes": [ObisCode.ELEC_CONSUMPTION_ACTIVE.value],
|
225
|
+
},
|
207
226
|
)
|
208
227
|
|
228
|
+
@patch("requests.request")
|
229
|
+
def test_unauthorized_error(self, mock_request):
|
230
|
+
"""Test handling of 401 Unauthorized errors."""
|
231
|
+
# Set up the mock response with 401 status
|
232
|
+
mock_response = MagicMock()
|
233
|
+
mock_response.status_code = 401
|
234
|
+
mock_response.content = b"Unauthorized"
|
235
|
+
mock_request.return_value = mock_response
|
236
|
+
|
237
|
+
# Call the method and check that it raises UnauthorizedException
|
238
|
+
with self.assertRaises(UnauthorizedException) as context:
|
239
|
+
self.client.get_metering_data(
|
240
|
+
"LU-METERING_POINT1",
|
241
|
+
ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
242
|
+
"2023-01-01T00:00:00Z",
|
243
|
+
"2023-01-02T00:00:00Z",
|
244
|
+
)
|
245
|
+
|
246
|
+
# Check the error message
|
247
|
+
self.assertIn("API authentication failed", str(context.exception))
|
248
|
+
|
249
|
+
@patch("requests.request")
|
250
|
+
def test_forbidden_error(self, mock_request):
|
251
|
+
"""Test handling of 403 Forbidden errors."""
|
252
|
+
# Set up the mock response with 403 status
|
253
|
+
mock_response = MagicMock()
|
254
|
+
mock_response.status_code = 403
|
255
|
+
mock_response.content = b"Forbidden"
|
256
|
+
mock_request.return_value = mock_response
|
257
|
+
|
258
|
+
# Call the method and check that it raises ForbiddenException
|
259
|
+
with self.assertRaises(ForbiddenException) as context:
|
260
|
+
self.client.get_metering_data(
|
261
|
+
"LU-METERING_POINT1",
|
262
|
+
ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
263
|
+
"2023-01-01T00:00:00Z",
|
264
|
+
"2023-01-02T00:00:00Z",
|
265
|
+
)
|
266
|
+
|
267
|
+
# Check the error message
|
268
|
+
self.assertIn("geoblocking", str(context.exception))
|
269
|
+
|
209
270
|
@patch("requests.request")
|
210
271
|
def test_error_handling(self, mock_request):
|
211
|
-
"""Test error handling."""
|
272
|
+
"""Test error handling for other HTTP errors."""
|
212
273
|
# Set up the mock response to raise an exception
|
213
274
|
mock_request.side_effect = requests.exceptions.HTTPError("404 Client Error")
|
214
275
|
|
@@ -216,11 +277,50 @@ class TestLenedaClient(unittest.TestCase):
|
|
216
277
|
with self.assertRaises(requests.exceptions.HTTPError):
|
217
278
|
self.client.get_metering_data(
|
218
279
|
"LU-METERING_POINT1",
|
219
|
-
|
280
|
+
ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
220
281
|
"2023-01-01T00:00:00Z",
|
221
282
|
"2023-01-02T00:00:00Z",
|
222
283
|
)
|
223
284
|
|
285
|
+
@patch("requests.request")
|
286
|
+
def test_test_metering_point_valid(self, mock_request):
|
287
|
+
"""Test test_metering_point with a valid metering point."""
|
288
|
+
# Set up the mock response with valid data
|
289
|
+
mock_response = MagicMock()
|
290
|
+
mock_response.status_code = 200
|
291
|
+
mock_response.json.return_value = {"unit": "kWh", "aggregatedTimeSeries": []}
|
292
|
+
mock_response.content = json.dumps(mock_response.json.return_value).encode()
|
293
|
+
mock_request.return_value = mock_response
|
294
|
+
|
295
|
+
# Call the method
|
296
|
+
result = self.client.test_metering_point("LU-METERING_POINT1")
|
297
|
+
|
298
|
+
# Check the result
|
299
|
+
self.assertTrue(result)
|
300
|
+
|
301
|
+
# Check that the request was made correctly
|
302
|
+
mock_request.assert_called_once()
|
303
|
+
|
304
|
+
@patch("requests.request")
|
305
|
+
def test_test_metering_point_invalid(self, mock_request):
|
306
|
+
"""Test test_metering_point with an invalid metering point."""
|
307
|
+
# Set up the mock response with null unit
|
308
|
+
mock_response = MagicMock()
|
309
|
+
mock_response.status_code = 200
|
310
|
+
mock_response.json.return_value = {"unit": None, "aggregatedTimeSeries": []}
|
311
|
+
mock_response.content = json.dumps(mock_response.json.return_value).encode()
|
312
|
+
mock_request.return_value = mock_response
|
313
|
+
|
314
|
+
# Call the method and check that it raises InvalidMeteringPointException
|
315
|
+
with self.assertRaises(InvalidMeteringPointException) as context:
|
316
|
+
self.client.test_metering_point("INVALID-METERING-POINT")
|
317
|
+
|
318
|
+
# Check the error message
|
319
|
+
self.assertIn("INVALID-METERING-POINT", str(context.exception))
|
320
|
+
|
321
|
+
# Check that the request was made correctly
|
322
|
+
mock_request.assert_called_once()
|
323
|
+
|
224
324
|
|
225
325
|
if __name__ == "__main__":
|
226
326
|
unittest.main()
|
leneda_client-0.1.1/README.md
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
# Leneda API Client
|
2
|
-
|
3
|
-
[![PyPI version]](https://pypi.org/project/leneda-client/)
|
4
|
-
[![Python versions]](https://pypi.org/project/leneda-client/)
|
5
|
-
[![License]](https://github.com/fedus/leneda-client/blob/main/LICENSE)
|
6
|
-
|
7
|
-
A Python client for interacting with the Leneda energy data platform API.
|
8
|
-
|
9
|
-
## Overview
|
10
|
-
|
11
|
-
This client provides a simple interface to the Leneda API, which allows users to:
|
12
|
-
|
13
|
-
- Retrieve metering data for specific time ranges
|
14
|
-
- Get aggregated metering data (hourly, daily, weekly, monthly, or total)
|
15
|
-
- Create metering data access requests
|
16
|
-
- Use predefined OBIS code constants for easy reference
|
17
|
-
|
18
|
-
## Installation
|
19
|
-
|
20
|
-
```bash
|
21
|
-
pip install leneda-client
|
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
|