Compare commits
9 Commits
9c18d5c060
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
c3b05c6bcb
|
|||
|
1bf8e41e0c
|
|||
|
1a530bd27d
|
|||
|
bad309b2f7
|
|||
|
83467531b0
|
|||
|
49ee2d15be
|
|||
|
805f461dd2
|
|||
|
c6c1f7dd39
|
|||
|
8f4b0b6a54
|
@@ -7,5 +7,4 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD [ "python", "./src/main.py", "-c", "/config.toml" ]
|
||||
|
||||
CMD [ "python", "-u", "./src/main.py", "-c", "/config.toml" ]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
nowblinkie:
|
||||
build: "https://git.alv.cx/alvierahman90/nowblinkie"
|
||||
build: "https://git.alv.cx/alvierahman90/nowblinkie.git"
|
||||
volumes:
|
||||
- "./res:/res"
|
||||
- "./out:/out"
|
||||
|
||||
@@ -20,3 +20,13 @@ command = ["bash", "-c", "/usr/bin/uptime -p | cut -d, -f-2"]
|
||||
font = "/res/curie.bdf"
|
||||
text_filters = [ "lowercase" ]
|
||||
text_offset = [21, 4]
|
||||
|
||||
[[image]]
|
||||
output = "/out/pdweather.gif"
|
||||
template = "/res/templates/forest.gif"
|
||||
command = [ "python", "/res/pdweather.py" ]
|
||||
|
||||
font = "/res/curie.bdf"
|
||||
text_filters = [ "lowercase" ]
|
||||
text_offset = [45, 4]
|
||||
text_fill = [255, 255, 255] # your template image must have alpha channel to use alpha
|
||||
|
||||
14
config.toml
14
config.toml
@@ -20,3 +20,17 @@ command = ["bash", "-c", "/usr/bin/uptime -p | cut -d, -f-2"]
|
||||
font = "./res/curie.bdf"
|
||||
text_filters = [ "lowercase" ]
|
||||
text_offset = [21, 4]
|
||||
|
||||
[[image]]
|
||||
output = "out/pdweather.gif"
|
||||
template = "./res/templates/forest.gif"
|
||||
command = [ "python", "./res/pdweather.py" ]
|
||||
|
||||
font = "./res/curie.bdf"
|
||||
text_filters = [ "lowercase" ]
|
||||
text_offset = [45, 4]
|
||||
text_fill = [255, 255, 255] # your template image must have alpha channel to use alpha
|
||||
|
||||
[[image]]
|
||||
output = "out/iloveseason.gif"
|
||||
command = [ "python", "./res/iloveseason.py" ]
|
||||
|
||||
53
readme.md
53
readme.md
@@ -12,12 +12,61 @@ such as nginx or caddy.
|
||||
1. install requirements: `pip install -r requirements.txt`
|
||||
2. run: `python main/src.py [-c config-file] [-L]`.
|
||||
|
||||
## docker
|
||||
### docker
|
||||
|
||||
a [dockerfile](./dockerfile) and [compose file](./compose.yaml) have been provided.
|
||||
a [dockerfile](./Dockerfile) and [compose file](./compose.yaml) have been provided.
|
||||
keep in mind the commands will run in the docker container, too.
|
||||
you do not need to clone this repository to use the compose file provided.
|
||||
|
||||
the docker image installs the python
|
||||
[`requests` library](https://docs.python-requests.org/en/latest/index.html),
|
||||
so you can use it for calling external APIs inside the container.
|
||||
|
||||
the `pdweather` blinkie example does this.
|
||||
|
||||
### custom images
|
||||
|
||||
to use utilities not installed in the standard docker image,
|
||||
you can build a new one based off of it:
|
||||
|
||||
```dockerfile
|
||||
FROM https://git.alv.cx/alvierahman90/nowblinkie.git
|
||||
|
||||
RUN apt-get update && apt-get install blahblahblah
|
||||
|
||||
CMD [ "python", "-u", "./src/main.py", "-c", "/config.toml" ]
|
||||
```
|
||||
|
||||
## writing commands
|
||||
|
||||
### basic
|
||||
|
||||
basic commands return just text.
|
||||
the text can end in 0 or 1 newlines but not multiple,
|
||||
else it will be interpreted as a dynamic config command.
|
||||
|
||||
### dynamic config commands
|
||||
|
||||
any config can be dynamically set by preceeding the text to be printed
|
||||
with TOML config:
|
||||
|
||||
```
|
||||
template = "pat/to/templat.gif"
|
||||
font = "fonts/custom_font.bdf"
|
||||
text_offset = [20, 4]
|
||||
hello world
|
||||
```
|
||||
|
||||
if you do not want to print any text,
|
||||
simply leave an extra blank/empty line
|
||||
(the text ends in two newlines)
|
||||
or a space character:
|
||||
|
||||
```
|
||||
template = "pat/to/templat.gif"
|
||||
|
||||
```
|
||||
|
||||
## config
|
||||
|
||||
the provided [example config file](./config.toml) lists all the options that can be used.
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
certifi==2026.4.22
|
||||
charset-normalizer==3.4.7
|
||||
idna==3.15
|
||||
pillow==12.2.0
|
||||
requests==2.34.2
|
||||
urllib3==2.7.0
|
||||
|
||||
BIN
res/autumn.gif
Normal file
BIN
res/autumn.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
42
res/iloveseason.py
Executable file
42
res/iloveseason.py
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Akbar Rahman <hi@alv.cx>
|
||||
#
|
||||
|
||||
import sys
|
||||
from datetime import datetime as dt
|
||||
|
||||
|
||||
def get_args():
|
||||
""" Get command line arguments """
|
||||
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main(args):
|
||||
""" Entry point for script """
|
||||
now = dt.now()
|
||||
|
||||
template = ""
|
||||
if now.month in [3, 4, 5]:
|
||||
template = './res/spring.gif'
|
||||
if now.month in [6, 7, 8]:
|
||||
template = './res/summer.gif'
|
||||
if now.month in [9, 10, 11]:
|
||||
template = './res/autumn.gif'
|
||||
if now.month in [12, 1, 2]:
|
||||
template = './res/winter.gif'
|
||||
|
||||
print(f'template = "{template}"')
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
sys.exit(main(get_args()))
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
61
res/pdweather.py
Executable file
61
res/pdweather.py
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Akbar Rahman <hi@alv.cx>
|
||||
#
|
||||
|
||||
import sys
|
||||
from datetime import datetime as dt
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def get_args():
|
||||
""" Get command line arguments """
|
||||
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main(args):
|
||||
""" Entry point for script """
|
||||
resp = requests.get("https://api.open-meteo.com/v1/forecast?latitude=53.35&longitude=-1.8333&hourly=temperature_2m,rain,cloud_cover&timezone=GMT&forecast_days=1").json()
|
||||
|
||||
now = dt.now()
|
||||
closest_past_date = None
|
||||
for idx, datetime_string in enumerate(resp['hourly']['time']):
|
||||
date = dt.fromisoformat(datetime_string)
|
||||
|
||||
if closest_past_date is None:
|
||||
closest_past_date = (idx, date)
|
||||
continue
|
||||
|
||||
if date > now: # date > now means date is in future, ignore
|
||||
continue
|
||||
|
||||
if (now - date) < (now - closest_past_date[1]):
|
||||
closest_past_date = (idx, date)
|
||||
continue
|
||||
|
||||
cur_weather = {}
|
||||
for key in resp['hourly'].keys():
|
||||
cur_weather[key] = resp['hourly'][key][closest_past_date[0]]
|
||||
|
||||
output = "sunny in peaks"
|
||||
|
||||
if cur_weather['cloud_cover'] > 40:
|
||||
output = "cloudy in peaks"
|
||||
|
||||
if cur_weather['rain'] > 0.5:
|
||||
output = "raining in peaks"
|
||||
|
||||
print(output, end='')
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
sys.exit(main(get_args()))
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
BIN
res/spring.gif
Normal file
BIN
res/spring.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
res/summer.gif
Normal file
BIN
res/summer.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
BIN
res/templates/forest.gif
Normal file
BIN
res/templates/forest.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
res/winter.gif
Normal file
BIN
res/winter.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
53
src/main.py
53
src/main.py
@@ -3,7 +3,7 @@
|
||||
# Akbar Rahman <hi@alv.cx>
|
||||
#
|
||||
|
||||
import shlex
|
||||
import json
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
@@ -21,32 +21,49 @@ def get_args():
|
||||
parser.add_argument("-L", "--no-loop", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def generate(config):
|
||||
if config.get('template'):
|
||||
img = Image.open(config['template'])
|
||||
else:
|
||||
img = Image.new(mode="RGBA", size = config['size'])
|
||||
|
||||
draw = ImageDraw.Draw(img)
|
||||
with open(config['font'], "rb") as fp:
|
||||
font = BdfFontFile.BdfFontFile(fp).to_imagefont()
|
||||
|
||||
process = subprocess.run(config['command'], stdout=subprocess.PIPE, shell=False)
|
||||
if process.returncode != config.get('return_code', 0):
|
||||
# process did not run succesfully.
|
||||
# do not risk displaying that to user
|
||||
return
|
||||
cmd_output = process.stdout.decode('utf-8').split('\n')
|
||||
print(f"{cmd_output=}")
|
||||
dynamic_config = {}
|
||||
if len(cmd_output) > 2:
|
||||
dynamic_config = tomllib.loads('\n'.join(cmd_output[:-2]))
|
||||
text = cmd_output[-2]
|
||||
else:
|
||||
text = cmd_output[0]
|
||||
|
||||
text = process.stdout.decode('utf-8').split('\n')[0]
|
||||
for filter in config.get('text_filters', []):
|
||||
if filter == "lowercase":
|
||||
text = text.lower()
|
||||
elif filter == "uppercase":
|
||||
text = text.upper()
|
||||
print(f"{dynamic_config=}")
|
||||
for key, val in dynamic_config.items():
|
||||
config[key] = val
|
||||
|
||||
draw.text(config.get('text_offset', [0, 0]), text, font=font)
|
||||
if config.get('template'):
|
||||
img = Image.open(config['template'])
|
||||
else:
|
||||
img = Image.new(mode="RGBA", size = config['size'])
|
||||
|
||||
img.save(config['output'], save_all=True)
|
||||
print(f"{text=}")
|
||||
|
||||
if text:
|
||||
draw = ImageDraw.Draw(img)
|
||||
with open(config['font'], "rb") as fp:
|
||||
font = BdfFontFile.BdfFontFile(fp).to_imagefont()
|
||||
|
||||
for filter in config.get('text_filters', []):
|
||||
if filter == "lowercase":
|
||||
text = text.lower()
|
||||
elif filter == "uppercase":
|
||||
text = text.upper()
|
||||
|
||||
fill = config.get('text_fill')
|
||||
fill = tuple(fill) if fill else None
|
||||
draw.text(config.get('text_offset', [0, 0]), text, font=font, fill=fill)
|
||||
|
||||
img.save(config['output'], save_all=True, loop=0)
|
||||
|
||||
|
||||
def main(args):
|
||||
|
||||
Reference in New Issue
Block a user