From 8f74c87df5a08451d98d0c2b595d0f7561a49440 Mon Sep 17 00:00:00 2001 From: okalintu Date: Sun, 29 Oct 2017 23:46:20 +0200 Subject: [PATCH] Reimplement serverside coffee scale --- coffee_scale/apps.py | 23 --- coffee_scale/mqtt.py | 58 -------- coffee_scale/static/js/coffee.js | 231 +++++++++++++++-------------- coffee_scale/templates/coffee.html | 2 + coffee_scale/tests.py | 28 ---- coffee_scale/urls.py | 4 +- coffee_scale/views.py | 16 -- requirements.txt | 1 - sikweb/base.py | 2 +- 9 files changed, 124 insertions(+), 241 deletions(-) delete mode 100644 coffee_scale/apps.py delete mode 100644 coffee_scale/mqtt.py diff --git a/coffee_scale/apps.py b/coffee_scale/apps.py deleted file mode 100644 index 57c4c5b..0000000 --- a/coffee_scale/apps.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.apps import AppConfig - -import logging -import sys - -from coffee_scale import mqtt - - -class CoffeeScaleConfig(AppConfig): - name = 'coffee_scale' - - def ready(self): - if ('makemigrations' in sys.argv or 'migrate' in sys.argv): - return - - try: - logging.info('Connecting to MQTT (coffee scale) at {}...'.format(mqtt.HOST)) - logging.info('If there is no confirmation, the MQTT connection has probably failed.') - mqtt.client.connect_async(mqtt.HOST, mqtt.PORT, 60) - mqtt.client.loop_start() - except Exception as ex: - logging.error(ex) - logging.error('Failed to connect to MQTT at {}'.format(mqtt.HOST)) diff --git a/coffee_scale/mqtt.py b/coffee_scale/mqtt.py deleted file mode 100644 index 47a4592..0000000 --- a/coffee_scale/mqtt.py +++ /dev/null @@ -1,58 +0,0 @@ -import paho.mqtt.client as mqtt -import logging -import datetime -from collections import deque - -from django.conf import settings - -HOST = settings.MQTT_SETTINGS['HOST'] -PORT = settings.MQTT_SETTINGS['PORT'] -TOPICS = settings.MQTT_SETTINGS['TOPICS'] -latest = {} - - -def on_connect(client, userdata, flags, rc): - logging.info('Connected successfully to MQTT.') - logging.info('Subscribing to all topics on {}.'.format(HOST)) - client.subscribe('sik/kiltahuone/kahvivaaka/#') - - -def update_latest(msg): - payload = msg.payload.decode('utf-8') - if msg.topic == TOPICS['WEIGHT']: - weight = float(payload) - latest['weight'] = weight - elif msg.topic == TOPICS['CUPS']: - cups = float(payload) - latest['cups'] = cups - elif msg.topic == TOPICS['BREWING']: - brewing = bool(int(payload)) - latest['brewing'] = brewing - elif msg.topic == TOPICS['BREW_TIME']: - brew_time = datetime.datetime.fromtimestamp(float(payload)) - latest['brew_time'] = brew_time - - -def on_message(client, userdata, msg): - try: - update_latest(msg) - except Exception as ex: - logging.exception('Failed to parse MQTT payload.') - - -def on_disconnect(client, userdata, rc): - if rc != 0: - logging.warning('MQTT unexpectedly disconnected.') - else: - client.loop_stop(force=False) - logging.warning('MQTT disconnected.') - - -def get_latest(): - return latest - - -client = mqtt.Client() -client.on_connect = on_connect -client.on_message = on_message -client.on_disconnect = on_disconnect diff --git a/coffee_scale/static/js/coffee.js b/coffee_scale/static/js/coffee.js index 6b9b4a6..95f338b 100644 --- a/coffee_scale/static/js/coffee.js +++ b/coffee_scale/static/js/coffee.js @@ -1,130 +1,139 @@ -var len = 0; -var lastBrew = "∞"; -var brewtext = ""; +//Inner state +var lastBrew = new Date(0); +var brewing = false; +var backoff = 2000; -$(document).ready(function(){ - $('#text').bind("DOMSubtreeModified", resize); - updateTime(); - setInterval(updateTime,1000); - formatBrewTime(); - setInterval(formatBrewTime,10000); -}); +//MQTT client config +var username = "coffee-user-"+ Math.random(); +var client = new Paho.MQTT.Client("sika.sahkoinsinoorikilta.fi", 9001, username); +client.onMessageArrived = function (message) { + console.log("Topic: "+message.destinationName+" msg: "+message.payloadString); + var ev = new CustomEvent(message.destinationName, {'detail': message.payloadString}); + window.dispatchEvent(ev); +} -$(window).resize(resize); - -function fetchdata(data, status){ - if (typeof status !== 'undefined'){ - if (status == "success"){ - parseData(data); +function reconnect(responseObject){ + if (responseObject.errorCode !== 0) { + console.log("connection lost! Reason: "+responseObject.errorMessage); + setTimeout(function(){ + client.connect({onSuccess:onConnect, useSSL:true, onFailure: reconnect}); + }, backoff); } - else if (status == "error"){ - handleError(); - } - } } -$.getJSON("/coffee/cups", fetchdata); -setInterval(function() { - $.getJSON("/coffee/cups", fetchdata); -}, 2000); - -function formatBrewTime(){ - if (!brewtext && lastBrew instanceof Date){ - var now = new Date(); - var timeDiff = Math.max(now.getTime() - lastBrew.getTime(), 0); - var tmp = (timeDiff < 3600000) - ? Math.round(timeDiff / 60000) + ' min' - : '~' + Math.round(timeDiff / 3600000 * 2) / 2 + ' h'; - - $("#brewtime").html(tmp); - } else { - $("#brewtime").html(brewtext); +function onConnect() { + console.log("MQTT connected"); + //set and reset reconnector + client.onConnectionLost = reconnect + // subscribe to topics + client.subscribe("sik/kiltahuone/kahvivaaka/cups"); + client.subscribe("sik/kiltahuone/kahvivaaka/brewing"); + client.subscribe("sik/kiltahuone/kahvivaaka/brewtime"); } + +// data update and parse functions +function parseCups(ev){ + var cups = parseFloat(ev.detail).toFixed(1) + var cupsEvent = new CustomEvent("cupsChanged", {'detail': cups}); + window.dispatchEvent(cupsEvent); +} +function updateCups(ev){ + $("#text").text(ev.detail); +} +function updateScale(ev){ + $("#scale2").css({width: Math.min(ev.detail/9*100,100) + '%'}); } -function handleError() { - setData("?", 0, 0, Number.MAX_VALUE, Number.MAX_VALUE); +function tick(){ + var ev = new CustomEvent("tick", {'detail': new Date()}); + window.dispatchEvent(ev); } -function parseData(data) { - if (data) { - var date = new Date(data.date); - lastBrew = new Date(data.last_brew); - var now = new Date(); - var cups = data.cups; - var brewing = data.brewing; - var timeDiff = Math.max(now.getTime() - lastBrew.getTime(),0); - var opa = Math.max(100 - timeDiff / 90000,0); - setData(cups, data.temp, opa,now.getTime()-date.getTime(), timeDiff, brewing); - } - else{ - handleError(); - } -} - -function setData(cups, temp, opa, timeFromUpdate, timeFromBrew, brewing){ - if (cups == 0) { - opa = 0; - } - brewtext = ""; - $("#upper").css({opacity: opa/100}); - $("#scale2").css({width: Math.min(cups/9*100,100) + '%'}); - $("#text,body").removeClass(); - - cups = Number(cups).toFixed(1); - - if(timeFromUpdate > 600000){ - cups = "?"; - brewtext = "∞"; - $("#text").addClass("unknown"); - } - else if(brewing){ - cups = "+"; - brewtext = ":)"; - $("#text").addClass("brewing"); - } - else if(cups <= 2){ - $("#text").addClass("hurry"); - } - formatBrewTime(); - if($("#text").html() == "+" && !brewing) - $("body").addClass("coffeeready"); - - var cupsString = cups.toString(); - len = cupsString.length; - $("#text").html(cups); -} - -function updateTime(){ - var now = new Date(); +function updateTime(ev){ + var now = ev.detail; $("#time").html(formatTime(now.getHours(),now.getMinutes(),now.getSeconds())); } -function formatTime(hours, minutes, seconds){ - var str = ""; - - if(hours < 10) - str += "0"; - str += hours; - - str += ":"; - - if(minutes < 10) - str += "0"; - str += minutes; - - str += ":"; - - if(seconds < 10) - str += "0"; - str += seconds; - - return str; +function coffeeLowEffect(ev){ + ev.detail <= 2 ? $("#text").addClass("hurry") : $("#text").removeClass("hurry"); } +function coffeeReadyEffect(ev){ + $("body").addClass("coffeeready"); + // autoclear animation class in 10s + setTimeout(() => {$("body").removeClass("coffeeready");}, 10000); +} +function brewAnimStart(ev){ + $("#text").addClass("brewing"); +} +function brewAnimEnd(ev){ + $("#text").removeClass("brewing"); +} +function brewNotifier(ev){ + var new_brewing = parseInt(ev.detail); + if (new_brewing == 1 && brewing == 0){ + window.dispatchEvent(new Event("brewStart")); + } else if (new_brewing == 0 && brewing == 1){ + window.dispatchEvent(new Event("brewEnd")); + } + brewing = new_brewing; +} +function brewTimeParser(ev){ + lastBrew = new Date(parseInt(ev.detail)*1000.0); +} +function updateBrewDiff(ev){ + var now = new Date(); + var timeDiff = Math.max(now.getTime() - lastBrew.getTime(), 0); + var timeStr = (timeDiff < 3600000) + ? Math.round(timeDiff / 60000) + ' min' + : '~' + Math.round(timeDiff / 3600000 * 2) / 2 + ' h'; + + $("#brewtime").html(timeStr); +} + +// Helpers + +function nToS(num){ + return num < 10 ? "0" + num : "" + num; +} + +function formatTime(hours, minutes, seconds){ + return nToS(hours)+":"+nToS(minutes)+":"+nToS(seconds) +} + function resize(){ var w = $("#container").width(); var h = $("#container").height(); var s = w > h ? h : w; - var font = s*0.8*0.38/Math.sqrt(len); - $("#text").css({ top: s*0.16-font/2 + 'px', fontSize: font + 'px', marginLeft: -font*len*3/10 + 'px'}); + var font = s * 0.8 * 0.38/Math.sqrt(3); + $("#text").css({ top: s*0.16-font/2 + 'px', + fontSize: font + 'px', + marginLeft: -font*3*3/10 + 'px'}); } + +// Init everything + +$(document).ready(function(){ + client.connect({onSuccess:onConnect, useSSL:true, onFailure:reconnect}); + + //connect MQTT event listeners + window.addEventListener("sik/kiltahuone/kahvivaaka/cups", parseCups); + window.addEventListener("sik/kiltahuone/kahvivaaka/brewing", brewNotifier); + window.addEventListener("sik/kiltahuone/kahvivaaka/brewtime", brewTimeParser); + + //connect other event listeners + window.addEventListener("cupsChanged", updateCups); + window.addEventListener("cupsChanged", coffeeLowEffect); + window.addEventListener("cupsChanged", updateScale); + window.addEventListener("cupsChanged", resize); + window.addEventListener("brewStart", brewAnimStart); + window.addEventListener("brewEnd", brewAnimEnd); + window.addEventListener("brewEnd", coffeeReadyEffect); + window.addEventListener("tick", updateTime); + window.addEventListener("tick", updateBrewDiff); + + //start time based events + setInterval(tick, 100); + tick(); + +}); +$(window).resize(resize); diff --git a/coffee_scale/templates/coffee.html b/coffee_scale/templates/coffee.html index 1c2fec9..faf215c 100644 --- a/coffee_scale/templates/coffee.html +++ b/coffee_scale/templates/coffee.html @@ -10,6 +10,8 @@ + diff --git a/coffee_scale/tests.py b/coffee_scale/tests.py index b1e5f9b..f9c051b 100644 --- a/coffee_scale/tests.py +++ b/coffee_scale/tests.py @@ -1,30 +1,2 @@ from django.test import TestCase, Client from django.conf import settings - -from coffee_scale.mqtt import on_message - -HOST = settings.MQTT_SETTINGS['HOST'] -PORT = settings.MQTT_SETTINGS['PORT'] -TOPICS = settings.MQTT_SETTINGS['TOPICS'] - - -class MQTTTestCase(TestCase): - """Tests MQTT functionality""" - - class MockMessage: - def __init__(self, payload, topic): - self.payload = payload - self.topic = topic - - def setUp(self): - payload = '10'.encode('utf-8') - topic = TOPICS['CUPS'] - msg = MQTTTestCase.MockMessage(payload, topic) - - on_message(None, None, msg) - self.c = Client() - - def test_receive_cups(self): - response = self.c.get('/coffee/cups') - payload = response.json() - self.assertEquals(payload['cups'], 10) diff --git a/coffee_scale/urls.py b/coffee_scale/urls.py index b9c54e6..f750576 100644 --- a/coffee_scale/urls.py +++ b/coffee_scale/urls.py @@ -1,7 +1,7 @@ from django.conf.urls import url from django.views.generic.base import RedirectView -from .views import coffee_view, cups_view +from .views import coffee_view favicon_view = RedirectView.as_view(url='static/img/favicon.ico', permanent=True) @@ -9,6 +9,4 @@ urlpatterns = [ # landing page url(r'^$', coffee_view), - url(r'^cups', cups_view), - ] diff --git a/coffee_scale/views.py b/coffee_scale/views.py index 9b8ae08..b973641 100644 --- a/coffee_scale/views.py +++ b/coffee_scale/views.py @@ -3,25 +3,9 @@ from django.http import JsonResponse from django.utils import timezone -from .mqtt import get_latest -import coffee_scale.mqtt # somehow this is needed - import logging from django.conf import settings def coffee_view(request): return render(request, 'coffee.html') - - -def cups_view(request): - now = timezone.now() - latest = get_latest() - data = { - 'date': now, - 'cups': latest.get('cups'), - 'last_brew': latest.get('brew_time'), - 'brewing': latest.get('brewing'), - 'weight': latest.get('weight') - } - return JsonResponse(data) diff --git a/requirements.txt b/requirements.txt index ef7a2fd..5e52874 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,6 @@ dealer==2.0.5 django-modeltranslation==0.12.1 django-auditlog==0.4.3 django-phonenumber-field==1.3.0 -paho-mqtt==1.3.0 django-autocomplete-light==3.2.10 six==1.10.0 django-suit==0.2.25 diff --git a/sikweb/base.py b/sikweb/base.py index c094344..527c645 100644 --- a/sikweb/base.py +++ b/sikweb/base.py @@ -78,7 +78,7 @@ INSTALLED_APPS = [ 'webapp', 'members', 'infoscreen', - 'coffee_scale.apps.CoffeeScaleConfig', + 'coffee_scale', 'rest_framework', 'django_nose', 'bootstrap3',