From 1ec6a9700f247fda797024abf6f63a8293e78cfb Mon Sep 17 00:00:00 2001 From: lbussy Date: Wed, 14 Aug 2019 19:11:31 -0500 Subject: [PATCH 01/29] Add Arduino I2C shield type --- brewpi-script/scriptlibs/pinList.py | 40 +++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/brewpi-script/scriptlibs/pinList.py b/brewpi-script/scriptlibs/pinList.py index 2d018bc9..879a5418 100644 --- a/brewpi-script/scriptlibs/pinList.py +++ b/brewpi-script/scriptlibs/pinList.py @@ -41,13 +41,13 @@ def getPinList(boardType, shieldType): {'val': 20, 'text': 'A2', 'type': 'free'}, {'val': 21, 'text': 'A3', 'type': 'free'}] elif boardType == "uno" and shieldType == "revC": - pinList = [{'val': 6, 'text': ' 6 (Act 1)', 'type': 'act'}, - {'val': 5, 'text': ' 5 (Act 2)', 'type': 'act'}, + pinList = [{'val': 0, 'text': ' 0', 'type': 'serial'}, + {'val': 1, 'text': ' 1', 'type': 'serial'}, {'val': 2, 'text': ' 2 (Act 3)', 'type': 'act'}, - {'val': 19, 'text': 'A5 (Act 4)', 'type': 'act'}, - {'val': 4, 'text': ' 4 (Door)', 'type': 'door'}, - {'val': 18, 'text': 'A4 (OneWire)', 'type': 'onewire'}, {'val': 3, 'text': ' 3', 'type': 'beep'}, + {'val': 4, 'text': ' 4 (Door)', 'type': 'door'}, + {'val': 5, 'text': ' 5 (Act 2)', 'type': 'act'}, + {'val': 6, 'text': ' 6 (Act 1)', 'type': 'act'}, {'val': 7, 'text': ' 7', 'type': 'rotary'}, {'val': 8, 'text': ' 8', 'type': 'rotary'}, {'val': 9, 'text': ' 9', 'type': 'rotary'}, @@ -55,12 +55,33 @@ def getPinList(boardType, shieldType): {'val': 11, ' text': '11', 'type': 'spi'}, {'val': 12, ' text': '12', 'type': 'spi'}, {'val': 13, ' text': '13', 'type': 'spi'}, - {'val': 0, 'text': ' 0', 'type': 'serial'}, - {'val': 1, 'text': ' 1', 'type': 'serial'}, {'val': 14, 'text': 'A0', 'type': 'free'}, {'val': 15, 'text': 'A1', 'type': 'free'}, {'val': 16, 'text': 'A2', 'type': 'free'}, - {'val': 17, 'text': 'A3', 'type': 'free'}] + {'val': 17, 'text': 'A3', 'type': 'free'}, + {'val': 18, 'text': 'A4 (OneWire)', 'type': 'onewire'}, + {'val': 19, 'text': 'A5 (Act 4)', 'type': 'act'}] + elif boardType == "uno" and shieldType == "I2C": + pinList = [{'val': 0, 'text': ' 0', 'type': 'serial'}, + {'val': 1, 'text': ' 1', 'type': 'serial'}, + {'val': 2, 'text': ' 2 (Act 3)', 'type': 'act'}, + {'val': 3, 'text': ' 3 (Alarm)', 'type': 'beep'}, + {'val': 4, 'text': ' 4 (Door)', 'type': 'door'}, + {'val': 5, 'text': ' 5 (Act 1)', 'type': 'act'}, + {'val': 6, 'text': ' 6 (Act 2)', 'type': 'act'}, + {'val': 7, 'text': ' 7', 'type': 'rotary'}, + {'val': 8, 'text': ' 8', 'type': 'rotary'}, + {'val': 9, 'text': ' 9', 'type': 'rotary'}, + {'val': 10, 'text': '10 (Act 4)', 'type': 'act'}, + {'val': 11, 'text': '11', 'type': 'free'}, + {'val': 12, 'text': '12', 'type': 'free'}, + {'val': 13, 'text': '13', 'type': 'free'}, + {'val': 14, 'text': 'A0 (OneWire)', 'type': 'onewire'}, + {'val': 15, 'text': 'A1 (OneWire)', 'type': 'free'}, + {'val': 16, 'text': 'A2 (OneWire)', 'type': 'free'}, + {'val': 17, 'text': 'A3 (Act 4)', 'type': 'act'}, + {'val': 18, 'text': 'A4 (SDA)', 'type': 'i2c'}, + {'val': 19, 'text': 'A5 (SCL)', 'type': 'i2c'}] elif boardType == "leonardo" and shieldType == "revA": pinList = [{'val': 6, 'text': ' 6 (Cool)', 'type': 'act'}, {'val': 5, 'text': ' 5 (Heat)', 'type': 'act'}, @@ -154,8 +175,7 @@ def getPinListJson(boardType, shieldType): def pinListTest(): print(getPinListJson("leonardo", "revC")) print(getPinListJson("uno", "revC")) - print(getPinListJson("leonardo", "revA")) - print(getPinListJson("uno", "revA")) + print(getPinListJson("uno", "I2C")) print(getPinListJson("core", "V1")) print(getPinListJson("core", "V2")) print(getPinListJson("photon", "V1")) From e445a665456b5b746a4d660a60b8ddbc34343b28 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Mon, 17 Feb 2020 22:16:56 -0500 Subject: [PATCH 02/29] Remove python 2 references/upgrade scripts --- app/views.py | 24 ++------ utils/force_upgrade.sh | 124 ----------------------------------------- utils/upgrade.sh | 123 ---------------------------------------- 3 files changed, 6 insertions(+), 265 deletions(-) delete mode 100755 utils/force_upgrade.sh delete mode 100755 utils/upgrade.sh diff --git a/app/views.py b/app/views.py index 0f79b2b2..72a4d797 100644 --- a/app/views.py +++ b/app/views.py @@ -500,26 +500,14 @@ def github_trigger_upgrade(request, variant=""): branch_to_use = request.POST.get('new_branch', "master") if variant == "": - if sys.version_info[0] < 3: - # TODO - After April 2018, delete the Python 2 option here - cmds['tag'] = "nohup utils/upgrade.sh -t \"{}\" -b \"master\" &".format(request.POST.get('tag', "")) - cmds['branch'] = "nohup utils/upgrade.sh -b \"{}\" &".format(branch_to_use) - messages.success(request, "Triggered an upgrade from GitHub") - else: - cmds['tag'] = "nohup utils/upgrade3.sh -t \"{}\" -b \"master\" &".format(request.POST.get('tag', "")) - cmds['branch'] = "nohup utils/upgrade3.sh -b \"{}\" &".format(branch_to_use) - messages.success(request, "Triggered an upgrade from GitHub") + cmds['tag'] = "nohup utils/upgrade3.sh -t \"{}\" -b \"master\" &".format(request.POST.get('tag', "")) + cmds['branch'] = "nohup utils/upgrade3.sh -b \"{}\" &".format(branch_to_use) + messages.success(request, "Triggered an upgrade from GitHub") elif variant == "force": - if sys.version_info[0] < 3: - # TODO - After April 2018, delete the Python 2 option here - cmds['tag'] = "nohup utils/force_upgrade.sh -t \"{}\" -b \"master\" &".format(request.POST.get('tag', "")) - cmds['branch'] = "nohup utils/force_upgrade.sh -b \"{}\" &".format(branch_to_use) - messages.success(request, "Triggered an upgrade from GitHub") - else: - cmds['tag'] = "nohup utils/force_upgrade3.sh -t \"{}\" -b \"master\" &".format(request.POST.get('tag', "")) - cmds['branch'] = "nohup utils/force_upgrade3.sh -b \"{}\" &".format(branch_to_use) - messages.success(request, "Triggered an upgrade from GitHub") + cmds['tag'] = "nohup utils/force_upgrade3.sh -t \"{}\" -b \"master\" &".format(request.POST.get('tag', "")) + cmds['branch'] = "nohup utils/force_upgrade3.sh -b \"{}\" &".format(branch_to_use) + messages.success(request, "Triggered an upgrade from GitHub") else: cmds['tag'] = "" diff --git a/utils/force_upgrade.sh b/utils/force_upgrade.sh deleted file mode 100755 index 12e79b09..00000000 --- a/utils/force_upgrade.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env bash - -# Defaults -BRANCH="master" -SILENT=0 -TAG="" -CIRCUSCTL="python -m circus.circusctl --timeout 10" - -# Colors (for printinfo/error/warn below) -green=$(tput setaf 76) -red=$(tput setaf 1) -tan=$(tput setaf 3) -reset=$(tput sgr0) - - -# Help text -function usage() { - echo "Usage: $0 [-h] [-s] [-b ] [-t ]" 1>&2 - exit 1 -} - -printinfo() { - if [ ${SILENT} -eq 0 ] - then - printf "::: ${green}%s${reset}\n" "$@" - fi -} - - -printwarn() { - if [ ${SILENT} -eq 0 ] - then - printf "${tan}*** WARNING: %s${reset}\n" "$@" - fi -} - - -printerror() { - if [ ${SILENT} -eq 0 ] - then - printf "${red}*** ERROR: %s${reset}\n" "$@" - fi -} - - -while getopts ":b:t:sh" opt; do - case ${opt} in - b) - BRANCH=${OPTARG} - ;; - t) - TAG=${OPTARG} - ;; - s) - SILENT=1 # Currently unused - usage - ;; - h) - usage - exit 1 - ;; - \?) - echo "Invalid option: -$OPTARG" >&2 - usage - exit 1 - ;; - esac -done - -shift $((OPTIND-1)) - - -exec > >(tee -ai upgrade.log) - - -printinfo "Forcing upgrade & reset to upstream branch ${BRANCH}" -# First, launch the virtualenv -source ~/venv/bin/activate # Assuming the directory based on a normal install with Fermentrack-tools - -# Given that this script can be called by the webapp proper, give it 2 seconds to finish sending a reply to the -# user if he/she initiated an upgrade through the webapp. -printinfo "Waiting 2 seconds for Fermentrack to send updates if triggered from the web..." -sleep 2s - -# Next, kill the running Fermentrack instance using circus -printinfo "Stopping circus..." -$CIRCUSCTL stop &>> upgrade.log - -# Pull the latest version of the script from GitHub -printinfo "Updating from git..." -cd ~/fermentrack # Assuming the directory based on a normal install with Fermentrack-tools -git fetch --all &>> upgrade.log -git reset --hard @{u} &>> upgrade.log - -# If we have a tag set, use it -if [ "${TAG}" = "" ] -then - git checkout ${BRANCH} &>> upgrade.log -else - # Not entirely sure if we need -B for this, but leaving it here just in case - git checkout tags/${TAG} -B ${BRANCH} &>> upgrade.log -fi - -git pull &>> upgrade.log - -# Install everything from requirements.txt -printinfo "Updating requirements via pip..." -pip install -r requirements.txt --upgrade &>> upgrade.log - -# Migrate to create/adjust anything necessary in the database -printinfo "Running manage.py migrate..." -python manage.py migrate &>> upgrade.log - -# Migrate to create/adjust anything necessary in the database -printinfo "Running manage.py collectstatic..." -python manage.py collectstatic --noinput >> /dev/null - - -# Finally, relaunch the Fermentrack instance using circus -printinfo "Relaunching circus..." -~/fermentrack/utils/updateCronCircus.sh startifstopped -$CIRCUSCTL reloadconfig &>> upgrade.log -$CIRCUSCTL start &>> upgrade.log -printinfo "Complete!" diff --git a/utils/upgrade.sh b/utils/upgrade.sh deleted file mode 100755 index ce14d2d7..00000000 --- a/utils/upgrade.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash - -# Defaults -BRANCH="master" -SILENT=0 -TAG="" -CIRCUSCTL="python -m circus.circusctl --timeout 10" - -# Colors (for printinfo/error/warn below) -green=$(tput setaf 76) -red=$(tput setaf 1) -tan=$(tput setaf 3) -reset=$(tput sgr0) - - -# Help text -function usage() { - echo "Usage: $0 [-h] [-s] [-b ] [-t ]" 1>&2 - exit 1 -} - -printinfo() { - if [ ${SILENT} -eq 0 ] - then - printf "::: ${green}%s${reset}\n" "$@" - fi -} - - -printwarn() { - if [ ${SILENT} -eq 0 ] - then - printf "${tan}*** WARNING: %s${reset}\n" "$@" - fi -} - - -printerror() { - if [ ${SILENT} -eq 0 ] - then - printf "${red}*** ERROR: %s${reset}\n" "$@" - fi -} - - -while getopts ":b:t:sh" opt; do - case ${opt} in - b) - BRANCH=${OPTARG} - ;; - t) - TAG=${OPTARG} - ;; - s) - SILENT=1 # Currently unused - usage - ;; - h) - usage - exit 1 - ;; - \?) - echo "Invalid option: -$OPTARG" >&2 - usage - exit 1 - ;; - esac -done - -shift $((OPTIND-1)) - - -exec > >(tee -ai upgrade.log) - - -printinfo "Triggering upgrade from branch ${BRANCH}" -# First, launch the virtualenv -source ~/venv/bin/activate # Assuming the directory based on a normal install with Fermentrack-tools - -# Given that this script can be called by the webapp proper, give it 2 seconds to finish sending a reply to the -# user if he/she initiated an upgrade through the webapp. -printinfo "Waiting 2 seconds for Fermentrack to send updates if triggered from the web..." -sleep 2s - -# Next, kill the running Fermentrack instance using circus -printinfo "Stopping circus..." -$CIRCUSCTL stop &>> upgrade.log - -# Pull the latest version of the script from GitHub -printinfo "Updating from git..." -cd ~/fermentrack # Assuming the directory based on a normal install with Fermentrack-tools -git fetch --prune &>> upgrade.log -git reset --hard &>> upgrade.log - -# If we have a tag set, use it -if [ "${TAG}" = "" ] -then - git checkout ${BRANCH} &>> upgrade.log -else - # Not entirely sure if we need -B for this, but leaving it here just in case - git checkout tags/${TAG} -B ${BRANCH} &>> upgrade.log -fi - -git pull &>> upgrade.log - -# Install everything from requirements.txt -printinfo "Updating requirements via pip..." -pip install -U -r requirements.txt --upgrade &>> upgrade.log - -# Migrate to create/adjust anything necessary in the database -printinfo "Running manage.py migrate..." -python manage.py migrate &>> upgrade.log - -# Migrate to create/adjust anything necessary in the database -printinfo "Running manage.py collectstatic..." -python manage.py collectstatic --noinput >> /dev/null - - -# Finally, relaunch the Fermentrack instance using circus -printinfo "Relaunching circus..." -$CIRCUSCTL reloadconfig &>> upgrade.log -$CIRCUSCTL start &>> upgrade.log -printinfo "Complete!" From 0aba878a80b08b37e7ffc3dd8724fda77866c86d Mon Sep 17 00:00:00 2001 From: Thorrak Date: Mon, 17 Feb 2020 22:24:00 -0500 Subject: [PATCH 03/29] Improve and expose upgrade.log in the UI (Closes #427) --- app/api/clog.py | 6 ++++-- app/templates/site_help.html | 4 ++++ utils/force_upgrade3.sh | 22 +++++++++++----------- utils/upgrade3.sh | 22 +++++++++++----------- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/app/api/clog.py b/app/api/clog.py index f8538054..869193c6 100644 --- a/app/api/clog.py +++ b/app/api/clog.py @@ -5,7 +5,7 @@ from gravity.models import GravitySensor -def get_filepath_to_log(device_type, logfile, device_id=None): +def get_filepath_to_log(device_type, logfile="", device_id=None): # get_filepath_to_log is being broken out so that we can use it in help/other templates to display which log file # is being loaded if device_type == "brewpi": @@ -22,6 +22,8 @@ def get_filepath_to_log(device_type, logfile, device_id=None): log_filename = 'fermentrack-stderr.log' elif device_type == "ispindel": log_filename = 'ispindel_raw_output.log' + elif device_type == "upgrade": + log_filename = 'upgrade.log' else: return None @@ -44,7 +46,7 @@ def get_device_log_combined(req, return_type, device_type, logfile, device_id=No # gravity - A specific gravity sensor object # spawner - the circus spawner # fermentrack - Fermentrack itself - valid_device_types = ['brewpi', 'gravity', 'spawner', 'fermentrack', 'ispindel'] + valid_device_types = ['brewpi', 'gravity', 'spawner', 'fermentrack', 'ispindel', 'upgrade'] if device_type not in valid_device_types: # TODO - Log this return HttpResponse("Cannot read log files for devices of type {} ".format(device_type), status=500) diff --git a/app/templates/site_help.html b/app/templates/site_help.html index 3ae04696..ee63a458 100644 --- a/app/templates/site_help.html +++ b/app/templates/site_help.html @@ -99,6 +99,10 @@

Other logs

iSpindel Raw Output {% log_file_path "ispindel" "stderr" %} + + Upgrade Log + {% log_file_path "upgrade" "stderr" %} + diff --git a/utils/force_upgrade3.sh b/utils/force_upgrade3.sh index aa12f7e7..c67a22f9 100755 --- a/utils/force_upgrade3.sh +++ b/utils/force_upgrade3.sh @@ -73,7 +73,7 @@ done shift $((OPTIND-1)) -exec > >(tee -i upgrade.log) +exec > >(tee -i log/upgrade.log) printinfo "Forcing upgrade & reset to upstream branch ${BRANCH}" @@ -87,32 +87,32 @@ sleep 2s # Next, kill the running Fermentrack instance using circus printinfo "Stopping circus..." -$CIRCUSCTL stop &>> upgrade.log +$CIRCUSCTL stop &>> log/upgrade.log # Pull the latest version of the script from GitHub printinfo "Updating from git..." cd ~/fermentrack # Assuming the directory based on a normal install with Fermentrack-tools -git fetch --all &>> upgrade.log -git reset --hard @{u} &>> upgrade.log +git fetch --all &>> log/upgrade.log +git reset --hard @{u} &>> log/upgrade.log # If we have a tag set, use it if [ "${TAG}" = "" ] then - git checkout ${BRANCH} &>> upgrade.log + git checkout ${BRANCH} &>> log/upgrade.log else # Not entirely sure if we need -B for this, but leaving it here just in case - git checkout tags/${TAG} -B ${BRANCH} &>> upgrade.log + git checkout tags/${TAG} -B ${BRANCH} &>> log/upgrade.log fi -git pull &>> upgrade.log +git pull &>> log/upgrade.log # Install everything from requirements.txt printinfo "Updating requirements via pip3..." -pip3 install -U -r requirements.txt --upgrade &>> upgrade.log +pip3 install -U -r requirements.txt --upgrade &>> log/upgrade.log # Migrate to create/adjust anything necessary in the database printinfo "Running manage.py migrate..." -python3 manage.py migrate &>> upgrade.log +python3 manage.py migrate &>> log/upgrade.log # Migrate to create/adjust anything necessary in the database printinfo "Running manage.py collectstatic..." @@ -122,6 +122,6 @@ python3 manage.py collectstatic --noinput >> /dev/null # Finally, relaunch the Fermentrack instance using circus printinfo "Relaunching circus..." ~/fermentrack/utils/updateCronCircus.sh startifstopped -$CIRCUSCTL reloadconfig &>> upgrade.log -$CIRCUSCTL start &>> upgrade.log +$CIRCUSCTL reloadconfig &>> log/upgrade.log +$CIRCUSCTL start &>> log/upgrade.log printinfo "Complete!" diff --git a/utils/upgrade3.sh b/utils/upgrade3.sh index 93cb0b1b..dae536be 100755 --- a/utils/upgrade3.sh +++ b/utils/upgrade3.sh @@ -78,7 +78,7 @@ done shift $((OPTIND-1)) -exec > >(tee -i upgrade.log) +exec > >(tee -i log/upgrade.log) printinfo "Triggering upgrade from branch ${BRANCH}" @@ -92,32 +92,32 @@ sleep 1s # Next, kill the running Fermentrack instance using circus printinfo "Stopping circus..." -$CIRCUSCTL stop &>> upgrade.log +$CIRCUSCTL stop &>> log/upgrade.log # Pull the latest version of the script from GitHub printinfo "Updating from git..." cd ~/fermentrack # Assuming the directory based on a normal install with Fermentrack-tools -git fetch --prune &>> upgrade.log -git reset --hard &>> upgrade.log +git fetch --prune &>> log/upgrade.log +git reset --hard &>> log/upgrade.log # If we have a tag set, use it if [ "${TAG}" = "" ] then - git checkout ${BRANCH} &>> upgrade.log + git checkout ${BRANCH} &>> log/upgrade.log else # Not entirely sure if we need -B for this, but leaving it here just in case - git checkout tags/${TAG} -B ${BRANCH} &>> upgrade.log + git checkout tags/${TAG} -B ${BRANCH} &>> log/upgrade.log fi -git pull &>> upgrade.log +git pull &>> log/upgrade.log # Install everything from requirements.txt printinfo "Updating requirements via pip3..." -pip3 install -U -r requirements.txt --upgrade &>> upgrade.log +pip3 install -U -r requirements.txt --upgrade &>> log/upgrade.log # Migrate to create/adjust anything necessary in the database printinfo "Running manage.py migrate..." -python3 manage.py migrate &>> upgrade.log +python3 manage.py migrate &>> log/upgrade.log # Migrate to create/adjust anything necessary in the database printinfo "Running manage.py collectstatic..." @@ -126,6 +126,6 @@ python3 manage.py collectstatic --noinput >> /dev/null # Finally, relaunch the Fermentrack instance using circus printinfo "Relaunching circus..." -$CIRCUSCTL reloadconfig &>> upgrade.log -$CIRCUSCTL start &>> upgrade.log +$CIRCUSCTL reloadconfig &>> log/upgrade.log +$CIRCUSCTL start &>> log/upgrade.log printinfo "Complete!" From 73b3f695ab1c495881a46fd839d9a71c1378b47d Mon Sep 17 00:00:00 2001 From: Thorrak Date: Mon, 17 Feb 2020 22:24:33 -0500 Subject: [PATCH 04/29] Reduce gravity sensor temperature precision to 0.1 degrees --- gravity/api/sensors.py | 2 +- gravity/models.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gravity/api/sensors.py b/gravity/api/sensors.py index a0d68fab..b8938355 100644 --- a/gravity/api/sensors.py +++ b/gravity/api/sensors.py @@ -28,7 +28,7 @@ def getGravitySensors(req, device_id=None): temp, temp_format = dev.retrieve_loggable_temp() if temp is None: - temp_string = "--.-° -" + temp_string = "--.-°" else: temp_string = "{}° {}".format(temp, temp_format) diff --git a/gravity/models.py b/gravity/models.py index 30c25187..071aec44 100644 --- a/gravity/models.py +++ b/gravity/models.py @@ -169,7 +169,8 @@ def retrieve_loggable_temp(self) -> (float, str): if point is None: return None, None else: - return None if point.temp is None else round(point.temp, 2), point.temp_format + # Changing to one degree of precision - more precise is nonsensical + return None if point.temp is None else round(point.temp, 1), point.temp_format def create_log_and_start_logging(self, name: str): # First, create the new gravity log From 16b0d763304ba9a4b3d0c18fa8e72ef62e9493d1 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Mon, 17 Feb 2020 22:28:56 -0500 Subject: [PATCH 05/29] Update link to documentation --- app/templates/site_help.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/site_help.html b/app/templates/site_help.html index ee63a458..f63ce793 100644 --- a/app/templates/site_help.html +++ b/app/templates/site_help.html @@ -8,7 +8,7 @@

Help

- Fermentrack Docs + Fermentrack Docs

From 85c5972989234b87b05fd2322b223ab6c1ac284c Mon Sep 17 00:00:00 2001 From: Thorrak Date: Thu, 20 Feb 2020 23:26:17 -0500 Subject: [PATCH 06/29] Store the time the last message was received from a Tilt (#385) --- gravity/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gravity/models.py b/gravity/models.py index 071aec44..1d8ce304 100644 --- a/gravity/models.py +++ b/gravity/models.py @@ -696,10 +696,13 @@ def save_extras_to_redis(self): # This saves the current (presumably complete) object as the 'current' point to redis r = redis.Redis(host=settings.REDIS_HOSTNAME, port=settings.REDIS_PORT, password=settings.REDIS_PASSWORD) + datetime_string = datetime.datetime.strftime(timezone.now(), "%c") + extras = { 'rssi': getattr(self, 'rssi', None), 'raw_gravity': getattr(self, 'raw_gravity', None), - 'raw_temp': getattr(self, 'raw_temp', None) + 'raw_temp': getattr(self, 'raw_temp', None), + 'saved_at': datetime_string, } r.set('tilt_{}_extras'.format(self.color), json.dumps(extras).encode(encoding="utf-8")) @@ -725,6 +728,8 @@ def load_extras_from_redis(self) -> dict: self.raw_gravity = extras['raw_gravity'] if 'raw_temp' in extras: self.raw_gravity = extras['raw_temp'] + if 'saved_at' in extras: + self.saved_at = datetime.datetime.strptime(extras['saved_at'], "%c") return extras From 2db61403e6d3df94b1fd5c49c4ca1299c8e42e42 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Thu, 20 Feb 2020 23:31:40 -0500 Subject: [PATCH 07/29] Add Sentry support to tilt_monitor_aio (Closes #432) --- gravity/tilt/tilt_monitor_aio.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gravity/tilt/tilt_monitor_aio.py b/gravity/tilt/tilt_monitor_aio.py index 128c2e38..7173d65e 100644 --- a/gravity/tilt/tilt_monitor_aio.py +++ b/gravity/tilt/tilt_monitor_aio.py @@ -1,5 +1,10 @@ #!/usr/bin/python +# Let's get sentry support going +from raven import Client +client = Client('http://3a1cc1f229ae4b0f88a4c6f7b5d8f394:c10eae5fd67a43a58957887a6b2484b1@sentry.optictheory.com:9000/2') + + import os, sys import time, datetime, getopt, pid from typing import List, Dict @@ -83,6 +88,7 @@ def processBLEBeacon(data): rssi = 0 # TODO - Fix this except Exception as e: LOG.error(e) + client.captureException() return if verbose: From 5d9d781005add7d4b0a5c2a692641a715cdd557b Mon Sep 17 00:00:00 2001 From: Thorrak Date: Thu, 20 Feb 2020 23:35:53 -0500 Subject: [PATCH 08/29] Lock pybluez/aioblescan versions (Closes #428) --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 49fdb76a..76ced824 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,8 +20,8 @@ esptool # for flashing ESP8266 devices redis # for huey & gravity sensor support -pybluez # for gravity sensor support -aioblescan # Replacement for beacontools for Tilt support +pybluez==0.23 # for gravity sensor support +aioblescan==0.2.6 # Replacement for beacontools for Tilt support From e55beca5002b00a7385a6aeb035651261c79920e Mon Sep 17 00:00:00 2001 From: Thorrak Date: Thu, 20 Feb 2020 23:40:12 -0500 Subject: [PATCH 09/29] Update changelog --- docs/source/develop/changelog.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/source/develop/changelog.rst b/docs/source/develop/changelog.rst index c1a8113b..8af2f259 100644 --- a/docs/source/develop/changelog.rst +++ b/docs/source/develop/changelog.rst @@ -6,6 +6,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) because it was the first relatively standard format to pop up when I googled "changelog formats". +[Unversioned] - Bugfixes +~~~~~~~~~~~~~~~~~~~~~~~~ + +Added +--------------------- + +- Added explicit support for LBussy's BrewPi-Remix I2C Board +- Exposed upgrade.log from the help screen +- Store the exact last time that a message was received from a Tilt to Redis +- Add sentry support to tilt_monitor_aio.py + + + +Changed +--------------------- + +- Removed legacy Python 2 code +- Reduced gravity sensor temp precision to 0.1 degrees +- Locked pybluez and aioblescan versions to prevent undesired format changes going forward + + + [2019-02-17] - Improved ESP32 Flashing Support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From f445b921d94f95b3ff4217fa2a4651a7b26ab6dd Mon Sep 17 00:00:00 2001 From: Eddie G Date: Tue, 25 Feb 2020 11:01:12 -0500 Subject: [PATCH 10/29] Update favicon.ico Fixed Favicon. 256x246 48x48 32x32 16x16 Fixed visual artifacts found in the original favicon. Filled in Hop Florette and Script F with white to allow for better icon visualizations on dark themed browsers. Changed the Scrip F orange to match the dashboard device chamber orange . --- app/static/favicon.ico | Bin 54452 -> 55270 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/static/favicon.ico b/app/static/favicon.ico index 7b7d5e7e49cfacd4d8ca870f9485814162860303..4da7270d05455ff02f0202b314ea0898dd0b71fc 100644 GIT binary patch literal 55270 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

*tik;?;U|eLX&H2X*7}7=nwW~@ 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 From 1e23119f4d90c562027c999ff1804880392241b8 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Fri, 10 Apr 2020 14:27:26 -0400 Subject: [PATCH 11/29] Add a test script for environmental issues that could cause Tilt hydrometers to not work --- brewpi-script/scriptlibs/pinList.py | 2 + docs/source/gravitysensors/tilt.rst | 33 +++++ .../templates/gravity/gravity_tilt_test.html | 99 +++++++++++++++ gravity/tilt/tilt_tests.py | 117 ++++++++++++++++++ gravity/urls.py | 1 + gravity/views_tilt.py | 29 +++++ requirements.txt | 3 +- requirements_macos.txt | 7 +- 8 files changed, 286 insertions(+), 5 deletions(-) create mode 100644 gravity/templates/gravity/gravity_tilt_test.html create mode 100644 gravity/tilt/tilt_tests.py diff --git a/brewpi-script/scriptlibs/pinList.py b/brewpi-script/scriptlibs/pinList.py index 879a5418..1864bf5c 100644 --- a/brewpi-script/scriptlibs/pinList.py +++ b/brewpi-script/scriptlibs/pinList.py @@ -173,7 +173,9 @@ def getPinListJson(boardType, shieldType): return 0 def pinListTest(): + print(getPinListJson("leonardo", "revA")) print(getPinListJson("leonardo", "revC")) + print(getPinListJson("uno", "revA")) print(getPinListJson("uno", "revC")) print(getPinListJson("uno", "I2C")) print(getPinListJson("core", "V1")) diff --git a/docs/source/gravitysensors/tilt.rst b/docs/source/gravitysensors/tilt.rst index 2a538385..d811bfd8 100644 --- a/docs/source/gravitysensors/tilt.rst +++ b/docs/source/gravitysensors/tilt.rst @@ -74,3 +74,36 @@ To integrate your Tilt logging with a BrewPi controller: #. Click "Attach sensor to controller" Now, to view your Tilt sensor readings, navigate to the BrewPi controller dashboard. You will have to restart a log if you had one running before associating the Tilt with the BrewPi sensor. Now, any wort you ferment with this controller will incorporate the Tilt's temperature and gravity readings onto your graph. Once the Tilt (or any gravity sensor) is attached to a BrewPi controller, that controller dashboard will become the main method with which to interact with the Tilt, specifically for things like logging. + + +Troubleshooting Tilt Support +---------------------------- + +Tilt Hydrometer support relies on a number of components beyond those used for other functions in Fermentrack, and as a result is particularly sensitive to changes in the program environment on the device on which Fermentrack is installed. Testing has been added to Fermentrack to help diagnose some of these environmental issues if they happen to impact an installation. + + +Fixing Missing System Packages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If there are system packages missing, you will unfortunately need to fix them manually. For Raspberry Pis running +Raspbian, here is how to fix this issue. For other OS's, please adapt these instructions as necessary + +#. Log into your Raspberry Pi via as the `pi` user +#. Type `sudo apt-get update` and allow the package system to update +#. Type `sudo-apt-get upgrade` and follow the prompts to upgrade all installed packages +#. For each missing package identified by the test script, type `sudo apt-get install -y {package name}` +#. Allow each package to install. Repeat the previous step for all missing packages. + + + +Fixing Missing/Incorrect Python Packages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Although all Python packages should be automatically installed as part of the installation script, it is possible that +packages come out of sync for a variety of reasons. If you are missing packages they will need to be installed for +Fermentrack to properly interface with your Tilt. + +.. todo:: Enrich this with steps for resolving missing Python packages + + + diff --git a/gravity/templates/gravity/gravity_tilt_test.html b/gravity/templates/gravity/gravity_tilt_test.html new file mode 100644 index 00000000..cf934d8f --- /dev/null +++ b/gravity/templates/gravity/gravity_tilt_test.html @@ -0,0 +1,99 @@ +{% extends "sitewide/flat_ui_template.html" %}{% load static %} +{% load custom_tags %} + +{% block title %}Debug Tilt{% endblock %} + +{% block content %} + +

Debugging Tilt Support

+ + +

System Packages

+ +

+ Fermentrack relies on several applications being installed on the system at the same time as Fermentrack. If any + of these packages are missing, it can impact the necessary Python packages from being installed or functioning + as expected. +

+ + {% if has_apt %} + {% if has_apt_packages %} +

All necessary system packages are installed

+ {% else %} +

Several system packages are missing.

+ {% endif %}{# has_apt_packages #} + + + + + + + + {% for test_result in apt_test_results %} + + + + + {% endfor %} + +
Package NameOK?
{{ test_result.package }}{% if test_result.result %}Installed{% else %}Not Installed{% endif %}
+ {% else %} +

Unable to locate the software necessary to check system packages

+

+ This test only works for systems that use apt/dpkg, like Raspbian, Debian, or Ubuntu. If you are running + on another operating system, you're unfortunately on your own for this test. +

+ {% endif %}{# has_apt #} + + +

Python Packages

+ +

+ Fermentrack is a Python-based application, and requires a number of Python packages to be installed and at the + correct version in order for all of its features to work. That said - it is designed to fail (somewhat) gracefully, + and as such if certain packages are missing only those features that rely on those packages (e.g. Tilt support) will + stop working. This test checks to ensure that all packages required for Tilt support are installed and at the + required version. +

+ +

+ {# TODO - Make this actually test the OS and hide the message for MacOS and Linux #} + Note - The package list being tested below does not apply to installations on Windows. +

+ + {% if has_python_packages %} +

All necessary python packages are installed

+ {% else %} +

Several python packages are missing.

+ {% endif %}{# has_python_packages #} + + + + + + + + + + + {% for test_result in apt_test_results %} + + + + + + + {% endfor %} +
Package NameRequired VersionInstalled VersionOK?
{{ test_result.package }}{{ test_result.required_version }}{{ test_result.installed_version }}{% if test_result.ok %}OK{% else %}Not OK{% endif %}
+ + +{% endblock %} + + +{% block scripts %} +{% endblock %} + + + + + diff --git a/gravity/tilt/tilt_tests.py b/gravity/tilt/tilt_tests.py new file mode 100644 index 00000000..7d88c88b --- /dev/null +++ b/gravity/tilt/tilt_tests.py @@ -0,0 +1,117 @@ +import os, subprocess, sys +import pkg_resources +from packaging import version + + +# This function is used ot check if an apt package is installed on Raspbian, Ubuntu, Debian, etc. +def apt_package_installed(package_name: str) -> bool: + devnull = open(os.devnull,"w") + retval = subprocess.call(["dpkg", "-s", package_name],stdout=devnull,stderr=subprocess.STDOUT) + devnull.close() + if retval != 0: + return False + return True + + + +# This is just a means to check if apt (dpkg) is installed at all +def has_apt() -> bool: + try: + devnull = open(os.devnull,"w") + retval = subprocess.call(["dpkg", "--version"],stdout=devnull,stderr=subprocess.STDOUT) + devnull.close() + if retval != 0: + return False + return True + except: + # dpkg doesn't exist + return False + + +def check_apt_packages() -> (bool, list): + package_list = ["bluez", "libcap2-bin", "libbluetooth3", "libbluetooth-dev", "redis-server", "python3-dev"] + test_results = [] + all_packages_ok = True + + for package in package_list: + result = {'package': package, 'result': True} + if apt_package_installed(package): + result['result'] = True + else: + result ['result'] = False + all_packages_ok = False + test_results.append(result) + + return all_packages_ok, test_results + + +def check_python_packages() -> (bool, list): + if sys.platform == "darwin": + # The MacOS support uses different packages from the support for Linux + package_list = [ + {'name': 'PyObjc', 'version': version.parse("6.2")}, + {'name': 'redis', 'version': version.parse("3.4.1")}, + ] + else: + package_list = [ + {'name': 'pybluez', 'version': version.parse("0.23")}, + {'name': 'aioblescan', 'version': version.parse("0.2.6")}, + {'name': 'redis', 'version': version.parse("3.4.1")}, + ] + + test_results = [] + all_packages_ok = True + + for package_to_find in package_list: + result_stub = { + 'package': package_to_find['name'], + 'required_version': package_to_find['version'], + 'installed_version': None, + 'ok': False, + } + + for package in pkg_resources.working_set: + if package.project_name == package_to_find['name']: + result_stub['installed_version'] = package.parsed_version + if result_stub['installed_version'] != result_stub['required_version']: + result_stub['ok'] = False + + if result_stub['ok'] is False: + all_packages_ok = False + test_results.append(result_stub) + + return all_packages_ok, test_results + + +# The following was used for testing during development +if __name__ == "__main__": + if has_apt(): + apt_ok, apt_test_results = check_apt_packages() + + if apt_ok: + print("All apt packages found. Package status:") + else: + print("Missing apt packages. Package status:") + + for this_test in apt_test_results: + print("Package {}: {}".format(this_test['package'], + ("Installed" if this_test['result'] else "Not Installed"))) + else: + print("dpkg not installed - not checking to see if system packages are installed") + print("") + + + # Next, check the python packages + python_ok, python_test_results = check_python_packages() + + if python_ok: + print("All required python packages found. Package status:") + else: + print("Missing/incorrect python packages. Package status:") + + for this_test in python_test_results: + print("Package {} - Required Version {} - Installed Version {} - OK? {}".format( + this_test['package'], this_test['required_version'], this_test['installed_version'], this_test['ok'])) + print("") + + diff --git a/gravity/urls.py b/gravity/urls.py index cfb162a4..d76b4a7b 100644 --- a/gravity/urls.py +++ b/gravity/urls.py @@ -59,6 +59,7 @@ url(r'^gravity/sensor/(?P[A-Za-z0-9]{1,20})/tilt/calibration/gravity/delete/(?P[A-Za-z0-9]{1,20})/$', gravity.views_tilt.gravity_tilt_delete_gravity_calibration_point, name='gravity_tilt_delete_gravity_calibration_point'), url(r'^gravity/sensor/(?P[A-Za-z0-9]{1,20})/tilt/calibration/gravity/calibrate/$', gravity.views_tilt.gravity_tilt_calibrate, name='gravity_tilt_calibrate'), url(r'^gravity/sensor/(?P[A-Za-z0-9]{1,20})/tilt/calibration/gravity/guided/(?P[A-Za-z0-9]{1,20})$', gravity.views_tilt.gravity_tilt_guided_calibration, name='gravity_tilt_guided_calibration'), + url(r'^gravity/tilt/test/$', gravity.views_tilt.gravity_tilt_test, name='gravity_tilt_test'), # TiltBridge specific views url(r'^gravity/tiltbridge/add/$', gravity.views_tilt.gravity_tiltbridge_add, name='gravity_tiltbridge_add'), diff --git a/gravity/views_tilt.py b/gravity/views_tilt.py index 865287a6..686a7cc4 100644 --- a/gravity/views_tilt.py +++ b/gravity/views_tilt.py @@ -15,6 +15,8 @@ from gravity import mdnsLocator +import gravity.tilt.tilt_tests as tilt_tests + import csv try: @@ -489,3 +491,30 @@ def gravity_tiltbridge_urlerror(request, tiltbridge_id): return render(request, template_name='gravity/gravity_tiltbridge_urlerror.html', context={'tiltbridge': selected_tiltbridge, 'fermentrack_url': fermentrack_url,}) + + + + +@login_required +@site_is_configured +def gravity_tilt_test(request): + # TODO - Add user permissioning + # if not request.user.has_perm('app.edit_device'): + # messages.error(request, 'Your account is not permissioned to edit devices. Please contact an admin') + # return redirect("/") + + # Check if we are on a system that actually has apt (e.g. Raspbian, Debian, Ubuntu, etc.) + has_apt = tilt_tests.has_apt() + if has_apt: + has_apt_packages, apt_test_results = tilt_tests.check_apt_packages() + else: + messages.error(request, u"Unable to locate dpkg - Cannot test for system packages") + has_apt_packages = False + apt_test_results = [] + + # Next, check the python packages + has_python_packages, python_test_results = tilt_tests.check_python_packages() + + return render(request, template_name='gravity/gravity_tilt_test.html', + context={'has_apt': has_apt, 'has_apt_packages': has_apt_packages, 'apt_test_results': apt_test_results, + 'has_python_packages': has_python_packages, 'python_test_results': python_test_results,}) diff --git a/requirements.txt b/requirements.txt index 76ced824..3a4b8b4d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,8 +18,9 @@ requests # for loading firmware data from websites esptool # for flashing ESP8266 devices -redis # for huey & gravity sensor support +# Reminder - Update the version checks in gravity/tilt/tilt_tests.py when changing versions for the BT/Tilt pkgs +redis==3.4.1 # for huey & gravity sensor support pybluez==0.23 # for gravity sensor support aioblescan==0.2.6 # Replacement for beacontools for Tilt support diff --git a/requirements_macos.txt b/requirements_macos.txt index 01a94423..cffa3d27 100644 --- a/requirements_macos.txt +++ b/requirements_macos.txt @@ -18,11 +18,10 @@ requests # for loading firmware data from websites esptool # for flashing ESP8266 devices -redis # for huey & gravity sensor support - +# Reminder - Update the version checks in gravity/tilt/tilt_tests.py when changing versions for the BT/Tilt pkgs +redis==3.4.1 # for huey & gravity sensor support #pybluez # for gravity sensor support #aioblescan # Replacement for beacontools for Tilt support -PyObjc # For bluetooth support - +pyobjc==6.2 # For MacOS X bluetooth support mod_wsgi # python in apache on macos From 93757602ff4ac6d3cd2566ad32eb264febdf888a Mon Sep 17 00:00:00 2001 From: Thorrak Date: Fri, 10 Apr 2020 14:31:12 -0400 Subject: [PATCH 12/29] Add 'packaging' to requirements.txt --- requirements.txt | 5 +---- requirements_macos.txt | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3a4b8b4d..0b986bf0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,12 +17,9 @@ pyudev # for managing udev rules for serial devices requests # for loading firmware data from websites esptool # for flashing ESP8266 devices - +packaging~=17.1 # for testing requirements in the Tilt tests # Reminder - Update the version checks in gravity/tilt/tilt_tests.py when changing versions for the BT/Tilt pkgs redis==3.4.1 # for huey & gravity sensor support pybluez==0.23 # for gravity sensor support aioblescan==0.2.6 # Replacement for beacontools for Tilt support - - - diff --git a/requirements_macos.txt b/requirements_macos.txt index cffa3d27..18c24090 100644 --- a/requirements_macos.txt +++ b/requirements_macos.txt @@ -17,6 +17,7 @@ pyudev # for managing udev rules for serial devices requests # for loading firmware data from websites esptool # for flashing ESP8266 devices +packaging~=17.1 # for testing requirements in the Tilt tests # Reminder - Update the version checks in gravity/tilt/tilt_tests.py when changing versions for the BT/Tilt pkgs redis==3.4.1 # for huey & gravity sensor support From 89f3d2bed2b486b2f788504f1130dfe8acf068a8 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Fri, 10 Apr 2020 14:34:36 -0400 Subject: [PATCH 13/29] Fix test results list --- gravity/templates/gravity/gravity_tilt_test.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gravity/templates/gravity/gravity_tilt_test.html b/gravity/templates/gravity/gravity_tilt_test.html index cf934d8f..cb108a27 100644 --- a/gravity/templates/gravity/gravity_tilt_test.html +++ b/gravity/templates/gravity/gravity_tilt_test.html @@ -76,7 +76,7 @@

Several python packages are missing.

OK? - {% for test_result in apt_test_results %} + {% for test_result in python_test_results %} {{ test_result.package }} {{ test_result.required_version }} From 5c5208efb0bcc4700069dcf110f8530ef7309733 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Fri, 10 Apr 2020 14:55:04 -0400 Subject: [PATCH 14/29] Apparently PyBluez is camel cased. Who knew? --- gravity/tilt/tilt_tests.py | 2 +- requirements.txt | 2 +- requirements_macos.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gravity/tilt/tilt_tests.py b/gravity/tilt/tilt_tests.py index 7d88c88b..96250bea 100644 --- a/gravity/tilt/tilt_tests.py +++ b/gravity/tilt/tilt_tests.py @@ -54,7 +54,7 @@ def check_python_packages() -> (bool, list): ] else: package_list = [ - {'name': 'pybluez', 'version': version.parse("0.23")}, + {'name': 'PyBluez', 'version': version.parse("0.23")}, {'name': 'aioblescan', 'version': version.parse("0.2.6")}, {'name': 'redis', 'version': version.parse("3.4.1")}, ] diff --git a/requirements.txt b/requirements.txt index 0b986bf0..fb955293 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,5 @@ packaging~=17.1 # for testing requirements in the Tilt tests # Reminder - Update the version checks in gravity/tilt/tilt_tests.py when changing versions for the BT/Tilt pkgs redis==3.4.1 # for huey & gravity sensor support -pybluez==0.23 # for gravity sensor support +PyBluez==0.23 # for gravity sensor support aioblescan==0.2.6 # Replacement for beacontools for Tilt support diff --git a/requirements_macos.txt b/requirements_macos.txt index 18c24090..7ce17439 100644 --- a/requirements_macos.txt +++ b/requirements_macos.txt @@ -21,7 +21,7 @@ packaging~=17.1 # for testing requirements in the Tilt tests # Reminder - Update the version checks in gravity/tilt/tilt_tests.py when changing versions for the BT/Tilt pkgs redis==3.4.1 # for huey & gravity sensor support -#pybluez # for gravity sensor support +#PyBluez # for gravity sensor support #aioblescan # Replacement for beacontools for Tilt support pyobjc==6.2 # For MacOS X bluetooth support From 09e519c567703e40a2b26a4b2e8f13dcf52cfb21 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Fri, 10 Apr 2020 15:07:09 -0400 Subject: [PATCH 15/29] Add a test for Redis connectivity to the Tilt test script --- gravity/gravity_debug.py | 26 ++++++++++------- .../templates/gravity/gravity_tilt_test.html | 28 ++++++++++++++++++- gravity/views_tilt.py | 8 +++++- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/gravity/gravity_debug.py b/gravity/gravity_debug.py index 3bfe2bc3..4cdfd1be 100644 --- a/gravity/gravity_debug.py +++ b/gravity/gravity_debug.py @@ -2,6 +2,8 @@ # and to assist with debugging anything that isn't working properly. The idea is that gravity support was added later # in the development cycle of Fermentrack and therefore cannot be assumed to be working out of the box. +from fermentrack_django import settings + import uuid try: @@ -11,7 +13,7 @@ redis_installed = False -def try_redis(host,port,password): +def try_redis(host=settings.REDIS_HOSTNAME, port=settings.REDIS_PORT, password=settings.REDIS_PASSWORD) -> (bool, bool, bool): # Returns a tuple: (redis_installed, able_to_connect_test_result, key_set_and_retreival_test_result) if not redis_installed: @@ -19,6 +21,7 @@ def try_redis(host,port,password): return False, False, False try: + r = redis.Redis(host=host, port=port, password=password, socket_timeout=3) r.ping() # Test if the connection is active except redis.exceptions.TimeoutError: @@ -40,13 +43,16 @@ def try_redis(host,port,password): # The following was used for testing during development, and is a standalone test that can be run if needed if __name__ == "__main__": # Setting these here for the standalone test - redis_host = '127.0.0.2' - redis_port = 6379 - redis_pass = '' - - redis_install_test, redis_connection_test, redis_value_test = try_redis(redis_host, redis_port, redis_pass) - print "Redis Installed: {}".format(redis_install_test) - print "Testing redis connection to ({},{},{}):".format(redis_host, redis_port, redis_pass) - print "Connection Test: {}".format(redis_connection_test) - print "Value Set/Read Test: {}".format(redis_value_test) + # redis_host = '127.0.0.2' + # redis_port = 6379 + # redis_pass = '' + + # redis_install_test, redis_connection_test, redis_value_test = try_redis(redis_host, redis_port, redis_pass) + redis_install_test, redis_connection_test, redis_value_test = try_redis() + + print("Redis Installed: {}".format(redis_install_test)) + # print("Testing redis connection to ({},{},{}):".format(redis_host, redis_port, redis_pass)) + print("Testing redis connection to ({},{},{}):".format(settings.REDIS_HOSTNAME, settings.REDIS_PORT, settings.REDIS_PASSWORD)) + print("Connection Test: {}".format(redis_connection_test)) + print("Value Set/Read Test: {}".format(redis_value_test)) diff --git a/gravity/templates/gravity/gravity_tilt_test.html b/gravity/templates/gravity/gravity_tilt_test.html index cb108a27..3a3aface 100644 --- a/gravity/templates/gravity/gravity_tilt_test.html +++ b/gravity/templates/gravity/gravity_tilt_test.html @@ -1,7 +1,7 @@ {% extends "sitewide/flat_ui_template.html" %}{% load static %} {% load custom_tags %} -{% block title %}Debug Tilt{% endblock %} +{% block title %}Debug Tilt Support{% endblock %} {% block content %} @@ -87,6 +87,32 @@

Several python packages are missing.

+

Redis Connectivity

+ +

+ Fermentrack uses Redis to store datapoints received from the Tilt for futher processing within Fermentrack. If + Redis support isn't installed, the Redis server isn't running (or is otherwise inaccessible), or we cannot set or + retrieve keys from Redis, then the Tilt support will not function. +

+ + + + + + + + + + + + + + + + +
Redis Support Installed?{% if redis_installed %}Yes{% else %}Not Installed!{% endif %}
Can Connect to Redis?{% if able_to_connect_to_redis %}Yes{% else %}Cannot connect!{% endif %}
Can Set/Retrieve Keys?{% if redis_key_test %}Yes{% else %}Not able to set/retrieve keys!{% endif %}
+ + {% endblock %} diff --git a/gravity/views_tilt.py b/gravity/views_tilt.py index 686a7cc4..a98e4503 100644 --- a/gravity/views_tilt.py +++ b/gravity/views_tilt.py @@ -16,6 +16,7 @@ from gravity import mdnsLocator import gravity.tilt.tilt_tests as tilt_tests +import gravity.gravity_debug as gravity_debug import csv @@ -515,6 +516,11 @@ def gravity_tilt_test(request): # Next, check the python packages has_python_packages, python_test_results = tilt_tests.check_python_packages() + # Then check Redis support + redis_installed, able_to_connect_to_redis, redis_key_test = gravity_debug.try_redis() + return render(request, template_name='gravity/gravity_tilt_test.html', context={'has_apt': has_apt, 'has_apt_packages': has_apt_packages, 'apt_test_results': apt_test_results, - 'has_python_packages': has_python_packages, 'python_test_results': python_test_results,}) + 'has_python_packages': has_python_packages, 'python_test_results': python_test_results, + 'redis_installed': redis_installed, 'able_to_connect_to_redis': able_to_connect_to_redis, + 'redis_key_test': redis_key_test}) From 2cbdd168733f3ddaa605181049961419fa3b83bf Mon Sep 17 00:00:00 2001 From: Thorrak Date: Fri, 10 Apr 2020 15:11:56 -0400 Subject: [PATCH 16/29] Add link to the Tilt debug workflow from the settings page --- gravity/templates/gravity/gravity_manage_tilt.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gravity/templates/gravity/gravity_manage_tilt.html b/gravity/templates/gravity/gravity_manage_tilt.html index d9c7281a..94bb61ad 100644 --- a/gravity/templates/gravity/gravity_manage_tilt.html +++ b/gravity/templates/gravity/gravity_manage_tilt.html @@ -140,6 +140,11 @@

Extra Data from Device

Current Signal Strength
+ +

Troubleshoot Tilt Connection

+ +

If you are having difficulty getting/keeping your Tilt connected, click here to debug the connection.

+ {% endif %} From d04f0bd683765df714e484ae39dc12e6dd4e23fb Mon Sep 17 00:00:00 2001 From: Thorrak Date: Fri, 10 Apr 2020 15:15:06 -0400 Subject: [PATCH 17/29] Update changelog & fix display of TiltBridge settings on Tilt settings page --- docs/source/develop/changelog.rst | 7 ++++++- gravity/templates/gravity/gravity_manage_tilt.html | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/source/develop/changelog.rst b/docs/source/develop/changelog.rst index 8af2f259..04068bd2 100644 --- a/docs/source/develop/changelog.rst +++ b/docs/source/develop/changelog.rst @@ -16,6 +16,7 @@ Added - Exposed upgrade.log from the help screen - Store the exact last time that a message was received from a Tilt to Redis - Add sentry support to tilt_monitor_aio.py +- Added "debug" scripts for bluetooth Tilt connections @@ -24,9 +25,13 @@ Changed - Removed legacy Python 2 code - Reduced gravity sensor temp precision to 0.1 degrees -- Locked pybluez and aioblescan versions to prevent undesired format changes going forward +- Locked pybluez, aioblescan, and redis versions to prevent undesired format changes going forward +Fixed +--------------------- + +- Fix display of TiltBridge mDNS settings on Tilt settings page [2019-02-17] - Improved ESP32 Flashing Support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/gravity/templates/gravity/gravity_manage_tilt.html b/gravity/templates/gravity/gravity_manage_tilt.html index 94bb61ad..6907e050 100644 --- a/gravity/templates/gravity/gravity_manage_tilt.html +++ b/gravity/templates/gravity/gravity_manage_tilt.html @@ -37,7 +37,7 @@

Configuration Options

{{ active_device.tilt_configuration.polling_frequency }} - {% if active_device.tilt_configuration.connection_type == "TiltBridge" %} + {% if active_device.tilt_configuration.connection_type == "Bridge" %} TiltBridge Name {{ active_device.tilt_configuration.tiltbridge.name }} From accf37a5405aee1560be926c3c44ba85d4cbb902 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Fri, 10 Apr 2020 15:17:39 -0400 Subject: [PATCH 18/29] Fix Tilt python package test --- gravity/tilt/tilt_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gravity/tilt/tilt_tests.py b/gravity/tilt/tilt_tests.py index 96250bea..d480dac5 100644 --- a/gravity/tilt/tilt_tests.py +++ b/gravity/tilt/tilt_tests.py @@ -73,8 +73,8 @@ def check_python_packages() -> (bool, list): for package in pkg_resources.working_set: if package.project_name == package_to_find['name']: result_stub['installed_version'] = package.parsed_version - if result_stub['installed_version'] != result_stub['required_version']: - result_stub['ok'] = False + if result_stub['installed_version'] == result_stub['required_version']: + result_stub['ok'] = True if result_stub['ok'] is False: all_packages_ok = False From b359d71a3c13720257167c57959a18b7a5ce9d07 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Fri, 10 Apr 2020 15:26:02 -0400 Subject: [PATCH 19/29] Fix Python package version checking --- gravity/tilt/tilt_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gravity/tilt/tilt_tests.py b/gravity/tilt/tilt_tests.py index d480dac5..6e91f173 100644 --- a/gravity/tilt/tilt_tests.py +++ b/gravity/tilt/tilt_tests.py @@ -49,7 +49,7 @@ def check_python_packages() -> (bool, list): if sys.platform == "darwin": # The MacOS support uses different packages from the support for Linux package_list = [ - {'name': 'PyObjc', 'version': version.parse("6.2")}, + {'name': 'pyobjc', 'version': version.parse("6.2")}, {'name': 'redis', 'version': version.parse("3.4.1")}, ] else: @@ -73,7 +73,7 @@ def check_python_packages() -> (bool, list): for package in pkg_resources.working_set: if package.project_name == package_to_find['name']: result_stub['installed_version'] = package.parsed_version - if result_stub['installed_version'] == result_stub['required_version']: + if result_stub['installed_version'].public == result_stub['required_version'].public: result_stub['ok'] = True if result_stub['ok'] is False: From c5b0bdec65a9dd19cecb4b5e14ebed49226e12a8 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Fri, 10 Apr 2020 16:33:10 -0400 Subject: [PATCH 20/29] Add check for packaging --- .../templates/gravity/gravity_tilt_test.html | 51 +++++++++++-------- gravity/tilt/tilt_tests.py | 14 ++++- gravity/views_tilt.py | 4 +- 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/gravity/templates/gravity/gravity_tilt_test.html b/gravity/templates/gravity/gravity_tilt_test.html index 3a3aface..394900ab 100644 --- a/gravity/templates/gravity/gravity_tilt_test.html +++ b/gravity/templates/gravity/gravity_tilt_test.html @@ -61,31 +61,40 @@

Python Packages

Note - The package list being tested below does not apply to installations on Windows.

- {% if has_python_packages %} -

All necessary python packages are installed

- {% else %} -

Several python packages are missing.

- {% endif %}{# has_python_packages #} + {% if has_packaging %} + {% if has_python_packages %} +

All necessary python packages are installed

+ {% else %} +

Several python packages are missing.

+ {% endif %}{# has_python_packages #} - - - - - - - - {% for test_result in python_test_results %} - - - - - - - {% endfor %} -
Package NameRequired VersionInstalled VersionOK?
{{ test_result.package }}{{ test_result.required_version }}{{ test_result.installed_version }}{% if test_result.ok %}OK{% else %}Not OK{% endif %}
+ + + + + + + + + {% for test_result in python_test_results %} + + + + + + + {% endfor %} +
Package NameRequired VersionInstalled VersionOK?
{{ test_result.package }}{{ test_result.required_version }}{{ test_result.installed_version }}{% if test_result.ok %}OK{% else %}Not OK{% endif %}
+ {% else %} +

Python 'packaging' module is not available - Test cannot run!

+

+ This is a fairly serious error, as it implies that your python packages are not being kept up-to-date. + Check the upgrade log to see what is happening. +

+ {% endif %}

Redis Connectivity

diff --git a/gravity/tilt/tilt_tests.py b/gravity/tilt/tilt_tests.py index 6e91f173..5540c396 100644 --- a/gravity/tilt/tilt_tests.py +++ b/gravity/tilt/tilt_tests.py @@ -1,6 +1,12 @@ import os, subprocess, sys import pkg_resources -from packaging import version + +try: + from packaging import version + has_packaging = True +except: + has_packaging = False + # This function is used ot check if an apt package is installed on Raspbian, Ubuntu, Debian, etc. @@ -45,7 +51,11 @@ def check_apt_packages() -> (bool, list): return all_packages_ok, test_results -def check_python_packages() -> (bool, list): +def check_python_packages() -> (bool, bool, list): + # Returns has_packaging, all_packages_ok, test_results[] + if not has_packaging: + return False, False, [] + if sys.platform == "darwin": # The MacOS support uses different packages from the support for Linux package_list = [ diff --git a/gravity/views_tilt.py b/gravity/views_tilt.py index a98e4503..56471daa 100644 --- a/gravity/views_tilt.py +++ b/gravity/views_tilt.py @@ -514,7 +514,7 @@ def gravity_tilt_test(request): apt_test_results = [] # Next, check the python packages - has_python_packages, python_test_results = tilt_tests.check_python_packages() + has_packaging, has_python_packages, python_test_results = tilt_tests.check_python_packages() # Then check Redis support redis_installed, able_to_connect_to_redis, redis_key_test = gravity_debug.try_redis() @@ -523,4 +523,4 @@ def gravity_tilt_test(request): context={'has_apt': has_apt, 'has_apt_packages': has_apt_packages, 'apt_test_results': apt_test_results, 'has_python_packages': has_python_packages, 'python_test_results': python_test_results, 'redis_installed': redis_installed, 'able_to_connect_to_redis': able_to_connect_to_redis, - 'redis_key_test': redis_key_test}) + 'redis_key_test': redis_key_test, 'has_packaging': has_packaging,}) From 4fc3038356c48e301347906c4f86ed5712f932eb Mon Sep 17 00:00:00 2001 From: Thorrak Date: Fri, 10 Apr 2020 17:50:53 -0400 Subject: [PATCH 21/29] Fix check for packaging --- gravity/tilt/tilt_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gravity/tilt/tilt_tests.py b/gravity/tilt/tilt_tests.py index 5540c396..9f4702af 100644 --- a/gravity/tilt/tilt_tests.py +++ b/gravity/tilt/tilt_tests.py @@ -54,7 +54,7 @@ def check_apt_packages() -> (bool, list): def check_python_packages() -> (bool, bool, list): # Returns has_packaging, all_packages_ok, test_results[] if not has_packaging: - return False, False, [] + return has_packaging, False, [] if sys.platform == "darwin": # The MacOS support uses different packages from the support for Linux @@ -90,7 +90,7 @@ def check_python_packages() -> (bool, bool, list): all_packages_ok = False test_results.append(result_stub) - return all_packages_ok, test_results + return has_packaging, all_packages_ok, test_results # The following was used for testing during development From 52d2e020dd223caa883e72ac3340e3fbc2c5ab43 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Fri, 10 Apr 2020 21:53:21 -0400 Subject: [PATCH 22/29] Create fix_python_requirements script & add links from the Tilt test workflow --- app/templates/github_trigger_upgrade.html | 6 ++ .../trigger_requirements_reload.html | 30 ++++++++ app/views.py | 14 ++++ docs/source/gravitysensors/tilt.rst | 7 +- fermentrack_django/urls.py | 1 + .../templates/gravity/gravity_tilt_test.html | 42 +++++++++-- utils/fix_python_requirements.sh | 73 +++++++++++++++++++ 7 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 app/templates/trigger_requirements_reload.html create mode 100755 utils/fix_python_requirements.sh diff --git a/app/templates/github_trigger_upgrade.html b/app/templates/github_trigger_upgrade.html index f5226ecc..850ddc36 100644 --- a/app/templates/github_trigger_upgrade.html +++ b/app/templates/github_trigger_upgrade.html @@ -74,6 +74,12 @@

Remote Commit Info:

{% endif %} +

Need to manually refresh the Python packages without upgrading? Click the below to trigger a refresh without pulling + new code from GitHub

+

+ Refresh Python Packages +

+ {% if allow_git_branch_switching %}

Switch Branch

diff --git a/app/templates/trigger_requirements_reload.html b/app/templates/trigger_requirements_reload.html new file mode 100644 index 00000000..d91f6f3b --- /dev/null +++ b/app/templates/trigger_requirements_reload.html @@ -0,0 +1,30 @@ +{% extends "sitewide/flat_ui_template.html" %} +{% load custom_tags %} +{% load static %} + +{% block title %}Reload Python Requirements{% endblock %} + +{% block header_scripts %} +{% endblock %} + + +{% block content %} +

Reload Python Requirements

+ +

+ You have triggered a manual reload of the Python packages required by Fermentrack. Please wait several minutes + for this refresh to complete and for Fermentrack to relaunch before proceeding. +

+ +

+ Once complete, you can view the upgrade log to see + what was installed/updated. +

+ + +{% endblock %} + + + +{% block scripts %} +{% endblock %} \ No newline at end of file diff --git a/app/views.py b/app/views.py index 72a4d797..e5b1460e 100644 --- a/app/views.py +++ b/app/views.py @@ -540,6 +540,20 @@ def github_trigger_force_upgrade(request): +@login_required +@site_is_configured +def trigger_requirements_reload(request): + # TODO - Add permission check here + + # All that this view does is trigger the utils/fix_python_requirements.sh shell script and return a message letting + # the user know that Fermentrack will take a few minutes to restart. + cmd = "nohup utils/fix_python_requirements.sh &" + messages.success(request, "Triggered a reload of your Python packages") + subprocess.call(cmd, shell=True) + + return render(request, template_name="trigger_requirements_reload.html", context={}) + + def login(request, next=None): if not next: if 'next' in request.GET: diff --git a/docs/source/gravitysensors/tilt.rst b/docs/source/gravitysensors/tilt.rst index d811bfd8..e0333c16 100644 --- a/docs/source/gravitysensors/tilt.rst +++ b/docs/source/gravitysensors/tilt.rst @@ -103,7 +103,12 @@ Although all Python packages should be automatically installed as part of the in packages come out of sync for a variety of reasons. If you are missing packages they will need to be installed for Fermentrack to properly interface with your Tilt. -.. todo:: Enrich this with steps for resolving missing Python packages +A manual refresh of the Python packages can be triggered from the GitHub upgrade page without updating Fermentrack from +GitHub. To trigger a refresh: +#. Log into Fermentrack +#. Click the 'gear' icon in the upper right hand corner of the page +#. Click 'Update from GitHub' +#. Click the 'Refresh Python Packages' button diff --git a/fermentrack_django/urls.py b/fermentrack_django/urls.py index 48477ad5..e6fa7ac0 100644 --- a/fermentrack_django/urls.py +++ b/fermentrack_django/urls.py @@ -31,6 +31,7 @@ url(r'^upgrade/$', app.views.github_trigger_upgrade, name='github_trigger_upgrade'), url(r'^upgrade/force/$', app.views.github_trigger_force_upgrade, name='github_trigger_force_upgrade'), + url(r'^upgrade/reload/$', app.views.trigger_requirements_reload, name='trigger_requirements_reload'), ### Device Views url(r'^devices/$', app.views.device_lcd_list, name='device_lcd_list'), diff --git a/gravity/templates/gravity/gravity_tilt_test.html b/gravity/templates/gravity/gravity_tilt_test.html index 394900ab..6e0b5ffa 100644 --- a/gravity/templates/gravity/gravity_tilt_test.html +++ b/gravity/templates/gravity/gravity_tilt_test.html @@ -25,8 +25,8 @@

Several system packages are missing.

- - + + {% for test_result in apt_test_results %} @@ -37,6 +37,19 @@

Several system packages are missing.

{% endfor %}
Package NameOK?Package NameOK?
+ + {% if not has_apt_packages %} +
NOTE - Fix the above errors first and then re-run the tests before proceeding
+ +

+ Several of the system packages listed above are required for all of the Python packages to install. If you + do not first install the missing system packages you may be unable to fix other errors that appear on this + page. Thankfully, installing these missing packages is pretty simple. Information on how to resolve this + can be found in the Fermentrack documentation. +

+ {% endif %}{# has_apt_packages #} + + {% else %}

Unable to locate the software necessary to check system packages

@@ -46,6 +59,8 @@

Unable to locate the software necessary to check system packages

{% endif %}{# has_apt #} + +

Python Packages

@@ -72,10 +87,10 @@

Several python packages are missing.

- - - - + + + + {% for test_result in python_test_results %} @@ -87,11 +102,24 @@

Several python packages are missing.

{% endfor %}
Package NameRequired VersionInstalled VersionOK?Package NameRequired VersionInstalled VersionOK?
+ + {% if not has_python_packages %} +

+ Refresh Python Packages +

+ {% endif %} + {% else %}

Python 'packaging' module is not available - Test cannot run!

This is a fairly serious error, as it implies that your python packages are not being kept up-to-date. - Check the upgrade log to see what is happening. + If there are missing system packages (above) resolve those before proceeding. Next, attempt a + manual refresh (below) of the Python requirements to see if the 'packaging' module can be installed. If + neither of these resolve this issue, check the + upgrade log to see what is happening. +

+

+ Refresh Python Packages

{% endif %} diff --git a/utils/fix_python_requirements.sh b/utils/fix_python_requirements.sh new file mode 100755 index 00000000..51ef6349 --- /dev/null +++ b/utils/fix_python_requirements.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +# Defaults +SILENT=0 +CIRCUSCTL="python3 -m circus.circusctl --timeout 10" + +# Colors (for printinfo/error/warn below) +green=$(tput setaf 76) +red=$(tput setaf 1) +tan=$(tput setaf 3) +reset=$(tput sgr0) + + + +printinfo() { + if [ ${SILENT} -eq 0 ] + then + printf "::: ${green}%s${reset}\n" "$@" + fi +} + + +printwarn() { + if [ ${SILENT} -eq 0 ] + then + printf "${tan}*** WARNING: %s${reset}\n" "$@" + fi +} + + +printerror() { + if [ ${SILENT} -eq 0 ] + then + printf "${red}*** ERROR: %s${reset}\n" "$@" + fi +} + + + +exec > >(tee -i log/upgrade.log) + + +printinfo "Re-installing Python packages from requirements.txt" +# First, launch the virtualenv +source ~/venv/bin/activate # Assuming the directory based on a normal install with Fermentrack-tools + +# Given that this script can be called by the webapp proper, give it 2 seconds to finish sending a reply to the +# user if he/she initiated an upgrade through the webapp. +printinfo "Waiting 1 second for Fermentrack to send updates if triggered from the web..." +sleep 1s + +# Next, kill the running Fermentrack instance using circus +printinfo "Stopping circus..." +$CIRCUSCTL stop &>> log/upgrade.log + +# Install everything from requirements.txt +printinfo "Updating requirements via pip3..." +pip3 install -U -r requirements.txt --upgrade &>> log/upgrade.log + +# Migrate to create/adjust anything necessary in the database +printinfo "Running manage.py migrate..." +python3 manage.py migrate &>> log/upgrade.log + +# Migrate to create/adjust anything necessary in the database +printinfo "Running manage.py collectstatic..." +python3 manage.py collectstatic --noinput >> /dev/null + + +# Finally, relaunch the Fermentrack instance using circus +printinfo "Relaunching circus..." +$CIRCUSCTL reloadconfig &>> log/upgrade.log +$CIRCUSCTL start &>> log/upgrade.log +printinfo "Complete!" From 754b4499b7d77d0c6bc275ff67454b2030903604 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Fri, 10 Apr 2020 22:08:01 -0400 Subject: [PATCH 23/29] Update text --- app/templates/github_trigger_upgrade.html | 2 +- docs/source/gravitysensors/tilt.rst | 2 +- .../gravity/gravity_manage_tilt.html | 6 +++++- .../templates/gravity/gravity_tilt_test.html | 19 ++++++++++++++++--- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/templates/github_trigger_upgrade.html b/app/templates/github_trigger_upgrade.html index 850ddc36..fc6bbf0d 100644 --- a/app/templates/github_trigger_upgrade.html +++ b/app/templates/github_trigger_upgrade.html @@ -77,7 +77,7 @@

Remote Commit Info:

Need to manually refresh the Python packages without upgrading? Click the below to trigger a refresh without pulling new code from GitHub

- Refresh Python Packages + Update/Install Missing Python Packages

{% if allow_git_branch_switching %} diff --git a/docs/source/gravitysensors/tilt.rst b/docs/source/gravitysensors/tilt.rst index e0333c16..f95fbfc1 100644 --- a/docs/source/gravitysensors/tilt.rst +++ b/docs/source/gravitysensors/tilt.rst @@ -109,6 +109,6 @@ GitHub. To trigger a refresh: #. Log into Fermentrack #. Click the 'gear' icon in the upper right hand corner of the page #. Click 'Update from GitHub' -#. Click the 'Refresh Python Packages' button +#. Click the 'Update/Install Missing Python Packages' button diff --git a/gravity/templates/gravity/gravity_manage_tilt.html b/gravity/templates/gravity/gravity_manage_tilt.html index 6907e050..f0f4a2f1 100644 --- a/gravity/templates/gravity/gravity_manage_tilt.html +++ b/gravity/templates/gravity/gravity_manage_tilt.html @@ -143,7 +143,11 @@

Extra Data from Device

Troubleshoot Tilt Connection

-

If you are having difficulty getting/keeping your Tilt connected, click here to debug the connection.

+

If you are having difficulty getting/keeping your Bluetooth Tilt connected, click the button below to launch the Tilt test script.

+ +

+ Troubleshoot Tilt Connection +

{% endif %} diff --git a/gravity/templates/gravity/gravity_tilt_test.html b/gravity/templates/gravity/gravity_tilt_test.html index 6e0b5ffa..433aca3a 100644 --- a/gravity/templates/gravity/gravity_tilt_test.html +++ b/gravity/templates/gravity/gravity_tilt_test.html @@ -104,13 +104,26 @@

Several python packages are missing.

{% if not has_python_packages %} + +

+ One or more of the Python packages required for Fermentrack's Tilt support to function are either + missing or at an incorrect version. + {% if not has_apt_packages %} + This may be caused by missing system packages. Before proceeding, please ensure that all required + system packages (see the earlier test) are installed. + {% endif %} + The easiest way to resolve this is to trigger a manual refresh of the Python requirements. To do this, + click the button below. If this does not resolve your missing packages check the + upgrade log to see what is happening. +

+

- Refresh Python Packages + Update/Install Missing Python Packages

{% endif %} {% else %} -

Python 'packaging' module is not available - Test cannot run!

+

Python 'packaging' module is not available - Test cannot run!

This is a fairly serious error, as it implies that your python packages are not being kept up-to-date. If there are missing system packages (above) resolve those before proceeding. Next, attempt a @@ -119,7 +132,7 @@

Python 'packaging' module is not available - Test cannot run!

upgrade log to see what is happening.

- Refresh Python Packages + Update/Install Missing Python Packages

{% endif %} From 4316bc154f2d8058626d540957c43e4451ef5aca Mon Sep 17 00:00:00 2001 From: Thorrak Date: Sat, 11 Apr 2020 13:01:11 -0400 Subject: [PATCH 24/29] Add link to Tilt bluetooth debug script to the site help page & tweak formatting --- app/templates/site_help.html | 7 ++++++ .../templates/gravity/gravity_tilt_test.html | 24 +++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/templates/site_help.html b/app/templates/site_help.html index f63ce793..d603eb1d 100644 --- a/app/templates/site_help.html +++ b/app/templates/site_help.html @@ -106,6 +106,13 @@

Other logs

+ +

Other troubleshooting tools

+ +

+ Troubleshoot Tilt/Bluetooth Support +

+ {% endblock %} {% block scripts %}{% endblock %} diff --git a/gravity/templates/gravity/gravity_tilt_test.html b/gravity/templates/gravity/gravity_tilt_test.html index 433aca3a..1b815858 100644 --- a/gravity/templates/gravity/gravity_tilt_test.html +++ b/gravity/templates/gravity/gravity_tilt_test.html @@ -24,10 +24,12 @@

Several system packages are missing.

{% endif %}{# has_apt_packages #} - - - - + + + + + + {% for test_result in apt_test_results %} @@ -86,12 +88,14 @@

Several python packages are missing.

Package NameOK?
Package NameOK?
- - - - - - + + + + + + + + {% for test_result in python_test_results %} From 425f9b45dd159e120292876dc0edd1644be96d03 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Sat, 11 Apr 2020 13:23:02 -0400 Subject: [PATCH 25/29] Add TiltBridge connection information to Tilt management screen --- docs/source/develop/changelog.rst | 1 + .../gravity/gravity_manage_tilt.html | 25 +++++++++++++++ gravity/urls.py | 2 ++ gravity/views.py | 20 +++++++++++- gravity/views_tilt.py | 31 +++++++++++++++++++ 5 files changed, 78 insertions(+), 1 deletion(-) diff --git a/docs/source/develop/changelog.rst b/docs/source/develop/changelog.rst index 04068bd2..f9b7c3a8 100644 --- a/docs/source/develop/changelog.rst +++ b/docs/source/develop/changelog.rst @@ -17,6 +17,7 @@ Added - Store the exact last time that a message was received from a Tilt to Redis - Add sentry support to tilt_monitor_aio.py - Added "debug" scripts for bluetooth Tilt connections +- Added TiltBridge connection settings to Tilt management page diff --git a/gravity/templates/gravity/gravity_manage_tilt.html b/gravity/templates/gravity/gravity_manage_tilt.html index f0f4a2f1..1b7fc374 100644 --- a/gravity/templates/gravity/gravity_manage_tilt.html +++ b/gravity/templates/gravity/gravity_manage_tilt.html @@ -151,6 +151,31 @@

Troubleshoot Tilt Connection

{% endif %} + {% if active_device.tilt_configuration.connection_type == "Bridge" %} +

TiltBridge Configuration

+ +

+ In order for your TiltBridge to communicate with Fermentrack, it needs to be told where Fermentrack can be + reached on your network. If the mDNS ID specified above is correct, this can be done automatically by + Fermentrack, or you can set this manually on the TiltBridge's configuration page. +

+ +
+
+
Package NameRequired VersionInstalled VersionOK?
Package NameRequired VersionInstalled VersionOK?
+ + + + +
Fermentrack URL (copy to TiltBridge){{ fermentrack_url }}
+ + + +

+ Update TiltBridge Automatically +

+ {% endif %} +

- Update TiltBridge Automatically + Update TiltBridge Automatically

{% endif %} From ad600649a38c3e4145f413d85781cd783c299005 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Sat, 11 Apr 2020 13:29:00 -0400 Subject: [PATCH 27/29] Fix TiltBridge ID --- gravity/views_tilt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gravity/views_tilt.py b/gravity/views_tilt.py index a8d1ff59..0fac4cfc 100644 --- a/gravity/views_tilt.py +++ b/gravity/views_tilt.py @@ -467,9 +467,9 @@ def gravity_tiltbridge_set_url(request, tiltbridge_id, sensor_id=None): # return redirect("/") try: - this_tiltbridge = TiltBridge.objects.get(id=tiltbridge_id) + this_tiltbridge = TiltBridge.objects.get(mdns_id=tiltbridge_id) except ObjectDoesNotExist: - messages.error(request, "Unable to locate TiltBridge with ID {}".format(tiltbridge_id)) + messages.error(request, "Unable to locate TiltBridge with mDNS ID {}".format(tiltbridge_id)) if sensor_id is not None: return redirect("gravity_manage", sensor_id=sensor_id) else: From 3c1747299f0fe3a024ee01a3b4c213ef443b3680 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Sat, 11 Apr 2020 13:44:11 -0400 Subject: [PATCH 28/29] Improve the quality of an error message --- gravity/views_tilt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gravity/views_tilt.py b/gravity/views_tilt.py index 0fac4cfc..6720c2ff 100644 --- a/gravity/views_tilt.py +++ b/gravity/views_tilt.py @@ -481,7 +481,7 @@ def gravity_tiltbridge_set_url(request, tiltbridge_id, sensor_id=None): if this_tiltbridge.update_fermentrack_url_on_tiltbridge(fermentrack_host): messages.success(request, u"Updated Fermentrack URL on TiltBridge '{}'".format(this_tiltbridge.name)) else: - messages.error(request, u"Unable to automatically update Fermentrack URL on TiltBridge {}".format(this_tiltbridge.name)) + messages.error(request, u"Unable to automatically update Fermentrack URL at {}.local".format(this_tiltbridge.mdns_id)) # If we were passed a sensor ID, we want to return to the management screen for that ID. if sensor_id is not None: From 8e56454cc872c20b4e3fad8245b5c6021bc5207f Mon Sep 17 00:00:00 2001 From: Thorrak Date: Sat, 11 Apr 2020 13:48:24 -0400 Subject: [PATCH 29/29] Finalize changelog for release to master --- docs/source/develop/changelog.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/develop/changelog.rst b/docs/source/develop/changelog.rst index f9b7c3a8..0aec2393 100644 --- a/docs/source/develop/changelog.rst +++ b/docs/source/develop/changelog.rst @@ -6,8 +6,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) because it was the first relatively standard format to pop up when I googled "changelog formats". -[Unversioned] - Bugfixes -~~~~~~~~~~~~~~~~~~~~~~~~ +[2020-04-11] - Bugfixes & Tilt Troubleshooting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added --------------------- @@ -34,7 +34,7 @@ Fixed - Fix display of TiltBridge mDNS settings on Tilt settings page -[2019-02-17] - Improved ESP32 Flashing Support +[2020-02-17] - Improved ESP32 Flashing Support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added @@ -49,7 +49,7 @@ Changed - SPIFFS partitions can now be flashed to ESP8266 devices -[2019-02-15] - ThingSpeak and Grainfather Support +[2020-02-15] - ThingSpeak and Grainfather Support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added