Move coffee scale to web2.0
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from . import mqtt
|
||||
|
||||
mqtt.client.loop_start()
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoffeeScaleConfig(AppConfig):
|
||||
name = 'coffee_scale'
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -0,0 +1,65 @@
|
||||
import paho.mqtt.client as mqtt
|
||||
import logging
|
||||
import datetime
|
||||
from collections import deque
|
||||
|
||||
TOPIC_TEMP = "sik/kiltahuone/kahvivaaka/temperature"
|
||||
TOPIC_WEIGHT = "sik/kiltahuone/kahvivaaka/weight"
|
||||
|
||||
BREWING_DIFFERENTIAL = 60
|
||||
# setting down the pan creates at least 800 g of weight
|
||||
BREWING_DIFFERENTIAL_ERROR_MAX = 800
|
||||
WEIGHT_DEQUE_LENGTH = 20
|
||||
weight_deque = deque(maxlen=WEIGHT_DEQUE_LENGTH)
|
||||
latest = {
|
||||
'last_brew': datetime.datetime.now()
|
||||
}
|
||||
|
||||
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
logging.info('Subscribing to all topics on mqtt.sik.party.')
|
||||
client.subscribe("#")
|
||||
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
if msg.topic == TOPIC_TEMP:
|
||||
latest['temp'] = float(msg.payload.decode('utf-8'))
|
||||
elif msg.topic == TOPIC_WEIGHT:
|
||||
weight = float(msg.payload.decode('utf-8'))
|
||||
latest['weight'] = weight
|
||||
weight_deque.appendleft(weight)
|
||||
|
||||
|
||||
def on_disconnect(client, userdata, rc):
|
||||
client.loop_stop(force=False)
|
||||
if rc != 0:
|
||||
print("Unexpected disconnection.")
|
||||
else:
|
||||
print("Disconnected")
|
||||
|
||||
|
||||
def get_latest():
|
||||
if len(weight_deque) > 2:
|
||||
first = weight_deque[0]
|
||||
last = weight_deque[len(weight_deque) - 1]
|
||||
diff = first - last # reverse order
|
||||
|
||||
if len(weight_deque) < WEIGHT_DEQUE_LENGTH:
|
||||
brewing = False
|
||||
else:
|
||||
if BREWING_DIFFERENTIAL < diff < BREWING_DIFFERENTIAL_ERROR_MAX:
|
||||
brewing = True
|
||||
latest['last_brew'] = datetime.datetime.now()
|
||||
else:
|
||||
brewing = False
|
||||
|
||||
latest['brewing'] = brewing
|
||||
return latest
|
||||
|
||||
|
||||
client = mqtt.Client()
|
||||
client.on_connect = on_connect
|
||||
client.on_message = on_message
|
||||
client.on_disconnect = on_disconnect
|
||||
|
||||
client.connect("mqtt.sik.party", 1883, 60)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
@@ -0,0 +1,292 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Coffee Cups @Guild Room - AYY SIK RY</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="3600">
|
||||
<meta http-equiv="cache-control" content="max-age=0" />
|
||||
<meta http-equiv="cache-control" content="no-cache" />
|
||||
<meta http-equiv="expires" content="0" />
|
||||
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
background-color: white;
|
||||
font-family: monospace;
|
||||
color: black;
|
||||
}
|
||||
#container{
|
||||
position:relative;
|
||||
width:95%;
|
||||
margin-left:auto;
|
||||
margin-right:auto;
|
||||
height:100%;
|
||||
overflow:hidden;
|
||||
|
||||
}
|
||||
#upper{
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom center;
|
||||
background-image: url("/static/img/smokes.png");
|
||||
transform-origin: bottom;
|
||||
animation: smokes 8s ease-in-out 0s infinite;
|
||||
opacity:0;
|
||||
height:40%;
|
||||
}
|
||||
#lower{
|
||||
position:relative;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: top center;
|
||||
background-image: url("/static/img/coffeecup3.png");
|
||||
height:60%;
|
||||
}
|
||||
#scale{
|
||||
position:absolute;
|
||||
top:80%;
|
||||
width:90%;
|
||||
height:10%;
|
||||
margin: 0% 5% 0% 5%;
|
||||
background: lightgrey;
|
||||
border-radius: 10px;
|
||||
overflow:hidden;
|
||||
}
|
||||
#scale2{
|
||||
width: 0%;
|
||||
transition: width 2s;
|
||||
height:100%;
|
||||
background: green;
|
||||
border-radius: 10px;
|
||||
}
|
||||
#brewtime{
|
||||
text-align:right;
|
||||
position:absolute;
|
||||
right:0px;
|
||||
z-index:5;
|
||||
font-size:10vw;
|
||||
}
|
||||
#address{
|
||||
text-align:left;
|
||||
position:absolute;
|
||||
left:0px;
|
||||
z-index:5;
|
||||
font-size:4vw;
|
||||
color: #333;
|
||||
}
|
||||
noscript{
|
||||
color:red;
|
||||
}
|
||||
#text{
|
||||
color:green;
|
||||
position:absolute;
|
||||
top:50%;
|
||||
left:50%;
|
||||
}
|
||||
.brewing{
|
||||
animation: brewing 5s ease-in-out 0s infinite;
|
||||
}
|
||||
.hurry{
|
||||
color:red !important;
|
||||
}
|
||||
.unknown{
|
||||
color:orange !important;
|
||||
animation: unknown 5s ease-in-out 0s infinite;
|
||||
}
|
||||
.friday{
|
||||
animation: friday 20s ease-in-out 0s infinite;
|
||||
}
|
||||
.normal{
|
||||
animation: normal 1000s ease-in-out 0s infinite;
|
||||
}
|
||||
.coffeeready{
|
||||
animation: coffeeready 10s ease-in-out 0s;
|
||||
}
|
||||
@keyframes smokes {
|
||||
0% {transform: skewX(-10deg);}
|
||||
50% {transform: skewX(10deg);}
|
||||
100% {transform: skewX(-10deg);}
|
||||
}
|
||||
@keyframes brewing {
|
||||
0% {color:green;}
|
||||
50% {color: transparent;}
|
||||
100% {color:green;}
|
||||
}
|
||||
@keyframes coffeeready {
|
||||
0% {background-color:white;}
|
||||
25% {background-color:green;}
|
||||
50% {background-color:white;}
|
||||
75% {background-color:green;}
|
||||
100% {background-color:white;}
|
||||
}
|
||||
@keyframes unknown {
|
||||
0%,40% {transform: rotate(0deg);}
|
||||
60%,100% {transform: rotate(360deg);}
|
||||
}
|
||||
@keyframes friday {
|
||||
0% {transform: rotate(0deg);}
|
||||
100% {transform: rotate(360deg);}
|
||||
}
|
||||
@keyframes normal {
|
||||
0%,49% {transform: rotate(0deg);}
|
||||
50%,100% {transform: rotate(360deg);}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var len = 0;
|
||||
var lastBrew = "∞";
|
||||
var brewtext = "";
|
||||
|
||||
$(document).ready(function(){
|
||||
$('#text').bind("DOMSubtreeModified", resize);
|
||||
fetchdata();
|
||||
updateTime();
|
||||
setInterval(updateTime,1000);
|
||||
formatBrewTime();
|
||||
setInterval(formatBrewTime,10000);
|
||||
});
|
||||
|
||||
$(window).resize(resize);
|
||||
|
||||
var defaultBackoffTime = 1000;
|
||||
var modified = 0;
|
||||
function fetchdata(hxr, status){
|
||||
var backoffTime = defaultBackoffTime;
|
||||
modified = 1;
|
||||
if(typeof status !== 'undefined'){
|
||||
if(status == "success"){
|
||||
backoffTime = defaultBackoffTime;
|
||||
parseData(hxr.responseText);
|
||||
}
|
||||
else if(status == "error"){
|
||||
backoffTime *= 2;
|
||||
modified = 0
|
||||
setTimeout(fetchdata, backoffTime);
|
||||
handleError();
|
||||
return;
|
||||
}
|
||||
}
|
||||
setTimeout(function() {
|
||||
$.ajax({cache: false, ifModified: modified, url: '/coffee/cups', complete: fetchdata, timeout:120000});
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function formatBrewTime(){
|
||||
if(!brewtext && lastBrew instanceof Date){
|
||||
var now = new Date();
|
||||
var timeDiff = Math.max(now.getTime()-lastBrew.getTime(),0);
|
||||
if(timeDiff < 3600000)
|
||||
tmp = Math.round(timeDiff/60000) + ' min';
|
||||
else
|
||||
tmp = '~' + Math.round(timeDiff/3600000*2)/2 + ' h';
|
||||
$("#brewtime").html(tmp);
|
||||
}else
|
||||
$("#brewtime").html(brewtext);
|
||||
}
|
||||
|
||||
function handleError(){
|
||||
setData("?",0,0,Number.MAX_VALUE,Number.MAX_VALUE);
|
||||
}
|
||||
|
||||
function parseData(string) {
|
||||
var data = JSON.parse(string);
|
||||
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){
|
||||
var now = new Date();
|
||||
if(cups == 0)
|
||||
opa = 0;
|
||||
brewtext = "";
|
||||
$("#upper").css({opacity: opa/100});
|
||||
$("#scale2").css({width: Math.min(cups/9*100,100) + '%'});
|
||||
$("#text,body").removeClass();
|
||||
|
||||
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");
|
||||
len = cups.toString().length;
|
||||
$("#text").html(cups);
|
||||
}
|
||||
|
||||
function updateTime(){
|
||||
var now = new Date();
|
||||
$("#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 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'});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="container">
|
||||
<span id="brewtime"></span>
|
||||
<span id="address">
|
||||
Kahvivaaka
|
||||
<noscript><br>This page uses JavaScript!</noscript>
|
||||
<br><span id="time"></span></span>
|
||||
</span>
|
||||
<div id="upper">
|
||||
</div>
|
||||
<!--Kahvinkeitin on rikki. Varakeittimellä keitettyä kahvia saattaa olla.-->
|
||||
<div id="lower" class="normal">
|
||||
<div id="text"></div>
|
||||
<div id="scale"><div id="scale2"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.conf.urls import url
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
from .views import coffee_view, cups_view
|
||||
|
||||
favicon_view = RedirectView.as_view(url='static/img/favicon.ico', permanent=True)
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# landing page
|
||||
url(r'^$', coffee_view),
|
||||
url(r'^cups', cups_view),
|
||||
|
||||
]
|
||||
@@ -0,0 +1,40 @@
|
||||
from django.shortcuts import render
|
||||
from django.http import JsonResponse
|
||||
|
||||
import datetime
|
||||
from .mqtt import get_latest
|
||||
|
||||
|
||||
def get_cups_from_weight(weight):
|
||||
if not weight:
|
||||
return 0
|
||||
|
||||
EMPTY = 820 # grams
|
||||
FULL = 2000 # grams
|
||||
cups = 10 * (weight - EMPTY) / (FULL - EMPTY)
|
||||
|
||||
cups = round(cups, 1)
|
||||
if cups < 0:
|
||||
cups = 0
|
||||
if cups > 10:
|
||||
cups = 10
|
||||
|
||||
return cups
|
||||
|
||||
|
||||
def coffee_view(request):
|
||||
return render(request, 'coffee.html')
|
||||
|
||||
|
||||
def cups_view(request):
|
||||
now = datetime.datetime.now()
|
||||
latest = get_latest()
|
||||
cups = get_cups_from_weight(latest.get('weight'))
|
||||
data = {
|
||||
'date': now,
|
||||
'cups': cups,
|
||||
'temp': latest.get('temp'),
|
||||
'last_brew': latest.get('last_brew'),
|
||||
'brewing': latest.get('brewing')
|
||||
}
|
||||
return JsonResponse(data)
|
||||
@@ -25,3 +25,4 @@ 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
|
||||
|
||||
@@ -50,6 +50,7 @@ INSTALLED_APPS = [
|
||||
'webapp',
|
||||
'members',
|
||||
'infoscreen',
|
||||
'coffee_scale',
|
||||
'rest_framework',
|
||||
'django_nose',
|
||||
'bootstrap3',
|
||||
|
||||
@@ -25,11 +25,13 @@ from django.contrib.staticfiles import views as static_views
|
||||
import webapp.urls
|
||||
import infoscreen.urls
|
||||
import members.urls
|
||||
import coffee_scale.urls
|
||||
|
||||
urlpatterns = [
|
||||
url(r'', include('webapp.urls')),
|
||||
url(r'^members/', include('members.urls')),
|
||||
url(r'^infoscreen/', include('infoscreen.urls')),
|
||||
url(r'^coffee/', include('coffee_scale.urls')),
|
||||
|
||||
# admin
|
||||
url(r'^admin/', admin.site.urls),
|
||||
|
||||
Reference in New Issue
Block a user