From e22da36b431c7cb73ca3b9470bb167a8f5ccd2ac Mon Sep 17 00:00:00 2001 From: Kyle Gabriel Date: Mon, 8 Mar 2021 11:57:24 -0500 Subject: [PATCH] Add Functions to API, add missing Input Channels to Input API calls --- CHANGELOG.md | 2 + mycodo/databases/models/controller.py | 5 + mycodo/mycodo_flask/api/__init__.py | 2 + mycodo/mycodo_flask/api/function.py | 103 +++++++++++++++++++ mycodo/mycodo_flask/api/input.py | 24 ++++- mycodo/mycodo_flask/api/output.py | 12 +-- mycodo/mycodo_flask/api/sql_schema_fields.py | 28 +++++ 7 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 mycodo/mycodo_flask/api/function.py diff --git a/CHANGELOG.md b/CHANGELOG.md index bead216f2..a69b6778a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,8 @@ The new weather input acquires current and future weather conditions from openwe - Add user_scripts directory for user code that's preserved during upgrade/export/import ([#930](https://github.com/kizniche/mycodo/issues/930)) - Add pin mode option (float, pull-up, pull-down) for Edge and State Inputs - Add Method: Cascaded Method, allows combining (multiply) any number of existing methods ([discussion](https://kylegabriel.com/forum/general-discussion/refactor-method-implementation-to-enable-further-methods/)) + - Add Functions and to API + - Add missing Input Channels to Input API calls ### Miscellaneous diff --git a/mycodo/databases/models/controller.py b/mycodo/databases/models/controller.py index c74093d1e..fae476596 100644 --- a/mycodo/databases/models/controller.py +++ b/mycodo/databases/models/controller.py @@ -31,6 +31,11 @@ def __repr__(self): return "<{cls}(id={s.id})>".format(s=self, cls=self.__class__.__name__) +class FunctionSchema(ModelSchema): + class Meta: + model = CustomController + + class FunctionChannel(CRUDMixin, db.Model): __tablename__ = "function_channel" __table_args__ = {'extend_existing': True} diff --git a/mycodo/mycodo_flask/api/__init__.py b/mycodo/mycodo_flask/api/__init__.py index 53e63b7b3..dc8bfc3cc 100644 --- a/mycodo/mycodo_flask/api/__init__.py +++ b/mycodo/mycodo_flask/api/__init__.py @@ -40,6 +40,7 @@ if 'application/json' in api.representations: del api.representations['application/json'] + # Add API v1 + json accept content type @api.representation('application/vnd.mycodo.v1+json') def api_v1(data, code, headers): @@ -61,6 +62,7 @@ def init_api(app): import mycodo.mycodo_flask.api.choices import mycodo.mycodo_flask.api.controller import mycodo.mycodo_flask.api.daemon + import mycodo.mycodo_flask.api.function import mycodo.mycodo_flask.api.input import mycodo.mycodo_flask.api.math import mycodo.mycodo_flask.api.measurement diff --git a/mycodo/mycodo_flask/api/function.py b/mycodo/mycodo_flask/api/function.py new file mode 100644 index 000000000..ed7661406 --- /dev/null +++ b/mycodo/mycodo_flask/api/function.py @@ -0,0 +1,103 @@ +# coding=utf-8 +import logging +import traceback + +import flask_login +from flask_accept import accept +from flask_restx import Resource +from flask_restx import abort +from flask_restx import fields + +from mycodo.databases.models import CustomController +from mycodo.databases.models import DeviceMeasurements +from mycodo.databases.models import FunctionChannel +from mycodo.databases.models.controller import FunctionChannelSchema +from mycodo.databases.models.controller import FunctionSchema +from mycodo.databases.models.measurement import DeviceMeasurementsSchema +from mycodo.mycodo_flask.api import api +from mycodo.mycodo_flask.api import default_responses +from mycodo.mycodo_flask.api.sql_schema_fields import device_measurement_fields +from mycodo.mycodo_flask.api.sql_schema_fields import function_channel_fields +from mycodo.mycodo_flask.api.sql_schema_fields import function_fields +from mycodo.mycodo_flask.api.utils import get_from_db +from mycodo.mycodo_flask.api.utils import return_list_of_dictionaries +from mycodo.mycodo_flask.utils import utils_general + +logger = logging.getLogger(__name__) + +ns_function = api.namespace('functions', description='Function operations') + +function_single_fields = api.model('Function Status Fields', { + 'function settings': fields.Nested(function_fields), + 'function channels': fields.List(fields.Nested(function_channel_fields)), + 'device measurements': fields.List(fields.Nested(device_measurement_fields)), +}) + +function_list_fields = api.model('Function Fields List', { + 'function settings': fields.List(fields.Nested(function_fields)), + 'function channels': fields.List(fields.Nested(function_channel_fields)) +}) + + +@ns_function.route('/') +@ns_function.doc(security='apikey', responses=default_responses) +class Functions(Resource): + """Function information""" + + @accept('application/vnd.mycodo.v1+json') + @ns_function.marshal_with(function_list_fields) + @flask_login.login_required + def get(self): + """Show all function settings""" + if not utils_general.user_has_permission('view_settings'): + abort(403) + try: + list_data = get_from_db(FunctionSchema, CustomController) + list_channels = get_from_db(FunctionChannelSchema, FunctionChannel) + if list_data: + return {'function settings': list_data, + 'function channels': list_channels}, 200 + except Exception: + abort(500, + message='An exception occurred', + error=traceback.format_exc()) + + +@ns_function.route('/') +@ns_function.doc( + security='apikey', + responses=default_responses, + params={'unique_id': 'The unique ID of the function'} +) +class SettingsFunctionsUniqueID(Resource): + """Interacts with function settings in the SQL database""" + + @accept('application/vnd.mycodo.v1+json') + @ns_function.marshal_with(function_single_fields) + @flask_login.login_required + def get(self, unique_id): + """Show the settings for an function""" + if not utils_general.user_has_permission('view_settings'): + abort(403) + try: + list_data = get_from_db(FunctionSchema, CustomController, unique_id=unique_id) + + function_channel_schema = FunctionChannelSchema() + list_channels = return_list_of_dictionaries( + function_channel_schema.dump( + FunctionChannel.query.filter_by( + function_id=unique_id).all(), many=True)) + + measure_schema = DeviceMeasurementsSchema() + list_measurements = return_list_of_dictionaries( + measure_schema.dump( + DeviceMeasurements.query.filter_by( + device_id=unique_id).all(), many=True)) + + return {'function settings': list_data, + 'function channels': list_channels, + 'device measurements': list_measurements}, 200 + except Exception: + abort(500, + message='An exception occurred', + error=traceback.format_exc()) diff --git a/mycodo/mycodo_flask/api/input.py b/mycodo/mycodo_flask/api/input.py index 5bacf34f2..1c3c277c5 100644 --- a/mycodo/mycodo_flask/api/input.py +++ b/mycodo/mycodo_flask/api/input.py @@ -10,12 +10,15 @@ from mycodo.databases.models import DeviceMeasurements from mycodo.databases.models import Input +from mycodo.databases.models import InputChannel +from mycodo.databases.models.input import InputChannelSchema from mycodo.databases.models.input import InputSchema from mycodo.databases.models.measurement import DeviceMeasurementsSchema from mycodo.mycodo_client import DaemonControl from mycodo.mycodo_flask.api import api from mycodo.mycodo_flask.api import default_responses from mycodo.mycodo_flask.api.sql_schema_fields import device_measurement_fields +from mycodo.mycodo_flask.api.sql_schema_fields import input_channel_fields from mycodo.mycodo_flask.api.sql_schema_fields import input_fields from mycodo.mycodo_flask.api.utils import get_from_db from mycodo.mycodo_flask.api.utils import return_list_of_dictionaries @@ -27,12 +30,14 @@ input_single_fields = api.model('Input Status Fields', { 'input settings': fields.Nested(input_fields), + 'input channels': fields.List(fields.Nested(input_channel_fields)), 'device measurements': fields.List( fields.Nested(device_measurement_fields)), }) input_list_fields = api.model('Input Fields List', { 'input settings': fields.List(fields.Nested(input_fields)), + 'input channels': fields.List(fields.Nested(input_channel_fields)) }) @@ -50,8 +55,10 @@ def get(self): abort(403) try: list_data = get_from_db(InputSchema, Input) + list_channels = get_from_db(InputChannelSchema, InputChannel) if list_data: - return {'input settings': list_data}, 200 + return {'input settings': list_data, + 'input channels': list_channels}, 200 except Exception: abort(500, message='An exception occurred', @@ -75,16 +82,23 @@ def get(self, unique_id): if not utils_general.user_has_permission('view_settings'): abort(403) try: - dict_data = get_from_db(InputSchema, Input, unique_id=unique_id) + list_data = get_from_db(InputSchema, Input, unique_id=unique_id) + + measure_schema = InputChannelSchema() + list_channels = return_list_of_dictionaries( + measure_schema.dump( + InputChannel.query.filter_by( + input_id=unique_id).all(), many=True)) measure_schema = DeviceMeasurementsSchema() - list_data = return_list_of_dictionaries( + list_measurements = return_list_of_dictionaries( measure_schema.dump( DeviceMeasurements.query.filter_by( device_id=unique_id).all(), many=True)) - return {'input settings': dict_data, - 'device measurements': list_data}, 200 + return {'input settings': list_data, + 'input channels': list_channels, + 'device measurements': list_measurements}, 200 except Exception: abort(500, message='An exception occurred', diff --git a/mycodo/mycodo_flask/api/output.py b/mycodo/mycodo_flask/api/output.py index 018746e4b..afd547f80 100644 --- a/mycodo/mycodo_flask/api/output.py +++ b/mycodo/mycodo_flask/api/output.py @@ -27,10 +27,10 @@ ns_output = api.namespace('outputs', description='Output operations') MODEL_STATES_STATE = ns_output.model('states', { - '*': fields.Wildcard(fields.String(description='on, off, or a duty cycle'),) + '*': fields.Wildcard(fields.String(description='on, off, or a duty cycle'),) }) MODEL_STATES_CHAN = ns_output.model('channels', { - '*': fields.Wildcard(fields.Nested( + '*': fields.Wildcard(fields.Nested( MODEL_STATES_STATE, description='Dictionary with channel as key and state data as value.')) }) @@ -140,10 +140,10 @@ def get(self, unique_id): abort(403) try: - dict_data = get_from_db(OutputSchema, Output, unique_id=unique_id) + list_data = get_from_db(OutputSchema, Output, unique_id=unique_id) output_channel_schema = OutputChannelSchema() - list_data = return_list_of_dictionaries( + list_channels = return_list_of_dictionaries( output_channel_schema.dump( OutputChannel.query.filter_by( output_id=unique_id).all(), many=True)) @@ -155,8 +155,8 @@ def get(self, unique_id): for each_channel in states[unique_id]: new_state_dict[str(each_channel)] = states[unique_id][each_channel] - return {'output device': dict_data, - 'output device channels': list_data, + return {'output device': list_data, + 'output device channels': list_channels, 'output device channel states': new_state_dict}, 200 except Exception: abort(500, diff --git a/mycodo/mycodo_flask/api/sql_schema_fields.py b/mycodo/mycodo_flask/api/sql_schema_fields.py index a0e1aeb03..36ae257de 100644 --- a/mycodo/mycodo_flask/api/sql_schema_fields.py +++ b/mycodo/mycodo_flask/api/sql_schema_fields.py @@ -3,6 +3,25 @@ from mycodo.mycodo_flask.api import api +function_fields = api.model('Function Device Fields', { + 'id': fields.Integer, + 'unique_id': fields.String, + 'name': fields.String, + 'device': fields.String, + 'is_activated': fields.Boolean, + 'log_level_debug': fields.Boolean, + 'custom_options': fields.String +}) + +function_channel_fields = api.model('Function Channel Fields', { + 'id': fields.Integer, + 'unique_id': fields.String, + 'function_id': fields.String, + 'channel': fields.Integer, + 'name': fields.String, + 'custom_options': fields.String, +}) + device_measurement_fields = api.model('Device Measurement Settings Fields', { 'id': fields.Integer, 'unique_id': fields.String, @@ -76,6 +95,15 @@ 'custom_options': fields.String }) +input_channel_fields = api.model('Function Channel Fields', { + 'id': fields.Integer, + 'unique_id': fields.String, + 'input_id': fields.String, + 'channel': fields.Integer, + 'name': fields.String, + 'custom_options': fields.String, +}) + math_fields = api.model('Math Settings Fields', { 'id': fields.Integer, 'unique_id': fields.String,