Reimplement serverside coffee scale
This commit is contained in:
@@ -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))
|
|
||||||
@@ -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
|
|
||||||
+120
-111
@@ -1,130 +1,139 @@
|
|||||||
var len = 0;
|
//Inner state
|
||||||
var lastBrew = "∞";
|
var lastBrew = new Date(0);
|
||||||
var brewtext = "";
|
var brewing = false;
|
||||||
|
var backoff = 2000;
|
||||||
|
|
||||||
$(document).ready(function(){
|
//MQTT client config
|
||||||
$('#text').bind("DOMSubtreeModified", resize);
|
var username = "coffee-user-"+ Math.random();
|
||||||
updateTime();
|
var client = new Paho.MQTT.Client("sika.sahkoinsinoorikilta.fi", 9001, username);
|
||||||
setInterval(updateTime,1000);
|
client.onMessageArrived = function (message) {
|
||||||
formatBrewTime();
|
console.log("Topic: "+message.destinationName+" msg: "+message.payloadString);
|
||||||
setInterval(formatBrewTime,10000);
|
var ev = new CustomEvent(message.destinationName, {'detail': message.payloadString});
|
||||||
});
|
window.dispatchEvent(ev);
|
||||||
|
}
|
||||||
|
|
||||||
$(window).resize(resize);
|
function reconnect(responseObject){
|
||||||
|
if (responseObject.errorCode !== 0) {
|
||||||
function fetchdata(data, status){
|
console.log("connection lost! Reason: "+responseObject.errorMessage);
|
||||||
if (typeof status !== 'undefined'){
|
setTimeout(function(){
|
||||||
if (status == "success"){
|
client.connect({onSuccess:onConnect, useSSL:true, onFailure: reconnect});
|
||||||
parseData(data);
|
}, backoff);
|
||||||
}
|
}
|
||||||
else if (status == "error"){
|
|
||||||
handleError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$.getJSON("/coffee/cups", fetchdata);
|
function onConnect() {
|
||||||
setInterval(function() {
|
console.log("MQTT connected");
|
||||||
$.getJSON("/coffee/cups", fetchdata);
|
//set and reset reconnector
|
||||||
}, 2000);
|
client.onConnectionLost = reconnect
|
||||||
|
// subscribe to topics
|
||||||
function formatBrewTime(){
|
client.subscribe("sik/kiltahuone/kahvivaaka/cups");
|
||||||
if (!brewtext && lastBrew instanceof Date){
|
client.subscribe("sik/kiltahuone/kahvivaaka/brewing");
|
||||||
var now = new Date();
|
client.subscribe("sik/kiltahuone/kahvivaaka/brewtime");
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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() {
|
function tick(){
|
||||||
setData("?", 0, 0, Number.MAX_VALUE, Number.MAX_VALUE);
|
var ev = new CustomEvent("tick", {'detail': new Date()});
|
||||||
|
window.dispatchEvent(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseData(data) {
|
function updateTime(ev){
|
||||||
if (data) {
|
var now = ev.detail;
|
||||||
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();
|
|
||||||
$("#time").html(formatTime(now.getHours(),now.getMinutes(),now.getSeconds()));
|
$("#time").html(formatTime(now.getHours(),now.getMinutes(),now.getSeconds()));
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(hours, minutes, seconds){
|
function coffeeLowEffect(ev){
|
||||||
var str = "";
|
ev.detail <= 2 ? $("#text").addClass("hurry") : $("#text").removeClass("hurry");
|
||||||
|
|
||||||
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 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(){
|
function resize(){
|
||||||
var w = $("#container").width();
|
var w = $("#container").width();
|
||||||
var h = $("#container").height();
|
var h = $("#container").height();
|
||||||
var s = w > h ? h : w;
|
var s = w > h ? h : w;
|
||||||
var font = s*0.8*0.38/Math.sqrt(len);
|
var font = s * 0.8 * 0.38/Math.sqrt(3);
|
||||||
$("#text").css({ top: s*0.16-font/2 + 'px', fontSize: font + 'px', marginLeft: -font*len*3/10 + 'px'});
|
$("#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);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
<meta http-equiv="pragma" content="no-cache" />
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.2/mqttws31.js"
|
||||||
|
type="text/javascript"></script>
|
||||||
<link rel="stylesheet" href="/static/css/coffee.css" />
|
<link rel="stylesheet" href="/static/css/coffee.css" />
|
||||||
<script src="/static/js/coffee.js"></script>
|
<script src="/static/js/coffee.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,30 +1,2 @@
|
|||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
from django.conf import settings
|
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)
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.views.generic.base import RedirectView
|
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)
|
favicon_view = RedirectView.as_view(url='static/img/favicon.ico', permanent=True)
|
||||||
|
|
||||||
@@ -9,6 +9,4 @@ urlpatterns = [
|
|||||||
|
|
||||||
# landing page
|
# landing page
|
||||||
url(r'^$', coffee_view),
|
url(r'^$', coffee_view),
|
||||||
url(r'^cups', cups_view),
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,25 +3,9 @@ from django.http import JsonResponse
|
|||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from .mqtt import get_latest
|
|
||||||
import coffee_scale.mqtt # somehow this is needed
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
def coffee_view(request):
|
def coffee_view(request):
|
||||||
return render(request, 'coffee.html')
|
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)
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ dealer==2.0.5
|
|||||||
django-modeltranslation==0.12.1
|
django-modeltranslation==0.12.1
|
||||||
django-auditlog==0.4.3
|
django-auditlog==0.4.3
|
||||||
django-phonenumber-field==1.3.0
|
django-phonenumber-field==1.3.0
|
||||||
paho-mqtt==1.3.0
|
|
||||||
django-autocomplete-light==3.2.10
|
django-autocomplete-light==3.2.10
|
||||||
six==1.10.0
|
six==1.10.0
|
||||||
django-suit==0.2.25
|
django-suit==0.2.25
|
||||||
|
|||||||
+1
-1
@@ -78,7 +78,7 @@ INSTALLED_APPS = [
|
|||||||
'webapp',
|
'webapp',
|
||||||
'members',
|
'members',
|
||||||
'infoscreen',
|
'infoscreen',
|
||||||
'coffee_scale.apps.CoffeeScaleConfig',
|
'coffee_scale',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'django_nose',
|
'django_nose',
|
||||||
'bootstrap3',
|
'bootstrap3',
|
||||||
|
|||||||
Reference in New Issue
Block a user