diff --git a/.gitignore b/.gitignore index 61c9711..ba64065 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ Pipfile.lock dash dcc +poetry.lock +homecontrol_dash.egg-info diff --git a/homecontrol-dash.py b/homecontrol-dash.py deleted file mode 100755 index a5e083e..0000000 --- a/homecontrol-dash.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env python3 -import dash -import dash_bootstrap_components as dbc -import dash_core_components as dcc -import dash_html_components as html -from dash.dependencies import Input, Output -import time -import requests -from math import log, log10 - -URL_BASE="http://innocence:5000" - -def get_sensors(): - ret = {} - try: - url = f"{URL_BASE}/sensor/get" - res = requests.get(url) - ret = res.json() - except Exception as ex: - print(f"Exception Type: {type(ex).__name__}, args:\n{ex.args}") - - return ret - - -def get_values(sensorId, min_ts, max_ts, limit): - ret = {} - if sensorId: - try: - url = f"{URL_BASE}/sensor/get_values/{sensorId}?min_ts={min_ts}&max_ts={max_ts}&limit={limit}" - # print(url) - res = requests.get(url) - ret = res.json()[sensorId] - except Exception as ex: - print(f"Exception Type: {type(ex).__name__}, args:\n{ex.args}") - return ret - -def pTime(value, fmt="%Y/%m/%d %H:%M"): - if value > 0: - return time.strftime(fmt, time.localtime(float(value))) - else: - return 0 - - -sensors = get_sensors() -sensor = next(iter(sensors), None) -tabs = [] -for s in sensors: - sensorType = sensors[s]["sensorType"] - tabs.append(dcc.Tab(label=sensorType, value=s)) - -external_stylesheets = [dbc.themes.BOOTSTRAP] -meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}] - -app = dash.Dash(__name__, external_stylesheets=external_stylesheets, meta_tags=meta_tags) -app.title = "dashboard.ykonni.de" - - -app.layout = html.Div(children=[ - html.H1(children='dashboard.ykonni.de'), - dbc.Row([ - dbc.Col(dcc.Tabs(id="tabs-select-sensor", value=sensor, children=tabs)) - ]), - dbc.Row([ - dbc.Col( - dcc.Graph( - id='graph-sensor-values', - figure={ - 'data': [ {'x': [0], 'y': [0], 'mode': 'line', 'name': 'None'} ], - 'layout': { 'title': 'initial values' } - } - ) - ) - ]), - dbc.Row([ - dbc.Col([ - html.Div(id='slider-min-txt', style={'marginLeft': '5em', 'marginRight': '3em'}), - html.Div([ - dcc.Slider(id='slider-min', min=0, max=round(time.time()), step=600, - value=0 ), - ], style={'marginLeft': '5em', 'marginRight': '3em'} - ), - ]), - dbc.Col([ - html.Div(id='slider-max-txt', style={'marginLeft': '3em', 'marginRight': - '3em'}), - html.Div([ - dcc.Slider(id='slider-max', min=0, max=round(time.time()), step=600, - value=round(time.time())), - ], style={'marginLeft': '3em', 'marginRight': '3em'} - ), - ]), - dbc.Col([ - html.Div(id='slider-limit-txt', style={'marginLeft': '3em', 'marginRight': '5em'}), - html.Div([ - dcc.Slider(id='slider-limit', min=0, max=1000, step=10, value=0 ), - ], style={'marginLeft': '3em', 'marginRight': '5em'} - ), - ]), - ]), -]) - -@app.callback( - [Output('slider-min', 'min'), Output('slider-min', 'value'), - Output('slider-min', 'max')], - [Input('tabs-select-sensor', 'value')]) -def update_slider_min(sensorId): - res = get_values(sensorId, 0, int(time.time()), 1) - min_ts = 0 - # default last 7 days - cur_ts = round(time.time() - (60*60*24*7)) - max_ts = int(time.time()) - sensorType = None - if "values" in res: - min_ts = int(res["values"][0]["ts"]) - sensorType = res["sensorType"] - - # print(f"min: [{min_ts} [{cur_ts}] {max_ts}]" - return min_ts, cur_ts, max_ts - - -@app.callback( - Output('slider-min-txt', 'children'), - [Input('slider-min', 'value')]) -def update_slider_min_txt(value): - return f"From: {pTime(value)}" - -@app.callback( - [Output('slider-max', 'min'), Output('slider-max', 'value'), - Output('slider-max', 'max')], - [Input('tabs-select-sensor', 'value')]) -def update_slider_max(sensorId): - res = get_values(sensorId, 0, int(time.time()), 1) - min_ts = 0 - max_ts = int(time.time()) - sensorType = None - if "values" in res: - min_ts = int(res["values"][0]["ts"]) - sensorType = res["sensorType"] - - # print(f"max: [{min_ts} [{max_ts}] {max_ts}]" - return min_ts, max_ts, max_ts - - -@app.callback( - Output('slider-max-txt', 'children'), - [Input('slider-max', 'value')]) -def update_slider_max_txt(value): - return f"To: {pTime(value)}" - - -@app.callback( - Output('slider-limit-txt', 'children'), - [Input('slider-limit', 'value')]) -def update_slider_limit_txt(value): - return f"Limit: {value if value > 0 else None}" - - -@app.callback( - Output('graph-sensor-values', 'figure'), - [Input('tabs-select-sensor', 'value'), Input('slider-min', 'value'), - Input('slider-max', 'value'), Input('slider-limit', 'value')]) -def update_graph_sensor_values(sensorId, min_ts, max_ts, limit): - res = {} - if min_ts > 0: - res = get_values(sensorId, min_ts, max_ts, limit) - if "values" in res: - v = res["values"] - s = res["sensorType"] - x = [pTime(v[i]["ts"], fmt="%m%d-%H%M") for i in range(len(v))] - if s == "luminance": - y = [log10(v[i]["value"]/5+1) for i in range(len(v))] - else: - y = [v[i]["value"] for i in range(len(v))] - else: - s = sensorId - x = [0] - y = [0] - return { - 'data': [ {'x': x, 'y': y, 'mode': 'line', 'name': f'{s}'} ], - 'layout': { 'title': f'Data for sensor: {s} ({len(x)} elements)' } - } - -if __name__ == '__main__': - - app.run_server(port=8081,debug=True) diff --git a/homecontrol_dash/__main__.py b/homecontrol_dash/__main__.py new file mode 100644 index 0000000..f5279ae --- /dev/null +++ b/homecontrol_dash/__main__.py @@ -0,0 +1,3 @@ +import hcdash + +hcdash.main() diff --git a/homecontrol_dash/__pycache__/hcdash.cpython-38.pyc b/homecontrol_dash/__pycache__/hcdash.cpython-38.pyc new file mode 100644 index 0000000..0428126 Binary files /dev/null and b/homecontrol_dash/__pycache__/hcdash.cpython-38.pyc differ diff --git a/homecontrol_dash/hcdash.py b/homecontrol_dash/hcdash.py new file mode 100755 index 0000000..a3d86cf --- /dev/null +++ b/homecontrol_dash/hcdash.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +import argparse +import dash +import dash_bootstrap_components as dbc +import dash_core_components as dcc +import dash_daq as daq +import dash_html_components as html +from dash.dependencies import Input, Output +import json +import logging +import os +import time +import requests +from math import log, log10 +from xdg import XDG_CONFIG_HOME + +CONFIG_FILE = f"{XDG_CONFIG_HOME}/hcdash.json" +ADDRESS="http://localhost:8200" +PORT=8201 + +def setup(): + # arguments + parser = argparse.ArgumentParser(description='homecontrol') + parser.add_argument("-a", "--address", dest="address", type=str, help="homecontrol address") + parser.add_argument("-p", "--port", dest="port", type=str, help="dashboard port") + parser.add_argument("-c", "--config", dest="config", type=str, help="config file", + default=CONFIG_FILE) + parser.add_argument("-d", "--debug", dest="debug", action="store_true", + help="debug mode") + + # parse arguments + args = parser.parse_args() + + # initialize config + if not os.path.exists(XDG_CONFIG_HOME): + os.makedirs(XDG_CONFIG_HOME) + config = {} + try: + config_file = open(args.config, "r") + config = json.load(config_file) + except: + pass + + # fill new keys with defaults + if not config.get("address"): + config["address"] = ADDRESS + if not config.get("port"): + config["port"] = PORT + + # overwrite with arguments + if args.address: + config["address"] = args.address + if args.port: + config["port"] = args.port + + # save to file + with open(args.config, 'w') as config_file: + json.dump(config, config_file) + + # temporary option + if args.debug: + config["debug"] = args.debug + else: + config["debug"] = False + + return config + +class HCDash: + def __init__(self, config): + self.config = config + # logging + FORMAT="[%(asctime)13s :: %(name)18s :: %(levelname)7s] %(message)s" + DATEFMT="%Y%m%d %H:%M:%S" + logging.basicConfig(level=logging.INFO, format=FORMAT, datefmt=DATEFMT) + self.logger = logging.getLogger("HCDash") + if self.config["debug"]: + self.logger.setLevel(logging.DEBUG) + + def get_actors(self): + ret = {} + try: + url = f"{self.config.get('address')}/actor/get" + res = requests.get(url) + self.logger.debug(f"get_actors: {res}") + ret = res.json() + self.logger.debug(f"get_actors: {ret}") + except Exception as ex: + self.logger.error(f"Exception get_actors, type: {type(ex).__name__}, args:\n{ex.args}") + finally: + return ret + + def get_sensors(self): + ret = {} + try: + url = f"{self.config.get('address')}/sensor/get" + res = requests.get(url) + ret = res.json() + self.logger.debug(f"get_sensors: {ret}") + except Exception as ex: + self.logger.error(f"Exception Type: {type(ex).__name__}, args:\n{ex.args}") + finally: + return ret + + def set_level(self, actorId, level): + req = {"id": actorId, "command": "set_level", "data": {"level": level}} + self.logger.debug(f"set_level: {req}") + requests.post(f"{self.config.get('address')}/actor/command", json=req) + + def get_values(self, sensorId, min_ts, max_ts, limit): + ret = {} + if sensorId: + try: + url = f"{self.config.get('address')}/sensor/get_values/{sensorId}?min_ts={min_ts}&max_ts={max_ts}&limit={limit}" + res = requests.get(url) + ret = res.json()[sensorId] + except Exception as ex: + self.logger.error(f"Exception Type: {type(ex).__name__}, args:\n{ex.args}") + return ret + + + def pTime(self, value, fmt="%Y/%m/%d %H:%M"): + if value > 0: + return time.strftime(fmt, time.localtime(float(value))) + else: + return 0 + + + def main(self): + tab = "sensors" + tabs = [ + dcc.Tab(label="sensors", value="sensors"), + dcc.Tab(label="actors", value="actors") + ] + + external_stylesheets = [dbc.themes.BOOTSTRAP] + meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}] + + app = dash.Dash(__name__, external_stylesheets=external_stylesheets, meta_tags=meta_tags) + app.title = "dashboard.ykonni.de" + app.config.suppress_callback_exceptions = True + + app.layout = html.Div(children=[ + html.H1(children='dashboard.ykonni.de'), + dbc.Row([ + dbc.Col(dcc.Tabs(id="tabs-select-class", value=tab, children=tabs)) + ]), + html.Div(id="tabs-content") + ]) + + @app.callback( + Output("tabs-content", "children"), + [Input("tabs-select-class", "value")]) + def updatefoo(value): + if value == "sensors": + sensors = self.get_sensors() + sensor = next(iter(sensors), None) + sensor_tabs = [] + for s in sensors: + sensorType = sensors[s]["sensorType"] + sensor_tabs.append(dcc.Tab(label=sensorType, value=s)) + return html.Div([ + dbc.Row([ + dbc.Col(dcc.Tabs(id="tabs-select-sensor", value=sensor, + children=sensor_tabs)) + ]), + dbc.Row([ + dbc.Col( + dcc.Graph( + id='graph-sensor-values', + figure={ + 'data': [ {'x': [0], 'y': [0], 'mode': 'line', 'name': 'None'} ], + 'layout': { 'title': 'initial values' } + } + ) + ) + ]), + dbc.Row([ + dbc.Col([ + html.Div(id='slider-min-txt', style={'marginLeft': '5em', 'marginRight': '3em'}), + html.Div([ + dcc.Slider(id='slider-min', min=0, max=round(time.time()), step=600, + value=0 ), + ], style={'marginLeft': '5em', 'marginRight': '3em'} + ), + ]), + dbc.Col([ + html.Div(id='slider-max-txt', style={'marginLeft': '3em', 'marginRight': + '3em'}), + html.Div([ + dcc.Slider(id='slider-max', min=0, max=round(time.time()), step=600, + value=round(time.time())), + ], style={'marginLeft': '3em', 'marginRight': '3em'} + ), + ]), + dbc.Col([ + html.Div(id='slider-limit-txt', style={'marginLeft': '3em', 'marginRight': '5em'}), + html.Div([ + dcc.Slider(id='slider-limit', min=0, max=1000, step=10, value=0 ), + ], style={'marginLeft': '3em', 'marginRight': '5em'} + ), + ]), + ]), + ]) + elif value == "actors": + actors = self.get_actors() + actor = next(iter(actors), None) + actor_tabs = [] + for a in actors: + actorType = actors[a]["actorType"] + actor_tabs.append(dcc.Tab(label=actorType, value=a)) + return html.Div([ + dbc.Row([ + dbc.Col(dcc.Tabs(id="tabs-select-actor", value=actor, + children=actor_tabs)) + ]), + dbc.Row([ + daq.ColorPicker( + id="color-picker", + label="Color Picker", + size=400, + value=dict(hex="#268bd2") + ), + html.P(id="empty") + ]), + ]) + else: + return html.Div([ + html.H3("undefined") + ]) + + + ## sensor callbacks + @app.callback( + [Output('slider-min', 'min'), Output('slider-min', 'value'), + Output('slider-min', 'max')], + [Input('tabs-select-sensor', 'value')]) + def update_slider_min(sensorId): + res = self.get_values(sensorId, 0, int(time.time()), 1) + min_ts = 0 + # default last 7 days + cur_ts = round(time.time() - (60*60*24*7)) + max_ts = int(time.time()) + sensorType = None + if "values" in res: + min_ts = int(res["values"][0]["ts"]) + sensorType = res["sensorType"] + + return min_ts, cur_ts, max_ts + + @app.callback( + Output('slider-min-txt', 'children'), + [Input('slider-min', 'value')]) + def update_slider_min_txt(value): + return f"From: {self.pTime(value)}" + + @app.callback( + [Output('slider-max', 'min'), Output('slider-max', 'value'), + Output('slider-max', 'max')], + [Input('tabs-select-sensor', 'value')]) + def update_slider_max(sensorId): + res = self.get_values(sensorId, 0, int(time.time()), 1) + min_ts = 0 + max_ts = int(time.time()) + sensorType = None + if "values" in res: + min_ts = int(res["values"][0]["ts"]) + sensorType = res["sensorType"] + + return min_ts, max_ts, max_ts + + + @app.callback( + Output('slider-max-txt', 'children'), + [Input('slider-max', 'value')]) + def update_slider_max_txt(value): + return f"To: {self.pTime(value)}" + + + @app.callback( + Output('slider-limit-txt', 'children'), + [Input('slider-limit', 'value')]) + def update_slider_limit_txt(value): + return f"Limit: {value if value > 0 else None}" + + + @app.callback( + Output('graph-sensor-values', 'figure'), + [Input('tabs-select-sensor', 'value'), Input('slider-min', 'value'), + Input('slider-max', 'value'), Input('slider-limit', 'value')]) + def update_graph_sensor_values(sensorId, min_ts, max_ts, limit): + res = {} + if min_ts > 0: + res = self.get_values(sensorId, min_ts, max_ts, limit) + if "values" in res: + v = res["values"] + s = res["sensorType"] + x = [self.pTime(v[i]["ts"], fmt="%m%d-%H%M") for i in range(len(v))] + if s == "luminance": + y = [log10(v[i]["value"]/5+1) for i in range(len(v))] + else: + y = [v[i]["value"] for i in range(len(v))] + else: + s = sensorId + x = [0] + y = [0] + return { + 'data': [ {'x': x, 'y': y, 'mode': 'line', 'name': f'{s}'} ], + 'layout': { 'title': f'Data for sensor: {s} ({len(x)} elements)' } + } + + ## actor callbacks + @app.callback( + Output("empty", "value"), + [Input('tabs-select-actor', 'value'), Input('color-picker', 'value')]) + def set_level(actorId, level): + rgb = level.get("rgb") + if rgb: + r = (rgb.get("r") or 0) << 16 + g = (rgb.get("g") or 0) << 8 + b = (rgb.get("b") or 0) + l = r + g + b + self.set_level(actorId, l) + return "" + + + app.run_server(host="0.0.0.0", port=self.config.get("port"), debug=self.config.get("debug")) + + +def main(): + config = setup() + hcd = HCDash(config) + hcd.main() + +if __name__ == '__main__': + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..70c521b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[tool.poetry] +name = "homecontrol-dash" +version = "0.1.0" +description = "dashboard for homecontrol" +authors = ["Konstantin Koslowski "] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.8" +dash = "^1.9.1" +dash-daq = "^0.3.3" +dash-bootstrap-components = "^0.8.3" +requests = "^2.23.0" +xdg = "^4.0.1" + +[tool.poetry.dev-dependencies] + +[tool.poetry.scripts] +hcdash = "homecontrol_dash.hcdash:main" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api"