a lot of stuff tbh
This commit is contained in:
parent
3bb4efb399
commit
cf9cd2e867
|
@ -0,0 +1,2 @@
|
|||
*/__pycache__/*
|
||||
*.pyc
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
|
@ -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__":
|
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue