Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master' into add-mysqlclient
Browse files Browse the repository at this point in the history
  • Loading branch information
jangrewe committed May 20, 2024
2 parents ae2f693 + eca3ba5 commit 7ae3b4e
Show file tree
Hide file tree
Showing 129 changed files with 4,840 additions and 916 deletions.
13 changes: 10 additions & 3 deletions backend/api/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from lib import cache
from lib import channels
from .octoprint_messages import process_octoprint_status
from .octoprint_messages import process_printer_status
from app.models import *
from lib.tunnelv2 import OctoprintTunnelV2Helper, TunnelAuthenticationError
from lib.view_helpers import touch_user_last_active
Expand Down Expand Up @@ -292,7 +292,7 @@ def receive(self, text_data=None, bytes_data=None, **kwargs):
channels.send_message_to_web(self.printer.id, data)
else:
self.printer.refresh_from_db()
process_octoprint_status(self.printer, data)
process_printer_status(self.printer, data)

@newrelic.agent.background_task()
@report_error
Expand All @@ -314,13 +314,20 @@ def close_duplicates(self, data):


class JanusWebConsumer(WebsocketConsumer):

def get_printer(self):
if 'token' in self.scope['url_route']['kwargs']:
return Printer.objects.get(
auth_token=self.scope['url_route']['kwargs']['token'],
)

# Mobileraker wants to use tunnel credential to connect janus websocket
try:
pt = OctoprintTunnelV2Helper.get_octoprinttunnel(self.scope)
if pt and str(pt.printer_id) == self.scope['url_route']['kwargs']['printer_id']:
return pt.printer
except TunnelAuthenticationError:
pass # Continue to other ways of authentication

if not self.scope['user'].is_authenticated:
raise Printer.DoesNotExist('session is not authenticated')

Expand Down
43 changes: 25 additions & 18 deletions backend/api/octoprint_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
LOGGER = logging.getLogger(__name__)
STATUS_TTL_SECONDS = 120

def process_octoprint_status(printer: Printer, msg: Dict) -> None:
def process_printer_status(printer: Printer, msg: Dict) -> None:
# Backward compatibility: octoprint_settings is for OctoPrint-Obico 2.1.2 or earlier, or moonraker-obico 0.5.1 or earlier
octoprint_settings = msg.get('settings') or msg.get('octoprint_settings')
if octoprint_settings:
cache.printer_settings_set(printer.id, settings_dict(octoprint_settings))
printer_settings = msg.get('settings') or msg.get('octoprint_settings')
if printer_settings:
cache.printer_settings_set(printer.id, settings_dict(printer_settings))

agent_name = octoprint_settings.get('agent', {}).get('name')
agent_version = octoprint_settings.get('agent', {}).get('version')
agent_name = printer_settings.get('agent', {}).get('name')
agent_version = printer_settings.get('agent', {}).get('version')
if agent_name != printer.agent_name or agent_version != printer.agent_version:
printer.agent_name = agent_name
printer.agent_version = agent_version
Expand Down Expand Up @@ -53,20 +53,27 @@ def process_octoprint_status(printer: Printer, msg: Dict) -> None:
process_heater_temps(printer, temps)


def settings_dict(octoprint_settings):
webcam_settings = dict(Printer.DEFAULT_WEBCAM_SETTINGS)

webcam_settings.update(octoprint_settings.get('webcam', {}))
settings = dict(('webcam_' + k, str(v)) for k, v in webcam_settings.items())

settings.update(dict(temp_profiles=json.dumps(octoprint_settings.get('temperature', {}).get('profiles', []))))
settings.update(dict(printer_metadata=json.dumps(octoprint_settings.get('printer_metadata', {}))))
def settings_dict(printer_settings):
# Backward compatibility: printer_settings.get('webcam') is for old agent versions, ie, OctoPrint-Obico 2.5.0 or earlier, or moonraker-obico 1.7.0 or earlier
if printer_settings.get('webcam'):
webcam_settings = Printer.DEFAULT_WEBCAM_SETTINGS.copy()
webcam_settings.update(printer_settings.get('webcam'))
webcams = [webcam_settings]
else:
webcams = printer_settings.get('webcams')

settings = dict()
if webcams is not None:
settings.update(dict(webcams=json.dumps(webcams)))
settings.update(dict(temp_profiles=json.dumps(printer_settings.get('temperature', {}).get('profiles', []))))
settings.update(dict(printer_metadata=json.dumps(printer_settings.get('printer_metadata', {}))))
settings.update(
tsd_plugin_version=octoprint_settings.get('tsd_plugin_version', ''),
octoprint_version=octoprint_settings.get('octoprint_version', ''),
tsd_plugin_version=printer_settings.get('tsd_plugin_version', ''),
octoprint_version=printer_settings.get('octoprint_version', ''),
)
settings.update(dict(platform_uname=json.dumps(octoprint_settings.get('platform_uname', []))))
settings.update(dict(installed_plugins=json.dumps(octoprint_settings.get('installed_plugins', []))))
settings.update(dict(platform_uname=json.dumps(printer_settings.get('platform_uname', []))))
settings.update(dict(installed_plugins=json.dumps(printer_settings.get('installed_plugins', []))))


return settings

Expand Down
22 changes: 16 additions & 6 deletions backend/api/octoprint_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ class OctoPrintPicView(APIView):

def post(self, request):
printer = request.auth
user = request.user

is_primary_camera = request.POST.get('is_primary_camera', 'true').lower() == 'true' # if not specified, it's from a legacy agent and hence is primary camera
is_nozzle_camera = request.POST.get('is_nozzle_camera', 'false').lower() == 'true'
camera_name = request.POST.get('camera_name', '') # If camera_name is not provided, it's from a legacy agent.

# TODO: Think about the use cases when non-primary camera sends a pic. For now, we are ignoring it.
if not is_primary_camera:
return Response({'result': 'ok'})

if settings.PIC_POST_LIMIT_PER_MINUTE and cache.pic_post_over_limit(printer.id, settings.PIC_POST_LIMIT_PER_MINUTE):
return Response(status=status.HTTP_429_TOO_MANY_REQUESTS)
Expand All @@ -92,14 +101,14 @@ def post(self, request):
if (not printer.current_print) or request.POST.get('viewing_boost'):
# Not need for failure detection if not printing, or the pic was send for viewing boost.
pic_path = f'snapshots/{printer.id}/latest_unrotated.jpg'
internal_url, external_url = save_file_obj(pic_path, pic, settings.PICS_CONTAINER, long_term_storage=False)
internal_url, external_url = save_file_obj(pic_path, pic, settings.PICS_CONTAINER, user.syndicate.name, long_term_storage=False)
cache.printer_pic_set(printer.id, {'img_url': external_url}, ex=IMG_URL_TTL_SECONDS)
send_status_to_web(printer.id)
return Response({'result': 'ok'})

pic_id = str(timezone.now().timestamp())
pic_path = f'raw/{printer.id}/{printer.current_print.id}/{pic_id}.jpg'
internal_url, external_url = save_file_obj(pic_path, pic, settings.PICS_CONTAINER, long_term_storage=False)
internal_url, external_url = save_file_obj(pic_path, pic, settings.PICS_CONTAINER, user.syndicate.name, long_term_storage=False)

img_url_updated = self.detect_if_needed(printer, pic, pic_id, internal_url)
if not img_url_updated:
Expand Down Expand Up @@ -142,14 +151,14 @@ def detect_if_needed(self, printer, pic, pic_id, raw_pic_url):
tagged_img.seek(0)

pic_path = f'tagged/{printer.id}/{printer.current_print.id}/{pic_id}.jpg'
_, external_url = save_file_obj(pic_path, tagged_img, settings.PICS_CONTAINER, long_term_storage=False)
_, external_url = save_file_obj(pic_path, tagged_img, settings.PICS_CONTAINER, printer.user.syndicate.name, long_term_storage=False)
cache.printer_pic_set(printer.id, {'img_url': external_url}, ex=IMG_URL_TTL_SECONDS)

prediction_json = serializers.serialize("json", [prediction, ])
p_out = io.BytesIO()
p_out.write(prediction_json.encode('UTF-8'))
p_out.seek(0)
save_file_obj(f'p/{printer.id}/{printer.current_print.id}/{pic_id}.json', p_out, settings.PICS_CONTAINER, long_term_storage=False)
save_file_obj(f'p/{printer.id}/{printer.current_print.id}/{pic_id}.json', p_out, settings.PICS_CONTAINER, printer.user.syndicate.name, long_term_storage=False)

if is_failing(prediction, printer.detective_sensitivity, escalating_factor=settings.ESCALATING_FACTOR):
# The prediction is high enough to match the "escalated" level and hence print needs to be paused
Expand Down Expand Up @@ -270,7 +279,7 @@ def post(self, request, format=None):
(maybe_new_one_time_passcode, verification_code) = request_one_time_passcode(one_time_passcode)
otp_response = {
'one_time_passcode': maybe_new_one_time_passcode,
'one_time_passlink': f'https://app.obico.io/otp/?one_time_passcode={maybe_new_one_time_passcode}',
'one_time_passlink': f'https://obico.onelink.me/fxEU/3ajxjqzd?deep_link_value=https://app.obico.io/printers/wizard/link/?one_time_passcode={maybe_new_one_time_passcode}',
'verification_code': verification_code}

messages = []
Expand Down Expand Up @@ -349,9 +358,10 @@ def post(self, request):
rotated_jpg_url = save_pic(
f'snapshots/{printer.id}/{str(timezone.now().timestamp())}_rotated.jpg',
pic,
request.user.syndicate.name,
rotated=True,
printer_settings=printer.settings,
to_long_term_storage=False
to_long_term_storage=False,
)

print_event = PrinterEvent.create(
Expand Down
2 changes: 1 addition & 1 deletion backend/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class UserSerializer(serializers.ModelSerializer):

class Meta:
model = User
exclude = ('password', 'last_login', 'is_superuser', 'is_staff', 'is_active', 'groups', 'user_permissions',)
exclude = ('password', 'last_login', 'is_superuser', 'is_staff', 'is_active', 'groups', 'user_permissions', 'syndicate')
extra_kwargs = {
'id': {'read_only': True},
'email': {'read_only': True},
Expand Down
60 changes: 31 additions & 29 deletions backend/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@

from app.models import Printer, Print, User
from api.octoprint_views import *
from api.octoprint_messages import process_octoprint_status
from api.octoprint_messages import process_printer_status


def init_data():
user = User.objects.create(email='[email protected]')
user.set_password('test')
user.save()
printer = Printer.objects.create(user=user)
print = Print.objects.create(
user=user, printer=printer, filename='test.gcode', started_at=timezone.now(), ext_id=1)
printer.current_print = print
printer.save()
client = Client()
client.force_login(user)
client.login(email='[email protected]', password='test')

return (user, printer, client)

# https://docs.python.org/3/library/unittest.mock.html#where-to-patch for why it is patching "api.octoprint_views.send_failure_alert" not "lib.notifications.send_failure_alert"
Expand Down Expand Up @@ -230,7 +232,7 @@ def test_error_resumed_then_warning_error_shortly_after(self, send_failure_alert
one_minute_ago = timezone.now() - timedelta(minutes=1)
with patch('django.utils.timezone.now', return_value=one_minute_ago):
pause_if_needed(self.printer, None)
process_octoprint_status(self.printer, status_msg(1, '1.gcode', 'PrintResumed'))
process_printer_status(self.printer, status_msg(1, '1.gcode', 'PrintResumed'))

alert_if_needed(self.printer, None)
send_failure_alert.assert_called_once_with(
Expand Down Expand Up @@ -279,7 +281,7 @@ def test_pause_resumed_in_octoprint_not_paused_again(self, pause_print):
one_hour_ago = timezone.now() - timedelta(hours=1)
with patch('django.utils.timezone.now', return_value=one_hour_ago):
pause_if_needed(self.printer, None)
process_octoprint_status(self.printer, status_msg(1, '1.gcode', 'PrintResumed'))
process_printer_status(self.printer, status_msg(1, '1.gcode', 'PrintResumed'))

pause_if_needed(self.printer, None)
pause_print.assert_called_once()
Expand All @@ -299,86 +301,86 @@ def setUp(self):
Print.objects.all().delete(force_policy=HARD_DELETE)

def test_neg_print_ts_is_ignored_when_no_current_print(self, celery_app):
process_octoprint_status(self.printer, status_msg(-1, '1.gcode', 'PrintStarted'))
process_printer_status(self.printer, status_msg(-1, '1.gcode', 'PrintStarted'))
self.assertIsNone(self.printer.current_print)

process_octoprint_status(self.printer, status_msg(-1, '1.gcode', 'PrintFailed'))
process_printer_status(self.printer, status_msg(-1, '1.gcode', 'PrintFailed'))
self.assertIsNone(self.printer.current_print)

process_octoprint_status(self.printer, status_msg(-1, '1.gcode', 'PrintCancelled'))
process_printer_status(self.printer, status_msg(-1, '1.gcode', 'PrintCancelled'))
self.assertIsNone(self.printer.current_print)

process_octoprint_status(self.printer, status_msg(-1, '1.gcode', 'PrintPaused'))
process_printer_status(self.printer, status_msg(-1, '1.gcode', 'PrintPaused'))
self.assertIsNone(self.printer.current_print)
celery_app.send_task.assert_not_called()

def test_print_is_done_normally(self, celery_app):
process_octoprint_status(self.printer, status_msg(1, '1.gcode', 'PrintStarted'))
process_printer_status(self.printer, status_msg(1, '1.gcode', 'PrintStarted'))
self.assertIsNotNone(self.printer.current_print)

process_octoprint_status(self.printer, status_msg(1, '1.gcode', 'PrintDone'))
process_printer_status(self.printer, status_msg(1, '1.gcode', 'PrintDone'))
self.assertIsNone(self.printer.current_print)
self.assertIsNotNone(Print.objects.first().finished_at)
celery_app.send_task.assert_has_calls(EVENT_CALLS)
self.assertEqual(celery_app.send_task.call_count, 1)

def test_print_is_canceled_normally(self, celery_app):
process_octoprint_status(self.printer, status_msg(1, '1.gcode', 'PrintStarted'))
process_printer_status(self.printer, status_msg(1, '1.gcode', 'PrintStarted'))
self.assertIsNotNone(self.printer.current_print)

process_octoprint_status(self.printer, status_msg(1, '1.gcode', 'PrintCancelled'))
process_octoprint_status(self.printer, status_msg(1, '1.gcode', 'PrintFailed'))
process_printer_status(self.printer, status_msg(1, '1.gcode', 'PrintCancelled'))
process_printer_status(self.printer, status_msg(1, '1.gcode', 'PrintFailed'))
self.assertIsNone(self.printer.current_print)
self.assertIsNone(Print.objects.first().finished_at)
self.assertIsNotNone(Print.objects.first().cancelled_at)
celery_app.send_task.assert_has_calls(EVENT_CALLS)
self.assertEqual(celery_app.send_task.call_count, 1)

def test_lost_end_event(self, celery_app):
process_octoprint_status(self.printer, status_msg(1, '1.gcode', 'PrintStarted'))
process_printer_status(self.printer, status_msg(1, '1.gcode', 'PrintStarted'))
self.assertIsNotNone(self.printer.current_print)

process_octoprint_status(self.printer, status_msg_without_event(-1, '1.gcode'))
process_octoprint_status(self.printer, status_msg(100, '1.gcode', 'PrintPaused'))
process_printer_status(self.printer, status_msg_without_event(-1, '1.gcode'))
process_printer_status(self.printer, status_msg(100, '1.gcode', 'PrintPaused'))
self.assertIsNotNone(self.printer.current_print)
self.assertEqual(self.printer.current_print.ext_id, 100)
self.assertIsNotNone(self.printer.current_print.started_at)
self.assertEqual(Print.objects.all_with_deleted().count(), 2)
celery_app.send_task.assert_has_calls(EVENT_CALLS)
self.assertEqual(celery_app.send_task.call_count, 1)

process_octoprint_status(self.printer, status_msg(100, '1.gcode', 'PrintDone'))
process_printer_status(self.printer, status_msg(100, '1.gcode', 'PrintDone'))
self.assertEqual(celery_app.send_task.call_count, 2)

def test_plugin_send_neg_print_ts_while_printing(self, celery_app):
process_octoprint_status(self.printer, status_msg(1, '1.gcode', 'PrintStarted'))
process_octoprint_status(self.printer, status_msg(-1, '1.gcode', 'PrintPaused'))
process_printer_status(self.printer, status_msg(1, '1.gcode', 'PrintStarted'))
process_printer_status(self.printer, status_msg(-1, '1.gcode', 'PrintPaused'))
self.assertIsNotNone(self.printer.current_print)
process_octoprint_status(self.printer, status_msg_without_event(1, '1.gcode'))
process_printer_status(self.printer, status_msg_without_event(1, '1.gcode'))
self.assertIsNotNone(self.printer.current_print)
self.assertEqual(Print.objects.all_with_deleted().count(), 1)
self.assertEqual(celery_app.send_task.call_count, 0)

def test_race_condition_at_end_of_print(self, celery_app):
eleven_hour_ago = timezone.now() - timedelta(hours=11)
with patch('django.utils.timezone.now', return_value=eleven_hour_ago):
process_octoprint_status(self.printer, status_msg(1, '1.gcode', 'PrintStarted'))
process_printer_status(self.printer, status_msg(1, '1.gcode', 'PrintStarted'))

process_octoprint_status(self.printer, status_msg_without_event(-1, '1.gcode'))
process_octoprint_status(self.printer, status_msg(1, '1.gcode', 'PrintFailed'))
process_octoprint_status(self.printer, status_msg(1, '1.gcode', 'PrintCancelled'))
process_printer_status(self.printer, status_msg_without_event(-1, '1.gcode'))
process_printer_status(self.printer, status_msg(1, '1.gcode', 'PrintFailed'))
process_printer_status(self.printer, status_msg(1, '1.gcode', 'PrintCancelled'))
self.assertIsNone(self.printer.current_print)
celery_app.send_task.assert_has_calls(EVENT_CALLS)
self.assertEqual(celery_app.send_task.call_count, 1)

def test_plugin_send_diff_print_ts_while_printing(self, celery_app):
process_octoprint_status(self.printer, status_msg(1, '1.gcode', 'PrintStarted'))
process_octoprint_status(self.printer, status_msg(50, '1.gcode', 'PrintPaused'))
process_octoprint_status(self.printer, status_msg_without_event(1, '1.gcode'))
process_printer_status(self.printer, status_msg(1, '1.gcode', 'PrintStarted'))
process_printer_status(self.printer, status_msg(50, '1.gcode', 'PrintPaused'))
process_printer_status(self.printer, status_msg_without_event(1, '1.gcode'))
self.assertIsNotNone(self.printer.current_print)
self.assertEqual(Print.objects.all_with_deleted().count(), 1)
self.assertEqual(celery_app.send_task.call_count, 0)

process_octoprint_status(self.printer, status_msg_without_event(100, '1.gcode'))
process_printer_status(self.printer, status_msg_without_event(100, '1.gcode'))
celery_app.send_task.assert_has_calls(EVENT_CALLS)
self.assertEqual(celery_app.send_task.call_count, 1)
self.assertEqual(celery_app.send_task.call_count, 1)
Loading

0 comments on commit 7ae3b4e

Please sign in to comment.