From 2f6ee21f11329eafacab6d33b2cd6e982f4d8079 Mon Sep 17 00:00:00 2001 From: Marcel Beyer <marcel.beyer@it-maker.eu> Date: Mon, 20 May 2019 21:05:16 +0200 Subject: [PATCH] remote sensors with api --- app.py | 62 ++++++++++++++++++- forms.py | 10 ++- .../versions/453b00e0ca8e_remote_sensors.py | 28 +++++++++ model.py | 34 +++++++++- templates/new_remote_sensor.html | 37 +++++++++++ templates/sensor.html | 17 +++++ 6 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 migrations/versions/453b00e0ca8e_remote_sensors.py create mode 100644 templates/new_remote_sensor.html diff --git a/app.py b/app.py index 52dc6cb..e294563 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,9 @@ import datetime +import hashlib import json from functools import wraps -from flask import Flask, render_template, redirect, url_for, request, flash +from flask import Flask, render_template, redirect, url_for, request, flash, abort from flask_navigation import Navigation from sqlalchemy import and_, or_ @@ -35,8 +36,8 @@ def create_app(): def gen_nav(): nav.Bar('main', [ nav.Item('Dashboard', 'dashboard'), - nav.Item('Sensors', 'index', items=[nav.Item(s.name, 'sensor', {'sensorID': s.id}) for s in get_sensors()]+ - [nav.Item('+ new', 'new_sensor')] + nav.Item('Sensors', 'index', items=[nav.Item(s.name, 'sensor', {'sensorID': s.id}) for s in get_sensors()] + + [nav.Item('+ new', 'new_sensor'), nav.Item('+ new remote', 'new_remote_sensor')] ), nav.Item('Relays', 'index', items=[nav.Item(r.name, 'relay', {'relayID': r.id}) for r in get_relays()] + [nav.Item('+ new', 'new_relay')] @@ -100,6 +101,7 @@ def sensor(sensorID): @app.route('/sensor/<int:sensorID>/delete') @with_navigation def del_sensor(sensorID): + sensor = model.Sensor.query.get(sensorID) model.SensorValue.query.filter_by(sensor_id=sensorID).delete() model.Condition_sensorCompare.query.filter( or_(model.Condition_sensorCompare.sensor1==sensorID, model.Condition_sensorCompare.sensor2==sensorID)).delete() @@ -107,6 +109,10 @@ def del_sensor(sensorID): model.Condition_sensorDiffCompare.sensor2 == sensorID)).delete() model.Condition_valueCompare.query.filter_by(sensor=sensorID).delete() + if sensor.is_remote: + id = sensor.address1w[4:] + model.RemoteSensor.query.filter_by(id=id).delete() + model.Sensor.query.filter_by(id=sensorID).delete() model.db.session.commit() flash('Deletion successfull.') @@ -127,6 +133,22 @@ def new_sensor(): return render_template('new_sensor.html', form=form) +@app.route('/sensor/new_remote', methods=['GET', 'POST']) +@with_navigation +def new_remote_sensor(): + form = forms.NewRemoteSensorForm(request.form) + + if request.method == 'POST' and form.validate(): + rem_sensor = model.RemoteSensor() + model.db.session.add(rem_sensor) + model.db.session.flush() + sensor = model.Sensor('rem-{}'.format(rem_sensor.id), form.name.data) + model.db.session.add(sensor) + model.db.session.commit() + flash('Sensor created successfully.') + + return render_template('new_remote_sensor.html', form=form) + @app.route('/relay/<int:relayID>/', methods=['GET', 'POST']) @with_navigation def relay(relayID): @@ -346,6 +368,40 @@ def new_user(): return render_template('new_user.html', form=form) +@app.route('/api/remotesensor/<int:sensorID>', methods=['GET']) +def api_remote_sensor_new_value(sensorID): + sensor = model.Sensor.query.get(sensorID) + + signature = request.args.get("signature","") + value = request.args.get("value","") + + if sensor.is_remote: + addr = sensor.address1w[4:] + remote_sensor = model.RemoteSensor.query.get(addr) + + # check if value is float + try: + float(value) + except ValueError: + return abort(400) + + # check signature + s = remote_sensor.key + value + s = hashlib.sha256(s.encode()).hexdigest() + + if s != signature: + # signature was wrong + return abort(403) + + # save value + sv = model.SensorValue(sensor, float(value)) + model.db.session.add(sv) + model.db.session.commit() + + return "OK" + else: + return abort(400) + @app.route('/logout/') def logout(): return "logout" diff --git a/forms.py b/forms.py index 58157a0..8efe61e 100644 --- a/forms.py +++ b/forms.py @@ -19,6 +19,14 @@ def validate_address1w(form, field): if sensors is not None: raise ValidationError("Sensor is already configured.") +class NewRemoteSensorForm(FlaskForm): + name = StringField('Name', validators=[DataRequired(message="No name given")]) + + def validate_name(form, field): + sensors = Sensor.query.filter_by(name=field.data).first() + if sensors is not None: + raise ValidationError("Name already in use.") + class NewRelayForm(FlaskForm): name = StringField('Name', validators=[DataRequired(message="No name given")]) port = SelectField('Port', validators=[DataRequired(message="No port specified")], coerce=int) @@ -83,4 +91,4 @@ class CreateUserForm(FlaskForm): def validate_name(form, field): u = User.query.filter_by(username=field.data).first() if u is not None: - raise ValidationError("Username already in use.") \ No newline at end of file + raise ValidationError("Username already in use.") diff --git a/migrations/versions/453b00e0ca8e_remote_sensors.py b/migrations/versions/453b00e0ca8e_remote_sensors.py new file mode 100644 index 0000000..4707cd9 --- /dev/null +++ b/migrations/versions/453b00e0ca8e_remote_sensors.py @@ -0,0 +1,28 @@ +"""remote sensors + +Revision ID: 453b00e0ca8e +Revises: 3cf7480569d1 +Create Date: 2019-05-20 19:37:40.689172 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '453b00e0ca8e' +down_revision = '3cf7480569d1' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table('remotesensor', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('key', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade(): + op.drop_table('remotesensor') diff --git a/model.py b/model.py index f0a9e12..5321e5b 100644 --- a/model.py +++ b/model.py @@ -1,4 +1,6 @@ import datetime +import random +import string from flask_sqlalchemy import SQLAlchemy from sqlalchemy.ext.hybrid import hybrid_property @@ -59,6 +61,15 @@ def check_password(self, password): def __repr__(self): return '<User {}>'.format(self.username) +class RemoteSensor(db.Model): + __tablename__ = 'remotesensor' + id = db.Column(db.Integer, primary_key=True) + key = db.Column(db.String, nullable=False) + + def __init__(self): + # After creating this remote sensor, a new Sensor('rem-{RemoteSensor.id}', '{name}') has to be created + self.key = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(15)) + class Sensor(db.Model): __tablename__ = 'sensor' id = db.Column(db.Integer, primary_key=True) @@ -69,9 +80,28 @@ def __init__(self, address1w, name): self.address1w = address1w self.name = name + @hybrid_property + def is_remote(self): + return self.address1w.startswith('rem-') + + @hybrid_property + def remoteSensor(self): + # returns object of remote sensor, if this sensor is a remote sensor, otherwise None + if self.is_remote: + id = self.address1w[4:] + return RemoteSensor.query.get(id) + else: + return None + @hybrid_property def value(self): - return get_sensor_value(self.address1w) + if self.is_remote is True: + # sensor is a remote sensor, get last known value from db + value = SensorValue.query.filter_by(sensor_id=self.id).order_by(SensorValue.time.desc()).first() + return value.value if value is not None else False + else: + # sensor is connected 1w-sensor + return get_sensor_value(self.address1w) class SensorValue(db.Model): __tablename__ = 'sensorValue' @@ -322,4 +352,4 @@ def __str__(self): @hybrid_property def fulfilled(self): time = datetime.datetime.now().time() - return self.start_time <= time <= self.end_time \ No newline at end of file + return self.start_time <= time <= self.end_time diff --git a/templates/new_remote_sensor.html b/templates/new_remote_sensor.html new file mode 100644 index 0000000..191c7d1 --- /dev/null +++ b/templates/new_remote_sensor.html @@ -0,0 +1,37 @@ +{% extends "_base.html" %} +{% set title='New sensor' %} +{% set heading='Configure new sensor' %} + +{% block breadcumb %} + <ol class="breadcrumb"> + <li>Home</li> + <li>Sensors</li> + <li class="active">New remote</li> + </ol> +{% endblock %} + +{% block content %} + <p> + For remote sensors it is possible to push new temperature values with an HTTP API. + </p> + <form action="" method="post"> + {{ form.csrf_token }} + <div class="row"> + <div class="col-md-2"> + {{ form.name.label }} + {% if form.name.errors %}<br><span class="text-danger">{{ form.name.errors[0] }}</span>{% endif %} + </div> + <div class="col-md-4"> + {{ form.name(class_="form-control") }} + </div> + </div> + <div class="row"> + <div class="col-md-2"> + </div> + <div class="col-md-4"> + <button type="submit" class="btn btn-primary">Create</button> + </div> + </div> + </form> + +{% endblock %} diff --git a/templates/sensor.html b/templates/sensor.html index 7b30d51..155ba93 100644 --- a/templates/sensor.html +++ b/templates/sensor.html @@ -58,6 +58,23 @@ </div> </div> + {% if sensor.is_remote %} + <div class="row"> + <div class="col-md-12"> + <p> + This sensor is a remote sensor. Values are not meassured on this device but received with API calls. + </p> + <p> + <b>API Credentials:</b><br> + ID: <tt>{{ sensor.id }}</tt><br> + Key: <tt>{{ sensor.remoteSensor.key }}</tt><br> + Send HTTP-GET-Requests to: <tt>{{ url_for('api_remote_sensor_new_value',sensorID=sensor.id) }}?value={value}&signature={signature}</tt><br> + where {value} is the current temperature in °C as a float and signature is the SHA256 hash of the value concatenated with the key. + </p> + </div> + </div> + {% endif %} + {% endblock %} {% block extraJS %} <script type="text/javascript">