Merge branch 'feature-coffee-upgrade' into 'develop'

Feature coffee upgrade

See merge request vtmk/web2.0!92
This commit is contained in:
Jan Tuomi
2017-11-01 13:14:44 +02:00
10 changed files with 153 additions and 247 deletions
-23
View File
@@ -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))
-58
View File
@@ -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
+7 -4
View File
@@ -47,7 +47,7 @@ body {
background: green;
border-radius: 10px;
}
#brewtime{
.brewtime{
text-align:right;
position:absolute;
right:0px;
@@ -62,10 +62,13 @@ body {
font-size:4vw;
color: #333;
}
.layertwo{
display: None;
}
noscript{
color:red;
}
#text{
.text{
color:green;
position:absolute;
top:50%;
@@ -102,9 +105,9 @@ noscript{
}
@keyframes coffeeready {
0% {background-color:white;}
25% {background-color:green;}
25% {background-color:rgb(100, 255, 100);}
50% {background-color:white;}
75% {background-color:green;}
75% {background-color:rgb(100, 255, 100);}
100% {background-color:white;}
}
@keyframes unknown {
+138 -111
View File
@@ -1,130 +1,157 @@
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();
// eslint-disable-next-line no-undef
var client = new Paho.MQTT.Client("sika.sahkoinsinoorikilta.fi", 9001, username);
client.onMessageArrived = function (message) {
// eslint-disable-next-line no-console
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); // eslint-disable-line no-console
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"); // eslint-disable-line no-console
//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 = "&infin;";
$("#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(){
$("body").addClass("coffeeready");
// autoclear animation class in 10s
setTimeout(function(){$("body").removeClass("coffeeready");}, 10000);
}
function hotEffect(ev){
var opa = Math.max(100 - ev.detail / 90000,0);
$("#upper").css({opacity: opa/100});
}
function brewAnimStart(){
$(".text").addClass("brewing");
$(".layerone").hide();
$(".layertwo").show();
}
function brewAnimEnd(){
$(".text").removeClass("brewing");
$(".layertwo").hide();
$(".layerone").show();
}
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(){
var now = new Date();
var timeDiff = Math.max(now.getTime() - lastBrew.getTime(), 0);
var eve = new CustomEvent("dtUpdate", {'detail': timeDiff});
window.dispatchEvent(eve);
}
function updateBrewTime(ev){
var timeDiff = ev.detail;
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);
window.addEventListener("dtUpdate", updateBrewTime);
window.addEventListener("dtUpdate", hotEffect);
//start time based events
setInterval(tick, 100);
tick();
});
$(window).resize(resize);
+6 -2
View File
@@ -10,13 +10,16 @@
<meta http-equiv="pragma" content="no-cache" />
<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://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.2/mqttws31.js"
type="text/javascript"></script>
<link rel="stylesheet" href="/static/css/coffee.css" />
<script src="/static/js/coffee.js"></script>
</head>
<body>
<div id="container">
<span id="brewtime"></span>
<span id="brewtime" class="brewtime layerone"></span>
<span class="brewtime layertwo">:)</span>
<span id="address">
ka.dy.fi
<noscript><br>This page uses JavaScript!</noscript>
@@ -26,7 +29,8 @@
</div>
<!--Kahvinkeitin on rikki. Varakeittimellä keitettyä kahvia saattaa olla.-->
<div id="lower" class="normal">
<div id="text"></div>
<div id="text" class="text layerone"></div>
<div class="text layertwo">&nbsp;+</div>
<div id="scale"><div id="scale2"></div></div>
</div>
</div>
-28
View File
@@ -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)
+1 -3
View File
@@ -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),
]
-16
View File
@@ -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)
-1
View File
@@ -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
+1 -1
View File
@@ -78,7 +78,7 @@ INSTALLED_APPS = [
'webapp',
'members',
'infoscreen',
'coffee_scale.apps.CoffeeScaleConfig',
'coffee_scale',
'rest_framework',
'django_nose',
'bootstrap3',