smallprint/smallprint.py
José Carlos Cuevas 7950c435bb Used proper OOP for the main module, as it grows
Fixed a bug with non titled RSS items like the ones found in
Mastodon network.
2022-12-03 15:14:54 +01:00

518 lines
15 KiB
Python

#! /usr/bin/env python
# python imports
import argparse
import logging
import math
import datetime
import os
import os.path
import sys
# 3rd party imports
from escpos.printer import Usb, File
import feedparser
import ignition
from PIL import Image
import re
import requests
from config import load_config, CONFIG_PATH
from webdav import Client
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logger = logging.getLogger(__name__)
config = {}
class SmallPrint:
def __init__(self, gen_qr=False):
"""
Initializes the printer and returns a printer object to
generate the print
"""
self.gen_qr = gen_qr
self.config = load_config()
if not self.config:
print(f"Config file created, check {CONFIG_PATH}")
if os.path.exists(self.config.get("PRINTER_FILE")):
self.printer = File(self.config.get("PRINTER_FILE"))
else:
printer_id = self.config.get("PRINTER_USB_ID")
if not printer_id:
logger.error("Please configure your printer")
prid1, prid2 = printer_id.split(":")
printer_interface = config.get("PRINTER_INTERFACE") or 0
printer_endpoint = config.get("PRINTER_ENDPOINT") or 0x01
printer = Usb(prid1, prid2, printer_interface, printer_endpoint)
printer.charcode(code="WEST_EUROPE")
self.printer = printer
def reset_defaults(self):
"""
Reset the printer to the defaults
"""
self.printer.set(align='left', font='a', width=1, text_type="normal",
height=1, density=9, invert=False, smooth=False, flip=False)
def print_weather(self, city=None):
"""
Gets a weather report and sends it to the printer
with a nice icon and data, on the city specified
or the default in the config
"""
# Get our printer
printer = self.printer
# Get it on the right shape for printing
self.reset_defaults()
# Get OpenWeatherMap key
appkey = self.config.get("OWM")
if not appkey:
logger.error("Open Weather key not set!")
return
if not city:
city = self.config.get("CITY")
if not city:
logger.error("No city set")
return
params = {"q": city, "APPID": appkey, "units": "metric"}
weatherdata = requests.get('http://api.openweathermap.org/data/2.5/weather', params=params)
if "weather" in weatherdata.json():
weather = weatherdata.json()
today = datetime.datetime.now()
current_day = today.strftime("%a, %d %b, %Y")
printer.set(align="center",
font="a",
text_type="b")
printer.text(f"{current_day}\n\n{city}\n")
self.reset_defaults()
printer.set(align="center", font="b")
description = weather['weather'][0]['description']
printer.text(f"{description}\n\n")
icon_code = weather['weather'][0]['icon']
dir_path = os.path.dirname(os.path.realpath(__file__)) # Get this file's current directory
if not os.path.exists(os.path.join(dir_path, "icons/weather")):
dir_path = "/usr/share/smallprint/icons/weather"
else:
dir_path = os.path.join(dir_path, "icons/weather")
if not os.path.exists(os.path.join(dir_path, f"{icon_code}.png")):
icon_path = os.path.join(dir_path, "any.png")
else:
icon_path = os.path.join(dir_path, f"{icon_code}.png")
# TODO: The image impl should be an option
printer.image(icon_path,
impl="bitImageColumn")
printer.text("\n")
# TODO: Print a nice icon based on the codes here: https://openweathermap.org/weather-conditions
temperature = weather['main']['temp']
humidity = weather['main']['humidity']
wind = weather['wind']['speed']
printer.text(f"Temperature: {temperature}C\n")
printer.text(f"Humidity: {humidity}%\n")
printer.text(f"Wind: {wind}km\\h\n")
else:
logger.error("No weather info available")
def print_gemini(self, link):
"""
Given a Gemini link, it prints the file to the printer
and tries to do some interpretation if it is gemtext
"""
printer = self.printer
response = ignition.request(link)
if not response.success():
logger.error(f"Received error {response.status}")
return
self.reset_defaults()
if response.meta == "text/gemini":
for raw_line in str(response.data()).split("\n"):
line = raw_line.rstrip()
if len(line) == 0:
printer.text("\n")
continue
if line.startswith("# "):
printer.set(align="left",
text_type="BU",
width=2, height=2,
smooth=True)
printer.text(line[2:])
elif line.startswith("## "):
printer.set(align="left",
text_type="BU",
width=1, height=1,
smooth=True)
printer.text(line[3:])
elif line.startswith("### "):
printer.set(align="left",
font="a",
text_type="U",
width=1, height=1,
smooth=True)
printer.text(line[4:])
elif line.startswith("#### "):
printer.set(align="left",
font="b",
text_type="U",
width=1, height=1,
smooth=True)
printer.text(line[5:])
elif line.startswith("=>"):
self.reset_defaults()
if generate_qr:
printer.set(align="left",
font="b")
printer.text(line[3:])
self.reset_defaults()
lnk = self._process_link(link, line[3:])
printer.text("\n")
printer.qr(lnk, size=6)
else:
printer.set(align="left",
font="b",
text_type="U")
printer.text(line)
printer.text("\n")
else:
printer.set(align="left",
font="b")
printer.text(line)
printer.text("\n")
else:
printer.text(response.data())
def _process_link(self, orig_link, link_text):
"""
Extracts the link from the text, and completes it if
necessary
"""
uri = link_text.split()[0]
if "://" in link_text:
return uri
if link_text.startswith("/"):
return orig_link + link_text
else:
return orig_link + "/" + link_text
def print_rss(self, link, news=3):
"""
Given an RSS link and a printer, prints the news from it
"""
printer = self.printer
self.reset_defaults()
feed = feedparser.parse(link)
title = feed["channel"].get("title", link)[:16]
printer.set(align="center",
text_type="BU",
width=2, height=2,
smooth=True)
printer.text(f"{title}\n\n")
self.reset_defaults()
items = feed["items"][:news]
for item in items:
self._print_rss_item(item)
def _print_rss_item(self, item):
"""
Process an item from an RSS feed and print it out
"""
printer = self.printer
title = item.get("title", "")
date = item["published"]
text = clear_html(item["summary"])
if len(text) > 255:
# Limit the text output in certain "summaries"
text = text[:252] + "...\n"
link = item["link"]
printer.set(align="center",
text_type="BU",
smooth=True)
printer.text(f"{title}\n")
self.reset_defaults()
printer.set(align="left",
font="b")
printer.text(f"{date}\n\n")
printer.text(text)
printer.text("\n")
if self.gen_qr:
printer.qr(link, size=6)
printer.text("\n")
def print_text(self, text):
"""
Prints a text in the smallest form possible
"""
printer = self.printer
self.reset_defaults()
# Set the font to a small one and align to
# the left
printer.set(align="left",
text_type="NORMAL",
font="b",
smooth=True)
printer.text("\n")
if text.strip() == "-":
# Load stdin data
for line in sys.stdin:
printer.text(line)
printer.text(text)
def print_empty_lines(self, lines=2):
"""
Generates several \n depending on lines to create
empty spaces in the ticket printed
"""
linebreaks = "\n" * lines
self.printer.text(linebreaks)
def print_file(self, file):
"""
Prints a file
"""
printer = self.printer
self.reset_defaults()
# Set the font to a small one and align to
# the left
printer.set(align="left",
text_type="NORMAL",
font="b",
smooth=True)
printer.text("\n")
for line in file:
printer.text(line)
file.close()
def print_image(self, image):
"""
Prints an image
"""
printer = self.printer
# Load the image to adjust it
im = Image.open(image)
ratio = float(im.size[0]) / float(im.size[1])
if im.size[0] > im.size[1]:
# The image needs to be rotated
width = math.floor(384 * ratio)
im = im.resize((width, 384))
im = im.transpose(Image.ROTATE_90)
else:
height = math.floor(384.0 / ratio)
im = im.resize((384, height))
im.save("temp.png")
printer.hw("INIT")
printer.image("temp.png")
os.remove("temp.png")
def print_calendar(self):
"""
Connects to a webdav calendar and retrieves
the events for today
"""
printer = self.printer
c = Client(self.config)
events = c.get_todays_events()
printer.set(align="center",
text_type="BU",
smooth=True)
printer.text("Today:\n")
printer.set(align="left",
text_type="NORMAL",
font="b",
smooth=True)
if len(events) > 0:
for event in events:
hour = event[1].hour
minutes = event[1].minute
if minutes < 10:
printer.text(f"{hour}:0{minutes}\n")
else:
printer.text(f"{hour}:{minutes}\n")
printer.text(f"{event[0]}\n--\n")
else:
printer.set(align="center",
text_type="NORMAL",
font="b",
smooth=True)
printer.text("No events today\n")
def printing_script(self):
"""
Gets the configuration and parses its script
"""
printer = self.printer
script = self.config.get("SCRIPT")
if not script:
logger.error("No script in config file")
return
for command in script:
key = list(command)[0]
value = command[key]
if key == 'WEATHER':
self.print_weather(city=value)
if key == 'RSS':
self.print_rss(value)
if key == 'GEMINI':
self.print_gemini(value)
if key == 'FILE':
self.print_file(value)
if key == 'IMAGE':
self.print_image(value)
if key == 'TEXT':
self.print_text(value)
printer.text('\n')
if key == 'CALENDAR':
self.print_calendar()
printer.set(align="center",
text_type="NORMAL",
font="b",
smooth=True)
printer.text("-----\n")
def clear_html(text):
cleanr = re.compile('<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});')
cleantext = re.sub(cleanr, '', text)
return cleantext
def create_parser():
"""
Create an argpaser for command line interaction
"""
parser = argparse.ArgumentParser()
parser.add_argument("--weather", default=False, help="Prints the weather in the given city")
parser.add_argument("--news", default=False, help="Loads a RSS or ATOM feed and prints it out")
parser.add_argument("--gemini", default=False, help="Loads a Gemini url and prints it out")
parser.add_argument("--text", nargs=1, default=None, help="Print the given text, use '-' to read from stdin")
parser.add_argument("--file", default=None, type=open, help="Loads a file and sends it to the printer (as text)")
parser.add_argument("--image", nargs=1, default=None, help="Print an image")
parser.add_argument("--calendar", action="store_true", help="Print configured calendar events for today")
parser.add_argument("--genqr", action="store_true", help="Activates the generation of QR codes for links in news and gemini")
return parser
if __name__ == "__main__":
parser = create_parser()
args = parser.parse_args()
generate_qr = args.genqr
printer = SmallPrint(generate_qr)
ops_called = 0
if args.text:
ops_called += 1
printer.print_text(args.text[0])
printer.print_empty_lines(2)
if args.file:
ops_called += 1
printer.print_file(args.file)
printer.print_empty_lines(2)
if args.weather:
ops_called += 1
printer.print_weather(args.weather)
printer.print_empty_lines(2)
if args.gemini:
ops_called += 1
printer.print_gemini(args.gemini)
printer.print_empty_lines(2)
if args.news:
ops_called += 1
printer.print_rss(args.news, gen_qr=generate_qr)
if args.image:
ops_called += 1
printer.print_image(args.image[0])
printer.print_empty_lines(2)
if args.calendar:
ops_called += 1
printer.print_calendar(printer)
printer.print_empty_lines(2)
# In case we get called (almost) empty
if ops_called == 0:
# Try to parse a config script
printer.printing_script()
printer.print_empty_lines(2)