Skip to content

Commit

Permalink
better snapshot and fix markdown in notification plain text (#13)
Browse files Browse the repository at this point in the history
* fix notification text (send plain text with out encodes)

* take snapshots via URL and handle flip and rotate, see #12, #7

* debug messagesa and bump version

* add MultiCam support

* bump version to 0.2.4

* Apply suggestions from code review

Co-authored-by: Stuart Mumford <stuart@cadair.com>

* use None not empty string

* Some small cleanup and don't use requests

---------

Co-authored-by: Markus <Links2004@users.noreply.github.com>
Co-authored-by: Stuart Mumford <stuart@cadair.com>
  • Loading branch information
3 people committed Mar 6, 2024
1 parent bf56135 commit 2141cc8
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 33 deletions.
6 changes: 4 additions & 2 deletions octoprint_matrix_notifier/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
import json
import logging
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from urllib.request import Request, urlopen
from uuid import uuid4
Expand Down Expand Up @@ -78,10 +79,11 @@ def upload_media(self, media_data, content_type):
)

def room_send_markdown_message(self, room_id, text):
html = markdown.markdown(text, extensions=['nl2br'])
content = {
"msgtype": "m.text",
"body": text,
"body": BeautifulSoup(html, 'html.parser').get_text(),
"format": "org.matrix.custom.html",
"formatted_body": markdown.markdown(text, extensions=['nl2br'])
"formatted_body": html
}
self.room_send(room_id, "m.room.message", content)
131 changes: 101 additions & 30 deletions octoprint_matrix_notifier/plugin.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import datetime
import io
import threading
import time
import urllib.request
from pathlib import Path
from textwrap import dedent

import octoprint.plugin
import octoprint.util
from get_image_size import get_image_size_from_bytesio
from octoprint.timelapse import Timelapse
from PIL import Image

from .matrix import SimpleMatrixClient


def threaded(fn):
def wrapper(*args, **kwargs):
t = threading.Thread(target=fn, args=args, kwargs=kwargs)
t.daemon = True
t.start()
return wrapper

class MatrixNotifierPlugin(octoprint.plugin.EventHandlerPlugin,
octoprint.plugin.ProgressPlugin,
octoprint.plugin.SettingsPlugin,
Expand All @@ -31,6 +40,12 @@ def get_settings_defaults(self):
"room": "#myprinter:matrix.org",
"send_snapshot": True,
"events": {
"Startup": {
"template": dedent("""\
## Printer Started ⭐
"""),
"enabled": True,
},
"PrintStarted": {
"template": dedent("""\
## Print Started 🚀
Expand Down Expand Up @@ -231,30 +246,6 @@ def on_print_progress(self, storage, path, progress):
if self._settings.get(["send_snapshot"]):
self.send_snapshot()

def capture_snapshot(self):
if not self._settings.global_get(["webcam", "snapshot"]):
self._logger.info(
"Please configure the webcam snapshot settings "
"before enabling sending snapshots!"
)

tl = Timelapse()
tl._image_number = 0
tl._capture_errors = 0
tl._capture_success = 0
tl._in_timelapse = True
tl._file_prefix = time.strftime("%Y%m%d%H%M%S")
file_path = Path(tl.capture_image())

# Ensure the file has actually finished being written before we return
# There appears to be a threading race condition or something going on here
for i in range(10):
if file_path.exists():
break
time.sleep(0.1)

return file_path

@property
def room_id(self):
"""
Expand Down Expand Up @@ -282,24 +273,104 @@ def room_id(self):

raise ValueError("The room configuration option must start with ! or #")

def get_snapshot_config(self):
# get MultiCam urls
multi_cam_urls = self._settings.global_get(["plugins", "multicam","multicam_profiles"])
if multi_cam_urls != None:
self._logger.debug("found multicam config %s", multi_cam_urls)
return multi_cam_urls

config = []
snapshot_url = self._settings.global_get(["webcam", "snapshot"])
if snapshot_url != None:
config.append({
'name': 'webcam',
'snapshot': snapshot_url,
'flipH': self._settings.global_get(["webcam", "flipH"]),
'flipV': self._settings.global_get(["webcam", "flipV"]),
'rotate90': self._settings.global_get(["webcam", "rotate90"])
})

self._logger.debug("cam config %s", config)
return config

def send_snapshot(self):
"""
Capture and then send a snapshot from the camera.
"""
file_path = self.capture_snapshot()
# take snapshots in parallel
for cam in self.get_snapshot_config():
self.send_snapshot_t(cam)

with open(file_path, "rb") as fobj:
data = fobj.read()
@threaded
def send_snapshot_t(self, cam):
self._logger.debug("send_snapshot_t %s", cam)
data = self.take_image(cam['snapshot'], cam['flipH'], cam['flipV'], cam['rotate90'])

mxc_url = self.client.upload_media(data, "image/jpg")["content_uri"]

img_w, img_h = get_image_size_from_bytesio(io.BytesIO(data), len(data))

content = {
"msgtype": "m.image",
"body": file_path.name,
"body": cam['name'] + "_" + time.strftime("%Y_%m_%d-%H_%M_%S") + ".jpg",
"info": {"mimetype": "image/jpg", "w": img_w, "h": img_h},
"url": mxc_url,
}

return self.client.room_send(self.room_id, "m.room.message", content)
self.client.room_send(self.room_id, "m.room.message", content)

@property
def http_proxy(self):
http_proxy = self._settings.get(["http_proxy"])
https_proxy = self._settings.get(["https_proxy"])
return {"http": http_proxy, "https": https_proxy}

def take_image(self, snapshot_url=None, flipH=False, flipV=False, rotate=False):
if snapshot_url == None:
self._logger.info(
"Please configure the webcam snapshot settings "
"before enabling sending snapshots!"
)
return None

self._logger.debug("Snapshot URL: %s", snapshot_url)
data = None
if snapshot_url:
try:
# Create a proxy handler and an opener with the proxy
proxy_handler = urllib.request.ProxyHandler(http_proxy)
opener = urllib.request.build_opener(proxy_handler)

# Make the request with a timeout of 10 seconds
with opener.open(snapshot_url, timeout=10) as response:
data = response.read()

except Exception as e:
self._logger.exception("Exception while retrieving snapshot URL: %s", e)
return None

self._logger.debug(
"Image transformations [H:%s, V:%s, R:%s]", flipH, flipV, rotate
)

if data == None:
return None

if flipH or flipV or rotate:
image = Image.open(io.BytesIO(data))
if flipH:
image = image.transpose(Image.FLIP_LEFT_RIGHT)
if flipV:
image = image.transpose(Image.FLIP_TOP_BOTTOM)
if rotate:
if not self._settings.get_boolean(["invertImgRot"]):
image = image.transpose(Image.ROTATE_270)
else:
image = image.transpose(Image.ROTATE_90)
output = io.BytesIO()
image.save(output, format="JPEG")
data = output.getvalue()
output.close()

return data
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
# Any additional requirements besides OctoPrint should be listed here
plugin_requires = [
"matrix-nio",
"opsdroid-get-image-size"
"opsdroid-get-image-size",
"beautifulsoup4",
"pillow",
]

### --------------------------------------------------------------------------------------------------------------------
Expand Down

0 comments on commit 2141cc8

Please sign in to comment.