From ea192972aa835b5cb5556c7d5b5414a7009d6856 Mon Sep 17 00:00:00 2001 From: John Doyle Date: Fri, 20 Dec 2019 12:27:43 +0000 Subject: [PATCH 01/25] Addition of ThingSpeak Push to external push locations --- docs/source/develop/push_support.rst | 30 +++- external_push/admin.py | 8 +- external_push/forms.py | 8 +- .../migrations/0005_ThingSpeak_Support.py | 35 ++++ external_push/models.py | 157 ++++++++++++++++++ external_push/tasks.py | 23 ++- .../push_target_list_embeddable.html | 19 +++ .../thingspeak_push_target_add.html | 57 +++++++ .../thingspeak_push_target_view.html | 62 +++++++ external_push/urls.py | 4 + external_push/views.py | 83 ++++++++- 11 files changed, 479 insertions(+), 7 deletions(-) create mode 100644 external_push/migrations/0005_ThingSpeak_Support.py create mode 100644 external_push/templates/external_push/thingspeak_push_target_add.html create mode 100644 external_push/templates/external_push/thingspeak_push_target_view.html diff --git a/docs/source/develop/push_support.rst b/docs/source/develop/push_support.rst index b49b53da..ae30b39f 100644 --- a/docs/source/develop/push_support.rst +++ b/docs/source/develop/push_support.rst @@ -17,6 +17,7 @@ Fermentrack currently supports three push targets: - **"Generic" Push Target** - Fermentrack's "native" push format - Pushes both specific gravity & temperature data - **Brewer's Friend** - Fermentrack's "native" push format - Pushes both specific gravity & temperature data - **Brewfather** - Fermentrack's "native" push format - Pushes both specific gravity & temperature data +- **ThingSpeak** - Fermentrack's "native" push format - Pushes temperature data @@ -92,8 +93,35 @@ Fermentrack supports pushing data from specific gravity sensors to Brewfather us Within 60 seconds, Fermentrack will begin sending data from your gravity sensor to Brewfather. This data can be seen on the `Devices `_ page. -**NOTE** - If your gravity sensor is attached to a BrewPi controller, the temperature readings from that controller will be used instead of the ones from the gravity sensor. +ThingSpeak Support +*********************** + +Fermentrack supports pushing data from specific sensors to a ThinkSpeak Channel. The Channel Speak API is fixed to receive fields in the channel, so the designation of each channel is already defined. This means that Field 1 is always Beer Name, Field 2 is Sensor Name, etc. To configure: + +#. Log into Fermentrack and click the "gear" icon in the upper right +#. Click "Add ThingSpeak Push Target" at the bottom of the page +#. Now log into your ThingSpeak acount and on the My Channels Page select New Channel +#. Enter the data below in +:: + Name - Give your Channel a Name + Description - Give your Channel a Description + Field 1 - Beer Name + Field 2 - Sensor Name + Field 3 - Temp Format + Field 4 - Beer Temp + Field 5 - Fridge Temp + Field 6 - Room Temp + Field 7 - Beer Gravity + +Feel free to fill out the optional elements but only the 'field' values above are sent. The values entered are just labels for the data sent and can be customised. For example you can change 'Beer Temp' to 'My Beer (°C)'. + +#. At the bottom of the page, select 'Save Channel' +#. Copy the "Write API Key" from the "API Keys" section +#. Within Fermentrack, paste the API Key you just copied into the "API Key" field +#. Set the desired push frequency and select the gravity sensor from which you want to push data +#. Click "Add Push Target" +Within 60 seconds, Fermentrack will begin sending data from to the ThingSpeak Channel. This data can be seen on the ThingSpeak 'Private View' tab in the channel page. Implementation Notes diff --git a/external_push/admin.py b/external_push/admin.py index 09ca9479..2e2cbca2 100644 --- a/external_push/admin.py +++ b/external_push/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget +from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget, ThingSpeakPushTarget @admin.register(GenericPushTarget) @@ -13,4 +13,8 @@ class BrewersFriendPushTargetAdmin(admin.ModelAdmin): @admin.register(BrewfatherPushTarget) class BrewfatherPushTargetAdmin(admin.ModelAdmin): - list_display = ('gravity_sensor_to_push', 'status', 'push_frequency') \ No newline at end of file + list_display = ('gravity_sensor_to_push', 'status', 'push_frequency') + +#@admin.register(ThingSpeakPushTarget) +#class ThingSpeakPushTargetAdmin(admin.ModelAdmin): +# list_display = ('gravity_sensors_to_push', 'status', 'push_frequency', 'target_host') \ No newline at end of file diff --git a/external_push/forms.py b/external_push/forms.py index 223a18df..bd3825d1 100644 --- a/external_push/forms.py +++ b/external_push/forms.py @@ -1,6 +1,6 @@ from django import forms -from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget +from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget, ThingSpeakPushTarget from django.core import validators import fermentrack_django.settings as settings @@ -24,3 +24,9 @@ class BrewfatherPushTargetModelForm(ModelForm): class Meta: model = BrewfatherPushTarget fields = ['gravity_sensor_to_push', 'push_frequency', 'logging_url'] + +class ThingSpeakPushTargetModelForm(ModelForm): + class Meta: + model = ThingSpeakPushTarget + fields = ['name', 'push_frequency', 'api_key', 'brewpi_to_push'] + diff --git a/external_push/migrations/0005_ThingSpeak_Support.py b/external_push/migrations/0005_ThingSpeak_Support.py new file mode 100644 index 00000000..80fe18a8 --- /dev/null +++ b/external_push/migrations/0005_ThingSpeak_Support.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2019-11-29 21:31 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('gravity', '0004_BrewersFriend_Support'), + ('external_push', '0003_BrewersFriend_Support'), + ] + + operations = [ + migrations.CreateModel( + name='ThingSpeakPushTarget', + 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'), ('error', 'Error')], default='active', help_text='Status of this push target', max_length=24)), + ('push_frequency', models.IntegerField(choices=[(901, '15 minutes'), (1801, '30 minutes'), (3601, '1 hour')], default=900, help_text='How often to push data to the target')), + ('api_key', models.CharField(default='', help_text='Brewers Friend API Key', max_length=256)), + ('brewpi_to_push_id', models.ForeignKey(blank=True, default=None, help_text="BrewPi Devices to push (ignored if 'all' devices selected)", related_name='push_targets', to='app.BrewPiDevice')), + ('gravity_sensor_to_push_id', models.ForeignKey(blank=True, default=None, help_text='Gravity Sensor to push (create one push target per sensor to push)', on_delete=django.db.models.deletion.CASCADE, related_name='push_target', to='gravity.GravitySensor')), + ('error_text', models.TextField(blank=True, default='', help_text='The error (if any) encountered on the last push attempt', null=True)), + ('last_triggered', models.DateTimeField(auto_now_add=True, help_text='The last time we pushed data to this target')), + ], + options={ + 'verbose_name': 'ThingSpeak Push Target', + 'verbose_name_plural': 'ThingSpeak Push Targets', + }, + ) + ] diff --git a/external_push/models.py b/external_push/models.py index 5520bf5b..4822ee40 100644 --- a/external_push/models.py +++ b/external_push/models.py @@ -471,3 +471,160 @@ def send_data(self): r = requests.post(self.logging_url, data=json_data, headers=headers) return True # TODO - Check if the post actually succeeded & react accordingly + + + + +class ThingSpeakPushTarget(models.Model): + class Meta: + verbose_name = "ThingSpeak Push Target" + verbose_name_plural = "ThingSpeak Push Targets" + + STATUS_ACTIVE = 'active' + STATUS_DISABLED = 'disabled' + STATUS_ERROR = 'error' + + STATUS_CHOICES = ( + (STATUS_ACTIVE, 'Active'), + (STATUS_DISABLED, 'Disabled'), + (STATUS_ERROR, 'Error'), + ) + + SENSOR_SELECT_ALL = "all" + SENSOR_SELECT_LIST = "list" + SENSOR_SELECT_NONE = "none" + + SENSOR_SELECT_CHOICES = ( + (SENSOR_SELECT_ALL, "All Active Sensors/Devices"), + (SENSOR_SELECT_LIST, "Specific Sensors/Devices"), + (SENSOR_SELECT_NONE, "Nothing of this type"), + ) + + PUSH_FREQUENCY_CHOICES = ( + # (30-1, '30 seconds'), + (60 - 1, '1 minute'), + (60 * 2 - 1, '2 minutes'), + (60 * 5 - 1, '5 minutes'), + (60 * 10 - 1, '10 minutes'), + (60 * 15 - 1, '15 minutes'), + (60 * 30 - 1, '30 minutes'), + (60 * 60 - 1, '1 hour'), + ) + + name = models.CharField(max_length=48, help_text="Unique name for this push target", unique=True) + status = models.CharField(max_length=24, help_text="Status of this push target", choices=STATUS_CHOICES, + default=STATUS_ACTIVE) + push_frequency = models.IntegerField(choices=PUSH_FREQUENCY_CHOICES, default=60 * 15, + help_text="How often to push data to the target") + api_key = models.CharField(max_length=256, help_text="ThingSpeak Channel API Key", default="") + + brewpi_to_push = models.ForeignKey(to=BrewPiDevice, related_name="thingspeak_push_targets", blank=True, default=None, on_delete=models.CASCADE, + help_text="BrewPi Devices to push (ignored if 'all' devices selected)") + +# gravity_sensor_to_push = models.ForeignKey(to=GravitySensor, related_name="thingspeak_push_targets", blank=True, default=None, on_delete=models.CASCADE, +# help_text="Gravity Sensor to push (create one push target per " +# "sensor to push)") + + error_text = models.TextField(blank=True, null=True, default="", help_text="The error (if any) encountered on the " + "last push attempt") + + last_triggered = models.DateTimeField(help_text="The last time we pushed data to this target", auto_now_add=True) + +# def __str__(self): +# return self.gravity_sensor_to_push.name + + def data_to_push(self): + brewpi_to_send = BrewPiDevice.objects.filter(status=BrewPiDevice.STATUS_ACTIVE) +# grav_sensors_to_send = GravitySensor.objects.filter(status=GravitySensor.STATUS_ACTIVE) + +# POST api.thingspeak.com/update.json +# Content-Type: application/json +# +# { +# "api_key": "XXXXXXXXXXXXXXXX" +# "created_at": "2018-04-23 21:36:20 +0200", +# "field1": brewpi device_name, +# "field2": brewpi temp_format, +# "field3": Beer Name, +# "field4": Beer Temp, +# "field5": Fridge Temp, +# "field6": Room Temp, +# "field7": brewpi Gravity, +# "latitude": "", +# "longitude": "", +# "status": "" +# } + # At this point we've obtained the list of objects to send - now we just need to format them. + string_to_send = "" # This is what ultimately needs to be populated. + GENERIC_DATA_FORMAT_VERSION = "1.0" + + data_to_send = {'api_key': self.api_key, 'version': GENERIC_DATA_FORMAT_VERSION} + if brewpi_to_send is not None: + for brewpi in brewpi_to_send: + # TODO - Handle this if the brewpi can't be loaded, given "get_dashpanel_info" communicates with BrewPi-Script + # TODO - Make it so that this data is stored in/loaded from Redis + device_info = brewpi.get_dashpanel_info() + if device_info is None: + continue + + # Have to coerce temps to floats, as Decimals aren't json serializable + data_to_send['field1'] = str(brewpi.active_beer) + data_to_send['field2'] = brewpi.device_name + data_to_send['field3'] = brewpi.temp_format + + # Because not every device will have temp sensors, only serialize the sensors that exist. + # Have to coerce temps to floats, as Decimals aren't json serializable + if device_info['BeerTemp'] is not None: + if device_info['BeerTemp'] != 0: + data_to_send['field4'] = float(device_info['BeerTemp']) + if device_info['FridgeTemp'] is not None: + if device_info['FridgeTemp'] != 0: + data_to_send['field5'] = float(device_info['FridgeTemp']) + if device_info['RoomTemp'] is not None: + if device_info['RoomTemp'] != 0: + data_to_send['field6'] = float(device_info['RoomTemp']) + + # Gravity isn't retrieved via get_dashpanel_info, and as such requires special handling + try: + if brewpi.gravity_sensor is not None: + gravity = brewpi.gravity_sensor.retrieve_latest_gravity() + if gravity is not None: + data_to_send['field7'] = float(gravity) + except: + pass + +# if grav_sensors_to_send is not None: +# for sensor in grav_sensors_to_send: +# data_to_send['field1'] = str(brewpi.active_beer) +# data_to_send['field2'] = sensor.device_name +# +# latest_log_point = sensor.retrieve_latest_point() +# if latest_log_point is not None: +# # For now, if we can't get a latest log point, let's default to just not sending anything. +# if latest_log_point.gravity != 0.0: +# data_to_send['field7'] = float(latest_log_point.gravity) +# +# # For now all gravity sensors have temp info, but just in case +# if latest_log_point.temp is not None: +# data_to_send['field4'] = float(latest_log_point.temp) +# data_to_send['field3'] = latest_log_point.temp_format + + string_to_send = json.dumps(data_to_send) + + # We've got the data (in a json'ed string) - lets send it + return string_to_send + + def send_data(self): + # self.data_to_push() returns a JSON-encoded string which we will push directly out + json_data = self.data_to_push() + + if len(json_data) <= 2: + # There was no data to push - do nothing. + return False + + headers = {'Content-Type': 'application/json', 'X-API-KEY': self.api_key} + thingspeak_url = "https://api.thingspeak.com/update.json" + self.api_key + + r = requests.post(thingspeak_url, data=json_data, headers=headers) + return True # TODO - Check if the post actually succeeded & react accordingly + diff --git a/external_push/tasks.py b/external_push/tasks.py index d3aaac12..fc6584a7 100644 --- a/external_push/tasks.py +++ b/external_push/tasks.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from huey import crontab from huey.contrib.djhuey import periodic_task, task, db_periodic_task, db_task -from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget +from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget, ThingSpeakPushTarget import datetime, pytz, time from django.utils import timezone @@ -44,6 +44,18 @@ def brewfather_push_target_push(target_id): return None +@db_task() +def thingspeak_push_target_push(target_id): + try: + push_target = ThingSpeakPushTarget.objects.get(id=target_id) + except: + # TODO - Replace with ObjNotFound + return None + + push_target.send_data() + + return None + # TODO - At some point write a validation function that will allow us to trigger more often than every minute @db_periodic_task(crontab(minute="*")) @@ -51,6 +63,7 @@ def dispatch_push_tasks(): generic_push_targets = GenericPushTarget.objects.filter(status=GenericPushTarget.STATUS_ACTIVE).all() brewers_friend_push_targets = BrewersFriendPushTarget.objects.filter(status=BrewersFriendPushTarget.STATUS_ACTIVE).all() brewfather_push_targets = BrewfatherPushTarget.objects.filter(status=BrewfatherPushTarget.STATUS_ACTIVE).all() + thingspeak_push_targets = ThingSpeakPushTarget.objects.filter(status=ThingSpeakPushTarget.STATUS_ACTIVE).all() # Run through the list of generic push targets and trigger a (future) data send for each for target in generic_push_targets: @@ -79,5 +92,13 @@ def dispatch_push_tasks(): # Queue the generic_push_target_push task (going to do it asynchronously) brewfather_push_target_push(target.id) + # Run through the list of ThingSpeak push targets and trigger a (future) data send for each + for target in thingspeak_push_targets: + if timezone.now() >= (target.last_triggered + datetime.timedelta(seconds=target.push_frequency)): + target.last_triggered = timezone.now() + target.save() + + # Queue the thingspeak_push_target_push task (going to do it asynchronously) + thingspeak_push_target_push(target.id) return None diff --git a/external_push/templates/external_push/push_target_list_embeddable.html b/external_push/templates/external_push/push_target_list_embeddable.html index 9c7d770a..4a5b1782 100644 --- a/external_push/templates/external_push/push_target_list_embeddable.html +++ b/external_push/templates/external_push/push_target_list_embeddable.html @@ -57,9 +57,28 @@

Brewfather Push Targets

{% endif %} + {# List the thingspeak push targets next #} + {% if thingspeak_push_targets.count > 0 %} +

ThingSpeak Push Targets

+ + {% endif %} +

Add Generic Push Target Add Brewer's Friend Push Target Add Brewfather Push Target + Add ThingSpeak Push Target

diff --git a/external_push/templates/external_push/thingspeak_push_target_add.html b/external_push/templates/external_push/thingspeak_push_target_add.html new file mode 100644 index 00000000..2060895c --- /dev/null +++ b/external_push/templates/external_push/thingspeak_push_target_add.html @@ -0,0 +1,57 @@ +{% extends "sitewide/flat_ui_template.html" %} +{% load custom_tags %} + + +{% block title %}ThingSpeak Push Target{% endblock %} + +{% block content %} + +

Add Push Target

+{% if form.errors %} +
Please correct the error {{ form.errors }} below.
+{% endif %} +

+ ThingSpeak for IoT Projects is a IOT data collection, analysis and Reaction Platform. + This will send the data for your chosen device to ThingSpeak in the format that is required for channel integration +

ThingSpeak Channel Settings
+ +

+

+

+ {% csrf_token %} + +
+

Push Target Settings

+ {% form_generic form.name %} + {% form_generic form.push_frequency %} + {% form_generic form.api_key %} + {% form_generic form.brewpi_to_push %} + +
+ + +
+

+ + +{% endblock %} + +{% block scripts %} + + +{% endblock %} + diff --git a/external_push/templates/external_push/thingspeak_push_target_view.html b/external_push/templates/external_push/thingspeak_push_target_view.html new file mode 100644 index 00000000..4e351898 --- /dev/null +++ b/external_push/templates/external_push/thingspeak_push_target_view.html @@ -0,0 +1,62 @@ +{% extends "sitewide/flat_ui_template.html" %} +{% load custom_tags %} + + +{% block title %}ThingSpeak Push Target {{ push_target }}{% endblock %} + +{% block content %} + +

{{ push_target.name }}

+{% if form.errors %} +
Please correct the error {{ form.errors }} below.
+{% endif %} +

+ ThingSpeak for IoT Projects is a IOT data collection, analysis and Reaction Platform. + This will send the data for your chosen device to ThingSpeak in the format that is required for channel integration +

ThingSpeak Channel Settings
+ +

+

+

+ {% csrf_token %} + +
+

Push Target Settings

+ {% form_generic form.name %} + {% form_generic form.push_frequency %} + {% form_generic form.api_key %} + {% form_generic form.brewpi_to_push %} + +
+ + +
+

+ + +

+ Delete Push Target +

+ + +{% endblock %} + +{% block scripts %} + + +{% endblock %} + diff --git a/external_push/urls.py b/external_push/urls.py index a6852d3e..8aae949c 100644 --- a/external_push/urls.py +++ b/external_push/urls.py @@ -24,4 +24,8 @@ url(r'^push/brewfather/view/(?P[0-9]{1,20})/$', external_push.views.external_push_brewfather_view, name='external_push_brewfather_view'), url(r'^push/brewfather/delete/(?P[0-9]{1,20})/$', external_push.views.external_push_brewfather_delete, name='external_push_brewfather_delete'), + url(r'^push/thingspeak/add/$', external_push.views.external_push_thingspeak_target_add, name='external_push_thingspeak_target_add'), + url(r'^push/thingspeak/view/(?P[0-9]{1,20})/$', external_push.views.external_push_thingspeak_view, name='external_push_thingspeak_view'), + url(r'^push/thingspeak/delete/(?P[0-9]{1,20})/$', external_push.views.external_push_thingspeak_delete, name='external_push_thingspeak_delete'), + ] diff --git a/external_push/views.py b/external_push/views.py index fd75d1c7..11d8a865 100644 --- a/external_push/views.py +++ b/external_push/views.py @@ -7,7 +7,7 @@ from django.http import JsonResponse, HttpResponse from django.core.exceptions import ObjectDoesNotExist -from .models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget +from .models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget, ThingSpeakPushTarget import fermentrack_django.settings as settings @@ -31,9 +31,10 @@ def external_push_list(request, context_only=False): all_push_targets = GenericPushTarget.objects.all() brewers_friend_push_targets = BrewersFriendPushTarget.objects.all() brewfather_push_targets = BrewfatherPushTarget.objects.all() + thingspeak_push_targets = ThingSpeakPushTarget.objects.all() context = {'all_push_targets': all_push_targets, 'brewfather_push_targets': brewfather_push_targets, - 'brewers_friend_push_targets': brewers_friend_push_targets} + 'brewers_friend_push_targets': brewers_friend_push_targets, 'thingspeak_push_targets': thingspeak_push_targets} # This allows us to embed this in the site configuration page... There's almost certainly a better way to do this. if not context_only: @@ -287,5 +288,83 @@ def external_push_brewfather_delete(request, push_target_id): return redirect('external_push_list') + + +@login_required +@site_is_configured +def external_push_thingspeak_target_add(request): + # TODO - Add user permissioning + # if not request.user.has_perm('app.add_device'): + # messages.error(request, 'Your account is not permissioned to add devices. Please contact an admin') + # return redirect("/") + + form = forms.ThingSpeakPushTargetModelForm() + + if request.POST: + form = forms.ThingSpeakPushTargetModelForm(request.POST) + if form.is_valid(): + new_push_target = form.save() + messages.success(request, 'Successfully added push target') + + # Update last triggered to force a refresh in the next cycle + new_push_target.last_triggered = new_push_target.last_triggered - datetime.timedelta(seconds=new_push_target.push_frequency) + new_push_target.save() + + return redirect('external_push_list') + + messages.error(request, 'Unable to add new push target') + + # Basically, if we don't get redirected, in every case we're just outputting the same template + return render(request, template_name='external_push/thingspeak_push_target_add.html', context={'form': form}) + +@login_required +@site_is_configured +def external_push_thingspeak_view(request, push_target_id): + # TODO - Add user permissioning + # if not request.user.has_perm('app.add_device'): + # messages.error(request, 'Your account is not permissioned to add devices. Please contact an admin') + # return redirect("/") + + try: + push_target = ThingSpeakPushTarget.objects.get(id=push_target_id) + except ObjectDoesNotExist: + messages.error(request, "ThingSpeak push target {} does not exist".format(push_target_id)) + return redirect('external_push_list') + + if request.POST: + form = forms.ThingSpeakPushTargetModelForm(request.POST, instance=push_target) + if form.is_valid(): + updated_push_target = form.save() + messages.success(request, 'Updated push target') + return redirect('external_push_list') + + messages.error(request, 'Unable to update push target') + + form = forms.ThingSpeakPushTargetModelForm(instance=push_target) + + return render(request, template_name='external_push/thingspeak_push_target_view.html', + context={'push_target': push_target, 'form': form}) + +@login_required +@site_is_configured +def external_push_thingspeak_delete(request, push_target_id): + # TODO - Add user permissioning + # if not request.user.has_perm('app.add_device'): + # messages.error(request, 'Your account is not permissioned to add devices. Please contact an admin') + # return redirect("/") + + try: + push_target = ThingSpeakPushTarget.objects.get(id=push_target_id) + except ObjectDoesNotExist: + messages.error(request, "ThingSpeak push target {} does not exist".format(push_target_id)) + return redirect('external_push_list') + + message = "ThingSpeak push target {} has been deleted".format(push_target_id) + push_target.delete() + messages.success(request, message) + + return redirect('external_push_list') + + From 56646f9883edeeae4f33d71407a7dbdea56e2e80 Mon Sep 17 00:00:00 2001 From: John Doyle Date: Fri, 20 Dec 2019 12:27:43 +0000 Subject: [PATCH 02/25] Addition of ThingSpeak Push to external push locations --- docs/source/develop/push_support.rst | 30 +++- external_push/admin.py | 8 +- external_push/forms.py | 8 +- .../migrations/0005_ThingSpeak_Support.py | 34 ++++ external_push/models.py | 157 ++++++++++++++++++ external_push/tasks.py | 23 ++- .../push_target_list_embeddable.html | 19 +++ .../thingspeak_push_target_add.html | 57 +++++++ .../thingspeak_push_target_view.html | 62 +++++++ external_push/urls.py | 4 + external_push/views.py | 83 ++++++++- 11 files changed, 478 insertions(+), 7 deletions(-) create mode 100644 external_push/migrations/0005_ThingSpeak_Support.py create mode 100644 external_push/templates/external_push/thingspeak_push_target_add.html create mode 100644 external_push/templates/external_push/thingspeak_push_target_view.html diff --git a/docs/source/develop/push_support.rst b/docs/source/develop/push_support.rst index b49b53da..ae30b39f 100644 --- a/docs/source/develop/push_support.rst +++ b/docs/source/develop/push_support.rst @@ -17,6 +17,7 @@ Fermentrack currently supports three push targets: - **"Generic" Push Target** - Fermentrack's "native" push format - Pushes both specific gravity & temperature data - **Brewer's Friend** - Fermentrack's "native" push format - Pushes both specific gravity & temperature data - **Brewfather** - Fermentrack's "native" push format - Pushes both specific gravity & temperature data +- **ThingSpeak** - Fermentrack's "native" push format - Pushes temperature data @@ -92,8 +93,35 @@ Fermentrack supports pushing data from specific gravity sensors to Brewfather us Within 60 seconds, Fermentrack will begin sending data from your gravity sensor to Brewfather. This data can be seen on the `Devices `_ page. -**NOTE** - If your gravity sensor is attached to a BrewPi controller, the temperature readings from that controller will be used instead of the ones from the gravity sensor. +ThingSpeak Support +*********************** + +Fermentrack supports pushing data from specific sensors to a ThinkSpeak Channel. The Channel Speak API is fixed to receive fields in the channel, so the designation of each channel is already defined. This means that Field 1 is always Beer Name, Field 2 is Sensor Name, etc. To configure: + +#. Log into Fermentrack and click the "gear" icon in the upper right +#. Click "Add ThingSpeak Push Target" at the bottom of the page +#. Now log into your ThingSpeak acount and on the My Channels Page select New Channel +#. Enter the data below in +:: + Name - Give your Channel a Name + Description - Give your Channel a Description + Field 1 - Beer Name + Field 2 - Sensor Name + Field 3 - Temp Format + Field 4 - Beer Temp + Field 5 - Fridge Temp + Field 6 - Room Temp + Field 7 - Beer Gravity + +Feel free to fill out the optional elements but only the 'field' values above are sent. The values entered are just labels for the data sent and can be customised. For example you can change 'Beer Temp' to 'My Beer (°C)'. + +#. At the bottom of the page, select 'Save Channel' +#. Copy the "Write API Key" from the "API Keys" section +#. Within Fermentrack, paste the API Key you just copied into the "API Key" field +#. Set the desired push frequency and select the gravity sensor from which you want to push data +#. Click "Add Push Target" +Within 60 seconds, Fermentrack will begin sending data from to the ThingSpeak Channel. This data can be seen on the ThingSpeak 'Private View' tab in the channel page. Implementation Notes diff --git a/external_push/admin.py b/external_push/admin.py index 09ca9479..2e2cbca2 100644 --- a/external_push/admin.py +++ b/external_push/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget +from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget, ThingSpeakPushTarget @admin.register(GenericPushTarget) @@ -13,4 +13,8 @@ class BrewersFriendPushTargetAdmin(admin.ModelAdmin): @admin.register(BrewfatherPushTarget) class BrewfatherPushTargetAdmin(admin.ModelAdmin): - list_display = ('gravity_sensor_to_push', 'status', 'push_frequency') \ No newline at end of file + list_display = ('gravity_sensor_to_push', 'status', 'push_frequency') + +#@admin.register(ThingSpeakPushTarget) +#class ThingSpeakPushTargetAdmin(admin.ModelAdmin): +# list_display = ('gravity_sensors_to_push', 'status', 'push_frequency', 'target_host') \ No newline at end of file diff --git a/external_push/forms.py b/external_push/forms.py index 223a18df..bd3825d1 100644 --- a/external_push/forms.py +++ b/external_push/forms.py @@ -1,6 +1,6 @@ from django import forms -from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget +from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget, ThingSpeakPushTarget from django.core import validators import fermentrack_django.settings as settings @@ -24,3 +24,9 @@ class BrewfatherPushTargetModelForm(ModelForm): class Meta: model = BrewfatherPushTarget fields = ['gravity_sensor_to_push', 'push_frequency', 'logging_url'] + +class ThingSpeakPushTargetModelForm(ModelForm): + class Meta: + model = ThingSpeakPushTarget + fields = ['name', 'push_frequency', 'api_key', 'brewpi_to_push'] + diff --git a/external_push/migrations/0005_ThingSpeak_Support.py b/external_push/migrations/0005_ThingSpeak_Support.py new file mode 100644 index 00000000..03a19352 --- /dev/null +++ b/external_push/migrations/0005_ThingSpeak_Support.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2019-11-29 21:31 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('gravity', '0004_BrewersFriend_Support'), + ('external_push', '0003_BrewersFriend_Support'), + ] + + operations = [ + migrations.CreateModel( + name='ThingSpeakPushTarget', + 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'), ('error', 'Error')], default='active', help_text='Status of this push target', max_length=24)), + ('push_frequency', models.IntegerField(choices=[(901, '15 minutes'), (1801, '30 minutes'), (3601, '1 hour')], default=900, help_text='How often to push data to the target')), + ('api_key', models.CharField(default='', help_text='Brewers Friend API Key', max_length=256)), + ('brewpi_to_push_id', models.ForeignKey(blank=True, default=None, help_text="BrewPi Devices to push (ignored if 'all' devices selected)", related_name='push_targets', to='app.BrewPiDevice')), + ('error_text', models.TextField(blank=True, default='', help_text='The error (if any) encountered on the last push attempt', null=True)), + ('last_triggered', models.DateTimeField(auto_now_add=True, help_text='The last time we pushed data to this target')), + ], + options={ + 'verbose_name': 'ThingSpeak Push Target', + 'verbose_name_plural': 'ThingSpeak Push Targets', + }, + ) + ] diff --git a/external_push/models.py b/external_push/models.py index 5520bf5b..4822ee40 100644 --- a/external_push/models.py +++ b/external_push/models.py @@ -471,3 +471,160 @@ def send_data(self): r = requests.post(self.logging_url, data=json_data, headers=headers) return True # TODO - Check if the post actually succeeded & react accordingly + + + + +class ThingSpeakPushTarget(models.Model): + class Meta: + verbose_name = "ThingSpeak Push Target" + verbose_name_plural = "ThingSpeak Push Targets" + + STATUS_ACTIVE = 'active' + STATUS_DISABLED = 'disabled' + STATUS_ERROR = 'error' + + STATUS_CHOICES = ( + (STATUS_ACTIVE, 'Active'), + (STATUS_DISABLED, 'Disabled'), + (STATUS_ERROR, 'Error'), + ) + + SENSOR_SELECT_ALL = "all" + SENSOR_SELECT_LIST = "list" + SENSOR_SELECT_NONE = "none" + + SENSOR_SELECT_CHOICES = ( + (SENSOR_SELECT_ALL, "All Active Sensors/Devices"), + (SENSOR_SELECT_LIST, "Specific Sensors/Devices"), + (SENSOR_SELECT_NONE, "Nothing of this type"), + ) + + PUSH_FREQUENCY_CHOICES = ( + # (30-1, '30 seconds'), + (60 - 1, '1 minute'), + (60 * 2 - 1, '2 minutes'), + (60 * 5 - 1, '5 minutes'), + (60 * 10 - 1, '10 minutes'), + (60 * 15 - 1, '15 minutes'), + (60 * 30 - 1, '30 minutes'), + (60 * 60 - 1, '1 hour'), + ) + + name = models.CharField(max_length=48, help_text="Unique name for this push target", unique=True) + status = models.CharField(max_length=24, help_text="Status of this push target", choices=STATUS_CHOICES, + default=STATUS_ACTIVE) + push_frequency = models.IntegerField(choices=PUSH_FREQUENCY_CHOICES, default=60 * 15, + help_text="How often to push data to the target") + api_key = models.CharField(max_length=256, help_text="ThingSpeak Channel API Key", default="") + + brewpi_to_push = models.ForeignKey(to=BrewPiDevice, related_name="thingspeak_push_targets", blank=True, default=None, on_delete=models.CASCADE, + help_text="BrewPi Devices to push (ignored if 'all' devices selected)") + +# gravity_sensor_to_push = models.ForeignKey(to=GravitySensor, related_name="thingspeak_push_targets", blank=True, default=None, on_delete=models.CASCADE, +# help_text="Gravity Sensor to push (create one push target per " +# "sensor to push)") + + error_text = models.TextField(blank=True, null=True, default="", help_text="The error (if any) encountered on the " + "last push attempt") + + last_triggered = models.DateTimeField(help_text="The last time we pushed data to this target", auto_now_add=True) + +# def __str__(self): +# return self.gravity_sensor_to_push.name + + def data_to_push(self): + brewpi_to_send = BrewPiDevice.objects.filter(status=BrewPiDevice.STATUS_ACTIVE) +# grav_sensors_to_send = GravitySensor.objects.filter(status=GravitySensor.STATUS_ACTIVE) + +# POST api.thingspeak.com/update.json +# Content-Type: application/json +# +# { +# "api_key": "XXXXXXXXXXXXXXXX" +# "created_at": "2018-04-23 21:36:20 +0200", +# "field1": brewpi device_name, +# "field2": brewpi temp_format, +# "field3": Beer Name, +# "field4": Beer Temp, +# "field5": Fridge Temp, +# "field6": Room Temp, +# "field7": brewpi Gravity, +# "latitude": "", +# "longitude": "", +# "status": "" +# } + # At this point we've obtained the list of objects to send - now we just need to format them. + string_to_send = "" # This is what ultimately needs to be populated. + GENERIC_DATA_FORMAT_VERSION = "1.0" + + data_to_send = {'api_key': self.api_key, 'version': GENERIC_DATA_FORMAT_VERSION} + if brewpi_to_send is not None: + for brewpi in brewpi_to_send: + # TODO - Handle this if the brewpi can't be loaded, given "get_dashpanel_info" communicates with BrewPi-Script + # TODO - Make it so that this data is stored in/loaded from Redis + device_info = brewpi.get_dashpanel_info() + if device_info is None: + continue + + # Have to coerce temps to floats, as Decimals aren't json serializable + data_to_send['field1'] = str(brewpi.active_beer) + data_to_send['field2'] = brewpi.device_name + data_to_send['field3'] = brewpi.temp_format + + # Because not every device will have temp sensors, only serialize the sensors that exist. + # Have to coerce temps to floats, as Decimals aren't json serializable + if device_info['BeerTemp'] is not None: + if device_info['BeerTemp'] != 0: + data_to_send['field4'] = float(device_info['BeerTemp']) + if device_info['FridgeTemp'] is not None: + if device_info['FridgeTemp'] != 0: + data_to_send['field5'] = float(device_info['FridgeTemp']) + if device_info['RoomTemp'] is not None: + if device_info['RoomTemp'] != 0: + data_to_send['field6'] = float(device_info['RoomTemp']) + + # Gravity isn't retrieved via get_dashpanel_info, and as such requires special handling + try: + if brewpi.gravity_sensor is not None: + gravity = brewpi.gravity_sensor.retrieve_latest_gravity() + if gravity is not None: + data_to_send['field7'] = float(gravity) + except: + pass + +# if grav_sensors_to_send is not None: +# for sensor in grav_sensors_to_send: +# data_to_send['field1'] = str(brewpi.active_beer) +# data_to_send['field2'] = sensor.device_name +# +# latest_log_point = sensor.retrieve_latest_point() +# if latest_log_point is not None: +# # For now, if we can't get a latest log point, let's default to just not sending anything. +# if latest_log_point.gravity != 0.0: +# data_to_send['field7'] = float(latest_log_point.gravity) +# +# # For now all gravity sensors have temp info, but just in case +# if latest_log_point.temp is not None: +# data_to_send['field4'] = float(latest_log_point.temp) +# data_to_send['field3'] = latest_log_point.temp_format + + string_to_send = json.dumps(data_to_send) + + # We've got the data (in a json'ed string) - lets send it + return string_to_send + + def send_data(self): + # self.data_to_push() returns a JSON-encoded string which we will push directly out + json_data = self.data_to_push() + + if len(json_data) <= 2: + # There was no data to push - do nothing. + return False + + headers = {'Content-Type': 'application/json', 'X-API-KEY': self.api_key} + thingspeak_url = "https://api.thingspeak.com/update.json" + self.api_key + + r = requests.post(thingspeak_url, data=json_data, headers=headers) + return True # TODO - Check if the post actually succeeded & react accordingly + diff --git a/external_push/tasks.py b/external_push/tasks.py index d3aaac12..fc6584a7 100644 --- a/external_push/tasks.py +++ b/external_push/tasks.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from huey import crontab from huey.contrib.djhuey import periodic_task, task, db_periodic_task, db_task -from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget +from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget, ThingSpeakPushTarget import datetime, pytz, time from django.utils import timezone @@ -44,6 +44,18 @@ def brewfather_push_target_push(target_id): return None +@db_task() +def thingspeak_push_target_push(target_id): + try: + push_target = ThingSpeakPushTarget.objects.get(id=target_id) + except: + # TODO - Replace with ObjNotFound + return None + + push_target.send_data() + + return None + # TODO - At some point write a validation function that will allow us to trigger more often than every minute @db_periodic_task(crontab(minute="*")) @@ -51,6 +63,7 @@ def dispatch_push_tasks(): generic_push_targets = GenericPushTarget.objects.filter(status=GenericPushTarget.STATUS_ACTIVE).all() brewers_friend_push_targets = BrewersFriendPushTarget.objects.filter(status=BrewersFriendPushTarget.STATUS_ACTIVE).all() brewfather_push_targets = BrewfatherPushTarget.objects.filter(status=BrewfatherPushTarget.STATUS_ACTIVE).all() + thingspeak_push_targets = ThingSpeakPushTarget.objects.filter(status=ThingSpeakPushTarget.STATUS_ACTIVE).all() # Run through the list of generic push targets and trigger a (future) data send for each for target in generic_push_targets: @@ -79,5 +92,13 @@ def dispatch_push_tasks(): # Queue the generic_push_target_push task (going to do it asynchronously) brewfather_push_target_push(target.id) + # Run through the list of ThingSpeak push targets and trigger a (future) data send for each + for target in thingspeak_push_targets: + if timezone.now() >= (target.last_triggered + datetime.timedelta(seconds=target.push_frequency)): + target.last_triggered = timezone.now() + target.save() + + # Queue the thingspeak_push_target_push task (going to do it asynchronously) + thingspeak_push_target_push(target.id) return None diff --git a/external_push/templates/external_push/push_target_list_embeddable.html b/external_push/templates/external_push/push_target_list_embeddable.html index 9c7d770a..4a5b1782 100644 --- a/external_push/templates/external_push/push_target_list_embeddable.html +++ b/external_push/templates/external_push/push_target_list_embeddable.html @@ -57,9 +57,28 @@

Brewfather Push Targets

{% endif %} + {# List the thingspeak push targets next #} + {% if thingspeak_push_targets.count > 0 %} +

ThingSpeak Push Targets

+
    + {% for push_target in thingspeak_push_targets %} + +
  • +
    + +
    {{ push_target.status }}
    {# TODO - Make this display an error if applicable #} +
    {{ push_target.target_host }}
    +
    +
  • + + {% endfor %} +
+ {% endif %} +

Add Generic Push Target Add Brewer's Friend Push Target Add Brewfather Push Target + Add ThingSpeak Push Target

diff --git a/external_push/templates/external_push/thingspeak_push_target_add.html b/external_push/templates/external_push/thingspeak_push_target_add.html new file mode 100644 index 00000000..2060895c --- /dev/null +++ b/external_push/templates/external_push/thingspeak_push_target_add.html @@ -0,0 +1,57 @@ +{% extends "sitewide/flat_ui_template.html" %} +{% load custom_tags %} + + +{% block title %}ThingSpeak Push Target{% endblock %} + +{% block content %} + +

Add Push Target

+{% if form.errors %} +
Please correct the error {{ form.errors }} below.
+{% endif %} +

+ ThingSpeak for IoT Projects is a IOT data collection, analysis and Reaction Platform. + This will send the data for your chosen device to ThingSpeak in the format that is required for channel integration +

ThingSpeak Channel Settings
+
    +
  • Field 1 - Beer Name
  • +
  • Field 2 - Sensor Name
  • +
  • Field 3 - Temp Format
  • +
  • Field 4 - Beer Temp
  • +
  • Field 5 - Fridge Temp
  • +
  • Field 6 - Room Temp
  • +
  • Field 7 - Beer Gravity
  • +
+

+

+

+ {% csrf_token %} + +
+

Push Target Settings

+ {% form_generic form.name %} + {% form_generic form.push_frequency %} + {% form_generic form.api_key %} + {% form_generic form.brewpi_to_push %} + +
+ + +
+

+ + +{% endblock %} + +{% block scripts %} + + +{% endblock %} + diff --git a/external_push/templates/external_push/thingspeak_push_target_view.html b/external_push/templates/external_push/thingspeak_push_target_view.html new file mode 100644 index 00000000..4e351898 --- /dev/null +++ b/external_push/templates/external_push/thingspeak_push_target_view.html @@ -0,0 +1,62 @@ +{% extends "sitewide/flat_ui_template.html" %} +{% load custom_tags %} + + +{% block title %}ThingSpeak Push Target {{ push_target }}{% endblock %} + +{% block content %} + +

{{ push_target.name }}

+{% if form.errors %} +
Please correct the error {{ form.errors }} below.
+{% endif %} +

+ ThingSpeak for IoT Projects is a IOT data collection, analysis and Reaction Platform. + This will send the data for your chosen device to ThingSpeak in the format that is required for channel integration +

ThingSpeak Channel Settings
+
    +
  • Field 1 - Beer Name
  • +
  • Field 2 - Sensor Name
  • +
  • Field 3 - Temp Format
  • +
  • Field 4 - Beer Temp
  • +
  • Field 5 - Fridge Temp
  • +
  • Field 6 - Room Temp
  • +
  • Field 7 - Beer Gravity
  • +
+

+

+

+ {% csrf_token %} + +
+

Push Target Settings

+ {% form_generic form.name %} + {% form_generic form.push_frequency %} + {% form_generic form.api_key %} + {% form_generic form.brewpi_to_push %} + +
+ + +
+

+ + +

+ Delete Push Target +

+ + +{% endblock %} + +{% block scripts %} + + +{% endblock %} + diff --git a/external_push/urls.py b/external_push/urls.py index a6852d3e..8aae949c 100644 --- a/external_push/urls.py +++ b/external_push/urls.py @@ -24,4 +24,8 @@ url(r'^push/brewfather/view/(?P[0-9]{1,20})/$', external_push.views.external_push_brewfather_view, name='external_push_brewfather_view'), url(r'^push/brewfather/delete/(?P[0-9]{1,20})/$', external_push.views.external_push_brewfather_delete, name='external_push_brewfather_delete'), + url(r'^push/thingspeak/add/$', external_push.views.external_push_thingspeak_target_add, name='external_push_thingspeak_target_add'), + url(r'^push/thingspeak/view/(?P[0-9]{1,20})/$', external_push.views.external_push_thingspeak_view, name='external_push_thingspeak_view'), + url(r'^push/thingspeak/delete/(?P[0-9]{1,20})/$', external_push.views.external_push_thingspeak_delete, name='external_push_thingspeak_delete'), + ] diff --git a/external_push/views.py b/external_push/views.py index fd75d1c7..11d8a865 100644 --- a/external_push/views.py +++ b/external_push/views.py @@ -7,7 +7,7 @@ from django.http import JsonResponse, HttpResponse from django.core.exceptions import ObjectDoesNotExist -from .models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget +from .models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget, ThingSpeakPushTarget import fermentrack_django.settings as settings @@ -31,9 +31,10 @@ def external_push_list(request, context_only=False): all_push_targets = GenericPushTarget.objects.all() brewers_friend_push_targets = BrewersFriendPushTarget.objects.all() brewfather_push_targets = BrewfatherPushTarget.objects.all() + thingspeak_push_targets = ThingSpeakPushTarget.objects.all() context = {'all_push_targets': all_push_targets, 'brewfather_push_targets': brewfather_push_targets, - 'brewers_friend_push_targets': brewers_friend_push_targets} + 'brewers_friend_push_targets': brewers_friend_push_targets, 'thingspeak_push_targets': thingspeak_push_targets} # This allows us to embed this in the site configuration page... There's almost certainly a better way to do this. if not context_only: @@ -287,5 +288,83 @@ def external_push_brewfather_delete(request, push_target_id): return redirect('external_push_list') + + +@login_required +@site_is_configured +def external_push_thingspeak_target_add(request): + # TODO - Add user permissioning + # if not request.user.has_perm('app.add_device'): + # messages.error(request, 'Your account is not permissioned to add devices. Please contact an admin') + # return redirect("/") + + form = forms.ThingSpeakPushTargetModelForm() + + if request.POST: + form = forms.ThingSpeakPushTargetModelForm(request.POST) + if form.is_valid(): + new_push_target = form.save() + messages.success(request, 'Successfully added push target') + + # Update last triggered to force a refresh in the next cycle + new_push_target.last_triggered = new_push_target.last_triggered - datetime.timedelta(seconds=new_push_target.push_frequency) + new_push_target.save() + + return redirect('external_push_list') + + messages.error(request, 'Unable to add new push target') + + # Basically, if we don't get redirected, in every case we're just outputting the same template + return render(request, template_name='external_push/thingspeak_push_target_add.html', context={'form': form}) + +@login_required +@site_is_configured +def external_push_thingspeak_view(request, push_target_id): + # TODO - Add user permissioning + # if not request.user.has_perm('app.add_device'): + # messages.error(request, 'Your account is not permissioned to add devices. Please contact an admin') + # return redirect("/") + + try: + push_target = ThingSpeakPushTarget.objects.get(id=push_target_id) + except ObjectDoesNotExist: + messages.error(request, "ThingSpeak push target {} does not exist".format(push_target_id)) + return redirect('external_push_list') + + if request.POST: + form = forms.ThingSpeakPushTargetModelForm(request.POST, instance=push_target) + if form.is_valid(): + updated_push_target = form.save() + messages.success(request, 'Updated push target') + return redirect('external_push_list') + + messages.error(request, 'Unable to update push target') + + form = forms.ThingSpeakPushTargetModelForm(instance=push_target) + + return render(request, template_name='external_push/thingspeak_push_target_view.html', + context={'push_target': push_target, 'form': form}) + +@login_required +@site_is_configured +def external_push_thingspeak_delete(request, push_target_id): + # TODO - Add user permissioning + # if not request.user.has_perm('app.add_device'): + # messages.error(request, 'Your account is not permissioned to add devices. Please contact an admin') + # return redirect("/") + + try: + push_target = ThingSpeakPushTarget.objects.get(id=push_target_id) + except ObjectDoesNotExist: + messages.error(request, "ThingSpeak push target {} does not exist".format(push_target_id)) + return redirect('external_push_list') + + message = "ThingSpeak push target {} has been deleted".format(push_target_id) + push_target.delete() + messages.success(request, message) + + return redirect('external_push_list') + + From 2b1a162847eb6e9adadedff62dbff237555d2611 Mon Sep 17 00:00:00 2001 From: John Doyle Date: Fri, 20 Dec 2019 12:42:02 +0000 Subject: [PATCH 03/25] Removed Gravity ID from table --- external_push/migrations/0005_ThingSpeak_Support.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/external_push/migrations/0005_ThingSpeak_Support.py b/external_push/migrations/0005_ThingSpeak_Support.py index 118bdf01..03a19352 100644 --- a/external_push/migrations/0005_ThingSpeak_Support.py +++ b/external_push/migrations/0005_ThingSpeak_Support.py @@ -23,10 +23,6 @@ class Migration(migrations.Migration): ('push_frequency', models.IntegerField(choices=[(901, '15 minutes'), (1801, '30 minutes'), (3601, '1 hour')], default=900, help_text='How often to push data to the target')), ('api_key', models.CharField(default='', help_text='Brewers Friend API Key', max_length=256)), ('brewpi_to_push_id', models.ForeignKey(blank=True, default=None, help_text="BrewPi Devices to push (ignored if 'all' devices selected)", related_name='push_targets', to='app.BrewPiDevice')), -<<<<<<< HEAD -======= - ('gravity_sensor_to_push_id', models.ForeignKey(blank=True, default=None, help_text='Gravity Sensor to push (create one push target per sensor to push)', on_delete=django.db.models.deletion.CASCADE, related_name='push_target', to='gravity.GravitySensor')), ->>>>>>> ea192972aa835b5cb5556c7d5b5414a7009d6856 ('error_text', models.TextField(blank=True, default='', help_text='The error (if any) encountered on the last push attempt', null=True)), ('last_triggered', models.DateTimeField(auto_now_add=True, help_text='The last time we pushed data to this target')), ], From 8b20aabc904f6616f9acd2f5d9e0e3a058046a93 Mon Sep 17 00:00:00 2001 From: Magnus Date: Sun, 22 Dec 2019 10:52:10 +0100 Subject: [PATCH 04/25] First update with push support for Grainfather --- external_push/admin.py | 6 +- external_push/forms.py | 8 +- external_push/models.py | 97 +++++++++++++++++++ external_push/tasks.py | 22 ++++- .../grainfather_push_target_add.html | 54 +++++++++++ .../grainfather_push_target_view.html | 46 +++++++++ external_push/urls.py | 3 + external_push/views.py | 81 +++++++++++++++- 8 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 external_push/templates/external_push/grainfather_push_target_add.html create mode 100644 external_push/templates/external_push/grainfather_push_target_view.html diff --git a/external_push/admin.py b/external_push/admin.py index 09ca9479..900d6720 100644 --- a/external_push/admin.py +++ b/external_push/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget +from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget, GrainfatherPushTarget @admin.register(GenericPushTarget) @@ -13,4 +13,8 @@ class BrewersFriendPushTargetAdmin(admin.ModelAdmin): @admin.register(BrewfatherPushTarget) class BrewfatherPushTargetAdmin(admin.ModelAdmin): + list_display = ('gravity_sensor_to_push', 'status', 'push_frequency') + +@admin.register(GrainfatherPushTarget) +class GrainfatherPushTargetAdmin(admin.ModelAdmin): list_display = ('gravity_sensor_to_push', 'status', 'push_frequency') \ No newline at end of file diff --git a/external_push/forms.py b/external_push/forms.py index 223a18df..79768939 100644 --- a/external_push/forms.py +++ b/external_push/forms.py @@ -1,6 +1,6 @@ from django import forms -from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget +from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget, GrainfatherPushTarget from django.core import validators import fermentrack_django.settings as settings @@ -24,3 +24,9 @@ class BrewfatherPushTargetModelForm(ModelForm): class Meta: model = BrewfatherPushTarget fields = ['gravity_sensor_to_push', 'push_frequency', 'logging_url'] + + +class GrainfatherPushTargetModelForm(ModelForm): + class Meta: + model = GrainfatherPushTarget + fields = ['gravity_sensor_to_push', 'push_frequency', 'logging_url'] diff --git a/external_push/models.py b/external_push/models.py index 75c6b7d9..10b1c241 100644 --- a/external_push/models.py +++ b/external_push/models.py @@ -434,3 +434,100 @@ def send_data(self): r = requests.post(self.logging_url, data=json_data, headers=headers) return True # TODO - Check if the post actually succeeded & react accordingly + +class GrainfatherPushTarget(models.Model): + class Meta: + verbose_name = "Grainfather Push Target" + verbose_name_plural = "Grainfather Push Targets" + + STATUS_ACTIVE = 'active' + STATUS_DISABLED = 'disabled' + STATUS_ERROR = 'error' + + STATUS_CHOICES = ( + (STATUS_ACTIVE, 'Active'), + (STATUS_DISABLED, 'Disabled'), + (STATUS_ERROR, 'Error'), + ) + + PUSH_FREQUENCY_CHOICES = ( + (60 * 15 + 1, '15 minutes'), + (60 * 30 + 1, '30 minutes'), + (60 * 60 + 1, '1 hour'), + ) + + status = models.CharField(max_length=24, help_text="Status of this push target", choices=STATUS_CHOICES, + default=STATUS_ACTIVE) + push_frequency = models.IntegerField(choices=PUSH_FREQUENCY_CHOICES, default=60 * 15, + help_text="How often to push data to the target") + logging_url = models.CharField(max_length=256, help_text="Grainfather Logging URL", default="") + + gravity_sensor_to_push = models.ForeignKey(to=GravitySensor, related_name="grainfather_push_target", on_delete=models.CASCADE, + help_text="Gravity Sensor to push (create one push target per " + "sensor to push)") + + error_text = models.TextField(blank=True, null=True, default="", help_text="The error (if any) encountered on the " + "last push attempt") + + last_triggered = models.DateTimeField(help_text="The last time we pushed data to this target", auto_now_add=True) + + # I'm on the fence as to whether or not to test when to trigger by selecting everything from the database and doing + # (last_triggered + push_frequency) < now, or to actually create a "trigger_next_at" field. + # trigger_next_at = models.DateTimeField(default=timezone.now, help_text="When to next trigger a push") + + def __str__(self): + return self.gravity_sensor_to_push.name + + def data_to_push(self): + # For Grainfather, we're just cascading a single gravity sensor downstream to the app + to_send = {'report_source': "Fermentrack", 'name': self.gravity_sensor_to_push.name, 'token':"grainfather", 'ID':"",'angle':"0",'battery':"0", 'interval':"0",'RSSI':"0" } + +#"name":"iSpindel001", +#"ID":14421487, +#"token":"fermentrack", +#"angle":57.54898, +#"temperature":24.1875, +#"temp_units":"C", +#"battery":4.103232, +#"gravity":16.9741, +#"interval":300, +#"RSSI":-68} + + # TODO - Add beer name to what is pushed + + latest_log_point = self.gravity_sensor_to_push.retrieve_latest_point() + + if latest_log_point is None: # If there isn't an available log point, return nothing + return {} + + # For now, if we can't get a latest log point, let's default to just not sending anything. + if latest_log_point.gravity != 0.0: + to_send['gravity'] = float(latest_log_point.gravity) +# to_send['gravity_unit'] = "G" + else: + return {} # Also return nothing if there isn't an available gravity + + # For now all gravity sensors have temp info, but just in case + if latest_log_point.temp is not None: + to_send['temperature'] = float(latest_log_point.temp) + to_send['temp_units'] = latest_log_point.temp_format + + # TODO - Add linked BrewPi temps if we have them + + string_to_send = json.dumps(to_send) + + # We've got the data (in a json'ed string) - lets send it + return string_to_send + + def send_data(self): + # self.data_to_push() returns a JSON-encoded string which we will push directly out + json_data = self.data_to_push() + + if len(json_data) <= 2: + # There was no data to push - do nothing. + return False + + headers = {'Content-Type': 'application/json'} + + r = requests.post(self.logging_url, data=json_data, headers=headers) + return True # TODO - Check if the post actually succeeded & react accordingly diff --git a/external_push/tasks.py b/external_push/tasks.py index d3aaac12..90d05356 100644 --- a/external_push/tasks.py +++ b/external_push/tasks.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from huey import crontab from huey.contrib.djhuey import periodic_task, task, db_periodic_task, db_task -from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget +from external_push.models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget, GrainfatherPushTarget import datetime, pytz, time from django.utils import timezone @@ -44,6 +44,17 @@ def brewfather_push_target_push(target_id): return None +@db_task() +def grainfather_push_target_push(target_id): + try: + push_target = GrainfatherPushTarget.objects.get(id=target_id) + except: + # TODO - Replace with ObjNotFound + return None + + push_target.send_data() + + return None # TODO - At some point write a validation function that will allow us to trigger more often than every minute @db_periodic_task(crontab(minute="*")) @@ -51,6 +62,7 @@ def dispatch_push_tasks(): generic_push_targets = GenericPushTarget.objects.filter(status=GenericPushTarget.STATUS_ACTIVE).all() brewers_friend_push_targets = BrewersFriendPushTarget.objects.filter(status=BrewersFriendPushTarget.STATUS_ACTIVE).all() brewfather_push_targets = BrewfatherPushTarget.objects.filter(status=BrewfatherPushTarget.STATUS_ACTIVE).all() + grainfather_push_targets = GrainfatherPushTarget.objects.filter(status=GrainfatherPushTarget.STATUS_ACTIVE).all() # Run through the list of generic push targets and trigger a (future) data send for each for target in generic_push_targets: @@ -79,5 +91,13 @@ def dispatch_push_tasks(): # Queue the generic_push_target_push task (going to do it asynchronously) brewfather_push_target_push(target.id) + # Run through the list of Grainfather push targets and trigger a (future) data send for each + for target in grainfather_push_targets: + if timezone.now() >= (target.last_triggered + datetime.timedelta(seconds=target.push_frequency)): + target.last_triggered = timezone.now() + target.save() + + # Queue the generic_push_target_push task (going to do it asynchronously) + grainfather_push_target_push(target.id) return None diff --git a/external_push/templates/external_push/grainfather_push_target_add.html b/external_push/templates/external_push/grainfather_push_target_add.html new file mode 100644 index 00000000..4c015527 --- /dev/null +++ b/external_push/templates/external_push/grainfather_push_target_add.html @@ -0,0 +1,54 @@ +{% extends "sitewide/flat_ui_template.html" %} +{% load custom_tags %} + + +{% block title %}Add Grainfather Push Target{% endblock %} + +{% block content %} + +

Add Grainfather Push Target

+{% if form.errors %} +
Please correct the error {{ form.errors }} below.
+{% endif %} +

+ Grainfather is a web-based app that allows for creating brewing recipes, tracking fermentation status, and + much more. With a Grainfather account you can log data from your gravity sensors and attach them to + your brews. +

+ +

+ To set up Grainfather integration, you will need your Custom Logging URL from the + My Equipment page of your Grainfather account. + Add the iSpindel fermentation device and select the logging URL. +

+ + +

+

+ {% csrf_token %} + +
+

Push Target Settings

+ {% form_generic form.push_frequency %} + {% form_generic form.logging_url %} + {% form_generic form.gravity_sensor_to_push %} +
+ + +
+

+ + +{% endblock %} + +{% block scripts %} + + +{% endblock %} + diff --git a/external_push/templates/external_push/grainfather_push_target_view.html b/external_push/templates/external_push/grainfather_push_target_view.html new file mode 100644 index 00000000..5de3013d --- /dev/null +++ b/external_push/templates/external_push/grainfather_push_target_view.html @@ -0,0 +1,46 @@ +{% extends "sitewide/flat_ui_template.html" %} +{% load custom_tags %} + + +{% block title %}Grainfather Push Target {{ push_target }}{% endblock %} + +{% block content %} + +

{{ push_target }}

+{% if form.errors %} +
Please correct the error {{ form.errors }} below.
+{% endif %} +

+

+ {% csrf_token %} + +
+

Push Target Settings

+ {% form_generic form.push_frequency %} + {% form_generic form.logging_url %} + {% form_generic form.gravity_sensor_to_push %} +
+ + +
+

+ + +

+ Delete Push Target +

+ + +{% endblock %} + +{% block scripts %} + + +{% endblock %} + diff --git a/external_push/urls.py b/external_push/urls.py index a6852d3e..358d982e 100644 --- a/external_push/urls.py +++ b/external_push/urls.py @@ -24,4 +24,7 @@ url(r'^push/brewfather/view/(?P[0-9]{1,20})/$', external_push.views.external_push_brewfather_view, name='external_push_brewfather_view'), url(r'^push/brewfather/delete/(?P[0-9]{1,20})/$', external_push.views.external_push_brewfather_delete, name='external_push_brewfather_delete'), + url(r'^push/grainfather/add/$', external_push.views.external_push_grainfather_target_add, name='external_push_grainfather_target_add'), + url(r'^push/grainfather/view/(?P[0-9]{1,20})/$', external_push.views.external_push_grainfather_view, name='external_push_grainfather_view'), + url(r'^push/grainfather/delete/(?P[0-9]{1,20})/$', external_push.views.external_push_grainfather_delete, name='external_push_grainfather_delete'), ] diff --git a/external_push/views.py b/external_push/views.py index fd75d1c7..054a80ab 100644 --- a/external_push/views.py +++ b/external_push/views.py @@ -7,7 +7,7 @@ from django.http import JsonResponse, HttpResponse from django.core.exceptions import ObjectDoesNotExist -from .models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget +from .models import GenericPushTarget, BrewersFriendPushTarget, BrewfatherPushTarget, GrainfatherPushTarget import fermentrack_django.settings as settings @@ -31,9 +31,10 @@ def external_push_list(request, context_only=False): all_push_targets = GenericPushTarget.objects.all() brewers_friend_push_targets = BrewersFriendPushTarget.objects.all() brewfather_push_targets = BrewfatherPushTarget.objects.all() + grainfather_push_targets = GrainfatherPushTarget.objects.all() context = {'all_push_targets': all_push_targets, 'brewfather_push_targets': brewfather_push_targets, - 'brewers_friend_push_targets': brewers_friend_push_targets} + 'brewers_friend_push_targets': brewers_friend_push_targets, 'grainfather_push_targets': grainfather_push_targets} # This allows us to embed this in the site configuration page... There's almost certainly a better way to do this. if not context_only: @@ -286,6 +287,82 @@ def external_push_brewfather_delete(request, push_target_id): return redirect('external_push_list') +@login_required +@site_is_configured +def external_push_grainfather_target_add(request): + # TODO - Add user permissioning + # if not request.user.has_perm('app.add_device'): + # messages.error(request, 'Your account is not permissioned to add devices. Please contact an admin') + # return redirect("/") + + form = forms.GrainfatherPushTargetModelForm() + + if request.POST: + form = forms.GrainfatherPushTargetModelForm(request.POST) + if form.is_valid(): + new_push_target = form.save() + messages.success(request, 'Successfully added push target') + + # Update last triggered to force a refresh in the next cycle + new_push_target.last_triggered = new_push_target.last_triggered - datetime.timedelta(seconds=new_push_target.push_frequency) + new_push_target.save() + + return redirect('external_push_list') + + messages.error(request, 'Unable to add new push target') + + # Basically, if we don't get redirected, in every case we're just outputting the same template + return render(request, template_name='external_push/grainfather_push_target_add.html', context={'form': form}) + + +@login_required +@site_is_configured +def external_push_grainfather_view(request, push_target_id): + # TODO - Add user permissioning + # if not request.user.has_perm('app.add_device'): + # messages.error(request, 'Your account is not permissioned to add devices. Please contact an admin') + # return redirect("/") + + try: + push_target = GrainfatherPushTarget.objects.get(id=push_target_id) + except ObjectDoesNotExist: + messages.error(request, "Grainfather push target {} does not exist".format(push_target_id)) + return redirect('external_push_list') + + if request.POST: + form = forms.GrainfatherPushTargetModelForm(request.POST, instance=push_target) + if form.is_valid(): + updated_push_target = form.save() + messages.success(request, 'Updated push target') + return redirect('external_push_list') + + messages.error(request, 'Unable to update push target') + + form = forms.GrainfatherPushTargetModelForm(instance=push_target) + + return render(request, template_name='external_push/grainfather_push_target_view.html', + context={'push_target': push_target, 'form': form}) + + +@login_required +@site_is_configured +def external_push_grainfather_delete(request, push_target_id): + # TODO - Add user permissioning + # if not request.user.has_perm('app.add_device'): + # messages.error(request, 'Your account is not permissioned to add devices. Please contact an admin') + # return redirect("/") + + try: + push_target = GrainfatherPushTarget.objects.get(id=push_target_id) + except ObjectDoesNotExist: + messages.error(request, "Grainfather push target {} does not exist".format(push_target_id)) + return redirect('external_push_list') + + message = "Grainfather push target {} has been deleted".format(push_target_id) + push_target.delete() + messages.success(request, message) + + return redirect('external_push_list') From 8cc09b1aa45f4f33e240d4d4a79462069a67953a Mon Sep 17 00:00:00 2001 From: Magnus Date: Mon, 23 Dec 2019 05:49:23 -0800 Subject: [PATCH 05/25] Working copy of grainfather support --- external_push/forms.py | 2 +- .../migrations/0005_Grainfather_Support.py | 34 +++++++++++++++++++ external_push/models.py | 31 ++++++++++------- .../grainfather_push_target_add.html | 5 ++- .../grainfather_push_target_view.html | 1 + .../push_target_list_embeddable.html | 17 ++++++++++ 6 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 external_push/migrations/0005_Grainfather_Support.py diff --git a/external_push/forms.py b/external_push/forms.py index 79768939..d2fbbaa7 100644 --- a/external_push/forms.py +++ b/external_push/forms.py @@ -29,4 +29,4 @@ class Meta: class GrainfatherPushTargetModelForm(ModelForm): class Meta: model = GrainfatherPushTarget - fields = ['gravity_sensor_to_push', 'push_frequency', 'logging_url'] + fields = ['gravity_sensor_to_push', 'push_frequency', 'logging_url', 'gf_name'] diff --git a/external_push/migrations/0005_Grainfather_Support.py b/external_push/migrations/0005_Grainfather_Support.py new file mode 100644 index 00000000..c2241481 --- /dev/null +++ b/external_push/migrations/0005_Grainfather_Support.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.27 on 2019-12-23 11:22 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('gravity', '0004_BrewersFriend_Support'), + ('external_push', '0004_Brewfather_Support'), + ] + + operations = [ + migrations.CreateModel( + name='GrainfatherPushTarget', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('active', 'Active'), ('disabled', 'Disabled'), ('error', 'Error')], default='active', help_text='Status of this push target', max_length=24)), + ('push_frequency', models.IntegerField(choices=[(901, '15 minutes'), (1801, '30 minutes'), (3601, '1 hour')], default=900, help_text='How often to push data to the target')), + ('logging_url', models.CharField(default='', help_text='Grainfather Logging URL', max_length=256)), + ('gf_name', models.CharField(default='', help_text='Grainfather Name (from brew)', max_length=256)), + ('error_text', models.TextField(blank=True, default='', help_text='The error (if any) encountered on the last push attempt', null=True)), + ('last_triggered', models.DateTimeField(auto_now_add=True, help_text='The last time we pushed data to this target')), + ('gravity_sensor_to_push', models.ForeignKey(help_text='Gravity Sensor to push (create one push target per sensor to push)', on_delete=django.db.models.deletion.CASCADE, related_name='grainfather_push_target', to='gravity.GravitySensor')), + ], + options={ + 'verbose_name': 'Grainfather Push Target', + 'verbose_name_plural': 'Grainfather Push Targets', + }, + ), + ] diff --git a/external_push/models.py b/external_push/models.py index 10b1c241..653a091a 100644 --- a/external_push/models.py +++ b/external_push/models.py @@ -435,6 +435,8 @@ def send_data(self): r = requests.post(self.logging_url, data=json_data, headers=headers) return True # TODO - Check if the post actually succeeded & react accordingly + + class GrainfatherPushTarget(models.Model): class Meta: verbose_name = "Grainfather Push Target" @@ -461,6 +463,7 @@ class Meta: push_frequency = models.IntegerField(choices=PUSH_FREQUENCY_CHOICES, default=60 * 15, help_text="How often to push data to the target") logging_url = models.CharField(max_length=256, help_text="Grainfather Logging URL", default="") + gf_name = models.CharField(max_length=256, help_text="Grainfather brew id (number)", default="") gravity_sensor_to_push = models.ForeignKey(to=GravitySensor, related_name="grainfather_push_target", on_delete=models.CASCADE, help_text="Gravity Sensor to push (create one push target per " @@ -480,18 +483,19 @@ def __str__(self): def data_to_push(self): # For Grainfather, we're just cascading a single gravity sensor downstream to the app - to_send = {'report_source': "Fermentrack", 'name': self.gravity_sensor_to_push.name, 'token':"grainfather", 'ID':"",'angle':"0",'battery':"0", 'interval':"0",'RSSI':"0" } - -#"name":"iSpindel001", -#"ID":14421487, -#"token":"fermentrack", -#"angle":57.54898, -#"temperature":24.1875, -#"temp_units":"C", -#"battery":4.103232, -#"gravity":16.9741, -#"interval":300, -#"RSSI":-68} + #to_send = {'report_source': "Fermentrack", 'name': self.gravity_sensor_to_push.name, 'token':"grainfather", 'ID':0,'angle':0,'battery':0, 'interval':900,'RSSI':0 } + to_send = {'report_source': "Fermentrack", 'name': self.gf_name + ",SG", 'token':"grainfather", 'ID':0,'angle':0,'battery':0, 'interval':900,'RSSI':0 } + + #"name":"nnnn,SG", // Ending with SG will force the use of our calculated gravity + #"ID":14421487, + #"token":"fermentrack", + #"angle":57.54898, + #"temperature":24.1875, + #"temp_units":"C", + #"battery":4.103232, + #"gravity":16.9741, + #"interval":300, + #"RSSI":-68} # TODO - Add beer name to what is pushed @@ -503,7 +507,7 @@ def data_to_push(self): # For now, if we can't get a latest log point, let's default to just not sending anything. if latest_log_point.gravity != 0.0: to_send['gravity'] = float(latest_log_point.gravity) -# to_send['gravity_unit'] = "G" + #to_send['gravity_unit'] = "G" else: return {} # Also return nothing if there isn't an available gravity @@ -531,3 +535,4 @@ def send_data(self): r = requests.post(self.logging_url, data=json_data, headers=headers) return True # TODO - Check if the post actually succeeded & react accordingly + diff --git a/external_push/templates/external_push/grainfather_push_target_add.html b/external_push/templates/external_push/grainfather_push_target_add.html index 4c015527..86ce8106 100644 --- a/external_push/templates/external_push/grainfather_push_target_add.html +++ b/external_push/templates/external_push/grainfather_push_target_add.html @@ -19,7 +19,9 @@

Add Grainfather Push Target

To set up Grainfather integration, you will need your Custom Logging URL from the My Equipment page of your Grainfather account. - Add the iSpindel fermentation device and select the logging URL. + Add an iSpindel fermentation device and select the logging URL. Data sent from fermentrack will have this format + independant of your device. The Grainfather name field should contain the number that is defined when adding the + iSpindel to your brew session.

@@ -31,6 +33,7 @@

Add Grainfather Push Target

Push Target Settings

{% form_generic form.push_frequency %} {% form_generic form.logging_url %} + {% form_generic form.gf_name %} {% form_generic form.gravity_sensor_to_push %} diff --git a/external_push/templates/external_push/grainfather_push_target_view.html b/external_push/templates/external_push/grainfather_push_target_view.html index 5de3013d..9f6e2135 100644 --- a/external_push/templates/external_push/grainfather_push_target_view.html +++ b/external_push/templates/external_push/grainfather_push_target_view.html @@ -18,6 +18,7 @@

{{ push_target }}

Push Target Settings

{% form_generic form.push_frequency %} {% form_generic form.logging_url %} + {% form_generic form.gf_name %} {% form_generic form.gravity_sensor_to_push %} diff --git a/external_push/templates/external_push/push_target_list_embeddable.html b/external_push/templates/external_push/push_target_list_embeddable.html index 9c7d770a..5bf8dc14 100644 --- a/external_push/templates/external_push/push_target_list_embeddable.html +++ b/external_push/templates/external_push/push_target_list_embeddable.html @@ -57,9 +57,26 @@

Brewfather Push Targets

{% endif %} + {# Then list the Grainfather push targets #} + {% if grainfather_push_targets.count > 0 %} +

Grainfather Push Targets

+
    + {% for push_target in grainfather_push_targets %} + +
  • +
    + +
    {{ push_target.status }}
    {# TODO - Make this display an error if applicable #} +
    +
  • + + {% endfor %} +
+ {% endif %}

Add Generic Push Target Add Brewer's Friend Push Target Add Brewfather Push Target + Add Grainfather Push Target

From b5a676d507a67c7cd11d8395c5ea26527d36307a Mon Sep 17 00:00:00 2001 From: Magnus Date: Mon, 23 Dec 2019 05:59:25 -0800 Subject: [PATCH 06/25] Fixed db migration to grainfather --- external_push/migrations/0005_Grainfather_Support.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/external_push/migrations/0005_Grainfather_Support.py b/external_push/migrations/0005_Grainfather_Support.py index c2241481..9268efb8 100644 --- a/external_push/migrations/0005_Grainfather_Support.py +++ b/external_push/migrations/0005_Grainfather_Support.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.27 on 2019-12-23 11:22 +# Generated by Django 1.11.27 on 2019-12-23 13:58 from __future__ import unicode_literals from django.db import migrations, models @@ -21,7 +21,7 @@ class Migration(migrations.Migration): ('status', models.CharField(choices=[('active', 'Active'), ('disabled', 'Disabled'), ('error', 'Error')], default='active', help_text='Status of this push target', max_length=24)), ('push_frequency', models.IntegerField(choices=[(901, '15 minutes'), (1801, '30 minutes'), (3601, '1 hour')], default=900, help_text='How often to push data to the target')), ('logging_url', models.CharField(default='', help_text='Grainfather Logging URL', max_length=256)), - ('gf_name', models.CharField(default='', help_text='Grainfather Name (from brew)', max_length=256)), + ('gf_name', models.CharField(default='', help_text='Grainfather brew id (number)', max_length=256)), ('error_text', models.TextField(blank=True, default='', help_text='The error (if any) encountered on the last push attempt', null=True)), ('last_triggered', models.DateTimeField(auto_now_add=True, help_text='The last time we pushed data to this target')), ('gravity_sensor_to_push', models.ForeignKey(help_text='Gravity Sensor to push (create one push target per sensor to push)', on_delete=django.db.models.deletion.CASCADE, related_name='grainfather_push_target', to='gravity.GravitySensor')), From 78e30265abc70c34ddac1c5cbe2d23dfc5c2c6d6 Mon Sep 17 00:00:00 2001 From: Magnus Date: Mon, 23 Dec 2019 06:26:34 -0800 Subject: [PATCH 07/25] Fixing formatting --- external_push/admin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/external_push/admin.py b/external_push/admin.py index 900d6720..d2c1a998 100644 --- a/external_push/admin.py +++ b/external_push/admin.py @@ -17,4 +17,5 @@ class BrewfatherPushTargetAdmin(admin.ModelAdmin): @admin.register(GrainfatherPushTarget) class GrainfatherPushTargetAdmin(admin.ModelAdmin): - list_display = ('gravity_sensor_to_push', 'status', 'push_frequency') \ No newline at end of file + list_display = ('gravity_sensor_to_push', 'status', 'push_frequency') + \ No newline at end of file From 4797a313fc4ce12a373417d2b8fab61489fbb5ab Mon Sep 17 00:00:00 2001 From: Magnus Date: Wed, 25 Dec 2019 01:17:44 -0800 Subject: [PATCH 08/25] Added docs for Grainfather push --- docs/source/develop/push_support.rst | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/source/develop/push_support.rst b/docs/source/develop/push_support.rst index e50c9a2f..614c2e17 100644 --- a/docs/source/develop/push_support.rst +++ b/docs/source/develop/push_support.rst @@ -12,11 +12,12 @@ future - support pushing via TCP (sockets)). Supported "Push" Targets ------------------------------ -Fermentrack currently supports three push targets: +Fermentrack currently supports four push targets: - **"Generic" Push Target** - Fermentrack's "native" push format - Pushes both specific gravity & temperature data - **Brewer's Friend** - Fermentrack's "native" push format - Pushes both specific gravity & temperature data - **Brewfather** - Fermentrack's "native" push format - Pushes both specific gravity & temperature data +- **Grainfather** - ispindel push format - Pushes both specific gravity & temperature data @@ -93,6 +94,22 @@ Fermentrack supports pushing data from specific gravity sensors to Brewfather us Within 60 seconds, Fermentrack will begin sending data from your gravity sensor to Brewfather. This data can be seen on the `Devices `_ page. +Grainfather Support +*********************** + +Fermentrack supports pushing data from specific gravity sensors (Gravity & Temperature) to Grainfather using the brew tracking API. To configure: + +#. Log into your Grainfather account and select Equipment. +#. Add a Fermentation device and select iSpindel as device type. Fermentrack will push data in this format independant of what your device is. Copy the logging URL. +#. The second thing you need to do is to go to an active brew and link the device to a brew session. This is done under the headline fermentration tracking and the function "Add Tracking Device". Make note of the Name value (this is the brew ID). +#. Log into Fermentrack and click the "gear" icon in the upper right +#. Click "Add Grainfather Push Target" at the bottom of the page +#. Within Fermentrack, paste the Logging URL you just copied into the "Logging URL" field and enter the name (brew id) under the "gf_name" field. +#. Set the desired push frequency and select the gravity sensor from which you want to push data +#. Click "Add Push Target" + +Within 60 seconds, Fermentrack will begin sending data from your gravity sensor to Grainfather. This data can be seen in your Grainfather account under Equipment or the Brew Session. + Implementation Notes ------------------------------ From 779e6435d1469fe454f9635a54745c044dc3e803 Mon Sep 17 00:00:00 2001 From: Magnus Date: Wed, 25 Dec 2019 01:47:59 -0800 Subject: [PATCH 09/25] added documentation for gf push target --- docs/source/develop/push_support.rst | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/source/develop/push_support.rst b/docs/source/develop/push_support.rst index e50c9a2f..614c2e17 100644 --- a/docs/source/develop/push_support.rst +++ b/docs/source/develop/push_support.rst @@ -12,11 +12,12 @@ future - support pushing via TCP (sockets)). Supported "Push" Targets ------------------------------ -Fermentrack currently supports three push targets: +Fermentrack currently supports four push targets: - **"Generic" Push Target** - Fermentrack's "native" push format - Pushes both specific gravity & temperature data - **Brewer's Friend** - Fermentrack's "native" push format - Pushes both specific gravity & temperature data - **Brewfather** - Fermentrack's "native" push format - Pushes both specific gravity & temperature data +- **Grainfather** - ispindel push format - Pushes both specific gravity & temperature data @@ -93,6 +94,22 @@ Fermentrack supports pushing data from specific gravity sensors to Brewfather us Within 60 seconds, Fermentrack will begin sending data from your gravity sensor to Brewfather. This data can be seen on the `Devices `_ page. +Grainfather Support +*********************** + +Fermentrack supports pushing data from specific gravity sensors (Gravity & Temperature) to Grainfather using the brew tracking API. To configure: + +#. Log into your Grainfather account and select Equipment. +#. Add a Fermentation device and select iSpindel as device type. Fermentrack will push data in this format independant of what your device is. Copy the logging URL. +#. The second thing you need to do is to go to an active brew and link the device to a brew session. This is done under the headline fermentration tracking and the function "Add Tracking Device". Make note of the Name value (this is the brew ID). +#. Log into Fermentrack and click the "gear" icon in the upper right +#. Click "Add Grainfather Push Target" at the bottom of the page +#. Within Fermentrack, paste the Logging URL you just copied into the "Logging URL" field and enter the name (brew id) under the "gf_name" field. +#. Set the desired push frequency and select the gravity sensor from which you want to push data +#. Click "Add Push Target" + +Within 60 seconds, Fermentrack will begin sending data from your gravity sensor to Grainfather. This data can be seen in your Grainfather account under Equipment or the Brew Session. + Implementation Notes ------------------------------ From d3d3d74dec0a6c6539ec2d4093ee820242767df5 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Wed, 25 Dec 2019 09:37:45 -0500 Subject: [PATCH 10/25] Eliminating conflicting migration numbers --- .../{0005_Grainfather_Support.py => 0006_Grainfather_Support.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename external_push/migrations/{0005_Grainfather_Support.py => 0006_Grainfather_Support.py} (100%) diff --git a/external_push/migrations/0005_Grainfather_Support.py b/external_push/migrations/0006_Grainfather_Support.py similarity index 100% rename from external_push/migrations/0005_Grainfather_Support.py rename to external_push/migrations/0006_Grainfather_Support.py From 61afbc80b13dec93f0709025e7cdc04e04ac3a0c Mon Sep 17 00:00:00 2001 From: Thorrak Date: Wed, 25 Dec 2019 09:38:02 -0500 Subject: [PATCH 11/25] Eliminating conflicting migration numbers --- external_push/migrations/0005_ThingSpeak_Support.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external_push/migrations/0005_ThingSpeak_Support.py b/external_push/migrations/0005_ThingSpeak_Support.py index 03a19352..a1b6739c 100644 --- a/external_push/migrations/0005_ThingSpeak_Support.py +++ b/external_push/migrations/0005_ThingSpeak_Support.py @@ -10,7 +10,7 @@ class Migration(migrations.Migration): dependencies = [ ('gravity', '0004_BrewersFriend_Support'), - ('external_push', '0003_BrewersFriend_Support'), + ('external_push', '0005_ThingSpeak_Support'), ] operations = [ From 06d1ad80388d0aa0cb7c43cd6066f2eb2e663fc3 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Thu, 26 Dec 2019 12:02:58 -0500 Subject: [PATCH 12/25] Lock panel display to 1 decimal place (Fixes #406) --- app/api/lcd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/lcd.py b/app/api/lcd.py index 09c02cfd..cb5238e0 100644 --- a/app/api/lcd.py +++ b/app/api/lcd.py @@ -33,7 +33,7 @@ def temp_text(temp, temp_format): if temp == 0: return "--° {}".format(temp_format) else: - return "{}° {}".format(temp, temp_format) + return "{:.1f}° {}".format(temp, temp_format) ret = [] try: From bd16c907f90ff7d31dc2ec0b2dac7b8b6f3ee4c6 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Thu, 26 Dec 2019 12:09:27 -0500 Subject: [PATCH 13/25] Update changelog --- docs/source/develop/changelog.rst | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/source/develop/changelog.rst b/docs/source/develop/changelog.rst index 7831cecb..d7add25b 100644 --- a/docs/source/develop/changelog.rst +++ b/docs/source/develop/changelog.rst @@ -7,28 +7,30 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) because it -[Unversioned] - Fixes -~~~~~~~~~~~~~~~~~~~~~ +[Unversioned] - ThingSpeak and Grainfather Support +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Fixed +Added --------------------- - -- Fixed where Fahrenheit readings coming from an iSpindel could be improperly reconverted to Fahrenheit +- Added support for pushing data to ThingSpeak (thanks @johndoyle!) +- Added support for pushing data to Grainfather (thanks @mp-se!) +Changed +--------------------- +- Gravity sensors attached to BrewPi controllers will now send those controller's temps to Brewfather -[Unversioned] - Changes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Changed +Fixed --------------------- -- Gravity sensors attached to BrewPi controllers will now send those controller's temps to Brewfather +- Fixed where Fahrenheit readings coming from an iSpindel could be improperly reconverted to Fahrenheit +- Lock temperature display on dashboard panels to one decimal place [2019-12-15] - Brewer's Friend, Brewfather, and MacOS BLE Support -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added --------------------- From 7bef22c17d6e6426edbdb2bef0351917c7e1949a Mon Sep 17 00:00:00 2001 From: Thorrak Date: Sun, 29 Dec 2019 12:43:13 -0500 Subject: [PATCH 14/25] Update requirements.txt for latest Circus install --- requirements.txt | 6 +++--- requirements_macos.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 838df7dd..96ef0521 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ raven # OPTIONAL - used for debugging django-constance[database] # for managing user-configured constants GitPython # for managing git integration zeroconf # for locating ESP8266 devices -circus # for managing Fermentrack wsgi & brewpi.py processes +circus>=0.16.0,<0.17.0 # for managing Fermentrack wsgi & brewpi.py processes #circus-web # for managing Fermentrack wsgi & brewpi.py processes chaussette # for managing Fermentrack wsgi huey>=2.0 # asynchronous task queue @@ -24,6 +24,6 @@ pybluez # for gravity sensor support aioblescan # Replacement for beacontools for Tilt support # Requirements to make Circus work -pyzmq<17 -tornado<5 +#pyzmq<17 +#tornado<5 diff --git a/requirements_macos.txt b/requirements_macos.txt index b1673c18..172dbdb6 100644 --- a/requirements_macos.txt +++ b/requirements_macos.txt @@ -6,7 +6,7 @@ raven # OPTIONAL - used for debugging django-constance[database] # for managing user-configured constants GitPython # for managing git integration zeroconf # for locating ESP8266 devices -circus # for managing Fermentrack wsgi & brewpi.py processes +circus>=0.16.0,<0.17.0 # for managing Fermentrack wsgi & brewpi.py processes #circus-web # for managing Fermentrack wsgi & brewpi.py processes chaussette # for managing Fermentrack wsgi huey>=2.0 # asynchronous task queue @@ -25,7 +25,7 @@ redis # for huey & gravity sensor support PyObjc # For bluetooth support # Requirements to make Circus work -pyzmq<17 -tornado<5 +#pyzmq<17 +#tornado<5 mod_wsgi # python in apache on macos From 4e0a4c73b9ab2955f311992e065b31932827d925 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Sun, 29 Dec 2019 12:47:45 -0500 Subject: [PATCH 15/25] Fix circular migration dependencies --- external_push/migrations/0005_ThingSpeak_Support.py | 2 +- external_push/migrations/0006_Grainfather_Support.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external_push/migrations/0005_ThingSpeak_Support.py b/external_push/migrations/0005_ThingSpeak_Support.py index a1b6739c..4fd35d4a 100644 --- a/external_push/migrations/0005_ThingSpeak_Support.py +++ b/external_push/migrations/0005_ThingSpeak_Support.py @@ -10,7 +10,7 @@ class Migration(migrations.Migration): dependencies = [ ('gravity', '0004_BrewersFriend_Support'), - ('external_push', '0005_ThingSpeak_Support'), + ('external_push', '0004_Brewfather_Support'), ] operations = [ diff --git a/external_push/migrations/0006_Grainfather_Support.py b/external_push/migrations/0006_Grainfather_Support.py index 9268efb8..2ffdc27c 100644 --- a/external_push/migrations/0006_Grainfather_Support.py +++ b/external_push/migrations/0006_Grainfather_Support.py @@ -10,7 +10,7 @@ class Migration(migrations.Migration): dependencies = [ ('gravity', '0004_BrewersFriend_Support'), - ('external_push', '0004_Brewfather_Support'), + ('external_push', '0005_ThingSpeak_Support'), ] operations = [ From 864cca12e18c70f2eef8658b88d617a89c89e01c Mon Sep 17 00:00:00 2001 From: Thorrak Date: Sun, 29 Dec 2019 12:53:25 -0500 Subject: [PATCH 16/25] Enable admin for ThingSpeak push target --- external_push/admin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/external_push/admin.py b/external_push/admin.py index 852ac993..5c99e94e 100644 --- a/external_push/admin.py +++ b/external_push/admin.py @@ -15,10 +15,9 @@ class BrewersFriendPushTargetAdmin(admin.ModelAdmin): class BrewfatherPushTargetAdmin(admin.ModelAdmin): list_display = ('gravity_sensor_to_push', 'status', 'push_frequency') -# TODO - Reenable this -#@admin.register(ThingSpeakPushTarget) -#class ThingSpeakPushTargetAdmin(admin.ModelAdmin): -# list_display = ('gravity_sensors_to_push', 'status', 'push_frequency', 'target_host') +@admin.register(ThingSpeakPushTarget) +class ThingSpeakPushTargetAdmin(admin.ModelAdmin): + list_display = ('name', 'status') @admin.register(GrainfatherPushTarget) class GrainfatherPushTargetAdmin(admin.ModelAdmin): From ef03da4e2d805eb3e3e0818053bd957591f52785 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Sun, 29 Dec 2019 12:54:05 -0500 Subject: [PATCH 17/25] Remove extraneous requirements --- requirements.txt | 4 +--- requirements_macos.txt | 3 --- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 96ef0521..49fdb76a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,5 @@ redis # for huey & gravity sensor support pybluez # for gravity sensor support aioblescan # Replacement for beacontools for Tilt support -# Requirements to make Circus work -#pyzmq<17 -#tornado<5 + diff --git a/requirements_macos.txt b/requirements_macos.txt index 172dbdb6..01a94423 100644 --- a/requirements_macos.txt +++ b/requirements_macos.txt @@ -24,8 +24,5 @@ redis # for huey & gravity sensor support #aioblescan # Replacement for beacontools for Tilt support PyObjc # For bluetooth support -# Requirements to make Circus work -#pyzmq<17 -#tornado<5 mod_wsgi # python in apache on macos From b9d4c87786b1a291db6ce4418143b232c8160cda Mon Sep 17 00:00:00 2001 From: Thorrak Date: Sun, 29 Dec 2019 12:56:32 -0500 Subject: [PATCH 18/25] Update requirements.txt for updated circus requirements --- requirements.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 838df7dd..84f14af9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ raven # OPTIONAL - used for debugging django-constance[database] # for managing user-configured constants GitPython # for managing git integration zeroconf # for locating ESP8266 devices -circus # for managing Fermentrack wsgi & brewpi.py processes +circus>=0.16.0,<0.17.0 # for managing Fermentrack wsgi & brewpi.py processes #circus-web # for managing Fermentrack wsgi & brewpi.py processes chaussette # for managing Fermentrack wsgi huey>=2.0 # asynchronous task queue @@ -23,7 +23,4 @@ redis # for huey & gravity sensor support pybluez # for gravity sensor support aioblescan # Replacement for beacontools for Tilt support -# Requirements to make Circus work -pyzmq<17 -tornado<5 From 938f443d800347293b9c7dc3e1e5c56e8b0f667b Mon Sep 17 00:00:00 2001 From: Thorrak Date: Sun, 2 Feb 2020 23:20:56 -0500 Subject: [PATCH 19/25] Actually fix bug where names are checked for uniqueness on updates to an existing object --- app/device_forms.py | 15 ++++++++------- docs/source/develop/changelog.rst | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/device_forms.py b/app/device_forms.py index 2018448f..651cc817 100644 --- a/app/device_forms.py +++ b/app/device_forms.py @@ -80,13 +80,14 @@ def clean_device_name(self): else: device_name = self.cleaned_data['device_name'] - try: - existing_device = BrewPiDevice.objects.get(device_name=device_name) - raise forms.ValidationError("A device already exists with the name {}".format(device_name)) - - except ObjectDoesNotExist: - # There was no existing device - we're good. - return device_name + # try: + # existing_device = BrewPiDevice.objects.get(device_name=device_name) + # raise forms.ValidationError("A device already exists with the name {}".format(device_name)) + # + # except ObjectDoesNotExist: + # # There was no existing device - we're good. + # return device_name + return device_name def clean(self): cleaned_data = self.cleaned_data diff --git a/docs/source/develop/changelog.rst b/docs/source/develop/changelog.rst index d7add25b..d2003be5 100644 --- a/docs/source/develop/changelog.rst +++ b/docs/source/develop/changelog.rst @@ -26,6 +26,7 @@ Fixed - Fixed where Fahrenheit readings coming from an iSpindel could be improperly reconverted to Fahrenheit - Lock temperature display on dashboard panels to one decimal place +- Allow updates to controller settings when controller name isn't changing (for real this time) From 892a254fd37cd980225e44dc01c02beeb4a28cc0 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Sun, 2 Feb 2020 23:29:55 -0500 Subject: [PATCH 20/25] Update ThingSpeak migrations (Fixes #416) --- .../migrations/0007_fix_thingspeak.py | 36 +++++++++++++++++++ fermentrack_django/settings.py | 1 + 2 files changed, 37 insertions(+) create mode 100644 external_push/migrations/0007_fix_thingspeak.py diff --git a/external_push/migrations/0007_fix_thingspeak.py b/external_push/migrations/0007_fix_thingspeak.py new file mode 100644 index 00000000..3d06a650 --- /dev/null +++ b/external_push/migrations/0007_fix_thingspeak.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.27 on 2020-02-03 04:28 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0009_auto_20180709_0256'), + ('external_push', '0006_Grainfather_Support'), + ] + + operations = [ + migrations.RemoveField( + model_name='thingspeakpushtarget', + name='brewpi_to_push_id', + ), + migrations.AddField( + model_name='thingspeakpushtarget', + name='brewpi_to_push', + field=models.ForeignKey(blank=True, default=None, help_text="BrewPi Devices to push (ignored if 'all' devices selected)", on_delete=django.db.models.deletion.CASCADE, related_name='thingspeak_push_targets', to='app.BrewPiDevice'), + ), + migrations.AlterField( + model_name='thingspeakpushtarget', + name='api_key', + field=models.CharField(default='', help_text='ThingSpeak Channel API Key', max_length=256), + ), + migrations.AlterField( + model_name='thingspeakpushtarget', + name='push_frequency', + field=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'), + ), + ] diff --git a/fermentrack_django/settings.py b/fermentrack_django/settings.py index 1aa782a1..02c8c016 100644 --- a/fermentrack_django/settings.py +++ b/fermentrack_django/settings.py @@ -42,6 +42,7 @@ 'huey.contrib.djhuey', ] +# TODO - Check the below as I'm getting errors when running on MacOS w/o Apache if sys.platform == "darwin": INSTALLED_APPS += 'mod_wsgi.server', # Used for the macOS setup From 7002fe7f3634263a3df31132c0bfc38920d1b0e1 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Sun, 2 Feb 2020 23:44:23 -0500 Subject: [PATCH 21/25] Update ThingSpeak migrations (Fixes #416) --- docs/source/develop/changelog.rst | 2 +- gravity/views.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/develop/changelog.rst b/docs/source/develop/changelog.rst index d2003be5..97f80396 100644 --- a/docs/source/develop/changelog.rst +++ b/docs/source/develop/changelog.rst @@ -27,7 +27,7 @@ Fixed - Fixed where Fahrenheit readings coming from an iSpindel could be improperly reconverted to Fahrenheit - Lock temperature display on dashboard panels to one decimal place - Allow updates to controller settings when controller name isn't changing (for real this time) - +- Fix bug that would default all Tilts to 'Bluetooth' even when a TiltBridge was selected [2019-12-15] - Brewer's Friend, Brewfather, and MacOS BLE Support diff --git a/gravity/views.py b/gravity/views.py index 1686e485..0fbd5557 100644 --- a/gravity/views.py +++ b/gravity/views.py @@ -82,6 +82,7 @@ def gravity_add_board(request): tilt_config = TiltConfiguration( sensor=sensor, color=tilt_form.cleaned_data['color'], + connection_type=tilt_form.cleaned_data['connection_type'], tiltbridge=tilt_bridge, ) tilt_config.save() From 5a30073d343d3a8147cad026385736929816b2c4 Mon Sep 17 00:00:00 2001 From: NecroBrews Date: Wed, 12 Feb 2020 08:48:17 +1300 Subject: [PATCH 22/25] Fix for the tilt not reporting any values --- gravity/tilt/tilt_monitor_aio.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/gravity/tilt/tilt_monitor_aio.py b/gravity/tilt/tilt_monitor_aio.py index f098a349..19798d56 100644 --- a/gravity/tilt/tilt_monitor_aio.py +++ b/gravity/tilt/tilt_monitor_aio.py @@ -6,6 +6,7 @@ import asyncio # import argparse, re import aioblescan as aiobs +import logging # done before importing django app as it does setup import tilt_monitor_utils @@ -23,6 +24,9 @@ verbose = tilt_monitor_utils.verbose mydev = tilt_monitor_utils.bluetooth_device +LOG = logging.getLogger("tiltTest") +LOG.setLevel(logging.INFO) + #### The main loop # Create a list of TiltHydrometer objects for us to use @@ -45,6 +49,7 @@ def processBLEBeacon(data): if ev.raw_data is None: if verbose: print("Event has no raw_data\r\n") + LOG.error("Event has no raw data") return False raw_data_hex = ev.raw_data.hex() @@ -64,16 +69,24 @@ def processBLEBeacon(data): try: # Let's use some of the functions of aioblesscan to tease out the mfg_specific_data payload - payload = ev.retrieve("Payload for mfg_specific_data")[0].val.hex() + + data = ev.retrieve("Manufacturer Specific Data") + + payload = data[0].payload + + payload = payload[1].val.hex() + + #payload = ev.retrieve("Payload for mfg_specific_data")[0].val.hex() # ...and then dissect said payload into a UUID, temp, gravity, and rssi (which isn't actually rssi) - uuid = payload[8:40] - temp = int.from_bytes(bytes.fromhex(payload[40:44]), byteorder='big') - gravity = int.from_bytes(bytes.fromhex(payload[44:48]), byteorder='big') + uuid = payload[4:36] + temp = int.from_bytes(bytes.fromhex(payload[36:40]), byteorder='big') + gravity = int.from_bytes(bytes.fromhex(payload[40:44]), byteorder='big') # tx_pwr = int.from_bytes(bytes.fromhex(payload[48:49]), byteorder='big') # rssi = int.from_bytes(bytes.fromhex(payload[49:50]), byteorder='big') rssi = 0 # TODO - Fix this - except: + except Exception as e: + LOG.error(e) return if verbose: From 6ef78626c7ed940468635a70da00470a31b33824 Mon Sep 17 00:00:00 2001 From: NecroBrews Date: Wed, 12 Feb 2020 08:50:49 +1300 Subject: [PATCH 23/25] Code cleanup --- gravity/tilt/tilt_monitor_aio.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gravity/tilt/tilt_monitor_aio.py b/gravity/tilt/tilt_monitor_aio.py index 19798d56..128c2e38 100644 --- a/gravity/tilt/tilt_monitor_aio.py +++ b/gravity/tilt/tilt_monitor_aio.py @@ -24,7 +24,7 @@ verbose = tilt_monitor_utils.verbose mydev = tilt_monitor_utils.bluetooth_device -LOG = logging.getLogger("tiltTest") +LOG = logging.getLogger("tilt") LOG.setLevel(logging.INFO) #### The main loop @@ -71,13 +71,9 @@ def processBLEBeacon(data): # Let's use some of the functions of aioblesscan to tease out the mfg_specific_data payload data = ev.retrieve("Manufacturer Specific Data") - payload = data[0].payload - payload = payload[1].val.hex() - #payload = ev.retrieve("Payload for mfg_specific_data")[0].val.hex() - # ...and then dissect said payload into a UUID, temp, gravity, and rssi (which isn't actually rssi) uuid = payload[4:36] temp = int.from_bytes(bytes.fromhex(payload[36:40]), byteorder='big') From ea4d8904e4dd1b9610fe4f00d7cf3d25e5924fa6 Mon Sep 17 00:00:00 2001 From: NecroBrews Date: Sat, 15 Feb 2020 16:36:16 +1300 Subject: [PATCH 24/25] Small fix for temp values not displaying on dashboard --- app/api/lcd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/lcd.py b/app/api/lcd.py index cb5238e0..90afb8ec 100644 --- a/app/api/lcd.py +++ b/app/api/lcd.py @@ -30,7 +30,7 @@ def getLCD(req, device_id): def getPanel(req, device_id): def temp_text(temp, temp_format): - if temp == 0: + if (temp) is None or (temp == 0): return "--° {}".format(temp_format) else: return "{:.1f}° {}".format(temp, temp_format) From 2480cb9b2d6f346ad7ea84d6ed623c6718ed8fbe Mon Sep 17 00:00:00 2001 From: NecroBrews Date: Sat, 15 Feb 2020 16:44:06 +1300 Subject: [PATCH 25/25] Small fix for temp values not displaying on dashboard --- app/api/lcd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/lcd.py b/app/api/lcd.py index 90afb8ec..b8626d94 100644 --- a/app/api/lcd.py +++ b/app/api/lcd.py @@ -30,7 +30,7 @@ def getLCD(req, device_id): def getPanel(req, device_id): def temp_text(temp, temp_format): - if (temp) is None or (temp == 0): + if (temp is None) or (temp == 0): return "--° {}".format(temp_format) else: return "{:.1f}° {}".format(temp, temp_format)