Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add measurement processing workflow #363

Merged
42 commits merged into from Apr 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3ea6d35
Add incoming measurement processing workflow
jackxujh Apr 1, 2020
589ae59
Remove unused Grafana dashboard export
jackxujh Apr 2, 2020
0719bd0
Remove unused presets
jackxujh Apr 2, 2020
7f4e645
Add delegate to create user custom sensor types
jackxujh Apr 2, 2020
e3db4fd
Move files
jackxujh Apr 2, 2020
036381a
Update tests
jackxujh Apr 8, 2020
8e42c05
Format code
jackxujh Apr 8, 2020
e7fe93a
Merge branch 'backend/develop' into backend/formula-processing
jackxujh Apr 8, 2020
8946cef
Format code
jackxujh Apr 8, 2020
bea61a4
Merge branch 'backend/develop' into backend/formula-processing
jackxujh Apr 10, 2020
d00b481
Merge branch 'backend/develop' into backend/formula-processing
jackxujh Apr 10, 2020
49d5181
Format code
jackxujh Apr 10, 2020
83af3ef
Update built-in sensor types
jackxujh Apr 10, 2020
6a4f5d9
Use unified return data type in ingestion engine
jackxujh Apr 10, 2020
feddbbf
Format foreign code
jackxujh Apr 10, 2020
89cbe73
Update createOrResetBuiltInSensorTypeAtPresetIndex
jackxujh Apr 10, 2020
d430886
Format code
jackxujh Apr 10, 2020
3c55c8a
Add unit tests for utility functions
jackxujh Apr 10, 2020
2a22f81
Add a test for createOrResetAllBuiltInSensorTypes
jackxujh Apr 10, 2020
1ed4c8c
Format code
jackxujh Apr 10, 2020
ccc161d
Remove unused tests
jackxujh Apr 10, 2020
4e5ed41
Rename built_in_sensor_types to built_in_content
jackxujh Apr 10, 2020
0ae5513
Add tests for createCustomSensorType
jackxujh Apr 10, 2020
e954a78
Update import
jackxujh Apr 10, 2020
61cc365
Format code
jackxujh Apr 10, 2020
4b19826
Rename test for createCustomSensorType
jackxujh Apr 10, 2020
f04263a
Revert "Rename test for createCustomSensorType"
jackxujh Apr 10, 2020
b400d2d
Add test for createSensorFromPresetAtIndex
jackxujh Apr 10, 2020
5106b30
Test multiple sensor creation
jackxujh Apr 10, 2020
98815b6
Add test for createVenueFromPresetAtIndex
jackxujh Apr 10, 2020
88cbda8
Format code
jackxujh Apr 10, 2020
ad91d94
Add test for createEventFromPresetAtIndex
jackxujh Apr 10, 2020
69cf9da
Update sensor creation tests
jackxujh Apr 10, 2020
863d8ea
Update venue creation tests
jackxujh Apr 10, 2020
66599df
Format code
jackxujh Apr 10, 2020
e75b00b
Fix broken test due to renaming
jackxujh Apr 10, 2020
6e3d743
Improve simulator and recover tests
jackxujh Apr 11, 2020
a1a5a48
Rename fPass() to fEmptyResult()
jackxujh Apr 11, 2020
a19056e
Add explanation to fMercurySimpleTemperatureSensor
jackxujh Apr 11, 2020
d3415da
Remove extraneous parameter name prefix
jackxujh Apr 11, 2020
fb29c4a
Return result directly for simple formulas
jackxujh Apr 11, 2020
9781d26
Rename local variable for clarity
jackxujh Apr 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 1 addition & 7 deletions ag_data/admin.py
@@ -1,12 +1,6 @@
from django.contrib import admin # noqa f401

from .models import (
AGVenue,
AGEvent,
AGSensorType,
AGSensor,
AGMeasurement,
)
from .models import AGVenue, AGEvent, AGSensorType, AGSensor, AGMeasurement

admin.site.register(AGVenue)
admin.site.register(AGEvent)
Expand Down
33 changes: 33 additions & 0 deletions ag_data/formulas/ingestion_engine.py
@@ -0,0 +1,33 @@
from ag_data import models
from ag_data.formulas.library.system import mercury_formulas as hgFormulas


class MeasurementIngestionEngine:

processingFormulas = {
This conversation was marked as resolved.
Show resolved Hide resolved
0: hgFormulas.fEmptyResult,
2: hgFormulas.fMercurySimpleTemperatureSensor,
4: hgFormulas.fMercuryDualTemperatureSensor,
6: hgFormulas.fMercuryFlowSensor,
}
jackxujh marked this conversation as resolved.
Show resolved Hide resolved

def saveMeasurement(self, measurementDict, event):
timestamp = measurementDict["measurement_timestamp"]
sensor_id = measurementDict["measurement_sensor"]
rawValue = measurementDict["measurement_values"]
jackxujh marked this conversation as resolved.
Show resolved Hide resolved

sensor = models.AGSensor.objects.get(pk=sensor_id)
sensor_type = sensor.type_id
jackxujh marked this conversation as resolved.
Show resolved Hide resolved
processing_formula = sensor_type.processing_formula
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure that I understand correctly, this sensor_type.processing_formula is expected to be one of 0, 2, 4, 6?
If so, how is this value guaranteed to be one of them?

My guessing is that our server somehow sends a user a list of available numbers (=0, 2, 4, 6) and there's no way for a user to choose a value other than the one in the list. But I couldn't find any link from the key of processingFomula dictionary (0, 2, 4, 6).

Could you explain this?

Copy link
Contributor

@alldne alldne Apr 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that there is built_in_content.py and also ag_data/presets/sample_user_data.py. I guess the implementation of the builtin function and also processingFormula dictionary should be inside the file.
And there are total 3 files that hardcode 0, 2, 4, 6, which makes maintenance harder. For instance, when you add a sensor with index 10, then you should edit three files (Shotgun surgery) and it's not even easy to find which files to edit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fast and cheap but not good implementation. Ideally, the mapping should be robust and easy to change, so the user or developer does not need to maintain the digits in different yet not directly linked files to modify the code and still make it work. I am thinking what Dan has been saying, that a string or other version of mapping may be more appropriate.

The problem here is, how do we map something defined in a dictionary (maybe a value of some dictionary-allowed type, such as numbers, strings, bool, etc) to a function handler?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's obvious that we should have this kind of in-memory dictionary that maps something to a function in our python code unless we dynamically evaluate a raw string from outside of our codebase hoping that it's a valid python code snippets.

I don't really want to evaluate an untrusted code. Providing a sandboxed environment for it is tricky especially for python.

So the conclusion is, the way that the current code works is good as-is but let's organize it for us not to edit multiple locations when we work on it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this topic is bigger than what we can accommodate in the discussion of a PR. So here is a dedicated issue #378 created for reimagining the formula function mapping.


formula = MeasurementIngestionEngine.processingFormulas.get(
processing_formula, hgFormulas.fEmptyResult
)

value = {"reading": rawValue}

value["result"] = formula(timestamp, sensor, rawValue)

return models.AGMeasurement.objects.create(
timestamp=timestamp, event_uuid=event, sensor_id=sensor, value=value
)
48 changes: 48 additions & 0 deletions ag_data/formulas/library/system/mercury_formulas.py
@@ -0,0 +1,48 @@
#
# Formulas for Mercury's built-in and natively supported sensor types
# Sample processing formulas with primary sensors and fictional Mercury-branded sensors
#

from ag_data import models


def fEmptyResult(timestamp, sensor, payload):

return {}


def fMercurySimpleTemperatureSensor(timestamp, sensor, payload):

jackxujh marked this conversation as resolved.
Show resolved Hide resolved
# This Simple Temperature Sensor decides it wants its own formula, even though it does
# nothing more than the fEmptyResult function.

return {}


def fMercuryDualTemperatureSensor(timestamp, sensor, payload):
mean = payload["internal"] / 2 + payload["external"] / 2
diff = payload["internal"] - payload["external"]

return {"mean": mean, "diff": diff}


def fMercuryFlowSensor(timestamp, sensor, payload):
result = {}

measurements = models.AGMeasurement.objects.filter(sensor_id=sensor.id)

if measurements.count() == 0:
result = {"gasLevel": 100}
else:
latest = measurements.latest("timestamp")
timeElapsed = timestamp - latest.timestamp

lastResult = latest.value["result"]["gasLevel"]

if lastResult is not None:
result = {
"gasLevel": lastResult
- payload["volumetricFlow"] * (timeElapsed.microseconds / 1000000)
}

return result
4 changes: 4 additions & 0 deletions ag_data/formulas/library/usr/alice_formulas.py
@@ -0,0 +1,4 @@
#
# Alice's Formulas for example sensor types
#
#
4 changes: 4 additions & 0 deletions ag_data/formulas/library/usr/bob_formulas.py
@@ -0,0 +1,4 @@
#
# Bob's Formulas for example sensor types
#
#
52 changes: 52 additions & 0 deletions ag_data/presets/built_in_content.py
@@ -0,0 +1,52 @@
built_in_sensor_types = [
{
"agSensorTypeID": 0,
"agSensorTypeName": "Coin Side Sensor",
"agSensorTypeFormula": 0,
"agSensorTypeFormat": {
"reading": {"side": {"unit": "", "format": "bool"}, "result": {}}
},
},
{
"agSensorTypeID": 2,
"agSensorTypeName": "Simple Temperature Sensor",
"agSensorTypeFormula": 2,
"agSensorTypeFormat": {
"reading": {"reading": {"unit": "Celsius", "format": "float"}},
"result": {},
},
},
{
"agSensorTypeID": 4,
"agSensorTypeName": "Dual Temperature Sensor",
"agSensorTypeFormula": 4,
"agSensorTypeFormat": {
"reading": {
"internal": {"unit": "Keivin", "format": "float"},
"external": {"unit": "Keivin", "format": "float"},
},
"result": {
"mean": {"unit": "Keivin", "format": "float"},
"diff": {"unit": "Keivin", "format": "float"},
},
},
},
{
"agSensorTypeID": 6,
"agSensorTypeName": "Gas Flow Sensor",
"agSensorTypeFormula": 6,
"agSensorTypeFormat": {
"reading": {"volumetricFlow": {"unit": "cc/sec", "format": "float"}},
"result": {"gasLevel": {"unit": "%", "format": "float"}},
},
},
{
"agSensorTypeID": 8,
"agSensorTypeName": "Gaussian Random Number Emitter",
"agSensorTypeFormula": 0,
"agSensorTypeFormat": {
"reading": {"sample": {"unit": "", "format": "float"}},
"result": {},
},
},
]
104 changes: 104 additions & 0 deletions ag_data/presets/helpers.py
@@ -0,0 +1,104 @@
from ag_data import models
from ag_data.presets import sample_user_data
from ag_data import utilities


jackxujh marked this conversation as resolved.
Show resolved Hide resolved
def createVenueFromPresetAtIndex(index):
"""Create a venue from available presets of venues

Arguments:

index {int} -- the index of the sensor preset to use.

Raises:

Exception: an exception raises when the index is not valid in presets.
"""

if index > len(sample_user_data.sample_venues) - 1:
raise Exception(
"Cannot find requested venue (index " + str(index) + ") from presets"
jackxujh marked this conversation as resolved.
Show resolved Hide resolved
)
else:
pass

preset = sample_user_data.sample_venues[index]

venue = models.AGVenue.objects.create(
name=preset["agVenueName"],
description=preset["agVenueDescription"],
latitude=preset["agVenueLatitude"],
longitude=preset["agVenueLongitude"],
)

return venue


def createEventFromPresetAtIndex(venue, index):
"""Create an event from available presets of events

Arguments:

index {int} -- the index of the sensor preset to use.

Raises:

Exception: an exception raises when the index is not valid in presets.

Assertion: an assertion error raises when there is no venue prior to the
creation of the event.
"""

if index > len(sample_user_data.sample_events) - 1:
raise Exception(
"Cannot find requested event (index " + str(index) + ") from presets"
)
else:
pass

preset = sample_user_data.sample_events[index]

utilities.assertVenue(venue)

event = models.AGEvent.objects.create(
name=preset["agEventName"],
date=preset["agEventDate"],
description=preset["agEventDescription"],
venue_uuid=venue,
)

return event


def createSensorFromPresetAtIndex(index):
"""Create a sensor from available presets of sensors

Arguments:

index {int} -- the index of the sensor preset to use.

cascadeCreation {bool=False} -- whether or not to create a corresponding sensor
type which the chosen sensor preset needs (default: {False})

Raises:

Exception: an exception raises when the index is not valid in presets.
"""

if index > len(sample_user_data.sample_sensors) - 1:
raise Exception(
"Cannot find requested sensor (index " + str(index) + ") from presets"
)
else:
pass

preset = sample_user_data.sample_sensors[index]

sensorType = models.AGSensorType.objects.get(pk=preset["agSensorType"])
utilities.assertSensorType(sensorType)

sensor = models.AGSensor.objects.create(
name=preset["agSensorName"], type_id=sensorType
)

return sensor
31 changes: 8 additions & 23 deletions ag_data/presets.py → ag_data/presets/sample_user_data.py
@@ -1,4 +1,4 @@
venue_presets = [
sample_venues = [
{
"agVenueName": "Washington Square Park",
"agVenueDescription": "Sunnyside Daycare's Butterfly Room.",
Expand All @@ -13,7 +13,7 @@
},
]

event_presets = [
sample_events = [
{
"agEventName": "Sunny Day Test Drive",
"agEventDate": "2020-02-02T20:21:22",
Expand All @@ -28,25 +28,10 @@
},
]

sensor_type_presets = [
{
"agSensorTypeID": 0,
"agSensorTypeName": "Simple Temperature Sensor",
"agSensorTypeFormula": 0,
"agSensorTypeFormat": {"reading": {"unit": "Celsius", "format": "float"}},
},
{
"agSensorTypeID": 1,
"agSensorTypeName": "Dual Temperature Sensor",
"agSensorTypeFormula": 0,
"agSensorTypeFormat": {
"internal": {"unit": "Keivin", "format": "float"},
"external": {"unit": "Keivin", "format": "float"},
},
},
]

sensor_presets = [
{"agSensorName": "Sample Simple Temperature", "agSensorType": 0},
{"agSensorName": "Sample Dual Temperature", "agSensorType": 1},
sample_sensors = [
{"agSensorName": "Sample Simple Temperature", "agSensorType": 2},
{"agSensorName": "Sample Dual Temperature", "agSensorType": 4},
{"agSensorName": "Sample Gas Tank Flow", "agSensorType": 6},
{"agSensorName": "Sample Coin Side", "agSensorType": 0},
{"agSensorName": "Sample Gaussian Value", "agSensorType": 8},
]