| @@ -76,10 +76,9 @@ class ConfigView(BaseView): | |||||
| @classmethod | @classmethod | ||||
| def init_cache(cls): | def init_cache(cls): | ||||
| with cls.api._app.app_context(): | with cls.api._app.app_context(): | ||||
| cls.api.cache[cls.cache_key] = {} | cls.api.cache[cls.cache_key] = {} | ||||
| for key, value in cls.model.get_all().iteritems(): | |||||
| for key, value in cls.model.get_all().iteritems(): | |||||
| cls.post_init_callback(value) | cls.post_init_callback(value) | ||||
| cls.api.cache[cls.cache_key][value.name] = value | cls.api.cache[cls.cache_key][value.name] = value | ||||
| @@ -87,3 +86,6 @@ class ConfigView(BaseView): | |||||
| def init(cbpi): | def init(cbpi): | ||||
| ConfigView.register(cbpi._app, route_base='/api/config') | ConfigView.register(cbpi._app, route_base='/api/config') | ||||
| ConfigView.init_cache() | ConfigView.init_cache() | ||||
| @@ -50,6 +50,11 @@ class Sensor(Base): | |||||
| unit = "" | unit = "" | ||||
| @staticmethod | |||||
| def chart(sensor): | |||||
| result = [{"name": sensor.name, "data_type": "sensor", "data_id": sensor.id}] | |||||
| return result | |||||
| @classmethod | @classmethod | ||||
| def init_global(cls): | def init_global(cls): | ||||
| pass | pass | ||||
| @@ -38,17 +38,18 @@ class LogView(FlaskView): | |||||
| description: List of all log files | description: List of all log files | ||||
| """ | """ | ||||
| filename = "./logs/action.log" | filename = "./logs/action.log" | ||||
| if os.path.isfile(filename) == False: | |||||
| return | |||||
| import csv | |||||
| array = [] | array = [] | ||||
| with open(filename, 'rb') as f: | |||||
| reader = csv.reader(f) | |||||
| for row in reader: | |||||
| try: | |||||
| array.append([int((datetime.datetime.strptime(row[0], "%Y-%m-%d %H:%M:%S") - datetime.datetime(1970, 1, 1)).total_seconds()) * 1000, row[1]]) | |||||
| except: | |||||
| pass | |||||
| if os.path.isfile(filename): | |||||
| import csv | |||||
| array = [] | |||||
| with open(filename, 'rb') as f: | |||||
| reader = csv.reader(f) | |||||
| for row in reader: | |||||
| try: | |||||
| array.append([int((datetime.datetime.strptime(row[0], "%Y-%m-%d %H:%M:%S") - datetime.datetime(1970, 1, 1)).total_seconds()) * 1000, row[1]]) | |||||
| except: | |||||
| pass | |||||
| return json.dumps(array) | return json.dumps(array) | ||||
| @route('/<file>', methods=["DELETE"]) | @route('/<file>', methods=["DELETE"]) | ||||
| @@ -0,0 +1,10 @@ | |||||
| from modules.core.core import cbpi | |||||
| from modules.time_series_recorder.recorder_service import RecorderService | |||||
| from modules.time_series_recorder.recorder_view import RecorderView | |||||
| @cbpi.addon.core.initializer(order=1000) | |||||
| def init(cbpi): | |||||
| RecorderView.register(cbpi._app, route_base='/api/recorder') | |||||
| @@ -0,0 +1,17 @@ | |||||
| import abc | |||||
| class BaseRecorder: | |||||
| __metaclass__ = abc.ABCMeta | |||||
| @abc.abstractmethod | |||||
| def load_time_series(self, ts_type, ts_id): | |||||
| raise NotImplementedError("Method not implemented") | |||||
| @abc.abstractmethod | |||||
| def check_status(self): | |||||
| raise NotImplementedError("Method not implemented") | |||||
| @abc.abstractmethod | |||||
| def store_datapoint(self, name, value, tags): | |||||
| raise NotImplementedError("Method not implemented") | |||||
| @@ -0,0 +1,12 @@ | |||||
| from modules.time_series_recorder.base_recorder import BaseRecorder | |||||
| class FileRecorder(BaseRecorder): | |||||
| def check_status(self): | |||||
| pass | |||||
| def load_time_series(self, ts_type, ts_id): | |||||
| pass | |||||
| def store_datapoint(self, name, value, tags): | |||||
| pass | |||||
| @@ -0,0 +1,73 @@ | |||||
| import logging | |||||
| import time | |||||
| import requests | |||||
| from flask import json | |||||
| from modules.time_series_recorder.base_recorder import BaseRecorder | |||||
| class KairosdbRecorder(BaseRecorder): | |||||
| kairosdb_ip = "http://192.168.178.20:" | |||||
| logger = logging.getLogger(__name__) | |||||
| kairosdb_api_status = "/api/v1/health/status" | |||||
| kairosdb_api_query = "/api/v1/datapoints/query" | |||||
| def __init__(self, port): | |||||
| self.kairosdb_url = self.kairosdb_ip + port | |||||
| def load_time_series(self, ts_type, ts_id): | |||||
| time_series_name = self.compose_time_series_name(ts_type, ts_id) | |||||
| data = dict(metrics=[ | |||||
| { | |||||
| "tags": {}, | |||||
| "name": time_series_name, | |||||
| "aggregators": [ | |||||
| { | |||||
| "name": "avg", | |||||
| "align_sampling": True, | |||||
| "sampling": { | |||||
| "value": "1", | |||||
| "unit": "minutes" | |||||
| }, | |||||
| "align_start_time": True | |||||
| } | |||||
| ] | |||||
| } | |||||
| ], | |||||
| cache_time=0, | |||||
| start_relative={ | |||||
| "value": "30", | |||||
| "unit": "days" | |||||
| }) | |||||
| response = requests.post(self.kairosdb_url + self.kairosdb_api_query, json.dumps(data)) | |||||
| if response.ok: | |||||
| self.logger.debug("Fetching time series for [{0}] took [{1}]".format(time_series_name, response.elapsed)) | |||||
| self.logger.debug("Time series for [{0}] is [{1}]".format(time_series_name, response.json())) | |||||
| return response.json()["queries"][0]["results"][0]["values"] | |||||
| else: | |||||
| self.logger.warning("Failed to fetch time series for [{0}]. Response [{1}]", time_series_name, response) | |||||
| def check_status(self): | |||||
| response = requests.get(self.kairosdb_url + self.kairosdb_api_status) | |||||
| return response.json() | |||||
| def store_datapoint(self, name, value, tags): | |||||
| data = [ | |||||
| dict(name=name, datapoints=[ | |||||
| [int(round(time.time() * 1000)), value] | |||||
| ], tags={ | |||||
| "cbpi": tags | |||||
| }) | |||||
| ] | |||||
| response = requests.post(self.kairosdb_url + "/api/v1/datapoints", json.dumps(data)) | |||||
| @staticmethod | |||||
| def compose_time_series_name(ts_type, ts_id): | |||||
| return "{0}_{1}".format(ts_type, ts_id) | |||||
| @@ -0,0 +1,65 @@ | |||||
| import logging | |||||
| from modules.core.core import cbpi | |||||
| from modules.time_series_recorder.file_recorder import FileRecorder | |||||
| from modules.time_series_recorder.kairosdb_recorder import KairosdbRecorder | |||||
| class RecorderService: | |||||
| _logger = logging.getLogger(__name__) | |||||
| def __init__(self): | |||||
| print "init" | |||||
| self.config_event_listener() | |||||
| self.recorder = self.determine_recorder_impl() | |||||
| def check_status(self): | |||||
| return self.recorder.check_status() | |||||
| def load_time_series(self, type_short, type_id): | |||||
| if type_short == "s": | |||||
| type_full = "sensors" | |||||
| sensor = cbpi.cache.get(type_full).get(type_id) | |||||
| chart = cbpi.sensor.get_sensors(sensor.type).get("class").chart(sensor) | |||||
| elif type_short == "k": | |||||
| type_full = "kettle" | |||||
| kettle = cbpi.cache.get(type_full).get(type_id) | |||||
| chart = cbpi.brewing.get_controller(kettle.logic).get("class").chart(kettle) | |||||
| elif type_short == "f": | |||||
| type_full = "fermenter" | |||||
| fermenter = cbpi.cache.get(type_full).get(type_id) | |||||
| chart = cbpi.fermentation.get_controller(fermenter.logic).get("class").chart(fermenter) | |||||
| else: | |||||
| chart = [] | |||||
| return map(self.convert_chart_data_to_json, chart) | |||||
| def convert_chart_data_to_json(self, chart_data): | |||||
| return {"name": chart_data["name"], | |||||
| "data": self.recorder.load_time_series(chart_data["data_type"], chart_data["data_id"])} | |||||
| def hallo2(self, **kwargs): | |||||
| print kwargs | |||||
| cbpi.beep() | |||||
| # if name == "kairos_db" or name == "kairos_db_port": | |||||
| # print name | |||||
| # print cbpi.cache["config"][name].__dict__["value"] | |||||
| def config_event_listener(self): | |||||
| if "CONFIG_UPDATE" not in cbpi.eventbus: | |||||
| cbpi.eventbus["CONFIG_UPDATE"] = [] | |||||
| cbpi.eventbus["CONFIG_UPDATE"].append({"function": self.hallo2, "async": False}) | |||||
| @staticmethod | |||||
| def determine_recorder_impl(): | |||||
| use_kairosdb = cbpi.cache["config"]["kairos_db"].__dict__["value"] | |||||
| kairosdb_port = cbpi.cache["config"]["kairos_db_port"].__dict__["value"] | |||||
| if use_kairosdb: | |||||
| recorder = KairosdbRecorder(kairosdb_port) | |||||
| else: | |||||
| recorder = FileRecorder() | |||||
| return recorder | |||||
| @@ -0,0 +1,40 @@ | |||||
| import logging | |||||
| from flask import jsonify | |||||
| from flask_api import status | |||||
| from flask_classy import FlaskView | |||||
| from flask_classy import route | |||||
| from modules.time_series_recorder.recorder_service import RecorderService | |||||
| class RecorderView(FlaskView): | |||||
| _logger = logging.getLogger(__name__) | |||||
| _service = None | |||||
| def __init__(self): | |||||
| if RecorderView._service is None: | |||||
| RecorderView._service = RecorderService() | |||||
| @route('/status', methods=["GET"]) | |||||
| def check_status(self): | |||||
| recorder_status = RecorderView._service.check_status() | |||||
| return self.make_json_response(recorder_status) | |||||
| @route('/<type_short>/<int:type_id>', methods=["GET"]) | |||||
| def load_time_series(self, type_short, type_id): | |||||
| acceptable_types = ("s", "k", "f") | |||||
| if type_short in acceptable_types: | |||||
| time_series = RecorderView._service.load_time_series(type_short, type_id) | |||||
| return self.make_json_response(time_series) | |||||
| else: | |||||
| content = "Invalid type. Only {0} are allowed.".format(acceptable_types) | |||||
| return content, status.HTTP_400_BAD_REQUEST | |||||
| @staticmethod | |||||
| def make_json_response(content): | |||||
| response = jsonify(content) | |||||
| response.headers["content-type"] = "application/json; charset=UTF-8" | |||||
| return response | |||||
| @@ -1,4 +1,5 @@ | |||||
| Flask==0.11.1 | Flask==0.11.1 | ||||
| Flask-API==1.0 | |||||
| Flask-SocketIO==2.6.2 | Flask-SocketIO==2.6.2 | ||||
| flask_login==0.4.0 | flask_login==0.4.0 | ||||
| flask_swagger==0.2.13 | flask_swagger==0.2.13 | ||||
| @@ -23,5 +23,6 @@ from modules.base_plugins.steps import * | |||||
| from modules.example_plugins.WebViewJquery import * | from modules.example_plugins.WebViewJquery import * | ||||
| from modules.example_plugins.WebViewReactJs import * | from modules.example_plugins.WebViewReactJs import * | ||||
| from modules.example_plugins.swagger import * | from modules.example_plugins.swagger import * | ||||
| from modules.time_series_recorder import * | |||||
| cbpi.run() | cbpi.run() | ||||
| @@ -0,0 +1,2 @@ | |||||
| INSERT OR IGNORE INTO config VALUES ('kairos_db', 'NO', 'select', 'Use timeseries database KairosDB for storing sensor values. For now you have to install KairosDB by yourself.', '["YES","NO"]' ); | |||||
| INSERT OR IGNORE INTO config VALUES ('kairos_db_port', 5001, 'number', 'Port for KairosDB. We assume the DB is running on your PI, so IP-Address is 127.0.0.1.', NULL ); | |||||