diff --git a/myems-api/app.py b/myems-api/app.py index 7b5913b0..1c40d810 100644 --- a/myems-api/app.py +++ b/myems-api/app.py @@ -38,6 +38,7 @@ from reports import fddspacefault from reports import fddstorefault from reports import fddtenantfault from reports import meterenergy +from reports import metercarbon from reports import metercost from reports import meterrealtime from reports import metersubmetersbalance @@ -557,6 +558,8 @@ api.add_route('/reports/fddtenantfault', fddtenantfault.Reporting()) api.add_route('/reports/meterbatch', meterbatch.Reporting()) +api.add_route('/reports/metercarbon', + metercarbon.Reporting()) api.add_route('/reports/metercost', metercost.Reporting()) api.add_route('/reports/meterenergy', diff --git a/myems-api/config.py b/myems-api/config.py index 30a590f2..631b4903 100644 --- a/myems-api/config.py +++ b/myems-api/config.py @@ -73,6 +73,14 @@ myems_reporting_db = { 'password': config('MYEMS_REPORTING_DB_PASSWORD', default='!MyEMS1'), } +myems_carbon_db = { + 'host': config('MYEMS_CARBON_DB_HOST', default='127.0.0.1'), + 'port': config('MYEMS_CARBON_DB_PORT', default=3306, cast=int), + 'database': config('MYEMS_CARBON_DB_DATABASE', default='myems_carbon_db'), + 'user': config('MYEMS_CARBON_DB_USER', default='root'), + 'password': config('MYEMS_CARBON_DB_PASSWORD', default='!MyEMS1'), +} + # indicated in how many minutes to calculate meter energy consumption # 30 for half hourly period # 60 for hourly period diff --git a/myems-api/excelexporters/metercarbon.py b/myems-api/excelexporters/metercarbon.py new file mode 100644 index 00000000..b216a2a7 --- /dev/null +++ b/myems-api/excelexporters/metercarbon.py @@ -0,0 +1,557 @@ +import base64 +import uuid +import os +import re +from openpyxl.chart import LineChart, Reference, Series +from openpyxl.styles import PatternFill, Border, Side, Alignment, Font +from openpyxl.drawing.image import Image +from openpyxl import Workbook +from openpyxl.chart.label import DataLabelList +import openpyxl.utils.cell as format_cell + + +######################################################################################################################## +# PROCEDURES +# Step 1: Validate the report data +# Step 2: Generate excelexporters file +# Step 3: Encode the excelexporters file to Base64 +######################################################################################################################## + +def export(report, name, reporting_start_datetime_local, reporting_end_datetime_local, period_type): + #################################################################################################################### + # Step 1: Validate the report data + #################################################################################################################### + if report is None: + return None + + if "reporting_period" not in report.keys() or \ + "values" not in report['reporting_period'].keys() or len(report['reporting_period']['values']) == 0: + return None + #################################################################################################################### + # Step 2: Generate excel file from the report data + #################################################################################################################### + filename = generate_excel(report, + name, + reporting_start_datetime_local, + reporting_end_datetime_local, + period_type) + #################################################################################################################### + # Step 3: Encode the excel file to Base64 + #################################################################################################################### + binary_file_data = b'' + try: + with open(filename, 'rb') as binary_file: + binary_file_data = binary_file.read() + except IOError as ex: + pass + + # Base64 encode the bytes + base64_encoded_data = base64.b64encode(binary_file_data) + # get the Base64 encoded data using human-readable characters. + base64_message = base64_encoded_data.decode('utf-8') + # delete the file from server + try: + os.remove(filename) + except NotImplementedError as ex: + pass + return base64_message + + +def generate_excel(report, name, reporting_start_datetime_local, reporting_end_datetime_local, period_type): + wb = Workbook() + + # todo + ws = wb.active + ws.title = "MeterCarbonEmission" + # Row height + ws.row_dimensions[1].height = 102 + for i in range(2, 2000 + 1): + ws.row_dimensions[i].height = 42 + + # Col width + ws.column_dimensions['A'].width = 1.5 + + ws.column_dimensions['B'].width = 25.0 + + for i in range(ord('C'), ord('L')): + ws.column_dimensions[chr(i)].width = 15.0 + + # Font + name_font = Font(name='Arial', size=15, bold=True) + title_font = Font(name='Arial', size=15, bold=True) + + table_fill = PatternFill(fill_type='solid', fgColor='1F497D') + f_border = Border(left=Side(border_style='medium', color='00000000'), + right=Side(border_style='medium', color='00000000'), + bottom=Side(border_style='medium', color='00000000'), + top=Side(border_style='medium', color='00000000') + ) + b_border = Border( + bottom=Side(border_style='medium', color='00000000'), + ) + + b_c_alignment = Alignment(vertical='bottom', + horizontal='center', + text_rotation=0, + wrap_text=True, + shrink_to_fit=False, + indent=0) + c_c_alignment = Alignment(vertical='center', + horizontal='center', + text_rotation=0, + wrap_text=True, + shrink_to_fit=False, + indent=0) + b_r_alignment = Alignment(vertical='bottom', + horizontal='right', + text_rotation=0, + wrap_text=True, + shrink_to_fit=False, + indent=0) + + # Img + img = Image("excelexporters/myems.png") + ws.add_image(img, 'A1') + + # Title + ws['B3'].alignment = b_r_alignment + ws['B3'] = 'Name:' + ws['C3'].border = b_border + ws['C3'].alignment = b_c_alignment + ws['C3'] = name + + ws['D3'].alignment = b_r_alignment + ws['D3'] = 'Period:' + ws['E3'].border = b_border + ws['E3'].alignment = b_c_alignment + ws['E3'] = period_type + + ws['B4'].alignment = b_r_alignment + ws['B4'] = 'Reporting Start Datetime:' + ws['C4'].border = b_border + ws['C4'].alignment = b_c_alignment + ws['C4'] = reporting_start_datetime_local + + ws['D4'].alignment = b_r_alignment + ws['D4'] = 'Reporting End Datetime:' + ws['E4'].border = b_border + ws['E4'].alignment = b_c_alignment + ws['E4'] = reporting_end_datetime_local + + if "reporting_period" not in report.keys() or \ + "values" not in report['reporting_period'].keys() or len(report['reporting_period']['values']) == 0: + filename = str(uuid.uuid4()) + '.xlsx' + wb.save(filename) + + return filename + + #################################################################################################################### + + has_cost_data_flag = True + + if "values" not in report['reporting_period'].keys() or len(report['reporting_period']['values']) == 0: + has_cost_data_flag = False + + if has_cost_data_flag: + ws['B6'].font = title_font + ws['B6'] = name + 'Reporting Period Carbon Emission' + + reporting_period_data = report['reporting_period'] + category = report['meter']['energy_category_name'] + ca_len = len(category) + + ws.row_dimensions[7].height = 60 + ws['B7'].fill = table_fill + ws['B7'].border = f_border + + ws['B8'].font = title_font + ws['B8'].alignment = c_c_alignment + ws['B8'] = 'Carbon Emission' + ws['B8'].border = f_border + + ws['B9'].font = title_font + ws['B9'].alignment = c_c_alignment + ws['B9'] = 'Increment Rate' + ws['B9'].border = f_border + + col = 'B' + + for i in range(0, ca_len): + col = chr(ord('C') + i) + + ws[col + '7'].fill = table_fill + ws[col + '7'].font = name_font + ws[col + '7'].alignment = c_c_alignment + ws[col + '7'] = report['meter']['energy_category_name'] + " (" + report['meter']['unit_of_measure'] + ")" + ws[col + '7'].border = f_border + + ws[col + '8'].font = name_font + ws[col + '8'].alignment = c_c_alignment + ws[col + '8'] = round(reporting_period_data['total_in_category'], 2) + ws[col + '8'].border = f_border + + ws[col + '9'].font = name_font + ws[col + '9'].alignment = c_c_alignment + ws[col + '9'] = str(round(reporting_period_data['increment_rate'] * 100, 2)) + "%" \ + if reporting_period_data['increment_rate'] is not None else "-" + ws[col + '9'].border = f_border + + # TCE TCO2E + end_col = col + # TCE + tce_col = chr(ord(end_col) + 1) + ws[tce_col + '7'].fill = table_fill + ws[tce_col + '7'].font = name_font + ws[tce_col + '7'].alignment = c_c_alignment + ws[tce_col + '7'] = 'Ton of Standard Coal (TCE)' + ws[tce_col + '7'].border = f_border + + ws[tce_col + '8'].font = name_font + ws[tce_col + '8'].alignment = c_c_alignment + ws[tce_col + '8'] = round(reporting_period_data['total_in_kgce'] / 1000, 2) + ws[tce_col + '8'].border = f_border + + ws[tce_col + '9'].font = name_font + ws[tce_col + '9'].alignment = c_c_alignment + ws[tce_col + '9'] = str(round(reporting_period_data['increment_rate'] * 100, 2)) + "%" \ + if reporting_period_data['increment_rate'] is not None else "-" + ws[tce_col + '9'].border = f_border + + # TCO2E + tco2e_col = chr(ord(end_col) + 2) + ws[tco2e_col + '7'].fill = table_fill + ws[tco2e_col + '7'].font = name_font + ws[tco2e_col + '7'].alignment = c_c_alignment + ws[tco2e_col + '7'] = 'Ton of Carbon Dioxide Emissions (TCO2E)' + ws[tco2e_col + '7'].border = f_border + + ws[tco2e_col + '8'].font = name_font + ws[tco2e_col + '8'].alignment = c_c_alignment + ws[tco2e_col + '8'] = round(reporting_period_data['total_in_kgco2e'] / 1000, 2) + ws[tco2e_col + '8'].border = f_border + + ws[tco2e_col + '9'].font = name_font + ws[tco2e_col + '9'].alignment = c_c_alignment + ws[tco2e_col + '9'] = str(round(reporting_period_data['increment_rate'] * 100, 2)) + "%" \ + if reporting_period_data['increment_rate'] is not None else "-" + ws[tco2e_col + '9'].border = f_border + + else: + for i in range(6, 9 + 1): + ws.rows_dimensions[i].height = 0.1 + + #################################################################################################################### + + has_cost_datail_flag = True + reporting_period_data = report['reporting_period'] + category = report['meter']['energy_category_name'] + ca_len = len(category) + times = reporting_period_data['timestamps'] + parameters_names_len = len(report['parameters']['names']) + parameters_data = report['parameters'] + parameters_parameters_datas_len = 0 + for i in range(0, parameters_names_len): + if len(parameters_data['timestamps'][i]) == 0: + continue + parameters_parameters_datas_len += 1 + + if "values" not in reporting_period_data.keys() or len(reporting_period_data['values']) == 0: + has_cost_datail_flag = False + + if has_cost_datail_flag: + start_detail_data_row_number = 13 + (parameters_parameters_datas_len + ca_len) * 6 + + ws['B11'].font = title_font + ws['B11'] = name + 'Detailed Data' + + ws.row_dimensions[start_detail_data_row_number].height = 60 + ws['B' + str(start_detail_data_row_number)].fill = table_fill + ws['B' + str(start_detail_data_row_number)].font = title_font + ws['B' + str(start_detail_data_row_number)].border = f_border + ws['B' + str(start_detail_data_row_number)].alignment = c_c_alignment + ws['B' + str(start_detail_data_row_number)] = 'Datetime' + time = times + has_data = False + max_row = 0 + if len(time) > 0: + has_data = True + max_row = start_detail_data_row_number + len(time) + + if has_data: + + end_data_row_number = start_detail_data_row_number + + for i in range(0, len(time)): + col = 'B' + end_data_row_number += 1 + row = str(end_data_row_number) + + ws[col + row].font = title_font + ws[col + row].alignment = c_c_alignment + ws[col + row] = time[i] + ws[col + row].border = f_border + + ws['B' + str(end_data_row_number + 1)].font = title_font + ws['B' + str(end_data_row_number + 1)].alignment = c_c_alignment + ws['B' + str(end_data_row_number + 1)] = 'Total' + ws['B' + str(end_data_row_number + 1)].border = f_border + + for i in range(0, ca_len): + + col = chr(ord('C') + i) + + ws[col + str(start_detail_data_row_number)].fill = table_fill + ws[col + str(start_detail_data_row_number)].font = title_font + ws[col + str(start_detail_data_row_number)].alignment = c_c_alignment + ws[col + str(start_detail_data_row_number)] = \ + report['meter']['energy_category_name']+" (" + report['meter']['unit_of_measure'] + ")" + ws[col + str(start_detail_data_row_number)].border = f_border + + time = times + time_len = len(time) + + for j in range(0, time_len): + row = str(start_detail_data_row_number + 1 + j) + + ws[col + row].font = title_font + ws[col + row].alignment = c_c_alignment + ws[col + row] = round(reporting_period_data['values'][j], 2) + ws[col + row].border = f_border + + ws[col + str(end_data_row_number + 1)].font = title_font + ws[col + str(end_data_row_number + 1)].alignment = c_c_alignment + ws[col + str(end_data_row_number + 1)] = round(reporting_period_data['total_in_category'], 2) + ws[col + str(end_data_row_number + 1)].border = f_border + + line = LineChart() + line.title = 'Reporting Period Costs - ' + report['meter']['energy_category_name'] + \ + " (" + report['meter']['unit_of_measure'] + ")" + line_data = Reference(ws, min_col=3, min_row=start_detail_data_row_number, max_row=max_row) + line.series.append(Series(line_data, title_from_data=True)) + labels = Reference(ws, min_col=2, min_row=start_detail_data_row_number + 1, max_row=max_row) + line.set_categories(labels) + line_data = line.series[0] + line_data.marker.symbol = "circle" + line_data.smooth = True + line.x_axis.crosses = 'min' + line.dLbls = DataLabelList() + line.dLbls.dLblPos = 't' + line.dLbls.showVal = True + line.height = 8.25 + line.width = 24 + ws.add_chart(line, "B12") + else: + for i in range(11, 43 + 1): + ws.row_dimensions[i].height = 0.0 + + #################################################################################################################### + has_parameters_names_and_timestamps_and_values_data = True + # 12 is the starting line number of the last line chart in the report period + time_len = len(reporting_period_data['timestamps']) + current_sheet_parameters_row_number = 12 + ca_len * 6 + if 'parameters' not in report.keys() or \ + report['parameters'] is None or \ + 'names' not in report['parameters'].keys() or \ + report['parameters']['names'] is None or \ + len(report['parameters']['names']) == 0 or \ + 'timestamps' not in report['parameters'].keys() or \ + report['parameters']['timestamps'] is None or \ + len(report['parameters']['timestamps']) == 0 or \ + 'values' not in report['parameters'].keys() or \ + report['parameters']['values'] is None or \ + len(report['parameters']['values']) == 0 or \ + timestamps_data_all_equal_0(report['parameters']['timestamps']): + has_parameters_names_and_timestamps_and_values_data = False + if has_parameters_names_and_timestamps_and_values_data: + + ################################################################################################################ + # new worksheet + ################################################################################################################ + + parameters_data = report['parameters'] + + parameters_names_len = len(parameters_data['names']) + + file_name = (re.sub(r'[^A-Z]', '', ws.title))+'_' + parameters_ws = wb.create_sheet(file_name + 'Parameters') + + parameters_timestamps_data_max_len = \ + get_parameters_timestamps_lists_max_len(list(parameters_data['timestamps'])) + + # Row height + parameters_ws.row_dimensions[1].height = 102 + for i in range(2, 7 + 1): + parameters_ws.row_dimensions[i].height = 42 + + for i in range(8, parameters_timestamps_data_max_len + 10): + parameters_ws.row_dimensions[i].height = 60 + + # Col width + parameters_ws.column_dimensions['A'].width = 1.5 + + parameters_ws.column_dimensions['B'].width = 25.0 + + for i in range(3, 12 + parameters_names_len * 3): + parameters_ws.column_dimensions[format_cell.get_column_letter(i)].width = 15.0 + + # Img + img = Image("excelexporters/myems.png") + parameters_ws.add_image(img, 'A1') + + # Title + parameters_ws['B3'].alignment = b_r_alignment + parameters_ws['B3'] = 'Name:' + parameters_ws['C3'].border = b_border + parameters_ws['C3'].alignment = b_c_alignment + parameters_ws['C3'] = name + + parameters_ws['D3'].alignment = b_r_alignment + parameters_ws['D3'] = 'Period:' + parameters_ws['E3'].border = b_border + parameters_ws['E3'].alignment = b_c_alignment + parameters_ws['E3'] = period_type + + parameters_ws['B4'].alignment = b_r_alignment + parameters_ws['B4'] = 'Reporting Start Datetime:' + parameters_ws['C4'].border = b_border + parameters_ws['C4'].alignment = b_c_alignment + parameters_ws['C4'] = reporting_start_datetime_local + + parameters_ws['D4'].alignment = b_r_alignment + parameters_ws['D4'] = 'Reporting End Datetime:' + parameters_ws['E4'].border = b_border + parameters_ws['E4'].alignment = b_c_alignment + parameters_ws['E4'] = reporting_end_datetime_local + + parameters_ws_current_row_number = 6 + + parameters_ws['B' + str(parameters_ws_current_row_number)].font = title_font + parameters_ws['B' + str(parameters_ws_current_row_number)] = name + ' ' + 'Parameters' + + parameters_ws_current_row_number += 1 + + parameters_table_start_row_number = parameters_ws_current_row_number + + parameters_ws.row_dimensions[parameters_ws_current_row_number].height = 80 + + parameters_ws_current_row_number += 1 + + table_current_col_number = 2 + + for i in range(0, parameters_names_len): + + if len(parameters_data['timestamps'][i]) == 0: + continue + + col = format_cell.get_column_letter(table_current_col_number) + + parameters_ws[col + str(parameters_ws_current_row_number - 1)].fill = table_fill + parameters_ws[col + str(parameters_ws_current_row_number - 1)].border = f_border + + col = format_cell.get_column_letter(table_current_col_number + 1) + + parameters_ws[col + str(parameters_ws_current_row_number - 1)].fill = table_fill + parameters_ws[col + str(parameters_ws_current_row_number - 1)].border = f_border + parameters_ws[col + str(parameters_ws_current_row_number - 1)].font = name_font + parameters_ws[col + str(parameters_ws_current_row_number - 1)].alignment = c_c_alignment + parameters_ws[col + str(parameters_ws_current_row_number - 1)] = parameters_data['names'][i] + + table_current_row_number = parameters_ws_current_row_number + + for j, value in enumerate(list(parameters_data['timestamps'][i])): + col = format_cell.get_column_letter(table_current_col_number) + + parameters_ws[col + str(table_current_row_number)].border = f_border + parameters_ws[col + str(table_current_row_number)].font = title_font + parameters_ws[col + str(table_current_row_number)].alignment = c_c_alignment + parameters_ws[col + str(table_current_row_number)] = value + + col = format_cell.get_column_letter(table_current_col_number + 1) + + parameters_ws[col + str(table_current_row_number)].border = f_border + parameters_ws[col + str(table_current_row_number)].font = title_font + parameters_ws[col + str(table_current_row_number)].alignment = c_c_alignment + parameters_ws[col + str(table_current_row_number)] = round(parameters_data['values'][i][j], 2) + + table_current_row_number += 1 + + table_current_col_number = table_current_col_number + 3 + + ################################################################################################################ + # parameters chart and parameters table + ################################################################################################################ + + ws['B' + str(current_sheet_parameters_row_number)].font = title_font + ws['B' + str(current_sheet_parameters_row_number)] = name + ' ' + 'Parameters' + + current_sheet_parameters_row_number += 1 + + chart_start_row_number = current_sheet_parameters_row_number + + col_index = 0 + + for i in range(0, parameters_names_len): + + if len(parameters_data['timestamps'][i]) == 0: + continue + + line = LineChart() + data_col = 3 + col_index * 3 + labels_col = 2 + col_index * 3 + col_index += 1 + line.title = 'Parameters - ' + \ + parameters_ws.cell(row=parameters_table_start_row_number, column=data_col).value + labels = Reference(parameters_ws, min_col=labels_col, min_row=parameters_table_start_row_number + 1, + max_row=(len(parameters_data['timestamps'][i]) + parameters_table_start_row_number)) + line_data = Reference(parameters_ws, min_col=data_col, min_row=parameters_table_start_row_number, + max_row=(len(parameters_data['timestamps'][i]) + parameters_table_start_row_number)) + line.add_data(line_data, titles_from_data=True) + line.set_categories(labels) + line_data = line.series[0] + line_data.marker.symbol = "circle" + line_data.smooth = True + line.x_axis.crosses = 'min' + line.height = 8.25 + line.width = 24 + line.dLbls = DataLabelList() + line.dLbls.dLblPos = 't' + line.dLbls.showVal = False + line.dLbls.showPercent = False + chart_col = 'B' + chart_cell = chart_col + str(chart_start_row_number) + chart_start_row_number += 6 + ws.add_chart(line, chart_cell) + + current_sheet_parameters_row_number = chart_start_row_number + + current_sheet_parameters_row_number += 1 + + filename = str(uuid.uuid4()) + '.xlsx' + wb.save(filename) + + return filename + + +def timestamps_data_all_equal_0(lists): + for i, value in enumerate(list(lists)): + if len(value) > 0: + return False + + return True + + +def get_parameters_timestamps_lists_max_len(parameters_timestamps_lists): + max_len = 0 + for i, value in enumerate(list(parameters_timestamps_lists)): + if len(value) > max_len: + max_len = len(value) + + return max_len + + +def timestamps_data_not_equal_0(lists): + number = 0 + for i, value in enumerate(list(lists)): + if len(value) > 0: + number += 1 + return number diff --git a/myems-api/reports/metercarbon.py b/myems-api/reports/metercarbon.py new file mode 100644 index 00000000..143141fe --- /dev/null +++ b/myems-api/reports/metercarbon.py @@ -0,0 +1,495 @@ +import re +import falcon +import simplejson as json +import mysql.connector +import config +from datetime import datetime, timedelta, timezone +from core import utilities +from decimal import Decimal +import excelexporters.metercost + + +class Reporting: + @staticmethod + def __init__(): + """"Initializes Reporting""" + pass + + @staticmethod + def on_options(req, resp): + resp.status = falcon.HTTP_200 + + #################################################################################################################### + # PROCEDURES + # Step 1: valid parameters + # Step 2: query the meter and carbon + # Step 3: query associated points + # Step 4: query base period carbon consumption + # Step 5: query base period carbon Emission + # Step 6: query reporting period energy consumption + # Step 7: query reporting period energy cost + # Step 8: query tariff data + # Step 9: query associated points data + # Step 10: construct the report + #################################################################################################################### + @staticmethod + def on_get(req, resp): + print(req.params) + meter_id = req.params.get('meterid') + meter_uuid = req.params.get('meteruuid') + period_type = req.params.get('periodtype') + base_period_start_datetime_local = req.params.get('baseperiodstartdatetime') + base_period_end_datetime_local = req.params.get('baseperiodenddatetime') + reporting_period_start_datetime_local = req.params.get('reportingperiodstartdatetime') + reporting_period_end_datetime_local = req.params.get('reportingperiodenddatetime') + + ################################################################################################################ + # Step 1: valid parameters + ################################################################################################################ + if meter_id is None and meter_uuid is None: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', description='API.INVALID_METER_ID') + + if meter_id is not None: + meter_id = str.strip(meter_id) + if not meter_id.isdigit() or int(meter_id) <= 0: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', description='API.INVALID_METER_ID') + + if meter_uuid is not None: + meter_uuid = str.strip(meter_uuid) + regex = re.compile('^[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) + match = regex.match(meter_uuid) + if not bool(match): + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', description='API.INVALID_METER_UUID') + + if period_type is None: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', description='API.INVALID_PERIOD_TYPE') + else: + period_type = str.strip(period_type) + if period_type not in ['hourly', 'daily', 'weekly', 'monthly', 'yearly']: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', description='API.INVALID_PERIOD_TYPE') + + timezone_offset = int(config.utc_offset[1:3]) * 60 + int(config.utc_offset[4:6]) + if config.utc_offset[0] == '-': + timezone_offset = -timezone_offset + + base_start_datetime_utc = None + if base_period_start_datetime_local is not None and len(str.strip(base_period_start_datetime_local)) > 0: + base_period_start_datetime_local = str.strip(base_period_start_datetime_local) + try: + base_start_datetime_utc = datetime.strptime(base_period_start_datetime_local, '%Y-%m-%dT%H:%M:%S') + except ValueError: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description="API.INVALID_BASE_PERIOD_START_DATETIME") + base_start_datetime_utc = base_start_datetime_utc.replace(tzinfo=timezone.utc) - \ + timedelta(minutes=timezone_offset) + + base_end_datetime_utc = None + if base_period_end_datetime_local is not None and len(str.strip(base_period_end_datetime_local)) > 0: + base_period_end_datetime_local = str.strip(base_period_end_datetime_local) + try: + base_end_datetime_utc = datetime.strptime(base_period_end_datetime_local, '%Y-%m-%dT%H:%M:%S') + except ValueError: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description="API.INVALID_BASE_PERIOD_END_DATETIME") + base_end_datetime_utc = base_end_datetime_utc.replace(tzinfo=timezone.utc) - \ + timedelta(minutes=timezone_offset) + + if base_start_datetime_utc is not None and base_end_datetime_utc is not None and \ + base_start_datetime_utc >= base_end_datetime_utc: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description='API.INVALID_BASE_PERIOD_END_DATETIME') + + if reporting_period_start_datetime_local is None: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description="API.INVALID_REPORTING_PERIOD_START_DATETIME") + else: + reporting_period_start_datetime_local = str.strip(reporting_period_start_datetime_local) + try: + reporting_start_datetime_utc = datetime.strptime(reporting_period_start_datetime_local, + '%Y-%m-%dT%H:%M:%S') + except ValueError: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description="API.INVALID_REPORTING_PERIOD_START_DATETIME") + reporting_start_datetime_utc = reporting_start_datetime_utc.replace(tzinfo=timezone.utc) - \ + timedelta(minutes=timezone_offset) + + if reporting_period_end_datetime_local is None: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description="API.INVALID_REPORTING_PERIOD_END_DATETIME") + else: + reporting_period_end_datetime_local = str.strip(reporting_period_end_datetime_local) + try: + reporting_end_datetime_utc = datetime.strptime(reporting_period_end_datetime_local, + '%Y-%m-%dT%H:%M:%S') + except ValueError: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description="API.INVALID_REPORTING_PERIOD_END_DATETIME") + reporting_end_datetime_utc = reporting_end_datetime_utc.replace(tzinfo=timezone.utc) - \ + timedelta(minutes=timezone_offset) + + if reporting_start_datetime_utc >= reporting_end_datetime_utc: + raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', + description='API.INVALID_REPORTING_PERIOD_END_DATETIME') + + ################################################################################################################ + # Step 2: query the meter and carbon + ################################################################################################################ + cnx_system = mysql.connector.connect(**config.myems_system_db) + cursor_system = cnx_system.cursor() + + cnx_carbon = mysql.connector.connect(**config.myems_carbon_db) + cursor_carbon = cnx_carbon.cursor() + + cnx_billing = mysql.connector.connect(**config.myems_billing_db) + cursor_billing = cnx_billing.cursor() + + cnx_historical = mysql.connector.connect(**config.myems_historical_db) + cursor_historical = cnx_historical.cursor() + if meter_id is not None: + cursor_system.execute(" SELECT m.id, m.name, m.cost_center_id, m.energy_category_id, " + " ec.name, ec.unit_of_measure, ec.kgce, ec.kgco2e " + " FROM tbl_meters m, tbl_energy_categories ec " + " WHERE m.id = %s AND m.energy_category_id = ec.id ", (meter_id,)) + row_meter = cursor_system.fetchone() + elif meter_uuid is not None: + cursor_system.execute(" SELECT m.id, m.name, m.cost_center_id, m.energy_category_id, " + " ec.name, ec.unit_of_measure, ec.kgce, ec.kgco2e " + " FROM tbl_meters m, tbl_energy_categories ec " + " WHERE m.uuid = %s AND m.energy_category_id = ec.id ", (meter_uuid,)) + row_meter = cursor_system.fetchone() + if row_meter is None: + if cursor_system: + cursor_system.close() + if cnx_system: + cnx_system.disconnect() + + if cursor_carbon: + cursor_carbon.close() + if cnx_carbon: + cnx_carbon.disconnect() + + if cursor_billing: + cursor_billing.close() + if cnx_billing: + cnx_billing.disconnect() + + if cursor_historical: + cursor_historical.close() + if cnx_historical: + cnx_historical.disconnect() + raise falcon.HTTPError(falcon.HTTP_404, title='API.NOT_FOUND', description='API.METER_NOT_FOUND') + if meter_id is not None and int(meter_id) != int(row_meter[0]): + raise falcon.HTTPError(falcon.HTTP_404, title='API.NOT_FOUND', description='API.METER_NOT_FOUND') + meter = dict() + meter['id'] = row_meter[0] + meter['name'] = row_meter[1] + meter['cost_center_id'] = row_meter[2] + meter['energy_category_id'] = row_meter[3] + meter['energy_category_name'] = row_meter[4] + meter['unit_of_measure'] = config.currency_unit + meter['kgce'] = row_meter[6] + meter['kgco2e'] = row_meter[7] + + ################################################################################################################ + # Step 3: query associated points + ################################################################################################################ + point_list = list() + cursor_system.execute(" SELECT p.id, p.name, p.units, p.object_type " + " FROM tbl_meters m, tbl_meters_points mp, tbl_points p " + " WHERE m.id = %s AND m.id = mp.meter_id AND mp.point_id = p.id " + " ORDER BY p.id ", (meter['id'],)) + rows_points = cursor_system.fetchall() + if rows_points is not None and len(rows_points) > 0: + for row in rows_points: + point_list.append({"id": row[0], "name": row[1], "units": row[2], "object_type": row[3]}) + + ################################################################################################################ + # Step 4: query base period carbon consumption + ################################################################################################################ + query = (" SELECT start_datetime_utc, actual_value " + " FROM tbl_meter_hourly " + " WHERE meter_id = %s " + " AND start_datetime_utc >= %s " + " AND start_datetime_utc < %s " + " ORDER BY start_datetime_utc ") + cursor_carbon.execute(query, (meter['id'], base_start_datetime_utc, base_end_datetime_utc)) + rows_meter_hourly = cursor_carbon.fetchall() + + rows_meter_periodically = utilities.aggregate_hourly_data_by_period(rows_meter_hourly, + base_start_datetime_utc, + base_end_datetime_utc, + period_type) + base = dict() + base['timestamps'] = list() + base['values'] = list() + base['total_in_category'] = Decimal(0.0) + base['total_in_kgce'] = Decimal(0.0) + base['total_in_kgco2e'] = Decimal(0.0) + + for row_meter_periodically in rows_meter_periodically: + current_datetime_local = row_meter_periodically[0].replace(tzinfo=timezone.utc) + \ + timedelta(minutes=timezone_offset) + if period_type == 'hourly': + current_datetime = current_datetime_local.strftime('%Y-%m-%dT%H:%M:%S') + elif period_type == 'daily': + current_datetime = current_datetime_local.strftime('%Y-%m-%d') + elif period_type == 'weekly': + current_datetime = current_datetime_local.strftime('%Y-%m-%d') + elif period_type == 'monthly': + current_datetime = current_datetime_local.strftime('%Y-%m') + elif period_type == 'yearly': + current_datetime = current_datetime_local.strftime('%Y') + + actual_value = Decimal(0.0) if row_meter_periodically[1] is None \ + else row_meter_periodically[1] + base['timestamps'].append(current_datetime) + base['total_in_kgce'] += actual_value * meter['kgce'] + base['total_in_kgco2e'] += actual_value * meter['kgco2e'] + + ################################################################################################################ + # Step 5: query base period Carbon Emission + ################################################################################################################ + query = (" SELECT start_datetime_utc, actual_value " + " FROM tbl_meter_hourly " + " WHERE meter_id = %s " + " AND start_datetime_utc >= %s " + " AND start_datetime_utc < %s " + " ORDER BY start_datetime_utc ") + cursor_billing.execute(query, (meter['id'], base_start_datetime_utc, base_end_datetime_utc)) + rows_meter_hourly = cursor_billing.fetchall() + + rows_meter_periodically = utilities.aggregate_hourly_data_by_period(rows_meter_hourly, + base_start_datetime_utc, + base_end_datetime_utc, + period_type) + + base['values'] = list() + base['total_in_category'] = Decimal(0.0) + + for row_meter_periodically in rows_meter_periodically: + actual_value = Decimal(0.0) if row_meter_periodically[1] is None \ + else row_meter_periodically[1] + base['values'].append(actual_value) + base['total_in_category'] += actual_value + + ################################################################################################################ + # Step 6: query reporting period carbon consumption + ################################################################################################################ + query = (" SELECT start_datetime_utc, actual_value " + " FROM tbl_meter_hourly " + " WHERE meter_id = %s " + " AND start_datetime_utc >= %s " + " AND start_datetime_utc < %s " + " ORDER BY start_datetime_utc ") + cursor_carbon.execute(query, (meter['id'], reporting_start_datetime_utc, reporting_end_datetime_utc)) + rows_meter_hourly = cursor_carbon.fetchall() + + rows_meter_periodically = utilities.aggregate_hourly_data_by_period(rows_meter_hourly, + reporting_start_datetime_utc, + reporting_end_datetime_utc, + period_type) + reporting = dict() + reporting['timestamps'] = list() + reporting['values'] = list() + reporting['total_in_category'] = Decimal(0.0) + reporting['total_in_kgce'] = Decimal(0.0) + reporting['total_in_kgco2e'] = Decimal(0.0) + + for row_meter_periodically in rows_meter_periodically: + current_datetime_local = row_meter_periodically[0].replace(tzinfo=timezone.utc) + \ + timedelta(minutes=timezone_offset) + if period_type == 'hourly': + current_datetime = current_datetime_local.strftime('%Y-%m-%dT%H:%M:%S') + elif period_type == 'daily': + current_datetime = current_datetime_local.strftime('%Y-%m-%d') + elif period_type == 'weekly': + current_datetime = current_datetime_local.strftime('%Y-%m-%d') + elif period_type == 'monthly': + current_datetime = current_datetime_local.strftime('%Y-%m') + elif period_type == 'yearly': + current_datetime = current_datetime_local.strftime('%Y') + + actual_value = Decimal(0.0) if row_meter_periodically[1] is None \ + else row_meter_periodically[1] + + reporting['timestamps'].append(current_datetime) + reporting['total_in_kgce'] += actual_value * meter['kgce'] + reporting['total_in_kgco2e'] += actual_value * meter['kgco2e'] + + ################################################################################################################ + # Step 7: query reporting period carbon cost + ################################################################################################################ + query = (" SELECT start_datetime_utc, actual_value " + " FROM tbl_meter_hourly " + " WHERE meter_id = %s " + " AND start_datetime_utc >= %s " + " AND start_datetime_utc < %s " + " ORDER BY start_datetime_utc ") + cursor_billing.execute(query, (meter['id'], reporting_start_datetime_utc, reporting_end_datetime_utc)) + rows_meter_hourly = cursor_billing.fetchall() + + rows_meter_periodically = utilities.aggregate_hourly_data_by_period(rows_meter_hourly, + reporting_start_datetime_utc, + reporting_end_datetime_utc, + period_type) + + for row_meter_periodically in rows_meter_periodically: + actual_value = Decimal(0.0) if row_meter_periodically[1] is None \ + else row_meter_periodically[1] + + reporting['values'].append(actual_value) + reporting['total_in_category'] += actual_value + + ################################################################################################################ + # Step 8: query tariff data + ################################################################################################################ + parameters_data = dict() + parameters_data['names'] = list() + parameters_data['timestamps'] = list() + parameters_data['values'] = list() + + tariff_dict = utilities.get_energy_category_tariffs(meter['cost_center_id'], + meter['energy_category_id'], + reporting_start_datetime_utc, + reporting_end_datetime_utc) + tariff_timestamp_list = list() + tariff_value_list = list() + for k, v in tariff_dict.items(): + # convert k from utc to local + k = k + timedelta(minutes=timezone_offset) + tariff_timestamp_list.append(k.isoformat()[0:19]) + tariff_value_list.append(v) + + parameters_data['names'].append('TARIFF-' + meter['energy_category_name']) + parameters_data['timestamps'].append(tariff_timestamp_list) + parameters_data['values'].append(tariff_value_list) + + ################################################################################################################ + # Step 9: query associated points data + ################################################################################################################ + for point in point_list: + point_values = [] + point_timestamps = [] + if point['object_type'] == 'ANALOG_VALUE': + query = (" SELECT utc_date_time, actual_value " + " FROM tbl_analog_value " + " WHERE point_id = %s " + " AND utc_date_time BETWEEN %s AND %s " + " ORDER BY utc_date_time ") + cursor_historical.execute(query, (point['id'], + reporting_start_datetime_utc, + reporting_end_datetime_utc)) + rows = cursor_historical.fetchall() + + if rows is not None and len(rows) > 0: + for row in rows: + current_datetime_local = row[0].replace(tzinfo=timezone.utc) + \ + timedelta(minutes=timezone_offset) + current_datetime = current_datetime_local.strftime('%Y-%m-%dT%H:%M:%S') + point_timestamps.append(current_datetime) + point_values.append(row[1]) + + elif point['object_type'] == 'ENERGY_VALUE': + query = (" SELECT utc_date_time, actual_value " + " FROM tbl_energy_value " + " WHERE point_id = %s " + " AND utc_date_time BETWEEN %s AND %s " + " ORDER BY utc_date_time ") + cursor_historical.execute(query, (point['id'], + reporting_start_datetime_utc, + reporting_end_datetime_utc)) + rows = cursor_historical.fetchall() + + if rows is not None and len(rows) > 0: + for row in rows: + current_datetime_local = row[0].replace(tzinfo=timezone.utc) + \ + timedelta(minutes=timezone_offset) + current_datetime = current_datetime_local.strftime('%Y-%m-%dT%H:%M:%S') + point_timestamps.append(current_datetime) + point_values.append(row[1]) + elif point['object_type'] == 'DIGITAL_VALUE': + query = (" SELECT utc_date_time, actual_value " + " FROM tbl_digital_value " + " WHERE point_id = %s " + " AND utc_date_time BETWEEN %s AND %s " + " ORDER BY utc_date_time ") + cursor_historical.execute(query, (point['id'], + reporting_start_datetime_utc, + reporting_end_datetime_utc)) + rows = cursor_historical.fetchall() + + if rows is not None and len(rows) > 0: + for row in rows: + current_datetime_local = row[0].replace(tzinfo=timezone.utc) + \ + timedelta(minutes=timezone_offset) + current_datetime = current_datetime_local.strftime('%Y-%m-%dT%H:%M:%S') + point_timestamps.append(current_datetime) + point_values.append(row[1]) + + parameters_data['names'].append(point['name'] + ' (' + point['units'] + ')') + parameters_data['timestamps'].append(point_timestamps) + parameters_data['values'].append(point_values) + + ################################################################################################################ + # Step 10: construct the report + ################################################################################################################ + if cursor_system: + cursor_system.close() + if cnx_system: + cnx_system.disconnect() + + if cursor_carbon: + cursor_carbon.close() + if cnx_carbon: + cnx_carbon.disconnect() + + if cursor_billing: + cursor_billing.close() + if cnx_billing: + cnx_billing.disconnect() + + if cursor_historical: + cursor_historical.close() + if cnx_historical: + cnx_historical.disconnect() + result = { + "meter": { + "cost_center_id": meter['cost_center_id'], + "energy_category_id": meter['energy_category_id'], + "energy_category_name": meter['energy_category_name'], + "unit_of_measure": config.currency_unit, + "kgce": meter['kgce'], + "kgco2e": meter['kgco2e'], + }, + "base_period": { + "total_in_category": base['total_in_category'], + "total_in_kgce": base['total_in_kgce'], + "total_in_kgco2e": base['total_in_kgco2e'], + "timestamps": base['timestamps'], + "values": base['values'], + }, + "reporting_period": { + "increment_rate": + (reporting['total_in_category']-base['total_in_category'])/base['total_in_category'] + if base['total_in_category'] > 0 else None, + "total_in_category": reporting['total_in_category'], + "total_in_kgce": reporting['total_in_kgce'], + "total_in_kgco2e": reporting['total_in_kgco2e'], + "timestamps": reporting['timestamps'], + "values": reporting['values'], + }, + "parameters": { + "names": parameters_data['names'], + "timestamps": parameters_data['timestamps'], + "values": parameters_data['values'] + }, + } + # export result to Excel file and then encode the file to base64 string + result['excel_bytes_base64'] = \ + excelexporters.metercost.export(result, + meter['name'], + reporting_period_start_datetime_local, + reporting_period_end_datetime_local, + period_type) + + resp.text = json.dumps(result)