diff --git a/.flaskenv b/.flaskenv new file mode 100644 index 0000000..862064c --- /dev/null +++ b/.flaskenv @@ -0,0 +1,2 @@ +FLASK_APP=flaskapp.py +FLASK_DEBUG=0 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16c5acf --- /dev/null +++ b/.gitignore @@ -0,0 +1,183 @@ + +# Created by https://www.gitignore.io/api/flask,python,visualstudiocode +# Edit at https://www.gitignore.io/?templates=flask,python,visualstudiocode + +### Flask ### +instance/* +!instance/.gitignore +.webassets-cache + +### Flask.Python Stack ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### Python ### +# Byte-compiled / optimized / DLL files + +# C extensions + +# Distribution / packaging + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. + +# Installer logs + +# Unit test / coverage reports + +# Translations + +# Scrapy stuff: + +# Sphinx documentation + +# PyBuilder + +# pyenv + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. + +# celery beat schedule file + +# SageMath parsed files + +# Spyder project settings + +# Rope project settings + +# Mr Developer + +# mkdocs documentation + +# mypy + +# Pyre type checker + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +# End of https://www.gitignore.io/api/flask,python,visualstudiocode + +src/iotvenv/ + +src/.vscode/ + +.vscode/ + +iotvenv/ + +*.db diff --git a/config.py b/config.py new file mode 100644 index 0000000..0a09d55 --- /dev/null +++ b/config.py @@ -0,0 +1,7 @@ +import os +basedir = os.path.abspath(os.path.dirname(__file__)) + +class Config(object): + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ + 'sqlite:///' + os.path.join(basedir, 'app.db') + SQLALCHEMY_TRACK_MODIFICATIONS = False \ No newline at end of file diff --git a/flaskapp.py b/flaskapp.py new file mode 100644 index 0000000..6e245a7 --- /dev/null +++ b/flaskapp.py @@ -0,0 +1,3 @@ +from flaskapp import app + +app.run(host='0.0.0.0', port='80') diff --git a/flaskapp/__init__.py b/flaskapp/__init__.py new file mode 100644 index 0000000..7ae5369 --- /dev/null +++ b/flaskapp/__init__.py @@ -0,0 +1,14 @@ +from flask import Flask +from config import Config +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_bootstrap import Bootstrap + +app = Flask(__name__) +app.config.from_object(Config) +db = SQLAlchemy(app) +migrate = Migrate(app, db) + +from flaskapp import routes, models, errors + +bootstrap = Bootstrap(app) diff --git a/flaskapp/dangercalc.py b/flaskapp/dangercalc.py new file mode 100644 index 0000000..13d747a --- /dev/null +++ b/flaskapp/dangercalc.py @@ -0,0 +1,13 @@ +from flaskapp.models import DangerLevel + +Critical_Temperature = 105 +Critical_Light = 40 +Critical_Humididty = 80 + +def danger_calculation(baro_pressure,light,humidity,avg_temp): + if avg_temp >= Critical_Temperature: + return DangerLevel.Critical + if light >= Critical_Light: + return DangerLevel.Critical + if humidity >= Critical_Humididty: + return DangerLevel.Critical \ No newline at end of file diff --git a/flaskapp/errors.py b/flaskapp/errors.py new file mode 100644 index 0000000..7ab247b --- /dev/null +++ b/flaskapp/errors.py @@ -0,0 +1,11 @@ +from flask import render_template +from flaskapp import app, db + +@app.errorhandler(404) +def not_found_error(error): + return render_template('404.html'), 404 + +@app.errorhandler(500) +def internal_error(error): + db.session.rollback() + return render_template('500.html'), 500 diff --git a/flaskapp/models.py b/flaskapp/models.py new file mode 100644 index 0000000..2a597e1 --- /dev/null +++ b/flaskapp/models.py @@ -0,0 +1,27 @@ +from flaskapp import db + +class SensorData(db.Model): + id = db.Column(db.Integer, primary_key=True) + humidity = db.Column(db.Float) + avg_temp = db.Column(db.Float) + baro_pressure = db.Column(db.Float) + light = db.Column(db.Float) + timestamp = db.Column(db.DateTime) + danger_level = db.Column(db.Integer) + + def __repr__(self): + return ''.format(self.id) + + @staticmethod + def getAll(): + return SensorData.query.all() + +class DangerLevel(): + Critical = 1 #The server is in a dangerous state, it should be automatically shutdown to prevent damage to components and data loss. + High = 2 #The server is outside of standard operating conditions, and its state should be manually reviewed to prevent data loss. + Medium = 3 #The server getting close to the limit of standard operating conditions and should be kept an eye on, but could also be due to sustained load. + Low = 4 #The server is a little above standard temperatures, however is standard for burst computing. + Nil = 5 #There is no danger level; data is normal. + Unknown = 6 #Some data may be incorrect, and the sensor should be checked. + Tampered = 7 #The server may have been tampered with, such as getting moved or physically accessed. + Other = 8 \ No newline at end of file diff --git a/flaskapp/routes.py b/flaskapp/routes.py new file mode 100644 index 0000000..823ea3c --- /dev/null +++ b/flaskapp/routes.py @@ -0,0 +1,39 @@ +from flaskapp import app, db +from flaskapp.models import SensorData, DangerLevel +from flask import render_template, request, jsonify +import datetime +from flaskapp.dangercalc import danger_calculation + +@app.route("/") +@app.route("/index") +def index(): + data = SensorData.query.order_by(SensorData.timestamp.desc()) + return render_template('index.html', title='Home', data=data) + +@app.route('/', methods=["POST"]) +def newdata(): + baro_temp = round(request.json['baro_temp'], 2) + baro_pressure = round(request.json['baro_pressure'], 2) + light = round(request.json['light'], 2) + humidity_temp = round(request.json['humidity_temp'], 2) + humidity = round(request.json['humidity'], 2) + avg_temp = round((baro_temp + humidity_temp)/2, 2) + timestamp = datetime.datetime.now() + danger_level = danger_calculation(baro_pressure, light, humidity, avg_temp) + + sensordata = SensorData(timestamp=timestamp, baro_pressure=baro_pressure,light=light,humidity=humidity, avg_temp=avg_temp, danger_level=danger_level) + + db.session.add(sensordata) + db.session.commit() + + return jsonify(), 201 + +@app.route("/graph") +def graph(): + line_labels = [''] + + line_values = SensorData.getAll() + line_values.sort(key=lambda x: x.timestamp) + line_values = line_values[-12:] + + return render_template('graph.html', title='Average Temperature Graph', max=40, labels=line_labels, values=line_values) diff --git a/flaskapp/templates/404.html b/flaskapp/templates/404.html new file mode 100644 index 0000000..fede184 --- /dev/null +++ b/flaskapp/templates/404.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + +{% block app_content %} +

Not Found

+

Back

+{% endblock %} \ No newline at end of file diff --git a/flaskapp/templates/500.html b/flaskapp/templates/500.html new file mode 100644 index 0000000..39c9ad3 --- /dev/null +++ b/flaskapp/templates/500.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + +{% block app_content %} +

500 Server Error

+

Back

+{% endblock %} \ No newline at end of file diff --git a/flaskapp/templates/base.html b/flaskapp/templates/base.html new file mode 100644 index 0000000..6c85938 --- /dev/null +++ b/flaskapp/templates/base.html @@ -0,0 +1,34 @@ +{% extends 'bootstrap/base.html' %} + +{% block title %} + {% if title %}{{ title }} - IoT Flask App{% else %}IoT Flask App{% endif %} +{% endblock %} + +{% block navbar %} + +{% endblock %} + +
+ {% block content %} + {% endblock %} +
diff --git a/flaskapp/templates/graph.html b/flaskapp/templates/graph.html new file mode 100644 index 0000000..845cb01 --- /dev/null +++ b/flaskapp/templates/graph.html @@ -0,0 +1,71 @@ + + + + + {{ title }} + + + + +
+

{{ title }}

+ + + +
+ + \ No newline at end of file diff --git a/flaskapp/templates/index.html b/flaskapp/templates/index.html new file mode 100644 index 0000000..9b71545 --- /dev/null +++ b/flaskapp/templates/index.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% block content %} + +

Sensor Data

+ + + + + + + + + + + {% for item in data %} + {% if item.danger_level == 1 %} + + {% else %} + + {% endif %} + + + + + + + {% endfor %} +
+
HumidityAverage TempBarometer PressureLightTimestamp
{{ item.humidity }}%{{ item.avg_temp }}°C{{ item.baro_pressure }} hPa{{ item.light }} lx{{ item.timestamp }}
+ +{% endblock %} \ No newline at end of file diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..f8ed480 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..79b8174 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,96 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from flask import current_app +config.set_main_option( + 'sqlalchemy.url', current_app.config.get( + 'SQLALCHEMY_DATABASE_URI').replace('%', '%%')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/40562f975781_updated_db.py b/migrations/versions/40562f975781_updated_db.py new file mode 100644 index 0000000..c518de9 --- /dev/null +++ b/migrations/versions/40562f975781_updated_db.py @@ -0,0 +1,36 @@ +"""Updated DB + +Revision ID: 40562f975781 +Revises: +Create Date: 2020-05-18 18:52:41.817526 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '40562f975781' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('sensor_data', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('humidity', sa.Float(), nullable=True), + sa.Column('avg_temp', sa.Float(), nullable=True), + sa.Column('baro_pressure', sa.Float(), nullable=True), + sa.Column('light', sa.Float(), nullable=True), + sa.Column('timestamp', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('sensor_data') + # ### end Alembic commands ### diff --git a/migrations/versions/b9900a0f475d_tweaks.py b/migrations/versions/b9900a0f475d_tweaks.py new file mode 100644 index 0000000..3d05c25 --- /dev/null +++ b/migrations/versions/b9900a0f475d_tweaks.py @@ -0,0 +1,28 @@ +"""Tweaks + +Revision ID: b9900a0f475d +Revises: 40562f975781 +Create Date: 2020-05-22 20:02:25.542693 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b9900a0f475d' +down_revision = '40562f975781' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('sensor_data', sa.Column('danger_level', sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('sensor_data', 'danger_level') + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..687c4b7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Flask_Migrate==2.5.2 +Flask_SQLAlchemy==2.4.1 +SQLAlchemy==1.3.13 +Flask==1.1.1 +requests==2.22.0 +alembic==1.3.3 +bluepy==1.3.0 diff --git a/sensor/thermometer.py b/sensor/thermometer.py new file mode 100644 index 0000000..d272706 --- /dev/null +++ b/sensor/thermometer.py @@ -0,0 +1,58 @@ +from bluepy.btle import BTLEException +from bluepy.sensortag import SensorTag +import time +import requests + +SENSOR_ADDRESS = '54:6C:0E:53:12:D5' +API = "http://192.168.0.21:5000" + +INTERVAL = 5 + +tag = SensorTag(SENSOR_ADDRESS) +tag.connect(tag.deviceAddr, tag.addrType) + +print("Connected to sensor") + +def enable_sensors(tag): + + tag.barometer.enable() + tag.IRtemperature.enable() + tag.humidity.enable() + tag.lightmeter.enable() + time.sleep(1) + +def disable_sensors(tag): + tag.barometer.disable() + tag.IRtemperature.disable() + tag.humidity.disable() + tag.lightmeter.disable() + +def get_readings(tag): + try: + enable_sensors(tag) + readings = {} + readings["baro_temp"],readings["pressure"]=tag.barometer.read() + readings["light"]=tag.lightmeter.read() + readings["humidity_temp"],readings["humidity"] = tag.humidity.read() + disable_sensors(tag) + return readings + except BTLEException as e: + print(e) + return {} + +while(True): + time.sleep(INTERVAL) + readings = get_readings(tag) + + # POST + data = { + 'baro_temp':readings["baro_temp"], + 'baro_pressure':readings["pressure"], + 'light':readings["light"], + 'humidity_temp':readings['humidity_temp'], + 'humidity':readings['humidity'] + } + + r = requests.post(url=API, json=data) + + print(r.status_code)