diff --git a/app/api/lcd.py b/app/api/lcd.py
index 09c02cfd..b8626d94 100644
--- a/app/api/lcd.py
+++ b/app/api/lcd.py
@@ -30,10 +30,10 @@ 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 "{}° {}".format(temp, temp_format)
+ return "{:.1f}° {}".format(temp, temp_format)
ret = []
try:
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 56389bf1..af325ea5 100644
--- a/docs/source/develop/changelog.rst
+++ b/docs/source/develop/changelog.rst
@@ -7,18 +7,36 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) because it
-[Unversioned] - Fixes
-~~~~~~~~~~~~~~~~~~~~~
+[2019-02-15] - ThingSpeak and Grainfather Support
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Added
+---------------------
+
+- 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
+- An explicit error message will now be displayed when a user attempts to manually access the ispindel endpoint
+
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
+- Fixed issue where Tilt readings were not being properly decoded (Thanks NecroBrews!)
+- Fixed issue where dashboard panels were not being updated (Thanks NecroBrews!)
[2019-12-15] - Brewer's Friend, Brewfather, and MacOS BLE Support
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Added
---------------------
diff --git a/docs/source/develop/push_support.rst b/docs/source/develop/push_support.rst
index e50c9a2f..4962e274 100644
--- a/docs/source/develop/push_support.rst
+++ b/docs/source/develop/push_support.rst
@@ -12,11 +12,13 @@ future - support pushing via TCP (sockets)).
Supported "Push" Targets
------------------------------
-Fermentrack currently supports three push targets:
+Fermentrack currently supports five 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
+- **Brewer's Friend** - Pushes both specific gravity & temperature data associated with gravty sensors
+- **Brewfather** - Pushes both specific gravity & temperature data associated with gravity sensors, as well as temperature data from BrewPi controllers with gravity sensors attached
+- **ThingSpeak** - Pushes temperature data
+- **Grainfather** - iSpindel push format - Pushes both specific gravity & temperature data from gravity sensors
@@ -92,6 +94,71 @@ 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.
+
+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.
+
+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
diff --git a/external_push/admin.py b/external_push/admin.py
index 09ca9479..5c99e94e 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, GrainfatherPushTarget
@admin.register(GenericPushTarget)
@@ -13,4 +13,12 @@ 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 = ('name', 'status')
+
+@admin.register(GrainfatherPushTarget)
+class GrainfatherPushTargetAdmin(admin.ModelAdmin):
+ list_display = ('gravity_sensor_to_push', 'status', 'push_frequency')
diff --git a/external_push/forms.py b/external_push/forms.py
index 223a18df..72740bdb 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, GrainfatherPushTarget
from django.core import validators
import fermentrack_django.settings as settings
@@ -24,3 +24,15 @@ 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']
+
+
+
+class GrainfatherPushTargetModelForm(ModelForm):
+ class Meta:
+ model = GrainfatherPushTarget
+ fields = ['gravity_sensor_to_push', 'push_frequency', 'logging_url', 'gf_name']
diff --git a/external_push/migrations/0005_ThingSpeak_Support.py b/external_push/migrations/0005_ThingSpeak_Support.py
new file mode 100644
index 00000000..4fd35d4a
--- /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', '0004_Brewfather_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/migrations/0006_Grainfather_Support.py b/external_push/migrations/0006_Grainfather_Support.py
new file mode 100644
index 00000000..2ffdc27c
--- /dev/null
+++ b/external_push/migrations/0006_Grainfather_Support.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.27 on 2019-12-23 13:58
+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', '0005_ThingSpeak_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 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')),
+ ],
+ options={
+ 'verbose_name': 'Grainfather Push Target',
+ 'verbose_name_plural': 'Grainfather Push Targets',
+ },
+ ),
+ ]
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/external_push/models.py b/external_push/models.py
index 75c6b7d9..c1cfb23f 100644
--- a/external_push/models.py
+++ b/external_push/models.py
@@ -348,6 +348,18 @@ def send_data(self):
return True # TODO - Check if the post actually succeeded & react accordingly
+# You think I would ahve done this before, but apparently not.
+# TODO - Refactor all of my temperature conversions to use this base formula
+def temp_convert(temp, from_units, to_units):
+ if from_units == to_units:
+ return temp
+ elif from_units == 'F' and to_units == 'C':
+ return (temp - 32) * 5 / 9
+ elif from_units == 'C' and to_units == 'F':
+ return (temp * 9 / 5) + 32
+ else:
+ # TODO - Figure out the proper error type for this
+ raise NotImplementedError
class BrewfatherPushTarget(models.Model):
class Meta:
@@ -415,6 +427,289 @@ def data_to_push(self):
to_send['temp'] = float(latest_log_point.temp)
to_send['temp_unit'] = latest_log_point.temp_format
+ if latest_log_point.associated_device.assigned_brewpi_device is not None:
+ # We have a controller - try to load the data from it
+ try:
+ device_info = latest_log_point.associated_device.assigned_brewpi_device.get_dashpanel_info()
+ except:
+ device_info = None
+
+ if device_info is not None:
+ # We were able to load data from the controller.
+ # Cache the BrewPi's temp format as we want to convert to use the gravity sensor's format in case they
+ # happen to be different
+ brewpi_temp_format = latest_log_point.associated_device.assigned_brewpi_device.temp_format
+ if device_info['BeerTemp'] is not None:
+ if device_info['BeerTemp'] != 0:
+ # If we have an explicit beer temp from a BrewPi controller, we're going to use that instead
+ # of a temp from the gravity sensor.
+ to_send['temp'] = temp_convert(float(device_info['BeerTemp']), brewpi_temp_format,
+ latest_log_point.temp_format)
+ if device_info['FridgeTemp'] is not None:
+ if device_info['FridgeTemp'] != 0:
+ to_send['aux_temp'] = temp_convert(float(device_info['FridgeTemp']),
+ brewpi_temp_format, latest_log_point.temp_format)
+ if device_info['RoomTemp'] is not None:
+ if device_info['RoomTemp'] != 0:
+ to_send['ext_temp'] = temp_convert(float(device_info['RoomTemp']),
+ brewpi_temp_format, latest_log_point.temp_format)
+
+ 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
+
+
+
+
+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
+
+
+
+
+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="")
+ 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 "
+ "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':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
+
+ 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)
@@ -434,3 +729,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/tasks.py b/external_push/tasks.py
index d3aaac12..8d993e1e 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, GrainfatherPushTarget
import datetime, pytz, time
from django.utils import timezone
@@ -44,6 +44,29 @@ 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
+
+@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 +74,8 @@ 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()
+ 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 +104,21 @@ 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)
+ # 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..86ce8106
--- /dev/null
+++ b/external_push/templates/external_push/grainfather_push_target_add.html
@@ -0,0 +1,57 @@
+{% extends "sitewide/flat_ui_template.html" %}
+{% load custom_tags %}
+
+
+{% block title %}Add Grainfather Push Target{% endblock %}
+
+{% block content %}
+
+
+{% 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 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.
+
+
+
+
+
+
+
+
+{% 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..9f6e2135
--- /dev/null
+++ b/external_push/templates/external_push/grainfather_push_target_view.html
@@ -0,0 +1,47 @@
+{% extends "sitewide/flat_ui_template.html" %}
+{% load custom_tags %}
+
+
+{% block title %}Grainfather Push Target {{ push_target }}{% endblock %}
+
+{% block content %}
+
+
+{% if form.errors %}
+ Please correct the error {{ form.errors }} below.
+{% endif %}
+
+
+
+
+
+
+ Delete Push Target
+
+
+
+{% endblock %}
+
+{% block scripts %}
+
+
+{% endblock %}
+
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..591e1042 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,45 @@ 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 %}
+
+ {# 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 ThingSpeak Push Target
+ Add Grainfather 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 %}
+
+
+{% 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
+
+
+
+
+
+
+
+{% 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 %}
+
+
+{% 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
+
+
+
+
+
+
+
+
+ Delete Push Target
+
+
+
+{% endblock %}
+
+{% block scripts %}
+
+
+{% endblock %}
+
diff --git a/external_push/urls.py b/external_push/urls.py
index a6852d3e..42514db4 100644
--- a/external_push/urls.py
+++ b/external_push/urls.py
@@ -24,4 +24,11 @@
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'),
+
+ 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..1525be08 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, GrainfatherPushTarget
import fermentrack_django.settings as settings
@@ -31,9 +31,12 @@ 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()
+ 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, 'thingspeak_push_targets': thingspeak_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:
@@ -289,3 +292,158 @@ def external_push_brewfather_delete(request, push_target_id):
+@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')
+
+
+@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')
+
+
+
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
diff --git a/gravity/tilt/tilt_monitor_aio.py b/gravity/tilt/tilt_monitor_aio.py
index f098a349..128c2e38 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("tilt")
+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,20 @@ 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()
# ...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:
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()
diff --git a/gravity/views_ispindel.py b/gravity/views_ispindel.py
index 34059e2f..dc60894f 100644
--- a/gravity/views_ispindel.py
+++ b/gravity/views_ispindel.py
@@ -90,7 +90,12 @@ def ispindel_handler(request):
# As of iSpindel 6.2.0 it looks like this:
# {"name":"iSpindel001","ID":9390968,"token":"fermentrack","angle":68.81093,"temperature":73.175,"temp_units":"F","battery":4.103232,"gravity":22.80585,"interval":20,"RSSI":-41}
- ispindel_data = json.loads(request.body.decode('utf-8'))
+ try:
+ ispindel_data = json.loads(request.body.decode('utf-8'))
+ except json.JSONDecodeError:
+ return JsonResponse({'status': 'failed', 'message': "No JSON data was posted (are you accessing this manually?)"}, safe=False,
+ json_dumps_params={'indent': 4})
+
with open(os.path.join(settings.BASE_DIR, "log", 'ispindel_json_output.log'), 'w') as logFile:
pprint.pprint(ispindel_data, logFile)
diff --git a/requirements.txt b/requirements.txt
index 84f14af9..49fdb76a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -24,3 +24,4 @@ pybluez # for gravity sensor support
aioblescan # Replacement for beacontools for Tilt support
+
diff --git a/requirements_macos.txt b/requirements_macos.txt
index b1673c18..01a94423 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
@@ -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