Commit 3e6d6092 authored by Federico Sismondi's avatar Federico Sismondi
Browse files

Merge branch 'moisture_battery_monitoring' into 'master'

Moisture battery monitoring

See merge request !19
parents dcc0eb0d 95c5f542
# IoT Platform Overview
This includes the NAIADES IoT Platform source code, shipping code (Dockerfiles and docker-compose), and scripts examples
demonstrating how clients can use IoT Platform services (a.k.a NAIADES SDK).
SDK shows how to use NAIADES IoT Platform services, among these services we have:
- message broker (`Orion`)
This project includes the NAIADES IoT Platform source code and DCA components.
It provides the sources for:
- declarative services files (Dockerfiles and docker-compose)
- platform management scripts (/scripts-admin)scripts
- examples demonstrating how clients can use IoT Platform services (/scripts)
and more.
Among the IoT platform services we find the following key components:
- context manager (`Orion`)
- access control (`Wilma`)
- identity management (`Keyrock`)
- time-series storage and API (`QuantumLeap`)
- how to build your own services (e.g. `weather-data-collection-carouge`)
- time-series storage (`QuantumLeap`)
- examples for data consumers (e.g. `wms-example`)
This is inspired from [FIWARE tutorial](https://github.com/Fiware/tutorials.PEP-Proxy/blob/master/README.md)
......@@ -31,7 +35,7 @@ Please see [wiki article](https://gitlab.distantaccess.com/naiades/naiades-platf
# Use Case Example
Using the `Carouge Watering` use case for exemplifying the interactions.
We have `IoT sensor for humidity`, and `WMS for watering scheduling`, then we could think of the following interactions:
We have `IoT sensor for soil moisture`, and `WMS for watering scheduling`, then we could think of the following interactions:
## Interfaces:
......
......@@ -145,12 +145,16 @@ def _log_level_of_moisture(cb, sensor):
app.logger.error("Measurement not in expected range: {} cb".format(cb))
def _log_level_of_battery_level(bat, sensor):
if bat < 0.5:
app.logger.warning("{} battery level is critically low: {} %!".format(sensor, bat))
else:
app.logger.info("{} battery level is: {} %".format(sensor, bat))
def _conv_micro_volts_to_moiture_cb(micro_volts):
"""
Lora Bridge device has to channels (A and B).
Only the channel A is connected. To read correctly the value from the channel A, you should take the byte 3 to 5.
Then, you should decoded from Hex to Decimal. IMPORTANT: The value is expressed in µV. (1V = 1.000.000µV)
Sensor Voltage range is from 0V to 3V.
This voltage range is linearly dependent to the real value of the sensor, which is expressed in cb (kPa):
......@@ -159,6 +163,7 @@ def _conv_micro_volts_to_moiture_cb(micro_volts):
:param micro_volts:
:return: pressure (in cb ~ KPa)
"""
assert type(micro_volts) is int
......@@ -168,19 +173,63 @@ def _conv_micro_volts_to_moiture_cb(micro_volts):
return int((micro_volts * 239) / 3000000)
def _get_reading(message_data):
def _conv_micro_volts_to_battery_level(micro_volts):
"""Battery is 0-12v
but is connected with a Voltage divider R1=R2= 5 K ohm
https://en.wikipedia.org/wiki/Voltage_divider
- measured voltage is half on the real value
- measured voltage is in micro volts
hence:
bat_percetage = 100 * (v_input /1.000.000 ) * 2 / 12
:param micro_volts:
:return: battery percentage 0-100%
"""
>>> base64.standard_b64decode("QmABACcnAQAAAA==").hex()
'42600100272701000000'
assert type(micro_volts) is int
if micro_volts == 0:
return 0
return 2 * (micro_volts / 1000000) / 12
def _get_reading(message_data):
""" Lora Bridge device has to channels (A and B).
channel A is connected to moisture sensors
channel B is connected to battery voltage output
To read correctly the value from the channel A, you should take the byte 3 to 5 (included).
To read correctly the value from the channel B, you should take the byte 6 to 8 (included).
Then, you should decoded from Hex to Decimal.
IMPORTANT: The value is expressed in µV if sensor is configured as voltage input (1V = 1.000.000µV)
For more info look at LoRaWAN 863-870 ANALOG PWR - Guide utilisateur / User guide version V2.0.2
>>> base64.standard_b64decode("QsABAqD7AVL5yQ==").hex()
'42c00102a0fb0152f9c9'
# deplexing data:
# 42 -> it's a data frame!
# c0 ->
# 01 -> configured as 0-10v input for channel A
# 02a0fb -> data channel A
# 01 -> configured as 0-10v input for channel A
# 52f9c9 -> data channel B
:param message_data:
:return:
:return: moisture, battery_level
"""
reading_hex = base64.standard_b64decode(message_data).hex()
if reading_hex[0:2] == '42': # it's a dataFrame
return _conv_micro_volts_to_moiture_cb(int(reading_hex[6:12], 16))
moisture = _conv_micro_volts_to_moiture_cb(int(reading_hex[6:12], 16))
battery_level = _conv_micro_volts_to_battery_level(int(reading_hex[14:20], 16))
return moisture, battery_level
else:
msg = 'Not a sensor reading (expected 0x42) {}'.format(reading_hex)
......@@ -203,20 +252,21 @@ def _update_flowerbed_entity(soil_moisture, sensor_eui):
if not flowerbed_id:
msg = 'No FlowerBed associated to sensor {}'.format(sensor_eui)
app.logger.warning(msg)
app.logger.error(msg)
return msg
app.logger.debug('Found FlowerBed {} associated to sensor {}'.format(flowerbed_id, sensor_eui))
body = {"soilMoisture": get_ngsiv2_typed_description(soil_moisture)}
url = "{}/v2/entities/{}/attrs".format(URL_BASE, flowerbed_id)
r = client_request.patch(
url=url,
headers=http_header_post,
json={
"soilMoisture": get_ngsiv2_typed_description(soil_moisture)
}
json=body
)
app.logger.debug('Sending data to CTX broker: {}'.format(body))
if not (200 <= r.status_code < 300):
app.logger.error('HTTP status code not 2xx, something went wrong..')
app.logger.error(r.status_code)
......@@ -224,7 +274,7 @@ def _update_flowerbed_entity(soil_moisture, sensor_eui):
app.logger.error(r.json())
return "Got error {}".format(r.reason)
else:
app.logger.info('HTTP response is {} - Entity updated'.format(r.status_code))
app.logger.info('Entity updated (code {}), url {}'.format(r.status_code, url))
return "ok"
......@@ -245,6 +295,7 @@ def _update_device_entity(deveui, comment, longitude, latitude, last_reception,
-value
-location
-rssi
-batteryLevel
NOTES:
name is regexed from LoRa platform <comment> field of device,
......@@ -299,7 +350,9 @@ def _update_device_entity(deveui, comment, longitude, latitude, last_reception,
app.logger.warning("No date parsed for updaging Device field: dateLastValueReported")
if dataFrame:
body.update({'value': get_ngsiv2_typed_description(_get_reading(dataFrame))})
moist, bat = _get_reading(dataFrame)
body.update({'value': get_ngsiv2_typed_description(moist)})
body.update({'batteryLevel': get_ngsiv2_typed_description(bat)})
if dataFrame_rssi:
body.update({'rssi': get_ngsiv2_typed_description(dataFrame_rssi)})
......@@ -320,7 +373,7 @@ def _update_device_entity(deveui, comment, longitude, latitude, last_reception,
app.logger.error(r.json())
return "Got error {}".format(r.reason)
else:
app.logger.info('HTTP response is {} - Entity updated'.format(r.status_code))
app.logger.info('Entity updated (code {}), url {}'.format(r.status_code, url))
return "ok"
......@@ -329,6 +382,7 @@ def _dump_raw_data_to_filesystem():
app.logger.info('dumping request to FS')
try:
filepath = os.path.join(COLLECTION_DIR, '{}.json'.format(int(time.time())))
with open(filepath, encoding='utf-8', mode='w') as f:
json.dump(
obj=request.json,
......@@ -339,6 +393,7 @@ def _dump_raw_data_to_filesystem():
app.logger.error(e)
return 'error'
app.logger.info("Dumped raw data at file {}".format(filepath))
return 'ok'
......@@ -365,10 +420,13 @@ def post_dca_carouge_watering_sensed_data_test():
@app.route('/dca-carouge-watering-sensed-data/rest/callback/payloads/ul', methods=['POST'])
def post_dca_carouge_watering_sensed_data_payload():
# get values
soil_humidity, sensor_eui = _get_reading(request.json['dataFrame']), request.json['deveui']
soil_humidity, battery_level = _get_reading(request.json['dataFrame'])
sensor_eui = request.json['deveui']
# log reading
_log_level_of_moisture(soil_humidity, sensor_eui)
_log_level_of_battery_level(battery_level,sensor_eui)
# saves locally and pushed data to FlowerBed and Device entities
resp = {}
......
......@@ -110,46 +110,45 @@ attrs = {
'phThresholdLow': 0.0,
'chlorineThresholdHigh': 0.0,
'chlorineThresholdLow': 0.0,
'refDevice': None,
}
}
# entities ids per pilot tuples
entities = [
# prediction use case - weather observed (current)
# weather prediction use case - weather observed (current)
('urn:ngsi-ld:WeatherObserved:WeatherObserved', ['carouge', 'braila']),
#('urn:ngsi-ld:WeatherObserved:WeatherObserved-1', ['alicante']),
#('urn:ngsi-ld:WeatherObserved:WeatherObserved-2', ['alicante']),
('urn:ngsi-ld:WeatherObserved:WeatherObserved-1', ['alicante']),
('urn:ngsi-ld:WeatherObserved:WeatherObserved-2', ['alicante']),
# forecast today
('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day0-0', ['carouge', 'alicante', 'braila']),
('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day0-1', ['carouge', 'alicante', 'braila']),
('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day0-2', ['carouge', 'alicante', 'braila']),
('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day0-3', ['carouge', 'alicante', 'braila']),
# forecast tomorrow
('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day1-0', ['carouge', 'alicante', 'braila']),
('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day1-1', ['carouge', 'alicante', 'braila']),
('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day1-2', ['carouge', 'alicante', 'braila']),
('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day1-3', ['carouge', 'alicante', 'braila']),
# forecast tomorrow
('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day2-0', ['carouge', 'alicante', 'braila']),
('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day2-1', ['carouge', 'alicante', 'braila']),
('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day2-2', ['carouge', 'alicante', 'braila']),
('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day2-3', ['carouge', 'alicante', 'braila']),
# watering use case - flower beds
('urn:ngsi-ld:FlowerBed:FlowerBed-1', ['carouge']),
('urn:ngsi-ld:FlowerBed:FlowerBed-2', ['carouge']),
('urn:ngsi-ld:FlowerBed:FlowerBed-3', ['carouge']),
('urn:ngsi-ld:FlowerBed:FlowerBed-4', ['carouge']),
('urn:ngsi-ld:FlowerBed:FlowerBed-5', ['carouge']),
('urn:ngsi-ld:FlowerBed:FlowerBed-6', ['carouge']),
('urn:ngsi-ld:FlowerBed:FlowerBed-7', ['carouge']),
('urn:ngsi-ld:FlowerBed:FlowerBed-8', ['carouge']),
# water quality use cases
("urn:ngsi-ld:WaterQualityObserved:Fountain-1", ['carouge']),
("urn:ngsi-ld:WaterQualityObserved:WaterTreatmentPlant", ['wtp_lab']),
# # prediction use case - forecast today
# ('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day0-0', ['carouge', 'alicante', 'braila']),
# ('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day0-1', ['carouge', 'alicante', 'braila']),
# ('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day0-2', ['carouge', 'alicante', 'braila']),
# ('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day0-3', ['carouge', 'alicante', 'braila']),
# #
# # # prediction use case - forecast tomorrow
# ('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day1-0', ['carouge', 'alicante', 'braila']),
# ('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day1-1', ['carouge', 'alicante', 'braila']),
# ('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day1-2', ['carouge', 'alicante', 'braila']),
# ('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day1-3', ['carouge', 'alicante', 'braila']),
# #
# # # prediction use case - forecast tomorrow
# ('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day2-0', ['carouge', 'alicante', 'braila']),
# ('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day2-1', ['carouge', 'alicante', 'braila']),
# ('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day2-2', ['carouge', 'alicante', 'braila']),
# ('urn:ngsi-ld:WeatherForecast:WeatherForecast-Day2-3', ['carouge', 'alicante', 'braila']),
#
# # watering use case - flower beds
# ('urn:ngsi-ld:FlowerBed:FlowerBed-1', ['carouge']),
# ('urn:ngsi-ld:FlowerBed:FlowerBed-2', ['carouge']),
# ('urn:ngsi-ld:FlowerBed:FlowerBed-3', ['carouge']),
# ('urn:ngsi-ld:FlowerBed:FlowerBed-4', ['carouge']),
# ('urn:ngsi-ld:FlowerBed:FlowerBed-5', ['carouge']),
# ('urn:ngsi-ld:FlowerBed:FlowerBed-6', ['carouge']),
# ('urn:ngsi-ld:FlowerBed:FlowerBed-7', ['carouge']),
# ('urn:ngsi-ld:FlowerBed:FlowerBed-8', ['carouge']),
]
# http headers
......@@ -161,7 +160,8 @@ headers = {
entities_map = {
'carouge': [],
'braila': [],
'alicante': []
'alicante': [],
'wtp_lab': [],
}
for urn, pilots in entities:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment