Skip to content

Commit

Permalink
Merge pull request #732 from thorrak/dev
Browse files Browse the repository at this point in the history
Update Master with Dev
  • Loading branch information
thorrak authored Jul 4, 2023
2 parents d125ac0 + 5585534 commit 373d557
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 64 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docker-hub-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: 'Build & Push to Docker Hub (Testing)'
on:
push:
branches:
- separate-tilt
- script

jobs:
buildx:
Expand Down
3 changes: 3 additions & 0 deletions app/connection_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def test_telnet(hostname, port):
return False, False, None
except ConnectionRefusedError:
return False, False, None
except OSError:
# e.g. [Errno 113] No route to host
return False, False, None

try:
tn.write(b"n\r\n")
Expand Down
46 changes: 22 additions & 24 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,11 +764,11 @@ def load_sensors_from_device(self):
self.installed_devices = None
if not device_response:
# We weren't able to reach brewpi-script
self.error_message = "Unable to reach brewpi-script. Try restarting brewpi-script."
self.error_message = "Unable to reach brewpi-script. Try restarting the controller. If that fails, restart Fermentrack."
else:
# We were able to reach brewpi-script, but it wasn't able to reach the controller
self.error_message = "BrewPi-script wasn't able to load sensors from the controller. "
self.error_message += "Try restarting brewpi-script. If that fails, try restarting the controller."
self.error_message += "Try restarting the controller. If that fails, restart Fermentrack."
return False # False

# Devices loaded
Expand Down Expand Up @@ -1739,39 +1739,37 @@ def profile_temp(self, time_started, temp_format) -> float:
# the profile point's format to the device's format.
profile_points = self.fermentationprofilepoint_set.order_by('ttl')

past_first_point=False # There's guaranteed to be a better way to do this
previous_setpoint = Decimal("0.0")
previous_ttl = 0.0
previous_ttl = timezone.timedelta(seconds=0)
current_time = timezone.now()

# If the first point in the profile has a TTL other than 0, then we assume we hold that temp from assignment
# until then.
if current_time <= (time_started + profile_points[0].ttl):
return float(profile_points[0].convert_temp(temp_format))

for this_point in profile_points:
if not past_first_point:
# If we haven't hit the first TTL yet, we are in the initial lag period where we hold a constant
# temperature. Return the temperature setting
if current_time < (time_started + this_point.ttl):
return float(this_point.convert_temp(temp_format))
past_first_point = True
else:
# Test if we are in this period
if current_time < (time_started + this_point.ttl):
# We are - Check if we need to interpolate, or if we can just use the static temperature
if this_point.convert_temp(temp_format) == previous_setpoint: # We can just use the static temperature
return float(this_point.convert_temp(temp_format))
else: # We have to interpolate
duration = this_point.ttl.total_seconds() - previous_ttl.total_seconds()
delta = (this_point.convert_temp(temp_format) - previous_setpoint)
slope = float(delta) / duration
# Test if we are in this period
if current_time < (time_started + this_point.ttl):
# this_point is in the future. Check if this is a "hold temp" block or a "ramp" block
if this_point.convert_temp(temp_format) == previous_setpoint: # We can just use the static temperature
# This is a "hold temp" block (previous point's temp = current point's temp)
return float(previous_setpoint)
else: # We have to interpolate
duration = this_point.ttl.total_seconds() - previous_ttl.total_seconds()
delta = (this_point.convert_temp(temp_format) - previous_setpoint)
slope = float(delta) / duration

seconds_into_point = (current_time - (time_started + previous_ttl)).total_seconds()
seconds_into_point = (current_time - (time_started + previous_ttl)).total_seconds()

return round(seconds_into_point * slope + float(previous_setpoint), 1)
return round(seconds_into_point * slope + float(previous_setpoint), 1)

previous_setpoint = this_point.convert_temp(temp_format)
previous_ttl = this_point.ttl

# If we hit this point, we looped through all the setpoints & aren't between two (or on the first one)
# That is to say - we're at the end. Just return the last setpoint.
return previous_setpoint
return float(previous_setpoint)

# past_end_of_profile allows us to test if we're in the last stage of a profile (which is effectively beer constant
# mode) so we can switch to explicitly be in beer constant mode
Expand Down Expand Up @@ -2051,7 +2049,7 @@ def convert_temp(self, desired_temp_format) -> Decimal:
elif self.temp_format == 'C' and desired_temp_format == 'F':
return self.temp_to_f()
else:
logger.error("Invalid temperature format {} specified".format(desired_temp_format))
logger.error("Invalid temperature format {} specified (current temp format {})".format(desired_temp_format, self.temp_format))
return self.temperature_setting

def ttl_to_string(self, short_code=False):
Expand Down
64 changes: 39 additions & 25 deletions brewpi-script/brewpi.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/python
# Copyright 2012 BrewPi
# This file is part of BrewPi.

import datetime
# BrewPi is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
Expand Down Expand Up @@ -36,6 +36,17 @@
from scriptlibs import expandLogMessage
from scriptlibs.backgroundserial import BackGroundSerial

import sentry_sdk
sentry_sdk.init(
"http://[email protected]:9000/13",

# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production.
traces_sample_rate=0.0
)


# Settings will be read from controller, initialize with same defaults as controller
# This is mainly to show what's expected. Will all be overwritten on the first update from the controller

Expand Down Expand Up @@ -668,30 +679,33 @@ def trigger_refresh(read_values=False):
logMessage("Error receiving mode from controller - restarting")
sys.exit(1)
if cs['mode'] == 'p':
new_temp = config_obj.get_profile_temp()

if new_temp is None: # If we had an error loading a temperature (from dbConfig) disable temp control
cs['mode'] = 'o'
bg_ser.writeln("j{mode:\"o\"}")
logMessage("Notification: Error in profile mode - turning off temp control")
# raise socket.timeout # go to serial communication to update controller
elif round(new_temp, 2) != cs['beerSet']:
try:
new_temp = float(new_temp)
cs['beerSet'] = round(new_temp, 2)
except ValueError:
logMessage("Cannot convert temperature '" + new_temp + "' to float")
continue
# if temperature has to be updated send settings to controller
bg_ser.writeln("j{beerSet:" + json.dumps(cs['beerSet']) + "}")

if config_obj.is_past_end_of_profile():
bg_ser.writeln("j{mode:\"b\", beerSet:" + json.dumps(cs['beerSet']) + "}")
cs['mode'] = 'b'
refresh_and_check(config_obj, run) # Reload dbConfig from the database
config_obj.reset_profile()
logMessage("Notification: Beer temperature set to constant " + str(cs['beerSet']) +
" degrees at end of profile")
# Limit profile updates to once every 30 seconds (prevents hammering the database)
if datetime.datetime.now() > (config_obj.last_profile_temp_check + datetime.timedelta(seconds=30)):
config_obj.last_profile_temp_check = datetime.datetime.now() # Update the last check time
new_temp = config_obj.get_profile_temp()

if new_temp is None: # If we had an error loading a temperature (from dbConfig) disable temp control
cs['mode'] = 'o'
bg_ser.writeln("j{mode:\"o\"}")
logMessage("Notification: Error in profile mode - turning off temp control")
# raise socket.timeout # go to serial communication to update controller
elif round(new_temp, 2) != cs['beerSet']:
try:
new_temp = float(new_temp)
cs['beerSet'] = round(new_temp, 2)
except ValueError:
logMessage("Cannot convert temperature '" + new_temp + "' to float")
continue
# if temperature has to be updated send settings to controller
bg_ser.writeln("j{beerSet:" + json.dumps(cs['beerSet']) + "}")

if config_obj.is_past_end_of_profile():
bg_ser.writeln("j{mode:\"b\", beerSet:" + json.dumps(cs['beerSet']) + "}")
cs['mode'] = 'b'
refresh_and_check(config_obj, run) # Reload dbConfig from the database
config_obj.reset_profile()
logMessage("Notification: Beer temperature set to constant " + str(cs['beerSet']) +
" degrees at end of profile")

except socket.error as e:
logMessage("Socket error(%d): %s" % (e.errno, e.strerror))
Expand Down
25 changes: 19 additions & 6 deletions brewpi-script/fermentrack_caller.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,19 @@
from fermentrack_config_loader import FermentrackBrewPiScriptConfig, get_active_brewpi_devices
from brewpi import BrewPiScript

import sentry_sdk
sentry_sdk.init(
"http://[email protected]:9000/13",

# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production.
traces_sample_rate=0.0
)

if __name__ == '__main__':
process_list = {}
config_list = {}

while 1:
# Clean out dead processes from the process list
Expand All @@ -22,6 +32,7 @@
for this_process in processes_to_delete:
# Do this as step 2 since we can't change the process list mid-iteration
# print(f"Deleting process for BrewPiDevice #{this_process}")
process_list[this_process].join(10) # Join the completed process
del process_list[this_process]

active_device_ids = get_active_brewpi_devices()
Expand All @@ -31,13 +42,15 @@
if this_id not in process_list:
# The process hasn't been spawned. Spawn it.
# print(f"Launching process for BrewPiDevice #{this_id}")
if this_id in config_list:
config_list.pop(this_id)
try:
brewpi_config = FermentrackBrewPiScriptConfig(brewpi_device_id=this_id)
brewpi_config.load_from_fermentrack(False)
config_list[this_id] = FermentrackBrewPiScriptConfig(brewpi_device_id=this_id)
config_list[this_id].load_from_fermentrack(False)
process_list[this_id] = Process(target=BrewPiScript, args=(config_list[this_id], ))
process_list[this_id].start()
time.sleep(10) # Give each controller 10 seconds to start up
except StopIteration:
pass
else:
process_list[this_id] = Process(target=BrewPiScript, args=(brewpi_config, ))
process_list[this_id].start()

time.sleep(5)
time.sleep(5) # Wait 5 seconds in each loop
32 changes: 28 additions & 4 deletions brewpi-script/fermentrack_config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def __init__(self, brewpi_device_id):
super().__init__()
self.brewpi_device_id = brewpi_device_id
self.brewpi_device = None
self.uuid = None

def load_from_fermentrack(self, false_on_connection_changes=False) -> bool:
try:
Expand All @@ -35,6 +36,14 @@ def load_from_fermentrack(self, false_on_connection_changes=False) -> bool:
except RuntimeError:
return False

if self.uuid is None:
self.uuid = brewpi_device.uuid
else:
if brewpi_device.uuid != self.uuid:
# Something went really, really wrong.
raise RuntimeError(f"BrewPiDevice {self.brewpi_device_id} ({brewpi_device.id}) has UUID {brewpi_device.uuid} which doesn't match cached UUID of {self.uuid}")
return False

self.brewpi_device = brewpi_device

self.status = brewpi_device.status
Expand Down Expand Up @@ -102,14 +111,29 @@ def refresh(self) -> bool:
return self.load_from_fermentrack(True)

def save_host_ip(self, ip_to_save):
# try:
# brewpi_device = app.models.BrewPiDevice.objects.get(id=self.brewpi_device_id)
# except ObjectDoesNotExist:
# return # cannot load the object from the database (deleted?)

# If we have devices that have a conflicting wifi_host_ip to the one we are about to save, then either those
# devices or this device are incorrect. Since we presumably just looked this device up, assume those are wrong.
# Unset their wifi_host_ip as otherwise we will get confused if mDNS lookup fails and attempt to treat those
# devices as being the same as this one.
try:
brewpi_device = app.models.BrewPiDevice.objects.get(id=self.brewpi_device_id)
brewpi_devices = app.models.BrewPiDevice.objects.filter(wifi_host_ip=ip_to_save)
except ObjectDoesNotExist:
return # cannot load the object from the database (deleted?)

brewpi_device.wifi_host_ip = ip_to_save
for brewpi_device in brewpi_devices:
brewpi_device.wifi_host_ip = None
brewpi_device.save()

self.wifi_host_ip = ip_to_save
brewpi_device.save()
# brewpi_device.wifi_host_ip = ip_to_save
# brewpi_device.save()
self.brewpi_device.wifi_host_ip = ip_to_save
self.brewpi_device.save()

def save_serial_port(self, serial_port_to_save):
try:
Expand Down Expand Up @@ -169,7 +193,7 @@ def save_beer_log_point(self, beer_row):
new_log_point.save()


def get_active_brewpi_devices() -> List:
def get_active_brewpi_devices() -> List[int]:
active_devices = app.models.BrewPiDevice.objects.filter(status=app.models.BrewPiDevice.STATUS_ACTIVE
).values_list('id', flat=True)
return active_devices
3 changes: 3 additions & 0 deletions brewpi-script/scriptlibs/brewpiScriptConfig.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import socket
import datetime

from . import udev_integration


Expand Down Expand Up @@ -49,6 +51,7 @@ def __init__(self):
# Log File Path Configuration
self.stderr_path = "" # If left as an empty string, will log to stderr
self.stdout_path = "" # If left as an empty string, will log to stdout
self.last_profile_temp_check = datetime.datetime.now() - datetime.timedelta(days=1) # Initialize in the past to immediately trigger an update

def get_profile_temp(self) -> float or None:
raise NotImplementedError("Must implement in subclass!")
Expand Down
21 changes: 21 additions & 0 deletions docs/source/develop/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ 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".


[2023-07-04] - BrewPi-Script Bugfixes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Changed
-------

- Changes the message when a BrewPi device cannot be reached
- Only allow BrewPi-Script to check for profile updates once every 30 seconds
- Prevents two BrewPi devices from being able to have the same cached IP address


Fixed
-------

- Resolves an error that would cause debugging connections to controllers to periodically raise an exception
- Resolves an error where a missing schema on a generic push target would not get corrected to http://
- Allow GravityLog.device to be None in backups, fixing errors that could prevent backups from being properly generated
- BrewPi-Script processes are now launched in a manner that allows StopIteration exceptions to be properly handled



[2023-06-23] - ESP32 Support, TiltBridge Jr & new BrewPi-Script
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
9 changes: 7 additions & 2 deletions external_push/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@ def generic_push_target_push(target_id):
else:
print_for_logs("FAILED - Unable to log data to generic push target")
except MissingSchema:
print_for_logs(f"FAILED - Missing schema from logging URL '{push_target.logging_url}'. Attempting update to logging URL")
error_message = f"FAILED - Missing schema from target host '{push_target.target_host}'."
print_for_logs(error_message + " Attempting update to target host.")
if push_target.check_target_host():
print_for_logs(f"Updated schema - new logging URL '{push_target.logging_url}'")
print_for_logs(f"Updated schema - new target host '{push_target.target_host}'")
else:
print_for_logs("Unable to update schema")
# Update the target with the error message
push_target.last_error = error_message
push_target.status = push_target.STATUS_ERROR
push_target.save()

return None

Expand Down
8 changes: 6 additions & 2 deletions gravity/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ def to_dict(self) -> dict:
"""Creates a Python dict representation of this object"""
return {
'name': self.name,
'device_uuid': str(self.device.uuid),
'device_uuid': str(self.device.uuid) if self.device else None,
'created': self.created.isoformat(),
'format': self.format,
'model_version': self.model_version,
Expand All @@ -388,8 +388,12 @@ def from_dict(cls, input_dict, update=False) -> 'GravityLog' or None:
except cls.DoesNotExist:
log = cls()

try:
log.device = GravitySensor.objects.get(uuid=input_dict['device_uuid'])
except GravitySensor.DoesNotExist:
log.device = None

log.name = input_dict['name']
log.device = GravitySensor.objects.get(uuid=input_dict['device_uuid'])
log.created = datetime.datetime.fromisoformat(input_dict['created'])
log.format = input_dict['format']
log.model_version = input_dict['model_version']
Expand Down

0 comments on commit 373d557

Please sign in to comment.