reports.metertrend.Reporting.on_get()   F
last analyzed

Complexity

Conditions 55

Size

Total Lines 283
Code Lines 208

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 208
dl 0
loc 283
rs 0
c 0
b 0
f 0
cc 55
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like reports.metertrend.Reporting.on_get() 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
Meter Trend Report API
3
4
This module provides REST API endpoints for generating meter trend reports.
5
It analyzes energy consumption trends and patterns over time to provide
6
insights into usage patterns, seasonal variations, and optimization opportunities.
7
8
Key Features:
9
- Meter energy consumption trend analysis
10
- Time-series trend identification
11
- Seasonal pattern analysis
12
- Trend-based optimization insights
13
- Excel export functionality
14
- Predictive analysis capabilities
15
16
Report Components:
17
- Meter trend summary
18
- Time-series trend data
19
- Seasonal pattern analysis
20
- Trend-based predictions
21
- Optimization recommendations
22
- Pattern identification
23
24
The module uses Falcon framework for REST API and includes:
25
- Database queries for trend data
26
- Trend analysis algorithms
27
- Pattern recognition 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
import falcon
36
import mysql.connector
37
import simplejson as json
38
import config
39
import excelexporters.metertrend
40
from core import utilities
41
from core.useractivity import access_control, api_key_control
42
43
44
class Reporting:
45
    def __init__(self):
46
        """"Initializes Reporting"""
47
        pass
48
49
    @staticmethod
50
    def on_options(req, resp):
51
        _ = req
52
        resp.status = falcon.HTTP_200
53
54
    ####################################################################################################################
55
    # PROCEDURES
56
    # Step 1: valid parameters
57
    # Step 2: query the meter and energy category
58
    # Step 3: query associated points
59
    # Step 4: query reporting period points trends
60
    # Step 5: query tariff data
61
    # Step 6: 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
        meter_id = req.params.get('meterid')
73
        meter_uuid = req.params.get('meteruuid')
74
        reporting_period_start_datetime_local = req.params.get('reportingperiodstartdatetime')
75
        reporting_period_end_datetime_local = req.params.get('reportingperiodenddatetime')
76
        language = req.params.get('language')
77
        quick_mode = req.params.get('quickmode')
78
79
        ################################################################################################################
80
        # Step 1: valid parameters
81
        ################################################################################################################
82
        if meter_id is None and meter_uuid is None:
83
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST', description='API.INVALID_METER_ID')
84
85
        if meter_id is not None:
86
            meter_id = str.strip(meter_id)
87
            if not meter_id.isdigit() or int(meter_id) <= 0:
88
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
89
                                       description='API.INVALID_METER_ID')
90
91
        if meter_uuid is not None:
92
            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)
93
            match = regex.match(str.strip(meter_uuid))
94
            if not bool(match):
95
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
96
                                       description='API.INVALID_METER_UUID')
97
98
        timezone_offset = int(config.utc_offset[1:3]) * 60 + int(config.utc_offset[4:6])
99
        if config.utc_offset[0] == '-':
100
            timezone_offset = -timezone_offset
101
102
        if reporting_period_start_datetime_local is None:
103
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
104
                                   description="API.INVALID_REPORTING_PERIOD_START_DATETIME")
105
        else:
106
            reporting_period_start_datetime_local = str.strip(reporting_period_start_datetime_local)
107
            try:
108
                reporting_start_datetime_utc = datetime.strptime(reporting_period_start_datetime_local,
109
                                                                 '%Y-%m-%dT%H:%M:%S')
110
            except ValueError:
111
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
112
                                       description="API.INVALID_REPORTING_PERIOD_START_DATETIME")
113
            reporting_start_datetime_utc = \
114
                reporting_start_datetime_utc.replace(tzinfo=timezone.utc) - timedelta(minutes=timezone_offset)
115
            # nomalize the start datetime
116
            if config.minutes_to_count == 30 and reporting_start_datetime_utc.minute >= 30:
117
                reporting_start_datetime_utc = reporting_start_datetime_utc.replace(minute=30, second=0, microsecond=0)
118
            else:
119
                reporting_start_datetime_utc = reporting_start_datetime_utc.replace(minute=0, second=0, microsecond=0)
120
121
        if reporting_period_end_datetime_local is None:
122
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
123
                                   description="API.INVALID_REPORTING_PERIOD_END_DATETIME")
124
        else:
125
            reporting_period_end_datetime_local = str.strip(reporting_period_end_datetime_local)
126
            try:
127
                reporting_end_datetime_utc = datetime.strptime(reporting_period_end_datetime_local,
128
                                                               '%Y-%m-%dT%H:%M:%S')
129
            except ValueError:
130
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
131
                                       description="API.INVALID_REPORTING_PERIOD_END_DATETIME")
132
            reporting_end_datetime_utc = reporting_end_datetime_utc.replace(tzinfo=timezone.utc) - \
133
                timedelta(minutes=timezone_offset)
134
135
        if reporting_start_datetime_utc >= reporting_end_datetime_utc:
136
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
137
                                   description='API.INVALID_REPORTING_PERIOD_END_DATETIME')
138
139
        # if turn quick mode on, do not return parameters data and excel file
140
        is_quick_mode = False
141
        if quick_mode is not None and \
142
                len(str.strip(quick_mode)) > 0 and \
143
                str.lower(str.strip(quick_mode)) in ('true', 't', 'on', 'yes', 'y'):
144
            is_quick_mode = True
145
146
        trans = utilities.get_translation(language)
147
        trans.install()
148
        _ = trans.gettext
149
150
        ################################################################################################################
151
        # Step 2: query the meter and energy category
152
        ################################################################################################################
153
        cnx_system = mysql.connector.connect(**config.myems_system_db)
154
        cursor_system = cnx_system.cursor()
155
156
        cnx_historical = mysql.connector.connect(**config.myems_historical_db)
157
        cursor_historical = cnx_historical.cursor()
158
        if meter_id is not None:
159
            cursor_system.execute(" SELECT m.id, m.name, m.cost_center_id, m.energy_category_id, "
160
                                  "        ec.name, ec.unit_of_measure, ec.kgce, ec.kgco2e "
161
                                  " FROM tbl_meters m, tbl_energy_categories ec "
162
                                  " WHERE m.id = %s AND m.energy_category_id = ec.id ", (meter_id,))
163
            row_meter = cursor_system.fetchone()
164
        elif meter_uuid is not None:
165
            cursor_system.execute(" SELECT m.id, m.name, m.cost_center_id, m.energy_category_id, "
166
                                  "        ec.name, ec.unit_of_measure, ec.kgce, ec.kgco2e "
167
                                  " FROM tbl_meters m, tbl_energy_categories ec "
168
                                  " WHERE m.uuid = %s AND m.energy_category_id = ec.id ", (meter_uuid,))
169
            row_meter = cursor_system.fetchone()
170
171
        if row_meter is None:
0 ignored issues
show
introduced by
The variable row_meter does not seem to be defined for all execution paths.
Loading history...
172
            if cursor_system:
173
                cursor_system.close()
174
            if cnx_system:
175
                cnx_system.close()
176
177
            if cursor_historical:
178
                cursor_historical.close()
179
            if cnx_historical:
180
                cnx_historical.close()
181
            raise falcon.HTTPError(status=falcon.HTTP_404, title='API.NOT_FOUND', description='API.METER_NOT_FOUND')
182
        meter = dict()
183
        meter['id'] = row_meter[0]
184
        meter['name'] = row_meter[1]
185
        meter['cost_center_id'] = row_meter[2]
186
        meter['energy_category_id'] = row_meter[3]
187
        meter['energy_category_name'] = row_meter[4]
188
        meter['unit_of_measure'] = row_meter[5]
189
        meter['kgce'] = row_meter[6]
190
        meter['kgco2e'] = row_meter[7]
191
192
        ################################################################################################################
193
        # Step 3: query associated points
194
        ################################################################################################################
195
        point_list = list()
196
        cursor_system.execute(" SELECT p.id, p.name, p.units, p.object_type  "
197
                              " FROM tbl_meters m, tbl_meters_points mp, tbl_points p "
198
                              " WHERE m.id = %s AND m.id = mp.meter_id AND mp.point_id = p.id "
199
                              " ORDER BY p.id ", (meter['id'],))
200
        rows_points = cursor_system.fetchall()
201
        if rows_points is not None and len(rows_points) > 0:
202
            for row in rows_points:
203
                point_list.append({"id": row[0], "name": row[1], "units": row[2], "object_type": row[3]})
204
205
        ################################################################################################################
206
        # Step 4: query reporting period points trends
207
        ################################################################################################################
208
        reporting = dict()
209
        reporting['names'] = list()
210
        reporting['timestamps'] = list()
211
        reporting['values'] = list()
212
213
        for point in point_list:
214
            if is_quick_mode and point['object_type'] != 'ENERGY_VALUE':
215
                continue
216
217
            point_value_list = list()
218
            point_timestamp_list = list()
219
            if point['object_type'] == 'ENERGY_VALUE':
220
                query = (" SELECT utc_date_time, actual_value "
221
                         " FROM tbl_energy_value "
222
                         " WHERE point_id = %s "
223
                         "       AND utc_date_time BETWEEN %s AND %s "
224
                         " ORDER BY utc_date_time ")
225
                cursor_historical.execute(query, (point['id'],
226
                                                  reporting_start_datetime_utc,
227
                                                  reporting_end_datetime_utc))
228
                rows = cursor_historical.fetchall()
229
230
                if rows is not None and len(rows) > 0:
231
                    for row in rows:
232
                        current_datetime_local = row[0].replace(tzinfo=timezone.utc) + \
233
                                                 timedelta(minutes=timezone_offset)
234
                        current_datetime = current_datetime_local.isoformat()[0:19]
235
                        point_timestamp_list.append(current_datetime)
236
                        point_value_list.append(row[1])
237
            elif point['object_type'] == 'ANALOG_VALUE':
238
                query = (" SELECT utc_date_time, actual_value "
239
                         " FROM tbl_analog_value "
240
                         " WHERE point_id = %s "
241
                         "       AND utc_date_time BETWEEN %s AND %s "
242
                         " ORDER BY utc_date_time ")
243
                cursor_historical.execute(query, (point['id'],
244
                                                  reporting_start_datetime_utc,
245
                                                  reporting_end_datetime_utc))
246
                rows = cursor_historical.fetchall()
247
248
                if rows is not None and len(rows) > 0:
249
                    for row in rows:
250
                        current_datetime_local = row[0].replace(tzinfo=timezone.utc) + \
251
                                                 timedelta(minutes=timezone_offset)
252
                        current_datetime = current_datetime_local.isoformat()[0:19]
253
                        point_timestamp_list.append(current_datetime)
254
                        point_value_list.append(row[1])
255
            elif point['object_type'] == 'DIGITAL_VALUE':
256
                query = (" SELECT utc_date_time, actual_value "
257
                         " FROM tbl_digital_value "
258
                         " WHERE point_id = %s "
259
                         "       AND utc_date_time BETWEEN %s AND %s "
260
                         " ORDER BY utc_date_time ")
261
                cursor_historical.execute(query, (point['id'],
262
                                                  reporting_start_datetime_utc,
263
                                                  reporting_end_datetime_utc))
264
                rows = cursor_historical.fetchall()
265
266
                if rows is not None and len(rows) > 0:
267
                    for row in rows:
268
                        current_datetime_local = row[0].replace(tzinfo=timezone.utc) + \
269
                                                 timedelta(minutes=timezone_offset)
270
                        current_datetime = current_datetime_local.isoformat()[0:19]
271
                        point_timestamp_list.append(current_datetime)
272
                        point_value_list.append(row[1])
273
274
            reporting['names'].append(point['name'] + ' (' + point['units'] + ')')
275
            reporting['timestamps'].append(point_timestamp_list)
276
            reporting['values'].append(point_value_list)
277
278
        ################################################################################################################
279
        # Step 5: query tariff data
280
        ################################################################################################################
281
        parameters_data = dict()
282
        parameters_data['names'] = list()
283
        parameters_data['timestamps'] = list()
284
        parameters_data['values'] = list()
285
        if config.is_tariff_appended and not is_quick_mode:
286
            tariff_dict = utilities.get_energy_category_tariffs(meter['cost_center_id'],
287
                                                                meter['energy_category_id'],
288
                                                                reporting_start_datetime_utc,
289
                                                                reporting_end_datetime_utc)
290
            tariff_timestamp_list = list()
291
            tariff_value_list = list()
292
            for k, v in tariff_dict.items():
293
                # convert k from utc to local
294
                k = k + timedelta(minutes=timezone_offset)
295
                tariff_timestamp_list.append(k.isoformat()[0:19])
296
                tariff_value_list.append(v)
297
298
            parameters_data['names'].append(_('Tariff') + '-' + meter['energy_category_name'])
299
            parameters_data['timestamps'].append(tariff_timestamp_list)
300
            parameters_data['values'].append(tariff_value_list)
301
302
        ################################################################################################################
303
        # Step 6: construct the report
304
        ################################################################################################################
305
        if cursor_system:
306
            cursor_system.close()
307
        if cnx_system:
308
            cnx_system.close()
309
310
        if cursor_historical:
311
            cursor_historical.close()
312
        if cnx_historical:
313
            cnx_historical.close()
314
315
        result = {
316
            "meter": {
317
                "cost_center_id": meter['cost_center_id'],
318
                "energy_category_id": meter['energy_category_id'],
319
                "energy_category_name": meter['energy_category_name'],
320
                "unit_of_measure": meter['unit_of_measure'],
321
                "kgce": meter['kgce'],
322
                "kgco2e": meter['kgco2e'],
323
            },
324
            "reporting_period": {
325
                "names": reporting['names'],
326
                "timestamps": reporting['timestamps'],
327
                "values": reporting['values'],
328
            },
329
            "parameters": {
330
                "names": parameters_data['names'],
331
                "timestamps": parameters_data['timestamps'],
332
                "values": parameters_data['values']
333
            },
334
            "excel_bytes_base64": None
335
        }
336
        # export result to Excel file and then encode the file to base64 string
337
        if not is_quick_mode:
338
            result['excel_bytes_base64'] = excelexporters.metertrend.export(result,
339
                                                                            meter['name'],
340
                                                                            reporting_period_start_datetime_local,
341
                                                                            reporting_period_end_datetime_local,
342
                                                                            None,
343
                                                                            language)
344
345
        resp.text = json.dumps(result)
346