merged
This commit is contained in:
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/greetings.yml
vendored
4
.github/workflows/greetings.yml
vendored
@@ -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."
|
||||
|
||||
9
.github/workflows/test-on-rpi.yml
vendored
9
.github/workflows/test-on-rpi.yml
vendored
@@ -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
|
||||
9
.github/workflows/update-docs.yml
vendored
9
.github/workflows/update-docs.yml
vendored
@@ -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
|
||||
|
||||
35
.github/workflows/update-os.yml
vendored
35
.github/workflows/update-os.yml
vendored
@@ -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
105
README.md
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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..
|
||||
|
||||
23
docs/_static/basic.css
vendored
23
docs/_static/basic.css
vendored
@@ -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 {
|
||||
|
||||
2
docs/_static/css/badge_only.css
vendored
2
docs/_static/css/badge_only.css
vendored
@@ -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}
|
||||
2
docs/_static/css/theme.css
vendored
2
docs/_static/css/theme.css
vendored
File diff suppressed because one or more lines are too long
7
docs/_static/doctools.js
vendored
7
docs/_static/doctools.js
vendored
@@ -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
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
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
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
BIN
docs/_static/fonts/Lato/lato-bold.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-bolditalic.eot
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-bolditalic.eot
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-bolditalic.ttf
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-bolditalic.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-bolditalic.woff
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-bolditalic.woff
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-bolditalic.woff2
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-bolditalic.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-italic.eot
vendored
Normal file
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
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
BIN
docs/_static/fonts/Lato/lato-italic.woff
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-italic.woff2
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-italic.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-regular.eot
vendored
Normal file
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
BIN
docs/_static/fonts/Lato/lato-regular.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-regular.woff
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-regular.woff2
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-regular.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2
vendored
Normal file
Binary file not shown.
228
docs/_static/js/versions.js
vendored
Normal file
228
docs/_static/js/versions.js
vendored
Normal 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);
|
||||
});
|
||||
});
|
||||
7
docs/_static/language_data.js
vendored
7
docs/_static/language_data.js
vendored
@@ -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"];
|
||||
|
||||
36
docs/_static/pygments.css
vendored
36
docs/_static/pygments.css
vendored
@@ -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 */
|
||||
51
docs/_static/searchtools.js
vendored
51
docs/_static/searchtools.js
vendored
@@ -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;
|
||||
|
||||
@@ -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 — 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.
|
||||
It’s 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>
|
||||
|
||||
@@ -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 — 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" />
|
||||
|
||||
@@ -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 — 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>
|
||||
|
||||
@@ -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 — 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" />
|
||||
|
||||
@@ -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 — 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">>>> </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">>>> </span><span class="kn">import</span><span class="w"> </span><span class="nn">arrow</span>
|
||||
<span class="gp">>>> </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">>>> </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>
|
||||
|
||||
BIN
docs/objects.inv
BIN
docs/objects.inv
Binary file not shown.
@@ -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 — 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" />
|
||||
|
||||
@@ -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 — 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>
|
||||
|
||||
@@ -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 — 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
@@ -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
|
||||
|
||||
@@ -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..
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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
7
inkycal/modules/inky_image.py
Executable file → Normal 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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
2
setup.py
2
setup.py
@@ -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
645
test_display.py
Normal 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())
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user