This commit is contained in:
2025-11-21 22:43:31 +01:00
72 changed files with 1544 additions and 546 deletions

View File

@@ -17,7 +17,7 @@ Before submitting a bug report, check if the issue is already reported in the [I
We welcome suggestions for new features or enhancements. Use the [Issues](https://github.com/aceinnolab/Inkycal/issues) section to submit your ideas, and provide as much detail as possible.
### Third party modules
So you had a great idea for an inkycal-module? Awesome! In fact, there is already a repo sepcfifically created for that purpose: [inkycal-modules-template](https://github.com/aceisace/inkycal-modules-template). Just fork that repo, add your module and give me a shout via Discord, Github or Email.
So you had a great idea for an inkycal-module? Awesome! In fact, there is already a repo sepcfifically created for that purpose: [inkycal-modules-template](https://github.com/aceinnolab/inkycal-modules-template). Just fork that repo, add your module and give me a shout via Discord, Github or Email.
### Pull Requests

View File

@@ -12,5 +12,5 @@ jobs:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: "Hi there and welcome to Inkycal. Thanks for opening this issue. As this is your first issue in this repository, please read through the [contributing guidelines](https://github.com/aceisace/Inkycal/blob/main/.github/CONTRIBUTING.md)"
pr-message: "Hi there and welcome to Inkycal. Thanks for opening this issue. As this is your first Pull-Request in this repository, please read through the [contributing guidelines](https://github.com/aceisace/Inkycal/blob/main/.github/CONTRIBUTING.md). Please note that non-critical pull-request cannot be merged into the main branch to ensure stability. Please create a new branch and ask to have it merged into main. Thanks for your understanding."
issue-message: "Hi there and welcome to Inkycal. Thanks for opening this issue. As this is your first issue in this repository, please read through the [contributing guidelines](https://github.com/aceinnolab/Inkycal/blob/main/.github/CONTRIBUTING.md)"
pr-message: "Hi there and welcome to Inkycal. Thanks for opening this issue. As this is your first Pull-Request in this repository, please read through the [contributing guidelines](https://github.com/aceinnolab/Inkycal/blob/main/.github/CONTRIBUTING.md). Please note that non-critical pull-request cannot be merged into the main branch to ensure stability. Please create a new branch and ask to have it merged into main. Thanks for your understanding."

View File

@@ -29,7 +29,7 @@ jobs:
TINDIE_USERNAME: ${{ secrets.TINDIE_USERNAME }}
with:
# Set the base_image to the desired Raspberry Pi OS version
base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2024-03-15/2024-03-15-raspios-bookworm-armhf-lite.img.xz
base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2025-05-13/2025-05-13-raspios-bookworm-armhf-lite.img.xz
image_additional_mb: 3072 # enlarge free space to 3GB
optimize_image: true
# user: inky --> not supported?
@@ -41,8 +41,9 @@ jobs:
echo $HOME
whoami
cd /home/inky
sudo apt update
sudo apt-get update -y
sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev -y
sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev build-essential libxml2-dev libxslt1-dev python3-dev -y
echo $PWD && ls
git clone https://github.com/aceinnolab/Inkycal
cd Inkycal
@@ -51,7 +52,7 @@ jobs:
python -m pip install --upgrade pip
pip install wheel
pip install -e ./
pip install RPi.GPIO==0.7.1 spidev==3.5 gpiozero==2.0
pip install RPi.GPIO==0.7.1 spidev==3.7 lgpio==0.2.2.0
wget https://raw.githubusercontent.com/aceinnolab/Inkycal/assets/tests/settings.json
pip install pytest
python -m pytest
python -m pytest

View File

@@ -44,5 +44,10 @@ jobs:
git config user.name "github-actions"
git config user.email "actions@github.com"
git add docs/*
git commit -m "update docs [bot]"
git push
# Check if anything is staged before committing
if git diff --cached --quiet; then
echo "Nothing to commit."
else
git commit -m "update docs [bot]"
git push
fi

View File

@@ -24,8 +24,8 @@ jobs:
TINDIE_USERNAME: ${{ secrets.TINDIE_USERNAME }}
with:
# Set the base_image to the desired Raspberry Pi OS version
# note: version 2023-12-11 seems to have issues with the kernel and gpio
base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf-lite.img.xz
# note: version 2023-12-11 onwards seems to have issues with the kernel and gpio. Using later versions requires some additional steps
base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2025-05-13/2025-05-13-raspios-bookworm-armhf-lite.img.xz
image_additional_mb: 3072 # enlarge free space to 3 GB
optimize_image: true
commands: |
@@ -37,11 +37,12 @@ jobs:
# get kernel info
uname -srm
cd /home/inky
sudo apt update
sudo apt-get update -y
# sudo apt-get dist-upgrade -y
sudo apt-get install -y python3-pip
sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev libxml2-dev libxslt-dev python-dev-is-python3 -y
sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev build-essential libxml2-dev libxslt1-dev python3-dev -y
# #334 & #335
git clone https://github.com/WiringPi/WiringPi
cd WiringPi
@@ -56,7 +57,31 @@ jobs:
python -m pip install --upgrade pip
pip install wheel
pip install -e ./
pip install RPi.GPIO==0.7.1 spidev==3.5 gpiozero==2.0
pip install RPi.GPIO==0.7.1 spidev==3.7 lgpio==0.2.2.0
# specific hacks to get this running on newer kernels, see #387. Special thanks to pbarthelemy
wget https://github.com/aceinnolab/Inkycal/raw/refs/heads/assets/hosting/pcre2-10.44.tar.bz2
bzip2 -d pcre2-10.44.tar.bz2
tar -xf pcre2-10.44.tar
cd pcre2-10.44/
./configure && make && sudo make install && make clean
cd ..
wget https://github.com/aceinnolab/Inkycal/raw/refs/heads/assets/hosting/swig-4.3.0.tar
tar -xf swig-4.3.0.tar
cd swig-4.3.0/
./configure && make && sudo make install && make clean
cd ..
wget https://github.com/aceinnolab/Inkycal/raw/refs/heads/assets/hosting/lg.zip
unzip lg.zip
cd lg
make && sudo make install && make clean
cd ..
pip install rpi-lgpio
# hacks section end
wget https://raw.githubusercontent.com/aceinnolab/Inkycal/assets/tests/settings.json
pip install pytest
python -m pytest
@@ -72,7 +97,7 @@ jobs:
# increase swap-size
# temporarily disabled due to unmounting issues
# sudo dphys-swapfile swapoff
# sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=512/' /etc/dphys-swapfile
# sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=1024/' /etc/dphys-swapfile
# sudo dphys-swapfile setup
# sudo dphys-swapfile swapon

105
README.md
View File

@@ -5,7 +5,7 @@
<a href="https://discord.gg/sHYKeSM"><img src="https://img.shields.io/discord/672082714190544899?style=flat&logo=discord&logoColor=blue&color=lightorange"></a>
<a href="https://github.com/aceinnolab/Inkycal/releases"><img alt="Version" src="https://img.shields.io/github/release/aceisace/Inkycal.svg"/></a>
<a href="https://github.com/aceinnolab/Inkycal/blob/main/LICENSE"><img alt="Licence" src="https://img.shields.io/github/license/aceisace/Inkycal.svg" /></a>
<a href="https://github.com/aceinnolab/Inkycal"><img alt="python" src="https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11-lightorange"></a>
<a href="https://github.com/aceinnolab/Inkycal"><img alt="python" src="https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-lightorange"></a>
<a href="https://github.com/aceinnolab/Inkycal/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/aceisace/Inkycal?color=yellow"></a>
</p>
@@ -22,15 +22,15 @@ Inkycal can run well even on the Raspberry Pi Zero W. Oh, and it's open for thir
## ⚠️ Warning: long installation time expected!
Starting october 2023, Raspberry Pi OS is now based on Debian bookworm and uses python 3.11 instead of 3.9 as the
default version. Inkycal has been updated to work with python3.11, but the installation of numpy can take a very long
time, in some cases even hours. If you do not want to wait this long to install Inkycal, you can also get a
ready-to-flash version of Inkycal called InkycalOS-Lite with everything pre-installed for you by sponsoring
via [GitHub Sponsors](https://github.com/sponsors/aceisace). This helps keep up maintenance costs, implement new
features and fixing bugs. Please choose the one-time sponsor option and select the one with the plug-and-play version of
Inkycal. Then, send your email-address to which InkycalOS-Lite should be sent.
Alternatively, you can also use the PayPal.me link and send the same amount as GitHub sponsors to get access to
InkycalOS-Lite!
Installing Inkycal, particularly on the Raspberry Pi Zero W models can take up to **a few hours**.
The good news is that this is one-time and InkyCal generally runs without an issue for months or even years.
The bad news is that the Zero W can run out of memory when installing the required packages. A temporary fix for this is to use SWAP (kind of like a file-based RAM) which is slow, but at least won't lead to
**TLDR: Skip the wait and several hours of headaches, sponsor InkyCal via [GitHub Sponsors](https://github.com/sponsors/aceisace) and you will shortly receive the download link
## Main features
@@ -80,8 +80,7 @@ display!**
| type | vendor | Where to buy |
|---------------------------------------------------------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 12.48" Inkycal (plug-and-play) | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-1248-build/) Pre-configured version of Inkycal with matte black aluminium designer frame and a web-ui. You do not need to buy anything extra. Includes Raspberry Pi Zero W, 12.48" e-paper, microSD card, driver board, custom packaging and 1m of cable. Comes pre-assembled for plug-and-play. |
| 7.5" Inkycal (plug-and-play) | Aceinnolab (author) |  [Buy on Tindie](https://www.tindie.com/products/aceisace4444/inkycal-build-v1/) Pre-configured version of Inkycal with custom frame and a web-ui. You do not need to buy anything extra. Includes Raspberry Pi Zero W, 7.5" e-paper, microSD card, driver board, custom packaging and 1m of cable. Comes pre-assembled for plug-and-play. |
| 7.5" Inkycal (plug-and-play) | Aceinnolab (author) |  [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-create-your-own-e-paper-dashboard/) 7" black-white-red e-paper with custom 3d-printed case, fully pre-assembled (Raspberry Pi Zero W, 7.5" e-paper, microSD card, driver board, custom packaging and 1m of cable). Also grants access to InkyCalOS-Lite. You only need to generate the settings.json file and copy it to the microSD card |
| Inkycal frame (kit -> requires wires, 7.5" Display and Zero W with microSD card | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-frame-custom-driver-board-only/) Ultraslim frame with custom-made front and backcover inkl. ultraslim driver board). You will need a Raspberry Pi, microSD card and a 7.5" e-paper display |
| Driver board | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/universal-e-paper-driver-board-for-24-pin-spi/) Ultraslim, 24-pin SPI driver board for many serial e-paper displays. |
| `[serial]` 12.48" (1304×984px) display | waveshare / gooddisplay |  Search for `Waveshare 12.48" E-Paper 1304×984` on amazon or similar |
@@ -113,7 +112,7 @@ Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager]
| set timezone | your local timezone |
1. Create and download `settings.json` file for Inkycal from
the [WEB-UI](https://aceinnolab.com/inkycal/ui). Add the modules you want with the add
the [WEB-UI](https://inkycal.aceinnolab.com/ui). Add the modules you want with the add
module button.
2. Copy the `settings.json` to the flashed microSD card.
3. Eject the microSD card from your computer now, insert it in the Raspberry Pi and power the Raspberry Pi.
@@ -141,16 +140,16 @@ sudo ./configure && sudo make && sudo make check && sudo make install
# If you are using the Raspberry Pi Zero models, you may need to increase the swapfile size to be able to install Inkycal:
sudo dphys-swapfile swapoff
sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=512/' /etc/dphys-swapfile
sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=1024/' /etc/dphys-swapfile
sudo dphys-swapfile setup
sudo dphys-swapfile swapon
```
These commands expand the filesystem, enable SPI and set up the correct timezone on the Raspberry Pi. When running the
last command, please select the continent you live in, press enter and then select the capital of the country you live
in. Lastly, press enter.
in. Lastly, press enter.
7. Follow the steps in `Installation` (see below) on how to install Inkycal.
Follow the steps in `Installation` (see below) on how to install Inkycal.
## Installing Inkycal
@@ -180,11 +179,18 @@ Run the following steps to install Inkycal. Do **not** use sudo for this, except
```bash
# Raspberry Pi specific section start
sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev
sudo apt update
sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev build-essential libxml2-dev libxslt1-dev python3-dev -y
git clone https://github.com/WiringPi/WiringPi
cd WiringPi
./build
cd ..
# python3.9 can lead to issues, hence an update to python3.11 is strongly recommended:
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install python3.11
# Raspberry Pi specific section end
cd $HOME
@@ -198,7 +204,7 @@ pip install -e ./
# only for Raspberry Pi:
pip install RPi.GPIO==0.7.1 spidev==3.5 gpiozero==2.0
pip install RPi.GPIO==0.7.1 spidev==3.7 lgpio==0.2.2.0
```
## Running Inkycal
@@ -269,10 +275,60 @@ With your setup being complete at this stage, you may want to 3d-print a case. T
friendly community:
[3D-printable case](https://github.com/aceinnolab/Inkycal/wiki/3D-printable-files)
## Directory structure
```tree
├── __init__.py
├── custom (custom functions of Inkycal are inside here)
│   ├── __init__.py
│   ├── functions.py
│   ├── inkycal_exceptions.py
│   └── openweathermap_wrapper.py
├── display (display drivers and functions)
│   ├── __init__.py
│   ├── display.py (this file acts like a wrapper for the display drivers)
│   ├── drivers (actual driver files are inside here)
│   │   ├── epd_7_in_5_colour.py (7.5" display driver). Each supported display has it's own driver
│   │   └── parallel_drivers (parallel display drivers, e.g. 9.7", 10.2" etc.)
│   ├── supported_models.py (this file contains the supported display models and is used to check which displays are supported)
│   └── test_display.py (a dummy driver which does not require a display to be attached)
├── fonts (fonts used by Inkycal are located here)
│   ├── NotoSansUI
│   ├── ProFont
│   └── WeatherFont
├── loggers.py (logging functions)
├── main.py (main file to run Inkycal)
├── modules (inkycal modules, e.g. calendar, weather, stocks etc.)
│   ├── __init__.py
│   ├── dev_module.py (a dummy module for development)
│   ├── ical_parser.py (parses icalendar files, not strictly a module, but helper class)
│   ├── inky_image.py (module to display images)
│   ├── inkycal_agenda.py (agenda module)
│   ├── inkycal_calendar.py (calendar module)
│   ├── inkycal_feeds.py (feeds module)
│   ├── inkycal_fullweather.py (full-weather module)
│   ├── inkycal_image.py (image module)
│   ├── inkycal_jokes.py (jokes module)
│   ├── inkycal_server.py (module for inkycal-server, by third party)
│   ├── inkycal_slideshow.py (slideshow module)
│   ├── inkycal_stocks.py (stocks module - credit to @worstface)
│   ├── inkycal_textfile_to_display.py (module to display text files)
│   ├── inkycal_tindie.py (tindie module)
│   ├── inkycal_todoist.py (todoist module)
│   ├── inkycal_weather.py (weather module)
│   ├── inkycal_webshot.py (webshot module - credit to @worstface)
│   ├── inkycal_xkcd.py (xkcd module - credit to @worstface)
│   └── template.py (template module)
├── settings.py (settings for Inkycal)
└── utils (utility functions)
├── __init__.py
├── json_cache.py
└── pisugar.py (PiSugar driver)
```
## Contributing
All sorts of contributions are most welcome and appreciated. To start contributing, please follow
the [Contribution Guidelines](https://github.com/aceisace/Inkycal/blob/main/.github/CONTRIBUTING.md)
the [Contribution Guidelines](https://github.com/aceinnolab/Inkycal/blob/main/.github/CONTRIBUTING.md)
The average response time for issues, PRs and emails is usually 24 hours. In some cases, it might be longer. If you want
to have some faster responses, please use Discord (link below)
@@ -282,20 +338,20 @@ to have some faster responses, please use Discord (link below)
## Join us on Discord!
We're happy to help, to beginners and developers alike. In fact, you are more likely to get faster support on Discord
than on Github.
than on GitHub.
<a href="https://discord.gg/sHYKeSM">
<img src="https://github.com/aceisace/Inkycal/blob/assets/Repo/discord-logo.png?raw=true" alt="Inkycal chatroom Discord" width=200>
<img src="https://github.com/aceinnolab/Inkycal/blob/assets/Repo/discord-logo.png?raw=true" alt="Inkycal chatroom Discord" width=200>
</a>
## Sponsoring
Inkycal relies on sponsors to keep up maintainance, development and bug-fixing. Please consider sponsoring Inkycal via
Inkycal relies on sponsors to keep up maintenance, development and bug-fixing. Please consider sponsoring Inkycal via
the sponsor button if you are happy with Inkycal.
We now offer perks depending on the amount contributed for sponsoring, ranging from pre-configured OS images for
plug-and-play to development of user-suggested modules. Check out the sponsor page to find out more.
If you have been a previous sponsor, please let us know on our Dicord server or by sending an email. We'll send you the
If you have been a previous sponsor, please let us know on our Discord server or by sending an email. We'll send you the
perks after confirming 💯
## As featured on
@@ -304,6 +360,7 @@ perks after confirming 💯
* [hackster.io](https://www.hackster.io/news/ace-innovation-lab-s-inkycal-v3-puts-a-raspberry-pi-powered-modular-epaper-dashboard-on-your-desk-b55a83cc0f46)
* [raspberryme.com](https://www.raspberryme.com/inkycal-v3-est-un-tableau-de-bord-epaper-alimente-par-raspberry-pi-pour-votre-bureau/)
* [adafruit.com](https://blog.adafruit.com/2023/12/19/icymi-python-on-microcontrollers-newsletter-circuitpython-9-alpha-6-released-gpt-via-circuitpython-new-books-and-more-circuitpython-python-micropython-icymi-raspberry_pi/)
* [all3dp.com](https://all3dp.com/1/best-raspberry-pi-projects/)
* [ittagesschau.de](https://www.ittagesschau.de/artikel/inkycal-v3-smartes-display-auf-grundlage-des-raspberry-pi-mit-elektronischem-papier-und-vielen-moglichkeiten_365893)
* [makeuseof - fantastic projects using an eink display](http://makeuseof.com/fantastic-projects-using-an-e-ink-display/)
* [notebookcheck.com](https://www.notebookcheck.com/Inkycal-V3-Smartes-Display-auf-Grundlage-des-Raspberry-Pi-mit-elektronischem-Papier-und-vielen-Moeglichkeiten.783012.0.html?ref=ittagesschau.de)
@@ -323,5 +380,5 @@ perks after confirming 💯
## Our Contributors
<table><tr><td align="center"><a href="https://github.com/aceisace"><img alt="aceisace" src="https://avatars.githubusercontent.com/u/29558518?v=4" width="117" /><br />aceisace</a></td><td align="center"><a href="https://github.com/Atrejoe"><img alt="Atrejoe" src="https://avatars.githubusercontent.com/u/585091?v=4" width="117" /><br />Atrejoe</a></td><td align="center"><a href="https://github.com/actions-user"><img alt="actions-user" src="https://avatars.githubusercontent.com/u/65916846?v=4" width="117" /><br />actions-user</a></td><td align="center"><a href="https://github.com/emilyboda"><img alt="emilyboda" src="https://avatars.githubusercontent.com/u/9170143?v=4" width="117" /><br />emilyboda</a></td><td align="center"><a href="https://github.com/StevenSeifried"><img alt="StevenSeifried" src="https://avatars.githubusercontent.com/u/39765956?v=4" width="117" /><br />StevenSeifried</a></td><td align="center"><a href="https://github.com/mrbwburns"><img alt="mrbwburns" src="https://avatars.githubusercontent.com/u/66523867?v=4" width="117" /><br />mrbwburns</a></td></tr><tr><td align="center"><a href="https://github.com/apps/dependabot"><img alt="dependabot[bot]" src="https://avatars.githubusercontent.com/in/29110?v=4" width="117" /><br />dependabot[bot]</a></td><td align="center"><a href="https://github.com/LakesideMiners"><img alt="LakesideMiners" src="https://avatars.githubusercontent.com/u/23389169?v=4" width="117" /><br />LakesideMiners</a></td><td align="center"><a href="https://github.com/hjiang"><img alt="hjiang" src="https://avatars.githubusercontent.com/u/18527?v=4" width="117" /><br />hjiang</a></td><td align="center"><a href="https://github.com/ch3lmi"><img alt="ch3lmi" src="https://avatars.githubusercontent.com/u/19972012?v=4" width="117" /><br />ch3lmi</a></td><td align="center"><a href="https://github.com/mygrexit"><img alt="mygrexit" src="https://avatars.githubusercontent.com/u/33792951?v=4" width="117" /><br />mygrexit</a></td><td align="center"><a href="https://github.com/tobychui"><img alt="tobychui" src="https://avatars.githubusercontent.com/u/24617523?v=4" width="117" /><br />tobychui</a></td></tr><tr><td align="center"><a href="https://github.com/worstface"><img alt="worstface" src="https://avatars.githubusercontent.com/u/72295005?v=4" width="117" /><br />worstface</a></td><td align="center"><a href="https://github.com/sapostoluk"><img alt="sapostoluk" src="https://avatars.githubusercontent.com/u/7192139?v=4" width="117" /><br />sapostoluk</a></td><td align="center"><a href="https://github.com/freezingDaniel"><img alt="freezingDaniel" src="https://avatars.githubusercontent.com/u/82905307?v=4" width="117" /><br />freezingDaniel</a></td><td align="center"><a href="https://github.com/dealyllama"><img alt="dealyllama" src="https://avatars.githubusercontent.com/u/5891782?v=4" width="117" /><br />dealyllama</a></td><td align="center"><a href="https://github.com/rafaljanicki"><img alt="rafaljanicki" src="https://avatars.githubusercontent.com/u/7746477?v=4" width="117" /><br />rafaljanicki</a></td><td align="center"><a href="https://github.com/priv-kweihmann"><img alt="priv-kweihmann" src="https://avatars.githubusercontent.com/u/46938494?v=4" width="117" /><br />priv-kweihmann</a></td></tr><tr><td align="center"><a href="https://github.com/surak"><img alt="surak" src="https://avatars.githubusercontent.com/u/878399?v=4" width="117" /><br />surak</a></td><td align="center"><a href="https://github.com/AlessandroMandelli"><img alt="AlessandroMandelli" src="https://avatars.githubusercontent.com/u/65062723?v=4" width="117" /><br />AlessandroMandelli</a></td><td align="center"><a href="https://github.com/DavidCamre"><img alt="DavidCamre" src="https://avatars.githubusercontent.com/u/1098069?v=4" width="117" /><br />DavidCamre</a></td><td align="center"><a href="https://github.com/jordanschau"><img alt="jordanschau" src="https://avatars.githubusercontent.com/u/412028?v=4" width="117" /><br />jordanschau</a></td><td align="center"><a href="https://github.com/mshulman"><img alt="mshulman" src="https://avatars.githubusercontent.com/u/1484420?v=4" width="117" /><br />mshulman</a></td><td align="center"><a href="https://github.com/vitasam"><img alt="vitasam" src="https://avatars.githubusercontent.com/u/5597505?v=4" width="117" /><br />vitasam</a></td></tr></table>
<table><tr><td align="center"><a href="https://github.com/aceinnolab"><img alt="aceinnolab" src="https://avatars.githubusercontent.com/u/29558518?v=4" width="117" /><br />aceisace</a></td><td align="center"><a href="https://github.com/Atrejoe"><img alt="Atrejoe" src="https://avatars.githubusercontent.com/u/585091?v=4" width="117" /><br />Atrejoe</a></td><td align="center"><a href="https://github.com/actions-user"><img alt="actions-user" src="https://avatars.githubusercontent.com/u/65916846?v=4" width="117" /><br />actions-user</a></td><td align="center"><a href="https://github.com/emilyboda"><img alt="emilyboda" src="https://avatars.githubusercontent.com/u/9170143?v=4" width="117" /><br />emilyboda</a></td><td align="center"><a href="https://github.com/StevenSeifried"><img alt="StevenSeifried" src="https://avatars.githubusercontent.com/u/39765956?v=4" width="117" /><br />StevenSeifried</a></td><td align="center"><a href="https://github.com/mrbwburns"><img alt="mrbwburns" src="https://avatars.githubusercontent.com/u/66523867?v=4" width="117" /><br />mrbwburns</a></td></tr><tr><td align="center"><a href="https://github.com/apps/dependabot"><img alt="dependabot[bot]" src="https://avatars.githubusercontent.com/in/29110?v=4" width="117" /><br />dependabot[bot]</a></td><td align="center"><a href="https://github.com/LakesideMiners"><img alt="LakesideMiners" src="https://avatars.githubusercontent.com/u/23389169?v=4" width="117" /><br />LakesideMiners</a></td><td align="center"><a href="https://github.com/hjiang"><img alt="hjiang" src="https://avatars.githubusercontent.com/u/18527?v=4" width="117" /><br />hjiang</a></td><td align="center"><a href="https://github.com/ch3lmi"><img alt="ch3lmi" src="https://avatars.githubusercontent.com/u/19972012?v=4" width="117" /><br />ch3lmi</a></td><td align="center"><a href="https://github.com/mygrexit"><img alt="mygrexit" src="https://avatars.githubusercontent.com/u/33792951?v=4" width="117" /><br />mygrexit</a></td><td align="center"><a href="https://github.com/tobychui"><img alt="tobychui" src="https://avatars.githubusercontent.com/u/24617523?v=4" width="117" /><br />tobychui</a></td></tr><tr><td align="center"><a href="https://github.com/worstface"><img alt="worstface" src="https://avatars.githubusercontent.com/u/72295005?v=4" width="117" /><br />worstface</a></td><td align="center"><a href="https://github.com/sapostoluk"><img alt="sapostoluk" src="https://avatars.githubusercontent.com/u/7192139?v=4" width="117" /><br />sapostoluk</a></td><td align="center"><a href="https://github.com/freezingDaniel"><img alt="freezingDaniel" src="https://avatars.githubusercontent.com/u/82905307?v=4" width="117" /><br />freezingDaniel</a></td><td align="center"><a href="https://github.com/dealyllama"><img alt="dealyllama" src="https://avatars.githubusercontent.com/u/5891782?v=4" width="117" /><br />dealyllama</a></td><td align="center"><a href="https://github.com/rafaljanicki"><img alt="rafaljanicki" src="https://avatars.githubusercontent.com/u/7746477?v=4" width="117" /><br />rafaljanicki</a></td><td align="center"><a href="https://github.com/priv-kweihmann"><img alt="priv-kweihmann" src="https://avatars.githubusercontent.com/u/46938494?v=4" width="117" /><br />priv-kweihmann</a></td></tr><tr><td align="center"><a href="https://github.com/surak"><img alt="surak" src="https://avatars.githubusercontent.com/u/878399?v=4" width="117" /><br />surak</a></td><td align="center"><a href="https://github.com/AlessandroMandelli"><img alt="AlessandroMandelli" src="https://avatars.githubusercontent.com/u/65062723?v=4" width="117" /><br />AlessandroMandelli</a></td><td align="center"><a href="https://github.com/DavidCamre"><img alt="DavidCamre" src="https://avatars.githubusercontent.com/u/1098069?v=4" width="117" /><br />DavidCamre</a></td><td align="center"><a href="https://github.com/jordanschau"><img alt="jordanschau" src="https://avatars.githubusercontent.com/u/412028?v=4" width="117" /><br />jordanschau</a></td><td align="center"><a href="https://github.com/mshulman"><img alt="mshulman" src="https://avatars.githubusercontent.com/u/1484420?v=4" width="117" /><br />mshulman</a></td><td align="center"><a href="https://github.com/vitasam"><img alt="vitasam" src="https://avatars.githubusercontent.com/u/5597505?v=4" width="117" /><br />vitasam</a></td></tr></table>

View File

@@ -1,5 +1,5 @@
# About Inkycal
<img align="center" src="https://raw.githubusercontent.com/aceisace/Inkycal/assets/Repo/logo.png" width="800" alt="inkycal logo">
<img align="center" src="https://raw.githubusercontent.com/aceinnolab/Inkycal/assets/Repo/logo.png" width="800" alt="inkycal logo">
Inkycal is a python3 software for selected E-Paper displays.
It's open-source (non-commercially), fully modular, user-friendly and even runs

View File

@@ -17,7 +17,7 @@ pip3 install -e ./
```
## Creating settings file
Please navigate to the [WEB-UI](https://aceisace.eu.pythonanywhere.com/index) to create your settings file.
Please navigate to the [WEB-UI](https://inkycal.aceinnolab.com) to create your settings file.
Copy the generated settings file to the Raspberry Pi
more coming soon..

View File

@@ -1,12 +1,5 @@
/*
* basic.css
* ~~~~~~~~~
*
* Sphinx stylesheet -- basic theme.
*
* :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
/* -- main layout ----------------------------------------------------------- */
@@ -115,15 +108,11 @@ img {
/* -- search page ----------------------------------------------------------- */
ul.search {
margin: 10px 0 0 20px;
padding: 0;
margin-top: 10px;
}
ul.search li {
padding: 5px 0 5px 20px;
background-image: url(file.png);
background-repeat: no-repeat;
background-position: 0 7px;
padding: 5px 0;
}
ul.search li a {
@@ -752,14 +741,6 @@ abbr, acronym {
cursor: help;
}
.translated {
background-color: rgba(207, 255, 207, 0.2)
}
.untranslated {
background-color: rgba(255, 207, 207, 0.2)
}
/* -- code displays --------------------------------------------------------- */
pre {

View File

@@ -1 +1 @@
.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}
.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px}

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,5 @@
/*
* doctools.js
* ~~~~~~~~~~~
*
* Base JavaScript utilities for all Sphinx HTML documentation.
*
* :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
"use strict";

BIN
docs/_static/fonts/Lato/lato-bold.eot vendored Normal file

Binary file not shown.

BIN
docs/_static/fonts/Lato/lato-bold.ttf vendored Normal file

Binary file not shown.

BIN
docs/_static/fonts/Lato/lato-bold.woff vendored Normal file

Binary file not shown.

BIN
docs/_static/fonts/Lato/lato-bold.woff2 vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
docs/_static/fonts/Lato/lato-italic.eot vendored Normal file

Binary file not shown.

BIN
docs/_static/fonts/Lato/lato-italic.ttf vendored Normal file

Binary file not shown.

BIN
docs/_static/fonts/Lato/lato-italic.woff vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
docs/_static/fonts/Lato/lato-regular.eot vendored Normal file

Binary file not shown.

BIN
docs/_static/fonts/Lato/lato-regular.ttf vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

228
docs/_static/js/versions.js vendored Normal file
View File

@@ -0,0 +1,228 @@
const themeFlyoutDisplay = "hidden";
const themeVersionSelector = true;
const themeLanguageSelector = true;
if (themeFlyoutDisplay === "attached") {
function renderLanguages(config) {
if (!config.projects.translations.length) {
return "";
}
// Insert the current language to the options on the selector
let languages = config.projects.translations.concat(config.projects.current);
languages = languages.sort((a, b) => a.language.name.localeCompare(b.language.name));
const languagesHTML = `
<dl>
<dt>Languages</dt>
${languages
.map(
(translation) => `
<dd ${translation.slug == config.projects.current.slug ? 'class="rtd-current-item"' : ""}>
<a href="${translation.urls.documentation}">${translation.language.code}</a>
</dd>
`,
)
.join("\n")}
</dl>
`;
return languagesHTML;
}
function renderVersions(config) {
if (!config.versions.active.length) {
return "";
}
const versionsHTML = `
<dl>
<dt>Versions</dt>
${config.versions.active
.map(
(version) => `
<dd ${version.slug === config.versions.current.slug ? 'class="rtd-current-item"' : ""}>
<a href="${version.urls.documentation}">${version.slug}</a>
</dd>
`,
)
.join("\n")}
</dl>
`;
return versionsHTML;
}
function renderDownloads(config) {
if (!Object.keys(config.versions.current.downloads).length) {
return "";
}
const downloadsNameDisplay = {
pdf: "PDF",
epub: "Epub",
htmlzip: "HTML",
};
const downloadsHTML = `
<dl>
<dt>Downloads</dt>
${Object.entries(config.versions.current.downloads)
.map(
([name, url]) => `
<dd>
<a href="${url}">${downloadsNameDisplay[name]}</a>
</dd>
`,
)
.join("\n")}
</dl>
`;
return downloadsHTML;
}
document.addEventListener("readthedocs-addons-data-ready", function (event) {
const config = event.detail.data();
const flyout = `
<div class="rst-versions" data-toggle="rst-versions" role="note">
<span class="rst-current-version" data-toggle="rst-current-version">
<span class="fa fa-book"> Read the Docs</span>
v: ${config.versions.current.slug}
<span class="fa fa-caret-down"></span>
</span>
<div class="rst-other-versions">
<div class="injected">
${renderLanguages(config)}
${renderVersions(config)}
${renderDownloads(config)}
<dl>
<dt>On Read the Docs</dt>
<dd>
<a href="${config.projects.current.urls.home}">Project Home</a>
</dd>
<dd>
<a href="${config.projects.current.urls.builds}">Builds</a>
</dd>
<dd>
<a href="${config.projects.current.urls.downloads}">Downloads</a>
</dd>
</dl>
<dl>
<dt>Search</dt>
<dd>
<form id="flyout-search-form">
<input
class="wy-form"
type="text"
name="q"
aria-label="Search docs"
placeholder="Search docs"
/>
</form>
</dd>
</dl>
<hr />
<small>
<span>Hosted by <a href="https://about.readthedocs.org/?utm_source=&utm_content=flyout">Read the Docs</a></span>
</small>
</div>
</div>
`;
// Inject the generated flyout into the body HTML element.
document.body.insertAdjacentHTML("beforeend", flyout);
// Trigger the Read the Docs Addons Search modal when clicking on the "Search docs" input from inside the flyout.
document
.querySelector("#flyout-search-form")
.addEventListener("focusin", () => {
const event = new CustomEvent("readthedocs-search-show");
document.dispatchEvent(event);
});
})
}
if (themeLanguageSelector || themeVersionSelector) {
function onSelectorSwitch(event) {
const option = event.target.selectedIndex;
const item = event.target.options[option];
window.location.href = item.dataset.url;
}
document.addEventListener("readthedocs-addons-data-ready", function (event) {
const config = event.detail.data();
const versionSwitch = document.querySelector(
"div.switch-menus > div.version-switch",
);
if (themeVersionSelector) {
let versions = config.versions.active;
if (config.versions.current.hidden || config.versions.current.type === "external") {
versions.unshift(config.versions.current);
}
const versionSelect = `
<select>
${versions
.map(
(version) => `
<option
value="${version.slug}"
${config.versions.current.slug === version.slug ? 'selected="selected"' : ""}
data-url="${version.urls.documentation}">
${version.slug}
</option>`,
)
.join("\n")}
</select>
`;
versionSwitch.innerHTML = versionSelect;
versionSwitch.firstElementChild.addEventListener("change", onSelectorSwitch);
}
const languageSwitch = document.querySelector(
"div.switch-menus > div.language-switch",
);
if (themeLanguageSelector) {
if (config.projects.translations.length) {
// Add the current language to the options on the selector
let languages = config.projects.translations.concat(
config.projects.current,
);
languages = languages.sort((a, b) =>
a.language.name.localeCompare(b.language.name),
);
const languageSelect = `
<select>
${languages
.map(
(language) => `
<option
value="${language.language.code}"
${config.projects.current.slug === language.slug ? 'selected="selected"' : ""}
data-url="${language.urls.documentation}">
${language.language.name}
</option>`,
)
.join("\n")}
</select>
`;
languageSwitch.innerHTML = languageSelect;
languageSwitch.firstElementChild.addEventListener("change", onSelectorSwitch);
}
else {
languageSwitch.remove();
}
}
});
}
document.addEventListener("readthedocs-addons-data-ready", function (event) {
// Trigger the Read the Docs Addons Search modal when clicking on "Search docs" input from the topnav.
document
.querySelector("[role='search'] input")
.addEventListener("focusin", () => {
const event = new CustomEvent("readthedocs-search-show");
document.dispatchEvent(event);
});
});

View File

@@ -1,13 +1,6 @@
/*
* language_data.js
* ~~~~~~~~~~~~~~~~
*
* This script contains the language-specific data used by searchtools.js,
* namely the list of stopwords, stemmer, scorer and splitter.
*
* :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"];

View File

@@ -6,9 +6,9 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left:
.highlight .hll { background-color: #ffffcc }
.highlight { background: #f8f8f8; }
.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */
.highlight .err { border: 1px solid #FF0000 } /* Error */
.highlight .err { border: 1px solid #F00 } /* Error */
.highlight .k { color: #008000; font-weight: bold } /* Keyword */
.highlight .o { color: #666666 } /* Operator */
.highlight .o { color: #666 } /* Operator */
.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #9C6500 } /* Comment.Preproc */
@@ -25,34 +25,34 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left:
.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.highlight .gt { color: #0044DD } /* Generic.Traceback */
.highlight .gt { color: #04D } /* Generic.Traceback */
.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008000 } /* Keyword.Pseudo */
.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #B00040 } /* Keyword.Type */
.highlight .m { color: #666666 } /* Literal.Number */
.highlight .m { color: #666 } /* Literal.Number */
.highlight .s { color: #BA2121 } /* Literal.String */
.highlight .na { color: #687822 } /* Name.Attribute */
.highlight .nb { color: #008000 } /* Name.Builtin */
.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */
.highlight .no { color: #880000 } /* Name.Constant */
.highlight .nd { color: #AA22FF } /* Name.Decorator */
.highlight .nc { color: #00F; font-weight: bold } /* Name.Class */
.highlight .no { color: #800 } /* Name.Constant */
.highlight .nd { color: #A2F } /* Name.Decorator */
.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */
.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0000FF } /* Name.Function */
.highlight .nf { color: #00F } /* Name.Function */
.highlight .nl { color: #767600 } /* Name.Label */
.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
.highlight .nn { color: #00F; font-weight: bold } /* Name.Namespace */
.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #19177C } /* Name.Variable */
.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #666666 } /* Literal.Number.Bin */
.highlight .mf { color: #666666 } /* Literal.Number.Float */
.highlight .mh { color: #666666 } /* Literal.Number.Hex */
.highlight .mi { color: #666666 } /* Literal.Number.Integer */
.highlight .mo { color: #666666 } /* Literal.Number.Oct */
.highlight .ow { color: #A2F; font-weight: bold } /* Operator.Word */
.highlight .w { color: #BBB } /* Text.Whitespace */
.highlight .mb { color: #666 } /* Literal.Number.Bin */
.highlight .mf { color: #666 } /* Literal.Number.Float */
.highlight .mh { color: #666 } /* Literal.Number.Hex */
.highlight .mi { color: #666 } /* Literal.Number.Integer */
.highlight .mo { color: #666 } /* Literal.Number.Oct */
.highlight .sa { color: #BA2121 } /* Literal.String.Affix */
.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */
.highlight .sc { color: #BA2121 } /* Literal.String.Char */
@@ -67,9 +67,9 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left:
.highlight .s1 { color: #BA2121 } /* Literal.String.Single */
.highlight .ss { color: #19177C } /* Literal.String.Symbol */
.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0000FF } /* Name.Function.Magic */
.highlight .fm { color: #00F } /* Name.Function.Magic */
.highlight .vc { color: #19177C } /* Name.Variable.Class */
.highlight .vg { color: #19177C } /* Name.Variable.Global */
.highlight .vi { color: #19177C } /* Name.Variable.Instance */
.highlight .vm { color: #19177C } /* Name.Variable.Magic */
.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
.highlight .il { color: #666 } /* Literal.Number.Integer.Long */

View File

@@ -1,12 +1,5 @@
/*
* searchtools.js
* ~~~~~~~~~~~~~~~~
*
* Sphinx JavaScript utilities for the full-text search.
*
* :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
"use strict";
@@ -20,7 +13,7 @@ if (typeof Scorer === "undefined") {
// and returns the new score.
/*
score: result => {
const [docname, title, anchor, descr, score, filename] = result
const [docname, title, anchor, descr, score, filename, kind] = result
return score
},
*/
@@ -47,6 +40,14 @@ if (typeof Scorer === "undefined") {
};
}
// Global search result kind enum, used by themes to style search results.
class SearchResultKind {
static get index() { return "index"; }
static get object() { return "object"; }
static get text() { return "text"; }
static get title() { return "title"; }
}
const _removeChildren = (element) => {
while (element && element.lastChild) element.removeChild(element.lastChild);
};
@@ -64,9 +65,13 @@ const _displayItem = (item, searchTerms, highlightTerms) => {
const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY;
const contentRoot = document.documentElement.dataset.content_root;
const [docName, title, anchor, descr, score, _filename] = item;
const [docName, title, anchor, descr, score, _filename, kind] = item;
let listItem = document.createElement("li");
// Add a class representing the item's type:
// can be used by a theme's CSS selector for styling
// See SearchResultKind for the class names.
listItem.classList.add(`kind-${kind}`);
let requestUrl;
let linkUrl;
if (docBuilder === "dirhtml") {
@@ -115,8 +120,10 @@ const _finishSearch = (resultCount) => {
"Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories."
);
else
Search.status.innerText = _(
"Search finished, found ${resultCount} page(s) matching the search query."
Search.status.innerText = Documentation.ngettext(
"Search finished, found one page matching the search query.",
"Search finished, found ${resultCount} pages matching the search query.",
resultCount,
).replace('${resultCount}', resultCount);
};
const _displayNextItem = (
@@ -138,7 +145,7 @@ const _displayNextItem = (
else _finishSearch(resultCount);
};
// Helper function used by query() to order search results.
// Each input is an array of [docname, title, anchor, descr, score, filename].
// Each input is an array of [docname, title, anchor, descr, score, filename, kind].
// Order the results by score (in opposite order of appearance, since the
// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically.
const _orderResultsByScoreThenName = (a, b) => {
@@ -248,6 +255,7 @@ const Search = {
searchSummary.classList.add("search-summary");
searchSummary.innerText = "";
const searchList = document.createElement("ul");
searchList.setAttribute("role", "list");
searchList.classList.add("search");
const out = document.getElementById("search-results");
@@ -318,7 +326,7 @@ const Search = {
const indexEntries = Search._index.indexentries;
// Collect multiple result groups to be sorted separately and then ordered.
// Each is an array of [docname, title, anchor, descr, score, filename].
// Each is an array of [docname, title, anchor, descr, score, filename, kind].
const normalResults = [];
const nonMainIndexResults = [];
@@ -337,6 +345,7 @@ const Search = {
null,
score + boost,
filenames[file],
SearchResultKind.title,
]);
}
}
@@ -354,6 +363,7 @@ const Search = {
null,
score,
filenames[file],
SearchResultKind.index,
];
if (isMain) {
normalResults.push(result);
@@ -475,6 +485,7 @@ const Search = {
descr,
score,
filenames[match[0]],
SearchResultKind.object,
]);
};
Object.keys(objects).forEach((prefix) =>
@@ -502,9 +513,11 @@ const Search = {
// perform the search on the required terms
searchTerms.forEach((word) => {
const files = [];
// find documents, if any, containing the query word in their text/title term indices
// use Object.hasOwnProperty to avoid mismatching against prototype properties
const arr = [
{ files: terms[word], score: Scorer.term },
{ files: titleTerms[word], score: Scorer.title },
{ files: terms.hasOwnProperty(word) ? terms[word] : undefined, score: Scorer.term },
{ files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, score: Scorer.title },
];
// add support for partial matches
if (word.length > 2) {
@@ -536,8 +549,9 @@ const Search = {
// set score for the word in each file
recordFiles.forEach((file) => {
if (!scoreMap.has(file)) scoreMap.set(file, {});
scoreMap.get(file)[word] = record.score;
if (!scoreMap.has(file)) scoreMap.set(file, new Map());
const fileScores = scoreMap.get(file);
fileScores.set(word, record.score);
});
});
@@ -576,7 +590,7 @@ const Search = {
break;
// select one (max) score for the file.
const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w]));
const score = Math.max(...wordList.map((w) => scoreMap.get(file).get(w)));
// add result to the result list
results.push([
docNames[file],
@@ -585,6 +599,7 @@ const Search = {
null,
score,
filenames[file],
SearchResultKind.text,
]);
}
return results;

View File

@@ -1,3 +1,5 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" data-content_root="./">
<head>
@@ -5,19 +7,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>About Inkycal &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
<!--[if lt IE 9]>
<script src="_static/js/html5shiv.min.js"></script>
<![endif]-->
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>
<link rel="author" title="About these documents" href="#" />
<link rel="index" title="Index" href="genindex.html" />
@@ -82,7 +80,7 @@
<section id="about-inkycal">
<h1>About Inkycal<a class="headerlink" href="#about-inkycal" title="Link to this heading"></a></h1>
<img align="center" src="https://raw.githubusercontent.com/aceisace/Inkycal/assets/Repo/logo.png" width="800" alt="inkycal logo"><p>Inkycal is a python3 software for selected E-Paper displays.
<img align="center" src="https://raw.githubusercontent.com/aceinnolab/Inkycal/assets/Repo/logo.png" width="800" alt="inkycal logo"><p>Inkycal is a python3 software for selected E-Paper displays.
Its open-source (non-commercially), fully modular, user-friendly and even runs
well even on the Raspberry Pi Zero. Inkycal even has a web-UI which takes
care of adding your details! No more editing files, Yay :partying_face:</p>

View File

@@ -1,3 +1,5 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" data-content_root="./">
<head>
@@ -5,19 +7,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Developer documentation &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
<!--[if lt IE 9]>
<script src="_static/js/html5shiv.min.js"></script>
<![endif]-->
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>
<link rel="author" title="About these documents" href="about.html" />
<link rel="index" title="Index" href="genindex.html" />

View File

@@ -1,22 +1,20 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" data-content_root="./">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Index &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
<!--[if lt IE 9]>
<script src="_static/js/html5shiv.min.js"></script>
<![endif]-->
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>
<link rel="author" title="About these documents" href="about.html" />
<link rel="index" title="Index" href="#" />
@@ -255,10 +253,6 @@
<h2 id="P">P</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="inkycal.html#inkycal.modules.inky_image.Inkyimage.preview">preview() (inkycal.modules.inky_image.Inkyimage static method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="inkycal.html#inkycal.main.Inkycal.process_module">process_module() (inkycal.main.Inkycal method)</a>
</li>

View File

@@ -1,3 +1,5 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" data-content_root="./">
<head>
@@ -5,19 +7,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Inkycal documentation &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
<!--[if lt IE 9]>
<script src="_static/js/html5shiv.min.js"></script>
<![endif]-->
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>
<link rel="author" title="About these documents" href="about.html" />
<link rel="index" title="Index" href="genindex.html" />

View File

@@ -1,3 +1,5 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" data-content_root="./">
<head>
@@ -5,19 +7,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Inkycal &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
<!--[if lt IE 9]>
<script src="_static/js/html5shiv.min.js"></script>
<![endif]-->
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>
<link rel="author" title="About these documents" href="about.html" />
<link rel="index" title="Index" href="genindex.html" />
@@ -87,7 +85,6 @@
<li class="toctree-l3"><a class="reference internal" href="#inkycal.modules.inky_image.Inkyimage.flip"><code class="docutils literal notranslate"><span class="pre">Inkyimage.flip()</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="#inkycal.modules.inky_image.Inkyimage.load"><code class="docutils literal notranslate"><span class="pre">Inkyimage.load()</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="#inkycal.modules.inky_image.Inkyimage.merge"><code class="docutils literal notranslate"><span class="pre">Inkyimage.merge()</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="#inkycal.modules.inky_image.Inkyimage.preview"><code class="docutils literal notranslate"><span class="pre">Inkyimage.preview()</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="#inkycal.modules.inky_image.Inkyimage.remove_alpha"><code class="docutils literal notranslate"><span class="pre">Inkyimage.remove_alpha()</span></code></a></li>
<li class="toctree-l3"><a class="reference internal" href="#inkycal.modules.inky_image.Inkyimage.resize"><code class="docutils literal notranslate"><span class="pre">Inkyimage.resize()</span></code></a></li>
</ul>
@@ -131,7 +128,7 @@
Copyright by aceinnolab</p>
<dl class="py class">
<dt class="sig sig-object py" id="inkycal.main.Inkycal">
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">inkycal.main.</span></span><span class="sig-name descname"><span class="pre">Inkycal</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">settings_path</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">render</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">bool</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">True</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">use_pi_sugar</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">bool</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">shutdown_after_run</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">bool</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.main.Inkycal" title="Link to this definition"></a></dt>
<em class="property"><span class="k"><span class="pre">class</span></span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">inkycal.main.</span></span><span class="sig-name descname"><span class="pre">Inkycal</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">settings_path</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">render</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">bool</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">True</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">use_pi_sugar</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">bool</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">shutdown_after_run</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">bool</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.main.Inkycal" title="Link to this definition"></a></dt>
<dd><p>Inkycal main class</p>
<p>Main class of Inkycal, test and run the main Inkycal program.</p>
<dl class="simple">
@@ -188,7 +185,7 @@ checks if the images could be generated correctly.</p>
<dl class="py method">
<dt class="sig sig-object py" id="inkycal.main.Inkycal.run">
<em class="property"><span class="pre">async</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">run</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">run_once</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.main.Inkycal.run" title="Link to this definition"></a></dt>
<em class="property"><span class="k"><span class="pre">async</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">run</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">run_once</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.main.Inkycal.run" title="Link to this definition"></a></dt>
<dd><p>Runs main program in nonstop mode or a single iteration based on the run_once flag.</p>
<dl class="simple">
<dt>Args:</dt><dd><dl class="simple">
@@ -295,7 +292,7 @@ printed fonts of this function:</p>
</dd>
</dl>
<p>The extracted timezone can be used to show the local time instead of UTC. e.g.</p>
<div class="doctest highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="kn">import</span> <span class="nn">arrow</span>
<div class="doctest highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="kn">import</span><span class="w"> </span><span class="nn">arrow</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">print</span><span class="p">(</span><span class="n">arrow</span><span class="o">.</span><span class="n">now</span><span class="p">())</span> <span class="c1"># returns non-timezone-aware time</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">print</span><span class="p">(</span><span class="n">arrow</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">tz</span><span class="o">=</span><span class="n">get_system_tz</span><span class="p">()))</span> <span class="c1"># prints timezone aware time.</span>
</pre></div>
@@ -379,12 +376,12 @@ maximum of 90% of the size of the full height of the text-box.</p></li>
Copyright by aceinnolab</p>
<dl class="py class">
<dt class="sig sig-object py" id="inkycal.modules.ical_parser.iCalendar">
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">inkycal.modules.ical_parser.</span></span><span class="sig-name descname"><span class="pre">iCalendar</span></span><a class="headerlink" href="#inkycal.modules.ical_parser.iCalendar" title="Link to this definition"></a></dt>
<em class="property"><span class="k"><span class="pre">class</span></span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">inkycal.modules.ical_parser.</span></span><span class="sig-name descname"><span class="pre">iCalendar</span></span><a class="headerlink" href="#inkycal.modules.ical_parser.iCalendar" title="Link to this definition"></a></dt>
<dd><p>iCalendar parsing moudule for inkycal.
Parses events from given iCalendar URLs / paths</p>
<dl class="py method">
<dt class="sig sig-object py" id="inkycal.modules.ical_parser.iCalendar.all_day">
<em class="property"><span class="pre">static</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">all_day</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">event</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.ical_parser.iCalendar.all_day" title="Link to this definition"></a></dt>
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">all_day</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">event</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.ical_parser.iCalendar.all_day" title="Link to this definition"></a></dt>
<dd><p>Check if an event is an all day event.
Returns True if event is all day, else False</p>
</dd></dl>
@@ -407,7 +404,7 @@ Returns a list of events sorted by date</p>
<dl class="py method">
<dt class="sig sig-object py" id="inkycal.modules.ical_parser.iCalendar.get_system_tz">
<em class="property"><span class="pre">static</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">get_system_tz</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.ical_parser.iCalendar.get_system_tz" title="Link to this definition"></a></dt>
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">get_system_tz</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.ical_parser.iCalendar.get_system_tz" title="Link to this definition"></a></dt>
<dd><p>Get the timezone set by the system</p>
</dd></dl>
@@ -450,7 +447,7 @@ images.</p>
<p>Copyright by aceinnolab</p>
<dl class="py class">
<dt class="sig sig-object py" id="inkycal.modules.inky_image.Inkyimage">
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">inkycal.modules.inky_image.</span></span><span class="sig-name descname"><span class="pre">Inkyimage</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">image</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.inky_image.Inkyimage" title="Link to this definition"></a></dt>
<em class="property"><span class="k"><span class="pre">class</span></span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">inkycal.modules.inky_image.</span></span><span class="sig-name descname"><span class="pre">Inkyimage</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">image</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.inky_image.Inkyimage" title="Link to this definition"></a></dt>
<dd><p>Custom Imgae class written for commonly used image operations.</p>
<dl class="py method">
<dt class="sig sig-object py" id="inkycal.modules.inky_image.Inkyimage.autoflip">
@@ -510,7 +507,7 @@ file-format, i.e. is not an image</p></li>
<dl class="py method">
<dt class="sig sig-object py" id="inkycal.modules.inky_image.Inkyimage.merge">
<em class="property"><span class="pre">static</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">merge</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">image1</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">image2</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.inky_image.Inkyimage.merge" title="Link to this definition"></a></dt>
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">merge</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">image1</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">image2</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.inky_image.Inkyimage.merge" title="Link to this definition"></a></dt>
<dd><p>Merges two images into one.</p>
<p>Replaces white pixels of the first image with transparent ones. Then pastes
the first image on the second one.</p>
@@ -527,12 +524,6 @@ the first image on the second one.</p>
</dl>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="inkycal.modules.inky_image.Inkyimage.preview">
<em class="property"><span class="pre">static</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">preview</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">image</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.inky_image.Inkyimage.preview" title="Link to this definition"></a></dt>
<dd><p>Previews an image on gpicview (only works on Rapsbian with Desktop).</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="inkycal.modules.inky_image.Inkyimage.remove_alpha">
<span class="sig-name descname"><span class="pre">remove_alpha</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.inky_image.Inkyimage.remove_alpha" title="Link to this definition"></a></dt>

Binary file not shown.

View File

@@ -1,22 +1,20 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" data-content_root="./">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Python Module Index &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
<!--[if lt IE 9]>
<script src="_static/js/html5shiv.min.js"></script>
<![endif]-->
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>
<link rel="author" title="About these documents" href="about.html" />
<link rel="index" title="Index" href="genindex.html" />

View File

@@ -1,3 +1,5 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" data-content_root="./">
<head>
@@ -5,19 +7,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Quickstart &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
<!--[if lt IE 9]>
<script src="_static/js/html5shiv.min.js"></script>
<![endif]-->
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>
<link rel="author" title="About these documents" href="about.html" />
<link rel="index" title="Index" href="genindex.html" />
@@ -102,7 +100,7 @@ pip3<span class="w"> </span>install<span class="w"> </span>-e<span class="w"> </
</section>
<section id="creating-settings-file">
<h2>Creating settings file<a class="headerlink" href="#creating-settings-file" title="Link to this heading"></a></h2>
<p>Please navigate to the <a class="reference external" href="https://aceisace.eu.pythonanywhere.com/index">WEB-UI</a> to create your settings file.</p>
<p>Please navigate to the <a class="reference external" href="https://inkycal.aceinnolab.com">WEB-UI</a> to create your settings file.</p>
<p>Copy the generated settings file to the Raspberry Pi
more coming soon..</p>
</section>

View File

@@ -1,23 +1,21 @@
<!DOCTYPE html>
<html class="writer-html5" lang="en" data-content_root="./">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Search &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
<!--[if lt IE 9]>
<script src="_static/js/html5shiv.min.js"></script>
<![endif]-->
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>
<script src="_static/searchtools.js"></script>
<script src="_static/language_data.js"></script>

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
# About Inkycal
<img align="center" src="https://raw.githubusercontent.com/aceisace/Inkycal/assets/Repo/logo.png" width="800" alt="inkycal logo">
<img align="center" src="https://raw.githubusercontent.com/aceinnolab/Inkycal/assets/Repo/logo.png" width="800" alt="inkycal logo">
Inkycal is a python3 software for selected E-Paper displays.
It's open-source (non-commercially), fully modular, user-friendly and even runs

View File

@@ -17,7 +17,7 @@ pip3 install -e ./
```
## Creating settings file
Please navigate to the [WEB-UI](https://aceisace.eu.pythonanywhere.com/index) to create your settings file.
Please navigate to the [WEB-UI](https://inkycal.aceinnolab.com) to create your settings file.
Copy the generated settings file to the Raspberry Pi
more coming soon..

View File

@@ -1,20 +1,20 @@
"""
Inkycal opwenweather API abstraction
- retrieves free weather data from OWM 2.5 API endpoints (given provided API key)
- handles unit, language and timezone conversions
- provides ready-to-use current weather, hourly and daily forecasts
Inkycal OpenWeatherMap API abstraction module
- Retrieves free weather data from OWM 2.5/3.0 API endpoints (with provided API key)
- Handles temperature and wind unit conversions
- Converts data to a standardized timezone and language
- Returns ready-to-use weather structures for current, hourly, and daily forecasts
"""
import json
import logging
from datetime import datetime
from datetime import timedelta
from typing import Dict
from typing import List
from typing import Literal
from datetime import datetime, timedelta
from typing import Dict, List, Literal
import requests
from dateutil import tz
# Type annotations for strict typing
TEMP_UNITS = Literal["celsius", "fahrenheit"]
WIND_UNITS = Literal["meters_sec", "km_hour", "miles_hour", "knots", "beaufort"]
WEATHER_TYPE = Literal["current", "forecast"]
@@ -27,15 +27,16 @@ logger.setLevel(level=logging.INFO)
def is_timestamp_within_range(timestamp: datetime, start_time: datetime, end_time: datetime) -> bool:
# Check if the timestamp is within the range
"""Check if the given timestamp lies between start_time and end_time."""
return start_time <= timestamp <= end_time
def get_json_from_url(request_url):
"""Performs an HTTP GET request and returns the parsed JSON response."""
response = requests.get(request_url)
if not response.ok:
raise AssertionError(
f"Failure getting the current weather: code {response.status_code}. Reason: {response.text}"
f"Failure getting weather: code {response.status_code}. Reason: {response.text}"
)
return json.loads(response.text)
@@ -44,280 +45,162 @@ class OpenWeatherMap:
def __init__(self, api_key: str, city_id: int = None, lat: float = None, lon: float = None,
api_version: API_VERSIONS = "2.5", temp_unit: TEMP_UNITS = "celsius",
wind_unit: WIND_UNITS = "meters_sec", language: str = "en", tz_name: str = "UTC") -> None:
"""
Initializes the OWM wrapper with localization settings.
Chooses API version, units, location and timezone preferences.
"""
self.api_key = api_key
self.temp_unit = temp_unit
self.wind_unit = wind_unit
self.language = language
self._api_version = api_version
if self._api_version == "3.0":
assert type(lat) is float and type(lon) is float
self.location_substring = (
f"lat={str(lat)}&lon={str(lon)}" if (lat is not None and lon is not None) else f"id={str(city_id)}"
)
self.tz_zone = tz.gettz(tz_name)
logger.info(
f"OWM wrapper initialized for API version {self._api_version}, language {self.language} and timezone {tz_name}."
if self._api_version == "3.0":
assert isinstance(lat, float) and isinstance(lon, float)
self.location_substring = (
f"lat={lat}&lon={lon}" if (lat and lon) else f"id={city_id}"
)
self.tz_zone = tz.gettz(tz_name)
logger.info(f"OWM wrapper initialized with API v{self._api_version}, lang={language}, tz={tz_name}.")
def get_weather_data_from_owm(self, weather: WEATHER_TYPE):
# Gets current weather or forecast from the configured OWM API.
"""Gets either current or forecast weather data."""
if weather == "current":
# Gets current weather status from the 2.5 API: https://openweathermap.org/current
# This is primarily using the 2.5 API since the 3.0 API actually has less info
weather_url = f"{API_BASE_URL}/2.5/weather?{self.location_substring}&appid={self.api_key}&units=Metric&lang={self.language}"
weather_data = get_json_from_url(weather_url)
# Only if we do have a 3.0 API-enabled key, we can also get the UVI reading from that endpoint: https://openweathermap.org/api/one-call-3
url = f"{API_BASE_URL}/2.5/weather?{self.location_substring}&appid={self.api_key}&units=Metric&lang={self.language}"
data = get_json_from_url(url)
if self._api_version == "3.0":
weather_url = f"{API_BASE_URL}/3.0/onecall?{self.location_substring}&appid={self.api_key}&exclude=minutely,hourly,daily&units=Metric&lang={self.language}"
weather_data["uvi"] = get_json_from_url(weather_url)["current"]["uvi"]
uvi_url = f"{API_BASE_URL}/3.0/onecall?{self.location_substring}&appid={self.api_key}&exclude=minutely,hourly,daily&units=Metric&lang={self.language}"
data["uvi"] = get_json_from_url(uvi_url)["current"].get("uvi")
elif weather == "forecast":
# Gets weather forecasts from the 2.5 API: https://openweathermap.org/forecast5
# This is only using the 2.5 API since the 3.0 API actually has less info
weather_url = f"{API_BASE_URL}/2.5/forecast?{self.location_substring}&appid={self.api_key}&units=Metric&lang={self.language}"
weather_data = get_json_from_url(weather_url)["list"]
return weather_data
url = f"{API_BASE_URL}/2.5/forecast?{self.location_substring}&appid={self.api_key}&units=Metric&lang={self.language}"
data = get_json_from_url(url)["list"]
return data
def get_current_weather(self) -> Dict:
"""
Decodes the OWM current weather data for our purposes
:return:
Current weather as dictionary
Fetches and processes current weather data.
Includes gust fallback and unit conversions.
"""
data = self.get_weather_data_from_owm("current")
wind_data = data.get("wind", {})
base_speed = wind_data.get("speed", 0.0)
gust_speed = wind_data.get("gust", base_speed)
converted_gust = self.get_converted_windspeed(gust_speed)
current_data = self.get_weather_data_from_owm(weather="current")
weather = {
"detailed_status": data["weather"][0]["description"],
"weather_icon_name": data["weather"][0]["icon"],
"temp": self.get_converted_temperature(data["main"]["temp"]),
"temp_feels_like": self.get_converted_temperature(data["main"]["feels_like"]),
"min_temp": self.get_converted_temperature(data["main"]["temp_min"]),
"max_temp": self.get_converted_temperature(data["main"]["temp_max"]),
"humidity": data["main"]["humidity"],
"wind": converted_gust,
"wind_gust": converted_gust,
"uvi": data.get("uvi"),
"sunrise": datetime.fromtimestamp(data["sys"]["sunrise"], tz=self.tz_zone),
"sunset": datetime.fromtimestamp(data["sys"]["sunset"], tz=self.tz_zone),
}
current_weather = {}
current_weather["detailed_status"] = current_data["weather"][0]["description"]
current_weather["weather_icon_name"] = current_data["weather"][0]["icon"]
current_weather["temp"] = self.get_converted_temperature(
current_data["main"]["temp"]
) # OWM Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit
current_weather["temp_feels_like"] = self.get_converted_temperature(current_data["main"]["feels_like"])
current_weather["min_temp"] = self.get_converted_temperature(current_data["main"]["temp_min"])
current_weather["max_temp"] = self.get_converted_temperature(current_data["main"]["temp_max"])
current_weather["humidity"] = current_data["main"]["humidity"] # OWM Unit: % rH
current_weather["wind"] = self.get_converted_windspeed(
current_data["wind"]["speed"]
) # OWM Unit Default: meter/sec, Metric: meter/sec
if "gust" in current_data["wind"]:
current_weather["wind_gust"] = self.get_converted_windspeed(current_data["wind"]["gust"])
else:
logger.info(
f"OpenWeatherMap response did not contain a wind gust speed. Using base wind: {current_weather['wind']} m/s."
)
current_weather["wind_gust"] = current_weather["wind"]
if "uvi" in current_data: # this is only supported in v3.0 API
current_weather["uvi"] = current_data["uvi"]
else:
current_weather["uvi"] = None
current_weather["sunrise"] = datetime.fromtimestamp(
current_data["sys"]["sunrise"], tz=self.tz_zone
) # unix timestamp -> to our timezone
current_weather["sunset"] = datetime.fromtimestamp(current_data["sys"]["sunset"], tz=self.tz_zone)
self.current_weather = current_weather
return current_weather
return weather
def get_weather_forecast(self) -> List[Dict]:
"""
Decodes the OWM weather forecast for our purposes
What you get is a list of 40 forecasts for 3-hour time slices, totaling to 5 days.
:return:
Forecasts data dictionary
Parses OWM 5-day / 3-hour forecast into a list of hourly dictionaries.
"""
#
forecast_data = self.get_weather_data_from_owm(weather="forecast")
forecasts = self.get_weather_data_from_owm("forecast")
hourly = []
# Add forecast data to hourly_data_dict list of dictionaries
hourly_forecasts = []
for forecast in forecast_data:
# calculate combined precipitation (snow + rain)
precip_mm = 0.0
if "rain" in forecast.keys():
precip_mm = +forecast["rain"]["3h"] # OWM Unit: mm
if "snow" in forecast.keys():
precip_mm = +forecast["snow"]["3h"] # OWM Unit: mm
hourly_forecasts.append(
{
"temp": self.get_converted_temperature(
forecast["main"]["temp"]
), # OWM Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit
"min_temp": self.get_converted_temperature(forecast["main"]["temp_min"]),
"max_temp": self.get_converted_temperature(forecast["main"]["temp_max"]),
"precip_3h_mm": precip_mm,
"wind": self.get_converted_windspeed(
forecast["wind"]["speed"]
), # OWM Unit Default: meter/sec, Metric: meter/sec, Imperial: miles/hour
"wind_gust": self.get_converted_windspeed(forecast["wind"]["gust"]),
"pressure": forecast["main"]["pressure"], # OWM Unit: hPa
"humidity": forecast["main"]["humidity"], # OWM Unit: % rH
"precip_probability": forecast["pop"]
* 100.0, # OWM value is unitless, directly converting to % scale
"icon": forecast["weather"][0]["icon"],
"datetime": datetime.fromtimestamp(forecast["dt"], tz=self.tz_zone),
}
)
logger.debug(
f"Added rain forecast at {datetime.fromtimestamp(forecast['dt'], tz=self.tz_zone)}: {precip_mm}"
)
for f in forecasts:
rain = f.get("rain", {}).get("3h", 0.0)
snow = f.get("snow", {}).get("3h", 0.0)
precip_mm = rain + snow
self.hourly_forecasts = hourly_forecasts
hourly.append({
"temp": self.get_converted_temperature(f["main"]["temp"]),
"min_temp": self.get_converted_temperature(f["main"]["temp_min"]),
"max_temp": self.get_converted_temperature(f["main"]["temp_max"]),
"precip_3h_mm": precip_mm,
"wind": self.get_converted_windspeed(f["wind"]["speed"]),
"wind_gust": self.get_converted_windspeed(f["wind"].get("gust", f["wind"]["speed"])),
"pressure": f["main"]["pressure"],
"humidity": f["main"]["humidity"],
"precip_probability": f.get("pop", 0.0) * 100.0,
"icon": f["weather"][0]["icon"],
"datetime": datetime.fromtimestamp(f["dt"], tz=self.tz_zone),
})
return self.hourly_forecasts
return hourly
def get_forecast_for_day(self, days_from_today: int) -> Dict:
"""
Get temperature range, rain and most frequent icon code
for the day that is days_from_today away.
"Today" is based on our local system timezone.
:param days_from_today:
should be int from 0-4: e.g. 2 -> 2 days from today
:return:
Forecast dictionary
Aggregates hourly data into daily summary with min/max temp, precip and icon.
"""
# Make sure hourly forecasts are up-to-date
_ = self.get_weather_forecast()
forecasts = self.get_weather_forecast()
now = datetime.now(tz=self.tz_zone)
start = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=days_from_today)
end = start + timedelta(days=1)
# Calculate the start and end times for the specified number of days from now
current_time = datetime.now(tz=self.tz_zone)
start_time = (
(current_time + timedelta(days=days_from_today))
.replace(hour=0, minute=0, second=0, microsecond=0)
.astimezone(tz=self.tz_zone)
)
end_time = (start_time + timedelta(days=1)).astimezone(tz=self.tz_zone)
daily = [f for f in forecasts if start <= f["datetime"] < end]
if not daily:
daily.append(forecasts[0]) # fallback to first forecast
# Get all the forecasts for that day's time range
forecasts = [
f
for f in self.hourly_forecasts
if is_timestamp_within_range(timestamp=f["datetime"], start_time=start_time, end_time=end_time)
]
temps = [f["temp"] for f in daily]
rain = sum(f["precip_3h_mm"] for f in daily)
icons = [f["icon"] for f in daily if f["icon"]]
icon = max(set(icons), key=icons.count)
# In case the next available forecast is already for the next day, use that one for the less than 3 remaining hours of today
if not forecasts:
forecasts.append(self.hourly_forecasts[0])
# Get rain and temperatures for that day
temps = [f["temp"] for f in forecasts]
rain = sum([f["precip_3h_mm"] for f in forecasts])
# Get all weather icon codes for this day
icons = [f["icon"] for f in forecasts]
day_icons = [icon for icon in icons if "d" in icon]
# Use the day icons if possible
icon = max(set(day_icons), key=icons.count) if len(day_icons) > 0 else max(set(icons), key=icons.count)
# Return a dict with that day's data
day_data = {
"datetime": start_time,
return {
"datetime": start,
"icon": icon,
"temp_min": min(temps),
"temp_max": max(temps),
"precip_mm": rain,
"precip_mm": rain
}
return day_data
def get_converted_temperature(self, value: float) -> float:
if self.temp_unit == "fahrenheit":
value = self.celsius_to_fahrenheit(value)
return value
return self.celsius_to_fahrenheit(value) if self.temp_unit == "fahrenheit" else value
def get_converted_windspeed(self, value: float) -> float:
Literal["meters_sec", "km_hour", "miles_hour", "knots", "beaufort"]
if self.wind_unit == "km_hour":
value = self.celsius_to_fahrenheit(value)
elif self.wind_unit == "km_hour":
value = self.mps_to_kph(value)
elif self.wind_unit == "miles_hour":
value = self.mps_to_mph(value)
elif self.wind_unit == "knots":
value = self.mps_to_knots(value)
elif self.wind_unit == "beaufort":
value = self.mps_to_beaufort(value)
return value
return self.mps_to_kph(value)
if self.wind_unit == "miles_hour":
return self.mps_to_mph(value)
if self.wind_unit == "knots":
return self.mps_to_knots(value)
if self.wind_unit == "beaufort":
return self.mps_to_beaufort(value)
return value # default is meters/sec
@staticmethod
def mps_to_beaufort(meters_per_second: float) -> int:
"""Map meters per second to the beaufort scale.
Args:
meters_per_second:
float representing meters per seconds
Returns:
an integer of the beaufort scale mapping the input
"""
def mps_to_beaufort(mps: float) -> int:
thresholds = [0.3, 1.6, 3.4, 5.5, 8.0, 10.8, 13.9, 17.2, 20.8, 24.5, 28.5, 32.7]
return next((i for i, threshold in enumerate(thresholds) if meters_per_second < threshold), 12)
return next((i for i, t in enumerate(thresholds) if mps < t), 12)
@staticmethod
def mps_to_mph(meters_per_second: float) -> float:
"""Map meters per second to miles per hour
Args:
meters_per_second:
float representing meters per seconds.
Returns:
float representing the input value in miles per hour.
"""
# 1 m/s is approximately equal to 2.23694 mph
miles_per_hour = meters_per_second * 2.23694
return miles_per_hour
def mps_to_mph(mps: float) -> float:
return mps * 2.23694
@staticmethod
def mps_to_kph(meters_per_second: float) -> float:
"""Map meters per second to kilometers per hour
Args:
meters_per_second:
float representing meters per seconds.
Returns:
float representing the input value in kilometers per hour.
"""
# 1 m/s is equal to 3.6 km/h
kph = meters_per_second * 3.6
return kph
def mps_to_kph(mps: float) -> float:
return mps * 3.6
@staticmethod
def mps_to_knots(meters_per_second: float) -> float:
"""Map meters per second to knots (nautical miles per hour)
Args:
meters_per_second:
float representing meters per seconds.
Returns:
float representing the input value in knots.
"""
# 1 m/s is equal to 1.94384 knots
knots = meters_per_second * 1.94384
return knots
def mps_to_knots(mps: float) -> float:
return mps * 1.94384
@staticmethod
def celsius_to_fahrenheit(celsius: int or float) -> float:
"""Converts the given temperate from degrees Celsius to Fahrenheit."""
fahrenheit = (float(celsius) * 9.0 / 5.0) + 32.0
return fahrenheit
def celsius_to_fahrenheit(c: float) -> float:
return c * 9.0 / 5.0 + 32.0
def main():
"""Main function, only used for testing purposes"""
# Simple test entry point
key = ""
city = 2643743
lang = "de"
owm = OpenWeatherMap(api_key=key, city_id=city, language=lang, tz="Europe/Berlin")
current_weather = owm.get_current_weather()
print(current_weather)
_ = owm.get_weather_forecast()
city = 2643743 # London
owm = OpenWeatherMap(api_key=key, city_id=city, language="de", tz_name="Europe/Berlin")
print(owm.get_current_weather())
print(owm.get_forecast_for_day(days_from_today=2))

View File

@@ -165,7 +165,12 @@ class Inkycal:
self.pisugar = PiSugar()
self.battery_capacity = self.pisugar.get_battery()
logger.info(f"PiSugar battery capacity: {self.battery_capacity}%")
if not self.battery_capacity:
logger.warning("[PISUGAR] Could not get battery capacity! Is the board off? Setting battery capacity to 0%")
self.battery_capacity = 100
else:
logger.info(f"PiSugar battery capacity: {self.battery_capacity}%")
if self.battery_capacity < 20:
logger.warning("Battery capacity is below 20%!")
@@ -342,8 +347,12 @@ class Inkycal:
logger.info("All images generated successfully!")
del errors
if self.battery_capacity < 20:
self.info += "Low battery! "
if self.use_pi_sugar:
self.battery_capacity = self.pisugar.get_battery() or 0
if self.battery_capacity < 20:
self.info += f"Low battery! ({self.battery_capacity})% "
else:
self.info += f"Battery: {self.battery_capacity}% "
# Assemble image from each module - add info section if specified
self._assemble()

7
inkycal/modules/inky_image.py Executable file → Normal file
View File

@@ -169,6 +169,13 @@ class Inkyimage:
logger.error("no height of width specified")
return
current_width, current_height = self.image.size
# Skip if dimensions are the same
if width == current_width and height == current_height:
logger.info(f"Image already correct size ({width}x{height}), skipping resize")
return
image = self.image
if width:

View File

@@ -114,7 +114,7 @@ class Feeds(inkycal_module):
# if "description" in posts:
if parsed_feeds:
parsed_feeds = [i.split("\n") for i in parsed_feeds][0]
parsed_feeds = [i.split("\n") for i in parsed_feeds]
parsed_feeds = [i for i in parsed_feeds if i]
# Shuffle the list to prevent showing the same content
@@ -129,7 +129,7 @@ class Feeds(inkycal_module):
filtered_feeds, counter = [], 0
for posts in parsed_feeds:
wrapped = text_wrap(posts, font=self.font, max_width=line_width)
wrapped = text_wrap(posts[0], font=self.font, max_width=line_width)
counter += len(wrapped)
if counter < max_lines:
filtered_feeds.append(wrapped)

View File

@@ -78,13 +78,17 @@ class TextToDisplay(inkycal_module):
with open(self.filepath, 'r') as file:
file_content = file.read()
fitted_content = text_wrap(file_content, font=self.font, max_width=im_width)
# Split content by lines if not making a request
if not self.make_request:
lines = file_content.split('\n')
else:
lines = text_wrap(file_content, font=self.font, max_width=im_width)
# Trim down the list to the max number of lines
del fitted_content[max_lines:]
del lines[max_lines:]
# Write feeds on image
for index, line in enumerate(fitted_content):
for index, line in enumerate(lines):
write(
im_black,
line_positions[index],

View File

@@ -3,11 +3,16 @@ Inkycal Todoist Module
Copyright by aceinnolab
"""
import arrow
import json
import os
import time
from datetime import datetime
from inkycal.modules.template import inkycal_module
from inkycal.custom import *
from todoist_api_python.api import TodoistAPI
import requests.exceptions
logger = logging.getLogger(__name__)
@@ -29,6 +34,10 @@ class Todoist(inkycal_module):
'project_filter': {
"label": "Show Todos only from following project (separated by a comma). Leave empty to show " +
"todos from all projects",
},
'show_priority': {
"label": "Show priority indicators for tasks (P1, P2, P3)",
"default": True
}
}
@@ -53,8 +62,15 @@ class Todoist(inkycal_module):
else:
self.project_filter = config['project_filter']
# Priority display option
self.show_priority = config.get('show_priority', True)
self._api = TodoistAPI(config['api_key'])
# Cache file path for storing last successful response
self.cache_file = os.path.join(os.path.dirname(__file__), '..', '..', 'temp', 'todoist_cache.json')
os.makedirs(os.path.dirname(self.cache_file), exist_ok=True)
# give an OK message
logger.debug(f'{__name__} loaded')
@@ -63,6 +79,93 @@ class Todoist(inkycal_module):
if not isinstance(self.api_key, str):
print('api_key has to be a string: "Yourtopsecretkey123" ')
def _fetch_with_retry(self, fetch_func, max_retries=3):
"""Fetch data with retry logic and exponential backoff"""
for attempt in range(max_retries):
try:
return fetch_func()
except requests.exceptions.HTTPError as e:
if e.response.status_code in [502, 503, 504]: # Retry on server errors
if attempt < max_retries - 1:
delay = (2 ** attempt) # Exponential backoff: 1s, 2s, 4s
logger.warning(f"API request failed (attempt {attempt + 1}/{max_retries}), retrying in {delay}s...")
time.sleep(delay)
continue
raise
except requests.exceptions.ConnectionError:
if attempt < max_retries - 1:
delay = (2 ** attempt)
logger.warning(f"Connection error (attempt {attempt + 1}/{max_retries}), retrying in {delay}s...")
time.sleep(delay)
continue
raise
raise Exception("Max retries exceeded")
def _save_cache(self, projects, tasks):
"""Save API response to cache file"""
try:
cache_data = {
'timestamp': datetime.now().isoformat(),
'projects': [{'id': p.id, 'name': p.name} for p in projects],
'tasks': [{
'content': t.content,
'project_id': t.project_id,
'priority': t.priority,
'due': {'date': t.due.date} if t.due else None
} for t in tasks]
}
with open(self.cache_file, 'w') as f:
json.dump(cache_data, f)
logger.debug("Saved Todoist data to cache")
except Exception as e:
logger.warning(f"Failed to save cache: {e}")
def _load_cache(self):
"""Load cached API response"""
try:
if os.path.exists(self.cache_file):
with open(self.cache_file, 'r') as f:
return json.load(f)
except Exception as e:
logger.warning(f"Failed to load cache: {e}")
return None
def _create_error_image(self, im_size, error_msg=None, cached_data=None):
"""Create an error message image when API fails"""
im_width, im_height = im_size
im_black = Image.new('RGB', size=im_size, color='white')
im_colour = Image.new('RGB', size=im_size, color='white')
# Display error message
line_spacing = 1
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
messages = []
if error_msg:
messages.append("Todoist temporarily unavailable")
if cached_data and 'timestamp' in cached_data:
timestamp = arrow.get(cached_data['timestamp']).format('D-MMM-YY HH:mm')
messages.append(f"Showing cached data from:")
messages.append(timestamp)
else:
messages.append("No cached data available")
messages.append("Please check your connection")
# Center the messages vertically
total_height = len(messages) * line_height
start_y = (im_height - total_height) // 2
for i, msg in enumerate(messages):
y_pos = start_y + (i * line_height)
# First line in red (colour image), rest in black
target_image = im_colour if i == 0 else im_black
write(target_image, (0, y_pos), (im_width, line_height),
msg, font=self.font, alignment='center')
return im_black, im_colour
def generate_image(self):
"""Generate image for this module"""
@@ -77,11 +180,45 @@ class Todoist(inkycal_module):
im_colour = Image.new('RGB', size=im_size, color='white')
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
if not internet_available():
logger.error("Network not reachable. Trying to use cached data.")
cached_data = self._load_cache()
if cached_data:
# Process cached data below
all_projects = [type('Project', (), p) for p in cached_data['projects']]
all_active_tasks = [type('Task', (), {
'content': t['content'],
'project_id': t['project_id'],
'priority': t['priority'],
'due': type('Due', (), {'date': t['due']['date']}) if t['due'] else None
}) for t in cached_data['tasks']]
else:
return self._create_error_image(im_size, "Network error", None)
else:
logger.error("Network not reachable. Please check your connection.")
raise NetworkNotReachableError
logger.info('Connection test passed')
# Try to fetch fresh data from API
try:
all_projects = self._fetch_with_retry(self._api.get_projects)
all_active_tasks = self._fetch_with_retry(self._api.get_tasks)
# Save to cache on successful fetch
self._save_cache(all_projects, all_active_tasks)
except Exception as e:
logger.error(f"Failed to fetch Todoist data: {e}")
# Try to use cached data
cached_data = self._load_cache()
if cached_data:
logger.info("Using cached Todoist data")
all_projects = [type('Project', (), p) for p in cached_data['projects']]
all_active_tasks = [type('Task', (), {
'content': t['content'],
'project_id': t['project_id'],
'priority': t['priority'],
'due': type('Due', (), {'date': t['due']['date']}) if t['due'] else None
}) for t in cached_data['tasks']]
else:
# No cached data available, show error
return self._create_error_image(im_size, str(e), None)
# Set some parameters for formatting todos
line_spacing = 1
@@ -97,10 +234,8 @@ class Todoist(inkycal_module):
line_positions = [
(0, spacing_top + _ * line_height) for _ in range(max_lines)]
# Get all projects by name and id
all_projects = self._api.get_projects()
# Process the fetched or cached data
filtered_project_ids_and_names = {project.id: project.name for project in all_projects}
all_active_tasks = self._api.get_tasks()
logger.debug(f"all_projects: {all_projects}")
print(f"all_projects: {all_projects}")
@@ -126,26 +261,57 @@ class Todoist(inkycal_module):
all_active_tasks = [task for task in all_active_tasks if task.project_id in filtered_project_ids]
# Simplify the tasks for faster processing
simplified = [
{
simplified = []
for task in all_active_tasks:
# Format priority indicator using circle symbols
priority_text = ""
if self.show_priority and task.priority > 1:
# Todoist uses reversed priority (4 = highest, 1 = lowest)
if task.priority == 4: # P1 - filled circle (red)
priority_text = "" # Filled circle for highest priority
elif task.priority == 3: # P2 - filled circle (black)
priority_text = "" # Filled circle for high priority
elif task.priority == 2: # P3 - empty circle (black)
priority_text = "" # Empty circle for medium priority
# Check if task is overdue
# Parse date in local timezone to ensure correct comparison
due_date = arrow.get(task.due.date, "YYYY-MM-DD").replace(tzinfo='local') if task.due else None
today = arrow.now('local').floor('day')
is_overdue = due_date and due_date < today if due_date else False
# Format due date display
if due_date:
if due_date.floor('day') == today:
due_display = "TODAY"
else:
due_display = due_date.format("D-MMM-YY")
else:
due_display = ""
simplified.append({
'name': task.content,
'due': arrow.get(task.due.date, "YYYY-MM-DD").format("D-MMM-YY") if task.due else "",
'due': due_display,
'due_date': due_date,
'is_overdue': is_overdue,
'priority': task.priority,
'priority_text': priority_text,
'project': filtered_project_ids_and_names[task.project_id]
}
for task in all_active_tasks
]
})
logger.debug(f'simplified: {simplified}')
project_lengths = []
due_lengths = []
priority_lengths = []
for task in simplified:
if task["project"]:
project_lengths.append(int(self.font.getlength(task['project']) * 1.1))
if task["due"]:
due_lengths.append(int(self.font.getlength(task['due']) * 1.1))
if task["priority_text"]:
priority_lengths.append(int(self.font.getlength(task['priority_text']) * 1.1))
# Get maximum width of project names for selected font
project_offset = int(max(project_lengths)) if project_lengths else 0
@@ -153,6 +319,9 @@ class Todoist(inkycal_module):
# Get maximum width of project dues for selected font
due_offset = int(max(due_lengths)) if due_lengths else 0
# Get maximum width of priority indicators
priority_offset = int(max(priority_lengths)) if priority_lengths else 0
# create a dict with names of filtered groups
groups = {group_name:[] for group_name in filtered_project_ids_and_names.values()}
for task in simplified:
@@ -160,6 +329,16 @@ class Todoist(inkycal_module):
if group_of_current_task in groups:
groups[group_of_current_task].append(task)
# Sort tasks within each project group by due date first, then priority
for project_name in groups:
groups[project_name].sort(
key=lambda task: (
task['due_date'] is None, # Tasks with dates come first
task['due_date'] if task['due_date'] else arrow.get('9999-12-31'), # Sort by date
-task['priority'] # Then by priority (higher priority first)
)
)
logger.debug(f"grouped: {groups}")
# Add the parsed todos on the image
@@ -179,18 +358,30 @@ class Todoist(inkycal_module):
# Add todos due if not empty
if todo['due']:
# Show overdue dates in red, normal dates in black
due_image = im_colour if todo.get('is_overdue', False) else im_black
write(
im_black,
due_image,
(line_x + project_offset, line_y),
(due_offset, line_height),
todo['due'], font=self.font, alignment='left')
# Add priority indicator if present
if todo['priority_text']:
# P1 (priority 4) in red, P2 and P3 in black
priority_image = im_colour if todo['priority'] == 4 else im_black
write(
priority_image,
(line_x + project_offset + due_offset, line_y),
(priority_offset, line_height),
todo['priority_text'], font=self.font, alignment='left')
if todo['name']:
# Add todos name
write(
im_black,
(line_x + project_offset + due_offset, line_y),
(im_width - project_offset - due_offset, line_height),
(line_x + project_offset + due_offset + priority_offset, line_y),
(im_width - project_offset - due_offset - priority_offset, line_height),
todo['name'], font=self.font, alignment='left')
cursor += 1

View File

@@ -1,54 +1,52 @@
appdirs==1.4.4
arrow==1.3.0
asyncio==3.4.3
beautifulsoup4==4.12.3
certifi==2024.7.4
beautifulsoup4==4.13.4
certifi==2025.8.3
cfgv==3.4.0
charset-normalizer==3.3.2
charset-normalizer==3.4.3
colorzero==2.0
cycler==0.12.1
distlib==0.3.8
distlib==0.4.0
feedparser==6.0.11
filelock==3.13.1
fonttools==4.48.1
frozendict==2.4.0
gpiozero==2.0
html2text==2020.1.16
filelock==3.18.0
fonttools==4.59.0
frozendict==2.4.6
gpiozero==2.0.1
html2text==2025.4.15
html5lib==1.1
htmlwebshot==0.1.2
icalendar==5.0.11
identify==2.5.34
idna==3.7
kiwisolver==1.4.5
lgpio==0.0.0.2
matplotlib==3.7.1
multitasking==0.0.11
nodeenv==1.8.0
numpy==1.26.2
packaging==23.2
pandas==2.2.0
peewee==3.17.1
pillow==10.3.0
platformdirs==4.2.0
pre-commit==3.6.1
pyparsing==3.1.1
python-dateutil==2.8.2
python-dotenv==1.0.1
pytz==2024.1
PyYAML==6.0.1
recurring-ical-events==2.1.2
requests==2.32.3
icalendar==6.3.1
identify==2.6.13
idna==3.10
kiwisolver==1.4.9
matplotlib==3.10.5
multitasking==0.0.12
nodeenv==1.9.1
numpy==2.3.2
packaging==25.0
pandas==2.3.1
peewee==3.18.2
pillow==11.3.0
platformdirs==4.3.8
pre-commit==4.3.0
pyparsing==3.2.3
python-dateutil==2.9.0
python-dotenv==1.1.1
pytz==2025.2
PyYAML==6.0.2
recurring-ical-events==3.8.0
requests==2.32.4
sgmllib3k==1.0.0
six==1.16.0
soupsieve==2.5
todoist-api-python==2.1.3
types-python-dateutil==2.8.19.20240106
typing_extensions==4.9.0
tzdata==2024.1
tzlocal==5.2
urllib3==2.2.2
virtualenv==20.25.0
six==1.17.0
soupsieve==2.7
todoist-api-python==3.1.0
types-python-dateutil==2.9.0.20250809
typing_extensions==4.14.1
tzdata==2025.2
tzlocal==5.3.1
urllib3==2.5.0
virtualenv==20.34.0
webencodings==0.5.1
x-wr-timezone==0.0.6
x-wr-timezone==2.0.1
xkcd==2.4.2
yfinance==0.2.40
yfinance==0.2.65

View File

@@ -17,7 +17,7 @@ __version__ = "2.0.4"
__description__ = "Inkycal is a python3 software for syncing icalendar events, weather and news on selected E-Paper displays"
__packages__ = ["inkycal"]
__author__ = "aceinnolab"
__author_email__ = "aceisace63@yahoo.com"
__author_email__ = "inkycal@aceinnolab.com"
__url__ = "https://github.com/aceinnolab/Inkycal"
__install_requires__ = required

645
test_display.py Normal file
View File

@@ -0,0 +1,645 @@
#!/usr/bin/env python3
"""
Universal E-Paper Display Test Script for Inkycal
Tests displays with various patterns for validation
Supports both color (3-color) and black/white displays
"""
import sys
import time
import json
import argparse
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
import logging
sys.path.insert(0, str(Path(__file__).parent))
from inkycal.display.display import Display
from inkycal.display.supported_models import supported_models
from inkycal.settings import Settings
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class UniversalDisplayTest:
def __init__(self, model=None, auto_detect=True):
"""Initialize the display test class
Args:
model: Display model name (optional)
auto_detect: Try to detect from settings.json if model not provided
"""
self.model = model
self.display = None
self.width = None
self.height = None
self.is_colour = False
# Color definitions
self.WHITE = (255, 255, 255)
self.BLACK = (0, 0, 0)
self.RED = (255, 0, 0)
# Auto-detect model if needed
if not self.model and auto_detect:
self.model = self._auto_detect_model()
if not self.model:
raise ValueError("No display model specified. Use --model or ensure settings.json exists")
# Validate and get display info
self._validate_model()
# Initialize display
self._init_display()
def _auto_detect_model(self):
"""Try to detect display model from settings.json"""
for settings_path in Settings.SETTINGS_JSON_PATHS:
settings_file = Path(settings_path)
if settings_file.exists():
try:
with open(settings_file, 'r') as f:
settings = json.load(f)
model = settings.get('model')
if model:
logger.info(f"Auto-detected model '{model}' from {settings_path}")
return model
except Exception as e:
logger.warning(f"Could not read {settings_path}: {e}")
logger.warning("Could not auto-detect display model from settings.json")
return None
def _validate_model(self):
"""Validate the display model and get its properties"""
if self.model not in supported_models:
logger.error(f"Model '{self.model}' not supported")
logger.info("Supported models:")
for model_name in sorted(supported_models.keys()):
width, height = supported_models[model_name]
color_info = " (colour)" if "colour" in model_name.lower() else ""
logger.info(f" - {model_name}: {width}x{height}{color_info}")
raise ValueError(f"Unsupported model: {self.model}")
# Get display dimensions
self.width, self.height = supported_models[self.model]
# Check if it's a color display
self.is_colour = "colour" in self.model.lower() or "color" in self.model.lower()
logger.info(f"Display model: {self.model}")
logger.info(f"Resolution: {self.width}x{self.height}")
logger.info(f"Type: {'Colour (3-color)' if self.is_colour else 'Black/White'}")
def _init_display(self):
"""Initialize the display hardware"""
try:
logger.info(f"Initializing {self.model} display...")
self.display = Display(self.model)
logger.info(f"Display initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize display: {e}")
raise
def clear_display(self):
"""Clear the display to white"""
logger.info("Clearing display...")
black_img = Image.new('RGB', (self.width, self.height), self.WHITE)
if self.is_colour:
red_img = Image.new('RGB', (self.width, self.height), self.WHITE)
self.display.render(black_img, red_img)
else:
self.display.render(black_img)
logger.info("Display cleared")
def test_solid_colors(self):
"""Test solid color fills"""
logger.info("Testing solid colors...")
# Test 1: Full black screen
logger.info("Test 1: Full black screen")
black_img = Image.new('RGB', (self.width, self.height), self.BLACK)
if self.is_colour:
red_img = Image.new('RGB', (self.width, self.height), self.WHITE)
self.display.render(black_img, red_img)
else:
self.display.render(black_img)
time.sleep(5)
# Test 2: Full white screen
logger.info("Test 2: Full white screen")
black_img = Image.new('RGB', (self.width, self.height), self.WHITE)
if self.is_colour:
red_img = Image.new('RGB', (self.width, self.height), self.WHITE)
self.display.render(black_img, red_img)
else:
self.display.render(black_img)
time.sleep(5)
# Test 3: Full red screen (color displays only)
if self.is_colour:
logger.info("Test 3: Full red/color screen")
black_img = Image.new('RGB', (self.width, self.height), self.WHITE)
red_img = Image.new('RGB', (self.width, self.height), self.RED)
self.display.render(black_img, red_img)
time.sleep(5)
def test_color_sections(self):
"""Test display with color sections"""
if self.is_colour:
logger.info("Testing color sections (thirds)...")
else:
logger.info("Testing black/white sections (halves)...")
# Create images
black_img = Image.new('RGB', (self.width, self.height), self.WHITE)
draw_black = ImageDraw.Draw(black_img)
if self.is_colour:
red_img = Image.new('RGB', (self.width, self.height), self.WHITE)
draw_red = ImageDraw.Draw(red_img)
# Divide screen into three vertical sections
section_width = self.width // 3
# Left section: Black
draw_black.rectangle([0, 0, section_width, self.height], fill=self.BLACK)
# Middle section: White (already white)
# Right section: Red
draw_red.rectangle([section_width * 2, 0, self.width, self.height], fill=self.RED)
self.display.render(black_img, red_img)
else:
# For B/W displays: half black, half white
section_width = self.width // 2
draw_black.rectangle([0, 0, section_width, self.height], fill=self.BLACK)
self.display.render(black_img)
logger.info("Color sections displayed")
time.sleep(5)
def test_checkerboard(self):
logger.info("Testing checkerboard pattern...")
black_img = Image.new('RGB', (self.width, self.height), self.WHITE)
draw_black = ImageDraw.Draw(black_img)
if self.is_colour:
red_img = Image.new('RGB', (self.width, self.height), self.WHITE)
draw_red = ImageDraw.Draw(red_img)
# Adjust square size based on display size
square_size = max(20, min(50, self.width // 20))
for y in range(0, self.height, square_size):
for x in range(0, self.width, square_size):
if self.is_colour:
# For color displays: cycle through black, white, red
pattern = ((x // square_size) + (y // square_size)) % 3
if pattern == 0:
draw_black.rectangle([x, y, x + square_size, y + square_size], fill=self.BLACK)
elif pattern == 1:
draw_red.rectangle([x, y, x + square_size, y + square_size], fill=self.RED)
else:
# For B/W displays: simple checkerboard
if ((x // square_size) + (y // square_size)) % 2 == 0:
draw_black.rectangle([x, y, x + square_size, y + square_size], fill=self.BLACK)
if self.is_colour:
self.display.render(black_img, red_img)
else:
self.display.render(black_img)
logger.info("Checkerboard pattern displayed")
time.sleep(5)
def test_geometric_shapes(self):
logger.info("Testing geometric shapes...")
black_img = Image.new('RGB', (self.width, self.height), self.WHITE)
draw_black = ImageDraw.Draw(black_img)
if self.is_colour:
red_img = Image.new('RGB', (self.width, self.height), self.WHITE)
draw_red = ImageDraw.Draw(red_img)
# Scale shapes based on display size
scale = min(self.width, self.height) / 400
# Black circle
circle_size = int(100 * scale)
draw_black.ellipse([50, 50, 50 + circle_size, 50 + circle_size], fill=self.BLACK)
# Rectangle (red for color, black for B/W)
rect_x = int(200 * scale)
rect_size = int(100 * scale)
if self.is_colour:
draw_red.rectangle([rect_x, 50, rect_x + rect_size, 50 + rect_size], fill=self.RED)
else:
draw_black.rectangle([rect_x, 50, rect_x + rect_size, 50 + rect_size], fill=self.BLACK)
# Cross lines
draw_black.line([0, self.height//2, self.width, self.height//2], fill=self.BLACK, width=3)
draw_black.line([self.width//2, 0, self.width//2, self.height], fill=self.BLACK, width=3)
# Diagonal lines (red for color displays)
if self.is_colour:
draw_red.line([0, 0, self.width, self.height], fill=self.RED, width=2)
draw_red.line([self.width, 0, 0, self.height], fill=self.RED, width=2)
else:
draw_black.line([0, 0, self.width, self.height], fill=self.BLACK, width=1)
draw_black.line([self.width, 0, 0, self.height], fill=self.BLACK, width=1)
if self.is_colour:
self.display.render(black_img, red_img)
else:
self.display.render(black_img)
logger.info("Geometric shapes displayed")
time.sleep(5)
def test_text_rendering(self):
logger.info("Testing text rendering...")
black_img = Image.new('RGB', (self.width, self.height), self.WHITE)
draw_black = ImageDraw.Draw(black_img)
if self.is_colour:
red_img = Image.new('RGB', (self.width, self.height), self.WHITE)
draw_red = ImageDraw.Draw(red_img)
# Scale font sizes based on display size
scale = min(self.width, self.height) / 400
# Try to load fonts
try:
font_small = ImageFont.truetype(f"{Settings.FONT_PATH}/NotoSans-Light.ttf", int(16 * scale))
font_medium = ImageFont.truetype(f"{Settings.FONT_PATH}/NotoSans-Regular.ttf", int(24 * scale))
font_large = ImageFont.truetype(f"{Settings.FONT_PATH}/NotoSans-Bold.ttf", int(36 * scale))
except:
logger.warning("Custom fonts not found, using default")
font_small = ImageFont.load_default()
font_medium = ImageFont.load_default()
font_large = ImageFont.load_default()
y_offset = 20
# Title
draw_black.text((20, y_offset), "E-Paper Display Test", font=font_large, fill=self.BLACK)
y_offset += int(50 * scale)
# Display info
info_text = [
f"Model: {self.model}",
f"Resolution: {self.width} x {self.height}",
f"Type: {'Colour' if self.is_colour else 'Black/White'}",
f"Time: {time.strftime('%Y-%m-%d %H:%M:%S')}"
]
for i, text in enumerate(info_text):
if self.is_colour and i % 2 == 1:
draw_red.text((20, y_offset), text, font=font_medium, fill=self.RED)
else:
draw_black.text((20, y_offset), text, font=font_medium, fill=self.BLACK)
y_offset += int(35 * scale)
# Sample text
y_offset += int(20 * scale)
draw_black.text((20, y_offset), "The quick brown fox jumps over the lazy dog",
font=font_small, fill=self.BLACK)
if self.is_colour:
y_offset += int(25 * scale)
draw_red.text((20, y_offset), "0123456789 !@#$%^&*()",
font=font_small, fill=self.RED)
if self.is_colour:
self.display.render(black_img, red_img)
else:
self.display.render(black_img)
logger.info("Text rendering displayed")
time.sleep(5)
def test_gradient_bars(self):
logger.info("Testing gradient/dither patterns...")
black_img = Image.new('RGB', (self.width, self.height), self.WHITE)
draw_black = ImageDraw.Draw(black_img)
if self.is_colour:
red_img = Image.new('RGB', (self.width, self.height), self.WHITE)
draw_red = ImageDraw.Draw(red_img)
# Create horizontal bars with different patterns
num_bars = 4 if self.is_colour else 3
bar_height = self.height // num_bars
# Bar 1: Dithered black gradient
for x in range(self.width):
if x % 2 == 0 or x < self.width // 3:
draw_black.line([(x, 0), (x, bar_height)], fill=self.BLACK)
# Bar 2: For color displays, dithered red
if self.is_colour:
for x in range(self.width):
if x % 2 == 0 or x < self.width // 3:
draw_red.line([(x, bar_height), (x, bar_height * 2)], fill=self.RED)
bar_start = 2
else:
bar_start = 1
# Vertical stripes
stripe_width = 10
for x in range(0, self.width, stripe_width * 2):
draw_black.rectangle([x, bar_height * bar_start, x + stripe_width, bar_height * (bar_start + 1)],
fill=self.BLACK)
# Fine checkerboard at bottom
for x in range(0, self.width, 4):
for y in range(bar_height * (num_bars - 1), self.height, 4):
if ((x // 4) + (y // 4)) % 2 == 0:
draw_black.rectangle([x, y, x + 4, y + 4], fill=self.BLACK)
if self.is_colour:
self.display.render(black_img, red_img)
else:
self.display.render(black_img)
logger.info("Gradient/dither patterns displayed")
time.sleep(5)
def test_calibration_pattern(self):
"""Display calibration pattern for alignment testing"""
logger.info("Testing calibration pattern...")
black_img = Image.new('RGB', (self.width, self.height), self.WHITE)
draw_black = ImageDraw.Draw(black_img)
if self.is_colour:
red_img = Image.new('RGB', (self.width, self.height), self.WHITE)
draw_red = ImageDraw.Draw(red_img)
# Draw border
draw_black.rectangle([0, 0, self.width-1, self.height-1], outline=self.BLACK, width=3)
# Inner border (red for color displays)
if self.is_colour:
draw_red.rectangle([10, 10, self.width-11, self.height-11], outline=self.RED, width=2)
else:
draw_black.rectangle([10, 10, self.width-11, self.height-11], outline=self.BLACK, width=1)
# Center crosshair
center_x = self.width // 2
center_y = self.height // 2
cross_size = min(50, self.width // 10)
draw_black.line([center_x - cross_size, center_y, center_x + cross_size, center_y],
fill=self.BLACK, width=2)
draw_black.line([center_x, center_y - cross_size, center_x, center_y + cross_size],
fill=self.BLACK, width=2)
# Corner markers
marker_size = min(50, self.width // 10)
# Top-left
draw_black.line([0, marker_size, marker_size, marker_size], fill=self.BLACK, width=2)
draw_black.line([marker_size, 0, marker_size, marker_size], fill=self.BLACK, width=2)
# Top-right
draw_black.line([self.width - marker_size, 0, self.width - marker_size, marker_size],
fill=self.BLACK, width=2)
draw_black.line([self.width - marker_size, marker_size, self.width, marker_size],
fill=self.BLACK, width=2)
# Bottom corners (red for color displays)
if self.is_colour:
# Bottom-left
draw_red.line([0, self.height - marker_size, marker_size, self.height - marker_size],
fill=self.RED, width=2)
draw_red.line([marker_size, self.height - marker_size, marker_size, self.height],
fill=self.RED, width=2)
# Bottom-right
draw_red.line([self.width - marker_size, self.height - marker_size,
self.width - marker_size, self.height], fill=self.RED, width=2)
draw_red.line([self.width - marker_size, self.height - marker_size,
self.width, self.height - marker_size], fill=self.RED, width=2)
else:
# Bottom-left
draw_black.line([0, self.height - marker_size, marker_size, self.height - marker_size],
fill=self.BLACK, width=2)
draw_black.line([marker_size, self.height - marker_size, marker_size, self.height],
fill=self.BLACK, width=2)
# Bottom-right
draw_black.line([self.width - marker_size, self.height - marker_size,
self.width - marker_size, self.height], fill=self.BLACK, width=2)
draw_black.line([self.width - marker_size, self.height - marker_size,
self.width, self.height - marker_size], fill=self.BLACK, width=2)
# Grid
grid_spacing = max(50, min(100, self.width // 10))
for x in range(0, self.width, grid_spacing):
draw_black.line([x, 0, x, self.height], fill=self.BLACK, width=1)
for y in range(0, self.height, grid_spacing):
draw_black.line([0, y, self.width, y], fill=self.BLACK, width=1)
if self.is_colour:
self.display.render(black_img, red_img)
else:
self.display.render(black_img)
logger.info("Calibration pattern displayed")
time.sleep(5)
def run_calibration_cycles(self, cycles=3):
"""Run calibration cycles to refresh the display"""
logger.info(f"Running {cycles} calibration cycles...")
for i in range(cycles):
logger.info(f"Calibration cycle {i+1}/{cycles}")
# Black
black_img = Image.new('RGB', (self.width, self.height), self.BLACK)
if self.is_colour:
red_img = Image.new('RGB', (self.width, self.height), self.WHITE)
self.display.render(black_img, red_img)
else:
self.display.render(black_img)
time.sleep(2)
# White
black_img = Image.new('RGB', (self.width, self.height), self.WHITE)
if self.is_colour:
red_img = Image.new('RGB', (self.width, self.height), self.WHITE)
self.display.render(black_img, red_img)
else:
self.display.render(black_img)
time.sleep(2)
# Red (color displays only)
if self.is_colour:
black_img = Image.new('RGB', (self.width, self.height), self.WHITE)
red_img = Image.new('RGB', (self.width, self.height), self.RED)
self.display.render(black_img, red_img)
time.sleep(2)
logger.info("Calibration cycles complete")
def run_all_tests(self, delay_between_tests=3):
"""Run all test patterns in sequence"""
logger.info(f"Starting comprehensive display test for {self.model}...")
tests = [
("Clear Display", self.clear_display),
("Solid Colors", self.test_solid_colors),
("Color Sections", self.test_color_sections),
("Checkerboard Pattern", self.test_checkerboard),
("Geometric Shapes", self.test_geometric_shapes),
("Text Rendering", self.test_text_rendering),
("Gradient/Dither Patterns", self.test_gradient_bars),
("Calibration Pattern", self.test_calibration_pattern),
]
for test_name, test_func in tests:
logger.info(f"\n--- Running: {test_name} ---")
try:
test_func()
time.sleep(delay_between_tests)
except Exception as e:
logger.error(f"Test '{test_name}' failed: {e}")
continue
logger.info("\nAll tests completed!")
def cleanup(self):
"""Clean up display resources"""
logger.info("Cleaning up...")
if self.display:
self.clear_display()
logger.info("Cleanup complete")
def list_supported_displays():
"""List all supported display models"""
print("\nSupported E-Paper Display Models:")
print("-" * 50)
# Separate color and B/W displays
color_displays = []
bw_displays = []
for model_name in sorted(supported_models.keys()):
if model_name == "image_file":
continue # Skip the virtual display
width, height = supported_models[model_name]
info = f"{model_name}: {width}x{height}"
if "colour" in model_name.lower() or "color" in model_name.lower():
color_displays.append(info)
else:
bw_displays.append(info)
print("\nColor Displays (3-color: black/white/red):")
for display in color_displays:
print(f" - {display}")
print("\nBlack/White Displays:")
for display in bw_displays:
print(f" - {display}")
print("\nVirtual Display (for testing without hardware):")
print(f" - image_file: {supported_models['image_file'][0]}x{supported_models['image_file'][1]}")
print("\nUsage: python test_display.py --model <model_name>")
print(" or: python test_display.py (auto-detect from settings.json)")
def main():
"""Main function to run display tests"""
parser = argparse.ArgumentParser(description='Universal E-Paper Display Test Script')
parser.add_argument('--model', type=str, default=None,
help='Display model name (e.g., epd_7_in_5_colour, epd_12_in_48_colour_V2)')
parser.add_argument('--test', type=str, default='all',
choices=['all', 'solid', 'sections', 'checkerboard', 'shapes',
'text', 'gradient', 'calibration', 'cycles'],
help='Specific test to run (default: all)')
parser.add_argument('--cycles', type=int, default=3,
help='Number of calibration cycles (default: 3)')
parser.add_argument('--delay', type=int, default=3,
help='Delay between tests in seconds (default: 3)')
parser.add_argument('--list', action='store_true',
help='List all supported display models')
parser.add_argument('--no-auto', action='store_true',
help='Disable auto-detection from settings.json')
args = parser.parse_args()
# List supported displays if requested
if args.list:
list_supported_displays()
return 0
# Create test instance
try:
tester = UniversalDisplayTest(
model=args.model,
auto_detect=not args.no_auto
)
except Exception as e:
logger.error(f"Failed to initialize display test: {e}")
logger.error("\nTroubleshooting:")
logger.error("1. Make sure you're running on a Raspberry Pi with display connected")
logger.error("2. Check that SPI is enabled (sudo raspi-config)")
logger.error("3. Try with sudo if you get permission errors")
logger.error("4. Use --list to see all supported models")
logger.error("5. Use --model to specify your display model explicitly")
return 1
try:
# Run requested test
if args.test == 'all':
tester.run_all_tests(delay_between_tests=args.delay)
elif args.test == 'solid':
tester.test_solid_colors()
elif args.test == 'sections':
tester.test_color_sections()
elif args.test == 'checkerboard':
tester.test_checkerboard()
elif args.test == 'shapes':
tester.test_geometric_shapes()
elif args.test == 'text':
tester.test_text_rendering()
elif args.test == 'gradient':
tester.test_gradient_bars()
elif args.test == 'calibration':
tester.test_calibration_pattern()
elif args.test == 'cycles':
tester.run_calibration_cycles(cycles=args.cycles)
# Always clear display at the end
time.sleep(2)
tester.cleanup()
except KeyboardInterrupt:
logger.info("\nTest interrupted by user")
tester.cleanup()
return 0
except Exception as e:
logger.error(f"Test failed: {e}")
tester.cleanup()
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -8,7 +8,6 @@ from inkycal.modules import Agenda
from inkycal.modules.inky_image import Inkyimage
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
logger = logging.getLogger(__name__)
@@ -72,4 +71,4 @@ class TestAgenda(unittest.TestCase):
im_black, im_colour = module.generate_image()
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()

View File

@@ -8,7 +8,6 @@ from inkycal.modules import Calendar
from inkycal.modules.inky_image import Inkyimage
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
sample_url = Config.SAMPLE_ICAL_URL
@@ -77,4 +76,4 @@ class TestCalendar(unittest.TestCase):
im_black, im_colour = module.generate_image()
print('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()

View File

@@ -7,7 +7,6 @@ from inkycal.modules import Feeds
from inkycal.modules.inky_image import Inkyimage
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
logger = logging.getLogger(__name__)
@@ -53,5 +52,5 @@ class TestFeeds(unittest.TestCase):
im_black, im_colour = module.generate_image()
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()

View File

@@ -11,7 +11,6 @@ from inkycal.modules import Inkyimage as Module
from inkycal.modules.inky_image import Inkyimage
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
url ="https://raw.githubusercontent.com/aceinnolab/Inkycal/assets/tests/Inkycal_cover.png"
@@ -113,4 +112,4 @@ class TestInkyImage(unittest.TestCase):
im_black, im_colour = module.generate_image()
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()

View File

@@ -8,7 +8,6 @@ from inkycal.modules import Jokes
from inkycal.modules.inky_image import Inkyimage
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
logger = logging.getLogger(__name__)
@@ -57,4 +56,4 @@ class TestJokes(unittest.TestCase):
im_black, im_colour = module.generate_image()
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()

View File

@@ -12,7 +12,6 @@ from inkycal.modules import Slideshow
from inkycal.modules.inky_image import Inkyimage
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
if not os.path.exists("tmp"):
@@ -144,21 +143,21 @@ class TestSlideshow(unittest.TestCase):
im_black, im_colour = module.generate_image()
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()
def test_switch_to_next_image(self):
logger.info(f'testing switching to next images..')
module = Slideshow(tests[0])
im_black, im_colour = module.generate_image()
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()
im_black, im_colour = module.generate_image()
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()
im_black, im_colour = module.generate_image()
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()
logger.info('OK')

View File

@@ -10,7 +10,6 @@ from inkycal.modules import TextToDisplay
from inkycal.modules.inky_image import Inkyimage
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
logger = logging.getLogger(__name__)
@@ -100,7 +99,7 @@ class TestTextToDisplay(unittest.TestCase):
im_black, im_colour = module.generate_image()
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()
def tearDown(self):
if os.path.exists(self.temp_path):

View File

@@ -8,7 +8,6 @@ from inkycal.modules import Tindie
from inkycal.modules.inky_image import Inkyimage
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
logger = logging.getLogger(__name__)
@@ -69,4 +68,4 @@ class TestTindie(unittest.TestCase):
im_black, im_colour = module.generate_image()
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()

View File

@@ -8,7 +8,7 @@ from inkycal.modules import Todoist
from inkycal.modules.inky_image import Inkyimage
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
api_key = Config.TODOIST_API_KEY
@@ -42,6 +42,6 @@ class TestTodoist(unittest.TestCase):
im_black, im_colour = module.generate_image()
print('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()
else:
print('No api key given, omitting test')

View File

@@ -8,7 +8,6 @@ from inkycal.modules import Weather
from inkycal.modules.inky_image import Inkyimage
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
owm_api_key = Config.OPENWEATHERMAP_API_KEY

View File

@@ -12,7 +12,6 @@ from tests import Config
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
preview = Inkyimage.preview
merge = Inkyimage.merge
tests = [
@@ -70,5 +69,5 @@ class TestWebshot(unittest.TestCase):
module = Webshot(test)
im_black, im_colour = module.generate_image()
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()
logger.info('OK')