diff --git a/README.md b/README.md index f87dbe69..0122b1ab 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,9 @@ There is one external API call to fetch required information related to the geog The documentation for this geocoding service is available at https://developers.arcgis.com/rest/geocode/api-reference/geocoding-suggest.htm . Please note that there is no need for keys on this API call. It is **free-of-charge**. +- **Humidity and Inside Temperature:** +There is the possibility of using one external API call to fetch information related to a location specified in the UI. The data is related to the inside temperature and humidity taken from an indoor measurement device. Note that the API currently used from ARVE is only available for the `CERN theme` as the authorised sensors are installed at CERN." + ## Update configuration If you need to **update** existing configuration, then modify this repository and after having logged in, run: diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index ee522bde..0c3505c2 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -18,6 +18,7 @@ import zlib import jinja2 import loky from tornado.web import Application, RequestHandler, StaticFileHandler +from tornado.httpclient import AsyncHTTPClient, HTTPRequest import tornado.log from . import markdown_tools @@ -249,6 +250,51 @@ class ReadmeHandler(BaseRequestHandler): self.finish(readme) +class ArveData(BaseRequestHandler): + async def get(self, hotel_id, floor_id): + client_id = self.settings["arve_client_id"] + client_secret = self.settings['arve_client_secret'] + arve_api_key = self.settings['arve_api_key'] + + http_client = AsyncHTTPClient() + + URL = 'https://arveapi.auth.eu-central-1.amazoncognito.com/oauth2/token' + headers = { "Content-Type": "application/x-www-form-urlencoded", + "Authorization": b"Basic " + base64.b64encode(f'{client_id}:{client_secret}'.encode()) + } + + try: + response = await http_client.fetch(HTTPRequest( + url=URL, + method='POST', + headers=headers, + body="grant_type=client_credentials" + ), + raise_error=True) + except Exception as e: + print("Something went wrong: %s" % e) + + access_token = json.loads(response.body)['access_token'] + + URL = f'https://api.arve.swiss/v1/{hotel_id}/{floor_id}' + headers = { + "x-api-key": arve_api_key, + "Authorization": f'Bearer {access_token}' + } + try: + response = await http_client.fetch(HTTPRequest( + url=URL, + method='GET', + headers=headers, + ), + raise_error=True) + except Exception as e: + print("Something went wrong: %s" % e) + + self.set_header("Content-Type", 'application/json') + return self.finish(response.body) + + def make_app( debug: bool = False, calculator_prefix: str = '/calculator', @@ -266,6 +312,7 @@ def make_app( (calculator_prefix + r'/report-json', ConcentrationModelJsonResponse), (calculator_prefix + r'/baseline-model/result', StaticModel), (calculator_prefix + r'/user-guide', ReadmeHandler), + (calculator_prefix + r'/api/arve/v1/(.*)/(.*)', ArveData), (calculator_prefix + r'/static/(.*)', StaticFileHandler, {'path': calculator_static_dir}), ] @@ -298,6 +345,9 @@ def make_app( # COOKIE_SECRET being undefined will result in no login information being # presented to the user. cookie_secret=os.environ.get('COOKIE_SECRET', ''), + arve_client_id=os.environ.get('ARVE_CLIENT_ID', ''), + arve_client_secret=os.environ.get('ARVE_CLIENT_SECRET', ''), + arve_api_key=os.environ.get('ARVE_API_KEY', ''), # Process parallelism controls. There is a balance between serving a single report # requests quickly or serving multiple requests concurrently. diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index 254ae02c..6b9a6796 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -31,6 +31,7 @@ class FormData: activity_type: str air_changes: float air_supply: float + arve_sensors_option: bool ceiling_height: float exposed_coffee_break_option: str exposed_coffee_duration: int @@ -77,6 +78,7 @@ class FormData: window_width: float windows_number: int window_opening_regime: str + sensor_in_use: str short_range_option: str short_range_interactions: list @@ -86,6 +88,7 @@ class FormData: 'activity_type': 'office', 'air_changes': 0., 'air_supply': 0., + 'arve_sensors_option': False, 'calculator_version': _NO_DEFAULT, 'ceiling_height': 0., 'exposed_coffee_break_option': 'coffee_break_0', @@ -109,7 +112,7 @@ class FormData: 'infected_lunch_start': '12:30', 'infected_people': _NO_DEFAULT, 'infected_start': '08:30', - 'inside_temp': 293., + 'inside_temp': _NO_DEFAULT, 'location_latitude': _NO_DEFAULT, 'location_longitude': _NO_DEFAULT, 'location_name': _NO_DEFAULT, @@ -132,6 +135,7 @@ class FormData: 'windows_frequency': 60., 'windows_number': 0, 'window_opening_regime': 'windows_open_permanently', + 'sensor_in_use': '', 'short_range_option': 'short_range_no', 'short_range_interactions': '[]', } @@ -287,14 +291,18 @@ class FormData: volume = self.room_volume else: volume = self.floor_area * self.ceiling_height - if self.humidity == '': + + if self.arve_sensors_option == False: if self.room_heating_option: humidity = 0.3 else: humidity = 0.5 + inside_temp = 293. else: humidity = float(self.humidity) - room = models.Room(volume=volume, inside_temp=models.PiecewiseConstant((0, 24), (self.inside_temp,)), humidity=humidity) + inside_temp = self.inside_temp + + room = models.Room(volume=volume, inside_temp=models.PiecewiseConstant((0, 24), (inside_temp,)), humidity=humidity) infected_population = self.infected_population() @@ -734,7 +742,7 @@ def baseline_raw_form_data() -> typing.Dict[str, typing.Union[str, float]]: 'floor_area': '', 'hepa_amount': '250', 'hepa_option': '0', - 'humidity': '', + 'humidity': '0.5', 'infected_coffee_break_option': 'coffee_break_4', 'infected_coffee_duration': '10', 'infected_dont_have_breaks_with_exposed': '1', @@ -744,7 +752,7 @@ def baseline_raw_form_data() -> typing.Dict[str, typing.Union[str, float]]: 'infected_lunch_start': '12:30', 'infected_people': '1', 'infected_start': '09:00', - 'inside_temp': 293., + 'inside_temp': '293.', 'location_latitude': 46.20833, 'location_longitude': 6.14275, 'location_name': 'Geneva', diff --git a/caimira/apps/calculator/static/js/form.js b/caimira/apps/calculator/static/js/form.js index 655ff5ac..25ca0af4 100644 --- a/caimira/apps/calculator/static/js/form.js +++ b/caimira/apps/calculator/static/js/form.js @@ -271,6 +271,64 @@ function on_wearing_mask_change() { }) } +function populate_temp_hum_values(data, index) { + $("#sensor_temperature").text(Math.round(data[index].Details.T) + '°C'); + $("#sensor_humidity").text(Math.round(data[index].Details.RH) + '%'); + $("[name='inside_temp']").val(data[index].Details.T + 273.15); + $("[name='humidity']").val(data[index].Details.RH/100); +}; + +//Data from ARVE sensors +var DATA_FROM_SENSORS; +function show_sensors_data(url) { + + const HOTEL_ID = "CERN" + const FLOOR_ID = "1" + + if ($('#sensors > option').length == 0) { + $.ajax({ + url: `${$('#url_prefix').data().calculator_prefix}/api/arve/v1/${HOTEL_ID}/${FLOOR_ID}`, + type: 'GET', + success: function (result) { + DATA_FROM_SENSORS = result; + result.map(room => { + $("#sensors").append(``); + }); + populate_temp_hum_values(result, 0); + if (url.searchParams.has('sensor_in_use')) { + $("#sensors").val(url.searchParams.get('sensor_in_use')); + populate_temp_hum_values(result, result.findIndex(function(sensor) { + return sensor.RoomId == url.searchParams.get('sensor_in_use'); + })); + } + }, + error: function() { + alert('Authentication Error - Something went wrong during the authentication process.'); + }, + }); + } +}; + +$("#sensors").change(function (el) { + sensor_id = DATA_FROM_SENSORS.findIndex(function(sensor) { + return sensor.RoomId == el.target.value + }); + populate_temp_hum_values(DATA_FROM_SENSORS, sensor_id); +}); + +function on_use_sensors_data_change(url) { + sensor_data = $('input[type=radio][name=arve_sensors_option]') + sensor_data.each(function (index) { + if (this.checked) { + getChildElement($(this)).show(); + show_sensors_data(url); + } + else { + getChildElement($(this)).hide(); + } + }) +} + function on_short_range_option_change() { short_range = $('input[type=radio][name=short_range_option]') short_range.each(function (index){ @@ -404,6 +462,11 @@ function validate_form(form) { }); } + // Logic for the API requests. Always set humity input as the empty string so that we can profit from the "room_heating_option default" values for humidity. + if ($("#arve_sensor_no").prop('checked')) { + $("[name='humidity']").val(''); + } + // Validate location input. if (submit) { // We make the non-visible location inputs mandatory, without marking them as "required" inputs. @@ -706,6 +769,10 @@ $(document).ready(function () { $("#sr_interactions").text(index - 1); } + else if (name == 'sensor_in_use') { + // TODO - Validate if sensor exists + } + //Ignore 0 (default) values from server side else if (!(elemObj.classList.contains("non_zero") || elemObj.classList.contains("remove_zero")) || (value != "0.0" && value != "0")) { elemObj.value = value; @@ -743,6 +810,14 @@ $(document).ready(function () { //Check all radio buttons previously selected $("input[type=radio]:checked").each(function() {require_fields(this)}); + // On CERN theme, when the arve_sensors_option changes we want to make its respective + // children show/hide. + if ($("input[type=radio][name=arve_sensors_option]").length > 0) { + $("input[type=radio][name=arve_sensors_option]").change(on_use_sensors_data_change); + // Call the function now to handle forward/back button presses in the browser. + on_use_sensors_data_change(url); + } + // When the ventilation_type changes we want to make its respective // children show/hide. $("input[type=radio][name=ventilation_type]").change(on_ventilation_type_change); @@ -841,9 +916,6 @@ $(document).ready(function () { templateSelection: formatLocationSelection }); - // Logic for the API requests. Always set humity input as the empty string so that we can profit from the "room_heating_option default" values for humidity. - $("[name='humidity']").val(""); - function formatlocation(suggestedLocation) { // Function is called for each location from the geocoding API. diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 37699a44..07561755 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -85,8 +85,6 @@ {% endblock room_data %} -
-
diff --git a/caimira/apps/templates/base/layout.html.j2 b/caimira/apps/templates/base/layout.html.j2 index 6e61f45b..e7ac6dea 100644 --- a/caimira/apps/templates/base/layout.html.j2 +++ b/caimira/apps/templates/base/layout.html.j2 @@ -5,6 +5,7 @@ + {% block title %} diff --git a/caimira/apps/templates/cern/calculator.form.html.j2 b/caimira/apps/templates/cern/calculator.form.html.j2 index 79a879d9..ffc67bd2 100644 --- a/caimira/apps/templates/cern/calculator.form.html.j2 +++ b/caimira/apps/templates/cern/calculator.form.html.j2 @@ -4,4 +4,27 @@ <div data-tooltip="The area you wish to study (choose one of the 2 options). Use GIS Portal or measure. Also indicate if a central (radiator-type) heating system is in use."> <span class="tooltip_text">?</span> </div> + + <div class="split"> + <div>Use data from ARVE sensors:</div> + <div> + <input class="ml-2" type="radio" id="arve_sensor_no" name="arve_sensors_option" value=0 checked="checked"> + <label for="arve_sensor_no">No</label> + <input class="ml-2" type="radio" id="arve_sensor_yes" name="arve_sensors_option" value=1 data-enables="#DIVsensors_data"> + <label for="arve_sensor_yes">Yes</label> + </div> + </div> + <div id="DIVsensors_data" style="display:none"> + <div class="form-group row mb-0"> + <div class="col-sm-4"><label class="col-form-label">Sensor:</label></div> + <div class="col-sm-6"> + <select id="sensors" name="sensor_in_use" class="form-control"> + </select> + </div> + </div> + <div> + <div><label>Temperature: </label><span class="ml-3 font-weight-bold" id="sensor_temperature"></span></div> + <div><label>Relative Humidity: </label><span class="ml-3 font-weight-bold" id="sensor_humidity"></span></div> + </div> + </div> {% endblock room_data %} \ No newline at end of file