reports.virtualmetercomparison   F
last analyzed

Complexity

Total Complexity 75

Size/Duplication

Total Lines 428
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 75
eloc 294
dl 0
loc 428
rs 2.4
c 0
b 0
f 0

3 Methods

Rating   Name   Duplication   Size   Complexity  
A Reporting.__init__() 0 3 1
A Reporting.on_options() 0 4 1
F Reporting.on_get() 0 365 73

How to fix   Complexity   

Complexity

Complex classes like reports.virtualmetercomparison often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""
2
Virtual Meter Comparison Report API
3
4
This module provides REST API endpoints for generating virtual meter comparison reports.
5
It compares energy consumption data between different virtual meters or time periods,
6
providing insights into performance differences and optimization opportunities.
7
8
Key Features:
9
- Virtual Meter-to-Virtual Meter comparison analysis
10
- Time period comparison
11
- Performance difference identification
12
- Comparative metrics calculation
13
- Excel export functionality
14
- Benchmarking insights
15
16
Report Components:
17
- Virtual Meter comparison summary
18
- Performance difference analysis
19
- Comparative metrics and KPIs
20
- Benchmarking data
21
- Performance gap identification
22
- Optimization recommendations
23
24
The module uses Falcon framework for REST API and includes:
25
- Database queries for comparison data
26
- Comparative analysis algorithms
27
- Performance benchmarking tools
28
- Excel export via excelexporters
29
- Multi-language support
30
- User authentication and authorization
31
"""
32
33
import re
34
from datetime import datetime, timedelta, timezone
35
from decimal import Decimal
36
import falcon
37
import mysql.connector
38
import simplejson as json
39
import config
40
import excelexporters.virtualmetercomparison  # 对应虚拟表Excel导出模块
41
from core import utilities
42
from core.useractivity import access_control, api_key_control
43
44
45
class Reporting:
46
    def __init__(self):
47
        """"Initializes Reporting"""
48
        pass
49
50
    @staticmethod
51
    def on_options(req, resp):
52
        _ = req
53
        resp.status = falcon.HTTP_200
54
55
    ####################################################################################################################
56
    # PROCEDURES
57
    # Step 1: valid parameters
58
    # Step 2: query the virtual meter and energy category
59
    # Step 3: parse equation and get associated points
60
    # Step 4: query reporting period energy consumption
61
    # Step 5: construct the report
62
    ####################################################################################################################
63
    @staticmethod
64
    def on_get(req, resp):
65
        if 'API-KEY' not in req.headers or \
66
                not isinstance(req.headers['API-KEY'], str) or \
67
                len(str.strip(req.headers['API-KEY'])) == 0:
68
            access_control(req)
69
        else:
70
            api_key_control(req)
71
        print(req.params)
72
        # this procedure accepts virtual meter id or virtual meter uuid to identify a virtual meter
73
        virtual_meter_id1 = req.params.get('virtualmeterid1')
74
        virtual_meter_uuid1 = req.params.get('virtualmeteruuid1')
75
        virtual_meter_id2 = req.params.get('virtualmeterid2')
76
        virtual_meter_uuid2 = req.params.get('virtualmeteruuid2')
77
        period_type = req.params.get('periodtype')
78
        reporting_period_start_datetime_local = req.params.get('reportingperiodstartdatetime')
79
        reporting_period_end_datetime_local = req.params.get('reportingperiodenddatetime')
80
        language = req.params.get('language')
81
        quick_mode = req.params.get('quickmode')
82
83
        ################################################################################################################
84
        # Step 1: valid parameters
85
        ################################################################################################################
86
        if virtual_meter_id1 is None and virtual_meter_uuid1 is None:
87
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST', description='API.INVALID_VIRTUAL_METER_ID')
88
89
        if virtual_meter_id1 is not None:
90
            virtual_meter_id1 = str.strip(virtual_meter_id1)
91
            if not virtual_meter_id1.isdigit() or int(virtual_meter_id1) <= 0:
92
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
93
                                       description='API.INVALID_VIRTUAL_METER_ID')
94
95
        if virtual_meter_uuid1 is not None:
96
            regex = re.compile(r'^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}\Z', re.I)
97
            match = regex.match(str.strip(virtual_meter_uuid1))
98
            if not bool(match):
99
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
100
                                       description='API.INVALID_VIRTUAL_METER_UUID')
101
102
        if virtual_meter_id2 is None and virtual_meter_uuid2 is None:
103
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
104
                                   description='API.INVALID_VIRTUAL_METER_ID')
105
106
        if virtual_meter_id2 is not None:
107
            virtual_meter_id2 = str.strip(virtual_meter_id2)
108
            if not virtual_meter_id2.isdigit() or int(virtual_meter_id2) <= 0:
109
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
110
                                       description='API.INVALID_VIRTUAL_METER_ID')
111
112
        if virtual_meter_uuid2 is not None:
113
            regex = re.compile(r'^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}\Z', re.I)
114
            match = regex.match(str.strip(virtual_meter_uuid2))
115
            if not bool(match):
116
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
117
                                       description='API.INVALID_VIRTUAL_METER_UUID')
118
119
        if period_type is None:
120
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
121
                                   description='API.INVALID_PERIOD_TYPE')
122
        else:
123
            period_type = str.strip(period_type)
124
            if period_type not in ['hourly', 'daily', 'weekly', 'monthly', 'yearly']:
125
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
126
                                       description='API.INVALID_PERIOD_TYPE')
127
128
        timezone_offset = int(config.utc_offset[1:3]) * 60 + int(config.utc_offset[4:6])
129
        if config.utc_offset[0] == '-':
130
            timezone_offset = -timezone_offset
131
132
        if reporting_period_start_datetime_local is None:
133
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
134
                                   description="API.INVALID_REPORTING_PERIOD_START_DATETIME")
135
        else:
136
            reporting_period_start_datetime_local = str.strip(reporting_period_start_datetime_local)
137
            try:
138
                reporting_start_datetime_utc = datetime.strptime(reporting_period_start_datetime_local,
139
                                                                 '%Y-%m-%dT%H:%M:%S')
140
            except ValueError:
141
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
142
                                       description="API.INVALID_REPORTING_PERIOD_START_DATETIME")
143
            reporting_start_datetime_utc = \
144
                reporting_start_datetime_utc.replace(tzinfo=timezone.utc) - timedelta(minutes=timezone_offset)
145
            # nomalize the start datetime
146
            if config.minutes_to_count == 30 and reporting_start_datetime_utc.minute >= 30:
147
                reporting_start_datetime_utc = reporting_start_datetime_utc.replace(minute=30, second=0, microsecond=0)
148
            else:
149
                reporting_start_datetime_utc = reporting_start_datetime_utc.replace(minute=0, second=0, microsecond=0)
150
151
        if reporting_period_end_datetime_local is None:
152
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
153
                                   description="API.INVALID_REPORTING_PERIOD_END_DATETIME")
154
        else:
155
            reporting_period_end_datetime_local = str.strip(reporting_period_end_datetime_local)
156
            try:
157
                reporting_end_datetime_utc = datetime.strptime(reporting_period_end_datetime_local,
158
                                                               '%Y-%m-%dT%H:%M:%S')
159
            except ValueError:
160
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
161
                                       description="API.INVALID_REPORTING_PERIOD_END_DATETIME")
162
            reporting_end_datetime_utc = reporting_end_datetime_utc.replace(tzinfo=timezone.utc) - \
163
                timedelta(minutes=timezone_offset)
164
165
        if reporting_start_datetime_utc >= reporting_end_datetime_utc:
166
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
167
                                   description='API.INVALID_REPORTING_PERIOD_END_DATETIME')
168
169
        # if turn quick mode on, do not return parameters data and excel file
170
        is_quick_mode = False
171
        if quick_mode is not None and \
172
                len(str.strip(quick_mode)) > 0 and \
173
                str.lower(str.strip(quick_mode)) in ('true', 't', 'on', 'yes', 'y'):
174
            is_quick_mode = True
175
176
        ################################################################################################################
177
        # Step 2: query the virtual meter and energy category
178
        ################################################################################################################
179
        cnx_system = mysql.connector.connect(**config.myems_system_db)
180
        cursor_system = cnx_system.cursor()
181
182
        cnx_energy = mysql.connector.connect(** config.myems_energy_db)
183
        cursor_energy = cnx_energy.cursor()
184
185
        cnx_historical = mysql.connector.connect(**config.myems_historical_db)
186
        cursor_historical = cnx_historical.cursor()
187
188
        if virtual_meter_id1 is not None:
189
            cursor_system.execute(" SELECT vm.id, vm.name, vm.energy_category_id, ec.name, ec.unit_of_measure, vm.equation "
190
                                  " FROM tbl_virtual_meters vm, tbl_energy_categories ec "
191
                                  " WHERE vm.id = %s AND vm.energy_category_id = ec.id ", (virtual_meter_id1,))
192
            row_virtual_meter1 = cursor_system.fetchone()
193
        elif virtual_meter_uuid1 is not None:
194
            cursor_system.execute(" SELECT vm.id, vm.name, vm.energy_category_id, ec.name, ec.unit_of_measure, vm.equation "
195
                                  " FROM tbl_virtual_meters vm, tbl_energy_categories ec "
196
                                  " WHERE vm.uuid = %s AND vm.energy_category_id = ec.id ", (virtual_meter_uuid1,))
197
            row_virtual_meter1 = cursor_system.fetchone()
198
199
        if row_virtual_meter1 is None:
0 ignored issues
show
introduced by
The variable row_virtual_meter1 does not seem to be defined for all execution paths.
Loading history...
200
            if cursor_system:
201
                cursor_system.close()
202
            if cnx_system:
203
                cnx_system.close()
204
205
            if cursor_energy:
206
                cursor_energy.close()
207
            if cnx_energy:
208
                cnx_energy.close()
209
210
            if cursor_historical:
211
                cursor_historical.close()
212
            if cnx_historical:
213
                cnx_historical.close()
214
            raise falcon.HTTPError(status=falcon.HTTP_404, title='API.NOT_FOUND', description='API.VIRTUAL_METER_NOT_FOUND')
215
216
        virtual_meter1 = dict()
217
        virtual_meter1['id'] = row_virtual_meter1[0]
218
        virtual_meter1['name'] = row_virtual_meter1[1]
219
        virtual_meter1['energy_category_id'] = row_virtual_meter1[2]
220
        virtual_meter1['energy_category_name'] = row_virtual_meter1[3]
221
        virtual_meter1['unit_of_measure'] = row_virtual_meter1[4]
222
        virtual_meter1['equation'] = row_virtual_meter1[5]  
223
224
        if virtual_meter_id2 is not None:
225
            cursor_system.execute(" SELECT vm.id, vm.name, vm.energy_category_id, ec.name, ec.unit_of_measure, vm.equation "
226
                                  " FROM tbl_virtual_meters vm, tbl_energy_categories ec "
227
                                  " WHERE vm.id = %s AND vm.energy_category_id = ec.id ", (virtual_meter_id2,))
228
            row_virtual_meter2 = cursor_system.fetchone()
229
        elif virtual_meter_uuid2 is not None:
230
            cursor_system.execute(" SELECT vm.id, vm.name, vm.energy_category_id, ec.name, ec.unit_of_measure, vm.equation "
231
                                  " FROM tbl_virtual_meters vm, tbl_energy_categories ec "
232
                                  " WHERE vm.uuid = %s AND vm.energy_category_id = ec.id ", (virtual_meter_uuid2,))
233
            row_virtual_meter2 = cursor_system.fetchone()
234
235
        if row_virtual_meter2 is None:
0 ignored issues
show
introduced by
The variable row_virtual_meter2 does not seem to be defined for all execution paths.
Loading history...
236
            if cursor_system:
237
                cursor_system.close()
238
            if cnx_system:
239
                cnx_system.close()
240
241
            if cursor_energy:
242
                cursor_energy.close()
243
            if cnx_energy:
244
                cnx_energy.close()
245
246
            if cursor_historical:
247
                cursor_historical.close()
248
            if cnx_historical:
249
                cnx_historical.close()
250
            raise falcon.HTTPError(status=falcon.HTTP_404, title='API.NOT_FOUND', description='API.VIRTUAL_METER_NOT_FOUND')
251
252
        virtual_meter2 = dict()
253
        virtual_meter2['id'] = row_virtual_meter2[0]
254
        virtual_meter2['name'] = row_virtual_meter2[1]
255
        virtual_meter2['energy_category_id'] = row_virtual_meter2[2]
256
        virtual_meter2['energy_category_name'] = row_virtual_meter2[3]
257
        virtual_meter2['unit_of_measure'] = row_virtual_meter2[4]
258
        virtual_meter2['equation'] = row_virtual_meter2[5]  
259
260
        ################################################################################################################
261
        # Step 3: parse equation and get associated points 
262
        ################################################################################################################
263
        def parse_equation_and_get_points(equation, cursor_system):
264
            if not equation:
265
                return []
266
            variables = re.findall(r'x\d+', equation)  
267
            if not variables:
268
                return []
269
            unique_vars = list(set(variables))
270
           
271
            placeholders = ', '.join(['%s'] * len(unique_vars))
272
            cursor_system.execute(f" SELECT p.id, p.name, p.units, p.object_type "
273
                                  f" FROM tbl_points p "
274
                                  f" WHERE p.name IN ({placeholders}) ", tuple(unique_vars))
275
            rows = cursor_system.fetchall()
276
            return [{"id": row[0], "name": row[1], "units": row[2], "object_type": row[3]} for row in rows]
277
278
        point_list1 = parse_equation_and_get_points(virtual_meter1['equation'], cursor_system)
279
        point_list2 = parse_equation_and_get_points(virtual_meter2['equation'], cursor_system)
280
281
        ################################################################################################################
282
        # Step 4: query reporting period energy consumption
283
        ################################################################################################################
284
        query1 = (" SELECT start_datetime_utc, actual_value "
285
                  " FROM tbl_virtual_meter_hourly "
286
                  " WHERE virtual_meter_id = %s "
287
                  " AND start_datetime_utc >= %s "
288
                  " AND start_datetime_utc < %s "
289
                  " ORDER BY start_datetime_utc ")
290
        cursor_energy.execute(query1, (virtual_meter1['id'], reporting_start_datetime_utc, reporting_end_datetime_utc))
291
        rows_virtual_meter1_hourly = cursor_energy.fetchall()
292
293
        rows_virtual_meter1_periodically = utilities.aggregate_hourly_data_by_period(rows_virtual_meter1_hourly,
294
                                                                                     reporting_start_datetime_utc,
295
                                                                                     reporting_end_datetime_utc,
296
                                                                                     period_type)
297
        reporting1 = dict()
298
        reporting1['timestamps'] = list()
299
        reporting1['values'] = list()
300
        reporting1['total_in_category'] = Decimal(0.0)
301
302
        for row_virtual_meter1_periodically in rows_virtual_meter1_periodically:
303
            current_datetime_local = row_virtual_meter1_periodically[0].replace(tzinfo=timezone.utc) + \
304
                timedelta(minutes=timezone_offset)
305
            if period_type == 'hourly':
306
                current_datetime = current_datetime_local.isoformat()[0:19]
307
            elif period_type == 'daily':
308
                current_datetime = current_datetime_local.isoformat()[0:10]
309
            elif period_type == 'weekly':
310
                current_datetime = current_datetime_local.isoformat()[0:10]
311
            elif period_type == 'monthly':
312
                current_datetime = current_datetime_local.isoformat()[0:7]
313
            elif period_type == 'yearly':
314
                current_datetime = current_datetime_local.isoformat()[0:4]
315
316
            actual_value = Decimal(0.0) if row_virtual_meter1_periodically[1] is None else row_virtual_meter1_periodically[1]
317
318
            reporting1['timestamps'].append(current_datetime)
0 ignored issues
show
introduced by
The variable current_datetime does not seem to be defined for all execution paths.
Loading history...
319
            reporting1['values'].append(actual_value)
320
            reporting1['total_in_category'] += actual_value
321
322
        query2 = (" SELECT start_datetime_utc, actual_value "
323
                  " FROM tbl_virtual_meter_hourly "
324
                  " WHERE virtual_meter_id = %s "
325
                  " AND start_datetime_utc >= %s "
326
                  " AND start_datetime_utc < %s "
327
                  " ORDER BY start_datetime_utc ")
328
        cursor_energy.execute(query2, (virtual_meter2['id'], reporting_start_datetime_utc, reporting_end_datetime_utc))
329
        rows_virtual_meter2_hourly = cursor_energy.fetchall()
330
331
        rows_virtual_meter2_periodically = utilities.aggregate_hourly_data_by_period(rows_virtual_meter2_hourly,
332
                                                                                     reporting_start_datetime_utc,
333
                                                                                     reporting_end_datetime_utc,
334
                                                                                     period_type)
335
        reporting2 = dict()
336
        diff = dict()
337
        reporting2['timestamps'] = list()
338
        reporting2['values'] = list()
339
        reporting2['total_in_category'] = Decimal(0.0)
340
        diff['values'] = list()
341
        diff['total_in_category'] = Decimal(0.0)
342
343
        for row_virtual_meter2_periodically in rows_virtual_meter2_periodically:
344
            current_datetime_local = row_virtual_meter2_periodically[0].replace(tzinfo=timezone.utc) + \
345
                                     timedelta(minutes=timezone_offset)
346
            if period_type == 'hourly':
347
                current_datetime = current_datetime_local.isoformat()[0:19]
348
            elif period_type == 'daily':
349
                current_datetime = current_datetime_local.isoformat()[0:10]
350
            elif period_type == 'weekly':
351
                current_datetime = current_datetime_local.isoformat()[0:10]
352
            elif period_type == 'monthly':
353
                current_datetime = current_datetime_local.isoformat()[0:7]
354
            elif period_type == 'yearly':
355
                current_datetime = current_datetime_local.isoformat()[0:4]
356
357
            actual_value = Decimal(0.0) if row_virtual_meter2_periodically[1] is None else row_virtual_meter2_periodically[1]
358
359
            reporting2['timestamps'].append(current_datetime)
360
            reporting2['values'].append(actual_value)
361
            reporting2['total_in_category'] += actual_value
362
363
        for virtual_meter1_value, virtual_meter2_value in zip(reporting1['values'], reporting2['values']):
364
            diff['values'].append(virtual_meter1_value - virtual_meter2_value)
365
            diff['total_in_category'] += virtual_meter1_value - virtual_meter2_value
366
367
        ################################################################################################################
368
        # Step 5: construct the report
369
        ################################################################################################################
370
        if cursor_system:
371
            cursor_system.close()
372
        if cnx_system:
373
            cnx_system.close()
374
375
        if cursor_energy:
376
            cursor_energy.close()
377
        if cnx_energy:
378
            cnx_energy.close()
379
380
        if cursor_historical:
381
            cursor_historical.close()
382
        if cnx_historical:
383
            cnx_historical.close()
384
385
        result = {
386
            "virtualmeter1": {
387
                "name": virtual_meter1['name'],
388
                "energy_category_id": virtual_meter1['energy_category_id'],
389
                "energy_category_name": virtual_meter1['energy_category_name'],
390
                "unit_of_measure": virtual_meter1['unit_of_measure'],
391
                "equation": virtual_meter1['equation']  # 新增:返回方程式
392
            },
393
            "reporting_period1": {
394
                "total_in_category": reporting1['total_in_category'],
395
                "timestamps": reporting1['timestamps'],
396
                "values": reporting1['values'],
397
            },
398
            "virtualmeter2": {
399
                "name": virtual_meter2['name'],
400
                "energy_category_id": virtual_meter2['energy_category_id'],
401
                "energy_category_name": virtual_meter2['energy_category_name'],
402
                "unit_of_measure": virtual_meter2['unit_of_measure'],
403
                "equation": virtual_meter2['equation'] 
404
            },
405
            "reporting_period2": {
406
                "total_in_category": reporting2['total_in_category'],
407
                "timestamps": reporting2['timestamps'],
408
                "values": reporting2['values'],
409
            },
410
411
            "diff": {
412
                "values": diff['values'],
413
                "total_in_category": diff['total_in_category'],
414
            }
415
        }
416
        # export result to Excel file and then encode the file to base64 string
417
        if not is_quick_mode:
418
            result['excel_bytes_base64'] = \
419
                excelexporters.virtualmetercomparison.export(result,
420
                                                             virtual_meter1['name'],
421
                                                             virtual_meter2['name'],
422
                                                             reporting_period_start_datetime_local,
423
                                                             reporting_period_end_datetime_local,
424
                                                             period_type,
425
                                                             language)
426
427
        resp.text = json.dumps(result)
428