a lot of stuff tbh

This commit is contained in:
vivlim 2023-11-23 02:22:27 -08:00
parent 3bb4efb399
commit cf9cd2e867
13 changed files with 389 additions and 15 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*/__pycache__/*
*.pyc

24
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,24 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Quart",
"type": "python",
"request": "launch",
"module": "quart",
"env": {
"QUART_APP": "app",
"QUART_DEBUG": "1"
},
"args": [
"run",
"--no-reload"
],
"jinja": true,
"justMyCode": true
}
]
}

BIN
fonts/DreamsComeTwo.ttf Normal file

Binary file not shown.

BIN
fonts/Extrude.ttf Normal file

Binary file not shown.

BIN
fonts/ImpactBits.ttf Normal file

Binary file not shown.

BIN
fonts/Macintosh128K.ttf Normal file

Binary file not shown.

BIN
fonts/Minimal4.ttf Normal file

Binary file not shown.

BIN
fonts/VCRFont.ttf Normal file

Binary file not shown.

14
fonts/fonts.md Normal file
View File

@ -0,0 +1,14 @@
Extrude by Nic - Public Domain
https://www.pentacom.jp/pentacom/bitfontmaker2/gallery/?id=238
Macintosh 128K
https://www.pentacom.jp/pentacom/bitfontmaker2/gallery/?id=1110
Minimal 4 by Saint11 - Public Domain
https://www.pentacom.jp/pentacom/bitfontmaker2/gallery/?id=2086
VCR Font by VCR Electronic Fonts - Public Domain
https://www.pentacom.jp/pentacom/bitfontmaker2/gallery/?id=1225
Dreams Come Two by DeAndreNinja - Public Domain
https://www.pentacom.jp/pentacom/bitfontmaker2/gallery/?id=3341

View File

@ -2,8 +2,6 @@ from flask import Flask, send_file
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
import requests
import logging
from datetime import datetime
import python_weather
import asyncio
@ -52,8 +50,7 @@ async def push_weather(mac):
draw = ImageDraw.Draw(image)
draw.rectangle([(0, 0), image.size], fill = (255, 255, 255))
draw.text((0, 0), "kirked land", fill = 'black', font_size=20)
draw.text((0, 20), datetime.now().strftime("%m/%d @ %H:%M"), fill = 'black', font_size=16)
draw.text((80, 16), f"{weather.current.temperature}F", fill = 'red', font_size=40)
draw.text((80, 20), f"{weather.current.temperature}F", fill = 'red', font_size=40)
draw.text((0, 50), weather.current.description, fill = 'black', font_size=20)
ypos = 90
@ -76,9 +73,9 @@ async def push_weather(mac):
response = requests.post(url, data=payload, files=files)
if response.status_code == 200:
logging.info("Image uploaded successfully!")
print("Image uploaded successfully!")
else:
logging.error("Failed to upload the image.")
print("Failed to upload the image.")
async def get_weather():
# declare the client. the measuring unit used defaults to the metric system (celcius, km/h, etc.)
@ -98,14 +95,11 @@ async def get_weather():
print(f' --> {hourly!r}')
async def main():
while True:
try:
for mac in macs:
print(f"sending to {mac}")
await push_weather(mac)
print("waiting")
except Exception:
logging.exception("failed to push weather")
while true:
for mac in macs:
print(f"sending to {mac}")
await push_weather(mac)
print("waiting")
await asyncio.sleep(600)
if __name__ == "__main__":

135
poetry.lock generated
View File

@ -1,5 +1,16 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "aiofiles"
version = "23.2.1"
description = "File support for asyncio."
optional = false
python-versions = ">=3.7"
files = [
{file = "aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107"},
{file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"},
]
[[package]]
name = "aiohttp"
version = "3.8.4"
@ -404,6 +415,77 @@ files = [
{file = "frozenlist-1.4.0.tar.gz", hash = "sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251"},
]
[[package]]
name = "h11"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.7"
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
]
[[package]]
name = "h2"
version = "4.1.0"
description = "HTTP/2 State-Machine based protocol implementation"
optional = false
python-versions = ">=3.6.1"
files = [
{file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"},
{file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"},
]
[package.dependencies]
hpack = ">=4.0,<5"
hyperframe = ">=6.0,<7"
[[package]]
name = "hpack"
version = "4.0.0"
description = "Pure-Python HPACK header compression"
optional = false
python-versions = ">=3.6.1"
files = [
{file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"},
{file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"},
]
[[package]]
name = "hypercorn"
version = "0.15.0"
description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn"
optional = false
python-versions = ">=3.7"
files = [
{file = "hypercorn-0.15.0-py3-none-any.whl", hash = "sha256:5008944999612fd188d7a1ca02e89d20065642b89503020ac392dfed11840730"},
{file = "hypercorn-0.15.0.tar.gz", hash = "sha256:d517f68d5dc7afa9a9d50ecefb0f769f466ebe8c1c18d2c2f447a24e763c9a63"},
]
[package.dependencies]
h11 = "*"
h2 = ">=3.1.0"
priority = "*"
wsproto = ">=0.14.0"
[package.extras]
docs = ["pydata_sphinx_theme", "sphinxcontrib_mermaid"]
h3 = ["aioquic (>=0.9.0,<1.0)"]
trio = ["exceptiongroup (>=1.1.0)", "trio (>=0.22.0)"]
uvloop = ["uvloop"]
[[package]]
name = "hyperframe"
version = "6.0.1"
description = "HTTP/2 framing layer for Python"
optional = false
python-versions = ">=3.6.1"
files = [
{file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"},
{file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"},
]
[[package]]
name = "idna"
version = "3.4"
@ -662,6 +744,17 @@ files = [
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
[[package]]
name = "priority"
version = "2.0.0"
description = "A pure-Python implementation of the HTTP/2 priority tree"
optional = false
python-versions = ">=3.6.1"
files = [
{file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"},
{file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"},
]
[[package]]
name = "python-weather"
version = "1.0.3"
@ -676,6 +769,32 @@ files = [
[package.dependencies]
aiohttp = "3.8.4"
[[package]]
name = "quart"
version = "0.19.4"
description = "A Python ASGI web microframework with the same API as Flask"
optional = false
python-versions = ">=3.8"
files = [
{file = "quart-0.19.4-py3-none-any.whl", hash = "sha256:959da9371b44b6f48d952661863f8f64e68a893481ef3f2ef45b177629dc0928"},
{file = "quart-0.19.4.tar.gz", hash = "sha256:22ff186cf164955a7bf7483ff42a739a9fad3b119041846b15dc9597ec74c85c"},
]
[package.dependencies]
aiofiles = "*"
blinker = ">=1.6"
click = ">=8.0.0"
flask = ">=3.0.0"
hypercorn = ">=0.11.2"
itsdangerous = "*"
jinja2 = "*"
markupsafe = "*"
werkzeug = ">=3.0.0"
[package.extras]
docs = ["pydata_sphinx_theme"]
dotenv = ["python-dotenv"]
[[package]]
name = "requests"
version = "2.31.0"
@ -730,6 +849,20 @@ MarkupSafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog (>=2.3)"]
[[package]]
name = "wsproto"
version = "1.2.0"
description = "WebSockets state-machine based protocol implementation"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"},
{file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"},
]
[package.dependencies]
h11 = ">=0.9.0,<1"
[[package]]
name = "yarl"
version = "1.9.3"
@ -836,4 +969,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "e7314c081b3004ce52e328216b60e09f3eb1fcc3ee292d98b32290aee2934914"
content-hash = "511c213c2e4e4cd7ed7bf88d409822bbed74986e151ea920fc4573b0e53b2f92"

View File

@ -12,6 +12,7 @@ flask = {extras = ["async"], version = "^3.0.0"}
pillow = "^10.1.0"
python-weather = "^1.0.3"
requests = "^2.31.0"
quart = "^0.19.4"
[build-system]

206
src/app/__init__.py Normal file
View File

@ -0,0 +1,206 @@
from quart import Quart, send_file
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
import requests
import logging
from datetime import datetime
import asyncio
from pathlib import Path
import python_weather
import asyncio
import typing
import os
import json
app = Quart(__name__)
apip = "192.168.1.103"
dither = 0
macs = [ "0000021EF597341F", "0000021D325F3413" ]
fonts = {}
for font_path in Path('./fonts').glob("**/*.ttf"):
if font_path.is_file():
font_name = str(font_path.stem)
fonts[font_name] = ImageFont.truetype(str(font_path.absolute()), size=16)
fonts[f"{font_name}_2x"] = ImageFont.truetype(str(font_path.absolute()), size=16*2)
fonts[f"{font_name}_4x"] = ImageFont.truetype(str(font_path.absolute()), size=16*4)
@app.route("/")
async def index():
async with python_weather.Client(unit=python_weather.IMPERIAL) as client:
# fetch a weather forecast from a city
weather = await client.get('Kirkland')
img_jpeg = weather_to_image(weather)
return await send_file(img_jpeg, mimetype='image/jpeg')
@app.route("/fonts")
async def list_fonts():
font_list = "\n".join(fonts.keys())
return f"<pre>{font_list}</pre>"
@app.route("/push")
async def push():
try:
for mac in macs:
print(f"sending to {mac}")
await push_weather(mac)
except Exception:
logging.exception("failed to push weather")
return "failed to push"
return "ok!"
def push_img_to_tag(pil_img, tag_mac):
url = "http://" + apip + "/imgupload"
payload = {"dither": dither, "mac": tag_mac} # Additional POST parameter
files = {"file": pil_img} # File to be uploaded
response = requests.post(url, data=payload, files=files)
if response.status_code == 200:
logging.info("Image uploaded successfully!")
else:
logging.error("Failed to upload the image.")
class Box:
def __init__(self, left: float, top: float, right: float, bottom: float):
self.left = left
self.top = top
self.right = right
self.bottom = bottom
def from_pil_box(box_tuple: tuple[float, float, float, float]):
return Box(box_tuple[0], box_tuple[1], box_tuple[2], box_tuple[3])
def height(self):
return self.bottom - self.top
def width(self):
return self.right - self.left
def translate(self, right, down):
return Box(self.left + right, self.top + down, self.right + right, self.bottom + down)
def mid_y(self):
return self.top + (self.height() / 2)
class LinearDrawable:
def __init__(self, bounds: Box, drawfn: typing.Callable[[any, Box], None], padding=0):
self.bounds = bounds
self.drawfn = drawfn
self.padding = padding
def text(text, fill='black', font_key='Macintosh128K', anchor="la"):
font = fonts[font_key]
bbox = Box.from_pil_box(font.getbbox(text))
def d(draw, available_rect: Box):
x = available_rect.left
if anchor[0] == 'r':
x = available_rect.right # right
draw.text((x, available_rect.top), text, fill=fill, font=font, anchor=anchor)
return LinearDrawable(bbox, d)
def linear_draw_v(area: Box, draw: ImageDraw.Draw, items: list[LinearDrawable]):
y = area.top
for item in items:
item_bbox = item.bounds.translate(0, y + item.padding)
if item_bbox.bottom >= area.bottom:
break # out of room.
item.drawfn(draw, Box(area.left, item_bbox.top, area.right, item_bbox.bottom))
y = item_bbox.bottom + item.padding
def weather_to_image(weather):
image = Image.new('P', (152, 152))
palette = [
255, 255, 255, # white
0, 0, 0, # black
255, 0, 0 # red
]
image.putpalette(palette)
draw = ImageDraw.Draw(image)
draw.rectangle([(0, 0), image.size], fill = (255, 255, 255))
linear_v_items = [
LinearDrawable.text(datetime.now().strftime("%m/%d @ %H:%M"), anchor="ra"),
LinearDrawable.text(f"{weather.current.temperature}F", font_key="Macintosh128K_4x", anchor="ra"),
LinearDrawable.text(weather.current.description, font_key="Macintosh128K_2x"),
LinearDrawable(Box(0,0,152,8), lambda d, b : d.line([(b.left, b.mid_y()), (b.right, b.mid_y())], fill="black"))
]
remaining_forecast = 5
for forecast in weather.forecasts:
for hourly in forecast.hourly:
if remaining_forecast <= 0:
break
forecast_dt = datetime.combine(forecast.date, hourly.time)
if forecast_dt <= datetime.now():
continue
remaining_forecast -= 1
hour_str = forecast_dt.strftime("%H%p")
linear_v_items.append(LinearDrawable.text(f"{hour_str}: {hourly.temperature}F - {hourly.description}"))
if remaining_forecast <= 0:
break
#draw.ellipse((20, 20, 130, 130), fill = 'red', outline = 'black')
linear_draw_v(Box(0, 0, 152, 152), draw, linear_v_items)
rgb_image = image.convert('RGB')
img_io = BytesIO()
rgb_image.save(img_io, 'JPEG', quality='maximum')
img_io.seek(0)
return img_io
async def push_weather(mac):
async with python_weather.Client(unit=python_weather.IMPERIAL) as client:
# fetch a weather forecast from a city
weather = await client.get('Kirkland')
img = weather_to_image(weather)
push_img_to_tag(img, mac)
async def get_weather():
# declare the client. the measuring unit used defaults to the metric system (celcius, km/h, etc.)
async with python_weather.Client(unit=python_weather.IMPERIAL) as client:
# fetch a weather forecast from a city
weather = await client.get('Kirkland')
# returns the current day's forecast temperature (int)
print(weather.current.temperature)
# get the weather forecast for a few days
for forecast in weather.forecasts:
print(forecast)
# hourly forecasts
for hourly in forecast.hourly:
print(f' --> {hourly!r}')
async def background_push():
while True:
try:
for mac in macs:
print(f"sending to {mac}")
await push_weather(mac)
print("waiting")
except Exception:
logging.exception("failed to push weather")
await asyncio.sleep(600)
@app.while_serving
async def lifespan():
push_task = asyncio.ensure_future(background_push())
yield
push_task.cancel()
if __name__ == "__main__":
app.run()