Compare commits

...

56 Commits

Author SHA1 Message Date
d72199db3b github module 2025-11-30 22:18:20 +01:00
29af1f19ce change to relative path 2025-11-30 21:44:20 +01:00
6bd7d39351 remove unnecessary config 2025-11-30 21:32:21 +01:00
8b99fc33ac add utils 2025-11-30 21:17:02 +01:00
70c36111a6 done for the left section 2025-11-30 18:01:30 +01:00
a00892586b chmod 2025-11-21 22:46:54 +01:00
fde437bbe2 chmod 2025-11-21 22:46:44 +01:00
26af2825e4 merged 2025-11-21 22:43:31 +01:00
8a071e43ad vikunja main 2025-11-21 22:09:47 +01:00
Ace
98bb1934ae Merge pull request #396 from aceinnolab/bug/#392
Use gust speed provided by own api (see #392)
2025-09-27 16:21:44 +02:00
Ace
bd1ab2ad81 Merge pull request #404 from IvanVas/feature/todoist-error-handling-and-priorities
Enhance Todoist module with error handling and priority indicators
2025-09-27 16:20:27 +02:00
Ivan Vaskevych
a9a9446352 Enhance Todoist module with error handling and priority indicators
- Add retry logic with exponential backoff for API failures
- Implement caching for offline resilience
- Add visual priority indicators (●/○) with color coding
- Sort tasks by date first, then priority
- Show overdue dates in red
2025-09-18 19:12:06 +02:00
Ace
0fcff61c9a Merge pull request #401 from IvanVas/main
Adds a eInk display testing script
2025-09-08 12:05:14 +02:00
Ace
b0240561af Merge pull request #403 from IvanVas/fix-todoist-date-sorting
Sort Todoist tasks by due date within each project
2025-09-08 12:03:49 +02:00
Ivan Vaskevych
cc20a1ed0c Sort Todoist tasks by due date within each project 2025-09-07 18:32:31 +02:00
Ivan Vaskevych
afbcef4f9c Adds a eInk display testing script 2025-08-22 22:22:43 +02:00
Ace
46986f6ccf Merge pull request #400 from vitasam/main
Skipping resize if the image already correct size
2025-08-17 12:57:44 +02:00
The Random DIY
e2d0d3140b Skipping resize if the image already correct size 2025-08-17 12:08:51 +03:00
ace
2b3c7ea100 Update deps and base image 2025-08-13 17:34:57 +02:00
Ace
0337eb712d deprecate python3.9 & 3.10 2025-08-08 22:36:39 +02:00
Ace
977be94f27 increase swap 2025-07-29 02:36:42 +02:00
Ace
d481eba8c0 use gust speed (see #392) 2025-06-19 15:19:58 +02:00
Ace
d9a57f7802 fix failing docs generation on no changes 2025-06-19 14:37:43 +02:00
Ace
8752324386 auto-confirm apt install 2025-06-19 14:06:44 +02:00
Ace
d1bb3bbbd8 add new requirements for yfinance 2025-06-19 13:59:57 +02:00
Ace
c6faf16719 Merge pull request #395 from aceinnolab/dependabot/pip/urllib3-2.5.0
Bump urllib3 from 2.3.0 to 2.5.0
2025-06-19 13:27:04 +02:00
dependabot[bot]
4083f46252 Bump urllib3 from 2.3.0 to 2.5.0
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.3.0 to 2.5.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.3.0...2.5.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.5.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-19 04:48:07 +00:00
github-actions
a5b44292c0 update docs [bot] 2025-06-10 11:08:01 +00:00
Ace
8983b898df Merge pull request #394 from aceinnolab/dependabot/pip/requests-2.32.4
Bump requests from 2.32.3 to 2.32.4
2025-06-10 13:06:41 +02:00
dependabot[bot]
aae1ec5414 Bump requests from 2.32.3 to 2.32.4
Bumps [requests](https://github.com/psf/requests) from 2.32.3 to 2.32.4.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.3...v2.32.4)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.32.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-10 08:58:29 +00:00
Ace
9a97785d47 test fix for newer kernels 2025-01-26 16:40:06 +01:00
Ace
0923b4564a update requirements 2025-01-26 16:39:19 +01:00
github-actions
548746436d update docs [bot] 2025-01-13 22:45:05 +00:00
Ace
d5305e7cd4 Merge pull request #386 from aceinnolab/dependabot/pip/virtualenv-20.26.6
Bump virtualenv from 20.25.0 to 20.26.6
2025-01-13 23:44:02 +01:00
dependabot[bot]
a0b80c5ade Bump virtualenv from 20.25.0 to 20.26.6
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.25.0 to 20.26.6.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/20.25.0...20.26.6)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 18:21:57 +00:00
github-actions
d3a028ec63 update docs [bot] 2024-11-07 07:53:17 +00:00
Ace
6260964c18 Merge pull request #384 from aceinnolab/feature/#383
Feature/#383
2024-11-07 08:52:10 +01:00
Ace
bd8dc0f32e add directory structure 2024-11-07 08:31:11 +01:00
Ace
ea39bc6687 fix some links 2024-11-07 08:30:57 +01:00
Ace
b8424ce94f Implement line break support when using text with text module 2024-11-07 08:30:22 +01:00
github-actions
dcbea490ca update docs [bot] 2024-09-22 16:51:29 +00:00
Ace
04d122bb35 Merge pull request #380 from aceinnolab/feature/#378
Feature/#378
2024-09-22 18:50:19 +02:00
Ace
a3f4a18654 Use .show instead of extra preview function 2024-09-21 00:21:55 +02:00
Ace
b91d5ff62a Show battery level of PiSugar when PiSugar support is enabled 2024-09-21 00:21:00 +02:00
Ace
0361d725f1 this and that 2024-09-09 22:18:35 +02:00
Ace
b7b951362a Merge remote-tracking branch 'origin/main' 2024-09-08 15:19:14 +02:00
Ace
9a7541793e adapt link to ui 2024-09-08 15:19:08 +02:00
Ace
00520007d4 Merge pull request #374 from aceinnolab/feature/#373
fix showing only single feed
2024-08-27 15:11:29 +02:00
Ace
05cd1e13a5 update apt as otherwise, apt gets 404s 2024-08-27 15:11:11 +02:00
Ace
f08907eb8a fix showing only single feed 2024-08-27 15:09:03 +02:00
Ace
311c68ad79 Merge pull request #372 from aceinnolab/feature/#370
improve handling when piSugar is off
2024-08-27 15:05:51 +02:00
Ace
bef2efcbdf improve handling when piSugar is off 2024-08-27 15:04:09 +02:00
Ace
5f34162a30 Merge pull request #371 from htwyford/patch-1
Add apt update command to README
2024-08-27 14:55:12 +02:00
Harry Twyford
561424b19f Add apt update command to README
The README suggests that the user download an old version of Raspbian. On this version, a `sudo apt update` is required before installing packages from apt-get. Otherwise, some of the packages 404 because the package servers changed branch names:
```
htwyford@inkycal:~ $ sudo apt update
Get:1 http://archive.raspberrypi.org/debian bullseye InRelease [39.0 kB]
Get:2 http://raspbian.raspberrypi.org/raspbian bullseye InRelease [15.0 kB]
Get:3 http://archive.raspberrypi.org/debian bullseye/main armhf Packages [321 kB]
Get:4 http://raspbian.raspberrypi.org/raspbian bullseye/main armhf Packages [13.2 MB]
Get:5 http://raspbian.raspberrypi.org/raspbian bullseye/non-free armhf Packages [106 kB]
Fetched 13.7 MB in 14s (990 kB/s)
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
109 packages can be upgraded. Run 'apt list --upgradable' to see them.
N: Repository 'http://archive.raspberrypi.org/debian bullseye InRelease' changed its 'Suite' value from 'stable' to 'oldstable'
N: Repository 'http://raspbian.raspberrypi.org/raspbian bullseye InRelease' changed its 'Suite' value from 'stable' to 'oldstable'
```
2024-08-27 08:06:32 -04:00
26f7ce419b remove cache func 2024-08-26 11:30:47 +02:00
680026cb54 add settings to get the rend 2024-08-24 16:09:12 +02:00
98 changed files with 2703 additions and 560 deletions

View File

@@ -7,7 +7,7 @@
},
// This is the settings.json mount
"mounts": ["source=/c/temp/settings_test.json,target=/boot/settings.json,type=bind,consistency=cached"],
"mounts": ["source=/mnt/c/temp/settings_test.json,target=/boot/settings.json,type=bind,consistency=cached"],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "dos2unix ./.devcontainer/postCreate.sh && chmod +x ./.devcontainer/postCreate.sh && ./.devcontainer/postCreate.sh",

View File

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

View File

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

View File

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

View File

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

View File

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

105
README.md
View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -40,4 +40,4 @@ async def clear_display():
if __name__ == "__main__":
asyncio.run(run())
asyncio.run(dry_run())

View File

@@ -3,6 +3,7 @@ import inkycal.modules.inkycal_agenda
import inkycal.modules.inkycal_calendar
import inkycal.modules.inkycal_feeds
import inkycal.modules.inkycal_fullweather
import inkycal.modules.inkycal_github
import inkycal.modules.inkycal_image
import inkycal.modules.inkycal_jokes
import inkycal.modules.inkycal_slideshow

View File

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

View File

@@ -63,6 +63,7 @@ class Inkycal:
logger.info("Checking if a settings file is present...")
# load settings file - throw an error if file could not be found
if settings_path:
print(settings_path)
logger.info(f"Custom location for settings.json file specified: {settings_path}")
try:
with open(settings_path, mode="r") as settings_file:
@@ -75,6 +76,7 @@ class Inkycal:
else:
found = False
for location in settings.SETTINGS_JSON_PATHS:
print(location)
if os.path.exists(location):
logger.info(f"Found settings.json file in {location}")
with open(location, mode="r") as settings_file:
@@ -163,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%!")
@@ -340,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()

2
inkycal/modules/__init__.py Executable file → Normal file
View File

@@ -13,3 +13,5 @@ from .inkycal_xkcd import Xkcd
from .inkycal_fullweather import Fullweather
from .inkycal_tindie import Tindie
from .inkycal_vikunja import Vikunja
from .inkycal_today import Today
from .inkycal_github import GitHub

0
inkycal/modules/dev_module.py Executable file → Normal file
View File

0
inkycal/modules/ical_parser.py Executable file → Normal file
View File

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

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

0
inkycal/modules/inkycal_agenda.py Executable file → Normal file
View File

0
inkycal/modules/inkycal_calendar.py Executable file → Normal file
View File

View File

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

View File

@@ -19,6 +19,11 @@ from PIL import ImageDraw
from PIL import ImageFont
from PIL import ImageOps
import sys
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if project_root not in sys.path:
sys.path.insert(0, project_root)
from icons.weather_icons.weather_icons import get_weather_icon
from inkycal.custom.functions import fonts
from inkycal.custom.functions import get_system_tz

View File

@@ -0,0 +1,405 @@
"""
GitHub Contributions Heatmap Module for Inkycal
Displays GitHub contribution activity as a heatmap
"""
import logging
from datetime import datetime, timedelta
import requests
from PIL import Image, ImageDraw
from inkycal.custom import write, internet_available
from inkycal.modules.template import inkycal_module
logger = logging.getLogger(__name__)
class GitHub(inkycal_module):
"""GitHub Contributions Heatmap Module
Displays a heatmap showing GitHub contribution activity for a user.
"""
name = "GitHub - Display contribution heatmap"
requires = {
"username": {
"label": "GitHub username to display contributions for"
}
}
optional = {
"weeks": {
"label": "Number of weeks to show (default: 12)",
"default": 12
},
"show_legend": {
"label": "Show contribution count legend (default: True)",
"options": [True, False],
"default": True
},
"token": {
"label": "GitHub Personal Access Token (optional, for higher rate limits)"
}
}
def __init__(self, config):
"""Initialize GitHub module"""
super().__init__(config)
config = config['config']
self.username = config['username']
self.weeks = config.get('weeks', 12)
self.show_legend = config.get('show_legend', True)
self.token = config.get('token', None)
logger.debug(f'{__name__} loaded for user: {self.username}')
def _get_contributions_via_scraping(self):
"""Fetch contribution data via GitHub's contribution graph (fallback method)"""
import re
url = f"https://github.com/users/{self.username}/contributions"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
html = response.text
# Parse contribution data from SVG
# Look for rect elements with data-count attribute
pattern = r'data-date="([^"]+)"[^>]*data-level="(\d)"[^>]*data-count="(\d+)"'
matches = re.findall(pattern, html)
if not matches:
# Try alternative pattern
pattern = r'data-count="(\d+)"[^>]*data-date="([^"]+)"[^>]*data-level="(\d)"'
matches = re.findall(pattern, html)
if matches:
# Reorder to match expected format (date, level, count)
matches = [(m[1], m[2], m[0]) for m in matches]
# Group by weeks
from collections import defaultdict
weeks_dict = defaultdict(list)
total = 0
for date_str, level, count_str in matches:
count = int(count_str)
total += count
date_obj = datetime.fromisoformat(date_str)
# Calculate week number from start
week_num = date_obj.isocalendar()[1]
weeks_dict[week_num].append({
'contributionCount': count,
'date': date_str
})
# Convert to expected format
weeks = []
for week_num in sorted(weeks_dict.keys())[-self.weeks:]:
weeks.append({
'contributionDays': weeks_dict[week_num]
})
return {
'totalContributions': total,
'weeks': weeks
}
except Exception as e:
logger.error(f"Failed to scrape GitHub contributions: {e}")
raise
def _get_contributions(self):
"""Fetch contribution data from GitHub GraphQL API"""
if not internet_available():
raise Exception('Network could not be reached')
# If no token provided, use scraping method
if not self.token:
logger.info("No token provided, using scraping method")
return self._get_contributions_via_scraping()
# Calculate date range
today = datetime.now()
from_date = today - timedelta(weeks=self.weeks)
# GitHub GraphQL query
query = """
query($username: String!, $from: DateTime!, $to: DateTime!) {
user(login: $username) {
contributionsCollection(from: $from, to: $to) {
contributionCalendar {
totalContributions
weeks {
contributionDays {
contributionCount
date
}
}
}
}
}
}
"""
variables = {
"username": self.username,
"from": from_date.isoformat(),
"to": today.isoformat()
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.token}"
}
try:
response = requests.post(
'https://api.github.com/graphql',
json={'query': query, 'variables': variables},
headers=headers,
timeout=10
)
response.raise_for_status()
data = response.json()
if 'errors' in data:
logger.error(f"GitHub API error: {data['errors']}")
# Fallback to scraping if GraphQL fails
logger.info("Falling back to scraping method")
return self._get_contributions_via_scraping()
return data['data']['user']['contributionsCollection']['contributionCalendar']
except Exception as e:
logger.error(f"Failed to fetch GitHub contributions via API: {e}")
# Fallback to scraping
logger.info("Falling back to scraping method")
return self._get_contributions_via_scraping()
def _get_color_level(self, count, max_count):
"""Determine color intensity level based on contribution count"""
if count == 0:
return 0
elif max_count == 0:
return 1
else:
# 4 levels: 0 (none), 1-4 (low to high)
percentage = count / max_count
if percentage <= 0.25:
return 1
elif percentage <= 0.5:
return 2
elif percentage <= 0.75:
return 3
else:
return 4
def generate_image(self):
"""Generate heatmap image for GitHub contributions"""
# Define image size with padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.debug(f'Image size: {im_width} x {im_height} px')
# Create images for black and color channels
im_black = Image.new('RGB', size=im_size, color='white')
im_colour = Image.new('RGB', size=im_size, color='white')
draw_black = ImageDraw.Draw(im_black)
draw_colour = ImageDraw.Draw(im_colour)
try:
# Fetch contribution data
logger.info(f'Fetching GitHub contributions for {self.username}...')
calendar_data = self._get_contributions()
weeks_data = calendar_data['weeks']
total = calendar_data['totalContributions']
logger.info(f'Total contributions: {total}')
# Calculate heatmap dimensions
num_weeks = len(weeks_data)
days_per_week = 7
# Reserve space for title and legend
title_height = int(im_height * 0.15)
legend_height = int(im_height * 0.15) if self.show_legend else 0
heatmap_height = im_height - title_height - legend_height
# Calculate cell size
cell_width = im_width // num_weeks
cell_height = heatmap_height // days_per_week
cell_size = min(cell_width, cell_height)
# Add spacing between cells
cell_spacing = max(1, cell_size // 10)
actual_cell_size = cell_size - cell_spacing
# Center the heatmap
heatmap_start_x = (im_width - (num_weeks * cell_size)) // 2
heatmap_start_y = title_height + (heatmap_height - (days_per_week * cell_size)) // 2
logger.debug(f'Cell size: {actual_cell_size}x{actual_cell_size} px')
logger.debug(f'Heatmap position: ({heatmap_start_x}, {heatmap_start_y})')
# Find max contribution count for color scaling
max_count = 0
for week in weeks_data:
for day in week['contributionDays']:
max_count = max(max_count, day['contributionCount'])
logger.debug(f'Max daily contributions: {max_count}')
# Draw title
title_text = f"@{self.username} - {total} contributions"
write(
im_black,
(0, 0),
(im_width, title_height),
title_text,
font=self.font,
alignment='center'
)
# Draw heatmap
for week_idx, week in enumerate(weeks_data):
for day_idx, day in enumerate(week['contributionDays']):
count = day['contributionCount']
level = self._get_color_level(count, max_count)
x = heatmap_start_x + week_idx * cell_size
y = heatmap_start_y + day_idx * cell_size
# Draw cell border in black
draw_black.rectangle(
[x, y, x + actual_cell_size, y + actual_cell_size],
outline='black',
width=1
)
# Fill cell based on contribution level
if level > 0:
# All levels use black channel
if level == 4:
# Level 4: 100% fill (completely filled)
draw_black.rectangle(
[x + 1, y + 1, x + actual_cell_size - 1, y + actual_cell_size - 1],
fill='black'
)
else:
# Level 1-3: Partial fill based on percentage
# Level 1: 25%, Level 2: 50%, Level 3: 75%
fill_percentage = level / 4
fill_size = int(actual_cell_size * fill_percentage)
center_x = x + actual_cell_size // 2
center_y = y + actual_cell_size // 2
draw_black.rectangle(
[center_x - fill_size // 2, center_y - fill_size // 2,
center_x + fill_size // 2, center_y + fill_size // 2],
fill='black'
)
# Draw legend if enabled
if self.show_legend:
# Use same cell size as heatmap
legend_cell_size = actual_cell_size
legend_spacing = cell_spacing
# Calculate text widths
less_text = "Less"
more_text = "More"
less_width = int(self.font.getlength(less_text))
more_width = int(self.font.getlength(more_text))
# Calculate total legend width
total_legend_width = (
less_width + 10 + # "Less" + spacing
5 * legend_cell_size + 4 * legend_spacing + # 5 cells with 4 gaps
10 + more_width # spacing + "More"
)
# Center the legend horizontally
legend_start_x = int((im_width - total_legend_width) // 2)
legend_y = int(title_height + heatmap_height + (legend_height - legend_cell_size) // 2)
# Draw "Less" text (centered vertically with cells)
write(
im_black,
(legend_start_x, legend_y),
(less_width + 10, legend_cell_size),
less_text,
font=self.font,
alignment='center'
)
# Draw legend cells
cells_start_x = int(legend_start_x + less_width + 10)
for level in range(5):
x = int(cells_start_x + level * (legend_cell_size + legend_spacing))
y = int(legend_y)
draw_black.rectangle(
[x, y, x + legend_cell_size, y + legend_cell_size],
outline='black',
width=1
)
if level > 0:
# All levels use black channel
if level == 4:
# Level 4: 100% fill (completely filled)
draw_black.rectangle(
[x + 1, y + 1, x + legend_cell_size - 1, y + legend_cell_size - 1],
fill='black'
)
else:
# Level 1-3: Partial fill based on percentage
# Level 1: 25%, Level 2: 50%, Level 3: 75%
fill_percentage = level / 4
fill_size = int(legend_cell_size * fill_percentage)
center_x = int(x + legend_cell_size // 2)
center_y = int(y + legend_cell_size // 2)
draw_black.rectangle(
[center_x - fill_size // 2, center_y - fill_size // 2,
center_x + fill_size // 2, center_y + fill_size // 2],
fill='black'
)
# Draw "More" text (centered vertically with cells)
more_x = int(cells_start_x + 5 * legend_cell_size + 4 * legend_spacing + 10)
write(
im_black,
(more_x, legend_y),
(more_width + 10, legend_cell_size),
more_text,
font=self.font,
alignment='center'
)
logger.info('GitHub heatmap generated successfully')
except Exception as e:
logger.error(f'Failed to generate GitHub heatmap: {e}')
# Show error message on display
error_msg = f"Error: {str(e)}"
write(
im_black,
(0, im_height // 2 - 20),
(im_width, 40),
error_msg,
font=self.font,
alignment='center'
)
return im_black, im_colour

0
inkycal/modules/inkycal_image.py Executable file → Normal file
View File

0
inkycal/modules/inkycal_jokes.py Executable file → Normal file
View File

0
inkycal/modules/inkycal_server.py Executable file → Normal file
View File

0
inkycal/modules/inkycal_slideshow.py Executable file → Normal file
View File

0
inkycal/modules/inkycal_stocks.py Executable file → Normal file
View File

View File

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

0
inkycal/modules/inkycal_tindie.py Executable file → Normal file
View File

View File

@@ -0,0 +1,379 @@
"""
Inkycal Calendar Module
Copyright by aceinnolab
"""
# pylint: disable=logging-fstring-interpolation
import calendar as cal
from inkycal.custom import *
from inkycal.modules.template import inkycal_module
logger = logging.getLogger(__name__)
class TaskEntry:
"""Class representing a task entry."""
def __init__(self, title, project=None, parent_project=None, subtasks=None):
self.title = title
self.project = project
self.consumed_time = 0 # in minutes
self.subtasks = []
def add_subtask(self, subtask):
"""Add a subtask to the task entry."""
self.subtasks.append(subtask)
def mock_task_list():
"""Generate a mock task list for testing purposes."""
finetune = TaskEntry("3 new models finetune work", project="AISentry")
generate_data = TaskEntry("Generate training data", project="AISentry")
function_development = TaskEntry("Function development", project="AISentry")
finetune.add_subtask(generate_data)
finetune.add_subtask(function_development)
check_llama = TaskEntry("Check Llama model performance", project="llama.cpp")
transform = TaskEntry("Transformers library exploration", project="llama.cpp")
check_llama.add_subtask(transform)
research_work = TaskEntry("Research new AI techniques", project="AISentry")
meeting = TaskEntry("Team meeting", project="General")
return [finetune, check_llama, research_work, meeting]
def get_ip_address():
"""Get public IP address from external service."""
try:
# 方法1: 使用 ipify.org
response = requests.get('https://api.ipify.org?format=json', timeout=5)
return response.json()['ip']
except Exception:
try:
# 方法2: 使用 icanhazip.com (备用)
response = requests.get('https://icanhazip.com', timeout=5)
return response.text.strip()
except Exception:
try:
# 方法3: 使用 ifconfig.me (备用)
response = requests.get('https://ifconfig.me/ip', timeout=5)
return response.text.strip()
except Exception:
return "N/A"
class Today(inkycal_module):
"""today class
Show today's date and events from given iCalendars
"""
name = "Today - Show today's date and events from iCalendars"
optional = {
"date_format": {
"label": "Use an arrow-supported token for custom date formatting "
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM",
"default": "D MMM",
},
"time_format": {
"label": "Use an arrow-supported token for custom time formatting "
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
"default": "HH:mm",
},
"webdav_hostname": {
"label": "WebDAV Hostname (e.g. https://webdav.server.com)",
"default": "",
},
"webdav_login": {
"label": "WebDAV Login Username",
"default": "",
},
"webdav_password": {
"label": "WebDAV Login Password",
"default": "",
},
"webdav_file_path": {
"label": "WebDAV File Path to Super Productivity JSON file",
"default": "",
},
}
def __init__(self, config):
"""Initialize inkycal_calendar module"""
super().__init__(config)
config = config['config']
self.ical = None
self.month_events = None
self._upcoming_events = None
self._days_with_events = None
# optional parameters
self.week_start = config['week_starts_on']
self.show_events = config['show_events']
self.date_format = config["date_format"]
self.time_format = config['time_format']
self.language = config['language']
# webdav configuration
self.webdav_options = {
'webdav_hostname': config.get('webdav_hostname', ''),
'webdav_login': config.get('webdav_login', ''),
'webdav_password': config.get('webdav_password', ''),
'webdav_file_path': config.get('webdav_file_path', ''),
}
# additional configuration
self.timezone = get_system_tz()
# 选择字体:优先使用支持中文的 NotoSansCJK否则使用 NotoSans
self._font_family = self._select_font_family()
# give an OK message
logger.debug(f'{__name__} loaded')
def _select_font_family(self) -> str:
preferred_fonts = [
'NotoSansCJKsc-Regular',
'NotoSans-SemiCondensed'
]
for font_name in preferred_fonts:
if font_name in fonts:
logger.debug(f'Selected font: {font_name}')
return font_name
return list(fonts.keys())[0]
def _get_font(self, size: int) -> ImageFont.FreeTypeFont:
return ImageFont.truetype(fonts[self._font_family], size=size)
@staticmethod
def flatten(values):
"""Flatten the values."""
return [x for y in values for x in y]
def generate_image(self):
"""Generate the image for today's date and events. """
# ****************************************************************************************************************
# Create base image
# Define new image size with respect to padding
im_width = self.width - 2 * self.padding_left
im_height = self.height - 2 * self.padding_top
im_size = (im_width, im_height)
event_height = 0
logger.debug(f'Generating Today module image of size {im_size}')
# Create an iamge for black and colour Inky displays
im_black = Image.new('RGB', im_size, color='white')
im_colour = Image.new('RGB', im_size, color='white')
# Split the image into two sections: date section and events section
left_section_width = int(im_width * 0.2)
right_section_width = im_width - left_section_width
# 5% bottom space will be reserved for show the day progress bar
left_section = (0, 0, left_section_width, im_height - int(im_height * 0.05))
right_section = (left_section_width, 0, im_width, im_height - int(im_height * 0.05))
section_height = left_section[3]
# ****************************************************************************************************************
# Edit left section - show today's date
now = arrow.now(tz=self.timezone)
month_height = int(im_height * 0.15)
month_font = self._get_font(int(self.fontsize * 1.5))
write(
im_black,
(0, 0),
(left_section_width, month_height),
now.format('MMMM', locale=self.language),
font=month_font,
autofit=False
)
date_height = int(im_height * 0.5)
date_y = month_height
large_font = self._get_font(int(self.fontsize * 4))
date_time = arrow.now()
day = date_time.day
print(str(day))
write(
im_colour,
(0, date_y),
(left_section_width, date_height),
str(day),
font=large_font,
autofit=False
)
weekday_y = month_height + date_height
weekday_height = int(im_height * 0.15)
weekday_font = self._get_font(int(self.fontsize * 2))
write(
im_black,
(0, weekday_y),
(left_section_width, weekday_height),
now.format('dddd', locale=self.language),
font=weekday_font,
autofit=False
)
# show IP address at the bottom left
ip_y = weekday_y + weekday_height
ip_height = im_height - ip_y - 5
ip_address = get_ip_address()
write(
im_black,
(0, ip_y),
(left_section_width, ip_height),
ip_address,
font=self.font,
alignment='center',
autofit=True
)
# ****************************************************************************************************************
# Draw a dash line to separate left and right sections
for _y in range(0, section_height, 8):
ImageDraw.Draw(im_black).line(
[(left_section_width, _y), (left_section_width, _y + 4)],
fill='black',
width=2,
)
# ****************************************************************************************************************
# Edit right section - show today's events
if self.show_events:
# 导入日历解析器
upcoming_events = True
# 计算右侧可用空间
right_x = left_section_width + 5 # 留5px边距
right_usable_width = right_section_width - 10 # 左右各留5px
# 计算行高
line_spacing = 2
text_bbox = self.font.getbbox("hg")
line_height = text_bbox[3] + line_spacing
max_lines = im_height // line_height
from inkycal.modules.super_productivity_utils import get_today_tasks
import requests
url = self.webdav_options['webdav_hostname'] + self.webdav_options['webdav_file_path']
response = requests.get(url, auth=(
self.webdav_options['webdav_login'],
self.webdav_options['webdav_password']
))
content = response.content
content = content[8:]
with open('/workspaces/Inkycal/inkycal/modules/super_productivity.json', 'wb') as f:
f.write(content)
json_file_path = '/workspaces/Inkycal/inkycal/modules/super_productivity.json'
task_list = get_today_tasks(json_file_path)
if upcoming_events:
# Split to 2 parts
# Left part: title
# Right part: Project name
current_line = 0
for idx, event in enumerate(task_list):
if current_line >= max_lines:
break # 超出显示范围,停止绘制
# 写任务标题
write(
im_black,
(right_x, current_line * line_height),
(int(right_usable_width * 0.7), line_height),
event.title,
font=self.font,
alignment='left'
)
# 写项目名称
project_name = event.project_name if event.project_name else "Inbox"
write(
im_colour,
(right_x + int(right_usable_width * 0.7), current_line * line_height),
(int(right_usable_width * 0.3), line_height),
project_name,
font=self.font,
alignment='right'
)
current_line += 1
if event.subtasks:
for sub_idx, subtask in enumerate(event.subtasks):
if subtask.is_done:
continue
if current_line + sub_idx + 1 >= max_lines:
break # 超出显示范围,停止绘制
# 写子任务标题,缩进显示
write(
im_black,
(right_x + 10, current_line * line_height),
(int(right_usable_width * 0.7) - 10, line_height),
f"- {subtask.title}",
font=self.font,
alignment='left'
)
current_line += 1 # 更新主循环的索引
pass
else:
# 没有事件时显示提示
write(
im_colour,
(right_x, int(im_height / 2)),
(right_usable_width, line_height),
"No events today",
font=self.font,
alignment='center'
)
# ****************************************************************************************************************
# Draw progress bar at the bottom (24 segments for 24 hours)
progress_bar_height = int(im_height * 0.05)
progress_bar_y = im_height - progress_bar_height
# 计算当前小时进度
current_hour = now.hour
current_minute = now.minute
current_progress = current_hour + (current_minute / 60.0) # 0-24 的浮点数
# 绘制24个格子
num_segments = 24
segment_spacing = 2 # 格子之间的间距
total_spacing = segment_spacing * (num_segments - 1)
segment_width = (im_width - total_spacing) / num_segments
draw = ImageDraw.Draw(im_black)
for i in range(num_segments):
# 计算每个格子的位置
x_start = int(i * (segment_width + segment_spacing))
x_end = int(x_start + segment_width)
# 判断该格子是否已完成
if i < current_progress:
# 已完成的格子填充黑色(在 im_colour 上会显示为红色)
draw.rectangle(
[(x_start, progress_bar_y), (x_end, im_height)],
fill='black',
outline='black'
)
else:
# 未完成的格子只画边框(在 im_black 上画)
ImageDraw.Draw(im_black).rectangle(
[(x_start, progress_bar_y), (x_end, im_height)],
fill='white',
outline='black',
width=1
)
return im_black, im_colour

View File

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

View File

@@ -92,15 +92,15 @@ class ApiVikunja():
return json_result
def get_projects(self):
if self._cache['projects'] is None:
self._cache['projects'] = self._get_json(self._create_url('projects'), headers=self._login.get_headers())
# if self._cache['projects'] is None:
self._cache['projects'] = self._get_json(self._create_url('projects'), headers=self._login.get_headers())
return self._cache['projects']
def get_tasks(self, exclude_completed=True):
if self._cache['tasks'] is None:
url = self._create_url('tasks/all')
params = {'filter': 'done=false'} if exclude_completed else {}
self._cache['tasks'] = self._get_json(url, params, headers=self._login.get_headers()) or []
# if self._cache['tasks'] is None:
url = self._create_url('tasks/all')
params = {'filter': 'done=false'} if exclude_completed else {}
self._cache['tasks'] = self._get_json(url, params, headers=self._login.get_headers()) or []
return self._cache['tasks']
@@ -215,8 +215,8 @@ class Vikunja(inkycal_module):
logger.debug(f"all_projects: {all_projects}")
logger.debug(f"all_active_tasks: {all_active_tasks}")
print(f"all_projects: {all_projects}")
print(f"all_active_tasks: {all_active_tasks}")
# print(f"all_projects: {all_projects}")
# print(f"all_active_tasks: {all_active_tasks}")
# Filter entries in all_projects if filter was given
if self.project_filter:

View File

@@ -0,0 +1,241 @@
"""
Super Productivity 数据解析工具
用于从 Super Productivity 导出的 JSON 中提取任务信息
"""
import json
from datetime import date
from typing import List, Dict, Optional, Tuple
class TaskEntry:
"""任务条目类"""
def __init__(
self,
task_id: str,
title: str,
project_name: str,
subtasks: Optional[List['TaskEntry']] = None,
is_done: bool = False,
time_spent: int = 0
):
self.id = task_id
self.title = title
self.project_name = project_name
self.subtasks = subtasks or []
self.is_done = is_done
self.time_spent = time_spent # 单位:毫秒
def __repr__(self):
return f"TaskEntry(title='{self.title}', project='{self.project_name}', subtasks={len(self.subtasks)})"
def get_time_spent_hours(self) -> float:
"""获取花费的时间(小时)"""
return self.time_spent / 3600000.0
def parse_super_productivity_json(json_file_path: str) -> Tuple[Dict, Dict, Dict]:
"""
解析 Super Productivity JSON 文件
Args:
json_file_path: JSON 文件路径
Returns:
(tasks, projects, tags) 元组
"""
with open(json_file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
tasks = data['mainModelData']['task']['entities']
projects = data['mainModelData']['project']['entities']
tags = data['mainModelData']['tag']['entities']
return tasks, projects, tags
def get_project_name(projects: Dict, project_id: str) -> str:
"""获取项目名称"""
if project_id in projects:
return projects[project_id]['title']
return "Inbox"
def parse_task_entry(
task_id: str,
tasks: Dict,
projects: Dict
) -> Optional[TaskEntry]:
"""
解析单个任务条目
Args:
task_id: 任务 ID
tasks: 所有任务字典
projects: 所有项目字典
Returns:
TaskEntry 对象或 None
"""
task = tasks.get(task_id)
if not task:
return None
# 获取基本信息
title = task.get('title', 'Untitled')
project_id = task.get('projectId', 'INBOX_PROJECT')
project_name = get_project_name(projects, project_id)
is_done = task.get('isDone', False)
time_spent = task.get('timeSpent', 0)
# 解析子任务
subtask_ids = task.get('subTaskIds', [])
subtasks = []
for subtask_id in subtask_ids:
subtask = parse_task_entry(subtask_id, tasks, projects)
if subtask:
subtasks.append(subtask)
return TaskEntry(
task_id=task_id,
title=title,
project_name=project_name,
subtasks=subtasks,
is_done=is_done,
time_spent=time_spent
)
def get_today_tasks(
json_file_path: str,
target_date: Optional[date] = None,
include_done: bool = False
) -> List[TaskEntry]:
"""
获取今天 due 的任务
Args:
json_file_path: JSON 文件路径
target_date: 目标日期,默认为今天
include_done: 是否包含已完成的任务
Returns:
今天的任务列表
"""
if target_date is None:
target_date = date.today()
today_str = target_date.strftime('%Y-%m-%d')
tasks, projects, _ = parse_super_productivity_json(json_file_path)
today_tasks = []
for task_id, task in tasks.items():
# 检查是否是今天的任务
due_day = task.get('dueDay')
# print(f'Task ID: {task_id}, Due Day: {due_day}, Today: {today_str}') # 调试输出
if due_day != today_str:
continue
# 检查是否已完成
if not include_done and task.get('isDone', False):
continue
# 只添加顶层任务(没有 parentId 的)
if not task.get('parentId'):
parsed_task = parse_task_entry(task_id, tasks, projects)
if parsed_task:
today_tasks.append(parsed_task)
return today_tasks
def get_tasks_by_tag(
json_file_path: str,
tag_name: str,
include_done: bool = False
) -> List[TaskEntry]:
"""
根据标签获取任务
Args:
json_file_path: JSON 文件路径
tag_name: 标签名称(如 "TODAY"
include_done: 是否包含已完成的任务
Returns:
带有该标签的任务列表
"""
tasks, projects, tags = parse_super_productivity_json(json_file_path)
# 查找标签 ID
tag_id = None
for tid, tag in tags.items():
if tag.get('title') == tag_name:
tag_id = tid
break
if not tag_id:
return []
# 获取带有该标签的任务
task_ids = tags[tag_id].get('taskIds', [])
result_tasks = []
for task_id in task_ids:
task = tasks.get(task_id)
if not task:
continue
# 检查是否已完成
if not include_done and task.get('isDone', False):
continue
parsed_task = parse_task_entry(task_id, tasks, projects)
if parsed_task:
result_tasks.append(parsed_task)
return result_tasks
if __name__ == "__main__":
"""测试代码"""
import sys
if len(sys.argv) < 2:
print("Usage: python super_productivity_utils.py <json_file_path>")
sys.exit(1)
json_file = sys.argv[1]
# 获取今天的任务
print("=" * 60)
print("📅 TODAY'S TASKS (with due date)")
print("=" * 60)
today_tasks = get_today_tasks(json_file)
for task in today_tasks:
print(f"\n📋 {task.title}")
print(f" 📁 Project: {task.project_name}")
if task.subtasks:
print(f" └─ Subtasks ({len(task.subtasks)}):")
for subtask in task.subtasks:
status = "" if subtask.is_done else ""
print(f" {status} {subtask.title}")
# 获取 TODAY 标签的任务
print("\n" + "=" * 60)
print("🏷️ TASKS WITH 'TODAY' TAG")
print("=" * 60)
tagged_tasks = get_tasks_by_tag(json_file, "TODAY")
for task in tagged_tasks:
print(f"\n📋 {task.title}")
print(f" 📁 Project: {task.project_name}")
if task.time_spent > 0:
print(f" ⏱️ Time spent: {task.get_time_spent_hours():.1f}h")
print("\n" + "=" * 60)
print(f"📊 Summary: {len(today_tasks)} due today, {len(tagged_tasks)} tagged TODAY")
print("=" * 60)

3
inkycal/modules/template.py Executable file → Normal file
View File

@@ -26,7 +26,8 @@ class inkycal_module(metaclass=abc.ABCMeta):
self.fontsize = conf["fontsize"]
self.font = ImageFont.truetype(
fonts['NotoSansUI-Regular'], size=self.fontsize)
fonts['NotoSansCJKsc-Regular'], size=self.fontsize)
# fonts['NotoSansUI-SemiCondensed'], size=self.fontsize)
def set(self, help=False, **kwargs):
"""Set attributes of class, e.g. class.set(key=value)

View File

@@ -18,7 +18,7 @@
},
{
"position": 2,
"name": "Calendar",
"name": "Today",
"config": {
"size": [
528,

View File

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

View File

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

649
test_display.py Normal file
View File

@@ -0,0 +1,649 @@
#!/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
$ python test_display.py --model epd_7_in_5_colour --test all
"""
"""Main function to run display tests"""
parser = argparse.ArgumentParser(description='Universal E-Paper Display Test Script')
parser.add_argument('--model', type=str, default=None,
help='Display model name (e.g., epd_7_in_5_colour, epd_12_in_48_colour_V2)')
parser.add_argument('--test', type=str, default='all',
choices=['all', 'solid', 'sections', 'checkerboard', 'shapes',
'text', 'gradient', 'calibration', 'cycles'],
help='Specific test to run (default: all)')
parser.add_argument('--cycles', type=int, default=3,
help='Number of calibration cycles (default: 3)')
parser.add_argument('--delay', type=int, default=3,
help='Delay between tests in seconds (default: 3)')
parser.add_argument('--list', action='store_true',
help='List all supported display models')
parser.add_argument('--no-auto', action='store_true',
help='Disable auto-detection from settings.json')
args = parser.parse_args()
# List supported displays if requested
if args.list:
list_supported_displays()
return 0
# Create test instance
try:
tester = UniversalDisplayTest(
model=args.model,
auto_detect=not args.no_auto
)
except Exception as e:
logger.error(f"Failed to initialize display test: {e}")
logger.error("\nTroubleshooting:")
logger.error("1. Make sure you're running on a Raspberry Pi with display connected")
logger.error("2. Check that SPI is enabled (sudo raspi-config)")
logger.error("3. Try with sudo if you get permission errors")
logger.error("4. Use --list to see all supported models")
logger.error("5. Use --model to specify your display model explicitly")
return 1
try:
# Run requested test
if args.test == 'all':
tester.run_all_tests(delay_between_tests=args.delay)
elif args.test == 'solid':
tester.test_solid_colors()
elif args.test == 'sections':
tester.test_color_sections()
elif args.test == 'checkerboard':
tester.test_checkerboard()
elif args.test == 'shapes':
tester.test_geometric_shapes()
elif args.test == 'text':
tester.test_text_rendering()
elif args.test == 'gradient':
tester.test_gradient_bars()
elif args.test == 'calibration':
tester.test_calibration_pattern()
elif args.test == 'cycles':
tester.run_calibration_cycles(cycles=args.cycles)
# Always clear display at the end
time.sleep(2)
tester.cleanup()
except KeyboardInterrupt:
logger.info("\nTest interrupted by user")
tester.cleanup()
return 0
except Exception as e:
logger.error(f"Test failed: {e}")
tester.cleanup()
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -15,7 +15,8 @@ class Config:
get = os.environ.get
# show generated images via preview?
USE_PREVIEW = False
# USE_PREVIEW = False
USE_PREVIEW = True
# ical_parser_test
OPENWEATHERMAP_API_KEY = get("OPENWEATHERMAP_API_KEY")
@@ -35,6 +36,8 @@ class Config:
TINDIE_API_KEY = get("TINDIE_API_KEY")
TINDIE_USERNAME = get("TINDIE_USERNAME")
OUTPUT_DIR = f"{basedir}/../image_folder"

View File

@@ -18,7 +18,7 @@
},
{
"position": 2,
"name": "Calendar",
"name": "Today",
"config": {
"size": [
528,
@@ -43,6 +43,22 @@
"padding_x": 10,"padding_y": 10,"fontsize": 14,"language": "en"
}
},
{
"position": 4,
"name": "Vikunja",
"config": {
"size": [528, 300],
"url-frontend": "http://ff.mhrooz.xyz:8077/",
"url-backend": "http://192.168.50.10:3456/api/v1/",
"username": "iicd",
"password": "9297519Mhz.",
"project_filter": ["LMU", "Master Thesis"],
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
}
]
}

0
tests/test_ical_parser.py Executable file → Normal file
View File

3
tests/test_inkycal_agenda.py Executable file → Normal file
View File

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

3
tests/test_inkycal_calendar.py Executable file → Normal file
View File

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

3
tests/test_inkycal_feeds.py Executable file → Normal file
View File

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

3
tests/test_inkycal_image.py Executable file → Normal file
View File

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

3
tests/test_inkycal_jokes.py Executable file → Normal file
View File

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

9
tests/test_inkycal_slideshow.py Executable file → Normal file
View File

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

0
tests/test_inkycal_stocks.py Executable file → Normal file
View File

View File

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

3
tests/test_inkycal_tindie.py Executable file → Normal file
View File

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

View File

@@ -0,0 +1,86 @@
"""
inkycal_today unittest
"""
import logging
import unittest
from inkycal.modules import Today
from inkycal.modules.inky_image import Inkyimage, image_to_palette
from tests import Config
merge = Inkyimage.merge
sample_url = Config.SAMPLE_ICAL_URL
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
# /workspaces/Inkycal/venv/bin/python3 -m pytest tests/test_inkycal_today.py::TestToday::test_generate_image -v -s
tests = [
{
"name": "Today",
"config": {
"size": [480, 390],
"week_starts_on": "Monday",
"show_events": True,
"date_format": "D MMM", "time_format": "HH:mm",
"padding_x": 10, "padding_y": 10, "fontsize": 14, "language": "zh",
"font": "NotoSansCJKsc-Regular",
"webdav_hostname": "https://webdav.mhrooz.xyz",
"webdav_login": "iicd",
"webdav_password": "wjslldhs",
"webdav_file_path": "/super-productivity/__meta_",
}
},
]
class TestToday(unittest.TestCase):
def test_generate_image(self):
output_dir = Config.OUTPUT_DIR
import os
from PIL import Image, ImageDraw
import numpy as np
os.makedirs(output_dir, exist_ok=True)
test_num = 0
for test in tests:
print(f'test {tests.index(test) + 1} generating image..', end="")
module = Today(test)
im_black, im_colour = module.generate_image()
print('OK')
# 创建最终的三色图像
# 在 E-Paper 上: im_black 的黑色像素显示黑色, im_colour 的黑色像素显示红色
result = Image.new('RGB', im_black.size, 'white')
result_array = np.array(result)
black_array = np.array(im_black)
colour_array = np.array(im_colour)
# 使用阈值处理抗锯齿:灰度值 < 128 的视为"黑色"
# im_black 上的深色区域 -> 黑色 (0, 0, 0)
black_gray = black_array[:,:,0] # 取灰度值RGB相同
black_mask = black_gray < 128
result_array[black_mask] = [0, 0, 0]
# im_colour 上的深色区域 -> 红色 (255, 0, 0)
colour_gray = colour_array[:,:,0]
colour_mask = colour_gray < 128
result_array[colour_mask] = [255, 0, 0]
# 保存最终图像
final_image = Image.fromarray(result_array)
output_path = os.path.join(output_dir, f"today_test_{test_num}.png")
final_image.save(output_path)
print(f' -> Saved to {output_path}')
# 统计颜色
red_count = np.sum(colour_mask)
black_count = np.sum(black_mask)
print(f' 🔴 Red pixels: {red_count}, ⚫ Black pixels: {black_count}')
test_num += 1
if Config.USE_PREVIEW:
final_image.show()

View File

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

1
tests/test_inkycal_weather.py Executable file → Normal file
View File

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

3
tests/test_inkycal_webshot.py Executable file → Normal file
View File

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