#! /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)