leneda-client 0.1.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 +36 -0
- leneda/client.py +226 -0
- leneda/models.py +235 -0
- leneda/obis_codes.py +232 -0
- leneda/version.py +3 -0
- leneda_client-0.1.0.dist-info/METADATA +58 -0
- leneda_client-0.1.0.dist-info/RECORD +9 -0
- leneda_client-0.1.0.dist-info/WHEEL +5 -0
- leneda_client-0.1.0.dist-info/top_level.txt +1 -0
leneda/__init__.py
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
"""
|
2
|
+
Leneda API client package.
|
3
|
+
|
4
|
+
This package provides a client for the Leneda API, which allows access to
|
5
|
+
energy consumption and production data for electricity and gas.
|
6
|
+
"""
|
7
|
+
|
8
|
+
# Import the client class
|
9
|
+
from .client import LenedaClient
|
10
|
+
|
11
|
+
# Import the data models
|
12
|
+
from .models import (
|
13
|
+
AggregatedMeteringData,
|
14
|
+
AggregatedMeteringValue,
|
15
|
+
MeteringData,
|
16
|
+
MeteringValue,
|
17
|
+
)
|
18
|
+
|
19
|
+
# Import the OBIS code constants
|
20
|
+
from .obis_codes import ElectricityConsumption, ElectricityProduction, GasConsumption
|
21
|
+
|
22
|
+
# Import the version
|
23
|
+
from .version import __version__
|
24
|
+
|
25
|
+
# Define what's available when using "from leneda import *"
|
26
|
+
__all__ = [
|
27
|
+
"LenedaClient",
|
28
|
+
"ElectricityConsumption",
|
29
|
+
"ElectricityProduction",
|
30
|
+
"GasConsumption",
|
31
|
+
"MeteringValue",
|
32
|
+
"MeteringData",
|
33
|
+
"AggregatedMeteringValue",
|
34
|
+
"AggregatedMeteringData",
|
35
|
+
"__version__",
|
36
|
+
]
|
leneda/client.py
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
"""
|
2
|
+
Leneda API client for accessing energy consumption and production data.
|
3
|
+
|
4
|
+
This module provides a client for the Leneda API, which allows access to
|
5
|
+
energy consumption and production data for electricity and gas.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import logging
|
10
|
+
from datetime import datetime
|
11
|
+
from typing import Any, Dict, Union
|
12
|
+
|
13
|
+
import requests
|
14
|
+
|
15
|
+
from .models import AggregatedMeteringData, MeteringData
|
16
|
+
|
17
|
+
# Set up logging
|
18
|
+
logger = logging.getLogger("leneda.client")
|
19
|
+
|
20
|
+
|
21
|
+
class LenedaClient:
|
22
|
+
"""Client for the Leneda API."""
|
23
|
+
|
24
|
+
BASE_URL = "https://api.leneda.lu/api"
|
25
|
+
|
26
|
+
def __init__(self, api_key: str, energy_id: str, debug: bool = False):
|
27
|
+
"""
|
28
|
+
Initialize the Leneda API client.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
api_key: Your Leneda API key
|
32
|
+
energy_id: Your Energy ID
|
33
|
+
debug: Enable debug logging
|
34
|
+
"""
|
35
|
+
self.api_key = api_key
|
36
|
+
self.energy_id = energy_id
|
37
|
+
|
38
|
+
# Set up headers for API requests
|
39
|
+
self.headers = {
|
40
|
+
"X-API-KEY": api_key,
|
41
|
+
"X-ENERGY-ID": energy_id,
|
42
|
+
"Content-Type": "application/json",
|
43
|
+
}
|
44
|
+
|
45
|
+
# Set up debug logging if requested
|
46
|
+
if debug:
|
47
|
+
logging.getLogger("leneda").setLevel(logging.DEBUG)
|
48
|
+
logger.setLevel(logging.DEBUG)
|
49
|
+
logger.debug("Debug logging enabled for Leneda client")
|
50
|
+
|
51
|
+
def _make_request(
|
52
|
+
self,
|
53
|
+
endpoint: str,
|
54
|
+
method: str = "GET",
|
55
|
+
params: Dict[str, Any] = None,
|
56
|
+
data: Dict[str, Any] = None,
|
57
|
+
) -> Dict[str, Any]:
|
58
|
+
"""
|
59
|
+
Make a request to the Leneda API.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
endpoint: API endpoint to call
|
63
|
+
method: HTTP method to use
|
64
|
+
params: Query parameters
|
65
|
+
data: Request body data
|
66
|
+
|
67
|
+
Returns:
|
68
|
+
API response as a dictionary
|
69
|
+
"""
|
70
|
+
url = f"{self.BASE_URL}/{endpoint}"
|
71
|
+
|
72
|
+
# Log the request details
|
73
|
+
logger.debug(f"Making {method} request to {url}")
|
74
|
+
if params:
|
75
|
+
logger.debug(f"Query parameters: {params}")
|
76
|
+
if data:
|
77
|
+
logger.debug(f"Request data: {data}")
|
78
|
+
|
79
|
+
try:
|
80
|
+
# Make the request
|
81
|
+
response = requests.request(
|
82
|
+
method=method, url=url, headers=self.headers, params=params, json=data
|
83
|
+
)
|
84
|
+
|
85
|
+
# Check for HTTP errors
|
86
|
+
response.raise_for_status()
|
87
|
+
|
88
|
+
# Parse the response
|
89
|
+
if response.content:
|
90
|
+
response_data = response.json()
|
91
|
+
logger.debug(f"Response status: {response.status_code}")
|
92
|
+
logger.debug(f"Response data: {json.dumps(response_data, indent=2)}")
|
93
|
+
return response_data
|
94
|
+
else:
|
95
|
+
logger.debug(f"Response status: {response.status_code} (no content)")
|
96
|
+
return {}
|
97
|
+
|
98
|
+
except requests.exceptions.HTTPError as e:
|
99
|
+
# Handle HTTP errors
|
100
|
+
logger.error(f"HTTP error: {e}")
|
101
|
+
if hasattr(e, "response") and e.response is not None:
|
102
|
+
logger.error(f"Response status: {e.response.status_code}")
|
103
|
+
logger.error(f"Response body: {e.response.text}")
|
104
|
+
raise
|
105
|
+
|
106
|
+
except requests.exceptions.RequestException as e:
|
107
|
+
# Handle other request errors
|
108
|
+
logger.error(f"Request error: {e}")
|
109
|
+
raise
|
110
|
+
|
111
|
+
except json.JSONDecodeError as e:
|
112
|
+
# Handle JSON parsing errors
|
113
|
+
logger.error(f"JSON decode error: {e}")
|
114
|
+
logger.error(f"Response text: {response.text}")
|
115
|
+
raise
|
116
|
+
|
117
|
+
def get_metering_data(
|
118
|
+
self,
|
119
|
+
metering_point_code: str,
|
120
|
+
obis_code: str,
|
121
|
+
start_date_time: Union[str, datetime],
|
122
|
+
end_date_time: Union[str, datetime],
|
123
|
+
) -> MeteringData:
|
124
|
+
"""
|
125
|
+
Get time series data for a specific metering point and OBIS code.
|
126
|
+
|
127
|
+
Args:
|
128
|
+
metering_point_code: The metering point code
|
129
|
+
obis_code: The OBIS code
|
130
|
+
start_date_time: Start date and time (ISO format string or datetime object)
|
131
|
+
end_date_time: End date and time (ISO format string or datetime object)
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
MeteringData object containing the time series data
|
135
|
+
"""
|
136
|
+
# Convert datetime objects to ISO format strings if needed
|
137
|
+
if isinstance(start_date_time, datetime):
|
138
|
+
start_date_time = start_date_time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
139
|
+
if isinstance(end_date_time, datetime):
|
140
|
+
end_date_time = end_date_time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
141
|
+
|
142
|
+
# Set up the endpoint and parameters
|
143
|
+
endpoint = f"metering-points/{metering_point_code}/time-series"
|
144
|
+
params = {
|
145
|
+
"obisCode": obis_code,
|
146
|
+
"startDateTime": start_date_time,
|
147
|
+
"endDateTime": end_date_time,
|
148
|
+
}
|
149
|
+
|
150
|
+
# Make the request
|
151
|
+
response_data = self._make_request(endpoint, params=params)
|
152
|
+
|
153
|
+
# 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
|
+
)
|
157
|
+
|
158
|
+
def get_aggregated_metering_data(
|
159
|
+
self,
|
160
|
+
metering_point_code: str,
|
161
|
+
obis_code: str,
|
162
|
+
start_date: Union[str, datetime],
|
163
|
+
end_date: Union[str, datetime],
|
164
|
+
aggregation_level: str = "Day",
|
165
|
+
transformation_mode: str = "Accumulation",
|
166
|
+
) -> AggregatedMeteringData:
|
167
|
+
"""
|
168
|
+
Get aggregated time series data for a specific metering point and OBIS code.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
metering_point_code: The metering point code
|
172
|
+
obis_code: The OBIS code
|
173
|
+
start_date: Start date (ISO format string or datetime object)
|
174
|
+
end_date: End date (ISO format string or datetime object)
|
175
|
+
aggregation_level: Aggregation level (Day, Week, Month, Quarter, Year)
|
176
|
+
transformation_mode: Transformation mode (Accumulation, Average, Maximum, Minimum)
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
AggregatedMeteringData object containing the aggregated time series data
|
180
|
+
"""
|
181
|
+
# Convert datetime objects to ISO format strings if needed
|
182
|
+
if isinstance(start_date, datetime):
|
183
|
+
start_date = start_date.strftime("%Y-%m-%d")
|
184
|
+
if isinstance(end_date, datetime):
|
185
|
+
end_date = end_date.strftime("%Y-%m-%d")
|
186
|
+
|
187
|
+
# Set up the endpoint and parameters
|
188
|
+
endpoint = f"metering-points/{metering_point_code}/time-series/aggregated"
|
189
|
+
params = {
|
190
|
+
"obisCode": obis_code,
|
191
|
+
"startDate": start_date,
|
192
|
+
"endDate": end_date,
|
193
|
+
"aggregationLevel": aggregation_level,
|
194
|
+
"transformationMode": transformation_mode,
|
195
|
+
}
|
196
|
+
|
197
|
+
# Make the request
|
198
|
+
response_data = self._make_request(endpoint, params=params)
|
199
|
+
|
200
|
+
# 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
|
+
)
|
208
|
+
|
209
|
+
def request_metering_data_access(self, metering_point_code: str) -> Dict[str, Any]:
|
210
|
+
"""
|
211
|
+
Request access to metering data for a specific metering point.
|
212
|
+
|
213
|
+
Args:
|
214
|
+
metering_point_code: The metering point code
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
Response data from the API
|
218
|
+
"""
|
219
|
+
# Set up the endpoint and data
|
220
|
+
endpoint = "metering-data-access-request"
|
221
|
+
data = {"meteringPointCode": metering_point_code}
|
222
|
+
|
223
|
+
# Make the request
|
224
|
+
response_data = self._make_request(endpoint, method="POST", data=data)
|
225
|
+
|
226
|
+
return response_data
|
leneda/models.py
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
"""
|
2
|
+
Data models for the Leneda API responses.
|
3
|
+
|
4
|
+
This module provides typed data classes for the various responses from the Leneda API,
|
5
|
+
making it easier to work with the data in a type-safe manner.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
from dataclasses import dataclass, field
|
10
|
+
from datetime import datetime
|
11
|
+
from typing import Any, Dict, List
|
12
|
+
|
13
|
+
from dateutil import parser
|
14
|
+
|
15
|
+
# Set up logging
|
16
|
+
logger = logging.getLogger("leneda.models")
|
17
|
+
|
18
|
+
|
19
|
+
@dataclass
|
20
|
+
class MeteringValue:
|
21
|
+
"""A single metering value from a time series."""
|
22
|
+
|
23
|
+
value: float
|
24
|
+
started_at: datetime
|
25
|
+
type: str
|
26
|
+
version: int
|
27
|
+
calculated: bool
|
28
|
+
|
29
|
+
@classmethod
|
30
|
+
def from_dict(cls, data: Dict[str, Any]) -> "MeteringValue":
|
31
|
+
"""Create a MeteringValue from a dictionary."""
|
32
|
+
try:
|
33
|
+
# Handle the required fields
|
34
|
+
value = float(data["value"])
|
35
|
+
|
36
|
+
# Parse ISO format string to datetime using dateutil
|
37
|
+
started_at = parser.isoparse(data["startedAt"])
|
38
|
+
|
39
|
+
# Get type, version and calculated
|
40
|
+
type_value = data["type"]
|
41
|
+
version = int(data["version"])
|
42
|
+
calculated = bool(data["calculated"])
|
43
|
+
|
44
|
+
return cls(
|
45
|
+
value=value,
|
46
|
+
started_at=started_at,
|
47
|
+
type=type_value,
|
48
|
+
version=version,
|
49
|
+
calculated=calculated,
|
50
|
+
)
|
51
|
+
except KeyError as e:
|
52
|
+
# Log the error and the data that caused it
|
53
|
+
logger.error(f"Missing key in API response: {e}")
|
54
|
+
logger.debug(f"API response data: {data}")
|
55
|
+
raise
|
56
|
+
except Exception as e:
|
57
|
+
logger.error(f"Error parsing metering value: {e}")
|
58
|
+
logger.debug(f"API response data: {data}")
|
59
|
+
raise
|
60
|
+
|
61
|
+
|
62
|
+
@dataclass
|
63
|
+
class MeteringData:
|
64
|
+
"""Metering data for a specific metering point and OBIS code."""
|
65
|
+
|
66
|
+
metering_point_code: str
|
67
|
+
obis_code: str
|
68
|
+
interval_length: str
|
69
|
+
unit: str
|
70
|
+
items: List[MeteringValue] = field(default_factory=list)
|
71
|
+
|
72
|
+
@classmethod
|
73
|
+
def from_dict(
|
74
|
+
cls, data: Dict[str, Any], metering_point_code: str = "", obis_code: str = ""
|
75
|
+
) -> "MeteringData":
|
76
|
+
"""Create a MeteringData from a dictionary."""
|
77
|
+
try:
|
78
|
+
# Log the raw data for debugging
|
79
|
+
logger.debug(f"Creating MeteringData from: {data}")
|
80
|
+
|
81
|
+
# Use values from the response, falling back to provided values if not in response
|
82
|
+
metering_point_code_value = data.get("meteringPointCode", metering_point_code)
|
83
|
+
obis_code_value = data.get("obisCode", obis_code)
|
84
|
+
|
85
|
+
# Extract items safely
|
86
|
+
items_data = data.get("items", [])
|
87
|
+
items = []
|
88
|
+
|
89
|
+
for item_data in items_data:
|
90
|
+
try:
|
91
|
+
item = MeteringValue.from_dict(item_data)
|
92
|
+
items.append(item)
|
93
|
+
except Exception as e:
|
94
|
+
logger.warning(f"Skipping invalid item: {e}")
|
95
|
+
logger.debug(f"Invalid item data: {item_data}")
|
96
|
+
|
97
|
+
return cls(
|
98
|
+
metering_point_code=metering_point_code_value,
|
99
|
+
obis_code=obis_code_value,
|
100
|
+
interval_length=data.get("intervalLength", ""),
|
101
|
+
unit=data.get("unit", ""),
|
102
|
+
items=items,
|
103
|
+
)
|
104
|
+
except Exception as e:
|
105
|
+
logger.error(f"Error creating MeteringData: {e}")
|
106
|
+
logger.debug(f"API response data: {data}")
|
107
|
+
raise
|
108
|
+
|
109
|
+
def to_dict(self) -> Dict[str, Any]:
|
110
|
+
"""Convert the MeteringData to a dictionary."""
|
111
|
+
return {
|
112
|
+
"meteringPointCode": self.metering_point_code,
|
113
|
+
"obisCode": self.obis_code,
|
114
|
+
"intervalLength": self.interval_length,
|
115
|
+
"unit": self.unit,
|
116
|
+
"items": [
|
117
|
+
{
|
118
|
+
"value": item.value,
|
119
|
+
"startedAt": item.started_at.isoformat(),
|
120
|
+
"type": item.type,
|
121
|
+
"version": item.version,
|
122
|
+
"calculated": item.calculated,
|
123
|
+
}
|
124
|
+
for item in self.items
|
125
|
+
],
|
126
|
+
}
|
127
|
+
|
128
|
+
def __str__(self) -> str:
|
129
|
+
"""Return a string representation of the MeteringData."""
|
130
|
+
return (
|
131
|
+
f"MeteringData(metering_point_code={self.metering_point_code}, "
|
132
|
+
f"obis_code={self.obis_code}, unit={self.unit}, "
|
133
|
+
f"items_count={len(self.items)})"
|
134
|
+
)
|
135
|
+
|
136
|
+
|
137
|
+
@dataclass
|
138
|
+
class AggregatedMeteringValue:
|
139
|
+
"""A single aggregated metering value."""
|
140
|
+
|
141
|
+
value: float
|
142
|
+
started_at: datetime
|
143
|
+
ended_at: datetime
|
144
|
+
calculated: bool
|
145
|
+
|
146
|
+
@classmethod
|
147
|
+
def from_dict(cls, data: Dict[str, Any]) -> "AggregatedMeteringValue":
|
148
|
+
"""Create an AggregatedMeteringValue from a dictionary."""
|
149
|
+
try:
|
150
|
+
# Handle the required fields
|
151
|
+
value = float(data["value"])
|
152
|
+
|
153
|
+
# Parse ISO format string to datetime using dateutil
|
154
|
+
started_at = parser.isoparse(data["startedAt"])
|
155
|
+
ended_at = parser.isoparse(data["endedAt"])
|
156
|
+
|
157
|
+
# Get calculated
|
158
|
+
calculated = bool(data["calculated"])
|
159
|
+
|
160
|
+
return cls(
|
161
|
+
value=value,
|
162
|
+
started_at=started_at,
|
163
|
+
ended_at=ended_at,
|
164
|
+
calculated=calculated,
|
165
|
+
)
|
166
|
+
except KeyError as e:
|
167
|
+
logger.error(f"Missing key in API response: {e}")
|
168
|
+
logger.debug(f"API response data: {data}")
|
169
|
+
raise
|
170
|
+
except Exception as e:
|
171
|
+
logger.error(f"Error parsing aggregated metering value: {e}")
|
172
|
+
logger.debug(f"API response data: {data}")
|
173
|
+
raise
|
174
|
+
|
175
|
+
|
176
|
+
@dataclass
|
177
|
+
class AggregatedMeteringData:
|
178
|
+
"""Aggregated metering data for a specific metering point and OBIS code."""
|
179
|
+
|
180
|
+
unit: str
|
181
|
+
aggregated_time_series: List[AggregatedMeteringValue] = field(default_factory=list)
|
182
|
+
|
183
|
+
@classmethod
|
184
|
+
def from_dict(
|
185
|
+
cls,
|
186
|
+
data: Dict[str, Any],
|
187
|
+
metering_point_code: str = "",
|
188
|
+
obis_code: str = "",
|
189
|
+
aggregation_level: str = "",
|
190
|
+
transformation_mode: str = "",
|
191
|
+
) -> "AggregatedMeteringData":
|
192
|
+
"""Create an AggregatedMeteringData from a dictionary."""
|
193
|
+
try:
|
194
|
+
# Log the raw data for debugging
|
195
|
+
logger.debug(f"Creating AggregatedMeteringData from: {data}")
|
196
|
+
|
197
|
+
# Extract items safely
|
198
|
+
time_series_data = data.get("aggregatedTimeSeries", [])
|
199
|
+
time_series = []
|
200
|
+
|
201
|
+
for item_data in time_series_data:
|
202
|
+
try:
|
203
|
+
item = AggregatedMeteringValue.from_dict(item_data)
|
204
|
+
time_series.append(item)
|
205
|
+
except Exception as e:
|
206
|
+
logger.warning(f"Skipping invalid aggregated item: {e}")
|
207
|
+
logger.debug(f"Invalid item data: {item_data}")
|
208
|
+
|
209
|
+
return cls(unit=data.get("unit", ""), aggregated_time_series=time_series)
|
210
|
+
except Exception as e:
|
211
|
+
logger.error(f"Error creating AggregatedMeteringData: {e}")
|
212
|
+
logger.debug(f"API response data: {data}")
|
213
|
+
raise
|
214
|
+
|
215
|
+
def to_dict(self) -> Dict[str, Any]:
|
216
|
+
"""Convert the AggregatedMeteringData to a dictionary."""
|
217
|
+
return {
|
218
|
+
"unit": self.unit,
|
219
|
+
"aggregatedTimeSeries": [
|
220
|
+
{
|
221
|
+
"value": item.value,
|
222
|
+
"startedAt": item.started_at.isoformat(),
|
223
|
+
"endedAt": item.ended_at.isoformat(),
|
224
|
+
"calculated": item.calculated,
|
225
|
+
}
|
226
|
+
for item in self.aggregated_time_series
|
227
|
+
],
|
228
|
+
}
|
229
|
+
|
230
|
+
def __str__(self) -> str:
|
231
|
+
"""Return a string representation of the AggregatedMeteringData."""
|
232
|
+
return (
|
233
|
+
f"AggregatedMeteringData(unit={self.unit}, "
|
234
|
+
f"items_count={len(self.aggregated_time_series)})"
|
235
|
+
)
|
leneda/obis_codes.py
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
"""
|
2
|
+
OBIS (Object Identification System) codes for the Leneda API.
|
3
|
+
|
4
|
+
This module provides constants for the various OBIS codes used in the Leneda platform,
|
5
|
+
making it easier to reference the correct codes when retrieving energy data.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from dataclasses import dataclass
|
9
|
+
from enum import Enum
|
10
|
+
from typing import Dict, List
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class ObisCodeInfo:
|
15
|
+
"""Information about an OBIS code."""
|
16
|
+
|
17
|
+
code: str
|
18
|
+
unit: str
|
19
|
+
service_type: str
|
20
|
+
description: str
|
21
|
+
|
22
|
+
|
23
|
+
class ElectricityConsumption(str, Enum):
|
24
|
+
"""OBIS codes for electricity consumption."""
|
25
|
+
|
26
|
+
ACTIVE = "1-1:1.29.0" # Measured active consumption (kW)
|
27
|
+
REACTIVE = "1-1:3.29.0" # Measured reactive consumption (kVAR)
|
28
|
+
|
29
|
+
# Consumption covered by production sharing groups
|
30
|
+
COVERED_LAYER1 = "1-65:1.29.1" # Layer 1 sharing Group (AIR)
|
31
|
+
COVERED_LAYER2 = "1-65:1.29.3" # Layer 2 sharing Group (ACR/ACF/AC1)
|
32
|
+
COVERED_LAYER3 = "1-65:1.29.2" # Layer 3 sharing Group (CEL)
|
33
|
+
COVERED_LAYER4 = "1-65:1.29.4" # Layer 4 sharing Group (APS/CER/CEN)
|
34
|
+
REMAINING = "1-65:1.29.9" # Remaining consumption after sharing invoiced by supplier
|
35
|
+
|
36
|
+
|
37
|
+
class ElectricityProduction(str, Enum):
|
38
|
+
"""OBIS codes for electricity production."""
|
39
|
+
|
40
|
+
ACTIVE = "1-1:2.29.0" # Measured active production (kW)
|
41
|
+
REACTIVE = "1-1:4.29.0" # Measured reactive production (kVAR)
|
42
|
+
|
43
|
+
# Production shared within sharing groups
|
44
|
+
SHARED_LAYER1 = "1-65:2.29.1" # Layer 1 sharing Group (AIR)
|
45
|
+
SHARED_LAYER2 = "1-65:2.29.3" # Layer 2 sharing Group (ACR/ACF/AC1)
|
46
|
+
SHARED_LAYER3 = "1-65:2.29.2" # Layer 3 sharing Group (CEL)
|
47
|
+
SHARED_LAYER4 = "1-65:2.29.4" # Layer 4 sharing Group (APS/CER/CEN)
|
48
|
+
REMAINING = "1-65:2.29.9" # Remaining production after sharing sold to market
|
49
|
+
|
50
|
+
|
51
|
+
class GasConsumption(str, Enum):
|
52
|
+
"""OBIS codes for gas consumption."""
|
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)
|
57
|
+
|
58
|
+
|
59
|
+
# Complete mapping of all OBIS codes to their details
|
60
|
+
OBIS_CODES: Dict[str, ObisCodeInfo] = {
|
61
|
+
# Electricity Consumption
|
62
|
+
ElectricityConsumption.ACTIVE: ObisCodeInfo(
|
63
|
+
ElectricityConsumption.ACTIVE,
|
64
|
+
"kW",
|
65
|
+
"Consumption",
|
66
|
+
"Measured active consumption",
|
67
|
+
),
|
68
|
+
ElectricityConsumption.REACTIVE: ObisCodeInfo(
|
69
|
+
ElectricityConsumption.REACTIVE,
|
70
|
+
"kVAR",
|
71
|
+
"Consumption",
|
72
|
+
"Measured reactive consumption",
|
73
|
+
),
|
74
|
+
ElectricityConsumption.COVERED_LAYER1: ObisCodeInfo(
|
75
|
+
ElectricityConsumption.COVERED_LAYER1,
|
76
|
+
"kW",
|
77
|
+
"Consumption",
|
78
|
+
"Consumption covered by production of layer 1 sharing Group (AIR)",
|
79
|
+
),
|
80
|
+
ElectricityConsumption.COVERED_LAYER2: ObisCodeInfo(
|
81
|
+
ElectricityConsumption.COVERED_LAYER2,
|
82
|
+
"kW",
|
83
|
+
"Consumption",
|
84
|
+
"Consumption covered by production of layer 2 sharing Group (ACR/ACF/AC1)",
|
85
|
+
),
|
86
|
+
ElectricityConsumption.COVERED_LAYER3: ObisCodeInfo(
|
87
|
+
ElectricityConsumption.COVERED_LAYER3,
|
88
|
+
"kW",
|
89
|
+
"Consumption",
|
90
|
+
"Consumption covered by production of layer 3 sharing Group (CEL)",
|
91
|
+
),
|
92
|
+
ElectricityConsumption.COVERED_LAYER4: ObisCodeInfo(
|
93
|
+
ElectricityConsumption.COVERED_LAYER4,
|
94
|
+
"kW",
|
95
|
+
"Consumption",
|
96
|
+
"Consumption covered by production of layer 4 sharing Group (APS/CER/CEN)",
|
97
|
+
),
|
98
|
+
ElectricityConsumption.REMAINING: ObisCodeInfo(
|
99
|
+
ElectricityConsumption.REMAINING,
|
100
|
+
"kW",
|
101
|
+
"Consumption",
|
102
|
+
"Remaining consumption after sharing invoiced by supplier",
|
103
|
+
),
|
104
|
+
# Electricity Production
|
105
|
+
ElectricityProduction.ACTIVE: ObisCodeInfo(
|
106
|
+
ElectricityProduction.ACTIVE, "kW", "Production", "Measured active production"
|
107
|
+
),
|
108
|
+
ElectricityProduction.REACTIVE: ObisCodeInfo(
|
109
|
+
ElectricityProduction.REACTIVE,
|
110
|
+
"kVAR",
|
111
|
+
"Consumption",
|
112
|
+
"Measured reactive production",
|
113
|
+
),
|
114
|
+
ElectricityProduction.SHARED_LAYER1: ObisCodeInfo(
|
115
|
+
ElectricityProduction.SHARED_LAYER1,
|
116
|
+
"kW",
|
117
|
+
"Production",
|
118
|
+
"Production shared within layer 1 sharing Group (AIR)",
|
119
|
+
),
|
120
|
+
ElectricityProduction.SHARED_LAYER2: ObisCodeInfo(
|
121
|
+
ElectricityProduction.SHARED_LAYER2,
|
122
|
+
"kW",
|
123
|
+
"Production",
|
124
|
+
"Production shared within layer 2 sharing Group (ACR/ACF/AC1)",
|
125
|
+
),
|
126
|
+
ElectricityProduction.SHARED_LAYER3: ObisCodeInfo(
|
127
|
+
ElectricityProduction.SHARED_LAYER3,
|
128
|
+
"kW",
|
129
|
+
"Production",
|
130
|
+
"Production shared within layer 3 sharing Group (CEL)",
|
131
|
+
),
|
132
|
+
ElectricityProduction.SHARED_LAYER4: ObisCodeInfo(
|
133
|
+
ElectricityProduction.SHARED_LAYER4,
|
134
|
+
"kW",
|
135
|
+
"Production",
|
136
|
+
"Production shared within layer 4 sharing Group (APS/CER/CEN)",
|
137
|
+
),
|
138
|
+
ElectricityProduction.REMAINING: ObisCodeInfo(
|
139
|
+
ElectricityProduction.REMAINING,
|
140
|
+
"kW",
|
141
|
+
"Production",
|
142
|
+
"Remaining production after sharing sold to market",
|
143
|
+
),
|
144
|
+
# Gas Consumption
|
145
|
+
GasConsumption.VOLUME: ObisCodeInfo(
|
146
|
+
GasConsumption.VOLUME, "m³", "Consumption", "Measured consumed volume"
|
147
|
+
),
|
148
|
+
GasConsumption.STANDARD_VOLUME: ObisCodeInfo(
|
149
|
+
GasConsumption.STANDARD_VOLUME,
|
150
|
+
"Nm³",
|
151
|
+
"Consumption",
|
152
|
+
"Measured consumed standard volume",
|
153
|
+
),
|
154
|
+
GasConsumption.ENERGY: ObisCodeInfo(
|
155
|
+
GasConsumption.ENERGY, "kWh", "Consumption", "Measured consumed energy"
|
156
|
+
),
|
157
|
+
}
|
158
|
+
|
159
|
+
|
160
|
+
def get_obis_info(obis_code: str) -> ObisCodeInfo:
|
161
|
+
"""
|
162
|
+
Get information about an OBIS code.
|
163
|
+
|
164
|
+
Args:
|
165
|
+
obis_code: The OBIS code to look up
|
166
|
+
|
167
|
+
Returns:
|
168
|
+
Information about the OBIS code
|
169
|
+
|
170
|
+
Raises:
|
171
|
+
KeyError: If the OBIS code is not found
|
172
|
+
|
173
|
+
Example:
|
174
|
+
>>> info = get_obis_info(ElectricityConsumption.ACTIVE)
|
175
|
+
>>> print(info.unit)
|
176
|
+
'kW'
|
177
|
+
"""
|
178
|
+
return OBIS_CODES[obis_code]
|
179
|
+
|
180
|
+
|
181
|
+
def get_unit(obis_code: str) -> str:
|
182
|
+
"""
|
183
|
+
Get the unit of measurement for an OBIS code.
|
184
|
+
|
185
|
+
Args:
|
186
|
+
obis_code: The OBIS code to look up
|
187
|
+
|
188
|
+
Returns:
|
189
|
+
The unit of measurement
|
190
|
+
|
191
|
+
Raises:
|
192
|
+
KeyError: If the OBIS code is not found
|
193
|
+
|
194
|
+
Example:
|
195
|
+
>>> unit = get_unit(ElectricityConsumption.ACTIVE)
|
196
|
+
>>> print(unit)
|
197
|
+
'kW'
|
198
|
+
"""
|
199
|
+
return OBIS_CODES[obis_code].unit
|
200
|
+
|
201
|
+
|
202
|
+
def list_all_obis_codes() -> List[ObisCodeInfo]:
|
203
|
+
"""
|
204
|
+
Get a list of all OBIS codes and their information.
|
205
|
+
|
206
|
+
Returns:
|
207
|
+
A list of all OBIS code information
|
208
|
+
|
209
|
+
Example:
|
210
|
+
>>> codes = list_all_obis_codes()
|
211
|
+
>>> print(len(codes))
|
212
|
+
17
|
213
|
+
"""
|
214
|
+
return list(OBIS_CODES.values())
|
215
|
+
|
216
|
+
|
217
|
+
def list_obis_codes_by_service_type(service_type: str) -> List[ObisCodeInfo]:
|
218
|
+
"""
|
219
|
+
Get a list of OBIS codes filtered by service type.
|
220
|
+
|
221
|
+
Args:
|
222
|
+
service_type: The service type to filter by (e.g., "Consumption", "Production")
|
223
|
+
|
224
|
+
Returns:
|
225
|
+
A list of OBIS code information matching the service type
|
226
|
+
|
227
|
+
Example:
|
228
|
+
>>> codes = list_obis_codes_by_service_type("Production")
|
229
|
+
>>> print(len(codes))
|
230
|
+
7
|
231
|
+
"""
|
232
|
+
return [info for info in OBIS_CODES.values() if info.service_type == service_type]
|
leneda/version.py
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: leneda-client
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Python client for the Leneda energy data platform
|
5
|
+
Home-page: https://github.com/fedus/leneda-client
|
6
|
+
Author: fedus
|
7
|
+
Author-email: fedus@dillendapp.eu
|
8
|
+
Project-URL: Bug Reports, https://github.com/yourusername/leneda-client/issues
|
9
|
+
Project-URL: Source, https://github.com/yourusername/leneda-client
|
10
|
+
Project-URL: Documentation, https://github.com/yourusername/leneda-client#readme
|
11
|
+
Keywords: leneda,energy,api,client
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
13
|
+
Classifier: Intended Audience :: Developers
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
21
|
+
Classifier: Topic :: Utilities
|
22
|
+
Requires-Python: >=3.8
|
23
|
+
Description-Content-Type: text/markdown
|
24
|
+
Requires-Dist: requests>=2.25.0
|
25
|
+
Requires-Dist: python-dateutil>=2.8.2
|
26
|
+
Dynamic: author
|
27
|
+
Dynamic: author-email
|
28
|
+
Dynamic: classifier
|
29
|
+
Dynamic: description
|
30
|
+
Dynamic: description-content-type
|
31
|
+
Dynamic: home-page
|
32
|
+
Dynamic: keywords
|
33
|
+
Dynamic: project-url
|
34
|
+
Dynamic: requires-dist
|
35
|
+
Dynamic: requires-python
|
36
|
+
Dynamic: summary
|
37
|
+
|
38
|
+
# Leneda API Client
|
39
|
+
|
40
|
+
[![PyPI version]](https://pypi.org/project/leneda-client/)
|
41
|
+
[![Python versions]](https://pypi.org/project/leneda-client/)
|
42
|
+
[![License]](https://github.com/fedus/leneda-client/blob/main/LICENSE)
|
43
|
+
|
44
|
+
A Python client for interacting with the Leneda energy data platform API.
|
45
|
+
|
46
|
+
## Overview
|
47
|
+
|
48
|
+
This client provides a simple interface to the Leneda API, which allows users to:
|
49
|
+
|
50
|
+
- Retrieve metering data for specific time ranges
|
51
|
+
- Get aggregated metering data (hourly, daily, weekly, monthly, or total)
|
52
|
+
- Create metering data access requests
|
53
|
+
- Use predefined OBIS code constants for easy reference
|
54
|
+
|
55
|
+
## Installation
|
56
|
+
|
57
|
+
```bash
|
58
|
+
pip install leneda-client
|
@@ -0,0 +1,9 @@
|
|
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=HOmU5sgXU14tXjdvSe0iIIyluugGL0Y7R8eo1JaeyxE,50
|
6
|
+
leneda_client-0.1.0.dist-info/METADATA,sha256=lVPMKoe-FKvngJjQXzFLEpmKn3nrhsMppBWhwEvoobI,1994
|
7
|
+
leneda_client-0.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
8
|
+
leneda_client-0.1.0.dist-info/top_level.txt,sha256=PANScm25ep7WLjKiph0fhJPb8s_sa_uLHemnBpQBaJ8,7
|
9
|
+
leneda_client-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
leneda
|