leneda-client 0.1.1__py3-none-any.whl → 0.3.0__py3-none-any.whl
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/__init__.py +2 -4
- leneda/client.py +105 -39
- leneda/exceptions.py +27 -0
- leneda/models.py +16 -12
- leneda/obis_codes.py +60 -63
- leneda/version.py +1 -1
- {leneda_client-0.1.1.dist-info → leneda_client-0.3.0.dist-info}/METADATA +37 -4
- leneda_client-0.3.0.dist-info/RECORD +11 -0
- {leneda_client-0.1.1.dist-info → leneda_client-0.3.0.dist-info}/WHEEL +1 -1
- leneda_client-0.3.0.dist-info/licenses/LICENSE +21 -0
- leneda_client-0.1.1.dist-info/RECORD +0 -9
- {leneda_client-0.1.1.dist-info → leneda_client-0.3.0.dist-info}/top_level.txt +0 -0
leneda/__init__.py
CHANGED
@@ -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",
|
leneda/client.py
CHANGED
@@ -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
|
leneda/exceptions.py
ADDED
@@ -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
|
leneda/models.py
CHANGED
@@ -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}")
|
leneda/obis_codes.py
CHANGED
@@ -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
|
"""
|
leneda/version.py
CHANGED
@@ -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,11 @@
|
|
1
|
+
leneda/__init__.py,sha256=-BHIXBfTTbX6EKDHr2Bq7rPzsPNYrrkgs9AJqWJR_1Q,732
|
2
|
+
leneda/client.py,sha256=X2seOTrUYLKuLxDJT6ryGdKfNelsOhVq7lenJjI3kmM,10825
|
3
|
+
leneda/exceptions.py,sha256=a84T3uSDQsMiKyFUZTbJTAp3hELfI1GQDriqv6aMDb8,600
|
4
|
+
leneda/models.py,sha256=jdU2cIZZDExUSiSfz9zaYjJepr0m3v_x5b1fyOaEI8Q,7930
|
5
|
+
leneda/obis_codes.py,sha256=VfsJQN1U80eZ5g1bIteDCLkkmBQ0AIkkm_zNAeM1Dog,7507
|
6
|
+
leneda/version.py,sha256=wtPxgfjm82dK-e3EK0shDP4_7DlFk8hkRkOIPd6mV58,50
|
7
|
+
leneda_client-0.3.0.dist-info/licenses/LICENSE,sha256=nAhDs625lK6v8oLqjWCABKBKlwxVoRDFXQvoZPfOtKQ,1062
|
8
|
+
leneda_client-0.3.0.dist-info/METADATA,sha256=RHArkKFQqY--FL7-RCwTUQTaJvMWLvdswIYbM-8Yr1U,3344
|
9
|
+
leneda_client-0.3.0.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
|
10
|
+
leneda_client-0.3.0.dist-info/top_level.txt,sha256=PANScm25ep7WLjKiph0fhJPb8s_sa_uLHemnBpQBaJ8,7
|
11
|
+
leneda_client-0.3.0.dist-info/RECORD,,
|
@@ -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,9 +0,0 @@
|
|
1
|
-
leneda/__init__.py,sha256=JLvEM9M0jk2LcYK9DWYKrSF_BXTvmC8i570N_BggPck,850
|
2
|
-
leneda/client.py,sha256=G_4kQi54ob3Te5gcQqH1KBRfkYBe00b-WDDTn6HCMNI,7716
|
3
|
-
leneda/models.py,sha256=hTDEsdgUZJA-9jSqbOOoeJbCvFmgHMj3eZ8JZilWGpo,7861
|
4
|
-
leneda/obis_codes.py,sha256=gPrxnYfUNvWrbKFA3z-4TSTHnOhWHt5hkm5GAwoo0-w,7170
|
5
|
-
leneda/version.py,sha256=YYQdu6Uqy9CTkjnQKsNKvrOd3DH1zWTn0ajJafGZDIc,50
|
6
|
-
leneda_client-0.1.1.dist-info/METADATA,sha256=5JebX58dz2b6fsPnFYIZx6Lh2ClUaC6fbJEb6zic-ZA,1994
|
7
|
-
leneda_client-0.1.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
8
|
-
leneda_client-0.1.1.dist-info/top_level.txt,sha256=PANScm25ep7WLjKiph0fhJPb8s_sa_uLHemnBpQBaJ8,7
|
9
|
-
leneda_client-0.1.1.dist-info/RECORD,,
|
File without changes
|