Issues (1751)

myems-api/reports/virtualmetercarbon.py (2 issues)

1
"""
2
Virtual Meter Carbon Report API
3
4
This module provides REST API endpoints for generating virtual meter carbon emissions reports.
5
It analyzes carbon dioxide emissions from virtual meter readings, providing insights
6
into environmental impact and carbon footprint reduction opportunities.
7
8
Key Features:
9
- Virtual meter carbon emissions analysis
10
- Base period vs reporting period comparison
11
- Time-of-use carbon breakdown
12
- Carbon footprint calculations
13
- Excel export functionality
14
- Environmental impact assessment
15
16
Report Components:
17
- Virtual meter carbon emissions summary
18
- Base period comparison data
19
- Time-of-use carbon breakdown
20
- Carbon footprint metrics
21
- Environmental impact indicators
22
- Reduction opportunity analysis
23
24
The module uses Falcon framework for REST API and includes:
25
- Database queries for carbon data
26
- Carbon emission calculations
27
- Time-of-use analysis
28
- Excel export via excelexporters
29
- Multi-language support
30
- User authentication and authorization
31
"""
32
33
from datetime import datetime, timedelta, timezone
34
from decimal import Decimal
35
import falcon
36
import mysql.connector
37
import simplejson as json
38
import config
39
import excelexporters.virtualmetercarbon
40
from core import utilities
41
from core.useractivity import access_control, api_key_control
42
43
44 View Code Duplication
class Reporting:
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
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 virtual meter and energy category
58
    # Step 3: query base period energy consumption
59
    # Step 4: query base period energy carbon dioxide emissions
60
    # Step 5: query reporting period energy consumption
61
    # Step 6: query reporting period energy carbon dioxide emissions
62
    # Step 7: query tariff data
63
    # Step 8: construct the report
64
    ####################################################################################################################
65
    @staticmethod
66
    def on_get(req, resp):
67
        if 'API-KEY' not in req.headers or \
68
                not isinstance(req.headers['API-KEY'], str) or \
69
                len(str.strip(req.headers['API-KEY'])) == 0:
70
            access_control(req)
71
        else:
72
            api_key_control(req)
73
        print(req.params)
74
        virtual_meter_id = req.params.get('virtualmeterid')
75
        period_type = req.params.get('periodtype')
76
        base_period_start_datetime_local = req.params.get('baseperiodstartdatetime')
77
        base_period_end_datetime_local = req.params.get('baseperiodenddatetime')
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_id is None:
87
            raise falcon.HTTPError(status=falcon.HTTP_400,
88
                                   title='API.BAD_REQUEST',
89
                                   description='API.INVALID_VIRTUAL_METER_ID')
90
        else:
91
            virtual_meter_id = str.strip(virtual_meter_id)
92
            if not virtual_meter_id.isdigit() or int(virtual_meter_id) <= 0:
93
                raise falcon.HTTPError(status=falcon.HTTP_400,
94
                                       title='API.BAD_REQUEST',
95
                                       description='API.INVALID_VIRTUAL_METER_ID')
96
97
        if period_type is None:
98
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
99
                                   description='API.INVALID_PERIOD_TYPE')
100
        else:
101
            period_type = str.strip(period_type)
102
            if period_type not in ['hourly', 'daily', 'weekly', 'monthly', 'yearly']:
103
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
104
                                       description='API.INVALID_PERIOD_TYPE')
105
106
        timezone_offset = int(config.utc_offset[1:3]) * 60 + int(config.utc_offset[4:6])
107
        if config.utc_offset[0] == '-':
108
            timezone_offset = -timezone_offset
109
110
        base_start_datetime_utc = None
111
        if base_period_start_datetime_local is not None and len(str.strip(base_period_start_datetime_local)) > 0:
112
            base_period_start_datetime_local = str.strip(base_period_start_datetime_local)
113
            try:
114
                base_start_datetime_utc = datetime.strptime(base_period_start_datetime_local, '%Y-%m-%dT%H:%M:%S')
115
            except ValueError:
116
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
117
                                       description="API.INVALID_BASE_PERIOD_START_DATETIME")
118
            base_start_datetime_utc = \
119
                base_start_datetime_utc.replace(tzinfo=timezone.utc) - timedelta(minutes=timezone_offset)
120
            # nomalize the start datetime
121
            if config.minutes_to_count == 30 and base_start_datetime_utc.minute >= 30:
122
                base_start_datetime_utc = base_start_datetime_utc.replace(minute=30, second=0, microsecond=0)
123
            else:
124
                base_start_datetime_utc = base_start_datetime_utc.replace(minute=0, second=0, microsecond=0)
125
126
        base_end_datetime_utc = None
127
        if base_period_end_datetime_local is not None and len(str.strip(base_period_end_datetime_local)) > 0:
128
            base_period_end_datetime_local = str.strip(base_period_end_datetime_local)
129
            try:
130
                base_end_datetime_utc = datetime.strptime(base_period_end_datetime_local, '%Y-%m-%dT%H:%M:%S')
131
            except ValueError:
132
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
133
                                       description="API.INVALID_BASE_PERIOD_END_DATETIME")
134
            base_end_datetime_utc = \
135
                base_end_datetime_utc.replace(tzinfo=timezone.utc) - timedelta(minutes=timezone_offset)
136
137
        if base_start_datetime_utc is not None and base_end_datetime_utc is not None and \
138
                base_start_datetime_utc >= base_end_datetime_utc:
139
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
140
                                   description='API.INVALID_BASE_PERIOD_END_DATETIME')
141
142
        if reporting_period_start_datetime_local is None:
143
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
144
                                   description="API.INVALID_REPORTING_PERIOD_START_DATETIME")
145
        else:
146
            reporting_period_start_datetime_local = str.strip(reporting_period_start_datetime_local)
147
            try:
148
                reporting_start_datetime_utc = datetime.strptime(reporting_period_start_datetime_local,
149
                                                                 '%Y-%m-%dT%H:%M:%S')
150
            except ValueError:
151
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
152
                                       description="API.INVALID_REPORTING_PERIOD_START_DATETIME")
153
            reporting_start_datetime_utc = \
154
                reporting_start_datetime_utc.replace(tzinfo=timezone.utc) - timedelta(minutes=timezone_offset)
155
            # nomalize the start datetime
156
            if config.minutes_to_count == 30 and reporting_start_datetime_utc.minute >= 30:
157
                reporting_start_datetime_utc = reporting_start_datetime_utc.replace(minute=30, second=0, microsecond=0)
158
            else:
159
                reporting_start_datetime_utc = reporting_start_datetime_utc.replace(minute=0, second=0, microsecond=0)
160
161
        if reporting_period_end_datetime_local is None:
162
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
163
                                   description="API.INVALID_REPORTING_PERIOD_END_DATETIME")
164
        else:
165
            reporting_period_end_datetime_local = str.strip(reporting_period_end_datetime_local)
166
            try:
167
                reporting_end_datetime_utc = datetime.strptime(reporting_period_end_datetime_local,
168
                                                               '%Y-%m-%dT%H:%M:%S')
169
            except ValueError:
170
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
171
                                       description="API.INVALID_REPORTING_PERIOD_END_DATETIME")
172
            reporting_end_datetime_utc = reporting_end_datetime_utc.replace(tzinfo=timezone.utc) - \
173
                timedelta(minutes=timezone_offset)
174
175
        if reporting_start_datetime_utc >= reporting_end_datetime_utc:
176
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
177
                                   description='API.INVALID_REPORTING_PERIOD_END_DATETIME')
178
179
        # if turn quick mode on, do not return parameters data and excel file
180
        is_quick_mode = False
181
        if quick_mode is not None and \
182
                len(str.strip(quick_mode)) > 0 and \
183
                str.lower(str.strip(quick_mode)) in ('true', 't', 'on', 'yes', 'y'):
184
            is_quick_mode = True
185
186
        trans = utilities.get_translation(language)
187
        trans.install()
188
        _ = trans.gettext
189
190
        ################################################################################################################
191
        # Step 2: query the virtual meter and energy category
192
        ################################################################################################################
193
        cnx_system = mysql.connector.connect(**config.myems_system_db)
194
        cursor_system = cnx_system.cursor()
195
196
        cnx_energy = mysql.connector.connect(**config.myems_energy_db)
197
        cursor_energy = cnx_energy.cursor()
198
199
        cnx_carbon = mysql.connector.connect(**config.myems_carbon_db)
200
        cursor_carbon = cnx_carbon.cursor()
201
202
        cursor_system.execute(" SELECT m.id, m.name, m.cost_center_id, m.energy_category_id, "
203
                              "        ec.name, ec.unit_of_measure, ec.kgce, ec.kgco2e "
204
                              " FROM tbl_virtual_meters m, tbl_energy_categories ec "
205
                              " WHERE m.id = %s AND m.energy_category_id = ec.id ", (virtual_meter_id,))
206
        row_virtual_meter = cursor_system.fetchone()
207
        if row_virtual_meter is None:
208
            if cursor_system:
209
                cursor_system.close()
210
            if cnx_system:
211
                cnx_system.close()
212
213
            if cursor_energy:
214
                cursor_energy.close()
215
            if cnx_energy:
216
                cnx_energy.close()
217
218
            if cursor_carbon:
219
                cursor_carbon.close()
220
            if cnx_carbon:
221
                cnx_carbon.close()
222
            raise falcon.HTTPError(status=falcon.HTTP_404, title='API.NOT_FOUND',
223
                                   description='API.VIRTUAL_METER_NOT_FOUND')
224
225
        virtual_meter = dict()
226
        virtual_meter['id'] = row_virtual_meter[0]
227
        virtual_meter['name'] = row_virtual_meter[1]
228
        virtual_meter['cost_center_id'] = row_virtual_meter[2]
229
        virtual_meter['energy_category_id'] = row_virtual_meter[3]
230
        virtual_meter['energy_category_name'] = row_virtual_meter[4]
231
        virtual_meter['unit_of_measure'] = config.currency_unit
232
        virtual_meter['kgce'] = row_virtual_meter[6]
233
        virtual_meter['kgco2e'] = row_virtual_meter[7]
234
235
        ################################################################################################################
236
        # Step 3: query base period energy consumption
237
        ################################################################################################################
238
        query = (" SELECT start_datetime_utc, actual_value "
239
                 " FROM tbl_virtual_meter_hourly "
240
                 " WHERE virtual_meter_id = %s "
241
                 " AND start_datetime_utc >= %s "
242
                 " AND start_datetime_utc < %s "
243
                 " ORDER BY start_datetime_utc ")
244
        cursor_energy.execute(query, (virtual_meter['id'], base_start_datetime_utc, base_end_datetime_utc))
245
        rows_virtual_meter_hourly = cursor_energy.fetchall()
246
247
        rows_virtual_meter_periodically = utilities.aggregate_hourly_data_by_period(rows_virtual_meter_hourly,
248
                                                                                    base_start_datetime_utc,
249
                                                                                    base_end_datetime_utc,
250
                                                                                    period_type)
251
        base = dict()
252
        base['timestamps'] = list()
253
        base['values'] = list()
254
        base['total_in_category'] = Decimal(0.0)
255
        base['total_in_kgce'] = Decimal(0.0)
256
        base['total_in_kgco2e'] = Decimal(0.0)
257
258
        for row_virtual_meter_periodically in rows_virtual_meter_periodically:
259
            current_datetime_local = row_virtual_meter_periodically[0].replace(tzinfo=timezone.utc) + \
260
                                     timedelta(minutes=timezone_offset)
261
            if period_type == 'hourly':
262
                current_datetime = current_datetime_local.isoformat()[0:19]
263
            elif period_type == 'daily':
264
                current_datetime = current_datetime_local.isoformat()[0:10]
265
            elif period_type == 'weekly':
266
                current_datetime = current_datetime_local.isoformat()[0:10]
267
            elif period_type == 'monthly':
268
                current_datetime = current_datetime_local.isoformat()[0:7]
269
            elif period_type == 'yearly':
270
                current_datetime = current_datetime_local.isoformat()[0:4]
271
272
            actual_value = Decimal(0.0) if row_virtual_meter_periodically[1] is None \
273
                else row_virtual_meter_periodically[1]
274
            base['timestamps'].append(current_datetime)
0 ignored issues
show
The variable current_datetime does not seem to be defined for all execution paths.
Loading history...
275
            base['total_in_kgce'] += actual_value * virtual_meter['kgce']
276
            base['total_in_kgco2e'] += actual_value * virtual_meter['kgco2e']
277
278
        ################################################################################################################
279
        # Step 4: query base period energy carbon dioxide emissions
280
        ################################################################################################################
281
        query = (" SELECT start_datetime_utc, actual_value "
282
                 " FROM tbl_virtual_meter_hourly "
283
                 " WHERE virtual_meter_id = %s "
284
                 " AND start_datetime_utc >= %s "
285
                 " AND start_datetime_utc < %s "
286
                 " ORDER BY start_datetime_utc ")
287
        cursor_carbon.execute(query, (virtual_meter['id'], base_start_datetime_utc, base_end_datetime_utc))
288
        rows_virtual_meter_hourly = cursor_carbon.fetchall()
289
290
        rows_virtual_meter_periodically = utilities.aggregate_hourly_data_by_period(rows_virtual_meter_hourly,
291
                                                                                    base_start_datetime_utc,
292
                                                                                    base_end_datetime_utc,
293
                                                                                    period_type)
294
295
        base['values'] = list()
296
        base['total_in_category'] = Decimal(0.0)
297
298
        for row_virtual_meter_periodically in rows_virtual_meter_periodically:
299
            actual_value = Decimal(0.0) if row_virtual_meter_periodically[1] is None \
300
                else row_virtual_meter_periodically[1]
301
            base['values'].append(actual_value)
302
            base['total_in_category'] += actual_value
303
304
        ################################################################################################################
305
        # Step 5: query reporting period energy consumption
306
        ################################################################################################################
307
        query = (" SELECT start_datetime_utc, actual_value "
308
                 " FROM tbl_virtual_meter_hourly "
309
                 " WHERE virtual_meter_id = %s "
310
                 " AND start_datetime_utc >= %s "
311
                 " AND start_datetime_utc < %s "
312
                 " ORDER BY start_datetime_utc ")
313
        cursor_energy.execute(query, (virtual_meter['id'], reporting_start_datetime_utc, reporting_end_datetime_utc))
314
        rows_virtual_meter_hourly = cursor_energy.fetchall()
315
316
        rows_virtual_meter_periodically = utilities.aggregate_hourly_data_by_period(rows_virtual_meter_hourly,
317
                                                                                    reporting_start_datetime_utc,
318
                                                                                    reporting_end_datetime_utc,
319
                                                                                    period_type)
320
        reporting = dict()
321
        reporting['timestamps'] = list()
322
        reporting['values'] = list()
323
        reporting['rates'] = list()
324
        reporting['total_in_category'] = Decimal(0.0)
325
        reporting['total_in_kgce'] = Decimal(0.0)
326
        reporting['total_in_kgco2e'] = Decimal(0.0)
327
328
        for row_virtual_meter_periodically in rows_virtual_meter_periodically:
329
            current_datetime_local = row_virtual_meter_periodically[0].replace(tzinfo=timezone.utc) + \
330
                                     timedelta(minutes=timezone_offset)
331
            if period_type == 'hourly':
332
                current_datetime = current_datetime_local.isoformat()[0:19]
333
            elif period_type == 'daily':
334
                current_datetime = current_datetime_local.isoformat()[0:10]
335
            elif period_type == 'weekly':
336
                current_datetime = current_datetime_local.isoformat()[0:10]
337
            elif period_type == 'monthly':
338
                current_datetime = current_datetime_local.isoformat()[0:7]
339
            elif period_type == 'yearly':
340
                current_datetime = current_datetime_local.isoformat()[0:4]
341
342
            actual_value = Decimal(0.0) if row_virtual_meter_periodically[1] is None \
343
                else row_virtual_meter_periodically[1]
344
345
            reporting['timestamps'].append(current_datetime)
346
            reporting['total_in_kgce'] += actual_value * virtual_meter['kgce']
347
            reporting['total_in_kgco2e'] += actual_value * virtual_meter['kgco2e']
348
349
        ################################################################################################################
350
        # Step 6: query reporting period energy carbon dioxide emissions
351
        ################################################################################################################
352
        query = (" SELECT start_datetime_utc, actual_value "
353
                 " FROM tbl_virtual_meter_hourly "
354
                 " WHERE virtual_meter_id = %s "
355
                 " AND start_datetime_utc >= %s "
356
                 " AND start_datetime_utc < %s "
357
                 " ORDER BY start_datetime_utc ")
358
        cursor_carbon.execute(query, (virtual_meter['id'], reporting_start_datetime_utc, reporting_end_datetime_utc))
359
        rows_virtual_meter_hourly = cursor_carbon.fetchall()
360
361
        rows_virtual_meter_periodically = utilities.aggregate_hourly_data_by_period(rows_virtual_meter_hourly,
362
                                                                                    reporting_start_datetime_utc,
363
                                                                                    reporting_end_datetime_utc,
364
                                                                                    period_type)
365
366
        for row_virtual_meter_periodically in rows_virtual_meter_periodically:
367
            actual_value = Decimal(0.0) if row_virtual_meter_periodically[1] is None \
368
                else row_virtual_meter_periodically[1]
369
370
            reporting['values'].append(actual_value)
371
            reporting['total_in_category'] += actual_value
372
373
        for index, value in enumerate(reporting['values']):
374
            if index < len(base['values']) and base['values'][index] != 0 and value != 0:
375
                reporting['rates'].append((value - base['values'][index]) / base['values'][index])
376
            else:
377
                reporting['rates'].append(None)
378
379
        ################################################################################################################
380
        # Step 7: query tariff data
381
        ################################################################################################################
382
        parameters_data = dict()
383
        parameters_data['names'] = list()
384
        parameters_data['timestamps'] = list()
385
        parameters_data['values'] = list()
386
        if config.is_tariff_appended and not is_quick_mode:
387
            tariff_dict = utilities.get_energy_category_tariffs(virtual_meter['cost_center_id'],
388
                                                                virtual_meter['energy_category_id'],
389
                                                                reporting_start_datetime_utc,
390
                                                                reporting_end_datetime_utc)
391
            tariff_timestamp_list = list()
392
            tariff_value_list = list()
393
            for k, v in tariff_dict.items():
394
                # convert k from utc to local
395
                k = k + timedelta(minutes=timezone_offset)
396
                tariff_timestamp_list.append(k.isoformat()[0:19])
397
                tariff_value_list.append(v)
398
399
            parameters_data['names'].append(_('Tariff') + '-' + virtual_meter['energy_category_name'])
400
            parameters_data['timestamps'].append(tariff_timestamp_list)
401
            parameters_data['values'].append(tariff_value_list)
402
403
        ################################################################################################################
404
        # Step 8: construct the report
405
        ################################################################################################################
406
        if cursor_system:
407
            cursor_system.close()
408
        if cnx_system:
409
            cnx_system.close()
410
411
        if cursor_energy:
412
            cursor_energy.close()
413
        if cnx_energy:
414
            cnx_energy.close()
415
416
        if cursor_carbon:
417
            cursor_carbon.close()
418
        if cnx_carbon:
419
            cnx_carbon.close()
420
421
        result = {
422
            "virtual_meter": {
423
                "cost_center_id": virtual_meter['cost_center_id'],
424
                "energy_category_id": virtual_meter['energy_category_id'],
425
                "energy_category_name": virtual_meter['energy_category_name'],
426
                "unit_of_measure": 'KG',
427
                "kgce": virtual_meter['kgce'],
428
                "kgco2e": virtual_meter['kgco2e'],
429
            },
430
            "base_period": {
431
                "total_in_category": base['total_in_category'],
432
                "total_in_kgce": base['total_in_kgce'],
433
                "total_in_kgco2e": base['total_in_kgco2e'],
434
                "timestamps": base['timestamps'],
435
                "values": base['values'],
436
            },
437
            "reporting_period": {
438
                "increment_rate":
439
                    (reporting['total_in_category'] - base['total_in_category']) / base['total_in_category']
440
                    if base['total_in_category'] != Decimal(0.0) else None,
441
                "total_in_category": reporting['total_in_category'],
442
                "total_in_kgce": reporting['total_in_kgce'],
443
                "total_in_kgco2e": reporting['total_in_kgco2e'],
444
                "timestamps": reporting['timestamps'],
445
                "values": reporting['values'],
446
                "rates": reporting['rates'],
447
            },
448
            "parameters": {
449
                "names": parameters_data['names'],
450
                "timestamps": parameters_data['timestamps'],
451
                "values": parameters_data['values']
452
            },
453
        }
454
455
        # export result to Excel file and then encode the file to base64 string
456
        if not is_quick_mode:
457
            result['excel_bytes_base64'] = \
458
                excelexporters.virtualmetercarbon.export(result,
459
                                                         virtual_meter['name'],
460
                                                         base_period_start_datetime_local,
461
                                                         base_period_end_datetime_local,
462
                                                         reporting_period_start_datetime_local,
463
                                                         reporting_period_end_datetime_local,
464
                                                         period_type,
465
                                                         language)
466
467
        resp.text = json.dumps(result)
468