Merge branch 'feature/sensors_fetch' into 'master'

ARVE sensors data API

See merge request cara/caimira!371
This commit is contained in:
Andre Henriques 2022-09-19 09:13:53 +02:00
commit 139e7ac91d
7 changed files with 165 additions and 10 deletions

View file

@ -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:

View file

@ -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', '<undefined>'),
arve_client_id=os.environ.get('ARVE_CLIENT_ID', '<undefined>'),
arve_client_secret=os.environ.get('ARVE_CLIENT_SECRET', '<undefined>'),
arve_api_key=os.environ.get('ARVE_API_KEY', '<undefined>'),
# Process parallelism controls. There is a balance between serving a single report
# requests quickly or serving multiple requests concurrently.

View file

@ -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',

View file

@ -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(`<option id=${room.RoomId} value=${room.RoomId}>Sensor ${room.RoomId}</option>`);
});
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.

View file

@ -85,8 +85,6 @@
</div>
{% endblock room_data %}
<br>
<div class="form-group row">
<div class="col-sm-5">
<input type="radio" id="room_data_volume" name="volume_type" value="room_volume_explicit" onclick="require_fields(this)" tabindex="-1" required>

View file

@ -5,6 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta id="url_prefix" data-calculator_prefix="{{ calculator_prefix }}">
<title>
{% block title %}

View file

@ -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 %}