Files
morningedition/morningedition.py

553 lines
22 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Morning Edition - Daily receipt printer for Bridgeland, Cypress TX
Prints to Epson TM-T88V via CUPS printer 'receipt'
Designed for 80mm paper (42 chars wide)
Usage:
morningedition.py -- print to receipt printer
morningedition.py --preview -- dump to stdout for testing
"""
import subprocess
import datetime
import json
import urllib.request
import urllib.error
import xml.etree.ElementTree as ET
import sys
import textwrap
# ── Config ─────────────────────────────────────────────────────────────────
PRINTER = "receipt"
WIDTH = 30 # printer NV is locked to 58mm mode (30 chars); change to 42 after fixing printer to 80mm
ZIPCODE = "77433"
CITY_LINE = "Bridgeland, Cypress TX 77433"
NAME = "Rich"
# ── ESC/POS commands ────────────────────────────────────────────────────────
ESC = b'\x1b'
GS = b'\x1d'
INIT = ESC + b'\x40' # Initialize printer
FONT_A = ESC + b'\x4d\x00' # Font A 12x24 — 42 chars/line at 80mm
BOLD_ON = ESC + b'\x45\x01'
BOLD_OFF = ESC + b'\x45\x00'
ALIGN_L = ESC + b'\x61\x00'
ALIGN_C = ESC + b'\x61\x01'
CUT = GS + b'\x56\x00' # Full cut
# ── Weather ─────────────────────────────────────────────────────────────────
def get_weather():
try:
url = f"https://wttr.in/{ZIPCODE}?format=j1"
req = urllib.request.Request(url, headers={"User-Agent": "morningedition/1.0"})
resp = urllib.request.urlopen(req, timeout=15)
data = json.loads(resp.read().decode())
cc = data["current_condition"][0]
day = data["weather"][0]
ast = day.get("astronomy", [{}])[0]
return {
"temp_f" : cc["temp_F"],
"feels_f" : cc["FeelsLikeF"],
"desc" : cc["weatherDesc"][0]["value"],
"humidity" : cc["humidity"],
"wind_mph" : cc["windspeedMiles"],
"wind_dir" : cc["winddir16Point"],
"visibility" : cc["visibility"],
"uv" : cc["uvIndex"],
"hi_f" : day["maxtempF"],
"lo_f" : day["mintempF"],
"sunrise" : ast.get("sunrise", "?"),
"sunset" : ast.get("sunset", "?"),
}
except Exception as e:
print(f"[weather error] {e}", file=sys.stderr)
return None
def weather_art(desc):
d = desc.lower()
if any(w in d for w in ["sunny", "clear"]):
return [
r" \ | / ",
r" .---. ",
r" --- ( * * * ) --- ",
r" `---' ",
r" / | \ ",
]
elif "partly" in d:
return [
r" \ .---. ",
r" --- ( sun ) ",
r" `---' )-. ",
r" .-~~~{ cloudy } ",
r" `-.__________.-' ",
]
elif any(w in d for w in ["overcast", "cloudy"]):
return [
r" .-~~~~-. ",
r" .-{ }-. ",
r"{ overcast } ",
r" `-.________.-' ",
r" ",
]
elif any(w in d for w in ["thunder", "storm"]):
return [
r" .-~~~~-. ",
r" .-{ STORM }-. ",
r" `-.________.-' ",
r" /\ ",
r" / \ ",
]
elif any(w in d for w in ["rain", "shower", "drizzle"]):
return [
r" .-~~~~-. ",
r" .-{ rain }-. ",
r" `-.________.-' ",
r" ' ' ' ' ' ' ' ",
r" ' ' ' ' ' ' ' ",
]
elif any(w in d for w in ["fog", "mist", "haze"]):
return [
r" ~ ~ ~ ~ ~ ~ ~ ~ ",
r" ~ foggy / hazy ~ ",
r" ~ ~ ~ ~ ~ ~ ~ ~ ",
r" ~ ~ ~ ~ ~ ~ ~ ~ ",
r" ~ ~ ~ ~ ~ ~ ~ ~ ",
]
else:
return [
r" \ | / ",
r" .---. ",
r" --- ( ) --- ",
r" `---' ",
r" / | \ ",
]
def uv_label(uv):
uv = int(uv)
if uv <= 2: return f"{uv} - Low"
if uv <= 5: return f"{uv} - Moderate"
if uv <= 7: return f"{uv} - High"
if uv <= 10: return f"{uv} - Very High"
return f"{uv} - Extreme!"
def get_weather_tip(weather):
if not weather:
return "Have a safe and productive day!"
desc = weather["desc"].lower()
temp = int(weather["temp_f"])
if any(w in desc for w in ["thunder", "storm"]):
return "Stay safe! Monitor weather alerts today."
if any(w in desc for w in ["rain", "shower", "drizzle"]):
return "Grab an umbrella -- Houston rain waits for no one!"
if any(w in desc for w in ["fog", "mist"]):
return "Allow extra drive time -- visibility may be low."
if temp >= 95:
return "Stay hydrated! Carry water everywhere today."
if temp <= 45:
return "Layer up -- don't let the cold catch you off guard."
if int(weather.get("uv", 0)) >= 7:
return "High UV today -- sunscreen before you head out!"
if int(weather.get("humidity", 0)) >= 85:
return "Steamy one today -- moisture is high out there."
return "Make it a great day -- you've got this!"
# ── News ─────────────────────────────────────────────────────────────────────
def fetch_rss_headlines(url, count=5):
try:
req = urllib.request.Request(url, headers={"User-Agent": "morningedition/1.0"})
resp = urllib.request.urlopen(req, timeout=15)
tree = ET.fromstring(resp.read())
items = tree.findall(".//item")[:count]
headlines = []
for item in items:
t = item.find("title")
if t is not None and t.text:
# Strip CDATA, collapse whitespace, ASCII-ify
text = t.text.strip()
text = text.encode("ascii", errors="replace").decode("ascii")
headlines.append(text)
return headlines
except Exception as e:
print(f"[rss error {url}] {e}", file=sys.stderr)
return []
# ── Rotating daily content ───────────────────────────────────────────────────
QUOTES = [
("The best time to plant a tree was 20 years ago. The second best time is now.",
"Chinese Proverb"),
("Every morning we are born again. What we do today matters most.",
"Buddha"),
("With the new day comes new strength and new thoughts.",
"Eleanor Roosevelt"),
("You have brains in your head. You have feet in your shoes. You can steer yourself any direction you choose.",
"Dr. Seuss"),
("Make each day your masterpiece.",
"John Wooden"),
("Do something today that your future self will thank you for.",
"Sean Patrick Flanery"),
("Start where you are. Use what you have. Do what you can.",
"Arthur Ashe"),
("Success is the sum of small efforts repeated day in and day out.",
"Robert Collier"),
("Believe you can and you're halfway there.",
"Theodore Roosevelt"),
("The secret of getting ahead is getting started.",
"Mark Twain"),
("What lies behind us and before us are tiny matters compared to what lies within us.",
"Ralph Waldo Emerson"),
("The only way to do great work is to love what you do.",
"Steve Jobs"),
("Life is 10% what happens to you and 90% how you react to it.",
"Charles R. Swindoll"),
("In the middle of every difficulty lies opportunity.",
"Albert Einstein"),
("It always seems impossible until it's done.",
"Nelson Mandela"),
("Don't watch the clock; do what it does. Keep going.",
"Sam Levenson"),
("Opportunities don't happen, you create them.",
"Chris Grosser"),
("Wake up determined. Go to bed satisfied.",
"Unknown"),
("Your imagination is your preview of life's coming attractions.",
"Albert Einstein"),
("The future belongs to those who believe in the beauty of their dreams.",
"Eleanor Roosevelt"),
("It does not matter how slowly you go as long as you do not stop.",
"Confucius"),
("Hardships often prepare ordinary people for an extraordinary destiny.",
"C.S. Lewis"),
]
JOKES = [
("Why don't scientists trust atoms?",
"Because they make up everything!"),
("Why did the scarecrow win an award?",
"He was outstanding in his field!"),
("Why can't you give Elsa a balloon?",
"Because she'll let it go!"),
("What do you call fake spaghetti?",
"An impasta!"),
("Why did the bicycle fall over?",
"Because it was two-tired!"),
("What's a skeleton's least favorite room?",
"The living room!"),
("Why do cows wear bells?",
"Because their horns don't work!"),
("Why don't eggs tell jokes?",
"They'd crack each other up!"),
("What do you call a fish without eyes?",
"A fsh!"),
("Why did the math book look so sad?",
"Because it had too many problems!"),
("What do you get when you cross a snowman and a vampire?",
"Frostbite!"),
("Why couldn't the leopard play hide and seek?",
"Because he was always spotted!"),
("What do you call cheese that isn't yours?",
"Nacho cheese!"),
("Why did the golfer bring extra socks?",
"In case he got a hole in one!"),
("What has ears but cannot hear?",
"A cornfield!"),
("Why did the coffee file a police report?",
"It got mugged!"),
("What do you call a pile of cats?",
"A meowtain!"),
("How do trees access the internet?",
"They log in!"),
("Why was the belt arrested?",
"It was holding up a pair of pants!"),
("What do you call a sleeping dinosaur?",
"A dino-snore!"),
("Why did the stadium get hot after the game?",
"All the fans left!"),
("What do you call a bear with no teeth?",
"A gummy bear!"),
("Why did the tomato turn red?",
"Because it saw the salad dressing!"),
]
FACTS = [
"A group of flamingos is called a 'flamboyance.'",
"Honey never spoils. Edible honey was found in 3,000-year-old Egyptian tombs.",
"Octopuses have THREE hearts and blue blood.",
"Crows can recognize and remember human faces.",
"Bananas are technically berries. Strawberries are not.",
"A day on Venus is longer than a year on Venus.",
"Wombats are the only animals that produce cube-shaped droppings.",
"Sharks existed before trees. Sharks: 450M yrs. Trees: 350M yrs.",
"Butterflies taste with their feet.",
"The shortest war in history lasted 38-45 minutes (Anglo-Zanzibar War, 1896).",
"Oxford University is older than the Aztec Empire.",
"A snail can sleep for 3 years at a stretch.",
"Cleopatra lived closer in time to the Moon landing than to the pyramids.",
"Nintendo was founded in 1889 -- as a playing card company.",
"The scent of rain on dry earth is called 'petrichor.'",
"Humans share about 60% of their DNA with bananas.",
"Sloths can hold their breath longer than dolphins -- up to 40 minutes.",
"A bolt of lightning is 5x hotter than the surface of the Sun.",
"The human nose can detect over 1 TRILLION different scents.",
"Goats have rectangular pupils to give them near-360-degree vision.",
"A group of owls is called a 'parliament.'",
"There are more possible chess games than atoms in the observable universe.",
"Dolphins sleep with one eye open.",
"Polar bear fur is actually transparent, not white.",
"Sea otters hold hands while sleeping so they don't drift apart.",
"An ostrich's eye is bigger than its brain.",
"Male seahorses carry and give birth to the young.",
"Platypuses don't have stomachs -- food goes straight to the intestine.",
"A group of crows is called a 'murder.'",
"The fingerprints of a koala are virtually identical to human fingerprints.",
"You can't hum while holding your nose closed.",
"Cats have fewer toes on their back paws than their front paws.",
]
HOUSTON_TIPS = [
"IAH tip: Terminal C is usually least crowded for TSA.",
"Beltway 8 (Sam Houston Tollway) runs 24/7 -- great I-10 bypass.",
"Bridgeland: Lakeland Activity Center open 5am-10pm weekdays.",
"H-E-B on Fry Rd is restocked overnight -- mornings = freshest produce.",
"Houston weather reminder: if you don't like it, wait 20 minutes.",
"Buc-ee's on I-10 in Katy -- best beaver nuggets in Texas. Just saying.",
"Bluebonnet season peaks late March -- drive FM 1093 for the show.",
"Houston Medical Center is the largest in the world. Right in our backyard.",
"Buffalo Bayou Park has great sunrise walks if you're up early.",
"Hike-and-bike trails around Bridgeland Lake -- 60+ miles of paths.",
"Discovery Green downtown hosts free outdoor events most weekends.",
"NASA Johnson Space Center is 45 min southeast -- worth a visit!",
"Hermann Park's McGovern Lake paddle boats open at 10am weekends.",
]
ON_THIS_DAY = {
# month-day: (year, event)
"04-03": (1973, "First handheld cell phone call made by Martin Cooper."),
"07-04": (1776, "United States Declaration of Independence adopted."),
"07-20": (1969, "Apollo 11 lands on the Moon."),
"12-17": (1903, "Wright Brothers make first successful airplane flight."),
"01-01": (2000, "Y2K passes without incident -- the world breathes a sigh of relief."),
"02-14": (1876, "Alexander Graham Bell applies for the telephone patent."),
"03-14": (1879, "Albert Einstein is born in Ulm, Germany."),
"10-31": (1517, "Martin Luther posts 95 Theses, sparking the Reformation."),
"11-22": (1963, "JFK assassinated in Dallas, TX."),
"05-05": (1961, "Alan Shepard becomes first American in space."),
"06-06": (1944, "D-Day: Allied forces land at Normandy, France."),
"08-06": (1945, "Atomic bomb dropped on Hiroshima, Japan."),
"09-11": (2001, "Terrorist attacks on the World Trade Center, Pentagon."),
"11-09": (1989, "Berlin Wall falls."),
"12-25": (0000, "Merry Christmas! Go spend time with family!"),
}
# ── Formatting helpers ───────────────────────────────────────────────────────
def ctr(text, w=WIDTH):
return text.center(w)[:w]
def trunc(text, w=WIDTH):
"""Truncate to fit on one line, no wrapping."""
text = text.replace("\n", " ")
if len(text) > w:
return text[:w-3] + "..."
return text
def wrap(text, w=WIDTH):
return textwrap.wrap(text.replace("\n", " "), w)
def section(title):
return [
"-" * WIDTH,
ctr(f"[ {title} ]"),
"-" * WIDTH,
]
def bullet_line(text, w=WIDTH, prefix="* "):
"""Single-line bullet — truncate to fit, no wrapping."""
return trunc(prefix + text, w)
# ── Receipt builder ──────────────────────────────────────────────────────────
def build_receipt(now):
L = []
doy = now.timetuple().tm_yday
wday = now.weekday() # 0=Mon .. 6=Sun
is_weekend = wday >= 5
month_day = now.strftime("%m-%d")
# ── Masthead ──────────────────────────────────────────────────────────
L += ["=" * WIDTH]
L += [ctr("* * * MORNING EDITION * * *")]
L += ["=" * WIDTH]
L += [ctr(" __ __ ___ ____ _ _")]
L += [ctr("| \\/ || __|| _ \\| \\| |")]
L += [ctr("| |\\/| || _| | |_) | ' |")]
L += [ctr("|_| |_||___||____/|_|\\_|")]
L += ["=" * WIDTH]
L += [ctr(f"Good morning, {NAME}!")]
L += ["=" * WIDTH]
L += [""]
# ── Date / Location ───────────────────────────────────────────────────
L += [ctr(CITY_LINE)]
L += [ctr(now.strftime("%A, %B %d, %Y"))]
L += [ctr(now.strftime("%-I:%M %p"))]
L += [ctr(f"Day {doy} of {now.year}")]
if is_weekend:
L += [""]
L += [ctr("* * * WEEKEND EDITION * * *")]
L += [""]
# ── On This Day ───────────────────────────────────────────────────────
if month_day in ON_THIS_DAY:
yr, evt = ON_THIS_DAY[month_day]
L += section("ON THIS DAY")
L += [""]
if yr:
L += [ctr(f"In {yr}:")]
for line in wrap(evt):
L += [line]
L += [""]
# ── Weather ───────────────────────────────────────────────────────────
L += section("WEATHER - BRIDGELAND")
L += [""]
weather = get_weather()
if weather:
art = weather_art(weather["desc"])
for a in art:
L += [ctr(a.strip())]
L += [""]
L += [ctr(weather["desc"].upper())]
L += [""]
L += [ctr(f"Now: {weather['temp_f']}F Feels: {weather['feels_f']}F")]
L += [ctr(f"Today: Hi {weather['hi_f']}F / Lo {weather['lo_f']}F")]
L += [ctr(f"Humidity:{weather['humidity']}% UV: {uv_label(weather['uv'])}")]
L += [ctr(f"Wind: {weather['wind_mph']} mph {weather['wind_dir']}")]
L += [ctr(f"Vis: {weather['visibility']} mi")]
L += [ctr(f"Rise: {weather['sunrise']} Set: {weather['sunset']}")]
L += [""]
tip = get_weather_tip(weather)
for line in wrap(f">> {tip} <<"):
L += [ctr(line)]
else:
L += [ctr("Weather unavailable")]
L += [ctr("Check weather.gov for updates")]
L += [""]
# ── Houston News ──────────────────────────────────────────────────────
L += section("HOUSTON NEWS - ABC13")
L += [""]
hou_headlines = fetch_rss_headlines("https://abc13.com/feed/", 5)
if hou_headlines:
for h in hou_headlines:
L += [bullet_line(h)]
else:
L += [" (Unable to fetch headlines)"]
L += [""]
# ── World News ────────────────────────────────────────────────────────
L += section("WORLD NEWS - BBC")
L += [""]
bbc_headlines = fetch_rss_headlines("https://feeds.bbci.co.uk/news/rss.xml", 5)
if bbc_headlines:
for h in bbc_headlines:
L += [bullet_line(h)]
else:
L += [" (Unable to fetch headlines)"]
L += [""]
# ── Daily Quote ───────────────────────────────────────────────────────
L += section("THOUGHT FOR THE DAY")
L += [""]
q_text, q_author = QUOTES[doy % len(QUOTES)]
for line in wrap(f'"{q_text}"'):
L += [line]
L += [f" -- {q_author}"]
L += [""]
# ── Joke ──────────────────────────────────────────────────────────────
L += section("MORNING CHUCKLE")
L += [""]
setup, punchline = JOKES[doy % len(JOKES)]
for line in wrap(f"Q: {setup}"):
L += [line]
L += [""]
for line in wrap(f"A: {punchline}"):
L += [line]
L += [""]
# ── Fun Fact ──────────────────────────────────────────────────────────
L += section("DID YOU KNOW?")
L += [""]
fact = FACTS[doy % len(FACTS)]
for line in wrap(fact):
L += [line]
L += [""]
# ── Local Houston Tip ─────────────────────────────────────────────────
L += section("LOCAL TIP")
L += [""]
for line in wrap(HOUSTON_TIPS[doy % len(HOUSTON_TIPS)]):
L += [line]
L += [""]
# ── Footer ────────────────────────────────────────────────────────────
L += ["=" * WIDTH]
if is_weekend:
L += [ctr("Enjoy your weekend, Rich!")]
else:
L += [ctr("Make today count!")]
L += [ctr("Bridgeland Strong!")]
L += ["=" * WIDTH]
L += [ctr(now.strftime("Printed %-I:%M %p"))]
L += [""]
L += [""]
L += [""] # feed before cut
text = "\n".join(L)
# Wrap text in ESC/POS framing: init → Font A → content → feed → cut
body = text.encode("ascii", errors="replace")
return INIT + FONT_A + ALIGN_L + body + b"\n\n\n" + CUT
# ── Print ─────────────────────────────────────────────────────────────────────
def print_receipt(data: bytes):
cmd = ["lpr", "-P", PRINTER, "-o", "raw"]
result = subprocess.run(cmd, input=data, capture_output=True)
if result.returncode != 0:
print(f"lpr error: {result.stderr.decode()}", file=sys.stderr)
sys.exit(1)
# ── Main ──────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
now = datetime.datetime.now()
data = build_receipt(now)
if "--preview" in sys.argv:
# Strip ESC/POS bytes for preview — just print the text portion
sys.stdout.buffer.write(data[len(INIT + FONT_A + ALIGN_L):-len(b"\n\n\n" + CUT)])
sys.stdout.buffer.write(b"\n")
else:
print_receipt(data)
print(f"Morning Edition printed at {now.strftime('%I:%M %p')} -- have a great day, {NAME}!")