Skip to content

Commit

Permalink
Merge pull request #3137 from SEED-platform/Add-Sensor-Readings-Model…
Browse files Browse the repository at this point in the history
…-and-Getter-and-UI

Add SensorReadings Model, View, and UI
  • Loading branch information
haneslinger committed Feb 21, 2022
2 parents c70fb58 + df49a4f commit 20db9b9
Show file tree
Hide file tree
Showing 9 changed files with 572 additions and 5 deletions.
26 changes: 26 additions & 0 deletions seed/migrations/0158_sensorreading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 3.2.10 on 2022-02-03 16:59

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('seed', '0157_sensor'),
]

operations = [
migrations.CreateModel(
name='SensorReading',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reading', models.FloatField(null=True)),
('timestamp', models.DateTimeField()),
('sensor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sensor_readings', to='seed.sensor')),
],
options={
'unique_together': {('timestamp', 'sensor')},
},
),
]
13 changes: 13 additions & 0 deletions seed/models/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,16 @@ class Sensor(models.Model):
units = models.CharField(max_length=63)

column_name = models.CharField(unique=True, max_length=255)


class SensorReading(models.Model):
reading = models.FloatField(null=True)
timestamp = models.DateTimeField()
sensor = models.ForeignKey(
Sensor,
on_delete=models.CASCADE,
related_name='sensor_readings',
)

class Meta:
unique_together = ('timestamp', 'sensor')
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,41 @@ angular.module('BE.seed.controller.inventory_detail_sensors', [])
.controller('inventory_detail_sensors_controller', [
'$scope',
'$stateParams',
'$window',
'cycles',
'inventory_service',
'inventory_payload',
'sensors',
'sensor_service',
'property_sensor_usage',
'spinner_utility',
'organization_payload',
function (
$scope,
$stateParams,
$window,
cycles,
inventory_service,
inventory_payload,
sensors,
sensor_service,
property_sensor_usage,
spinner_utility,
organization_payload,
) {
spinner_utility.show();
$scope.item_state = inventory_payload.state;
$scope.inventory_type = $stateParams.inventory_type;
$scope.organization = organization_payload.organization;
$scope.property_sensor_usage = property_sensor_usage;
$scope.filler_cycle = cycles.cycles[0].id;

$scope.inventory = {
view_id: $stateParams.view_id
};

var getSensorLabel = function (sensor) {
return sensor.display_name + " - " + sensor.units;
return sensor.display_name;
};

var resetSelections = function () {
Expand All @@ -37,9 +49,175 @@ angular.module('BE.seed.controller.inventory_detail_sensors', [])
});
};

$scope.data = property_sensor_usage.readings.map(reading => {
readings = _.omit(reading, "timestamp");
readings_by_sensor = Object.keys(readings).map(function(key) {
return {
sensor: key,
value: readings[key]
}
});

return {
timestamp: reading["timestamp"],
readings: readings_by_sensor
}
});

// On page load, all sensors and readings
$scope.has_sensor_readings = $scope.data.length > 0;
$scope.has_sensors = sensors.length > 0;

var sorted_sensors = _.sortBy(sensors, ['id']);
resetSelections();

$scope.sensor_selection_toggled = function (is_open) {
if (!is_open) {
$scope.applyFilters();
}
};

var base_sensor_col_defs = [{
field: 'display_name',
enableHiding: false,
type: 'string'
}, {
field: 'type',
enableHiding: false
}, {
field: 'location_identifier',
displayName: 'location identifier',
enableHiding: false
},{
field: 'units',
enableHiding: false
}, {
field: 'column_name',
enableHiding: false
},{
field: 'description',
enableHiding: false
}];

$scope.sensorGridOptions = {
data: sensors,
columnDefs: base_sensor_col_defs,
enableColumnResizing: true,
enableFiltering: true,
flatEntityAccess: true,
fastWatch: true,
};

$scope.usageGridOptions = {
data: 'data',
columnDefs: property_sensor_usage.column_defs,
enableColumnResizing: true,
enableFiltering: true,
flatEntityAccess: true,
fastWatch: true,
};

$scope.apply_column_settings = function () {
_.forEach($scope.usageGridOptions.columnDefs, function (column) {
column.enableHiding = false;
column.enableColumnResizing = true;

if (column.field === 'year') {
// Filter years like integers
column.filter = inventory_service.combinedFilter();
} else if (column._filter_type === 'reading') {
column.cellFilter = 'number: 2';
column.filter = inventory_service.combinedFilter();
} else if (column._filter_type === 'datetime') {
column.filter = inventory_service.dateFilter();
}
});
};

$scope.apply_column_settings();

// Ideally, these should be translatable, but the "selected" property
// should always be in English as this gets sent to BE.
$scope.interval = {
options: [
'Exact',
'Month',
'Year'
],
selected: 'Exact'
};

// given a list of sensor labels, it returns the filtered readings and column defs
// This is used by the primary filterBy... functions
var filterBySensorLabels = function filterBySensorLabels (readings, columnDefs, sensorLabels) {
var timeColumns = ['timestamp', 'month', 'year'];
var selectedColumns = sensorLabels.concat(timeColumns);
var filteredReadings = readings.map(function (reading) {
return Object.entries(reading).reduce(function (newReading, _ref) {
var key = _ref[0],
value = _ref[1];

if (selectedColumns.includes(key)) {
newReading[key] = value;
}

return newReading;
}, {});
});

var filteredColumnDefs = columnDefs.filter(function (columnDef) {
return selectedColumns.includes(columnDef.field);
});
return {
readings: filteredReadings,
columnDefs: filteredColumnDefs
};
};

// given the sensor selections, it returns the filtered readings and column defs
var filterBySensorSelections = function (readings, columnDefs, sensorSelections) {
// filter according to sensor selections
var selectedSensorLabels = sensorSelections.filter(function (selection) {
return selection.selected;
})
.map(function (selection) {
return selection.label;
});

return filterBySensorLabels(readings, columnDefs, selectedSensorLabels);
};

// filters the sensor readings by selected sensors and updates the table
$scope.applyFilters = function () {
results = filterBySensorSelections(property_sensor_usage.readings, property_sensor_usage.column_defs, $scope.sensor_selections);
readings = results.readings;
columnDefs = results.columnDefs;

$scope.data = readings;
$scope.usageGridOptions.columnDefs = columnDefs;
$scope.has_sensor_readings = $scope.data.length > 0;
$scope.apply_column_settings();
};

// refresh_readings make an API call to refresh the base readings data
// according to the selected interval
$scope.refresh_readings = function () {
spinner_utility.show();
sensor_service.property_sensor_usage(
$scope.inventory.view_id,
$scope.organization.id,
$scope.interval.selected,
[] // Not excluding any sensors from the query
).then(function (usage) {
// update the base data and reset filters
property_sensor_usage = usage;

resetSelections();
$scope.applyFilters();
spinner_utility.hide();
});
};

$scope.inventory_display_name = function (property_type) {
let error = '';
let field = property_type == 'property' ? $scope.organization.property_display_field : $scope.organization.taxlot_display_field;
Expand Down
7 changes: 7 additions & 0 deletions seed/static/seed/js/seed.js
Original file line number Diff line number Diff line change
Expand Up @@ -1747,10 +1747,17 @@ SEED_app.config(['stateHelperProvider', '$urlRouterProvider', '$locationProvider
});
return promise;
}],
property_sensor_usage: ['$stateParams', 'user_service', 'sensor_service', function ($stateParams, user_service, sensor_service) {
var organization_id = user_service.get_organization().id;
return sensor_service.property_sensor_usage($stateParams.view_id, organization_id, 'Exact');
}],
sensors: ['$stateParams', 'user_service', 'sensor_service', function ($stateParams, user_service, sensor_service) {
var organization_id = user_service.get_organization().id;
return sensor_service.get_sensors($stateParams.view_id, organization_id);
}],
cycles: ['cycle_service', function (cycle_service) {
return cycle_service.get_cycles();
}],
organization_payload: ['user_service', 'organization_service', function (user_service, organization_service) {
return organization_service.get_organization(user_service.get_organization().id);
}]
Expand Down
13 changes: 13 additions & 0 deletions seed/static/seed/js/services/sensor_service.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ angular.module('BE.seed.service.sensor', [])
});
};

sensor_factory.property_sensor_usage = function (property_view_id, organization_id, interval, excluded_sensor_ids) {
if (_.isUndefined(excluded_sensor_ids)) excluded_sensor_ids = [];
return $http.post(
'/api/v3/properties/' + property_view_id + '/sensor_usage/?organization_id=' + organization_id,
{
interval: interval,
excluded_sensor_ids: excluded_sensor_ids
}
).then(function (response) {
return response.data;
});
};

return sensor_factory;
}
]);
58 changes: 55 additions & 3 deletions seed/static/seed/partials/inventory_detail_sensors.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,64 @@ <h2>
</div>
</div>
</div>
<div class="section_content_container" ng-cloak>
<div class="section_content_container" ng-cloak style="padding-left:15px; padding-right:15px;">

<div id="sensor-info-container">
<div id="grid-container" style="padding-top: 15px">
<div ng-show="has_sensors">
<div ui-grid="sensorGridOptions" ui-grid-resize-columns ui-grid-auto-resize></div>
</div>
<div ng-hide="has_sensors">
<div class="jumbotron text-center" translate>No Data</div>
</div>
</div>
</div>

<div class="sensor-usage-info-container" style="padding-top: 15px; margin-bottom: 15px;">
<div class="form-group">
<label>{$:: 'Interval' | translate $}:</label>
<div style="display: inline-block;">
<select class="form-control"
ng-model="interval.selected"
ng-change="refresh_readings()"
ng-options="option for option in interval.options"
style="margin-left: 10px;">
</select>
</div>
</div>
<div class="form-group" uib-dropdown is-open="sensors_options.isopen" auto-close="outsideClick" on-toggle="sensor_selection_toggled(open)" style="margin-top: 15px;">
<button type="button" class="btn btn-default" uib-dropdown-toggle>
Filter Sensors <span class="caret"></span>
</button>
<ul class="dropdown-menu" uib-dropdown-menu>
<li ng-repeat="sensor in sensor_selections">
<div>
<label class="btn btn-default" style="width: 100%; display: flex; justify-content: space-between; border-radius: 0px;">
{$:: sensor.label $}<input type="checkbox" ng-model="sensor.selected" name="sensor.label" style="width: 14px; height: 14px; margin-left: 10px;">
</label>
</div>
</li>
</ul>
</div>
<div class="section_content" ng-show="has_sensor_readings">
<div id="grid-container">
<div ui-grid="usageGridOptions" ui-grid-resize-columns></div>
</div>
</div>
<div ng-hide="has_sensor_readings">
<div class="jumbotron text-center" translate>No Data</div>
</div>
</div>
<div>
Sensors
<ul>
<li ng-repeat="sensor in sensor_selections">
{$:: sensor.label $}
<li ng-repeat="row in data">
{$:: row.timestamp $}
<ul>
<li ng-repeat="reading in row.readings">
{$:: reading.sensor $}: {$:: reading.value $}
</li>
</ul>
</li>
</ul>
</div>
Expand Down

0 comments on commit 20db9b9

Please sign in to comment.