e-data 2.0.0b1__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.
- e_data-2.0.0b1.dist-info/METADATA +214 -0
- e_data-2.0.0b1.dist-info/RECORD +21 -0
- e_data-2.0.0b1.dist-info/WHEEL +5 -0
- e_data-2.0.0b1.dist-info/licenses/LICENSE +674 -0
- e_data-2.0.0b1.dist-info/top_level.txt +1 -0
- edata/__init__.py +0 -0
- edata/const.py +56 -0
- edata/helpers.py +312 -0
- edata/tests/__init__.py +0 -0
- edata/tests/connectors/__init__.py +0 -0
- edata/tests/connectors/test_datadis_connector.py +178 -0
- edata/tests/connectors/test_redata_connector.py +29 -0
- edata/tests/services/__init__.py +0 -0
- edata/tests/services/test_billing_service.py +1169 -0
- edata/tests/services/test_consumption_service.py +955 -0
- edata/tests/services/test_contract_service.py +216 -0
- edata/tests/services/test_database_service.py +415 -0
- edata/tests/services/test_maximeter_service.py +83 -0
- edata/tests/services/test_supply_service.py +251 -0
- edata/tests/test_helpers.py +612 -0
- edata/utils.py +164 -0
edata/const.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Constants file."""
|
|
2
|
+
|
|
3
|
+
PROG_NAME = "edata"
|
|
4
|
+
DEFAULT_STORAGE_DIR = "edata.storage"
|
|
5
|
+
|
|
6
|
+
# Attributes definition for backward compatibility
|
|
7
|
+
ATTRIBUTES = {
|
|
8
|
+
"cups": None,
|
|
9
|
+
"contract_p1_kW": "kW",
|
|
10
|
+
"contract_p2_kW": "kW",
|
|
11
|
+
"yesterday_kWh": "kWh",
|
|
12
|
+
"yesterday_hours": "h",
|
|
13
|
+
"yesterday_p1_kWh": "kWh",
|
|
14
|
+
"yesterday_p2_kWh": "kWh",
|
|
15
|
+
"yesterday_p3_kWh": "kWh",
|
|
16
|
+
"yesterday_surplus_kWh": "kWh",
|
|
17
|
+
"yesterday_surplus_p1_kWh": "kWh",
|
|
18
|
+
"yesterday_surplus_p2_kWh": "kWh",
|
|
19
|
+
"yesterday_surplus_p3_kWh": "kWh",
|
|
20
|
+
"last_registered_date": None,
|
|
21
|
+
"last_registered_day_kWh": "kWh",
|
|
22
|
+
"last_registered_day_hours": "h",
|
|
23
|
+
"last_registered_day_p1_kWh": "kWh",
|
|
24
|
+
"last_registered_day_p2_kWh": "kWh",
|
|
25
|
+
"last_registered_day_p3_kWh": "kWh",
|
|
26
|
+
"last_registered_day_surplus_kWh": "kWh",
|
|
27
|
+
"last_registered_day_surplus_p1_kWh": "kWh",
|
|
28
|
+
"last_registered_day_surplus_p2_kWh": "kWh",
|
|
29
|
+
"last_registered_day_surplus_p3_kWh": "kWh",
|
|
30
|
+
"month_kWh": "kWh",
|
|
31
|
+
"month_daily_kWh": "kWh",
|
|
32
|
+
"month_days": "d",
|
|
33
|
+
"month_p1_kWh": "kWh",
|
|
34
|
+
"month_p2_kWh": "kWh",
|
|
35
|
+
"month_p3_kWh": "kWh",
|
|
36
|
+
"month_surplus_kWh": "kWh",
|
|
37
|
+
"month_surplus_p1_kWh": "kWh",
|
|
38
|
+
"month_surplus_p2_kWh": "kWh",
|
|
39
|
+
"month_surplus_p3_kWh": "kWh",
|
|
40
|
+
"month_€": "€",
|
|
41
|
+
"last_month_kWh": "kWh",
|
|
42
|
+
"last_month_daily_kWh": "kWh",
|
|
43
|
+
"last_month_days": "d",
|
|
44
|
+
"last_month_p1_kWh": "kWh",
|
|
45
|
+
"last_month_p2_kWh": "kWh",
|
|
46
|
+
"last_month_p3_kWh": "kWh",
|
|
47
|
+
"last_month_surplus_kWh": "kWh",
|
|
48
|
+
"last_month_surplus_p1_kWh": "kWh",
|
|
49
|
+
"last_month_surplus_p2_kWh": "kWh",
|
|
50
|
+
"last_month_surplus_p3_kWh": "kWh",
|
|
51
|
+
"last_month_€": "€",
|
|
52
|
+
"max_power_kW": "kW",
|
|
53
|
+
"max_power_date": None,
|
|
54
|
+
"max_power_mean_kW": "kW",
|
|
55
|
+
"max_power_90perc_kW": "kW",
|
|
56
|
+
}
|
edata/helpers.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""A module for edata helpers."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
from edata.connectors.datadis import DatadisConnector
|
|
8
|
+
from edata.const import ATTRIBUTES
|
|
9
|
+
from edata.models.pricing import PricingRules
|
|
10
|
+
from edata.services.billing import BillingService
|
|
11
|
+
from edata.services.consumption import ConsumptionService
|
|
12
|
+
from edata.services.contract import ContractService
|
|
13
|
+
from edata.services.maximeter import MaximeterService
|
|
14
|
+
from edata.services.supply import SupplyService
|
|
15
|
+
|
|
16
|
+
_LOGGER = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def acups(cups):
|
|
20
|
+
"""Print an abbreviated and anonymized CUPS."""
|
|
21
|
+
return cups[-5:]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EdataHelper:
|
|
25
|
+
"""Main EdataHelper class using service-based architecture."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
datadis_username: str,
|
|
30
|
+
datadis_password: str,
|
|
31
|
+
cups: str,
|
|
32
|
+
datadis_authorized_nif: str | None = None,
|
|
33
|
+
pricing_rules: PricingRules | None = None,
|
|
34
|
+
storage_dir_path: str | None = None,
|
|
35
|
+
enable_smart_fetch: bool = True,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initialize EdataHelper with service-based architecture.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
datadis_username: Datadis username
|
|
41
|
+
datadis_password: Datadis password
|
|
42
|
+
cups: CUPS identifier
|
|
43
|
+
datadis_authorized_nif: Optional authorized NIF
|
|
44
|
+
pricing_rules: Pricing configuration
|
|
45
|
+
storage_dir_path: Directory for database and cache storage
|
|
46
|
+
enable_smart_fetch: Enable smart fetching in datadis connector
|
|
47
|
+
"""
|
|
48
|
+
self._cups = cups
|
|
49
|
+
self._scups = acups(cups)
|
|
50
|
+
self._authorized_nif = datadis_authorized_nif
|
|
51
|
+
self._storage_dir = storage_dir_path
|
|
52
|
+
self.pricing_rules = pricing_rules
|
|
53
|
+
|
|
54
|
+
# Initialize summary attributes
|
|
55
|
+
self.summary: Dict[str, Any] = {}
|
|
56
|
+
for attr in ATTRIBUTES:
|
|
57
|
+
self.summary[attr] = None
|
|
58
|
+
|
|
59
|
+
# For backward compatibility, alias 'attributes' to 'summary'
|
|
60
|
+
self.attributes = self.summary
|
|
61
|
+
|
|
62
|
+
# Determine if using PVPC pricing
|
|
63
|
+
self.enable_billing = pricing_rules is not None
|
|
64
|
+
if self.enable_billing:
|
|
65
|
+
self.is_pvpc = not all(
|
|
66
|
+
getattr(pricing_rules, x, None) is not None
|
|
67
|
+
for x in ("p1_kwh_eur", "p2_kwh_eur", "p3_kwh_eur")
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
self.is_pvpc = False
|
|
71
|
+
|
|
72
|
+
# Create shared Datadis connector
|
|
73
|
+
self._datadis_connector = DatadisConnector(
|
|
74
|
+
username=datadis_username,
|
|
75
|
+
password=datadis_password,
|
|
76
|
+
enable_smart_fetch=enable_smart_fetch,
|
|
77
|
+
storage_path=storage_dir_path,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Initialize services with dependency injection
|
|
81
|
+
self._supply_service = SupplyService(
|
|
82
|
+
datadis_connector=self._datadis_connector,
|
|
83
|
+
storage_dir=storage_dir_path,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
self._contract_service = ContractService(
|
|
87
|
+
datadis_connector=self._datadis_connector,
|
|
88
|
+
storage_dir=storage_dir_path,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self._consumption_service = ConsumptionService(
|
|
92
|
+
datadis_connector=self._datadis_connector,
|
|
93
|
+
storage_dir=storage_dir_path,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
self._maximeter_service = MaximeterService(
|
|
97
|
+
datadis_connector=self._datadis_connector,
|
|
98
|
+
storage_dir=storage_dir_path,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if self.enable_billing:
|
|
102
|
+
self._billing_service = BillingService(storage_dir=storage_dir_path)
|
|
103
|
+
|
|
104
|
+
_LOGGER.info(f"EdataHelper initialized for CUPS {self._scups}")
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def datadis_connector(self) -> DatadisConnector:
|
|
108
|
+
"""Get the shared Datadis connector instance."""
|
|
109
|
+
return self._datadis_connector
|
|
110
|
+
|
|
111
|
+
async def update(
|
|
112
|
+
self,
|
|
113
|
+
date_from: datetime = datetime(1970, 1, 1),
|
|
114
|
+
date_to: datetime = datetime.today(),
|
|
115
|
+
):
|
|
116
|
+
"""Update all data and calculate summary attributes.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
date_from: Start date for data updates
|
|
120
|
+
date_to: End date for data updates
|
|
121
|
+
incremental_update: Whether to update incrementally (deprecated, ignored)
|
|
122
|
+
"""
|
|
123
|
+
_LOGGER.info(
|
|
124
|
+
f"{self._scups}: Starting update from {date_from.date()} to {date_to.date()}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# Step 1: Update supplies
|
|
129
|
+
_LOGGER.info(f"{self._scups}: Updating supplies")
|
|
130
|
+
supply_result = await self._supply_service.update_supplies(
|
|
131
|
+
authorized_nif=self._authorized_nif
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if not supply_result["success"]:
|
|
135
|
+
_LOGGER.error(
|
|
136
|
+
f"{self._scups}: Failed to update supplies: {supply_result.get('error', 'Unknown error')}"
|
|
137
|
+
)
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
# Validate that our CUPS exists
|
|
141
|
+
if not await self._supply_service.validate_cups(self._cups):
|
|
142
|
+
_LOGGER.error(f"{self._scups}: CUPS not found in account")
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
_LOGGER.info(f"{self._scups}: CUPS validated successfully")
|
|
146
|
+
|
|
147
|
+
# Get supply information
|
|
148
|
+
supply = await self._supply_service.get_supply_by_cups(self._cups)
|
|
149
|
+
if not supply:
|
|
150
|
+
_LOGGER.error(f"{self._scups}: Could not retrieve supply details")
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
distributor_code = supply.distributor_code
|
|
154
|
+
point_type = supply.point_type
|
|
155
|
+
|
|
156
|
+
_LOGGER.info(
|
|
157
|
+
f"{self._scups}: Supply dates from {supply.date_start.date()} to {supply.date_end.date()}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Adjust date range to supply validity period
|
|
161
|
+
effective_start = max(date_from, supply.date_start)
|
|
162
|
+
effective_end = min(date_to, supply.date_end)
|
|
163
|
+
|
|
164
|
+
# Step 2: Update contracts
|
|
165
|
+
_LOGGER.info(f"{self._scups}: Updating contracts")
|
|
166
|
+
contract_result = await self._contract_service.update_contracts(
|
|
167
|
+
cups=self._cups,
|
|
168
|
+
distributor_code=distributor_code,
|
|
169
|
+
authorized_nif=self._authorized_nif,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if not contract_result["success"]:
|
|
173
|
+
_LOGGER.warning(
|
|
174
|
+
f"{self._scups}: Contract update failed: {contract_result.get('error', 'Unknown error')}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Step 3: Update consumptions in monthly chunks
|
|
178
|
+
_LOGGER.info(f"{self._scups}: Updating consumptions")
|
|
179
|
+
consumption_result = (
|
|
180
|
+
await self._consumption_service.update_consumption_range_by_months(
|
|
181
|
+
cups=self._cups,
|
|
182
|
+
distributor_code=distributor_code,
|
|
183
|
+
start_date=effective_start,
|
|
184
|
+
end_date=effective_end,
|
|
185
|
+
measurement_type="0",
|
|
186
|
+
point_type=point_type,
|
|
187
|
+
authorized_nif=self._authorized_nif,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if not consumption_result["success"]:
|
|
192
|
+
_LOGGER.warning(f"{self._scups}: Consumption update failed")
|
|
193
|
+
|
|
194
|
+
# Step 4: Update maximeter data
|
|
195
|
+
_LOGGER.info(f"{self._scups}: Updating maximeter")
|
|
196
|
+
maximeter_result = (
|
|
197
|
+
await self._maximeter_service.update_maxpower_range_by_months(
|
|
198
|
+
cups=self._cups,
|
|
199
|
+
distributor_code=distributor_code,
|
|
200
|
+
start_date=effective_start,
|
|
201
|
+
end_date=effective_end,
|
|
202
|
+
authorized_nif=self._authorized_nif,
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if not maximeter_result["success"]:
|
|
207
|
+
_LOGGER.warning(f"{self._scups}: Maximeter update failed")
|
|
208
|
+
|
|
209
|
+
# Step 5: Update PVPC prices if needed
|
|
210
|
+
if self.enable_billing and self.is_pvpc:
|
|
211
|
+
_LOGGER.info(f"{self._scups}: Updating PVPC prices")
|
|
212
|
+
try:
|
|
213
|
+
pvpc_result = await self._billing_service.update_pvpc_prices(
|
|
214
|
+
start_date=effective_start,
|
|
215
|
+
end_date=effective_end,
|
|
216
|
+
is_ceuta_melilla=False, # Default to Peninsula
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if not pvpc_result["success"]:
|
|
220
|
+
_LOGGER.warning(
|
|
221
|
+
f"{self._scups}: PVPC price update failed: {pvpc_result.get('error', 'Unknown error')}"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
_LOGGER.warning(
|
|
226
|
+
f"{self._scups}: PVPC price update failed with exception: {str(e)}"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Step 6: Update billing costs if pricing rules are defined
|
|
230
|
+
if self.enable_billing and self.pricing_rules:
|
|
231
|
+
_LOGGER.info(f"{self._scups}: Updating billing costs")
|
|
232
|
+
try:
|
|
233
|
+
billing_result = await self._billing_service.update_missing_costs(
|
|
234
|
+
cups=self._cups,
|
|
235
|
+
pricing_rules=self.pricing_rules,
|
|
236
|
+
start_date=effective_start,
|
|
237
|
+
end_date=effective_end,
|
|
238
|
+
is_ceuta_melilla=False,
|
|
239
|
+
force_recalculate=False,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if not billing_result["success"]:
|
|
243
|
+
_LOGGER.warning(
|
|
244
|
+
f"{self._scups}: Billing cost update failed: {billing_result.get('error', 'Unknown error')}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
except Exception as e:
|
|
248
|
+
_LOGGER.warning(
|
|
249
|
+
f"{self._scups}: Billing cost update failed with exception: {str(e)}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Step 7: Calculate summary attributes
|
|
253
|
+
_LOGGER.info(f"{self._scups}: Calculating summary attributes")
|
|
254
|
+
await self._calculate_summary_attributes()
|
|
255
|
+
|
|
256
|
+
_LOGGER.info(f"{self._scups}: Update completed successfully")
|
|
257
|
+
return True
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
_LOGGER.error(f"{self._scups}: Update failed with exception: {str(e)}")
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
async def _calculate_summary_attributes(self):
|
|
264
|
+
"""Calculate summary attributes from all services."""
|
|
265
|
+
|
|
266
|
+
# Reset all attributes
|
|
267
|
+
for attr in ATTRIBUTES:
|
|
268
|
+
self.summary[attr] = None
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
# Get supply summary
|
|
272
|
+
supply_summary = await self._supply_service.get_supply_summary(self._cups)
|
|
273
|
+
self.summary.update(supply_summary)
|
|
274
|
+
|
|
275
|
+
# Get contract summary
|
|
276
|
+
contract_summary = await self._contract_service.get_contract_summary(
|
|
277
|
+
self._cups
|
|
278
|
+
)
|
|
279
|
+
self.summary.update(contract_summary)
|
|
280
|
+
|
|
281
|
+
# Get consumption summary
|
|
282
|
+
consumption_summary = (
|
|
283
|
+
await self._consumption_service.get_consumption_summary(self._cups)
|
|
284
|
+
)
|
|
285
|
+
self.summary.update(consumption_summary)
|
|
286
|
+
|
|
287
|
+
# Get maximeter summary
|
|
288
|
+
maximeter_summary = await self._maximeter_service.get_maximeter_summary(
|
|
289
|
+
self._cups
|
|
290
|
+
)
|
|
291
|
+
self.summary.update(maximeter_summary)
|
|
292
|
+
|
|
293
|
+
# Get billing summary if enabled
|
|
294
|
+
if self.enable_billing and self.pricing_rules and self._billing_service:
|
|
295
|
+
billing_summary = await self._billing_service.get_billing_summary(
|
|
296
|
+
cups=self._cups,
|
|
297
|
+
pricing_rules=self.pricing_rules,
|
|
298
|
+
is_ceuta_melilla=False,
|
|
299
|
+
)
|
|
300
|
+
self.summary.update(billing_summary)
|
|
301
|
+
|
|
302
|
+
# Round numeric values to 2 decimal places for consistency
|
|
303
|
+
for key, value in self.summary.items():
|
|
304
|
+
if isinstance(value, float):
|
|
305
|
+
self.summary[key] = round(value, 2)
|
|
306
|
+
|
|
307
|
+
_LOGGER.debug(f"{self._scups}: Summary attributes calculated successfully")
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
_LOGGER.error(
|
|
311
|
+
f"{self._scups}: Error calculating summary attributes: {str(e)}"
|
|
312
|
+
)
|
edata/tests/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Datadis connector module testing."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from unittest.mock import AsyncMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from edata.connectors.datadis import DatadisConnector
|
|
9
|
+
|
|
10
|
+
MOCK_USERNAME = "fake_user"
|
|
11
|
+
MOCK_PASSWORD = "fake_password"
|
|
12
|
+
|
|
13
|
+
SUPPLIES_RESPONSE = [
|
|
14
|
+
{
|
|
15
|
+
"cups": "ESXXXXXXXXXXXXXXXXTEST",
|
|
16
|
+
"validDateFrom": "2022/01/01",
|
|
17
|
+
"validDateTo": "",
|
|
18
|
+
"pointType": 5,
|
|
19
|
+
"distributorCode": "2",
|
|
20
|
+
"address": "fake address, fake 12345",
|
|
21
|
+
"postalCode": "12345",
|
|
22
|
+
"province": "FAKE PROVINCE",
|
|
23
|
+
"municipality": "FAKE MUNICIPALITY",
|
|
24
|
+
"distributor": "FAKE DISTRIBUTOR",
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
SUPPLIES_EXPECTATIONS = [
|
|
29
|
+
{
|
|
30
|
+
"cups": "ESXXXXXXXXXXXXXXXXTEST",
|
|
31
|
+
"date_start": datetime.datetime(2022, 1, 1, 0, 0),
|
|
32
|
+
"date_end": datetime.datetime.now().replace(
|
|
33
|
+
hour=0, minute=0, second=0, microsecond=0
|
|
34
|
+
)
|
|
35
|
+
+ datetime.timedelta(days=1),
|
|
36
|
+
"point_type": 5,
|
|
37
|
+
"distributor_code": "2",
|
|
38
|
+
"address": "fake address, fake 12345",
|
|
39
|
+
"postal_code": "12345",
|
|
40
|
+
"province": "FAKE PROVINCE",
|
|
41
|
+
"municipality": "FAKE MUNICIPALITY",
|
|
42
|
+
"distributor": "FAKE DISTRIBUTOR",
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
CONTRACTS_RESPONSE = [
|
|
47
|
+
{
|
|
48
|
+
"startDate": "2022/10/22",
|
|
49
|
+
"endDate": "2022/10/22",
|
|
50
|
+
"marketer": "fake_marketer",
|
|
51
|
+
"contractedPowerkW": [1.5, 1.5],
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
CONTRACTS_EXPECTATIONS = [
|
|
56
|
+
{
|
|
57
|
+
"date_start": datetime.datetime(2022, 10, 22, 0, 0),
|
|
58
|
+
"date_end": datetime.datetime(2022, 10, 22, 0, 0),
|
|
59
|
+
"marketer": "fake_marketer",
|
|
60
|
+
"distributor_code": "2",
|
|
61
|
+
"power_p1": 1.5,
|
|
62
|
+
"power_p2": 1.5,
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
CONSUMPTIONS_RESPONSE = [
|
|
67
|
+
{
|
|
68
|
+
"cups": "ESXXXXXXXXXXXXXXXXTEST",
|
|
69
|
+
"date": "2022/10/22",
|
|
70
|
+
"time": "01:00",
|
|
71
|
+
"consumptionKWh": 1.0,
|
|
72
|
+
"obtainMethod": "Real",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"cups": "ESXXXXXXXXXXXXXXXXTEST",
|
|
76
|
+
"date": "2022/10/22",
|
|
77
|
+
"time": "02:00",
|
|
78
|
+
"consumptionKWh": 1.0,
|
|
79
|
+
"obtainMethod": "Real",
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
CONSUMPTIONS_EXPECTATIONS = [
|
|
84
|
+
{
|
|
85
|
+
"datetime": datetime.datetime(2022, 10, 22, 0, 0),
|
|
86
|
+
"delta_h": 1,
|
|
87
|
+
"value_kwh": 1.0,
|
|
88
|
+
"surplus_kwh": 0,
|
|
89
|
+
"real": True,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"datetime": datetime.datetime(2022, 10, 22, 1, 0),
|
|
93
|
+
"delta_h": 1,
|
|
94
|
+
"value_kwh": 1.0,
|
|
95
|
+
"surplus_kwh": 0,
|
|
96
|
+
"real": True,
|
|
97
|
+
},
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
MAXIMETER_RESPONSE = [
|
|
101
|
+
{
|
|
102
|
+
"cups": "ESXXXXXXXXXXXXXXXXTEST",
|
|
103
|
+
"date": "2022/03/01",
|
|
104
|
+
"time": "12:00",
|
|
105
|
+
"maxPower": 1.0,
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
MAXIMETER_EXPECTATIONS = [
|
|
110
|
+
{
|
|
111
|
+
"datetime": datetime.datetime(2022, 3, 1, 12, 0),
|
|
112
|
+
"value_kw": 1.0,
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Tests for async methods (now the only methods available)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
@patch.object(DatadisConnector, "_get_token", AsyncMock(return_value=True))
|
|
122
|
+
@patch.object(DatadisConnector, "_get", AsyncMock(return_value=SUPPLIES_RESPONSE))
|
|
123
|
+
async def test_get_supplies():
|
|
124
|
+
"""Test a successful 'get_supplies' query."""
|
|
125
|
+
connector = DatadisConnector(MOCK_USERNAME, MOCK_PASSWORD)
|
|
126
|
+
result = await connector.get_supplies()
|
|
127
|
+
# Note: Now returns Pydantic models instead of dicts
|
|
128
|
+
# Convert to dicts for comparison with expectations
|
|
129
|
+
result_dicts = [supply.model_dump() for supply in result]
|
|
130
|
+
assert result_dicts == SUPPLIES_EXPECTATIONS
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.mark.asyncio
|
|
134
|
+
@patch.object(DatadisConnector, "_get_token", AsyncMock(return_value=True))
|
|
135
|
+
@patch.object(DatadisConnector, "_get", AsyncMock(return_value=CONTRACTS_RESPONSE))
|
|
136
|
+
async def test_get_contract_detail():
|
|
137
|
+
"""Test a successful 'get_contract_detail' query."""
|
|
138
|
+
connector = DatadisConnector(MOCK_USERNAME, MOCK_PASSWORD)
|
|
139
|
+
result = await connector.get_contract_detail("ESXXXXXXXXXXXXXXXXTEST", "2")
|
|
140
|
+
# Note: Now returns Pydantic models instead of dicts
|
|
141
|
+
result_dicts = [contract.model_dump() for contract in result]
|
|
142
|
+
assert result_dicts == CONTRACTS_EXPECTATIONS
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@pytest.mark.asyncio
|
|
146
|
+
@patch.object(DatadisConnector, "_get_token", AsyncMock(return_value=True))
|
|
147
|
+
@patch.object(DatadisConnector, "_get", AsyncMock(return_value=CONSUMPTIONS_RESPONSE))
|
|
148
|
+
async def test_get_consumption_data():
|
|
149
|
+
"""Test a successful 'get_consumption_data' query."""
|
|
150
|
+
connector = DatadisConnector(MOCK_USERNAME, MOCK_PASSWORD)
|
|
151
|
+
result = await connector.get_consumption_data(
|
|
152
|
+
"ESXXXXXXXXXXXXXXXXTEST",
|
|
153
|
+
"2",
|
|
154
|
+
datetime.datetime(2022, 10, 22, 0, 0, 0),
|
|
155
|
+
datetime.datetime(2022, 10, 22, 2, 0, 0),
|
|
156
|
+
"0", # measurement_type as string
|
|
157
|
+
5,
|
|
158
|
+
)
|
|
159
|
+
# Note: Now returns Pydantic models instead of dicts
|
|
160
|
+
result_dicts = [consumption.model_dump() for consumption in result]
|
|
161
|
+
assert result_dicts == CONSUMPTIONS_EXPECTATIONS
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@pytest.mark.asyncio
|
|
165
|
+
@patch.object(DatadisConnector, "_get_token", AsyncMock(return_value=True))
|
|
166
|
+
@patch.object(DatadisConnector, "_get", AsyncMock(return_value=MAXIMETER_RESPONSE))
|
|
167
|
+
async def test_get_max_power():
|
|
168
|
+
"""Test a successful 'get_max_power' query."""
|
|
169
|
+
connector = DatadisConnector(MOCK_USERNAME, MOCK_PASSWORD)
|
|
170
|
+
result = await connector.get_max_power(
|
|
171
|
+
"ESXXXXXXXXXXXXXXXXTEST",
|
|
172
|
+
"2",
|
|
173
|
+
datetime.datetime(2022, 3, 1, 0, 0, 0),
|
|
174
|
+
datetime.datetime(2022, 4, 1, 0, 0, 0),
|
|
175
|
+
)
|
|
176
|
+
# Note: Now returns Pydantic models instead of dicts
|
|
177
|
+
result_dicts = [maxpower.model_dump() for maxpower in result]
|
|
178
|
+
assert result_dicts == MAXIMETER_EXPECTATIONS
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Tests for REData (online)"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from edata.connectors.redata import REDataConnector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.asyncio
|
|
11
|
+
async def test_get_realtime_prices():
|
|
12
|
+
"""Test a successful 'get_realtime_prices' query"""
|
|
13
|
+
connector = REDataConnector()
|
|
14
|
+
yesterday = datetime.now().replace(hour=0, minute=0, second=0) - timedelta(days=1)
|
|
15
|
+
response = await connector.get_realtime_prices(
|
|
16
|
+
yesterday, yesterday + timedelta(days=1) - timedelta(minutes=1), False
|
|
17
|
+
)
|
|
18
|
+
assert len(response) == 24
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.asyncio
|
|
22
|
+
async def test_async_get_realtime_prices():
|
|
23
|
+
"""Test a successful 'get_realtime_prices' query (legacy test name)"""
|
|
24
|
+
connector = REDataConnector()
|
|
25
|
+
yesterday = datetime.now().replace(hour=0, minute=0, second=0) - timedelta(days=1)
|
|
26
|
+
response = await connector.get_realtime_prices(
|
|
27
|
+
yesterday, yesterday + timedelta(days=1) - timedelta(minutes=1), False
|
|
28
|
+
)
|
|
29
|
+
assert len(response) == 24
|
|
File without changes
|