Skip to content

Commit

Permalink
External Push Support
Browse files Browse the repository at this point in the history
* Add remote logging (external push) support (#277)
* Remove the constant polling for LCD refreshes (#278)
* Add "NewCCModelForm" for "modern" controllers (#282)
* Support debugging connection for ports other than 23 (Fixes #279)
* Add support for temperature calibration offsets (Closes #281)
* Update documentation
* Catch ObjectNotFound errors generated by the iSpindle calibration API
* Fix link to favicon (Fixes #313)
* Remove error not available in Python 3.4 (Fixes #321)
* Catch error when API called for nonexistant gravity sensor (Fixes #330)
* Map "Log1Temp" to "RoomTemp" value
  • Loading branch information
thorrak committed Feb 17, 2019
2 parents a3745f2 + 290ccfd commit 94bad39
Show file tree
Hide file tree
Showing 29 changed files with 871 additions and 37 deletions.
6 changes: 0 additions & 6 deletions app/api/lcd.py
@@ -1,6 +1,3 @@
from django.shortcuts import render
from django.contrib import messages
from django.shortcuts import render_to_response, redirect
from django.http import JsonResponse
from django.urls import reverse
from constance import config
Expand Down Expand Up @@ -33,7 +30,6 @@ def getLCD(req, device_id):

def getPanel(req, device_id):

# Don't repeat yourself...
def temp_text(temp, temp_format):
if temp == 0:
return "--° {}".format(temp_format)
Expand All @@ -51,7 +47,6 @@ def temp_text(temp, temp_format):
'log_interval': 0})
return JsonResponse(ret, safe=False, json_dumps_params={'indent': 4})


if device_info['Mode'] == "o":
device_mode = "Off"
elif device_info['Mode'] == "f":
Expand All @@ -73,7 +68,6 @@ def temp_text(temp, temp_format):
if int(device_info['LogInterval']) >= (60*60*2):
interval_text += "s" # IT WORKS. QUIT JUDGING.


ret.append({'beer_temp': temp_text(device_info['BeerTemp'], dev.temp_format),
'fridge_temp': temp_text(device_info['FridgeTemp'], dev.temp_format),
'room_temp': temp_text(device_info['RoomTemp'], dev.temp_format),
Expand Down
22 changes: 22 additions & 0 deletions app/templates/site_config.html
Expand Up @@ -18,6 +18,28 @@ <h3 class="form-header">Configuration Tools</h3>
{# Setting this as an include so we can reuse it on our site settings page #}
{% include 'setup/constance_app_configuration.html' %}

<h3 class="form-header">External Push Targets</h3>

{% if all_push_targets.count > 0 %}
<ul class="list-group list-group-flush">
{% for push_target in all_push_targets %}
<!-- GenericPushTarget Line -->
<li class="list-group-item">
<div class="row">
<div class="col-sm-12 col-md-6"><a href="{% url "external_push_view" push_target.id %}">{{ push_target.name }}</a></div>
<div class="col-sm-2 col-md-2">{{ push_target.status }}</div>
<div class="col-sm-10 col-md-4">{{ push_target.target_host }}</div>
</div>
</li>
<!-- End GenericPushTarget Line -->
{% endfor %}
</ul>
{% endif %}

<p>
<a href="{% url 'external_push_generic_target_add' %}" class="btn btn-large btn-lg btn-primary">Add Push Target</a>
</p>



{% endblock %}
Expand Down
3 changes: 2 additions & 1 deletion app/templates/sitewide/flat_ui_template.html
Expand Up @@ -10,7 +10,8 @@
<link href="{% static "vendor/flat-ui/css/flat-ui.min.css" %}" rel="stylesheet">
<link href="{% static "vendor/font-awesome/css/font-awesome.min.css" %}" rel="stylesheet">
<link href="{% static "css/custom_style.css" %}" rel="stylesheet">
<link rel="shortcut icon" href="img/favicon.ico">
<link rel="shortcut icon" href="{% static "favicon.ico" %}">
<link rel="icon" href="{% static "favicon.ico" %}">
{% block header_scripts %}{% endblock %}
{# TODO - Move dygraph out to only the pages that use it #}
<script type="text/javascript" src="{% static "vendor/dygraph/js/dygraph.min.js" %}"></script>
Expand Down
13 changes: 5 additions & 8 deletions app/views.py
Expand Up @@ -18,6 +18,7 @@


from app.models import BrewPiDevice, OldControlConstants, NewControlConstants, PinDevice, SensorDevice, BeerLogPoint, Beer
from external_push.models import GenericPushTarget
from django.contrib.auth.models import User


Expand All @@ -30,12 +31,6 @@ def error_notifications(request):
try:
if config.LAST_GIT_CHECK < now_time - datetime.timedelta(hours=6):

# TODO - Remove this check after April 2018
if sys.version_info[0] < 3:
messages.warning(request, "This app is currently running on Python 2 which will no longer be " +
"supported after April 2018. To upgrade to Python 3, simply follow the " +
'instructions <a href="http://www.fermentrack.com/help/python3/">at ' +
'this link.</a>')
try:
if git_integration.app_is_current():
config.LAST_GIT_CHECK = now_time
Expand Down Expand Up @@ -597,6 +592,8 @@ def site_settings(request):
if not config.USER_HAS_COMPLETED_CONFIGURATION:
return redirect('siteroot')

all_push_targets = GenericPushTarget.objects.all()

if request.POST:
form = setup_forms.GuidedSetupConfigForm(request.POST)
if form.is_valid():
Expand Down Expand Up @@ -627,12 +624,12 @@ def site_settings(request):
return redirect('siteroot')
else:
return render(request, template_name='site_config.html',
context={'form': form,
context={'form': form, 'all_push_targets': all_push_targets,
'completed_config': config.USER_HAS_COMPLETED_CONFIGURATION})
else:
form = setup_forms.GuidedSetupConfigForm()
return render(request, template_name='site_config.html',
context={'form': form,
context={'form': form, 'all_push_targets': all_push_targets,
'completed_config': config.USER_HAS_COMPLETED_CONFIGURATION})


Expand Down
10 changes: 5 additions & 5 deletions circus.ini
Expand Up @@ -18,13 +18,13 @@ stderr_stream.class = FileStream
stderr_stream.filename = $(circus.env.HOME)/fermentrack/log/fermentrack-stderr.log
#stderr_stream.time_format = %Y-%m-%d %H:%M:%S
stderr_stream.max_bytes = 2097152
stderr_stream.backup_count = 5
stderr_stream.backup_count = 3

#stdout_stream.class = FileStream
#stdout_stream.filename = $(circus.env.HOME)/fermentrack/log/fermentrack-stdout.log
#stdout_stream.time_format = %Y-%m-%d %H:%M:%S
#stdout_stream.max_bytes = 2097152
#stdout_stream.backup_count = 5
#stdout_stream.backup_count = 3

[socket:Fermentrack]
host = 0.0.0.0
Expand All @@ -51,7 +51,7 @@ stderr_stream.class = FileStream
stderr_stream.filename = $(circus.env.HOME)/fermentrack/log/fermentrack-processmgr.log
#stderr_stream.time_format = %Y-%m-%d %H:%M:%S
stderr_stream.max_bytes = 2097152
stderr_stream.backup_count = 5
stderr_stream.backup_count = 3


[watcher:huey]
Expand All @@ -69,10 +69,10 @@ stderr_stream.class = FileStream
stderr_stream.filename = $(circus.env.HOME)/fermentrack/log/huey-stderr.log
stderr_stream.refresh_time = 0.3
stderr_stream.max_bytes = 2097152
stderr_stream.backup_count = 5
stderr_stream.backup_count = 3

stdout_stream.class = FileStream
stdout_stream.filename = $(circus.env.HOME)/fermentrack/log/huey-stdout.log
stdout_stream.refresh_time = 0.3
stdout_stream.max_bytes = 2097152
stdout_stream.backup_count = 5
stdout_stream.backup_count = 3
9 changes: 4 additions & 5 deletions docs/source/about.rst
Expand Up @@ -65,9 +65,8 @@ Getting started with Fermentrack is incredibly easy! All you need to do is:

#. Install Raspbian on your Raspberry Pi
#. Install Fermentrack (one command!)
#. Configure Fermentrack
#. Connect your BrewPi temperature controllers or specific gravity sensors
#. Configure your BrewPi Temperature Controllers or specific gravity sensors
#. Configure Fermentrack from your web browser
#. Connect & configure your BrewPi temperature controllers or specific gravity sensors

It can be done from start to finish in a bit under an hour, assuming all your hardware is assembled & ready to go. To
learn how, read :doc:`getting started/index`.
Expand All @@ -76,8 +75,8 @@ learn how, read :doc:`getting started/index`.
Other Notes
-------------

Fermentrack is currently designed for "legacy" brewpi firmware running on ESP8266 and Arduino hardware, and does not
support "modern" firmware such as that included with official BrewPi controllers.
Fermentrack is currently designed for "legacy" BrewPi firmware running on ESP8266 and Arduino hardware, and does not
support "modern" firmware such as that included with current official BrewPi controllers.

A full table of controllers/expected hardware availability is available in :doc:`hardware`.

Expand Down
19 changes: 19 additions & 0 deletions docs/source/develop/changelog.rst
Expand Up @@ -7,6 +7,25 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) because it



[2019-02-17] - External Push (Remote Logging) Support
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Added
---------------------

- Fermentrack can now periodically "push" readings out to an external device/app
- Added "new control constants" support for "modern" controllers

Fixed
---------------------

- Explicitly linked Favicon from template
- Fixed BrewPi-Script error when attempting to use feature not available in Python 3.4
- Properly catch error in BrewPi-Script when pidfile already exists
- Added filesize check for gravity sensor & brewpi-device logfiles
- Add support for temperature calibration offsets


[2018-10-24] - Tilt Monitor Refactoring
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
1 change: 1 addition & 0 deletions docs/source/develop/index.rst
Expand Up @@ -15,6 +15,7 @@ Changelog, Licensing, and Development
changelog
components
contribute
push support
license


80 changes: 80 additions & 0 deletions docs/source/develop/push support.rst
@@ -0,0 +1,80 @@
.. include:: ../global.rst

"Push" Support
==============================

Although Fermentrack is focused on the "fermentation" phase of your brewing operation, Fermentrack is designed to
integrate with your brewing operation as a whole. To support the use of data collected by Fermentrack in other
applications, Fermentrack allows for data to be "pushed" on a periodic basis via HTTP requests (and will - in the
future - support pushing via TCP (sockets)).


Supported "Push" Targets
------------------------------

Fermentrack currently supports one push target, though more are likely to be added in the near future:

- "Generic" Push Target - Fermentrack's "native" push format - Pushes both specific gravity & temperature data




Generic Push Target Format
******************************

The "generic" push target format is the one recommended for use by developers who are adding native Fermentrack
support to their apps. This format contains temperature/gravity data collected by Fermentrack for each available
specific gravity sensor and BrewPi controller.

This format is supported by the `Fermentrack Push Target app <http://github.com/thorrak/fermentrack_push_target>`__ for
testing/development purposes.

::

{'api_key': 'abcde',
'brewpi_devices': [{'control_mode': 'f',
'internal_id': 1,
'name': 'Legacy 2',
'temp_format': 'F'},
{'beer_temp': 31.97,
'control_mode': 'f',
'fridge_temp': 36.26,
'internal_id': 2,
'name': 'Kegerator',
'temp_format': 'F'}],
'gravity_sensors': [{'gravity': 1.247,
'internal_id': 1,
'name': 'Pinky',
'sensor_type': 'tilt',
'temp': 78.0,
'temp_format': 'F'},
{'internal_id': 3,
'name': 'Spindly',
'sensor_type': 'ispindel',
'temp': 86.225,
'temp_format': 'F'}],
'version': '1.0'}


Implementation Notes
------------------------------

Push support within Fermentrack is implemented through the use of a "helper script" which currently is launched once
every minute. The helper script polls the defined push targets to determine which (if any) are overdue for data to be
pushed, and - for those targets - then executes the push based on how those targets are configured. Fermentrack polls
Redis for the latest available data point for specific gravity sensors, and polls BrewPi controllers for the latest data
point directly. This data is then encoded based on the selected push format and sent downstream to the requested target.

Push requests are handled asynchronously. Due to the way that the polling script is implemented, push "frequencies" may
be up to one polling cycle (currently 1 minute) later than expected. For 1 minute push cycles, this means that the
actual frequency could be as high as 2 minutes.


Feedback
------------------------------

Push support was designed to support future applications that do not yet exist, and as such, may not be perfect for
*your* application. That said, feedback is always appreciated and welcome. Feel free to reach out (HBT forums,
GitHub, Reddit...) if you have something in mind that you'd like to integrate Fermentrack into, and don't think the
existing push options will quite work.

4 changes: 4 additions & 0 deletions docs/source/getting started/Apache and PHP Support.rst
Expand Up @@ -9,6 +9,10 @@ Raspberry Pi you will need to configure nginx to serve those applications instea

Fermentrack-tools includes a script which can be used to install this support automatically.

.. warning:: Support for php-5 was discontinued in the latest versions of Raspbian. You may need to use an older version
of raspbian to obtain support for apps like RaspberryPints designed for php-5.



Understanding Legacy Support
-------------------------------
Expand Down
Empty file added external_push/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions external_push/admin.py
@@ -0,0 +1,8 @@
from django.contrib import admin

from external_push.models import GenericPushTarget


@admin.register(GenericPushTarget)
class GenericPushTargetAdmin(admin.ModelAdmin):
list_display = ('name', 'status', 'target_host')
7 changes: 7 additions & 0 deletions external_push/apps.py
@@ -0,0 +1,7 @@
from __future__ import unicode_literals

from django.apps import AppConfig


class AppConfig(AppConfig):
name = 'external_push'
16 changes: 16 additions & 0 deletions external_push/forms.py
@@ -0,0 +1,16 @@
from django import forms

from external_push.models import GenericPushTarget
from django.core import validators
import fermentrack_django.settings as settings

from django.forms import ModelForm


class GenericPushTargetModelForm(ModelForm):
class Meta:
model = GenericPushTarget
fields = ['name', 'push_frequency', 'api_key', 'brewpi_push_selection', 'brewpi_to_push',
'gravity_push_selection', 'gravity_sensors_to_push', 'target_type', 'target_host', 'target_port']


43 changes: 43 additions & 0 deletions external_push/migrations/0001_initial.py
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-12-02 21:43
from __future__ import unicode_literals

import django.core.validators
from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

initial = True

dependencies = [
('gravity', '0002_tilt_refactor'),
('app', '0009_auto_20180709_0256'),
]

operations = [
migrations.CreateModel(
name='GenericPushTarget',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Unique name for this push target', max_length=48, unique=True)),
('status', models.CharField(choices=[('active', 'Active'), ('disabled', 'Disabled')], default='active', help_text='Status of this push target', max_length=24)),
('push_frequency', models.IntegerField(choices=[(59, '1 minute'), (119, '2 minutes'), (299, '5 minutes'), (599, '10 minutes'), (899, '15 minutes'), (1799, '30 minutes'), (3599, '1 hour')], default=900, help_text='How often to push data to the target')),
('api_key', models.CharField(blank=True, default='', help_text='API key required by the push target (if any)', max_length=256)),
('brewpi_push_selection', models.CharField(choices=[('all', 'All Active Sensors/Devices'), ('list', 'Specific Sensors/Devices'), ('none', 'Nothing of this type')], default='all', help_text='How the BrewPi devices to push are selected', max_length=12)),
('gravity_push_selection', models.CharField(choices=[('all', 'All Active Sensors/Devices'), ('list', 'Specific Sensors/Devices'), ('none', 'Nothing of this type')], default='all', help_text='How the gravity sensors to push are selected', max_length=12)),
('target_type', models.CharField(choices=[('http (post)', 'HTTP/HTTPS'), ('tcp', 'TCP (Telnet/Socket)')], default='http (post)', help_text='Protocol to use to connect to the push target', max_length=24)),
('target_host', models.CharField(blank=True, default='http://127.0.0.1/', help_text='The URL to push to (for HTTP/HTTPS) or hostname/IP address (for TCP)', max_length=256)),
('target_port', models.IntegerField(default=80, help_text='The port to use (not used for HTTP/HTTPS)', validators=[django.core.validators.MinValueValidator(10, 'Port must be 10 or higher'), django.core.validators.MaxValueValidator(65535, 'Port must be 65535 or lower')])),
('data_format', models.CharField(choices=[('generic', 'All Data (Generic)'), ('brewersfriend', 'Brewers Friend')], default='generic', help_text='The data format to send to the push target', max_length=24)),
('last_triggered', models.DateTimeField(default=django.utils.timezone.now, help_text='The last time we pushed data to this target')),
('brewpi_to_push', models.ManyToManyField(help_text="BrewPi Devices to push (ignored if 'all' devices selected)", related_name='push_targets', to='app.BrewPiDevice')),
('gravity_sensors_to_push', models.ManyToManyField(help_text="Gravity Sensors to push (ignored if 'all' sensors selected)", related_name='push_targets', to='gravity.GravitySensor')),
],
options={
'verbose_name': 'Generic Push Target',
'verbose_name_plural': 'Generic Push Targets',
},
),
]
Empty file.

0 comments on commit 94bad39

Please sign in to comment.