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 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,3 @@
1
+ """Version information."""
2
+
3
+ __version__ = "0.1.0"
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ leneda