diff --git a/coffee_scale/mqtt.py b/coffee_scale/mqtt.py index 3a984fb..ee49cd7 100644 --- a/coffee_scale/mqtt.py +++ b/coffee_scale/mqtt.py @@ -3,74 +3,52 @@ import logging import datetime from collections import deque -MQTT_HOST = "mqtt.sik.party" +from django.conf import settings -TOPIC_TEMP = "sik/kiltahuone/kahvivaaka/temperature" -TOPIC_WEIGHT = "sik/kiltahuone/kahvivaaka/weight" - -BREWING_DIFFERENTIAL_MIN = 60 -# setting down the pan creates at least 800 g of weight -BREWING_DIFFERENTIAL_MAX = 180 - -WEIGHT_DEQUE_LENGTH = 20 -weight_deque = deque(maxlen=WEIGHT_DEQUE_LENGTH) -latest = { - 'last_brew': datetime.datetime.now() -} - - -def calc_averaged_weight(): - if len(weight_deque) > 2: - half = [] - for i in range(0, int(len(weight_deque) / 2)): - half.append(weight_deque[i]) - - return sum(half) / len(half) - else: - return 0 +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('Subscribing to all topics on mqtt.sik.party.') - client.subscribe("#") + 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): - 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')) - - # avoid showing erroneous data from an empty scale - if weight > 50: # g - weight_deque.appendleft(weight) + 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.") + logging.warning('MQTT unexpectedly disconnected.') else: client.loop_stop(force=False) - logging.warning("MQTT disconnected.") + logging.warning('MQTT 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_MIN < diff < BREWING_DIFFERENTIAL_MAX: - brewing = True - latest['last_brew'] = datetime.datetime.now() - else: - brewing = False - - latest['brewing'] = brewing - latest['weight'] = calc_averaged_weight() return latest @@ -81,9 +59,9 @@ client.on_disconnect = on_disconnect try: - logging.info('Connecting to MQTT at {}...'.format(MQTT_HOST)) - client.connect_async(MQTT_HOST, 1883, 60) - logging.info('Connected successfully to MQTT.') + logging.info('Connecting to MQTT at {}...'.format(HOST)) + logging.info('If there is no confirmation, the MQTT connection has probably failed.') + client.connect_async(HOST, PORT, 60) except Exception as ex: logging.error(ex) - logging.error('Failed to connect to MQTT at {}'.format(MQTT_HOST)) + logging.error('Failed to connect to MQTT at {}'.format(HOST)) diff --git a/coffee_scale/views.py b/coffee_scale/views.py index fcfbc50..4d226e3 100644 --- a/coffee_scale/views.py +++ b/coffee_scale/views.py @@ -9,24 +9,6 @@ import logging from django.conf import settings -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.5: - cups = 0 - if cups > 10: - cups = 10 - - # logging.debug("Coffee cups: {}, weight: {}".format(cups, weight)) - return cups - - def coffee_view(request): logging.info('User navigated to coffee page!') return render(request, 'coffee.html') @@ -35,12 +17,11 @@ def coffee_view(request): 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') + 'cups': latest.get('cups'), + 'last_brew': latest.get('brew_time'), + 'brewing': latest.get('brewing'), + 'weight': latest.get('weight') } return JsonResponse(data) diff --git a/sikweb/base.py b/sikweb/base.py new file mode 100644 index 0000000..1e3c808 --- /dev/null +++ b/sikweb/base.py @@ -0,0 +1,245 @@ +import os +import logging +from os.path import expanduser +from django.utils.translation import ugettext_lazy as _ + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +IS_DOCKER = bool(os.getenv('IS_DOCKER', None)) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + + +SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' + +if not IS_DOCKER: + ALLOWED_HOSTS = [] +else: + ALLOWED_HOSTS = ["*"] + +# Logger level + +LOGGERLEVEL = logging.DEBUG +LOGPATH = os.path.join(BASE_DIR, "logs", "debug.log") + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(module)s: %(message)s' + }, + }, + 'handlers': { + 'file': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': LOGPATH, + 'formatter': 'verbose', + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + }, + 'root': { + 'handlers': ['file', 'console'], + 'level': 'DEBUG', + 'propagate': True, + }, + 'loggers': { + 'django': { + 'handlers': ['file', 'console'], + 'level': 'WARNING', + 'propagate': True, + }, + }, +} + + +# Application definition + +INSTALLED_APPS = [ + 'modeltranslation', # has to be before admin for translation admin to work + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'corsheaders', + 'webapp', + 'members', + 'infoscreen', + 'coffee_scale', + 'rest_framework', + 'django_nose', + 'bootstrap3', + 'django_tables2', + 'auditlog', + 'phonenumber_field', +] + +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + +NOSE_ARGS = [ + '--with-coverage', + '--cover-package=webapp,members,infoscreen', + '--exclude-dir={}'.format(os.path.join(BASE_DIR, 'members', 'migrations')), + '--exclude-dir={}'.format(os.path.join(BASE_DIR, + 'infoscreen', 'migrations')), + '--exclude-dir={}'.format(os.path.join(BASE_DIR, 'webapp', 'migrations')), +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'auditlog.middleware.AuditlogMiddleware' +] +CORS_ORIGIN_ALLOW_ALL = True + +ROOT_URLCONF = 'sikweb.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': ['templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.template.context_processors.i18n', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'django.template.context_processors.static', + 'dealer.contrib.django.context_processor', + ], + }, + }, +] + +WSGI_APPLICATION = 'sikweb.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases + +if not IS_DOCKER: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'sik', + 'USER': 'sik', + 'PASSWORD': 'password123', + 'HOST': 'localhost', + 'PORT': '5432', + 'TEST': { + 'NAME': 'sik_test', + }, + }, + } +else: + logging.info('Using docker database configuration') + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'postgres', + 'USER': 'postgres', + 'PASSWORD': 'postgres', + 'HOST': 'db', + 'PORT': '5432', + }, + } +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.' + 'UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.' + 'MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.' + 'CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.' + 'NumericPasswordValidator', + }, +] + +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + 'rest_framework.permissions.DjangoModelPermissions', + 'rest_framework.permissions.IsAdminUser', + ), + 'DEFAULT_THROTTLE_CLASSES': ( + 'members.throttles.BurstRateThrottle', + 'members.throttles.SustainedRateThrottle' + ), + 'DEFAULT_THROTTLE_RATES': { + 'burst': '60/min', + 'sustained': '1000/day' + }, +} + +# Email settings (tested working with gmail) +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_USE_TLS = True +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_PORT = 587 + + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ + +LANGUAGES = ( + ('fi', _('Finnish')), + ('en', _('English')), +) + +LANGUAGE_CODE = 'fi' + +LOCALE_PATHS = ( + os.path.join(BASE_DIR, 'locale'), +) + +TIME_ZONE = 'Europe/Helsinki' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'django.contrib.staticfiles.finders.FileSystemFinder', +) +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static') +STATICFILES_DIRS = ( + os.path.join(BASE_DIR, 'global_static'), +) +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = '/media/' diff --git a/sikweb/settings-sample.py b/sikweb/settings-sample.py index 9ad9d7d..a2cca87 100644 --- a/sikweb/settings-sample.py +++ b/sikweb/settings-sample.py @@ -11,264 +11,55 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.9/ref/settings/ """ -import os -import logging -from os.path import expanduser -from django.utils.translation import ugettext_lazy as _ - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -IS_DOCKER = bool(os.getenv('IS_DOCKER', None)) - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '7p$85^4ibb^p4-=vs44b7!y0e-zemugze18@a#30&71=a8)dp(' +from sikweb.base import * # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -if not IS_DOCKER: - ALLOWED_HOSTS = [] -else: - ALLOWED_HOSTS = ["*"] +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '7p$85^4ibb^p4-=vs44b7!y0e-zemugze18@a#30&71=a8)dp(' -# Logger level +# HSL API settings +HSL_USERHASH = 'YOUR HSL USERHASH HERE' +HSL_DEPARTURE_THRESHOLD = 8 # minutes +HSL_HURRY_THRESHOLD = 13 # minutes -LOGGERLEVEL = logging.DEBUG -LOGPATH = os.path.join(BASE_DIR, "logs", "debug.log") - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '%(levelname)s %(asctime)s %(module)s: %(message)s' - }, - }, - 'handlers': { - 'file': { - 'level': 'DEBUG', - 'class': 'logging.FileHandler', - 'filename': LOGPATH, - 'formatter': 'verbose', - }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'verbose', - }, - }, - 'root': { - 'handlers': ['file', 'console'], - 'level': 'DEBUG', - 'propagate': True, - }, - 'loggers': { - 'django': { - 'handlers': ['file', 'console'], - 'level': 'WARNING', - 'propagate': True, - }, - }, +# MQTT settings +MQTT_SETTINGS = { + 'HOST': 'mqtt.sik.party', + 'PORT': 1883, + 'TOPICS': { + 'BREW_TIME': 'sik/kiltahuone/kahvivaaka/brewtime', + 'WEIGHT': 'sik/kiltahuone/kahvivaaka/weight', + 'BREWING': 'sik/kiltahuone/kahvivaaka/brewing', + 'CUPS': 'sik/kiltahuone/kahvivaaka/cups', + } } +# ReCaptcha +# http://www.yaconiello.com/blog/integrating-google-recaptcha-to-django/ +GOOGLE_RECAPTCHA_SITE_KEY = "YOUR-PUBLIC-KEY" +GOOGLE_RECAPTCHA_SECRET_KEY = "YOUR-PRIVATE-KEY" -# Application definition - -INSTALLED_APPS = [ - 'modeltranslation', # has to be before admin for translation admin to work - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'corsheaders', - 'webapp', - 'members', - 'infoscreen', - 'coffee_scale', - 'rest_framework', - 'django_nose', - 'bootstrap3', - 'django_tables2', - 'auditlog', - 'phonenumber_field', -] - -TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' - -NOSE_ARGS = [ - '--with-coverage', - '--cover-package=webapp,members,infoscreen', - '--exclude-dir={}'.format(os.path.join(BASE_DIR, 'members', 'migrations')), - '--exclude-dir={}'.format(os.path.join(BASE_DIR, - 'infoscreen', 'migrations')), - '--exclude-dir={}'.format(os.path.join(BASE_DIR, 'webapp', 'migrations')), -] - -MIDDLEWARE_CLASSES = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'auditlog.middleware.AuditlogMiddleware' -] -CORS_ORIGIN_ALLOW_ALL = True - -ROOT_URLCONF = 'sikweb.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': ['templates'], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.template.context_processors.i18n', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'django.template.context_processors.static', - 'dealer.contrib.django.context_processor', - ], - }, - }, -] - -WSGI_APPLICATION = 'sikweb.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/1.9/ref/settings/#databases - -if not IS_DOCKER: - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'sik', - 'USER': 'sik', - 'PASSWORD': 'password123', - 'HOST': 'localhost', - 'PORT': '5432', - 'TEST': { - 'NAME': 'sik_test', - }, - }, - } -else: - logging.info('Using docker database configuration') - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'postgres', - 'USER': 'postgres', - 'PASSWORD': 'postgres', - 'HOST': 'db', - 'PORT': '5432', - }, - } -# Password validation -# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.' - 'UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.' - 'MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.' - 'CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.' - 'NumericPasswordValidator', - }, -] - -REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', - 'rest_framework.permissions.DjangoModelPermissions', - 'rest_framework.permissions.IsAdminUser', - ), - 'DEFAULT_THROTTLE_CLASSES': ( - 'members.throttles.BurstRateThrottle', - 'members.throttles.SustainedRateThrottle' - ), - 'DEFAULT_THROTTLE_RATES': { - 'burst': '60/min', - 'sustained': '1000/day' - }, -} - -# Email settings (tested working with gmail) -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_USE_TLS = True -EMAIL_HOST = 'smtp.gmail.com' -EMAIL_PORT = 587 +# Email settings (more settings in base.py) EMAIL_HOST_USER = '@gmail.com' EMAIL_HOST_PASSWORD = '' DEFAULT_EMAIL_FROM = 'SIK Viestintä ' ENABLE_AUTOMATIC_EMAILS = False -# ReCaptcha -# http://www.yaconiello.com/blog/integrating-google-recaptcha-to-django/ +# Database settings +# Only uncomment if default settings in base.py are not ok -GOOGLE_RECAPTCHA_SITE_KEY = "YOUR-PUBLIC-KEY" -GOOGLE_RECAPTCHA_SECRET_KEY = "YOUR-PRIVATE-KEY" - -# Internationalization -# https://docs.djangoproject.com/en/1.9/topics/i18n/ - -LANGUAGES = ( - ('fi', _('Finnish')), - ('en', _('English')), -) - -LANGUAGE_CODE = 'fi' - -LOCALE_PATHS = ( - os.path.join(BASE_DIR, 'locale'), -) - -TIME_ZONE = 'Europe/Helsinki' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.9/howto/static-files/ -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - 'django.contrib.staticfiles.finders.FileSystemFinder', -) -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'static') -STATICFILES_DIRS = ( - os.path.join(BASE_DIR, 'global_static'), -) -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -MEDIA_URL = '/media/' - -HSL_USERHASH = 'YOUR HSL USERHASH HERE' -HSL_DEPARTURE_THRESHOLD = 8 -HSL_HURRY_THRESHOLD = 13 +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.postgresql_psycopg2', +# 'NAME': 'sik', +# 'USER': 'sik', +# 'PASSWORD': 'password123', +# 'HOST': 'localhost', +# 'PORT': '5432', +# 'TEST': { +# 'NAME': 'sik_test', +# }, +# }, +# }