From 025fe02f71c92d2447ade828eca26e5131c7f1ac Mon Sep 17 00:00:00 2001 From: Peter Yefi Date: Tue, 17 Jan 2023 18:59:59 -0500 Subject: [PATCH 01/10] Read energy demand as an array of floats --- hub_api/city_info.py | 1 + hub_api/config.py | 0 utils/__init__.py | 1 + utils/hp_simulator.py | 16 +++++++++------- utils/misc.py | 17 +++++++++++++++++ 5 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 hub_api/config.py diff --git a/hub_api/city_info.py b/hub_api/city_info.py index 8971cd3..5cd9454 100644 --- a/hub_api/city_info.py +++ b/hub_api/city_info.py @@ -21,6 +21,7 @@ class CityInfo(Resource): pass @staticmethod + @role_required([UserRoles.Admin.value]) def get(): session = refresh_session(request) if session is None: diff --git a/hub_api/config.py b/hub_api/config.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/__init__.py b/utils/__init__.py index 1de5e8f..311bf12 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1,2 +1,3 @@ from .misc import validate_hp_model from .hp_simulator import HeatPumpSimulator +from .misc import expand_energy_demand diff --git a/utils/hp_simulator.py b/utils/hp_simulator.py index 42a0b6a..342c563 100644 --- a/utils/hp_simulator.py +++ b/utils/hp_simulator.py @@ -17,10 +17,10 @@ class HeatPumpSimulator: :param user_input: the user parameters for running simulation """ + self._user_input = user_input self._hp_type = user_input['HeatPumpType'].replace(" ", "_").lower() # file to have results after simulation is run - self._output_path = Path(Path(__file__).parent.parent / "data/dompark_{}.csv".format(self._hp_type)) self._city = city EnergySystemsFactory(user_input['HeatPumpType'].lower(), self._city).enrich() @@ -32,10 +32,12 @@ class HeatPumpSimulator: """ hp_type = 'water' if 'water' in self._hp_type else 'air' del self._user_input['HeatPumpType'] + del self._user_input['EnergyDemand'] model = self._user_input.pop('HeatPumpModel') - EnergySystemsExportFactory(self._city, - self._user_input, - model, - self._output_path, - self._user_input['SimType']).export(hp_type) - return str(self._output_path) + energy_demand_path = Path(Path(__file__).parent.parent / "data/energy_demand.txt") + return EnergySystemsExportFactory(city=self._city, + user_input=self._user_input, + hp_model=model, + output_path=None, + sim_type=self._user_input['SimType'], + demand_path=energy_demand_path).export(hp_type) diff --git a/utils/misc.py b/utils/misc.py index dc0b169..0f23955 100644 --- a/utils/misc.py +++ b/utils/misc.py @@ -3,6 +3,11 @@ Miscellaneous SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author Peter Yefi peteryefi@gmail.com """ +from typing import List +from pathlib import Path +import numpy as np + + hp_models = { 'air_source': ['012', '015', '018', '023', '030', '033', '037', '044', '047', '057', '070', '087', '097', '102', '120', '130', '140'], @@ -26,3 +31,15 @@ def validate_hp_model(hp_type: str, model: str) -> bool: if model in hp_models['water_to_water']: return True return False + + +def expand_energy_demand(hourly_energy_demand: List[float]): + """ + Replicates each value in the list 11 times and persist the values to a file + :param hourly_energy_demand: a list of hourly energy demand data + """ + energy_demand = Path(Path(__file__).parent.parent / "data/energy_demand.txt") + with open(energy_demand, 'w') as demand_file: + repeated_demand_values = np.repeat(hourly_energy_demand, 12).tolist() + for demand in repeated_demand_values: + demand_file.write("%.6f\n" % demand) \ No newline at end of file From f893959c7808211ab9f0591e42d3c3558bcf773e Mon Sep 17 00:00:00 2001 From: Peter Yefi Date: Tue, 17 Jan 2023 19:00:22 -0500 Subject: [PATCH 02/10] Removed city attachment to session. City is read from the DB --- hub_api/city_info.py | 24 ++-- hub_api/config.py | 27 ++++ hub_api/construction.py | 24 ++-- hub_api/docs/openapi-specs.yml | 235 ++++++++++++++++++++++++++++++++- hub_api/energy_demand.py | 22 +-- hub_api/geometry.py | 14 +- hub_api/greenery.py | 31 +++-- hub_api/greenery_catalog.py | 2 - hub_api/heat_pump.py | 74 +++-------- hub_api/helpers/auth.py | 9 +- hub_api/lca.py | 46 +++---- hub_api/usage.py | 29 ++-- hub_api/usage_catalog.py | 1 + hub_api/user.py | 2 +- 14 files changed, 370 insertions(+), 170 deletions(-) diff --git a/hub_api/city_info.py b/hub_api/city_info.py index 5cd9454..080dece 100644 --- a/hub_api/city_info.py +++ b/hub_api/city_info.py @@ -2,11 +2,11 @@ City info SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca +Code contributors: Peter Yefi peteryefi@gmail.com """ import json from flask import Response, request, g from flask_restful import Resource -from hub_api.helpers.session_helper import refresh_session from hub_api.helpers.auth import role_required from persistence.models import UserRoles from hub_logger import logger @@ -14,21 +14,17 @@ from imports.geometry_factory import GeometryFactory from pathlib import Path from imports.db_factory import DBFactory import os +from hub_api.config import Config -class CityInfo(Resource): +class CityInfo(Resource, Config): def __init__(self): - pass + super().__init__() - @staticmethod @role_required([UserRoles.Admin.value]) - def get(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers + def get(self, city_id): + city = self.get_city(city_id) - city = session.city # TODO: this is only for dompark project and need to be removed in future versions. floor_area = 0 wall_construction = 'unknown' @@ -71,11 +67,11 @@ class CityInfo(Resource): } buildings = [building_dic] - response = {'city_name': 'Montreal', + response = {'city_name': city.name, 'climate_reference_city': str(city.climate_reference_city), 'buildings': buildings } - return Response(json.dumps(response), headers=headers) + return Response(json.dumps(response), status=200) class City(Resource): @@ -83,7 +79,7 @@ class City(Resource): def __init__(self): pass - @role_required([UserRoles.Admin.value]) + @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) def post(self): allowed_ext = {'gml', '3dm', 'xml', 'obj', 'rhino'} try: @@ -119,4 +115,4 @@ class City(Resource): return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400) except Exception as err: logger.error(err) - return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating city'}), status=400) \ No newline at end of file + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating city'}), status=400) diff --git a/hub_api/config.py b/hub_api/config.py index e69de29..1ca930e 100644 --- a/hub_api/config.py +++ b/hub_api/config.py @@ -0,0 +1,27 @@ +""" +Config +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Project Peter Yefi peteryefi@gmail.com +""" +from exports.db_factory import DBFactory as CityExportFactory +import os +import pickle + + +class Config: + + def __init__(self): + self.factory = CityExportFactory(db_name='hub_prod', app_env='PROD', + dotenv_path="{}/.env".format(os.path.expanduser('~'))) + + def get_city(self, city_id): + city_obj = self.factory.get_city(city_id) + city = pickle.loads(city_obj.city) + for building in city.buildings: + building.heated = True + building.cooled = True + building.attic_heated = 0 + building.basement_heated = 0 + for surface in building.surfaces: + surface.swr = 0.2 + return city diff --git a/hub_api/construction.py b/hub_api/construction.py index d5be55f..7d371b5 100644 --- a/hub_api/construction.py +++ b/hub_api/construction.py @@ -2,29 +2,27 @@ Construction SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca +Code contributors: Peter Yefi peteryefi@gmail.com """ import json import uuid from flask import Response, request from flask_restful import Resource - +from hub_api.config import Config from city_model_structure.building_demand.layer import Layer from city_model_structure.building_demand.material import Material -from hub_api.helpers.session_helper import refresh_session +from persistence.models import UserRoles +from hub_api.helpers.auth import role_required -class Construction(Resource): +class Construction(Resource, Config): def __init__(self): - pass + super().__init__() - @staticmethod - def put(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - city = session.city + @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) + def put(self, city_id): + city = self.get_city(city_id) try: building_names = request.json['building_names'] constructions = request.json['constructions'] @@ -73,6 +71,6 @@ class Construction(Resource): response = {'result': 'succeed'} except KeyError as ex: response = {'error': f'Mandatory parameter {ex} is missing'} - return Response(json.dumps(response), headers=headers, status=400) + return Response(json.dumps(response), status=400) - return Response(json.dumps(response), headers=headers) + return Response(json.dumps(response), status=200) diff --git a/hub_api/docs/openapi-specs.yml b/hub_api/docs/openapi-specs.yml index afa35ab..78be2be 100644 --- a/hub_api/docs/openapi-specs.yml +++ b/hub_api/docs/openapi-specs.yml @@ -78,7 +78,131 @@ paths: $ref: '#/components/schemas/ApiResponse' security: - BearerAuth: [] - + /v1.4/heat-pump/{city_id}: + post: + tags: + - heatpump + summary: Create a heat pump simulation + operationId: createHeatpump + description: heatpump simulation with existing catalog data + parameters: + - in: header + name: appId + schema: + type: string + required: true + description: the Id of the application access this API + - in: path + name: city_id + schema: + type: integer + required: true + description: Numeric ID of the city to get + requestBody: + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/HeatPump' + required: true + responses: + '201': + description: Heatpump simulation created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/HeatPumpRes' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - BearerAuth: [ ] + /v1.4/city/{city_id}: + get: + tags: + - city + summary: Get a city + operationId: getCity + description: Retrieve a city with a given city ID + parameters: + - in: header + name: appId + schema: + type: string + required: true + description: the ID of the application access this API + - in: path + name: city_id + schema: + type: integer + required: true + description: Numeric ID of the city to get + responses: + '200': + description: City created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/City' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '404': + description: City not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - BearerAuth: [ ] /v1.4/user: post: tags: @@ -268,6 +392,86 @@ components: updated: type: string example: 2023-01-15 18:40:54.64877 + HeatPump: + type: object + properties: + StartYear: + type: integer + format: int64 + example: 10 + EndYear: + type: integer + format: int64 + example: 10 + MaximumHPEnergyInput: + type: float + example: 9.8382 + HoursOfStorageAtMaxDemand: + type: integer + format: int64 + example: 9 + BuildingSuppTemp: + type: integer + format: int64 + example: 40 + TemperatureDifference: + type: float + example: 9.8382 + FuelLHV: + type: float + example: 9.8382 + FuelPrice: + type: float + example: 9.8382 + FuelEF: + type: integer + format: int64 + example: 40 + FuelDensity: + type: float + example: 9.8382 + HPSupTemp: + type: float + example: 9.8382 + HeatPumpType: + type: string + example: Water to Water HP + enum: + - Water to Water HP + - Air Source HP + HeatPumpModel: + type: string + example: Water to Water HP + enum: + - ClimateMaster 156 kW + - ClimateMaster 256 kW + - ClimateMaster 335 kW + - 012 + - 015 + - 018 + - 023 + - 030 + - 033 + - 037 + - 044 + - 047 + - 057 + - 070 + - 087 + - 097 + - 102 + - 120 + - 130 + - 140 + SimType: + type: int + example: 1 + format: int64 + EnergyDemand: + type: array + items: + type: float + example: [ 610.610, 754.746, 288.338 ] User: type: object properties: @@ -289,6 +493,29 @@ components: enum: - Admin - Hub_Reader + HeatPumpRes: + type: object + properties: + hourly_electricity_demand: + type: array + items: + type: object + daily_electricity_demand: + type: array + items: + type: object + monthly_electricity_demand: + type: array + items: + type: object + daily_fossil_consumption: + type: array + items: + type: object + monthly_fossil_consumption: + type: array + items: + type: object Login: type: object properties: @@ -325,14 +552,14 @@ components: application/xml: schema: $ref: '#/components/schemas/User' - UserArray: - description: List of user object + CityArray: + description: List of city object content: application/json: schema: type: array items: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/City' securitySchemes: BearerAuth: type: http diff --git a/hub_api/energy_demand.py b/hub_api/energy_demand.py index 9c099fe..5685c3a 100644 --- a/hub_api/energy_demand.py +++ b/hub_api/energy_demand.py @@ -5,13 +5,15 @@ from pathlib import Path from geomeppy import IDF import os import glob -from hub_api.helpers.session_helper import refresh_session import hub_api.helpers.session_helper as sh import helpers.constants as cte import csv +from hub_api.helpers.auth import role_required +from persistence.models import UserRoles +from hub_api.config import Config -class EnergyDemand(Resource): +class EnergyDemand(Resource, Config): _THERMOSTAT = 'HVACTEMPLATE:THERMOSTAT' _IDEAL_LOAD_AIR_SYSTEM = 'HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM' _SURFACE = 'BUILDINGSURFACE:DETAILED' @@ -92,6 +94,7 @@ class EnergyDemand(Resource): def __init__(self): # this class is mostly hardcoded, as is intended to be used only for Dompark project, # other projects should use the normal idf workflow instead. + super().__init__() self._output_path = Path(Path(__file__).parent.parent / 'tmp').resolve() self._data_path = Path(Path(__file__).parent.parent / 'data').resolve() self._city = None @@ -314,13 +317,12 @@ class EnergyDemand(Resource): return - def get(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - self._city = session.city - self._greenery_percentage = round(float(session.greenery_percentage) / 10) * 10 + @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) + def get(self, city_id): + payload = request.get_json() + self._city = self.get_city(city_id) + + self._greenery_percentage = round(float(payload['greenery_percentage']) / 10) * 10 output_file = str((self._output_path / 'dompark.idf').resolve()) idd_file = str((self._data_path / 'energy+.idd').resolve()) epw_file = str((self._data_path / 'CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').resolve()) @@ -391,4 +393,4 @@ class EnergyDemand(Resource): 'lighting_demand': lighting, 'appliances_demand': appliances } - return Response(json.dumps(response), headers=headers) + return Response(json.dumps(response), status=200) diff --git a/hub_api/geometry.py b/hub_api/geometry.py index f63c562..9fdd68d 100644 --- a/hub_api/geometry.py +++ b/hub_api/geometry.py @@ -2,14 +2,14 @@ Geometry SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca +Code contributors: Peter Yefi peteryefi@gmail.com """ -import json -from flask import make_response, send_file, request, Response +from flask import make_response, send_file from flask_restful import Resource from pathlib import Path - -from hub_api.helpers.session_helper import refresh_session +from hub_api.helpers.auth import role_required +from persistence.models import UserRoles class Geometry(Resource): @@ -17,13 +17,9 @@ class Geometry(Resource): data_path = (Path(__file__).parent.parent / 'data').resolve() self._gtlf_path = (Path(data_path / 'DomparkBuilding.gltf')).resolve() + @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) def get(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) response = make_response(send_file(self._gtlf_path, as_attachment=True, mimetype='model/gltf+json, model/gltf-binary')) - response.headers['session_id'] = session.id - response.headers['token'] = session.token return response diff --git a/hub_api/greenery.py b/hub_api/greenery.py index 17fb5fc..96dc84e 100644 --- a/hub_api/greenery.py +++ b/hub_api/greenery.py @@ -2,36 +2,35 @@ Greenery SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca +Code contributors: Peter Yefi peteryefi@gmail.com """ import json from flask import Response, request from flask_restful import Resource - +from hub_api.config import Config from city_model_structure.greenery.vegetation import Vegetation from city_model_structure.greenery.soil import Soil from city_model_structure.greenery.plant import Plant import helpers.constants as cte -from hub_api.helpers.session_helper import refresh_session +from persistence.models import UserRoles +from hub_api.helpers.auth import role_required -class Greenery(Resource): +class Greenery(Resource, Config): def __init__(self): - pass + super().__init__() - @staticmethod - def put(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - city = session.city + @role_required([UserRoles.Admin.value]) + def put(self, city_id): + + city = self.get_city(city_id) try: - session.greenery_percentage = request.json['greenery_percentage'] - if session.greenery_percentage == 0: + greenery_percentage = request.json['greenery_percentage'] + if greenery_percentage == 0: response = {'result': 'succeed'} - return Response(json.dumps(response), headers=headers) + return Response(json.dumps(response), status=200) building_names = request.json['building_names'] vegetation_requested = request.json['vegetation'] @@ -80,6 +79,6 @@ class Greenery(Resource): response = {'result': 'succeed'} except KeyError as ex: response = {'error': f'Mandatory parameter {ex} is missing'} - return Response(json.dumps(response), headers=headers, status=400) + return Response(json.dumps(response), status=400) - return Response(json.dumps(response), headers=headers) + return Response(json.dumps(response)) diff --git a/hub_api/greenery_catalog.py b/hub_api/greenery_catalog.py index 1451626..504b86a 100644 --- a/hub_api/greenery_catalog.py +++ b/hub_api/greenery_catalog.py @@ -5,10 +5,8 @@ Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca """ import json - from flask import request, Response from flask_restful import Resource - from catalog_factories.data_models.greenery.plant import Plant from catalog_factories.data_models.greenery.soil import Soil from catalog_factories.data_models.greenery.vegetation import Vegetation diff --git a/hub_api/heat_pump.py b/hub_api/heat_pump.py index 19ff78f..54aa214 100644 --- a/hub_api/heat_pump.py +++ b/hub_api/heat_pump.py @@ -5,77 +5,35 @@ Copyright © 2022 Project Author Peter Yefi peteryefi@gmail.com """ import json -from flask import send_file, request, make_response, Response -from flask_apispec import use_kwargs, doc -from flask_apispec.views import MethodResource +from flask import request, Response +from hub_api.config import Config from flask_restful import Resource -from marshmallow import Schema, fields from hub_api.helpers.auth import role_required -from hub_api.helpers.session_helper import refresh_session from utils import HeatPumpSimulator from utils import validate_hp_model from persistence.models import UserRoles +from utils import expand_energy_demand +from hub_logger import logger -class HeatPumpPostData(Schema): - """ - Defines post data for heat-pump simulation - """ - StartYear = fields.Integer(required=True, description='Start year for simulation data') - EndYear = fields.Integer(required=True, description='End year for simulation data') - MaximumHPEnergyInput = fields.Float(required=True, description='Maximum heat pump energy input') - HoursOfStorageAtMaxDemand = fields.Integer(required=True, description='Hours of storage at maximum demand') - BuildingSuppTemp = fields.Integer(required=True, description='Building supply temperature') - TemperatureDifference = fields.Float(required=True, description='Temperature difference') - FuelLHV = fields.Float(required=True, description='Fuel LHV') - FuelPrice = fields.Float(required=True, description='Fuel price') - FuelEF = fields.Integer(required=True, description='Fuel EF') - FuelDensity = fields.Float(required=True, description='Fuel Density') - HPSupTemp = fields.Float(required=True, description='Heat pump supply temperature') - HeatPumpType = fields.String(required=True, description='Type of Heat pump', - enum=['Water to Water HP', 'Air Source HP']) - HeatPumpModel = fields.String(required=True, description='Model of heat pump to run simulation for', - enum=['ClimateMaster 156 kW', 'ClimateMaster 256 kW', 'ClimateMaster 335 kW', - '012', '015', '018', '023', '030', '033', '037', '044', '047', '057', '070', - '087', '097', '102', '120', '130', '140']) - SimType = fields.Integer(required=True, description='Series or Parallel simulation [0 for series, 1 for parallel', - enum=[0, 1]) - - -class HeatPump(MethodResource, Resource): +class HeatPump(Config, Resource): def __init__(self): - pass + super().__init__() - @doc(description='Heat pump simulation run', tags=['HeatPump']) - @use_kwargs(HeatPumpPostData) @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def post(self, **kwargs): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - city = session.city - if validate_hp_model(kwargs['HeatPumpType'], kwargs['HeatPumpModel']): + def post(self, city_id): + payload = request.get_json() + city = self.get_city(city_id) + if validate_hp_model(payload['HeatPumpType'], payload['HeatPumpModel']): + # expand energy demand values + expand_energy_demand(payload['EnergyDemand']) try: # Run simulation and return output file here - hp_simulator = HeatPumpSimulator(city, kwargs) - result_file = hp_simulator.run_hp_simulation() - response = self._send_response(result_file, session) - return response + hp_simulator = HeatPumpSimulator(city, payload) + results = hp_simulator.run_hp_simulation() + return Response(json.dumps(results), status=200) except Exception as err: - print(err) + logger.error(err) return Response(json.dumps({'error_message': 'Sorry an error occurred while running HP Simulation'})) else: return Response(json.dumps({'error_message': 'Wrong heat pump type/model combination'}), status=400) - - @staticmethod - def _send_response(result_file, session): - """ - Sends insel results file after simulation - :param result_file: the insel output file - :param session: session variable - :return CSV output file - """ - response = make_response(send_file(result_file, as_attachment=True, mimetype='text/csv')) - response.headers['session_id'] = session.id - response.headers['token'] = session.token - return response diff --git a/hub_api/helpers/auth.py b/hub_api/helpers/auth.py index 2c6e141..76fd2bd 100644 --- a/hub_api/helpers/auth.py +++ b/hub_api/helpers/auth.py @@ -1,3 +1,9 @@ +""" +HeatPump Service +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Project Author Peter Yefi peteryefi@gmail.com +""" + from datetime import datetime, timedelta, timezone from typing import Dict from jwt import JWT, jwk_from_pem @@ -48,14 +54,11 @@ def role_required(roles: [str]): try: token = request.headers['Authorization'].split()[1] user = validate_auth_token(token) - if user is None: return {'messages': 'You have not been authenticated'}, 401 allowed = auth_module(user['user']) - if user['user']['role'] == UserRoles.Admin.value and 'localhost' not in request.headers['Host']: allowed = False - if not allowed: return {'messages': 'You are not authorized'}, 403 return f(*args, **kwargs) diff --git a/hub_api/lca.py b/hub_api/lca.py index e2f84e1..b4e516a 100644 --- a/hub_api/lca.py +++ b/hub_api/lca.py @@ -2,19 +2,23 @@ LCA SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2021 Project Author name Atiya +Code contributors: Peter Yefi peteryefi@gmail.com """ import json -from flask import request, Response +from flask import Response from flask_restful import Resource from lca_calculations import LcaCalculations -from hub_api.helpers.session_helper import refresh_session from itertools import groupby from operator import itemgetter +from hub_api.helpers.auth import role_required +from hub_api.config import Config +from persistence.models import UserRoles -class MaterialLCACatalog(Resource): + +class MaterialLCACatalog(Resource, Config): def __init__(self): - pass + super().__init__() @staticmethod def get_lca_value(city, nrel_id = None): @@ -56,40 +60,30 @@ class MaterialLCACatalog(Resource): return material.embodied_carbon, material.id, material.type, material.name, material.density # return material.embodied_carbon - - - def get(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - city = session.city - + @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) + def get(self, city_id): + city = self.get_city(city_id) try: - return Response(json.dumps(self.get_lca_value(city)), headers=headers) + return Response(json.dumps(self.get_lca_value(city)), status=200) except ValueError: - response = {'error': f'No Catalog Available'} - return Response(json.dumps(response), headers=headers, status=400) + response = {'err_msg': f'No Catalog Available'} + return Response(json.dumps(response), status=400) -class MaterialLCACalculations(Resource): +class MaterialLCACalculations(Resource, Config): """ LCA class """ def __init__(self): - pass + super().__init__() - @staticmethod - def get(): + @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) + def get(self, city_id): """ Auto-method for processing the lca request :return: lca demand """ - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - city = session.city + city = self.get_city(city_id) materials_lca = {'Wall': [], 'Ground': [], 'Roof': []} for building in city.buildings: @@ -131,4 +125,4 @@ class MaterialLCACalculations(Resource): total_embodied_carbon['end_of_life_carbon'] = sum_end_of_life materials_embodied_carbon[key].append(total_embodied_carbon) - return Response(json.dumps(materials_embodied_carbon), headers=headers) + return Response(json.dumps(materials_embodied_carbon), status=200) diff --git a/hub_api/usage.py b/hub_api/usage.py index f798c83..8d127f7 100644 --- a/hub_api/usage.py +++ b/hub_api/usage.py @@ -2,6 +2,7 @@ Usage SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca +Code contributors: Peter Yefi peteryefi@gmail.com """ import json @@ -14,22 +15,22 @@ from city_model_structure.building_demand.lighting import Lighting from city_model_structure.building_demand.occupancy import Occupancy from city_model_structure.building_demand.thermal_control import ThermalControl from city_model_structure.building_demand.usage_zone import UsageZone -from hub_api.helpers.session_helper import refresh_session import helpers.constants as cte +from hub_api.helpers.auth import role_required +from hub_api.config import Config +from persistence.models import UserRoles -class Usage(Resource): +class Usage(Resource, Config): def __init__(self): - pass + super().__init__() + + @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) + def put(self, city_id): + + city = self.get_city(city_id) + catalog = request.json['usage_catalog'] - @staticmethod - def put(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - city = session.city - catalog = session.usage_catalog usage_name = None try: building_names = request.json['building_names'] @@ -146,8 +147,8 @@ class Usage(Resource): response = {'result': 'succeed'} except KeyError as ex: response = {'error': f'Mandatory parameter {ex} is missing'} - return Response(json.dumps(response), headers=headers, status=400) + return Response(json.dumps(response), status=400) except IndexError: response = {'error': f'Name "{usage_name}" unknown'} - return Response(json.dumps(response), headers=headers, status=400) - return Response(json.dumps(response), headers=headers) + return Response(json.dumps(response), status=400) + return Response(json.dumps(response)) diff --git a/hub_api/usage_catalog.py b/hub_api/usage_catalog.py index dfa1ec7..026df07 100644 --- a/hub_api/usage_catalog.py +++ b/hub_api/usage_catalog.py @@ -97,6 +97,7 @@ class ToJson: } return schedule_dictionary + class UsageCatalogEntry(Resource): def __init__(self): pass diff --git a/hub_api/user.py b/hub_api/user.py index 5462594..e2e97b5 100644 --- a/hub_api/user.py +++ b/hub_api/user.py @@ -1,7 +1,7 @@ """ HeatPump Service SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Peter Yefi peteryefi@gmail.com +Copyright © 2023 Project Author Peter Yefi peteryefi@gmail.com """ import json from flask import Response, request From aa9cc24f29b352d2154a951d41afd96b17dbb8a0 Mon Sep 17 00:00:00 2001 From: Peter Yefi Date: Tue, 17 Jan 2023 19:02:49 -0500 Subject: [PATCH 03/10] Refactored city removal from session --- bootstrap.py | 14 +++++++------- gamification.py | 13 ------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/bootstrap.py b/bootstrap.py index 79f2d92..0f195b6 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -29,7 +29,7 @@ from hub_api.session import SessionStart, SessionEnd, KeepSessionAlive from hub_api.uptime import Uptime from hub_api.greenery import Greenery from hub_api.user import User, UserLogin -from flasgger import LazyJSONEncoder, LazyString, Swagger +from flasgger import LazyJSONEncoder, Swagger app = flask.Flask('gamification') app.json_encoder = LazyJSONEncoder @@ -64,21 +64,21 @@ api.add_resource(GreeneryCatalogNames, '/v1.4/greenery-catalog/names') api.add_resource(ConstructionCatalogEntry, '/v1.4/construction-catalog/entry') api.add_resource(ConstructionCatalogEntries, '/v1.4/construction-catalog/entries') api.add_resource(ConstructionCatalogNames, '/v1.4/construction-catalog/names') -api.add_resource(Construction, '/v1.4/construction') +api.add_resource(Construction, '/v1.4/construction/') api.add_resource(UsageCatalogEntry, '/v1.4/usage-catalog/entry') api.add_resource(UsageCatalogEntries, '/v1.4/usage-catalog/entries') api.add_resource(UsageCatalogNames, '/v1.4/usage-catalog/names') api.add_resource(Usage, '/v1.4/usage') -api.add_resource(EnergyDemand, '/v1.4/energy-demand') +api.add_resource(EnergyDemand, '/v1.4/energy-demand/') # api.add_resource(LCA, '/v1.4/lca') -api.add_resource(MaterialLCACatalog, '/v1.4/material_lca_catalog/entries') -api.add_resource(MaterialLCACalculations, '/v1.4/material_lca_catalog/calculations') -api.add_resource(HeatPump, '/v1.4/heat-pump') +api.add_resource(MaterialLCACatalog, '/v1.4/material_lca_catalog/entries/') +api.add_resource(MaterialLCACalculations, '/v1.4/material_lca_catalog/calculations/') +api.add_resource(HeatPump, '/v1.4/heat-pump/') api.add_resource(User, '/v1.4/user') api.add_resource(UserLogin, '/v1.4/user/login') api.add_resource(SessionStart, '/v1.4/session/start') api.add_resource(SessionEnd, '/v1.4/session/end') api.add_resource(KeepSessionAlive, '/v1.4/session/keep_alive') -api.add_resource(CityInfo, '/v1.4/city_info') +api.add_resource(CityInfo, '/v1.4/city/') api.add_resource(City, '/v1.4/city') api.add_resource(Greenery, '/v1.4/greenery') diff --git a/gamification.py b/gamification.py index 1599e1e..211fa75 100644 --- a/gamification.py +++ b/gamification.py @@ -53,19 +53,6 @@ WeatherFactory('epw', city, file_name=montreal_weather_file).enrich() city.name = 'Montreal' city.climate_reference_city = 'Montreal' -# SRA Calculations - -for building in city.buildings: - building.heated = True - building.cooled = True - building.attic_heated = 0 - building.basement_heated = 0 - for surface in building.surfaces: - surface.swr = 0.2 - -# Pass the city to the session helper to be used as default status. -sh.city = city - @app.route("/") def home(): From 582c18ef0c49e61b55d133be1ee16baf68b74f25 Mon Sep 17 00:00:00 2001 From: Peter Yefi Date: Tue, 17 Jan 2023 20:10:20 -0500 Subject: [PATCH 04/10] included db import reference to config --- hub_api/city_info.py | 12 +++++------- hub_api/config.py | 3 +++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/hub_api/city_info.py b/hub_api/city_info.py index 080dece..4a356aa 100644 --- a/hub_api/city_info.py +++ b/hub_api/city_info.py @@ -12,7 +12,6 @@ from persistence.models import UserRoles from hub_logger import logger from imports.geometry_factory import GeometryFactory from pathlib import Path -from imports.db_factory import DBFactory import os from hub_api.config import Config @@ -74,12 +73,12 @@ class CityInfo(Resource, Config): return Response(json.dumps(response), status=200) -class City(Resource): +class City(Resource, Config): def __init__(self): - pass + super().__init__() - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) + @role_required([UserRoles.Admin.value]) def post(self): allowed_ext = {'gml', '3dm', 'xml', 'obj', 'rhino'} try: @@ -96,9 +95,8 @@ class City(Resource): file_path = (Path(__file__).parent.parent / 'data/uploaded_city/{}'.format(city_file.filename)).resolve() city_file.save(file_path) city = GeometryFactory(city_file_type, file_path).city - db_factory = DBFactory(city=city, db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) - saved_city = db_factory.persist_city(1) + + saved_city = self.import_db_factory.persist_city(g.user['id'], city) if os.path.exists(file_path): os.remove(file_path) if type(saved_city) is not dict: diff --git a/hub_api/config.py b/hub_api/config.py index 1ca930e..6e970a2 100644 --- a/hub_api/config.py +++ b/hub_api/config.py @@ -4,6 +4,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2023 Project Peter Yefi peteryefi@gmail.com """ from exports.db_factory import DBFactory as CityExportFactory +from imports.db_factory import DBFactory import os import pickle @@ -13,6 +14,8 @@ class Config: def __init__(self): self.factory = CityExportFactory(db_name='hub_prod', app_env='PROD', dotenv_path="{}/.env".format(os.path.expanduser('~'))) + self.import_db_factory = DBFactory(db_name='hub_prod', app_env='PROD', + dotenv_path="{}/.env".format(os.path.expanduser('~'))) def get_city(self, city_id): city_obj = self.factory.get_city(city_id) From c462d75e084b1b711bde4e4034810035853ba580 Mon Sep 17 00:00:00 2001 From: Peter Yefi Date: Tue, 17 Jan 2023 20:13:54 -0500 Subject: [PATCH 05/10] Renamed export factory --- hub_api/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hub_api/config.py b/hub_api/config.py index 6e970a2..aa68fd3 100644 --- a/hub_api/config.py +++ b/hub_api/config.py @@ -12,13 +12,13 @@ import pickle class Config: def __init__(self): - self.factory = CityExportFactory(db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) + self.export_db_factory = CityExportFactory(db_name='hub_prod', app_env='PROD', + dotenv_path="{}/.env".format(os.path.expanduser('~'))) self.import_db_factory = DBFactory(db_name='hub_prod', app_env='PROD', dotenv_path="{}/.env".format(os.path.expanduser('~'))) def get_city(self, city_id): - city_obj = self.factory.get_city(city_id) + city_obj = self.export_db_factory.get_city(city_id) city = pickle.loads(city_obj.city) for building in city.buildings: building.heated = True From fdd4a31ac0412be5397a7dafa663b91327a15257 Mon Sep 17 00:00:00 2001 From: Peter Yefi Date: Fri, 20 Jan 2023 13:41:46 -0500 Subject: [PATCH 06/10] Added openapi specs for construction and construction catalog --- hub_api/construction_catalog.py | 10 +- hub_api/docs/openapi-specs.yml | 309 ++++++++++++++++++++++++++++++++ 2 files changed, 315 insertions(+), 4 deletions(-) diff --git a/hub_api/construction_catalog.py b/hub_api/construction_catalog.py index b1c50d7..8607026 100644 --- a/hub_api/construction_catalog.py +++ b/hub_api/construction_catalog.py @@ -14,6 +14,8 @@ from catalog_factories.data_models.construction.construction import Construction from catalog_factories.data_models.construction.material import Material from catalog_factories.data_models.construction.window import Window from hub_api.helpers.session_helper import refresh_session +from hub_api.helpers.auth import role_required +from persistence.models import UserRoles class ToJson: @@ -96,8 +98,8 @@ class ConstructionCatalogEntry(Resource): def __init__(self): pass - @staticmethod - def post(): + @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) + def post(self): session = refresh_session(request) if session is None: return Response(json.dumps({'error': 'invalid session'}), status=401) @@ -129,8 +131,8 @@ class ConstructionCatalogEntries(Resource): def __init__(self): pass - @staticmethod - def post(): + @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) + def post(self): session = refresh_session(request) if session is None: return Response(json.dumps({'error': 'invalid session'}), status=401) diff --git a/hub_api/docs/openapi-specs.yml b/hub_api/docs/openapi-specs.yml index 78be2be..73d3dde 100644 --- a/hub_api/docs/openapi-specs.yml +++ b/hub_api/docs/openapi-specs.yml @@ -342,6 +342,180 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiResponse' + /v1.4/construction/{city_id}: + put: + tags: + - construction + summary: updates a city with building construction + description: updates a city with building construction + operationId: updateConstruction + parameters: + - in: header + name: appId + schema: + type: string + required: true + description: the Id of the application access this API + - in: path + name: city_id + schema: + type: integer + required: true + description: Numeric ID of the city to get + requestBody: + description: creates building construction catalog + content: + application/json: + schema: + $ref: '#/components/schemas/Construction' + application/xml: + schema: + $ref: '#/components/schemas/Construction' + responses: + '201': + description: city updated with building construction successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Construction' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - BearerAuth: [ ] + /v1.4/construction-catalog/entries: + post: + tags: + - construction-catalog + summary: creates building construction catalog entries + description: create the construction catalog entries + operationId: catalogEntries + parameters: + - in: header + name: appId + schema: + type: string + required: true + description: the Id of the application access this API + requestBody: + description: creates building construction catalog + content: + application/json: + schema: + $ref: '#/components/schemas/ConstructionCatalogEntries' + application/xml: + schema: + $ref: '#/components/schemas/ConstructionCatalogEntries' + responses: + '201': + description: Construction catalog entries created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ConstructionCatalogEntries' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - BearerAuth: [ ] + /v1.4/construction-catalog/entry: + post: + tags: + - construction-catalog + summary: creates building construction catalog + description: create the construction catalog of a building + operationId: catalogEntry + parameters: + - in: header + name: appId + schema: + type: string + required: true + description: the Id of the application access this API + requestBody: + description: creates building construction catalog + content: + application/json: + schema: + $ref: '#/components/schemas/ConstructionCatalogEntry' + application/xml: + schema: + $ref: '#/components/schemas/ConstructionCatalogEntry' + responses: + '201': + description: Construction catalog created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ConstructionCatalogEntry' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - BearerAuth: [ ] components: schemas: City: @@ -472,6 +646,132 @@ components: items: type: float example: [ 610.610, 754.746, 288.338 ] + required: + - SimType + - EnergyDemand + - HeatPumpModel + - HeatPumpType + - FuelEF + - FuelDensity + - FuelPrice + - FuelLHV + - BuildingSuppTemp + - TemperatureDifference + - HoursOfStorageAtMaxDemand + - MaximumHPEnergyInput + - StartYear + - EndYear + ConstructionCatalogEntry: + type: object + properties: + name: + type: string + example: 189.1-2009 Nonres 4B Ext Wall Mass + required: + - name + ConstructionCatalogEntries: + type: object + properties: + category: + type: string + example: archetypes + required: + - category + Construction: + type: object + properties: + building_names: + type: array + items: + type: string + example: ["Dompark"] + extra_loses_due_to_thermal_bridges: + type: float + example: 0.15 + indirect_heated_ratio: + type: float + example: 0.15 + infiltration_rate_for_ventilation_system_off: + type: float + example: 0.5 + infiltration_rate_for_ventilation_system_on: + type: float + example: 0.0 + constructions: + type: array + items: + type: object + properties: + name: + type: string + example: 189.1-2009 Res 4B Ext Wall Steel-Framed + type: + type: string + example: Wall + layers: + type: array + items: + type: object + $ref: '#components/schemas/Layers' + + required: + - building_names + - extra_loses_due_to_thermal_bridges + - indirect_heated_ratio + - infiltration_rate_for_ventilation_system_off + - infiltration_rate_for_ventilation_system_on + - constructions + Layers: + type: object + properties: + name: + type: string + example: Layer 1 + thickness: + type: float + example: 0.0 + material: + type: object + $ref: '#/components/schemas/Material' + Material: + type: object + properties: + id: + type: integer + format: int64 + example: 12 + name: + type: string + example: MAT-SHEAT + solar_absorptance: + type: float + example: 0.7 + thermal_absorptance: + type: float + example: 0.9 + visible_absorptance: + type: float + example: 0.7 + no_mass: + type: string + example: "True" + thermal_resistance: + type: float + example: 0.36256 + conductivity: + type: string + density: + type: string + specific_heat: + type: string + required: + - id + - name + - solar_absorptance + - thermal_absorptance + - visible_absorptance + - no_mass + - thermal_resistance User: type: object properties: @@ -484,6 +784,7 @@ components: example: Peter Yefi email: type: string + format: email example: peteryefi@gmail.com password: type: string @@ -493,6 +794,11 @@ components: enum: - Admin - Hub_Reader + required: + - name + - email + - password + - role HeatPumpRes: type: object properties: @@ -525,6 +831,9 @@ components: password: type: string example: 'Hub@183838' + required: + - email + - password LoginRes: type: object properties: From bc76ac86d1995a3e6851101d3bbdba7eae35a17b Mon Sep 17 00:00:00 2001 From: Peter Yefi Date: Fri, 20 Jan 2023 14:01:40 -0500 Subject: [PATCH 07/10] Added documentation for energy demand --- hub_api/docs/openapi-specs.yml | 73 +++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/hub_api/docs/openapi-specs.yml b/hub_api/docs/openapi-specs.yml index 73d3dde..0eed27a 100644 --- a/hub_api/docs/openapi-specs.yml +++ b/hub_api/docs/openapi-specs.yml @@ -342,6 +342,59 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiResponse' + /v1.4/energy-demand/{city_id}: + get: + tags: + - energy-demand + summary: Get energy demand + description: Retrieve energy demand data + operationId: getEnergyDemand + parameters: + - in: header + name: appId + schema: + type: string + required: true + description: the Id of the application access this API + - in: path + name: city_id + schema: + type: integer + required: true + description: Numeric ID of the city to get + responses: + '200': + description: Successfully retrieved energy demand data + content: + application/json: + schema: + $ref: '#/components/schemas/EnergyDemand' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - BearerAuth: [ ] /v1.4/construction/{city_id}: put: tags: @@ -713,7 +766,6 @@ components: items: type: object $ref: '#components/schemas/Layers' - required: - building_names - extra_loses_due_to_thermal_bridges @@ -822,6 +874,25 @@ components: type: array items: type: object + EnergyDemand: + type: object + properties: + heating_demand: + type: array + items: + type: object + cooling_demand: + type: array + items: + type: object + lighting_demand: + type: array + items: + type: object + appliances_demand: + type: array + items: + type: object Login: type: object properties: From 4b322148ba169dc42f534a20466dc3fce055f5f4 Mon Sep 17 00:00:00 2001 From: Peter Yefi Date: Mon, 30 Jan 2023 19:45:02 -0500 Subject: [PATCH 08/10] Removed unneeded files --- bootstrap.py | 45 +-- gamification.py | 62 ---- hub_api/city_info.py | 65 +--- hub_api/config.py | 16 +- hub_api/construction.py | 76 ----- hub_api/construction_catalog.py | 196 ------------ hub_api/docs/openapi-specs.yml | 546 +------------------------------- hub_api/energy_demand.py | 396 ----------------------- hub_api/geometry.py | 25 -- hub_api/greenery.py | 84 ----- hub_api/greenery_catalog.py | 178 ----------- hub_api/heat_pump.py | 39 --- hub_api/helpers/auth.py | 4 +- hub_api/lca.py | 128 -------- hub_api/usage.py | 154 --------- hub_api/usage_catalog.py | 156 --------- hub_api/user.py | 8 +- 17 files changed, 35 insertions(+), 2143 deletions(-) delete mode 100644 gamification.py delete mode 100644 hub_api/construction.py delete mode 100644 hub_api/construction_catalog.py delete mode 100644 hub_api/energy_demand.py delete mode 100644 hub_api/geometry.py delete mode 100644 hub_api/greenery.py delete mode 100644 hub_api/greenery_catalog.py delete mode 100644 hub_api/heat_pump.py delete mode 100644 hub_api/lca.py delete mode 100644 hub_api/usage.py delete mode 100644 hub_api/usage_catalog.py diff --git a/bootstrap.py b/bootstrap.py index 0f195b6..5c14aab 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -9,33 +9,16 @@ import flask import yaml from flask_restful import Api from hub_api.city_info import CityInfo, City -from hub_api.geometry import Geometry -from hub_api.greenery_catalog import GreeneryCatalogEntries -from hub_api.greenery_catalog import GreeneryCatalogEntry -from hub_api.greenery_catalog import GreeneryCatalogNames -from hub_api.construction_catalog import ConstructionCatalogEntries -from hub_api.construction_catalog import ConstructionCatalogEntry -from hub_api.construction_catalog import ConstructionCatalogNames -from hub_api.usage_catalog import UsageCatalogEntries -from hub_api.usage_catalog import UsageCatalogEntry -from hub_api.usage_catalog import UsageCatalogNames -from hub_api.heat_pump import HeatPump -from hub_api.lca import MaterialLCACatalog -from hub_api.lca import MaterialLCACalculations -from hub_api.construction import Construction -from hub_api.usage import Usage -from hub_api.energy_demand import EnergyDemand from hub_api.session import SessionStart, SessionEnd, KeepSessionAlive from hub_api.uptime import Uptime -from hub_api.greenery import Greenery from hub_api.user import User, UserLogin from flasgger import LazyJSONEncoder, Swagger +from flask import Response app = flask.Flask('gamification') app.json_encoder = LazyJSONEncoder api = Api(app) - with open("hub_api/docs/openapi-specs.yml", "r") as stream: swagger_config = { "headers": [], @@ -57,23 +40,6 @@ with open("hub_api/docs/openapi-specs.yml", "r") as stream: print(exc) api.add_resource(Uptime, '/v1.4/uptime') -api.add_resource(Geometry, '/v1.4/geometry') -api.add_resource(GreeneryCatalogEntry, '/v1.4/greenery-catalog/entry') -api.add_resource(GreeneryCatalogEntries, '/v1.4/greenery-catalog/entries') -api.add_resource(GreeneryCatalogNames, '/v1.4/greenery-catalog/names') -api.add_resource(ConstructionCatalogEntry, '/v1.4/construction-catalog/entry') -api.add_resource(ConstructionCatalogEntries, '/v1.4/construction-catalog/entries') -api.add_resource(ConstructionCatalogNames, '/v1.4/construction-catalog/names') -api.add_resource(Construction, '/v1.4/construction/') -api.add_resource(UsageCatalogEntry, '/v1.4/usage-catalog/entry') -api.add_resource(UsageCatalogEntries, '/v1.4/usage-catalog/entries') -api.add_resource(UsageCatalogNames, '/v1.4/usage-catalog/names') -api.add_resource(Usage, '/v1.4/usage') -api.add_resource(EnergyDemand, '/v1.4/energy-demand/') -# api.add_resource(LCA, '/v1.4/lca') -api.add_resource(MaterialLCACatalog, '/v1.4/material_lca_catalog/entries/') -api.add_resource(MaterialLCACalculations, '/v1.4/material_lca_catalog/calculations/') -api.add_resource(HeatPump, '/v1.4/heat-pump/') api.add_resource(User, '/v1.4/user') api.add_resource(UserLogin, '/v1.4/user/login') api.add_resource(SessionStart, '/v1.4/session/start') @@ -81,4 +47,11 @@ api.add_resource(SessionEnd, '/v1.4/session/end') api.add_resource(KeepSessionAlive, '/v1.4/session/keep_alive') api.add_resource(CityInfo, '/v1.4/city/') api.add_resource(City, '/v1.4/city') -api.add_resource(Greenery, '/v1.4/greenery') + + +@app.route("/") +def home(): + return Response(headers={'Access-Control-Allow-Origin': '*'}) + + +app.run(port=15789, host="0.0.0.0", debug=False) diff --git a/gamification.py b/gamification.py deleted file mode 100644 index 211fa75..0000000 --- a/gamification.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Main -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2021 Project Author name guillermo.gutierrezmorote@concordia.ca -Project Collaborator name peteryefi@gmail.com -""" - -from catalog_factories.construction_catalog_factory import ConstructionCatalogFactory -from catalog_factories.greenery_catalog_factory import GreeneryCatalogFactory -from catalog_factories.usage_catalog_factory import UsageCatalogFactory -from imports.construction_factory import ConstructionFactory -from imports.geometry_factory import GeometryFactory -from imports.life_cycle_assessment_factory import LifeCycleAssessment -# from imports.schedules_factory import SchedulesFactory -from imports.usage_factory import UsageFactory -from imports.weather_factory import WeatherFactory -from flask import Response -import hub_api.helpers.session_helper as sh -import datetime -from pathlib import Path -from bootstrap import app - -sh.begin_time = datetime.datetime.now() -# initialize catalogs -sh.greenery_catalog = GreeneryCatalogFactory('nrel').catalog -sh.construction_catalog = ConstructionCatalogFactory('nrel').catalog -sh.usage_catalog = UsageCatalogFactory('comnet').catalog - -# Enrich the city -data_path = (Path(__file__).parent / 'data').resolve() -rihno_path = (Path(data_path / 'dompark.3dm')).resolve() -city = GeometryFactory('rhino', rihno_path).city - -for building in city.buildings: - # Rihno files have no information about the function or the year of construction - building.year_of_construction = 1995 - building.function = 'industry' - building.human_readable_name = "Dompark" - -ConstructionFactory('nrel', city).enrich() -UsageFactory('comnet', city).enrich() -# SchedulesFactory('comnet', city).enrich() -LifeCycleAssessment('material', city).enrich() -LifeCycleAssessment('machine', city).enrich() -LifeCycleAssessment('fuel', city).enrich() -LifeCycleAssessment('vehicle', city).enrich() - -montreal_weather_file = (Path(__file__).parent / './data/CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').resolve() -city.climate_file = (data_path / f'{city.climate_reference_city}.cli').resolve() -WeatherFactory('epw', city, file_name=montreal_weather_file).enrich() - -# Rihno files have no information about the building location -city.name = 'Montreal' -city.climate_reference_city = 'Montreal' - - -@app.route("/") -def home(): - return Response(headers={'Access-Control-Allow-Origin': '*'}) - - -app.run(port=15789, host="0.0.0.0", debug=False) diff --git a/hub_api/city_info.py b/hub_api/city_info.py index 4a356aa..afe2e1a 100644 --- a/hub_api/city_info.py +++ b/hub_api/city_info.py @@ -8,9 +8,9 @@ import json from flask import Response, request, g from flask_restful import Resource from hub_api.helpers.auth import role_required -from persistence.models import UserRoles -from hub_logger import logger -from imports.geometry_factory import GeometryFactory +from hub.persistence.models import UserRoles +from hub.hub_logger import logger +from hub.imports.geometry_factory import GeometryFactory from pathlib import Path import os from hub_api.config import Config @@ -23,54 +23,17 @@ class CityInfo(Resource, Config): @role_required([UserRoles.Admin.value]) def get(self, city_id): city = self.get_city(city_id) - - # TODO: this is only for dompark project and need to be removed in future versions. - floor_area = 0 - wall_construction = 'unknown' - floor_construction = 'unknown' - roof_construction = 'unknown' - window_type = 'unknown' - building_dic = {} - for building in city.buildings: - - usages = [] # This is only valid for dompark project as all the building have the same usage - if building.lower_corner[2] == 0: - floor_area += building.floor_area - for internal_zone in building.internal_zones: - for usage_zone in internal_zone.usage_zones: - usages.append({'percentage': usage_zone.percentage, 'usage': usage_zone.usage}) - for thermal_zone in internal_zone.thermal_zones: - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.parent_surface.type == 'Ground': - floor_construction = thermal_boundary.construction_name - elif thermal_boundary.parent_surface.type == 'Wall': - wall_construction = thermal_boundary.construction_name - for thermal_opening in thermal_boundary.thermal_openings: - if thermal_opening.construction_name is not None: - window_type = thermal_opening.construction_name - break - else: - roof_construction = thermal_boundary.construction_name - name = building.human_readable_name - year_of_construction = str(building.year_of_construction) - building_dic = { - 'name': str(name), - 'floor_area': str(floor_area), - 'year_of_construction': str(year_of_construction), - 'usages': usages, - 'wall_construction': wall_construction, - 'floor_construction': floor_construction, - 'roof_construction': roof_construction, - 'window_type': window_type, - 'default_archetype': 'industry ASHRAE_2004:4A non_standard_dompark' - } - - buildings = [building_dic] - response = {'city_name': city.name, - 'climate_reference_city': str(city.climate_reference_city), - 'buildings': buildings - } - return Response(json.dumps(response), status=200) + print(city.name) + if city: + return Response(response=json.dumps({ + 'id': city.id, 'name': city.name, 'srs_name': city.srs_name, + 'time_zone': city.time_zone, 'version': city.city_version, 'country': city.country_code, + 'lat': city.latitude, 'lon': city.longitude, 'lower_corner': city.lower_corner, + 'upper_corner': city.upper_corner, 'created': city.created, 'updated': city.updated, + 'user': {'id': city.user.id, 'name': city.user.name, 'email': city.user.email, + 'role': city.user.role.value} + }, default=str), status=200) + return Response(response=json.dumps({'err_msg': 'City not found'}), status=404) class City(Resource, Config): diff --git a/hub_api/config.py b/hub_api/config.py index aa68fd3..a00b6ca 100644 --- a/hub_api/config.py +++ b/hub_api/config.py @@ -3,10 +3,9 @@ Config SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2023 Project Peter Yefi peteryefi@gmail.com """ -from exports.db_factory import DBFactory as CityExportFactory -from imports.db_factory import DBFactory +from hub.exports.db_factory import DBFactory as CityExportFactory +from hub.imports.db_factory import DBFactory import os -import pickle class Config: @@ -18,13 +17,4 @@ class Config: dotenv_path="{}/.env".format(os.path.expanduser('~'))) def get_city(self, city_id): - city_obj = self.export_db_factory.get_city(city_id) - city = pickle.loads(city_obj.city) - for building in city.buildings: - building.heated = True - building.cooled = True - building.attic_heated = 0 - building.basement_heated = 0 - for surface in building.surfaces: - surface.swr = 0.2 - return city + return self.export_db_factory.get_city(city_id) diff --git a/hub_api/construction.py b/hub_api/construction.py deleted file mode 100644 index 7d371b5..0000000 --- a/hub_api/construction.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Construction -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -Code contributors: Peter Yefi peteryefi@gmail.com -""" -import json -import uuid - -from flask import Response, request -from flask_restful import Resource -from hub_api.config import Config -from city_model_structure.building_demand.layer import Layer -from city_model_structure.building_demand.material import Material -from persistence.models import UserRoles -from hub_api.helpers.auth import role_required - - -class Construction(Resource, Config): - def __init__(self): - super().__init__() - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def put(self, city_id): - city = self.get_city(city_id) - try: - building_names = request.json['building_names'] - constructions = request.json['constructions'] - for construction in constructions: - construction_type = construction['type'] - layers = construction['layers'] - for building_name in building_names: - for building in city.buildings: - if building.human_readable_name != building_name: - continue - for internal_zone in building.internal_zones: - for thermal_zone in internal_zone.thermal_zones: - thermal_zone.additional_thermal_bridge_u_value = request.json['extra_loses_due_to_thermal_bridges'] - thermal_zone.indirect_heated_area_ratio = request.json['indirect_heated_ratio'] - thermal_zone.infiltration_rate_system_on = request.json['infiltration_rate_for_ventilation_system_on'] - thermal_zone.infiltration_rate_system_off = request.json['infiltration_rate_for_ventilation_system_off'] - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.parent_surface.type == construction_type: - _layers = [] - for layer in layers: - _layer = Layer() - thermal_resistance = layer['material']['thermal_resistance'] - conductivity = layer['material']['conductivity'] - density = layer['material']['density'] - specific_heat = layer['material']['specific_heat'] - _layer.thickness = layer['thickness'] - _material = Material() - _material.id = layer['material']['id'] - _material.name = layer['material']['name'] - _material.solar_absorptance = layer['material']['solar_absorptance'] - _material.thermal_absorptance = layer['material']['thermal_absorptance'] - _material.visible_absorptance = layer['material']['visible_absorptance'] - _material.no_mass = layer['material']['no_mass'] - _material.thermal_resistance = (thermal_resistance if thermal_resistance != '' else None) - _material.conductivity = (conductivity if conductivity != '' else None) - _material.density = (density if density != '' else None) - _material.specific_heat = (specific_heat if specific_heat != '' else None) - _layer.material = _material - _layers.append(_layer) - thermal_boundary.layers = _layers - if 'window' in construction.keys(): - for thermal_opening in thermal_boundary.thermal_openings: - thermal_opening.frame_ratio = construction['window']['frame_ratio'] - thermal_opening.g_value = construction['window']['g_value'] - thermal_opening.overall_u_value = construction['window']['overall_u_value'] - response = {'result': 'succeed'} - except KeyError as ex: - response = {'error': f'Mandatory parameter {ex} is missing'} - return Response(json.dumps(response), status=400) - - return Response(json.dumps(response), status=200) diff --git a/hub_api/construction_catalog.py b/hub_api/construction_catalog.py deleted file mode 100644 index 8607026..0000000 --- a/hub_api/construction_catalog.py +++ /dev/null @@ -1,196 +0,0 @@ -""" -Construction catalog -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -""" - -import json - -from flask import request, Response -from flask_restful import Resource - -from catalog_factories.data_models.construction.archetype import Archetype -from catalog_factories.data_models.construction.construction import Construction -from catalog_factories.data_models.construction.material import Material -from catalog_factories.data_models.construction.window import Window -from hub_api.helpers.session_helper import refresh_session -from hub_api.helpers.auth import role_required -from persistence.models import UserRoles - - -class ToJson: - @staticmethod - def archetype_to_json(archetype): - constructions = [] - for construction in archetype.constructions: - constructions.append(ToJson.construction_to_json(construction)) - dictionary = { - 'name': archetype.name, - 'function': archetype.function, - 'construction_period': archetype.construction_period, - 'average_storey_height': archetype.average_storey_height, - 'thermal_capacity': archetype.thermal_capacity, - 'extra_loses_due_to_thermal_bridges': archetype.extra_loses_due_to_thermal_bridges, - 'indirect_heated_ratio': archetype.indirect_heated_ratio, - 'infiltration_rate_for_ventilation_system_off': archetype.infiltration_rate_for_ventilation_system_off, - 'infiltration_rate_for_ventilation_system_on': archetype.infiltration_rate_for_ventilation_system_on, - 'constructions': constructions - } - return dictionary - - @staticmethod - def construction_to_json(construction): - layers = [] - for layer in construction.layers: - layers.append(ToJson.layer_to_json(layer)) - dictionary = {'name': construction.name, - 'type': construction.type, - 'layers': layers - } - if construction.window is not None: - dictionary['window_ratio'] = construction.window_ratio - dictionary['window'] = ToJson.window_to_json(construction.window) - return dictionary - - @staticmethod - def window_to_json(window): - if window is None: - return {} - dictionary = { - 'name': window.name, - 'frame_ratio': window.frame_ratio, - 'g_value': window.g_value, - 'overall_u_value': str(window.overall_u_value) - } - return dictionary - - @staticmethod - def layer_to_json(layer): - dictionary = {'name': layer.name, - 'thickness': layer.thickness, - 'material': ToJson.material_to_json(layer.material), - } - return dictionary - - @staticmethod - def material_to_json(material): - dictionary = {'id': material.id, - 'name': material.name, - 'solar_absorptance': material.solar_absorptance, - 'thermal_absorptance': material.thermal_absorptance, - 'visible_absorptance': material.visible_absorptance, - 'no_mass': str(material.no_mass), - 'thermal_resistance': '', - 'conductivity': '', - 'density': '', - 'specific_heat': '' - } - if material.no_mass: - dictionary['thermal_resistance'] = material.thermal_resistance - else: - dictionary['conductivity'] = material.conductivity - dictionary['density'] = material.density - dictionary['specific_heat'] = material.specific_heat - return dictionary - - -class ConstructionCatalogEntry(Resource): - def __init__(self): - pass - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def post(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.construction_catalog - name = None - if request.data == b'' or request.json['name'] is None: - response = {'error': 'Mandatory parameter "name" is missing'} - return Response(json.dumps(response), headers=headers, status=400) - try: - name = request.json['name'] - entry = catalog.get_entry(name) - output = {} - if isinstance(entry, Archetype): - output['archetypes'] = ToJson.archetype_to_json(entry) - if isinstance(entry, Construction): - output['constructions'] = ToJson.construction_to_json(entry) - if isinstance(entry, Material): - output['materials'] = ToJson.material_to_json(entry) - if isinstance(entry, Window): - output['windows'] = ToJson.window_to_json(entry) - return Response(json.dumps(output), headers=headers) - except IndexError: - response = {'error': f'Name "{name}" unknown'} - return Response(json.dumps(response), headers=headers, status=400) - - -class ConstructionCatalogEntries(Resource): - def __init__(self): - pass - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def post(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.construction_catalog - category = None - if request.data != b'': - category = request.json['category'] - output = {} - if category is None: - output = {'archetypes': [], 'constructions':[], 'materials':[], 'windows':[]} - content = catalog.entries() - for archetype in content.archetypes: - output['archetypes'].append(ToJson.archetype_to_json(archetype)) - for construction in content.constructions: - output['constructions'].append(ToJson.construction_to_json(construction)) - for material in content.materials: - output['materials'].append(ToJson.material_to_json(material)) - for window in content.windows: - output['windows'].append(ToJson.window_to_json(window)) - else: - try: - output[category] = [] - content = catalog.entries(category) - for entry in content: - if isinstance(entry, Archetype): - output[category].append(ToJson.archetype_to_json(entry)) - if isinstance(entry, Construction): - output[category].append(ToJson.construction_to_json(entry)) - if isinstance(entry, Material): - output[category].append(ToJson.material_to_json(entry)) - if isinstance(entry, Window): - output[category].append(ToJson.window_to_json(entry)) - except ValueError: - output = {'error': f'Category "{category}" unknown'} - return Response(json.dumps(output), headers=headers, status=400) - return Response(json.dumps(output), headers=headers) - - -class ConstructionCatalogNames(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.construction_catalog - category = None - if request.data != b'': - category = request.json['category'] - if category is None: - return Response(json.dumps(catalog.names()), headers=headers) - else: - try: - return Response(json.dumps(catalog.names(category)), headers=headers) - except ValueError: - response = {'error': f'Category "{category}" unknown'} - return Response(json.dumps(response), headers=headers, status=400) diff --git a/hub_api/docs/openapi-specs.yml b/hub_api/docs/openapi-specs.yml index 0eed27a..e15dadb 100644 --- a/hub_api/docs/openapi-specs.yml +++ b/hub_api/docs/openapi-specs.yml @@ -3,7 +3,7 @@ info: description: NextGen Cities Institute Gamification API termsOfService: http://swagger.io/terms/ contact: - email: peteryefi@gmail.com + email: nextgen-cities@gmail.com version: 1.4 externalDocs: description: Find out more about Swagger @@ -78,72 +78,6 @@ paths: $ref: '#/components/schemas/ApiResponse' security: - BearerAuth: [] - /v1.4/heat-pump/{city_id}: - post: - tags: - - heatpump - summary: Create a heat pump simulation - operationId: createHeatpump - description: heatpump simulation with existing catalog data - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - - in: path - name: city_id - schema: - type: integer - required: true - description: Numeric ID of the city to get - requestBody: - content: - application/json: - schema: - type: object - $ref: '#/components/schemas/HeatPump' - required: true - responses: - '201': - description: Heatpump simulation created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/HeatPumpRes' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '404': - description: Not found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] /v1.4/city/{city_id}: get: tags: @@ -166,7 +100,7 @@ paths: description: Numeric ID of the city to get responses: '200': - description: City created successfully + description: City retrieved successfully content: application/json: schema: @@ -342,233 +276,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiResponse' - /v1.4/energy-demand/{city_id}: - get: - tags: - - energy-demand - summary: Get energy demand - description: Retrieve energy demand data - operationId: getEnergyDemand - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - - in: path - name: city_id - schema: - type: integer - required: true - description: Numeric ID of the city to get - responses: - '200': - description: Successfully retrieved energy demand data - content: - application/json: - schema: - $ref: '#/components/schemas/EnergyDemand' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] - /v1.4/construction/{city_id}: - put: - tags: - - construction - summary: updates a city with building construction - description: updates a city with building construction - operationId: updateConstruction - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - - in: path - name: city_id - schema: - type: integer - required: true - description: Numeric ID of the city to get - requestBody: - description: creates building construction catalog - content: - application/json: - schema: - $ref: '#/components/schemas/Construction' - application/xml: - schema: - $ref: '#/components/schemas/Construction' - responses: - '201': - description: city updated with building construction successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Construction' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] - /v1.4/construction-catalog/entries: - post: - tags: - - construction-catalog - summary: creates building construction catalog entries - description: create the construction catalog entries - operationId: catalogEntries - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - requestBody: - description: creates building construction catalog - content: - application/json: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntries' - application/xml: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntries' - responses: - '201': - description: Construction catalog entries created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntries' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] - /v1.4/construction-catalog/entry: - post: - tags: - - construction-catalog - summary: creates building construction catalog - description: create the construction catalog of a building - operationId: catalogEntry - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - requestBody: - description: creates building construction catalog - content: - application/json: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntry' - application/xml: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntry' - responses: - '201': - description: Construction catalog created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntry' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] components: schemas: City: @@ -619,211 +326,6 @@ components: updated: type: string example: 2023-01-15 18:40:54.64877 - HeatPump: - type: object - properties: - StartYear: - type: integer - format: int64 - example: 10 - EndYear: - type: integer - format: int64 - example: 10 - MaximumHPEnergyInput: - type: float - example: 9.8382 - HoursOfStorageAtMaxDemand: - type: integer - format: int64 - example: 9 - BuildingSuppTemp: - type: integer - format: int64 - example: 40 - TemperatureDifference: - type: float - example: 9.8382 - FuelLHV: - type: float - example: 9.8382 - FuelPrice: - type: float - example: 9.8382 - FuelEF: - type: integer - format: int64 - example: 40 - FuelDensity: - type: float - example: 9.8382 - HPSupTemp: - type: float - example: 9.8382 - HeatPumpType: - type: string - example: Water to Water HP - enum: - - Water to Water HP - - Air Source HP - HeatPumpModel: - type: string - example: Water to Water HP - enum: - - ClimateMaster 156 kW - - ClimateMaster 256 kW - - ClimateMaster 335 kW - - 012 - - 015 - - 018 - - 023 - - 030 - - 033 - - 037 - - 044 - - 047 - - 057 - - 070 - - 087 - - 097 - - 102 - - 120 - - 130 - - 140 - SimType: - type: int - example: 1 - format: int64 - EnergyDemand: - type: array - items: - type: float - example: [ 610.610, 754.746, 288.338 ] - required: - - SimType - - EnergyDemand - - HeatPumpModel - - HeatPumpType - - FuelEF - - FuelDensity - - FuelPrice - - FuelLHV - - BuildingSuppTemp - - TemperatureDifference - - HoursOfStorageAtMaxDemand - - MaximumHPEnergyInput - - StartYear - - EndYear - ConstructionCatalogEntry: - type: object - properties: - name: - type: string - example: 189.1-2009 Nonres 4B Ext Wall Mass - required: - - name - ConstructionCatalogEntries: - type: object - properties: - category: - type: string - example: archetypes - required: - - category - Construction: - type: object - properties: - building_names: - type: array - items: - type: string - example: ["Dompark"] - extra_loses_due_to_thermal_bridges: - type: float - example: 0.15 - indirect_heated_ratio: - type: float - example: 0.15 - infiltration_rate_for_ventilation_system_off: - type: float - example: 0.5 - infiltration_rate_for_ventilation_system_on: - type: float - example: 0.0 - constructions: - type: array - items: - type: object - properties: - name: - type: string - example: 189.1-2009 Res 4B Ext Wall Steel-Framed - type: - type: string - example: Wall - layers: - type: array - items: - type: object - $ref: '#components/schemas/Layers' - required: - - building_names - - extra_loses_due_to_thermal_bridges - - indirect_heated_ratio - - infiltration_rate_for_ventilation_system_off - - infiltration_rate_for_ventilation_system_on - - constructions - Layers: - type: object - properties: - name: - type: string - example: Layer 1 - thickness: - type: float - example: 0.0 - material: - type: object - $ref: '#/components/schemas/Material' - Material: - type: object - properties: - id: - type: integer - format: int64 - example: 12 - name: - type: string - example: MAT-SHEAT - solar_absorptance: - type: float - example: 0.7 - thermal_absorptance: - type: float - example: 0.9 - visible_absorptance: - type: float - example: 0.7 - no_mass: - type: string - example: "True" - thermal_resistance: - type: float - example: 0.36256 - conductivity: - type: string - density: - type: string - specific_heat: - type: string - required: - - id - - name - - solar_absorptance - - thermal_absorptance - - visible_absorptance - - no_mass - - thermal_resistance User: type: object properties: @@ -851,48 +353,6 @@ components: - email - password - role - HeatPumpRes: - type: object - properties: - hourly_electricity_demand: - type: array - items: - type: object - daily_electricity_demand: - type: array - items: - type: object - monthly_electricity_demand: - type: array - items: - type: object - daily_fossil_consumption: - type: array - items: - type: object - monthly_fossil_consumption: - type: array - items: - type: object - EnergyDemand: - type: object - properties: - heating_demand: - type: array - items: - type: object - cooling_demand: - type: array - items: - type: object - lighting_demand: - type: array - items: - type: object - appliances_demand: - type: array - items: - type: object Login: type: object properties: @@ -924,7 +384,7 @@ components: type: string requestBodies: User: - description: Pet object that needs to be added to the store + description: User object that is to be created content: application/json: schema: diff --git a/hub_api/energy_demand.py b/hub_api/energy_demand.py deleted file mode 100644 index 5685c3a..0000000 --- a/hub_api/energy_demand.py +++ /dev/null @@ -1,396 +0,0 @@ -import json -from flask import request, Response -from flask_restful import Resource -from pathlib import Path -from geomeppy import IDF -import os -import glob -import hub_api.helpers.session_helper as sh -import helpers.constants as cte -import csv -from hub_api.helpers.auth import role_required -from persistence.models import UserRoles -from hub_api.config import Config - - -class EnergyDemand(Resource, Config): - _THERMOSTAT = 'HVACTEMPLATE:THERMOSTAT' - _IDEAL_LOAD_AIR_SYSTEM = 'HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM' - _SURFACE = 'BUILDINGSURFACE:DETAILED' - _WINDOW_SURFACE = 'FENESTRATIONSURFACE:DETAILED' - _CONSTRUCTION = 'CONSTRUCTION' - _MATERIAL = 'MATERIAL' - _MATERIAL_NOMASS = 'MATERIAL:NOMASS' - _ROUGHNESS = 'MediumRough' - _HOURLY_SCHEDULE = 'SCHEDULE:DAY:HOURLY' - _COMPACT_SCHEDULE = 'SCHEDULE:COMPACT' - _FILE_SCHEDULE = 'SCHEDULE:FILE' - _ZONE = 'ZONE' - _LIGHTS = 'LIGHTS' - _PEOPLE = 'PEOPLE' - _APPLIANCES = 'OTHEREQUIPMENT' - _HEATING_COOLING = 'THERMOSTATSETPOINT:DUALSETPOINT' - _INFILTRATION = 'ZONEINFILTRATION:DESIGNFLOWRATE' - _BUILDING_SURFACE = 'BuildingSurfaceDetailed' - _SCHEDULE_LIMIT = 'SCHEDULETYPELIMITS' - _ON_OFF = 'On/Off' - _FRACTION = 'Fraction' - _ANY_NUMBER = 'Any Number' - _CONTINUOUS = 'Continuous' - _DISCRETE = 'Discrete' - _BUILDING = 'BUILDING' - _SIZING_PERIODS = 'SIZINGPERIOD:DESIGNDAY' - _LOCATION = 'SITE:LOCATION' - _WINDOW_MATERIAL_SIMPLE = 'WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM' - _WINDOW = 'WINDOW' - _MATERIAL_ROOFVEGETATION = 'MATERIAL:ROOFVEGETATION' - _SIMPLE = 'Simple' - - idf_surfaces = { - # todo: make an enum for all the surface types - cte.WALL: 'wall', - cte.GROUND: 'floor', - cte.ROOF: 'roof' - } - idf_usage = { - # todo: make an enum for all the usage types - cte.RESIDENTIAL: 'residential_building' - } - idf_type_limits = { - cte.ON_OFF: 'on/off', - cte.FRACTION: 'Fraction', - cte.ANY_NUMBER: 'Any Number', - cte.CONTINUOUS: 'Continuous', - cte.DISCRETE: 'Discrete', - cte.TEMPERATURE: 'Any Number' - } - idf_day_types = { - cte.MONDAY: 'Monday', - cte.TUESDAY: 'Tuesday', - cte.WEDNESDAY: 'Wednesday', - cte.THURSDAY: 'Thursday', - cte.FRIDAY: 'Friday', - cte.SATURDAY: 'Saturday', - cte.SUNDAY: 'Sunday', - cte.HOLIDAY: 'Holidays', - cte.WINTER_DESIGN_DAY: 'WinterDesignDay', - cte.SUMMER_DESIGN_DAY: 'SummerDesignDay' - } - idf_schedule_types = { - 'compact': 'Compact', - cte.DAY: 'Day', - cte.WEEK: 'Week', - cte.YEAR: 'Year', - 'file': 'File' - } - idf_schedule_data_type = { - 'compact': 'Compact', - 'hourly': 'Hourly', - 'daily': 'Daily', - 'interval': 'Interval', - 'list': 'List', - } - - def __init__(self): - # this class is mostly hardcoded, as is intended to be used only for Dompark project, - # other projects should use the normal idf workflow instead. - super().__init__() - self._output_path = Path(Path(__file__).parent.parent / 'tmp').resolve() - self._data_path = Path(Path(__file__).parent.parent / 'data').resolve() - self._city = None - self._greenery_percentage = 0 - - def _set_layers(self, _idf, name, layers, vegetation=None): - if vegetation is not None: - _kwargs = {'Name': name, 'Outside_Layer': vegetation.name} - for i in range(0, len(layers)): - _kwargs[f'Layer_{i + 2}'] = layers[i].material.name - else: - _kwargs = {'Name': name, 'Outside_Layer': layers[0].material.name} - for i in range(1, len(layers)): - _kwargs[f'Layer_{i + 1}'] = layers[i].material.name - _idf.newidfobject(self._CONSTRUCTION, **_kwargs) - - def _update_constructions(self, _idf, ground, roof, wall, vegetation): - for construction in _idf.idfobjects[self._CONSTRUCTION]: - if construction.Name == 'Project ground floor': - # floor - self._set_layers(_idf, 'user_floor', ground) - elif construction.Name == 'Dompark Roof': - # roof - self._set_layers(_idf, 'user_roof', roof) - elif construction.Name == 'Dompark Roof Vegetation': - # roof - self._set_layers(_idf, 'user_roof_vegetation', roof, vegetation) - elif construction.Name == 'Dompark Wall': - # wall - self._set_layers(_idf, 'user_wall', wall) - else: - continue - for surface in _idf.idfobjects[self._SURFACE]: - if surface.Construction_Name == 'Project ground floor': - # floor - surface.Construction_Name = 'user_floor' - elif surface.Construction_Name == 'Dompark Wall': - # wall - surface.Construction_Name = 'user_wall' - elif surface.Construction_Name == 'Dompark Roof' or surface.Construction_Name == 'Dompark Roof Vegetation': - # roof - surface.Construction_Name = 'user_roof' - if self._greenery_percentage > 0: - if surface.Name in sh.roofs_associated_to_percentage[str(self._greenery_percentage)]: - surface.Construction_Name = 'user_roof_vegetation' - else: - continue - - for window in _idf.idfobjects[self._WINDOW_SURFACE]: - window.Construction_Name = 'window_construction_1' - - def _add_material(self, _idf, layers): - for layer in layers: - for material in _idf.idfobjects[self._MATERIAL]: - if material.Name == layer.material.name: - return - for material in _idf.idfobjects[self._MATERIAL_NOMASS]: - if material.Name == layer.material.name: - return - if str(layer.material.no_mass) == 'True': - _idf.newidfobject(self._MATERIAL_NOMASS, - Name=layer.material.name, - Roughness=self._ROUGHNESS, - Thermal_Resistance=layer.material.thermal_resistance, - Thermal_Absorptance=layer.material.thermal_absorptance, - Solar_Absorptance=layer.material.solar_absorptance, - Visible_Absorptance=layer.material.visible_absorptance - ) - else: - _idf.newidfobject(self._MATERIAL, - Name=layer.material.name, - Roughness=self._ROUGHNESS, - Thickness=layer.thickness, - Conductivity=layer.material.conductivity, - Density=layer.material.density, - Specific_Heat=layer.material.specific_heat, - Thermal_Absorptance=layer.material.thermal_absorptance, - Solar_Absorptance=layer.material.solar_absorptance, - Visible_Absorptance=layer.material.visible_absorptance - ) - - def _add_vegetation_material(self, _idf, vegetation): - for vegetation_material in _idf.idfobjects[self._MATERIAL_ROOFVEGETATION]: - if vegetation_material.Name == vegetation.name: - return - soil = vegetation.soil - height = 0 - leaf_area_index = 0 - leaf_reflectivity = 0 - leaf_emissivity = 0 - minimal_stomatal_resistance = 0 - for plant in vegetation.plants: - percentage = float(plant.percentage) / 100 - height += percentage * float(plant.height) - leaf_area_index += percentage * float(plant.leaf_area_index) - leaf_reflectivity += percentage * float(plant.leaf_reflectivity) - leaf_emissivity += percentage * float(plant.leaf_emissivity) - minimal_stomatal_resistance += percentage * float(plant.minimal_stomatal_resistance) - _idf.newidfobject(self._MATERIAL_ROOFVEGETATION, - Name=vegetation.name, - Height_of_Plants=height, - Leaf_Area_Index=leaf_area_index, - Leaf_Reflectivity=leaf_reflectivity, - Leaf_Emissivity=leaf_emissivity, - Minimum_Stomatal_Resistance=minimal_stomatal_resistance, - Soil_Layer_Name=soil.name, - Roughness=soil.roughness, - Thickness=vegetation.soil_thickness, - Conductivity_of_Dry_Soil=soil.dry_conductivity, - Density_of_Dry_Soil=soil.dry_density, - Specific_Heat_of_Dry_Soil=soil.dry_specific_heat, - Thermal_Absorptance=soil.thermal_absorptance, - Solar_Absorptance=soil.solar_absorptance, - Visible_Absorptance=soil.visible_absorptance, - Saturation_Volumetric_Moisture_Content_of_the_Soil_Layer= - soil.saturation_volumetric_moisture_content, - Residual_Volumetric_Moisture_Content_of_the_Soil_Layer= - soil.residual_volumetric_moisture_content, - Initial_Volumetric_Moisture_Content_of_the_Soil_Layer= - soil.initial_volumetric_moisture_content, - Moisture_Diffusion_Calculation_Method=self._SIMPLE - ) - - def _add_window_construction_and_material(self, _idf, window): - name = 'glazing_1' - _kwargs = {'Name': name, 'UFactor': window.overall_u_value, - 'Solar_Heat_Gain_Coefficient': window.g_value} - _idf.newidfobject(self._WINDOW_MATERIAL_SIMPLE, **_kwargs) - window_construction_name = 'window_construction_1' - _kwargs = {'Name': window_construction_name, 'Outside_Layer': name} - return _idf.newidfobject(self._CONSTRUCTION, **_kwargs) - - def _add_materials(self, _idf): - building = self._city.buildings[0] - ground_surface = building.grounds[0] - roof_surface = building.roofs[0] - wall_surface = building.walls[0] - internal_zone = building.internal_zones[0] - thermal_zone = internal_zone.thermal_zones[0] - ground = None - roof = None - roof_vegetation = None - wall = None - window = None - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.parent_surface.id == wall_surface.id: - wall = thermal_boundary.layers - if thermal_boundary.parent_surface.id == roof_surface.id: - roof = thermal_boundary.layers - roof_vegetation = thermal_boundary.vegetation - if thermal_boundary.parent_surface.id == ground_surface.id: - ground = thermal_boundary.layers - if thermal_boundary.thermal_openings is not None and len(thermal_boundary.thermal_openings) > 0: - window = thermal_boundary.thermal_openings[0] - if ground is not None and roof is not None and wall is not None and window is not None: - # we have all the needed surfaces type - break - self._add_material(_idf, ground) - self._add_material(_idf, roof) - self._add_material(_idf, wall) - if roof_vegetation is not None: - self._add_vegetation_material(_idf, roof_vegetation) - self._update_constructions(_idf, ground, roof, wall, roof_vegetation) - self._add_window_construction_and_material(_idf, window) - - def _add_standard_compact_hourly_schedule(self, _idf, schedule_name, schedules): - _kwargs = {'Name': f'{schedule_name}', - 'Schedule_Type_Limits_Name': self.idf_type_limits[schedules[0].data_type], - 'Field_1': 'Through: 12/31'} - for j, schedule in enumerate(schedules): - _val = schedule.values - _new_field = '' - for day_type in schedule.day_types: - _new_field += f' {self.idf_day_types[day_type]}' - _kwargs[f'Field_{j * 25 + 2}'] = f'For:{_new_field}' - for i in range(0, len(_val)): - _kwargs[f'Field_{j * 25 + 3 + i}'] = f'Until: {i + 1:02d}:00,{_val[i]}' - _idf.newidfobject(self._COMPACT_SCHEDULE, **_kwargs) - - def _add_schedules(self, _idf, thermal_zone): - self._add_standard_compact_hourly_schedule(_idf, 'user_occupancy_schedule', - thermal_zone.occupancy.occupancy_schedules) - self._add_standard_compact_hourly_schedule(_idf, 'user_lighting_schedule', - thermal_zone.lighting.schedules) - self._add_standard_compact_hourly_schedule(_idf, 'user_appliances_schedule', - thermal_zone.appliances.schedules) - self._add_standard_compact_hourly_schedule(_idf, 'user_heating_schedule', - thermal_zone.thermal_control.heating_set_point_schedules) - self._add_standard_compact_hourly_schedule(_idf, 'user_cooling_schedule', - thermal_zone.thermal_control.cooling_set_point_schedules) - - def _add_usage(self, _idf): - _thermal_zone = None - for building in self._city.buildings: - for internal_zone in building.internal_zones: - for thermal_zone in internal_zone.thermal_zones: - _thermal_zone = thermal_zone - # Dompark project share schedules and usages among all the buildings so we could add just the first one - break - break - break - self._add_schedules(_idf, _thermal_zone) - fraction_radiant = _thermal_zone.occupancy.sensible_radiative_internal_gain / \ - (_thermal_zone.occupancy.sensible_radiative_internal_gain + - _thermal_zone.occupancy.sensible_convective_internal_gain) - for idf_object in _idf.idfobjects[self._PEOPLE]: - idf_object['Number_of_People_Schedule_Name'] = 'user_occupancy_schedule' - idf_object['People_per_Zone_Floor_Area'] = _thermal_zone.occupancy.occupancy_density - idf_object['Fraction_Radiant'] = fraction_radiant - - for idf_object in _idf.idfobjects[self._LIGHTS]: - idf_object['Schedule_Name'] = 'user_lighting_schedule' - idf_object['Watts_per_Zone_Floor_Area'] = _thermal_zone.lighting.density - for idf_object in _idf.idfobjects[self._APPLIANCES]: - idf_object['Schedule_Name'] = 'user_appliances_schedule' - idf_object['Power_per_Zone_Floor_Area'] = _thermal_zone.appliances.density - for idf_object in _idf.idfobjects[self._HEATING_COOLING]: - idf_object['Heating_Setpoint_Temperature_Schedule_Name'] = 'user_heating_schedule' - idf_object['Cooling_Setpoint_Temperature_Schedule_Name'] = 'user_cooling_schedule' - - return - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def get(self, city_id): - payload = request.get_json() - self._city = self.get_city(city_id) - - self._greenery_percentage = round(float(payload['greenery_percentage']) / 10) * 10 - output_file = str((self._output_path / 'dompark.idf').resolve()) - idd_file = str((self._data_path / 'energy+.idd').resolve()) - epw_file = str((self._data_path / 'CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').resolve()) - idf_file = str((self._data_path / 'dompark.idf').resolve()) - IDF.setiddname(idd_file) - _idf = IDF(idf_file, epw_file) - self._add_materials(_idf) - self._add_usage(_idf) - _idf.newidfobject( - "OUTPUT:VARIABLE", - Variable_Name="Zone Ideal Loads Supply Air Total Heating Energy", - Reporting_Frequency="Hourly", - ) - _idf.newidfobject( - "OUTPUT:VARIABLE", - Variable_Name="Zone Ideal Loads Supply Air Total Cooling Energy", - Reporting_Frequency="Hourly", - ) - # From EnergyPlus documentation: Lights Electric Energy [J] - # The lighting electrical consumption including ballasts, if present. These will have the same value as Lights - # Total Heating Energy (above). - _idf.newidfobject( - "OUTPUT:VARIABLE", - Variable_Name="Lights Total Heating Energy", - Reporting_Frequency="Hourly", - ) - _idf.newidfobject( - "OUTPUT:VARIABLE", - Variable_Name="Other Equipment Total Heating Energy", - Reporting_Frequency="Hourly", - ) - - _idf.match() - _idf.saveas(str(output_file)) - _idf.run(expandobjects=True, readvars=True, output_directory=self._output_path, output_prefix='dompark_') - # Todo: set the heating and cooling - heating = [] - cooling = [] - lighting = [] - appliances = [] - with open((self._output_path / f'dompark_out.csv').resolve()) as f: - reader = csv.reader(f, delimiter=',') - for row in reader: - if '00:00' in row[0]: - cooling_value = 0.0 - heating_value = 0.0 - lighting_value = 0.0 - appliances_value = 0.0 - for i in range(1, 38): - lighting_value += float(row[i]) - for i in range(38, 73): - appliances_value += float(row[i]) - for i in range(73, 133, 2): - heating_value += float(row[i]) - cooling_value += float(row[i + 1]) - cooling.append(cooling_value) - heating.append(heating_value) - lighting.append(lighting_value) - appliances.append(appliances_value) - - files = glob.glob(f'{self._output_path}/dompark*') - for file in files: - os.remove(file) - continue - - response = {'heating_demand': heating, - 'cooling_demand': cooling, - 'lighting_demand': lighting, - 'appliances_demand': appliances - } - return Response(json.dumps(response), status=200) diff --git a/hub_api/geometry.py b/hub_api/geometry.py deleted file mode 100644 index 9fdd68d..0000000 --- a/hub_api/geometry.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Geometry -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -Code contributors: Peter Yefi peteryefi@gmail.com -""" - -from flask import make_response, send_file -from flask_restful import Resource -from pathlib import Path -from hub_api.helpers.auth import role_required -from persistence.models import UserRoles - - -class Geometry(Resource): - def __init__(self): - data_path = (Path(__file__).parent.parent / 'data').resolve() - self._gtlf_path = (Path(data_path / 'DomparkBuilding.gltf')).resolve() - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def get(self): - response = make_response(send_file(self._gtlf_path, - as_attachment=True, - mimetype='model/gltf+json, model/gltf-binary')) - return response diff --git a/hub_api/greenery.py b/hub_api/greenery.py deleted file mode 100644 index 96dc84e..0000000 --- a/hub_api/greenery.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Greenery -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca -Code contributors: Peter Yefi peteryefi@gmail.com -""" - -import json - -from flask import Response, request -from flask_restful import Resource -from hub_api.config import Config -from city_model_structure.greenery.vegetation import Vegetation -from city_model_structure.greenery.soil import Soil -from city_model_structure.greenery.plant import Plant -import helpers.constants as cte -from persistence.models import UserRoles -from hub_api.helpers.auth import role_required - - -class Greenery(Resource, Config): - def __init__(self): - super().__init__() - - @role_required([UserRoles.Admin.value]) - def put(self, city_id): - - city = self.get_city(city_id) - try: - greenery_percentage = request.json['greenery_percentage'] - if greenery_percentage == 0: - response = {'result': 'succeed'} - return Response(json.dumps(response), status=200) - - building_names = request.json['building_names'] - vegetation_requested = request.json['vegetation'] - vegetation_name = vegetation_requested['vegetation_name'] - soil_thickness = vegetation_requested['soil_thickness'] - soil_name = vegetation_requested['soil_name'] - roughness = vegetation_requested['soil_roughness'] - dry_conductivity = vegetation_requested['dry_soil_conductivity'] - dry_density = vegetation_requested['dry_soil_density'] - dry_specific_heat = vegetation_requested['dry_soil_specific_heat'] - thermal_absorptance = vegetation_requested['soil_thermal_absorptance'] - solar_absorptance = vegetation_requested['soil_solar_absorptance'] - visible_absorptance = vegetation_requested['soil_visible_absorptance'] - saturation_volumetric_moisture_content = vegetation_requested['soil_saturation_volumetric_moisture_content'] - residual_volumetric_moisture_content = vegetation_requested['soil_residual_volumetric_moisture_content'] - soil = Soil(soil_name, roughness, dry_conductivity, dry_density, dry_specific_heat, thermal_absorptance, - solar_absorptance, visible_absorptance, saturation_volumetric_moisture_content, - residual_volumetric_moisture_content) - soil.initial_volumetric_moisture_content = '0.1' - plant_percentages = vegetation_requested['plant_percentages'] - plants = [] - for plant_percentage in plant_percentages: - plant_name = plant_percentage['plant_name'] - height = plant_percentage['plant_height'] - leaf_area_index = plant_percentage['plant_leaf_area_index'] - leaf_reflectivity = plant_percentage['plant_leaf_reflectivity'] - leaf_emissivity = plant_percentage['plant_leaf_emissivity'] - minimal_stomatal_resistance = plant_percentage['plant_minimal_stomatal_resistance'] - co2_sequestration = plant_percentage['plant_co2_sequestration'] - grows_on_soils = plant_percentage['plant_grows_on'] - plant = Plant(plant_name, height, leaf_area_index, leaf_reflectivity, leaf_emissivity, - minimal_stomatal_resistance, co2_sequestration, grows_on_soils) - plant.percentage = plant_percentage['plant_percentage'] - plants.append(plant) - vegetation = Vegetation(vegetation_name, soil, soil_thickness, plants) - for building_name in building_names: - for building in city.buildings: - if building.human_readable_name != building_name: - continue - for internal_zone in building.internal_zones: - for thermal_zone in internal_zone.thermal_zones: - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.type == cte.ROOF: - thermal_boundary.vegetation = vegetation - - response = {'result': 'succeed'} - except KeyError as ex: - response = {'error': f'Mandatory parameter {ex} is missing'} - return Response(json.dumps(response), status=400) - - return Response(json.dumps(response)) diff --git a/hub_api/greenery_catalog.py b/hub_api/greenery_catalog.py deleted file mode 100644 index 504b86a..0000000 --- a/hub_api/greenery_catalog.py +++ /dev/null @@ -1,178 +0,0 @@ -""" -Greenery catalog -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -""" - -import json -from flask import request, Response -from flask_restful import Resource -from catalog_factories.data_models.greenery.plant import Plant -from catalog_factories.data_models.greenery.soil import Soil -from catalog_factories.data_models.greenery.vegetation import Vegetation -from hub_api.helpers.session_helper import refresh_session - - -class ToJson: - @staticmethod - def plant_to_json(plant, percentage=None): - dictionary = { - 'plant_percentage': percentage, - 'plant_name': plant.name, - 'plant_category': plant.category, - 'plant_height': plant.height, - 'plant_leaf_area_index': plant.leaf_area_index, - 'plant_leaf_reflectivity': plant.leaf_reflectivity, - 'plant_leaf_emissivity': plant.leaf_emissivity, - 'plant_minimal_stomatal_resistance': plant.minimal_stomatal_resistance, - 'plant_co2_sequestration': plant.co2_sequestration, - 'plant_grows_on': [] - } - if percentage is None: - dictionary.pop('plant_percentage') - soils = [] - for soil in plant.grows_on: - soil_dic = ToJson.soil_to_json(soil) - soils.append(soil_dic) - dictionary['plant_grows_on'] = soils - return dictionary - - @staticmethod - def soil_to_json(soil): - dictionary = {'soil_name': soil.name, - 'soil_roughness': str(soil.roughness), - 'soil_dry_conductivity': soil.dry_conductivity, - 'soil_dry_density': soil.dry_density, - 'soil_dry_specific_heat': soil.dry_specific_heat, - 'soil_thermal_absortance': soil.thermal_absorptance, - 'soil_solar_absortance': soil.solar_absorptance, - 'soil_visible_absortance': soil.visible_absorptance, - 'soil_saturation_volumetric_moisture_content': soil.saturation_volumetric_moisture_content, - 'soil_residual_volumetric_moisture_content': soil.residual_volumetric_moisture_content, - 'soil_initial_volumetric_moisture_content': soil.initial_volumetric_moisture_content - } - return dictionary - - @staticmethod - def vegetation_to_json(vegetation): - - dictionary = {'vegetation_name': vegetation.name, - 'vegetation_category': vegetation.category, - 'soil_thickness': vegetation.soil_thickness, - 'management': str(vegetation.management), - 'air_gap': vegetation.air_gap, - 'soil_name': vegetation.soil_name, - 'soil_roughness': str(vegetation.soil_roughness), - 'dry_soil_conductivity': vegetation.dry_soil_conductivity, - 'dry_soil_density': vegetation.dry_soil_density, - 'dry_soil_specific_heat': vegetation.dry_soil_specific_heat, - 'soil_thermal_absorptance': vegetation.soil_thermal_absorptance, - 'soil_solar_absorptance': vegetation.soil_solar_absorptance, - 'soil_visible_absorptance': vegetation.soil_visible_absorptance, - 'soil_saturation_volumetric_moisture_content': vegetation.soil_saturation_volumetric_moisture_content, - 'soil_residual_volumetric_moisture_content': vegetation.soil_residual_volumetric_moisture_content, - 'soil_initial_volumetric_moisture_content': vegetation.soil_initial_volumetric_moisture_content, - 'plant_percentages': [] - } - percentages = [] - for percentage in vegetation.plant_percentages: - percentage_dic = ToJson.plant_to_json(percentage, percentage.percentage) - percentages.append(percentage_dic) - dictionary['plant_percentages'] = percentages - return dictionary - - -class GreeneryCatalogEntry(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.greenery_catalog - name = None - if request.data == b'' or request.json['name'] is None: - response = {'error': 'Mandatory parameter "name" is missing'} - return Response(json.dumps(response), headers=headers, status=400) - try: - name = request.json['name'] - entry = catalog.get_entry(name) - output = {} - if isinstance(entry, Vegetation): - output['vegetations'] = ToJson.vegetation_to_json(entry) - if isinstance(entry, Plant): - output['plants'] = ToJson.plant_to_json(entry) - if isinstance(entry, Soil): - output['soils'] = ToJson.soil_to_json(entry) - return Response(json.dumps(output), headers=headers) - except IndexError: - response = {'error': f'Name "{name}" unknown'} - return Response(json.dumps(response), headers=headers, status=400) - - -class GreeneryCatalogEntries(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.greenery_catalog - category = None - if request.data != b'': - category = request.json['category'] - output = {} - if category is None: - content = catalog.entries() - output = {'vegetations': [], 'plants': [], 'soils': []} - for vegetation in content.vegetations: - output['vegetations'].append(ToJson.vegetation_to_json(vegetation)) - for plant in content.plants: - output['plants'].append(ToJson.plant_to_json(plant)) - for soil in content.soils: - output['soils'].append(ToJson.soil_to_json(soil)) - else: - try: - content = catalog.entries(category) - output[category] = [] - for entry in content: - if isinstance(entry, Vegetation): - output[category].append(ToJson.vegetation_to_json(entry)) - if isinstance(entry, Plant): - output[category].append(ToJson.plant_to_json(entry)) - if isinstance(entry, Soil): - output[category].append(ToJson.soil_to_json(entry)) - except ValueError: - output = {'error': f'Category "{category}" unknown'} - return Response(json.dumps(output), headers=headers, status=400) - return Response(json.dumps(output), headers=headers) - - -class GreeneryCatalogNames(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.greenery_catalog - category = None - if request.data != b'': - category = request.json['category'] - if category is None: - return Response(json.dumps(catalog.names()), headers=headers) - else: - try: - return Response(json.dumps(catalog.names(category)), headers=headers) - except ValueError: - response = {'error': f'Category "{category}" unknown'} - return Response(json.dumps(response), headers=headers, status=400) diff --git a/hub_api/heat_pump.py b/hub_api/heat_pump.py deleted file mode 100644 index 54aa214..0000000 --- a/hub_api/heat_pump.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -HeatPump Service -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Peter Yefi peteryefi@gmail.com -""" -import json - -from flask import request, Response -from hub_api.config import Config -from flask_restful import Resource -from hub_api.helpers.auth import role_required -from utils import HeatPumpSimulator -from utils import validate_hp_model -from persistence.models import UserRoles -from utils import expand_energy_demand -from hub_logger import logger - - -class HeatPump(Config, Resource): - def __init__(self): - super().__init__() - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def post(self, city_id): - payload = request.get_json() - city = self.get_city(city_id) - if validate_hp_model(payload['HeatPumpType'], payload['HeatPumpModel']): - # expand energy demand values - expand_energy_demand(payload['EnergyDemand']) - try: - # Run simulation and return output file here - hp_simulator = HeatPumpSimulator(city, payload) - results = hp_simulator.run_hp_simulation() - return Response(json.dumps(results), status=200) - except Exception as err: - logger.error(err) - return Response(json.dumps({'error_message': 'Sorry an error occurred while running HP Simulation'})) - else: - return Response(json.dumps({'error_message': 'Wrong heat pump type/model combination'}), status=400) diff --git a/hub_api/helpers/auth.py b/hub_api/helpers/auth.py index 76fd2bd..0a04845 100644 --- a/hub_api/helpers/auth.py +++ b/hub_api/helpers/auth.py @@ -11,8 +11,8 @@ import os from jwt.utils import get_int_from_datetime from functools import wraps from flask import request, g -from hub_logger import logger -from persistence.models import UserRoles +from hub.hub_logger import logger +from hub.persistence.models import UserRoles from jwt.exceptions import JWTException instance = JWT() diff --git a/hub_api/lca.py b/hub_api/lca.py deleted file mode 100644 index b4e516a..0000000 --- a/hub_api/lca.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -LCA -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2021 Project Author name Atiya -Code contributors: Peter Yefi peteryefi@gmail.com -""" -import json -from flask import Response -from flask_restful import Resource -from lca_calculations import LcaCalculations -from itertools import groupby -from operator import itemgetter -from hub_api.helpers.auth import role_required -from hub_api.config import Config -from persistence.models import UserRoles - - - -class MaterialLCACatalog(Resource, Config): - def __init__(self): - super().__init__() - - @staticmethod - def get_lca_value(city, nrel_id = None): - nrel_material = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "14", "15", "16", "18", "20", "21", - "22", "101", "102", "103", "104", "105", "106", "107", "108", "109", "110", "111", "112", "113", "114"] - lca_material = ["12", "32", "21", "25", "11", "20", "40", "27", "29", "28", "19", "40", "41", "25", "32", "32", "32", "21", "41", - "21", "11", "33", "28", "11", "40", "1", "28", "41", "0", "0", "0", "0", "0", ] - keys = ['nrel_material_id', 'lca_material_id'] - material_mapping = [dict(zip(keys, i)) for i in zip(nrel_material, lca_material)] - - if nrel_id is None: - materials_lca = {"material_lca_catalog": []} - for mat in material_mapping: - - if mat["nrel_material_id"] not in ("110", "111", "112", "113", "114"): - for material in city.lca_materials: - if(int(mat["lca_material_id"]) == material.id): - material_lca_catalog = {} - material_lca_catalog['nrel_material_id'] = mat["nrel_material_id"] - material_lca_catalog['embodied_carbon'] = material.embodied_carbon - mat_end_of_life = LcaCalculations(city).end_life_carbon(material.type) - material_lca_catalog['end_of_life_carbon'] = mat_end_of_life - materials_lca["material_lca_catalog"].append(material_lca_catalog) - - else: - material_lca_catalog = {} - material_lca_catalog['nrel_material_id'] = mat["nrel_material_id"] - material_lca_catalog['embodied_carbon'] = 0.0 - material_lca_catalog['end_of_life_carbon'] = 0.0 - materials_lca["material_lca_catalog"].append(material_lca_catalog) - - return materials_lca - - else: - for mat in material_mapping: - if mat["nrel_material_id"] == str(nrel_id): - for material in city.lca_materials: - if (material.id == int(mat["lca_material_id"])): - return material.embodied_carbon, material.id, material.type, material.name, material.density - # return material.embodied_carbon - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def get(self, city_id): - city = self.get_city(city_id) - try: - return Response(json.dumps(self.get_lca_value(city)), status=200) - except ValueError: - response = {'err_msg': f'No Catalog Available'} - return Response(json.dumps(response), status=400) - - -class MaterialLCACalculations(Resource, Config): - """ - LCA class - """ - def __init__(self): - super().__init__() - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def get(self, city_id): - """ - Auto-method for processing the lca request - :return: lca demand - """ - city = self.get_city(city_id) - - materials_lca = {'Wall': [], 'Ground': [], 'Roof': []} - for building in city.buildings: - for internal_zone in building.internal_zones: - for thermal_zone in internal_zone.thermal_zones: - for thermal_boundary in thermal_zone.thermal_boundaries: - for i, layer in enumerate(thermal_boundary.layers): - material_layers_lca = {} - if layer.material.no_mass == False: - embodied_carbon, mat_id, mat_type, mat_name, mat_density = MaterialLCACatalog.get_lca_value(city, layer.material.id) - material_layers_lca['layer_name'] = f'Layer {i+1}' - material_layers_lca['nrel_material_id'] = layer.material.id - material_layers_lca['lca_material_id'] = mat_id - material_layers_lca['embodied_carbon'] = (layer.thickness * thermal_boundary.opaque_area) * mat_density * embodied_carbon - mat_end_of_life = LcaCalculations(city).end_life_carbon(mat_type) - material_end_of_life = mat_end_of_life * (layer.thickness * thermal_boundary.opaque_area) * mat_density - material_layers_lca['end_of_life_per_layer'] = material_end_of_life - materials_lca[thermal_boundary.type].append(material_layers_lca) - else: - material_layers_lca['layer_name'] = f'Layer {i+1}' - material_layers_lca['nrel_material_id'] = layer.material.id - material_layers_lca['lca_material_id'] = mat_id - material_layers_lca['embodied_carbon'] = (layer.thickness * thermal_boundary.opaque_area) * mat_density * embodied_carbon - material_layers_lca['end_of_life_per_layer'] = 0.0 - materials_lca[thermal_boundary.type].append(material_layers_lca) - - materials_embodied_carbon = {'Wall': [], 'Ground': [], 'Roof': []} - for key, value in materials_lca.items(): - boundary_layers = sorted(value, key=itemgetter('layer_name')) - for b_layer, layer_properties in groupby(boundary_layers, key=itemgetter('layer_name')): - sum_embodied = 0.0 - sum_end_of_life = 0.0 - total_embodied_carbon = {} - for k in layer_properties: - sum_embodied += k["embodied_carbon"] - sum_end_of_life += k["end_of_life_per_layer"] - total_embodied_carbon['layer_name'] = b_layer - total_embodied_carbon['embodied_carbon'] = sum_embodied - total_embodied_carbon['end_of_life_carbon'] = sum_end_of_life - materials_embodied_carbon[key].append(total_embodied_carbon) - - return Response(json.dumps(materials_embodied_carbon), status=200) diff --git a/hub_api/usage.py b/hub_api/usage.py deleted file mode 100644 index 8d127f7..0000000 --- a/hub_api/usage.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Usage -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -Code contributors: Peter Yefi peteryefi@gmail.com -""" - -import json -from flask import Response, request -from flask_restful import Resource - -from city_model_structure.attributes.schedule import Schedule -from city_model_structure.building_demand.appliances import Appliances -from city_model_structure.building_demand.lighting import Lighting -from city_model_structure.building_demand.occupancy import Occupancy -from city_model_structure.building_demand.thermal_control import ThermalControl -from city_model_structure.building_demand.usage_zone import UsageZone -import helpers.constants as cte -from hub_api.helpers.auth import role_required -from hub_api.config import Config -from persistence.models import UserRoles - - -class Usage(Resource, Config): - def __init__(self): - super().__init__() - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def put(self, city_id): - - city = self.get_city(city_id) - catalog = request.json['usage_catalog'] - - usage_name = None - try: - building_names = request.json['building_names'] - usages = request.json['usages'] - seconds_in_hour = cte.MINUTES_TO_SECONDS * cte.HOUR_TO_MINUTES - for building_name in building_names: - for building in city.buildings: - if building.human_readable_name != building_name: - continue - for internal_zone in building.internal_zones: - internal_zone.usage_zones = [] - for usage in usages: - usage_entry = catalog.get_entry(usage['name']) - occupancy_schedules = [] - for schedule in usage_entry.occupancy.schedules: - occupancy_schedule = Schedule() - occupancy_schedule.type = schedule.type - occupancy_schedule.values = schedule.values - occupancy_schedule.data_type = schedule.data_type - occupancy_schedule.time_step = schedule.time_step - occupancy_schedule.time_range = schedule.time_range - occupancy_schedule.day_types = schedule.day_types - occupancy_schedules.append(occupancy_schedule) - occupancy = Occupancy() - occupancy.occupancy_density = usage_entry.occupancy.occupancy_density - occupancy.sensible_convective_internal_gain = usage_entry.occupancy.sensible_convective_internal_gain - occupancy.sensible_radiative_internal_gain = usage_entry.occupancy.sensible_radiative_internal_gain - occupancy.latent_internal_gain = usage_entry.occupancy.latent_internal_gain - occupancy.occupancy_schedules = occupancy_schedules - - lighting_schedules = [] - for schedule in usage_entry.lighting.schedules: - lighting_schedule = Schedule() - lighting_schedule.type = schedule.type - lighting_schedule.values = schedule.values - lighting_schedule.data_type = schedule.data_type - lighting_schedule.time_step = schedule.time_step - lighting_schedule.time_range = schedule.time_range - lighting_schedule.day_types = schedule.day_types - lighting_schedules.append(lighting_schedule) - lighting = Lighting() - lighting.density = usage_entry.lighting.density - lighting.convective_fraction = usage_entry.lighting.convective_fraction - lighting.radiative_fraction = usage_entry.lighting.radiative_fraction - lighting.latent_fraction = usage_entry.lighting.latent_fraction - lighting.schedules = lighting_schedules - appliances_schedules = [] - for schedule in usage_entry.appliances.schedules: - appliances_schedule = Schedule() - appliances_schedule.type = schedule.type - appliances_schedule.values = schedule.values - appliances_schedule.data_type = schedule.data_type - appliances_schedule.time_step = schedule.time_step - appliances_schedule.time_range = schedule.time_range - appliances_schedule.day_types = schedule.day_types - appliances_schedules.append(appliances_schedule) - appliances = Appliances() - appliances.density = usage_entry.appliances.density - appliances.convective_fraction = usage_entry.appliances.convective_fraction - appliances.radiative_fraction = usage_entry.appliances.radiative_fraction - appliances.latent_fraction = usage_entry.appliances.latent_fraction - appliances.schedules = appliances_schedules - hvac_schedules = [] - for schedule in usage_entry.thermal_control.hvac_availability_schedules: - hvac_schedule = Schedule() - hvac_schedule.type = schedule.type - hvac_schedule.values = schedule.values - hvac_schedule.data_type = schedule.data_type - hvac_schedule.time_step = schedule.time_step - hvac_schedule.time_range = schedule.time_range - hvac_schedule.day_types = schedule.day_types - hvac_schedules.append(hvac_schedule) - heating_schedules = [] - for schedule in usage_entry.thermal_control.heating_set_point_schedules: - heating_schedule = Schedule() - heating_schedule.type = schedule.type - heating_schedule.values = schedule.values - heating_schedule.data_type = schedule.data_type - heating_schedule.time_step = schedule.time_step - heating_schedule.time_range = schedule.time_range - heating_schedule.day_types = schedule.day_types - heating_schedules.append(heating_schedule) - cooling_schedules = [] - for schedule in usage_entry.thermal_control.cooling_set_point_schedules: - cooling_schedule = Schedule() - cooling_schedule.type = schedule.type - cooling_schedule.values = schedule.values - cooling_schedule.data_type = schedule.data_type - cooling_schedule.time_step = schedule.time_step - cooling_schedule.time_range = schedule.time_range - cooling_schedule.day_types = schedule.day_types - cooling_schedules.append(cooling_schedule) - thermal_control = ThermalControl() - thermal_control.mean_heating_set_point = usage_entry.thermal_control.mean_heating_set_point - thermal_control.heating_set_back = usage_entry.thermal_control.heating_set_back - thermal_control.mean_cooling_set_point = usage_entry.thermal_control.mean_cooling_set_point - thermal_control.hvac_availability_schedules = hvac_schedules - thermal_control.heating_set_point_schedules = heating_schedules - thermal_control.cooling_set_point_schedules = cooling_schedules - usage_zone = UsageZone() - usage_zone.usage = usage_entry.usage - usage_zone.percentage = usage['percentage'] - usage_zone.hours_day = usage_entry.hours_day - usage_zone.days_year = usage_entry.days_year - usage_zone.mechanical_air_change = usage_entry.mechanical_air_change - if usage_entry.mechanical_air_change is None: - usage_zone.mechanical_air_change = ((usage_entry.ventilation_rate * internal_zone.area) / - internal_zone.volume) * seconds_in_hour - usage_zone.occupancy = occupancy - usage_zone.lighting = lighting - usage_zone.appliances = appliances - usage_zone.thermal_control = thermal_control - internal_zone.usage_zones.append(usage_zone) - response = {'result': 'succeed'} - except KeyError as ex: - response = {'error': f'Mandatory parameter {ex} is missing'} - return Response(json.dumps(response), status=400) - except IndexError: - response = {'error': f'Name "{usage_name}" unknown'} - return Response(json.dumps(response), status=400) - return Response(json.dumps(response)) diff --git a/hub_api/usage_catalog.py b/hub_api/usage_catalog.py deleted file mode 100644 index 026df07..0000000 --- a/hub_api/usage_catalog.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -Construction catalog -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -""" - -import json - -from flask import request, Response -from flask_restful import Resource - -from hub_api.helpers.session_helper import refresh_session - - -class ToJson: - @staticmethod - def usage_to_json(usage): - dictionary = { - 'usage': usage.usage, - 'hours_day': usage.hours_day, - 'days_year': usage.days_year, - 'mechanical_air_change': usage.mechanical_air_change if usage.mechanical_air_change is not None else '', - 'ventilation_rate': usage.ventilation_rate if usage.ventilation_rate is not None else '', - 'occupancy': ToJson.occupancy_to_json(usage.occupancy), - 'lighting': ToJson.lighting_to_json(usage.lighting), - 'appliances': ToJson.appliances_to_json(usage.appliances), - 'thermal_control': ToJson.thermal_control_to_json(usage.thermal_control) - } - return dictionary - - @staticmethod - def occupancy_to_json(occupancy): - dictionary = { - 'occupancy_density': occupancy.occupancy_density, - 'sensible_convective_internal_gain': occupancy.sensible_convective_internal_gain, - 'sensible_radiative_internal_gain': occupancy.sensible_radiative_internal_gain, - 'latent_internal_gain': occupancy.latent_internal_gain, - 'schedules': [] - } - for schedule in occupancy.schedules: - dictionary['schedules'].append(ToJson.schedule_to_json(schedule)) - return dictionary - - @staticmethod - def lighting_to_json(lighting): - dictionary = { - 'density': lighting.density, - 'convective_fraction': lighting.convective_fraction, - 'radiative_fraction': lighting.radiative_fraction, - 'latent_fraction': lighting.latent_fraction, - 'schedules': [] - } - for schedule in lighting.schedules: - dictionary['schedules'].append(ToJson.schedule_to_json(schedule)) - return dictionary - - @staticmethod - def appliances_to_json(appliances): - dictionary = { - 'density': appliances.density, - 'convective_fraction': appliances.convective_fraction, - 'radiative_fraction': appliances.radiative_fraction, - 'latent_fraction': appliances.latent_fraction, - 'schedules': [] - } - for schedule in appliances.schedules: - dictionary['schedules'].append(ToJson.schedule_to_json(schedule)) - return dictionary - - @staticmethod - def thermal_control_to_json(thermal_control): - dictionary = { - 'mean_heating_set_point': thermal_control.mean_heating_set_point, - 'heating_set_back': thermal_control.heating_set_back, - 'mean_cooling_set_point': thermal_control.mean_cooling_set_point, - 'hvac_availability_schedules': [], - 'heating_set_point_schedules': [], - 'cooling_set_point_schedules': [], - } - for schedule in thermal_control.hvac_availability_schedules: - dictionary['hvac_availability_schedules'].append(ToJson.schedule_to_json(schedule)) - for schedule in thermal_control.heating_set_point_schedules: - dictionary['heating_set_point_schedules'].append(ToJson.schedule_to_json(schedule)) - for schedule in thermal_control.cooling_set_point_schedules: - dictionary['cooling_set_point_schedules'].append(ToJson.schedule_to_json(schedule)) - return dictionary - - @staticmethod - def schedule_to_json(schedule): - schedule_dictionary = { - 'type': schedule.type, - 'data_type': schedule.data_type, - 'time_step': schedule.time_step, - 'time_range': schedule.time_range, - 'day_types': schedule.day_types, - 'values': schedule.values, - } - return schedule_dictionary - - -class UsageCatalogEntry(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.usage_catalog - - name = None - if request.data == b'' or request.json['name'] is None: - response = {'error': 'Mandatory parameter "name" is missing'} - return Response(json.dumps(response), headers=headers, status=400) - try: - name = request.json['name'] - entry = catalog.get_entry(name) - output = {'usages': [ToJson.usage_to_json(entry)]} - return Response(json.dumps(output), headers=headers) - except IndexError: - response = {'error': f'Name "{name}" unknown'} - return Response(json.dumps(response), headers=headers, status=400) - - -class UsageCatalogEntries(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.usage_catalog - output = {'usages': []} - content = catalog.entries() - for usage in content.usages: - output['usages'].append(ToJson.usage_to_json(usage)) - return Response(json.dumps(output), headers=headers) - - -class UsageCatalogNames(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.usage_catalog - return Response(json.dumps(catalog.names()), headers=headers) diff --git a/hub_api/user.py b/hub_api/user.py index e2e97b5..d7e0936 100644 --- a/hub_api/user.py +++ b/hub_api/user.py @@ -6,12 +6,12 @@ Copyright © 2023 Project Author Peter Yefi peteryefi@gmail.com import json from flask import Response, request from flask_restful import Resource -from imports.user_factory import UserFactory -from exports.user_factory import UserFactory as ExUserFactory +from hub.imports.user_factory import UserFactory +from hub.exports.user_factory import UserFactory as ExUserFactory import os -from hub_logger import logger +from hub.hub_logger import logger from hub_api.helpers.auth import generate_auth_token, role_required -from persistence.models import UserRoles +from hub.persistence.models import UserRoles class User(Resource): From d96124107967801b8a9c46be15a0d208b3ff6789 Mon Sep 17 00:00:00 2001 From: Peter Yefi Date: Wed, 1 Feb 2023 15:48:14 -0500 Subject: [PATCH 09/10] Added tests for user --- bootstrap.py | 3 +- hub_api/city_info.py | 12 +++-- hub_api/config.py | 20 ++++++- hub_api/user.py | 36 ++++++------- tests/__init__.py | 0 tests/base_test.py | 58 ++++++++++++++++++++ tests/test_user.py | 124 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 227 insertions(+), 26 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/base_test.py create mode 100644 tests/test_user.py diff --git a/bootstrap.py b/bootstrap.py index 5c14aab..a2353ec 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -54,4 +54,5 @@ def home(): return Response(headers={'Access-Control-Allow-Origin': '*'}) -app.run(port=15789, host="0.0.0.0", debug=False) +if __name__ == '__main__': + app.run(port=15789, host="0.0.0.0", debug=False) diff --git a/hub_api/city_info.py b/hub_api/city_info.py index afe2e1a..292eb33 100644 --- a/hub_api/city_info.py +++ b/hub_api/city_info.py @@ -15,6 +15,8 @@ from pathlib import Path import os from hub_api.config import Config +headers = {'Content-Type': 'application/json'} + class CityInfo(Resource, Config): def __init__(self): @@ -32,8 +34,8 @@ class CityInfo(Resource, Config): 'upper_corner': city.upper_corner, 'created': city.created, 'updated': city.updated, 'user': {'id': city.user.id, 'name': city.user.name, 'email': city.user.email, 'role': city.user.role.value} - }, default=str), status=200) - return Response(response=json.dumps({'err_msg': 'City not found'}), status=404) + }, default=str), status=200, headers=headers) + return Response(response=json.dumps({'err_msg': 'City not found'}), status=404, headers=headers) class City(Resource, Config): @@ -71,9 +73,9 @@ class City(Resource, Config): 'user': {'id': saved_city.user.id, 'name': saved_city.user.name, 'email': saved_city.user.email, 'role': saved_city.user.role.value} }, default=str), status=201) - return Response(response=json.dumps(saved_city), status=200) + return Response(response=json.dumps(saved_city), status=200, headers=headers) else: - return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400) + return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400, headers=headers) except Exception as err: logger.error(err) - return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating city'}), status=400) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating city'}), status=400, headers=headers) diff --git a/hub_api/config.py b/hub_api/config.py index a00b6ca..be8e79a 100644 --- a/hub_api/config.py +++ b/hub_api/config.py @@ -6,15 +6,31 @@ Copyright © 2023 Project Peter Yefi peteryefi@gmail.com from hub.exports.db_factory import DBFactory as CityExportFactory from hub.imports.db_factory import DBFactory import os +from hub.imports.user_factory import UserFactory +from hub.exports.user_factory import UserFactory as ExUserFactory class Config: def __init__(self): - self.export_db_factory = CityExportFactory(db_name='hub_prod', app_env='PROD', + db_name = None + app_env = None + if os.getenv("FLASK_ENV") == 'production': + db_name = 'hub_prod' + app_env = 'PROD' + elif os.getenv("FLASK_ENV") == 'testing': + db_name = 'test_db' + app_env = 'TEST' + + self.export_db_factory = CityExportFactory(db_name=db_name, app_env=app_env, dotenv_path="{}/.env".format(os.path.expanduser('~'))) - self.import_db_factory = DBFactory(db_name='hub_prod', app_env='PROD', + self.import_db_factory = DBFactory(db_name=db_name, app_env=app_env, dotenv_path="{}/.env".format(os.path.expanduser('~'))) + self.user_factory = UserFactory(db_name=db_name, app_env=app_env, + dotenv_path="{}/.env".format(os.path.expanduser('~'))) + + self.ex_user_factory = ExUserFactory(db_name=db_name, app_env=app_env, + dotenv_path="{}/.env".format(os.path.expanduser('~'))) def get_city(self, city_id): return self.export_db_factory.get_city(city_id) diff --git a/hub_api/user.py b/hub_api/user.py index d7e0936..3f5d01f 100644 --- a/hub_api/user.py +++ b/hub_api/user.py @@ -6,18 +6,17 @@ Copyright © 2023 Project Author Peter Yefi peteryefi@gmail.com import json from flask import Response, request from flask_restful import Resource -from hub.imports.user_factory import UserFactory -from hub.exports.user_factory import UserFactory as ExUserFactory -import os from hub.hub_logger import logger from hub_api.helpers.auth import generate_auth_token, role_required from hub.persistence.models import UserRoles +from hub_api.config import Config + +headers = {'Content-Type': 'application/json'} -class User(Resource): +class User(Resource, Config): def __init__(self): - self.user_factory = UserFactory(db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) + super().__init__() @role_required([UserRoles.Admin.value]) def post(self): @@ -26,12 +25,14 @@ class User(Resource): user = self.user_factory.create_user(name=payload["name"], email=payload["email"], password=payload["password"], role=payload["role"]) if type(user) is dict: - return Response(response=json.dumps(user), status=400) + return Response(response=json.dumps(user), status=400, headers=headers) return Response(response=json.dumps({'user': {'id': user.id, 'name': user.name, 'email': user.email, - 'password': user.password, 'role': user.role.value}}), status=201) + 'password': user.password, 'role': user.role.value}}), status=201, + headers=headers) except Exception as err: logger.error(err) - return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating user'}), status=400) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating user'}), status=400, + headers=headers) @role_required([UserRoles.Admin.value]) def put(self): @@ -40,25 +41,24 @@ class User(Resource): res = self.user_factory.update_user(user_id=payload['id'], name=payload['name'], password=payload['password'], role=payload['role'], email=payload['email']) if res: - return Response(response=json.dumps(res), status=400) - return Response(response=json.dumps({'success': 'user updated successfully'}), status=200) + return Response(response=json.dumps(res), status=400, headers=headers) + return Response(response=json.dumps({'success': 'user updated successfully'}), status=200, headers=headers) except Exception as err: logger.error(err) return Response(response=json.dumps({'err_msg': 'Sorry, an error occurred while updating user'}), - status=400) + status=400, headers=headers) -class UserLogin(Resource): +class UserLogin(Resource, Config): def __init__(self): - self.user_factory = ExUserFactory(db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) + super().__init__() def post(self): try: payload = request.get_json() - user = self.user_factory.login_user(email=payload["email"], password=payload["password"]) + user = self.ex_user_factory.login_user(email=payload["email"], password=payload["password"]) if type(user) is dict: - return Response(response=json.dumps(user), status=400) + return Response(response=json.dumps(user), status=400, headers=headers) user = user[0] user_dict = { 'user': { @@ -70,7 +70,7 @@ class UserLogin(Resource): } } user_dict['token'] = generate_auth_token(user_dict) - return Response(response=json.dumps(user_dict), status=200) + return Response(response=json.dumps(user_dict), status=200, headers=headers) except Exception as err: logger.error(err) return Response(response=json.dumps({'err_msg': 'An error occurred while authenticating user'}), status=400) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/base_test.py b/tests/base_test.py new file mode 100644 index 0000000..cdc25b5 --- /dev/null +++ b/tests/base_test.py @@ -0,0 +1,58 @@ +import os +from unittest import TestCase +from bootstrap import app +from hub.persistence.base_repo import BaseRepo +from sqlalchemy import create_engine +from hub.persistence.models import City +from hub.persistence.models import User +from sqlalchemy.exc import ProgrammingError + + +# function to ensure tests run in order shown in fle +def arrange(): + order = {} + + def ordered(f): + order[f.__name__] = len(order) + return f + + def compare(a, b): + return [1, -1][order[a] < order[b]] + + return ordered, compare + + +class BaseTest(TestCase): + """ + Tests for payment resource + """ + + @classmethod + def setUpClass(cls): + os.environ['FLASK_ENV'] = 'testing' + cls.app = app + cls.client = cls.app.test_client() + + # Create test database + repo = BaseRepo(db_name='test_db', app_env='TEST', dotenv_path="{}/.env".format(os.path.expanduser('~'))) + eng = create_engine(f'postgresql://{repo.config.get_db_user()}@/{repo.config.get_db_user()}') + + try: + # delete test database if it exists + conn = eng.connect() + conn.execute('commit') + conn.execute('DROP DATABASE test_db') + conn.close() + except ProgrammingError as err: + print(f'Database does not exist. Nothing to delete') + + cnn = eng.connect() + cnn.execute('commit') + cnn.execute("CREATE DATABASE test_db") + cnn.close() + User.__table__.create(bind=repo.engine, checkfirst=True) + City.__table__.create(bind=repo.engine, checkfirst=True) + + @classmethod + def tearDownClass(cls) -> None: + pass diff --git a/tests/test_user.py b/tests/test_user.py new file mode 100644 index 0000000..0993b24 --- /dev/null +++ b/tests/test_user.py @@ -0,0 +1,124 @@ +import json +from .base_test import BaseTest, arrange +import unittest +from hub_api.helpers.auth import generate_auth_token + +ordered, compare = arrange() +unittest.defaultTestLoader.sortTestMethodsUsing = compare + + +class UserTest(BaseTest): + """ + Tests for User API endpoints + """ + + @classmethod + def setUpClass(cls): + # Call setUp from parent + super().setUpClass() + cls.user_dict = {"user": { + "name": "Test User", + "email": "testuser@gmail.com", + "password": "TestUser@12345", + "role": "Admin", + }} + cls.token = generate_auth_token(cls.user_dict) + + @ordered + def test_create_user_by_non_admin(self): + # When + res = self.client.post('/v1.4/user', data=json.dumps(self.user_dict['user'])) + # Then + self.assertEqual('Invalid payload', res.json['messages']) + self.assertEqual(400, res.status_code) + + + @ordered + def test_create_user_by_admin(self): + # When + res = self.client.post('/v1.4/user', + headers={ + 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + str(self.token) + }, + data=json.dumps(self.user_dict['user'])) + # Then + user = res.json + self.assertEqual(201, res.status_code) + self.assertEqual(type(user['user']), dict) + self.assertEqual(user['user']['email'], self.user_dict['user']['email']) + self.assertEqual(user['user']['role'], self.user_dict['user']['role']) + + @ordered + def test_create_user_with_existing_email(self): + # When + res = self.client.post('/v1.4/user', + headers={ + 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + str(self.token) + }, + data=json.dumps(self.user_dict['user'])) + # Then + self.assertEqual('user with testuser@gmail.com email already exists', res.json['message']) + self.assertEqual(400, res.status_code) + + @ordered + def test_create_user_with_weak_password(self): + # When + self.user_dict['user']['password'] = '1234' + self.user_dict['user']['email'] = 'new@gmail.com' + res = self.client.post('/v1.4/user', + headers={ + 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + str(self.token) + }, + data=json.dumps(self.user_dict['user'])) + # Then + self.assertEqual('Sorry an error occurred while creating user', res.json['err_msg']) + self.assertEqual(400, res.status_code) + + + @ordered + def test_login_user_with_wrong_credential(self): + # When + res = self.client.post('/v1.4/user/login', + headers={ + 'Content-Type': 'application/json' + }, + data=json.dumps({'email': 'wrong@gmail.com', 'password': 'wrong'})) + + # Then + message = res.json + self.assertEqual('user not found', message['message']) + self.assertEqual(400, res.status_code) + + @ordered + def test_login_user_with_correct_credential(self): + # When + self.user_dict['user']['password'] = 'TestUser@12345' + self.user_dict['user']['email'] = 'testuser@gmail.com' + login_data = { + "email": self.user_dict['user']['email'], + "password": self.user_dict['user']['password'] + } + res = self.client.post('/v1.4/user/login', headers={'Content-Type': 'application/json'}, + data=json.dumps(login_data)) + + # Then + user = res.json + self.assertEqual(user['user']['email'], self.user_dict['user']['email']) + self.assertEqual(user['user']['role'], self.user_dict['user']['role']) + self.assertIsNotNone(user['token']) + self.assertEqual(200, res.status_code) + + @classmethod + def tearDownClass(cls) -> None: + # Call tearDown from parent + super().tearDownClass() + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest(unittest.TestLoader( + ).loadTestsFromTestCase(UserTest)) + + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) From 0aef0c32fcb2b7e44d0e8cad203cd6bc9c0d69d6 Mon Sep 17 00:00:00 2001 From: Peter Yefi Date: Wed, 1 Feb 2023 15:58:20 -0500 Subject: [PATCH 10/10] Removed deprecated FLASK_ENV --- hub_api/config.py | 4 ++-- tests/base_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hub_api/config.py b/hub_api/config.py index be8e79a..626563e 100644 --- a/hub_api/config.py +++ b/hub_api/config.py @@ -15,10 +15,10 @@ class Config: def __init__(self): db_name = None app_env = None - if os.getenv("FLASK_ENV") == 'production': + if os.getenv("FLASK_DEBUG") == 'production': db_name = 'hub_prod' app_env = 'PROD' - elif os.getenv("FLASK_ENV") == 'testing': + elif os.getenv("FLASK_DEBUG") == 'testing': db_name = 'test_db' app_env = 'TEST' diff --git a/tests/base_test.py b/tests/base_test.py index cdc25b5..627aef1 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -29,7 +29,7 @@ class BaseTest(TestCase): @classmethod def setUpClass(cls): - os.environ['FLASK_ENV'] = 'testing' + os.environ['FLASK_DEBUG'] = 'testing' cls.app = app cls.client = cls.app.test_client()