remove state capabilities
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			This commit is contained in:
		@@ -2,12 +2,5 @@ sentry_sdk[flask]
 | 
			
		||||
gunicorn
 | 
			
		||||
Flask
 | 
			
		||||
Flask-RESTful
 | 
			
		||||
sqlalchemy
 | 
			
		||||
flask_sqlalchemy
 | 
			
		||||
marshmallow
 | 
			
		||||
marshmallow-sqlalchemy
 | 
			
		||||
flask-marshmallow
 | 
			
		||||
paho-mqtt
 | 
			
		||||
flask-mqtt
 | 
			
		||||
psycopg2-binary
 | 
			
		||||
py-healthcheck
 | 
			
		||||
							
								
								
									
										22
									
								
								src/app.py
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								src/app.py
									
									
									
									
									
								
							@@ -1,21 +1,17 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
import logging
 | 
			
		||||
from flask import Flask
 | 
			
		||||
from flask_mqtt import logger
 | 
			
		||||
from flask_restful import Api
 | 
			
		||||
import sentry_sdk
 | 
			
		||||
from sentry_sdk.integrations.flask import FlaskIntegration
 | 
			
		||||
from sentry_sdk.integrations.logging import LoggingIntegration
 | 
			
		||||
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
 | 
			
		||||
from healthcheck import HealthCheck
 | 
			
		||||
 | 
			
		||||
from resources import *
 | 
			
		||||
from config import *
 | 
			
		||||
from db import db
 | 
			
		||||
from marshm import ma
 | 
			
		||||
from mqtt_flask_instance import mqtt
 | 
			
		||||
 | 
			
		||||
from mqtt_methods import handle_status_message
 | 
			
		||||
from healthchecks import health_database_status
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Main Flask RESTful API
 | 
			
		||||
@@ -33,7 +29,7 @@ if SENTRY_DSN:
 | 
			
		||||
    )
 | 
			
		||||
    sentry_sdk.init(
 | 
			
		||||
        dsn=SENTRY_DSN,
 | 
			
		||||
        integrations=[FlaskIntegration(), sentry_logging, SqlalchemyIntegration()],
 | 
			
		||||
        integrations=[FlaskIntegration(), sentry_logging],
 | 
			
		||||
        traces_sample_rate=1.0,
 | 
			
		||||
        send_default_pii=True,
 | 
			
		||||
        release=RELEASE_ID,
 | 
			
		||||
@@ -48,18 +44,10 @@ app.config['MQTT_BROKER_PORT'] = MQTT_PORT
 | 
			
		||||
app.config['MQTT_USERNAME'] = MQTT_USERNAME
 | 
			
		||||
app.config['MQTT_PASSWORD'] = MQTT_PASSWORD
 | 
			
		||||
app.config['MQTT_REFRESH_TIME'] = 1.0  # refresh time in seconds
 | 
			
		||||
app.config['SQLALCHEMY_DATABASE_URI'] = \
 | 
			
		||||
    f"postgresql://{POSTGRES_USERNAME}:{POSTGRES_PASSWORD}@{POSTGRES_HOSTNAME}:5432/{POSTGRES_DB}?sslmode=require"
 | 
			
		||||
 | 
			
		||||
api = Api(app)
 | 
			
		||||
db.init_app(app)
 | 
			
		||||
ma.init_app(app)
 | 
			
		||||
mqtt.init_app(app)
 | 
			
		||||
health = HealthCheck()
 | 
			
		||||
 | 
			
		||||
with app.app_context():
 | 
			
		||||
    db.create_all()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@mqtt.on_log()
 | 
			
		||||
def handle_logging(client, userdata, level, buf):
 | 
			
		||||
@@ -80,15 +68,9 @@ def handle_status_message_proxy(*args, **kwargs):
 | 
			
		||||
        handle_status_message(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
api.add_resource(AllDevicesResource, "/devices")
 | 
			
		||||
api.add_resource(AllDevicesOfflineResource, "/devices/offline")
 | 
			
		||||
api.add_resource(AllDevicesOnlineResource, "/devices/online")
 | 
			
		||||
api.add_resource(DeviceResource, "/devices/{deviceid}")
 | 
			
		||||
api.add_resource(DeviceOfflineResrouce, "/devices/{deviceid}/offline")
 | 
			
		||||
api.add_resource(DeviceOnlineResrouce, "/devices/{deviceid}/online")
 | 
			
		||||
api.add_resource(SensorResource, "/devices/{deviceid}/{sensorid}")
 | 
			
		||||
api.add_resource(SensorOfflineResource, "/devices/{deviceid}/{sensorid}/offline")
 | 
			
		||||
api.add_resource(SensorOnlineResource, "/devices/{deviceid}/{sensorid}/online")
 | 
			
		||||
 | 
			
		||||
health.add_check(health_database_status)
 | 
			
		||||
app.add_url_rule("/healthz", "healthcheck", view_func=lambda: health.run())
 | 
			
		||||
 
 | 
			
		||||
@@ -20,11 +20,6 @@ SENTRY_DSN = os.environ.get("SENTRY_DSN")
 | 
			
		||||
RELEASE_ID = os.environ.get("RELEASE_ID", "test")
 | 
			
		||||
RELEASEMODE = os.environ.get("CNC_SERVICE_RELEASEMODE", "dev")
 | 
			
		||||
 | 
			
		||||
POSTGRES_HOSTNAME = os.getenv("CNC_POSTGRES_HOSTNAME", "localhost")
 | 
			
		||||
POSTGRES_USERNAME = os.getenv("CNC_POSTGRES_USERNAME", "cnc-service")
 | 
			
		||||
POSTGRES_PASSWORD = os.getenv("CNC_POSTGRES_PASSWORD", "cnc-service")
 | 
			
		||||
POSTGRES_DB = os.getenv("CNC_POSTGRES_DB", "cnc-service")
 | 
			
		||||
 | 
			
		||||
MQTT_HOSTNAME = os.getenv("CNC_MQTT_HOSTNAME", "localhost")
 | 
			
		||||
MQTT_PORT = os.getenv("CNC_MQTT_PORT", "1883")
 | 
			
		||||
MQTT_USERNAME = os.getenv("CNC_MQTT_USERNAME", "cnc-service")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								src/db.py
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/db.py
									
									
									
									
									
								
							@@ -1,13 +0,0 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
from flask_sqlalchemy import SQLAlchemy
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
SQLAlchemy definition
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
__author__ = '@tormakris'
 | 
			
		||||
__copyright__ = "Copyright 2020, Birbnetes Team"
 | 
			
		||||
__module_name__ = "db"
 | 
			
		||||
__version__text__ = "1"
 | 
			
		||||
 | 
			
		||||
db = SQLAlchemy()
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
 | 
			
		||||
from db import db
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Healthchek functions
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
__author__ = "@tormakris"
 | 
			
		||||
__copyright__ = "Copyright 2020, Birbnetes Team"
 | 
			
		||||
__module_name__ = "healthchecks"
 | 
			
		||||
__version__text__ = "1"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def health_database_status():
 | 
			
		||||
    is_database_working = True
 | 
			
		||||
    output = 'database is ok'
 | 
			
		||||
    try:
 | 
			
		||||
        db.session.execute('SELECT 1')
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        output = str(e)
 | 
			
		||||
        is_database_working = False
 | 
			
		||||
    return is_database_working, output
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
 | 
			
		||||
from flask_marshmallow import Marshmallow
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Marshmallow definition
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
__author__ = '@tormakris'
 | 
			
		||||
__copyright__ = "Copyright 2020, Birbnetes Team"
 | 
			
		||||
__module_name__ = "marshm"
 | 
			
		||||
__version__text__ = "1"
 | 
			
		||||
 | 
			
		||||
ma = Marshmallow()
 | 
			
		||||
@@ -1,43 +0,0 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
import enum
 | 
			
		||||
from sqlalchemy.sql import func
 | 
			
		||||
from db import db
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
SQLAlchemy models
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
__author__ = '@tormakris'
 | 
			
		||||
__copyright__ = "Copyright 2020, Birbnetes Team"
 | 
			
		||||
__module_name__ = "models"
 | 
			
		||||
__version__text__ = "1"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeviceStatusEnum(enum.Enum):
 | 
			
		||||
    error = "error"
 | 
			
		||||
    online = "online"
 | 
			
		||||
    offline = "offline"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SensorStatusEnum(enum.Enum):
 | 
			
		||||
    unknown = "unknown"
 | 
			
		||||
    online = "online"
 | 
			
		||||
    offline = "offline"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Device(db.Model):
 | 
			
		||||
    __tablename__ = 'device'
 | 
			
		||||
    id = db.Column(db.Integer, primary_key=True)
 | 
			
		||||
    deviceid = db.Column(db.String, nullable=False)
 | 
			
		||||
    status = db.Column(db.Enum(DeviceStatusEnum), nullable=False)
 | 
			
		||||
    url = db.Column(db.String, nullable=False)
 | 
			
		||||
    lastupdate = db.Column(db.TIMESTAMP, nullable=False, server_default=func.now(), onupdate=func.current_timestamp())
 | 
			
		||||
    sensors = db.relationship("Sensor")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Sensor(db.Model):
 | 
			
		||||
    __tablename__ = 'sensor'
 | 
			
		||||
    id = db.Column(db.Integer, primary_key=True)
 | 
			
		||||
    sensorid = db.Column(db.String, nullable=False)
 | 
			
		||||
    status = db.Column(db.Enum(SensorStatusEnum), nullable=False)
 | 
			
		||||
    device_id = db.Column(db.Integer, db.ForeignKey('device.id'))
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
from flask import current_app as app
 | 
			
		||||
import config
 | 
			
		||||
from db import db
 | 
			
		||||
from schemas import DeviceSchema, SensorSchema
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
MQTT Stuff
 | 
			
		||||
@@ -13,9 +11,6 @@ __copyright__ = "Copyright 2020, Birbnetes Team"
 | 
			
		||||
__module_name__ = "mqtt_methods"
 | 
			
		||||
__version__text__ = "1"
 | 
			
		||||
 | 
			
		||||
deviceschema = DeviceSchema(many=False)
 | 
			
		||||
sensorschema = SensorSchema(many=False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def handle_status_message(client, userdata, message):
 | 
			
		||||
    data = dict(
 | 
			
		||||
@@ -27,18 +22,11 @@ def handle_status_message(client, userdata, message):
 | 
			
		||||
        ids = data['topic'].replace(f"{config.MQTT_STATUS_TOPIC}/", "").split("/")
 | 
			
		||||
        if len(ids) == 1:
 | 
			
		||||
            data['payload']['deviceID'] = ids[0]
 | 
			
		||||
            status_message = deviceschema.load(data['payload'], session=db.session, transient=True).data
 | 
			
		||||
            app.logger.info(f"Recieved status message from {data['payload']['deviceID']}, persisting to db.")
 | 
			
		||||
            db.session.merge(status_message)
 | 
			
		||||
        else:
 | 
			
		||||
            if len(ids) == 2:
 | 
			
		||||
                data['payload']['deviceID'] = ids[0]
 | 
			
		||||
                data['payload']['sensorID'] = ids[1]
 | 
			
		||||
                status_message = sensorschema.load(data['payload'], session=db.session, transient=True).data
 | 
			
		||||
                app.logger.info(f"Recieved status message from sensor {data['payload']['sensorID']}, persisting to db.")
 | 
			
		||||
                db.session.merge(status_message)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        db.session.rollback()
 | 
			
		||||
        app.logger.exception(e)
 | 
			
		||||
    else:
 | 
			
		||||
        db.session.commit()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,7 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
from flask_restful import Resource
 | 
			
		||||
from db import db
 | 
			
		||||
from mqtt_flask_instance import mqtt
 | 
			
		||||
import config
 | 
			
		||||
import models
 | 
			
		||||
import schemas
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Flask Restful endpoints
 | 
			
		||||
@@ -16,21 +13,6 @@ __module_name__ = "resources"
 | 
			
		||||
__version__text__ = "1"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AllDevicesResource(Resource):
 | 
			
		||||
    """
 | 
			
		||||
    Query all known devices
 | 
			
		||||
    """
 | 
			
		||||
    alldeviceschema = schemas.DeviceSchema(many=True)
 | 
			
		||||
 | 
			
		||||
    def get(self):
 | 
			
		||||
        """
 | 
			
		||||
        Get all stored items
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        alldevices = models.Device.query.all()
 | 
			
		||||
        return self.alldeviceschema.dump(list(alldevices)), 200
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AllDevicesOfflineResource(Resource):
 | 
			
		||||
    """
 | 
			
		||||
    Shut down all devices
 | 
			
		||||
@@ -55,22 +37,6 @@ class AllDevicesOnlineResource(Resource):
 | 
			
		||||
        mqtt.publish(f"{config.MQTT_COMMAND_TOPIC}/+", {"command": "online"})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeviceResource(Resource):
 | 
			
		||||
    """
 | 
			
		||||
    Query and control a particular device
 | 
			
		||||
    """
 | 
			
		||||
    deviceschema = schemas.DeviceSchema(many=False)
 | 
			
		||||
 | 
			
		||||
    def get(self, deviceid: str):
 | 
			
		||||
        """
 | 
			
		||||
        Query a device
 | 
			
		||||
        :param deviceid: UUID of device
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        device = models.Device.query.filter_by(id=deviceid).first_or_404()
 | 
			
		||||
        return self.deviceschema.dump(device), 200
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeviceOfflineResrouce(Resource):
 | 
			
		||||
    """
 | 
			
		||||
    Bring a device offline
 | 
			
		||||
@@ -78,11 +44,10 @@ class DeviceOfflineResrouce(Resource):
 | 
			
		||||
    def post(self, deviceid: str):
 | 
			
		||||
        """
 | 
			
		||||
        Shut down a device
 | 
			
		||||
        :param deviceid: UUID of device
 | 
			
		||||
        :param deviceid: ID of device
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        device = db.session.query(models.Device.id).filter(str(models.Device.id) == deviceid).first_or_404()[0]
 | 
			
		||||
        mqtt.publish(f"{config.MQTT_COMMAND_TOPIC}/{device}", {"command": "offline"})
 | 
			
		||||
        mqtt.publish(f"{config.MQTT_COMMAND_TOPIC}/{deviceid}", {"command": "offline"})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeviceOnlineResrouce(Resource):
 | 
			
		||||
@@ -92,28 +57,10 @@ class DeviceOnlineResrouce(Resource):
 | 
			
		||||
    def post(self, deviceid: str):
 | 
			
		||||
        """
 | 
			
		||||
        Bring a device online
 | 
			
		||||
        :param deviceid: UUID of device
 | 
			
		||||
        :param deviceid: ID of device
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        device = db.session.query(models.Device.id).filter(str(models.Device.id) == deviceid).first_or_404()[0]
 | 
			
		||||
        mqtt.publish(f"{config.MQTT_COMMAND_TOPIC}/{device}", {"command": "online"})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SensorResource(Resource):
 | 
			
		||||
    """
 | 
			
		||||
    Query and control a particular sensor of a device
 | 
			
		||||
    """
 | 
			
		||||
    sensorschema = schemas.SensorSchema(many=False)
 | 
			
		||||
 | 
			
		||||
    def get(self, deviceid: str, sensorid: str):
 | 
			
		||||
        """
 | 
			
		||||
        Query a sensor
 | 
			
		||||
        :param deviceid: UUID of device
 | 
			
		||||
        :param sensorid: UUID of sensor
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        sensor = models.Sensor.query.filter_by(device_id=deviceid, sensorid=sensorid).first_or_404()
 | 
			
		||||
        return self.sensorschema.dump(sensor)
 | 
			
		||||
        mqtt.publish(f"{config.MQTT_COMMAND_TOPIC}/{deviceid}", {"command": "online"})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SensorOfflineResource(Resource):
 | 
			
		||||
@@ -123,13 +70,11 @@ class SensorOfflineResource(Resource):
 | 
			
		||||
    def post(self, deviceid: str, sensorid: str):
 | 
			
		||||
        """
 | 
			
		||||
        Shut down a sensor of a device
 | 
			
		||||
        :param deviceid: UUID of device
 | 
			
		||||
        :param sensorid: UUID of sensor
 | 
			
		||||
        :param deviceid: ID of device
 | 
			
		||||
        :param sensorid: ID of sensor
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        sensor = db.session.query(models.Sensor.device_id, models.Sensor.id).filter(
 | 
			
		||||
            str(models.Sensor.device_id) == deviceid and str(models.Sensor.id) == sensorid).first_or_404()
 | 
			
		||||
        mqtt.publish(f"{config.MQTT_COMMAND_TOPIC}/{sensor[0]}/{sensor[1]}", {"command": "offline"})
 | 
			
		||||
        mqtt.publish(f"{config.MQTT_COMMAND_TOPIC}/{deviceid}/{sensorid}", {"command": "offline"})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SensorOnlineResource(Resource):
 | 
			
		||||
@@ -139,10 +84,8 @@ class SensorOnlineResource(Resource):
 | 
			
		||||
    def post(self, deviceid: str, sensorid: str):
 | 
			
		||||
        """
 | 
			
		||||
        Bring a sensor online
 | 
			
		||||
        :param deviceid: UUID of device
 | 
			
		||||
        :param sensorid: UUID of sensor
 | 
			
		||||
        :param deviceid: ID of device
 | 
			
		||||
        :param sensorid: ID of sensor
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        sensor = db.session.query(models.Sensor.device_id, models.Sensor.id).filter(
 | 
			
		||||
            str(models.Sensor.device_id) == deviceid and str(models.Sensor.id) == sensorid).first_or_404()
 | 
			
		||||
        mqtt.publish(f"{config.MQTT_COMMAND_TOPIC}/{sensor[0]}/{sensor[1]}", {"command": "online"})
 | 
			
		||||
        mqtt.publish(f"{config.MQTT_COMMAND_TOPIC}/{deviceid}/{sensorid}", {"command": "online"})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
import models
 | 
			
		||||
from marshm import ma
 | 
			
		||||
from marshmallow import fields
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Schemas of SQLAlchemy objects
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__author__ = "@tormakris"
 | 
			
		||||
__copyright__ = "Copyright 2020, Birbnetes Team"
 | 
			
		||||
__module_name__ = "schemas"
 | 
			
		||||
__version__text__ = "1"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SensorSchema(ma.SQLAlchemyAutoSchema):
 | 
			
		||||
    """
 | 
			
		||||
    Sensor schema autogenerated
 | 
			
		||||
    """
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = models.Sensor
 | 
			
		||||
        include_fk = True
 | 
			
		||||
        exclude = ('id',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeviceSchema(ma.SQLAlchemyAutoSchema):
 | 
			
		||||
    """
 | 
			
		||||
    Device schema autogenerated
 | 
			
		||||
    """
 | 
			
		||||
    sensors = fields.Nested(SensorSchema, many=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = models.Device
 | 
			
		||||
        include_relationships = True
 | 
			
		||||
        exclude = ('lastupdate',)
 | 
			
		||||
		Reference in New Issue
	
	Block a user