Compare commits
56 Commits
4beba1ab24
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d72199db3b | |||
| 29af1f19ce | |||
| 6bd7d39351 | |||
| 8b99fc33ac | |||
| 70c36111a6 | |||
| a00892586b | |||
| fde437bbe2 | |||
| 26af2825e4 | |||
| 8a071e43ad | |||
|
|
98bb1934ae | ||
|
|
bd1ab2ad81 | ||
|
|
a9a9446352 | ||
|
|
0fcff61c9a | ||
|
|
b0240561af | ||
|
|
cc20a1ed0c | ||
|
|
afbcef4f9c | ||
|
|
46986f6ccf | ||
|
|
e2d0d3140b | ||
|
|
2b3c7ea100 | ||
|
|
0337eb712d | ||
|
|
977be94f27 | ||
|
|
d481eba8c0 | ||
|
|
d9a57f7802 | ||
|
|
8752324386 | ||
|
|
d1bb3bbbd8 | ||
|
|
c6faf16719 | ||
|
|
4083f46252 | ||
|
|
a5b44292c0 | ||
|
|
8983b898df | ||
|
|
aae1ec5414 | ||
|
|
9a97785d47 | ||
|
|
0923b4564a | ||
|
|
548746436d | ||
|
|
d5305e7cd4 | ||
|
|
a0b80c5ade | ||
|
|
d3a028ec63 | ||
|
|
6260964c18 | ||
|
|
bd8dc0f32e | ||
|
|
ea39bc6687 | ||
|
|
b8424ce94f | ||
|
|
dcbea490ca | ||
|
|
04d122bb35 | ||
|
|
a3f4a18654 | ||
|
|
b91d5ff62a | ||
|
|
0361d725f1 | ||
|
|
b7b951362a | ||
|
|
9a7541793e | ||
|
|
00520007d4 | ||
|
|
05cd1e13a5 | ||
|
|
f08907eb8a | ||
|
|
311c68ad79 | ||
|
|
bef2efcbdf | ||
|
|
5f34162a30 | ||
|
|
561424b19f | ||
| 26f7ce419b | |||
| 680026cb54 |
@@ -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",
|
||||
|
||||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -17,7 +17,7 @@ Before submitting a bug report, check if the issue is already reported in the [I
|
||||
We welcome suggestions for new features or enhancements. Use the [Issues](https://github.com/aceinnolab/Inkycal/issues) section to submit your ideas, and provide as much detail as possible.
|
||||
|
||||
### Third party modules
|
||||
So you had a great idea for an inkycal-module? Awesome! In fact, there is already a repo sepcfifically created for that purpose: [inkycal-modules-template](https://github.com/aceisace/inkycal-modules-template). Just fork that repo, add your module and give me a shout via Discord, Github or Email.
|
||||
So you had a great idea for an inkycal-module? Awesome! In fact, there is already a repo sepcfifically created for that purpose: [inkycal-modules-template](https://github.com/aceinnolab/inkycal-modules-template). Just fork that repo, add your module and give me a shout via Discord, Github or Email.
|
||||
|
||||
|
||||
### Pull Requests
|
||||
|
||||
4
.github/workflows/greetings.yml
vendored
4
.github/workflows/greetings.yml
vendored
@@ -12,5 +12,5 @@ jobs:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: "Hi there and welcome to Inkycal. Thanks for opening this issue. As this is your first issue in this repository, please read through the [contributing guidelines](https://github.com/aceisace/Inkycal/blob/main/.github/CONTRIBUTING.md)"
|
||||
pr-message: "Hi there and welcome to Inkycal. Thanks for opening this issue. As this is your first Pull-Request in this repository, please read through the [contributing guidelines](https://github.com/aceisace/Inkycal/blob/main/.github/CONTRIBUTING.md). Please note that non-critical pull-request cannot be merged into the main branch to ensure stability. Please create a new branch and ask to have it merged into main. Thanks for your understanding."
|
||||
issue-message: "Hi there and welcome to Inkycal. Thanks for opening this issue. As this is your first issue in this repository, please read through the [contributing guidelines](https://github.com/aceinnolab/Inkycal/blob/main/.github/CONTRIBUTING.md)"
|
||||
pr-message: "Hi there and welcome to Inkycal. Thanks for opening this issue. As this is your first Pull-Request in this repository, please read through the [contributing guidelines](https://github.com/aceinnolab/Inkycal/blob/main/.github/CONTRIBUTING.md). Please note that non-critical pull-request cannot be merged into the main branch to ensure stability. Please create a new branch and ask to have it merged into main. Thanks for your understanding."
|
||||
|
||||
9
.github/workflows/test-on-rpi.yml
vendored
9
.github/workflows/test-on-rpi.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
TINDIE_USERNAME: ${{ secrets.TINDIE_USERNAME }}
|
||||
with:
|
||||
# Set the base_image to the desired Raspberry Pi OS version
|
||||
base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2024-03-15/2024-03-15-raspios-bookworm-armhf-lite.img.xz
|
||||
base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2025-05-13/2025-05-13-raspios-bookworm-armhf-lite.img.xz
|
||||
image_additional_mb: 3072 # enlarge free space to 3GB
|
||||
optimize_image: true
|
||||
# user: inky --> not supported?
|
||||
@@ -41,8 +41,9 @@ jobs:
|
||||
echo $HOME
|
||||
whoami
|
||||
cd /home/inky
|
||||
sudo apt update
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev -y
|
||||
sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev build-essential libxml2-dev libxslt1-dev python3-dev -y
|
||||
echo $PWD && ls
|
||||
git clone https://github.com/aceinnolab/Inkycal
|
||||
cd Inkycal
|
||||
@@ -51,7 +52,7 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pip install wheel
|
||||
pip install -e ./
|
||||
pip install RPi.GPIO==0.7.1 spidev==3.5 gpiozero==2.0
|
||||
pip install RPi.GPIO==0.7.1 spidev==3.7 lgpio==0.2.2.0
|
||||
wget https://raw.githubusercontent.com/aceinnolab/Inkycal/assets/tests/settings.json
|
||||
pip install pytest
|
||||
python -m pytest
|
||||
python -m pytest
|
||||
9
.github/workflows/update-docs.yml
vendored
9
.github/workflows/update-docs.yml
vendored
@@ -44,5 +44,10 @@ jobs:
|
||||
git config user.name "github-actions"
|
||||
git config user.email "actions@github.com"
|
||||
git add docs/*
|
||||
git commit -m "update docs [bot]"
|
||||
git push
|
||||
# Check if anything is staged before committing
|
||||
if git diff --cached --quiet; then
|
||||
echo "Nothing to commit."
|
||||
else
|
||||
git commit -m "update docs [bot]"
|
||||
git push
|
||||
fi
|
||||
|
||||
35
.github/workflows/update-os.yml
vendored
35
.github/workflows/update-os.yml
vendored
@@ -24,8 +24,8 @@ jobs:
|
||||
TINDIE_USERNAME: ${{ secrets.TINDIE_USERNAME }}
|
||||
with:
|
||||
# Set the base_image to the desired Raspberry Pi OS version
|
||||
# note: version 2023-12-11 seems to have issues with the kernel and gpio
|
||||
base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf-lite.img.xz
|
||||
# note: version 2023-12-11 onwards seems to have issues with the kernel and gpio. Using later versions requires some additional steps
|
||||
base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2025-05-13/2025-05-13-raspios-bookworm-armhf-lite.img.xz
|
||||
image_additional_mb: 3072 # enlarge free space to 3 GB
|
||||
optimize_image: true
|
||||
commands: |
|
||||
@@ -37,11 +37,12 @@ jobs:
|
||||
# get kernel info
|
||||
uname -srm
|
||||
cd /home/inky
|
||||
sudo apt update
|
||||
sudo apt-get update -y
|
||||
# sudo apt-get dist-upgrade -y
|
||||
|
||||
sudo apt-get install -y python3-pip
|
||||
sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev libxml2-dev libxslt-dev python-dev-is-python3 -y
|
||||
sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev build-essential libxml2-dev libxslt1-dev python3-dev -y
|
||||
# #334 & #335
|
||||
git clone https://github.com/WiringPi/WiringPi
|
||||
cd WiringPi
|
||||
@@ -56,7 +57,31 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pip install wheel
|
||||
pip install -e ./
|
||||
pip install RPi.GPIO==0.7.1 spidev==3.5 gpiozero==2.0
|
||||
pip install RPi.GPIO==0.7.1 spidev==3.7 lgpio==0.2.2.0
|
||||
|
||||
# specific hacks to get this running on newer kernels, see #387. Special thanks to pbarthelemy
|
||||
wget https://github.com/aceinnolab/Inkycal/raw/refs/heads/assets/hosting/pcre2-10.44.tar.bz2
|
||||
bzip2 -d pcre2-10.44.tar.bz2
|
||||
tar -xf pcre2-10.44.tar
|
||||
cd pcre2-10.44/
|
||||
./configure && make && sudo make install && make clean
|
||||
cd ..
|
||||
|
||||
wget https://github.com/aceinnolab/Inkycal/raw/refs/heads/assets/hosting/swig-4.3.0.tar
|
||||
tar -xf swig-4.3.0.tar
|
||||
cd swig-4.3.0/
|
||||
./configure && make && sudo make install && make clean
|
||||
cd ..
|
||||
|
||||
wget https://github.com/aceinnolab/Inkycal/raw/refs/heads/assets/hosting/lg.zip
|
||||
unzip lg.zip
|
||||
cd lg
|
||||
make && sudo make install && make clean
|
||||
cd ..
|
||||
|
||||
pip install rpi-lgpio
|
||||
# hacks section end
|
||||
|
||||
wget https://raw.githubusercontent.com/aceinnolab/Inkycal/assets/tests/settings.json
|
||||
pip install pytest
|
||||
python -m pytest
|
||||
@@ -72,7 +97,7 @@ jobs:
|
||||
# increase swap-size
|
||||
# temporarily disabled due to unmounting issues
|
||||
# sudo dphys-swapfile swapoff
|
||||
# sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=512/' /etc/dphys-swapfile
|
||||
# sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=1024/' /etc/dphys-swapfile
|
||||
# sudo dphys-swapfile setup
|
||||
# sudo dphys-swapfile swapon
|
||||
|
||||
|
||||
105
README.md
105
README.md
@@ -5,7 +5,7 @@
|
||||
<a href="https://discord.gg/sHYKeSM"><img src="https://img.shields.io/discord/672082714190544899?style=flat&logo=discord&logoColor=blue&color=lightorange"></a>
|
||||
<a href="https://github.com/aceinnolab/Inkycal/releases"><img alt="Version" src="https://img.shields.io/github/release/aceisace/Inkycal.svg"/></a>
|
||||
<a href="https://github.com/aceinnolab/Inkycal/blob/main/LICENSE"><img alt="Licence" src="https://img.shields.io/github/license/aceisace/Inkycal.svg" /></a>
|
||||
<a href="https://github.com/aceinnolab/Inkycal"><img alt="python" src="https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11-lightorange"></a>
|
||||
<a href="https://github.com/aceinnolab/Inkycal"><img alt="python" src="https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-lightorange"></a>
|
||||
<a href="https://github.com/aceinnolab/Inkycal/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/aceisace/Inkycal?color=yellow"></a>
|
||||
</p>
|
||||
|
||||
@@ -22,15 +22,15 @@ Inkycal can run well even on the Raspberry Pi Zero W. Oh, and it's open for thir
|
||||
|
||||
## ⚠️ Warning: long installation time expected!
|
||||
|
||||
Starting october 2023, Raspberry Pi OS is now based on Debian bookworm and uses python 3.11 instead of 3.9 as the
|
||||
default version. Inkycal has been updated to work with python3.11, but the installation of numpy can take a very long
|
||||
time, in some cases even hours. If you do not want to wait this long to install Inkycal, you can also get a
|
||||
ready-to-flash version of Inkycal called InkycalOS-Lite with everything pre-installed for you by sponsoring
|
||||
via [GitHub Sponsors](https://github.com/sponsors/aceisace). This helps keep up maintenance costs, implement new
|
||||
features and fixing bugs. Please choose the one-time sponsor option and select the one with the plug-and-play version of
|
||||
Inkycal. Then, send your email-address to which InkycalOS-Lite should be sent.
|
||||
Alternatively, you can also use the PayPal.me link and send the same amount as GitHub sponsors to get access to
|
||||
InkycalOS-Lite!
|
||||
Installing Inkycal, particularly on the Raspberry Pi Zero W models can take up to **a few hours**.
|
||||
|
||||
The good news is that this is one-time and InkyCal generally runs without an issue for months or even years.
|
||||
|
||||
The bad news is that the Zero W can run out of memory when installing the required packages. A temporary fix for this is to use SWAP (kind of like a file-based RAM) which is slow, but at least won't lead to
|
||||
|
||||
|
||||
**TLDR: Skip the wait and several hours of headaches, sponsor InkyCal via [GitHub Sponsors](https://github.com/sponsors/aceisace) and you will shortly receive the download link
|
||||
|
||||
|
||||
## Main features
|
||||
|
||||
@@ -80,8 +80,7 @@ display!**
|
||||
|
||||
| type | vendor | Where to buy |
|
||||
|---------------------------------------------------------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 12.48" Inkycal (plug-and-play) | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-1248-build/) Pre-configured version of Inkycal with matte black aluminium designer frame and a web-ui. You do not need to buy anything extra. Includes Raspberry Pi Zero W, 12.48" e-paper, microSD card, driver board, custom packaging and 1m of cable. Comes pre-assembled for plug-and-play. |
|
||||
| 7.5" Inkycal (plug-and-play) | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceisace4444/inkycal-build-v1/) Pre-configured version of Inkycal with custom frame and a web-ui. You do not need to buy anything extra. Includes Raspberry Pi Zero W, 7.5" e-paper, microSD card, driver board, custom packaging and 1m of cable. Comes pre-assembled for plug-and-play. |
|
||||
| 7.5" Inkycal (plug-and-play) | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-create-your-own-e-paper-dashboard/) 7" black-white-red e-paper with custom 3d-printed case, fully pre-assembled (Raspberry Pi Zero W, 7.5" e-paper, microSD card, driver board, custom packaging and 1m of cable). Also grants access to InkyCalOS-Lite. You only need to generate the settings.json file and copy it to the microSD card |
|
||||
| Inkycal frame (kit -> requires wires, 7.5" Display and Zero W with microSD card | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-frame-custom-driver-board-only/) Ultraslim frame with custom-made front and backcover inkl. ultraslim driver board). You will need a Raspberry Pi, microSD card and a 7.5" e-paper display |
|
||||
| Driver board | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/universal-e-paper-driver-board-for-24-pin-spi/) Ultraslim, 24-pin SPI driver board for many serial e-paper displays. |
|
||||
| `[serial]` 12.48" (1304×984px) display | waveshare / gooddisplay | Search for `Waveshare 12.48" E-Paper 1304×984` on amazon or similar |
|
||||
@@ -113,7 +112,7 @@ Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager]
|
||||
| set timezone | your local timezone |
|
||||
|
||||
1. Create and download `settings.json` file for Inkycal from
|
||||
the [WEB-UI](https://aceinnolab.com/inkycal/ui). Add the modules you want with the add
|
||||
the [WEB-UI](https://inkycal.aceinnolab.com/ui). Add the modules you want with the add
|
||||
module button.
|
||||
2. Copy the `settings.json` to the flashed microSD card.
|
||||
3. Eject the microSD card from your computer now, insert it in the Raspberry Pi and power the Raspberry Pi.
|
||||
@@ -141,16 +140,16 @@ sudo ./configure && sudo make && sudo make check && sudo make install
|
||||
|
||||
# If you are using the Raspberry Pi Zero models, you may need to increase the swapfile size to be able to install Inkycal:
|
||||
sudo dphys-swapfile swapoff
|
||||
sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=512/' /etc/dphys-swapfile
|
||||
sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=1024/' /etc/dphys-swapfile
|
||||
sudo dphys-swapfile setup
|
||||
sudo dphys-swapfile swapon
|
||||
```
|
||||
|
||||
These commands expand the filesystem, enable SPI and set up the correct timezone on the Raspberry Pi. When running the
|
||||
last command, please select the continent you live in, press enter and then select the capital of the country you live
|
||||
in. Lastly, press enter.
|
||||
in. Lastly, press enter.
|
||||
|
||||
7. Follow the steps in `Installation` (see below) on how to install Inkycal.
|
||||
Follow the steps in `Installation` (see below) on how to install Inkycal.
|
||||
|
||||
## Installing Inkycal
|
||||
|
||||
@@ -180,11 +179,18 @@ Run the following steps to install Inkycal. Do **not** use sudo for this, except
|
||||
|
||||
```bash
|
||||
# Raspberry Pi specific section start
|
||||
sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev
|
||||
sudo apt update
|
||||
sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev build-essential libxml2-dev libxslt1-dev python3-dev -y
|
||||
git clone https://github.com/WiringPi/WiringPi
|
||||
cd WiringPi
|
||||
./build
|
||||
cd ..
|
||||
|
||||
# python3.9 can lead to issues, hence an update to python3.11 is strongly recommended:
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
sudo apt update
|
||||
sudo apt install python3.11
|
||||
|
||||
# Raspberry Pi specific section end
|
||||
|
||||
cd $HOME
|
||||
@@ -198,7 +204,7 @@ pip install -e ./
|
||||
|
||||
|
||||
# only for Raspberry Pi:
|
||||
pip install RPi.GPIO==0.7.1 spidev==3.5 gpiozero==2.0
|
||||
pip install RPi.GPIO==0.7.1 spidev==3.7 lgpio==0.2.2.0
|
||||
```
|
||||
|
||||
## Running Inkycal
|
||||
@@ -269,10 +275,60 @@ With your setup being complete at this stage, you may want to 3d-print a case. T
|
||||
friendly community:
|
||||
[3D-printable case](https://github.com/aceinnolab/Inkycal/wiki/3D-printable-files)
|
||||
|
||||
## Directory structure
|
||||
```tree
|
||||
├── __init__.py
|
||||
├── custom (custom functions of Inkycal are inside here)
|
||||
│ ├── __init__.py
|
||||
│ ├── functions.py
|
||||
│ ├── inkycal_exceptions.py
|
||||
│ └── openweathermap_wrapper.py
|
||||
├── display (display drivers and functions)
|
||||
│ ├── __init__.py
|
||||
│ ├── display.py (this file acts like a wrapper for the display drivers)
|
||||
│ ├── drivers (actual driver files are inside here)
|
||||
│ │ ├── epd_7_in_5_colour.py (7.5" display driver). Each supported display has it's own driver
|
||||
│ │ └── parallel_drivers (parallel display drivers, e.g. 9.7", 10.2" etc.)
|
||||
│ ├── supported_models.py (this file contains the supported display models and is used to check which displays are supported)
|
||||
│ └── test_display.py (a dummy driver which does not require a display to be attached)
|
||||
├── fonts (fonts used by Inkycal are located here)
|
||||
│ ├── NotoSansUI
|
||||
│ ├── ProFont
|
||||
│ └── WeatherFont
|
||||
├── loggers.py (logging functions)
|
||||
├── main.py (main file to run Inkycal)
|
||||
├── modules (inkycal modules, e.g. calendar, weather, stocks etc.)
|
||||
│ ├── __init__.py
|
||||
│ ├── dev_module.py (a dummy module for development)
|
||||
│ ├── ical_parser.py (parses icalendar files, not strictly a module, but helper class)
|
||||
│ ├── inky_image.py (module to display images)
|
||||
│ ├── inkycal_agenda.py (agenda module)
|
||||
│ ├── inkycal_calendar.py (calendar module)
|
||||
│ ├── inkycal_feeds.py (feeds module)
|
||||
│ ├── inkycal_fullweather.py (full-weather module)
|
||||
│ ├── inkycal_image.py (image module)
|
||||
│ ├── inkycal_jokes.py (jokes module)
|
||||
│ ├── inkycal_server.py (module for inkycal-server, by third party)
|
||||
│ ├── inkycal_slideshow.py (slideshow module)
|
||||
│ ├── inkycal_stocks.py (stocks module - credit to @worstface)
|
||||
│ ├── inkycal_textfile_to_display.py (module to display text files)
|
||||
│ ├── inkycal_tindie.py (tindie module)
|
||||
│ ├── inkycal_todoist.py (todoist module)
|
||||
│ ├── inkycal_weather.py (weather module)
|
||||
│ ├── inkycal_webshot.py (webshot module - credit to @worstface)
|
||||
│ ├── inkycal_xkcd.py (xkcd module - credit to @worstface)
|
||||
│ └── template.py (template module)
|
||||
├── settings.py (settings for Inkycal)
|
||||
└── utils (utility functions)
|
||||
├── __init__.py
|
||||
├── json_cache.py
|
||||
└── pisugar.py (PiSugar driver)
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
All sorts of contributions are most welcome and appreciated. To start contributing, please follow
|
||||
the [Contribution Guidelines](https://github.com/aceisace/Inkycal/blob/main/.github/CONTRIBUTING.md)
|
||||
the [Contribution Guidelines](https://github.com/aceinnolab/Inkycal/blob/main/.github/CONTRIBUTING.md)
|
||||
|
||||
The average response time for issues, PRs and emails is usually 24 hours. In some cases, it might be longer. If you want
|
||||
to have some faster responses, please use Discord (link below)
|
||||
@@ -282,20 +338,20 @@ to have some faster responses, please use Discord (link below)
|
||||
## Join us on Discord!
|
||||
|
||||
We're happy to help, to beginners and developers alike. In fact, you are more likely to get faster support on Discord
|
||||
than on Github.
|
||||
than on GitHub.
|
||||
|
||||
<a href="https://discord.gg/sHYKeSM">
|
||||
<img src="https://github.com/aceisace/Inkycal/blob/assets/Repo/discord-logo.png?raw=true" alt="Inkycal chatroom Discord" width=200>
|
||||
<img src="https://github.com/aceinnolab/Inkycal/blob/assets/Repo/discord-logo.png?raw=true" alt="Inkycal chatroom Discord" width=200>
|
||||
</a>
|
||||
|
||||
## Sponsoring
|
||||
|
||||
Inkycal relies on sponsors to keep up maintainance, development and bug-fixing. Please consider sponsoring Inkycal via
|
||||
Inkycal relies on sponsors to keep up maintenance, development and bug-fixing. Please consider sponsoring Inkycal via
|
||||
the sponsor button if you are happy with Inkycal.
|
||||
|
||||
We now offer perks depending on the amount contributed for sponsoring, ranging from pre-configured OS images for
|
||||
plug-and-play to development of user-suggested modules. Check out the sponsor page to find out more.
|
||||
If you have been a previous sponsor, please let us know on our Dicord server or by sending an email. We'll send you the
|
||||
If you have been a previous sponsor, please let us know on our Discord server or by sending an email. We'll send you the
|
||||
perks after confirming 💯
|
||||
|
||||
## As featured on
|
||||
@@ -304,6 +360,7 @@ perks after confirming 💯
|
||||
* [hackster.io](https://www.hackster.io/news/ace-innovation-lab-s-inkycal-v3-puts-a-raspberry-pi-powered-modular-epaper-dashboard-on-your-desk-b55a83cc0f46)
|
||||
* [raspberryme.com](https://www.raspberryme.com/inkycal-v3-est-un-tableau-de-bord-epaper-alimente-par-raspberry-pi-pour-votre-bureau/)
|
||||
* [adafruit.com](https://blog.adafruit.com/2023/12/19/icymi-python-on-microcontrollers-newsletter-circuitpython-9-alpha-6-released-gpt-via-circuitpython-new-books-and-more-circuitpython-python-micropython-icymi-raspberry_pi/)
|
||||
* [all3dp.com](https://all3dp.com/1/best-raspberry-pi-projects/)
|
||||
* [ittagesschau.de](https://www.ittagesschau.de/artikel/inkycal-v3-smartes-display-auf-grundlage-des-raspberry-pi-mit-elektronischem-papier-und-vielen-moglichkeiten_365893)
|
||||
* [makeuseof - fantastic projects using an eink display](http://makeuseof.com/fantastic-projects-using-an-e-ink-display/)
|
||||
* [notebookcheck.com](https://www.notebookcheck.com/Inkycal-V3-Smartes-Display-auf-Grundlage-des-Raspberry-Pi-mit-elektronischem-Papier-und-vielen-Moeglichkeiten.783012.0.html?ref=ittagesschau.de)
|
||||
@@ -323,5 +380,5 @@ perks after confirming 💯
|
||||
|
||||
## Our Contributors
|
||||
|
||||
<table><tr><td align="center"><a href="https://github.com/aceisace"><img alt="aceisace" src="https://avatars.githubusercontent.com/u/29558518?v=4" width="117" /><br />aceisace</a></td><td align="center"><a href="https://github.com/Atrejoe"><img alt="Atrejoe" src="https://avatars.githubusercontent.com/u/585091?v=4" width="117" /><br />Atrejoe</a></td><td align="center"><a href="https://github.com/actions-user"><img alt="actions-user" src="https://avatars.githubusercontent.com/u/65916846?v=4" width="117" /><br />actions-user</a></td><td align="center"><a href="https://github.com/emilyboda"><img alt="emilyboda" src="https://avatars.githubusercontent.com/u/9170143?v=4" width="117" /><br />emilyboda</a></td><td align="center"><a href="https://github.com/StevenSeifried"><img alt="StevenSeifried" src="https://avatars.githubusercontent.com/u/39765956?v=4" width="117" /><br />StevenSeifried</a></td><td align="center"><a href="https://github.com/mrbwburns"><img alt="mrbwburns" src="https://avatars.githubusercontent.com/u/66523867?v=4" width="117" /><br />mrbwburns</a></td></tr><tr><td align="center"><a href="https://github.com/apps/dependabot"><img alt="dependabot[bot]" src="https://avatars.githubusercontent.com/in/29110?v=4" width="117" /><br />dependabot[bot]</a></td><td align="center"><a href="https://github.com/LakesideMiners"><img alt="LakesideMiners" src="https://avatars.githubusercontent.com/u/23389169?v=4" width="117" /><br />LakesideMiners</a></td><td align="center"><a href="https://github.com/hjiang"><img alt="hjiang" src="https://avatars.githubusercontent.com/u/18527?v=4" width="117" /><br />hjiang</a></td><td align="center"><a href="https://github.com/ch3lmi"><img alt="ch3lmi" src="https://avatars.githubusercontent.com/u/19972012?v=4" width="117" /><br />ch3lmi</a></td><td align="center"><a href="https://github.com/mygrexit"><img alt="mygrexit" src="https://avatars.githubusercontent.com/u/33792951?v=4" width="117" /><br />mygrexit</a></td><td align="center"><a href="https://github.com/tobychui"><img alt="tobychui" src="https://avatars.githubusercontent.com/u/24617523?v=4" width="117" /><br />tobychui</a></td></tr><tr><td align="center"><a href="https://github.com/worstface"><img alt="worstface" src="https://avatars.githubusercontent.com/u/72295005?v=4" width="117" /><br />worstface</a></td><td align="center"><a href="https://github.com/sapostoluk"><img alt="sapostoluk" src="https://avatars.githubusercontent.com/u/7192139?v=4" width="117" /><br />sapostoluk</a></td><td align="center"><a href="https://github.com/freezingDaniel"><img alt="freezingDaniel" src="https://avatars.githubusercontent.com/u/82905307?v=4" width="117" /><br />freezingDaniel</a></td><td align="center"><a href="https://github.com/dealyllama"><img alt="dealyllama" src="https://avatars.githubusercontent.com/u/5891782?v=4" width="117" /><br />dealyllama</a></td><td align="center"><a href="https://github.com/rafaljanicki"><img alt="rafaljanicki" src="https://avatars.githubusercontent.com/u/7746477?v=4" width="117" /><br />rafaljanicki</a></td><td align="center"><a href="https://github.com/priv-kweihmann"><img alt="priv-kweihmann" src="https://avatars.githubusercontent.com/u/46938494?v=4" width="117" /><br />priv-kweihmann</a></td></tr><tr><td align="center"><a href="https://github.com/surak"><img alt="surak" src="https://avatars.githubusercontent.com/u/878399?v=4" width="117" /><br />surak</a></td><td align="center"><a href="https://github.com/AlessandroMandelli"><img alt="AlessandroMandelli" src="https://avatars.githubusercontent.com/u/65062723?v=4" width="117" /><br />AlessandroMandelli</a></td><td align="center"><a href="https://github.com/DavidCamre"><img alt="DavidCamre" src="https://avatars.githubusercontent.com/u/1098069?v=4" width="117" /><br />DavidCamre</a></td><td align="center"><a href="https://github.com/jordanschau"><img alt="jordanschau" src="https://avatars.githubusercontent.com/u/412028?v=4" width="117" /><br />jordanschau</a></td><td align="center"><a href="https://github.com/mshulman"><img alt="mshulman" src="https://avatars.githubusercontent.com/u/1484420?v=4" width="117" /><br />mshulman</a></td><td align="center"><a href="https://github.com/vitasam"><img alt="vitasam" src="https://avatars.githubusercontent.com/u/5597505?v=4" width="117" /><br />vitasam</a></td></tr></table>
|
||||
<table><tr><td align="center"><a href="https://github.com/aceinnolab"><img alt="aceinnolab" src="https://avatars.githubusercontent.com/u/29558518?v=4" width="117" /><br />aceisace</a></td><td align="center"><a href="https://github.com/Atrejoe"><img alt="Atrejoe" src="https://avatars.githubusercontent.com/u/585091?v=4" width="117" /><br />Atrejoe</a></td><td align="center"><a href="https://github.com/actions-user"><img alt="actions-user" src="https://avatars.githubusercontent.com/u/65916846?v=4" width="117" /><br />actions-user</a></td><td align="center"><a href="https://github.com/emilyboda"><img alt="emilyboda" src="https://avatars.githubusercontent.com/u/9170143?v=4" width="117" /><br />emilyboda</a></td><td align="center"><a href="https://github.com/StevenSeifried"><img alt="StevenSeifried" src="https://avatars.githubusercontent.com/u/39765956?v=4" width="117" /><br />StevenSeifried</a></td><td align="center"><a href="https://github.com/mrbwburns"><img alt="mrbwburns" src="https://avatars.githubusercontent.com/u/66523867?v=4" width="117" /><br />mrbwburns</a></td></tr><tr><td align="center"><a href="https://github.com/apps/dependabot"><img alt="dependabot[bot]" src="https://avatars.githubusercontent.com/in/29110?v=4" width="117" /><br />dependabot[bot]</a></td><td align="center"><a href="https://github.com/LakesideMiners"><img alt="LakesideMiners" src="https://avatars.githubusercontent.com/u/23389169?v=4" width="117" /><br />LakesideMiners</a></td><td align="center"><a href="https://github.com/hjiang"><img alt="hjiang" src="https://avatars.githubusercontent.com/u/18527?v=4" width="117" /><br />hjiang</a></td><td align="center"><a href="https://github.com/ch3lmi"><img alt="ch3lmi" src="https://avatars.githubusercontent.com/u/19972012?v=4" width="117" /><br />ch3lmi</a></td><td align="center"><a href="https://github.com/mygrexit"><img alt="mygrexit" src="https://avatars.githubusercontent.com/u/33792951?v=4" width="117" /><br />mygrexit</a></td><td align="center"><a href="https://github.com/tobychui"><img alt="tobychui" src="https://avatars.githubusercontent.com/u/24617523?v=4" width="117" /><br />tobychui</a></td></tr><tr><td align="center"><a href="https://github.com/worstface"><img alt="worstface" src="https://avatars.githubusercontent.com/u/72295005?v=4" width="117" /><br />worstface</a></td><td align="center"><a href="https://github.com/sapostoluk"><img alt="sapostoluk" src="https://avatars.githubusercontent.com/u/7192139?v=4" width="117" /><br />sapostoluk</a></td><td align="center"><a href="https://github.com/freezingDaniel"><img alt="freezingDaniel" src="https://avatars.githubusercontent.com/u/82905307?v=4" width="117" /><br />freezingDaniel</a></td><td align="center"><a href="https://github.com/dealyllama"><img alt="dealyllama" src="https://avatars.githubusercontent.com/u/5891782?v=4" width="117" /><br />dealyllama</a></td><td align="center"><a href="https://github.com/rafaljanicki"><img alt="rafaljanicki" src="https://avatars.githubusercontent.com/u/7746477?v=4" width="117" /><br />rafaljanicki</a></td><td align="center"><a href="https://github.com/priv-kweihmann"><img alt="priv-kweihmann" src="https://avatars.githubusercontent.com/u/46938494?v=4" width="117" /><br />priv-kweihmann</a></td></tr><tr><td align="center"><a href="https://github.com/surak"><img alt="surak" src="https://avatars.githubusercontent.com/u/878399?v=4" width="117" /><br />surak</a></td><td align="center"><a href="https://github.com/AlessandroMandelli"><img alt="AlessandroMandelli" src="https://avatars.githubusercontent.com/u/65062723?v=4" width="117" /><br />AlessandroMandelli</a></td><td align="center"><a href="https://github.com/DavidCamre"><img alt="DavidCamre" src="https://avatars.githubusercontent.com/u/1098069?v=4" width="117" /><br />DavidCamre</a></td><td align="center"><a href="https://github.com/jordanschau"><img alt="jordanschau" src="https://avatars.githubusercontent.com/u/412028?v=4" width="117" /><br />jordanschau</a></td><td align="center"><a href="https://github.com/mshulman"><img alt="mshulman" src="https://avatars.githubusercontent.com/u/1484420?v=4" width="117" /><br />mshulman</a></td><td align="center"><a href="https://github.com/vitasam"><img alt="vitasam" src="https://avatars.githubusercontent.com/u/5597505?v=4" width="117" /><br />vitasam</a></td></tr></table>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# About Inkycal
|
||||
<img align="center" src="https://raw.githubusercontent.com/aceisace/Inkycal/assets/Repo/logo.png" width="800" alt="inkycal logo">
|
||||
<img align="center" src="https://raw.githubusercontent.com/aceinnolab/Inkycal/assets/Repo/logo.png" width="800" alt="inkycal logo">
|
||||
|
||||
Inkycal is a python3 software for selected E-Paper displays.
|
||||
It's open-source (non-commercially), fully modular, user-friendly and even runs
|
||||
|
||||
@@ -17,7 +17,7 @@ pip3 install -e ./
|
||||
```
|
||||
|
||||
## Creating settings file
|
||||
Please navigate to the [WEB-UI](https://aceisace.eu.pythonanywhere.com/index) to create your settings file.
|
||||
Please navigate to the [WEB-UI](https://inkycal.aceinnolab.com) to create your settings file.
|
||||
|
||||
Copy the generated settings file to the Raspberry Pi
|
||||
more coming soon..
|
||||
|
||||
23
docs/_static/basic.css
vendored
23
docs/_static/basic.css
vendored
@@ -1,12 +1,5 @@
|
||||
/*
|
||||
* basic.css
|
||||
* ~~~~~~~~~
|
||||
*
|
||||
* Sphinx stylesheet -- basic theme.
|
||||
*
|
||||
* :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
|
||||
/* -- main layout ----------------------------------------------------------- */
|
||||
@@ -115,15 +108,11 @@ img {
|
||||
/* -- search page ----------------------------------------------------------- */
|
||||
|
||||
ul.search {
|
||||
margin: 10px 0 0 20px;
|
||||
padding: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
ul.search li {
|
||||
padding: 5px 0 5px 20px;
|
||||
background-image: url(file.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 7px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
ul.search li a {
|
||||
@@ -752,14 +741,6 @@ abbr, acronym {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.translated {
|
||||
background-color: rgba(207, 255, 207, 0.2)
|
||||
}
|
||||
|
||||
.untranslated {
|
||||
background-color: rgba(255, 207, 207, 0.2)
|
||||
}
|
||||
|
||||
/* -- code displays --------------------------------------------------------- */
|
||||
|
||||
pre {
|
||||
|
||||
2
docs/_static/css/badge_only.css
vendored
2
docs/_static/css/badge_only.css
vendored
@@ -1 +1 @@
|
||||
.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}
|
||||
.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px}
|
||||
2
docs/_static/css/theme.css
vendored
2
docs/_static/css/theme.css
vendored
File diff suppressed because one or more lines are too long
7
docs/_static/doctools.js
vendored
7
docs/_static/doctools.js
vendored
@@ -1,12 +1,5 @@
|
||||
/*
|
||||
* doctools.js
|
||||
* ~~~~~~~~~~~
|
||||
*
|
||||
* Base JavaScript utilities for all Sphinx HTML documentation.
|
||||
*
|
||||
* :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
|
||||
BIN
docs/_static/fonts/Lato/lato-bold.eot
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-bold.eot
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-bold.ttf
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-bold.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-bold.woff
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-bold.woff
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-bold.woff2
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-bold.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-bolditalic.eot
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-bolditalic.eot
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-bolditalic.ttf
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-bolditalic.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-bolditalic.woff
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-bolditalic.woff
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-bolditalic.woff2
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-bolditalic.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-italic.eot
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-italic.eot
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-italic.ttf
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-italic.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-italic.woff
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-italic.woff
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-italic.woff2
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-italic.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-regular.eot
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-regular.eot
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-regular.ttf
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-regular.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-regular.woff
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Lato/lato-regular.woff2
vendored
Normal file
BIN
docs/_static/fonts/Lato/lato-regular.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2
vendored
Normal file
BIN
docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2
vendored
Normal file
Binary file not shown.
228
docs/_static/js/versions.js
vendored
Normal file
228
docs/_static/js/versions.js
vendored
Normal file
@@ -0,0 +1,228 @@
|
||||
const themeFlyoutDisplay = "hidden";
|
||||
const themeVersionSelector = true;
|
||||
const themeLanguageSelector = true;
|
||||
|
||||
if (themeFlyoutDisplay === "attached") {
|
||||
function renderLanguages(config) {
|
||||
if (!config.projects.translations.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Insert the current language to the options on the selector
|
||||
let languages = config.projects.translations.concat(config.projects.current);
|
||||
languages = languages.sort((a, b) => a.language.name.localeCompare(b.language.name));
|
||||
|
||||
const languagesHTML = `
|
||||
<dl>
|
||||
<dt>Languages</dt>
|
||||
${languages
|
||||
.map(
|
||||
(translation) => `
|
||||
<dd ${translation.slug == config.projects.current.slug ? 'class="rtd-current-item"' : ""}>
|
||||
<a href="${translation.urls.documentation}">${translation.language.code}</a>
|
||||
</dd>
|
||||
`,
|
||||
)
|
||||
.join("\n")}
|
||||
</dl>
|
||||
`;
|
||||
return languagesHTML;
|
||||
}
|
||||
|
||||
function renderVersions(config) {
|
||||
if (!config.versions.active.length) {
|
||||
return "";
|
||||
}
|
||||
const versionsHTML = `
|
||||
<dl>
|
||||
<dt>Versions</dt>
|
||||
${config.versions.active
|
||||
.map(
|
||||
(version) => `
|
||||
<dd ${version.slug === config.versions.current.slug ? 'class="rtd-current-item"' : ""}>
|
||||
<a href="${version.urls.documentation}">${version.slug}</a>
|
||||
</dd>
|
||||
`,
|
||||
)
|
||||
.join("\n")}
|
||||
</dl>
|
||||
`;
|
||||
return versionsHTML;
|
||||
}
|
||||
|
||||
function renderDownloads(config) {
|
||||
if (!Object.keys(config.versions.current.downloads).length) {
|
||||
return "";
|
||||
}
|
||||
const downloadsNameDisplay = {
|
||||
pdf: "PDF",
|
||||
epub: "Epub",
|
||||
htmlzip: "HTML",
|
||||
};
|
||||
|
||||
const downloadsHTML = `
|
||||
<dl>
|
||||
<dt>Downloads</dt>
|
||||
${Object.entries(config.versions.current.downloads)
|
||||
.map(
|
||||
([name, url]) => `
|
||||
<dd>
|
||||
<a href="${url}">${downloadsNameDisplay[name]}</a>
|
||||
</dd>
|
||||
`,
|
||||
)
|
||||
.join("\n")}
|
||||
</dl>
|
||||
`;
|
||||
return downloadsHTML;
|
||||
}
|
||||
|
||||
document.addEventListener("readthedocs-addons-data-ready", function (event) {
|
||||
const config = event.detail.data();
|
||||
|
||||
const flyout = `
|
||||
<div class="rst-versions" data-toggle="rst-versions" role="note">
|
||||
<span class="rst-current-version" data-toggle="rst-current-version">
|
||||
<span class="fa fa-book"> Read the Docs</span>
|
||||
v: ${config.versions.current.slug}
|
||||
<span class="fa fa-caret-down"></span>
|
||||
</span>
|
||||
<div class="rst-other-versions">
|
||||
<div class="injected">
|
||||
${renderLanguages(config)}
|
||||
${renderVersions(config)}
|
||||
${renderDownloads(config)}
|
||||
<dl>
|
||||
<dt>On Read the Docs</dt>
|
||||
<dd>
|
||||
<a href="${config.projects.current.urls.home}">Project Home</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a href="${config.projects.current.urls.builds}">Builds</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a href="${config.projects.current.urls.downloads}">Downloads</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>Search</dt>
|
||||
<dd>
|
||||
<form id="flyout-search-form">
|
||||
<input
|
||||
class="wy-form"
|
||||
type="text"
|
||||
name="q"
|
||||
aria-label="Search docs"
|
||||
placeholder="Search docs"
|
||||
/>
|
||||
</form>
|
||||
</dd>
|
||||
</dl>
|
||||
<hr />
|
||||
<small>
|
||||
<span>Hosted by <a href="https://about.readthedocs.org/?utm_source=&utm_content=flyout">Read the Docs</a></span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Inject the generated flyout into the body HTML element.
|
||||
document.body.insertAdjacentHTML("beforeend", flyout);
|
||||
|
||||
// Trigger the Read the Docs Addons Search modal when clicking on the "Search docs" input from inside the flyout.
|
||||
document
|
||||
.querySelector("#flyout-search-form")
|
||||
.addEventListener("focusin", () => {
|
||||
const event = new CustomEvent("readthedocs-search-show");
|
||||
document.dispatchEvent(event);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
if (themeLanguageSelector || themeVersionSelector) {
|
||||
function onSelectorSwitch(event) {
|
||||
const option = event.target.selectedIndex;
|
||||
const item = event.target.options[option];
|
||||
window.location.href = item.dataset.url;
|
||||
}
|
||||
|
||||
document.addEventListener("readthedocs-addons-data-ready", function (event) {
|
||||
const config = event.detail.data();
|
||||
|
||||
const versionSwitch = document.querySelector(
|
||||
"div.switch-menus > div.version-switch",
|
||||
);
|
||||
if (themeVersionSelector) {
|
||||
let versions = config.versions.active;
|
||||
if (config.versions.current.hidden || config.versions.current.type === "external") {
|
||||
versions.unshift(config.versions.current);
|
||||
}
|
||||
const versionSelect = `
|
||||
<select>
|
||||
${versions
|
||||
.map(
|
||||
(version) => `
|
||||
<option
|
||||
value="${version.slug}"
|
||||
${config.versions.current.slug === version.slug ? 'selected="selected"' : ""}
|
||||
data-url="${version.urls.documentation}">
|
||||
${version.slug}
|
||||
</option>`,
|
||||
)
|
||||
.join("\n")}
|
||||
</select>
|
||||
`;
|
||||
|
||||
versionSwitch.innerHTML = versionSelect;
|
||||
versionSwitch.firstElementChild.addEventListener("change", onSelectorSwitch);
|
||||
}
|
||||
|
||||
const languageSwitch = document.querySelector(
|
||||
"div.switch-menus > div.language-switch",
|
||||
);
|
||||
|
||||
if (themeLanguageSelector) {
|
||||
if (config.projects.translations.length) {
|
||||
// Add the current language to the options on the selector
|
||||
let languages = config.projects.translations.concat(
|
||||
config.projects.current,
|
||||
);
|
||||
languages = languages.sort((a, b) =>
|
||||
a.language.name.localeCompare(b.language.name),
|
||||
);
|
||||
|
||||
const languageSelect = `
|
||||
<select>
|
||||
${languages
|
||||
.map(
|
||||
(language) => `
|
||||
<option
|
||||
value="${language.language.code}"
|
||||
${config.projects.current.slug === language.slug ? 'selected="selected"' : ""}
|
||||
data-url="${language.urls.documentation}">
|
||||
${language.language.name}
|
||||
</option>`,
|
||||
)
|
||||
.join("\n")}
|
||||
</select>
|
||||
`;
|
||||
|
||||
languageSwitch.innerHTML = languageSelect;
|
||||
languageSwitch.firstElementChild.addEventListener("change", onSelectorSwitch);
|
||||
}
|
||||
else {
|
||||
languageSwitch.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("readthedocs-addons-data-ready", function (event) {
|
||||
// Trigger the Read the Docs Addons Search modal when clicking on "Search docs" input from the topnav.
|
||||
document
|
||||
.querySelector("[role='search'] input")
|
||||
.addEventListener("focusin", () => {
|
||||
const event = new CustomEvent("readthedocs-search-show");
|
||||
document.dispatchEvent(event);
|
||||
});
|
||||
});
|
||||
7
docs/_static/language_data.js
vendored
7
docs/_static/language_data.js
vendored
@@ -1,13 +1,6 @@
|
||||
/*
|
||||
* language_data.js
|
||||
* ~~~~~~~~~~~~~~~~
|
||||
*
|
||||
* This script contains the language-specific data used by searchtools.js,
|
||||
* namely the list of stopwords, stemmer, scorer and splitter.
|
||||
*
|
||||
* :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
|
||||
var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"];
|
||||
|
||||
36
docs/_static/pygments.css
vendored
36
docs/_static/pygments.css
vendored
@@ -6,9 +6,9 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left:
|
||||
.highlight .hll { background-color: #ffffcc }
|
||||
.highlight { background: #f8f8f8; }
|
||||
.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */
|
||||
.highlight .err { border: 1px solid #FF0000 } /* Error */
|
||||
.highlight .err { border: 1px solid #F00 } /* Error */
|
||||
.highlight .k { color: #008000; font-weight: bold } /* Keyword */
|
||||
.highlight .o { color: #666666 } /* Operator */
|
||||
.highlight .o { color: #666 } /* Operator */
|
||||
.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
|
||||
.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #9C6500 } /* Comment.Preproc */
|
||||
@@ -25,34 +25,34 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left:
|
||||
.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
||||
.highlight .gs { font-weight: bold } /* Generic.Strong */
|
||||
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.highlight .gt { color: #0044DD } /* Generic.Traceback */
|
||||
.highlight .gt { color: #04D } /* Generic.Traceback */
|
||||
.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
|
||||
.highlight .kp { color: #008000 } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #B00040 } /* Keyword.Type */
|
||||
.highlight .m { color: #666666 } /* Literal.Number */
|
||||
.highlight .m { color: #666 } /* Literal.Number */
|
||||
.highlight .s { color: #BA2121 } /* Literal.String */
|
||||
.highlight .na { color: #687822 } /* Name.Attribute */
|
||||
.highlight .nb { color: #008000 } /* Name.Builtin */
|
||||
.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */
|
||||
.highlight .no { color: #880000 } /* Name.Constant */
|
||||
.highlight .nd { color: #AA22FF } /* Name.Decorator */
|
||||
.highlight .nc { color: #00F; font-weight: bold } /* Name.Class */
|
||||
.highlight .no { color: #800 } /* Name.Constant */
|
||||
.highlight .nd { color: #A2F } /* Name.Decorator */
|
||||
.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */
|
||||
.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
|
||||
.highlight .nf { color: #0000FF } /* Name.Function */
|
||||
.highlight .nf { color: #00F } /* Name.Function */
|
||||
.highlight .nl { color: #767600 } /* Name.Label */
|
||||
.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
|
||||
.highlight .nn { color: #00F; font-weight: bold } /* Name.Namespace */
|
||||
.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
|
||||
.highlight .nv { color: #19177C } /* Name.Variable */
|
||||
.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
|
||||
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.highlight .mb { color: #666666 } /* Literal.Number.Bin */
|
||||
.highlight .mf { color: #666666 } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #666666 } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #666666 } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #666666 } /* Literal.Number.Oct */
|
||||
.highlight .ow { color: #A2F; font-weight: bold } /* Operator.Word */
|
||||
.highlight .w { color: #BBB } /* Text.Whitespace */
|
||||
.highlight .mb { color: #666 } /* Literal.Number.Bin */
|
||||
.highlight .mf { color: #666 } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #666 } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #666 } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #666 } /* Literal.Number.Oct */
|
||||
.highlight .sa { color: #BA2121 } /* Literal.String.Affix */
|
||||
.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #BA2121 } /* Literal.String.Char */
|
||||
@@ -67,9 +67,9 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left:
|
||||
.highlight .s1 { color: #BA2121 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #19177C } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
|
||||
.highlight .fm { color: #0000FF } /* Name.Function.Magic */
|
||||
.highlight .fm { color: #00F } /* Name.Function.Magic */
|
||||
.highlight .vc { color: #19177C } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #19177C } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #19177C } /* Name.Variable.Instance */
|
||||
.highlight .vm { color: #19177C } /* Name.Variable.Magic */
|
||||
.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
|
||||
.highlight .il { color: #666 } /* Literal.Number.Integer.Long */
|
||||
51
docs/_static/searchtools.js
vendored
51
docs/_static/searchtools.js
vendored
@@ -1,12 +1,5 @@
|
||||
/*
|
||||
* searchtools.js
|
||||
* ~~~~~~~~~~~~~~~~
|
||||
*
|
||||
* Sphinx JavaScript utilities for the full-text search.
|
||||
*
|
||||
* :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
@@ -20,7 +13,7 @@ if (typeof Scorer === "undefined") {
|
||||
// and returns the new score.
|
||||
/*
|
||||
score: result => {
|
||||
const [docname, title, anchor, descr, score, filename] = result
|
||||
const [docname, title, anchor, descr, score, filename, kind] = result
|
||||
return score
|
||||
},
|
||||
*/
|
||||
@@ -47,6 +40,14 @@ if (typeof Scorer === "undefined") {
|
||||
};
|
||||
}
|
||||
|
||||
// Global search result kind enum, used by themes to style search results.
|
||||
class SearchResultKind {
|
||||
static get index() { return "index"; }
|
||||
static get object() { return "object"; }
|
||||
static get text() { return "text"; }
|
||||
static get title() { return "title"; }
|
||||
}
|
||||
|
||||
const _removeChildren = (element) => {
|
||||
while (element && element.lastChild) element.removeChild(element.lastChild);
|
||||
};
|
||||
@@ -64,9 +65,13 @@ const _displayItem = (item, searchTerms, highlightTerms) => {
|
||||
const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY;
|
||||
const contentRoot = document.documentElement.dataset.content_root;
|
||||
|
||||
const [docName, title, anchor, descr, score, _filename] = item;
|
||||
const [docName, title, anchor, descr, score, _filename, kind] = item;
|
||||
|
||||
let listItem = document.createElement("li");
|
||||
// Add a class representing the item's type:
|
||||
// can be used by a theme's CSS selector for styling
|
||||
// See SearchResultKind for the class names.
|
||||
listItem.classList.add(`kind-${kind}`);
|
||||
let requestUrl;
|
||||
let linkUrl;
|
||||
if (docBuilder === "dirhtml") {
|
||||
@@ -115,8 +120,10 @@ const _finishSearch = (resultCount) => {
|
||||
"Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories."
|
||||
);
|
||||
else
|
||||
Search.status.innerText = _(
|
||||
"Search finished, found ${resultCount} page(s) matching the search query."
|
||||
Search.status.innerText = Documentation.ngettext(
|
||||
"Search finished, found one page matching the search query.",
|
||||
"Search finished, found ${resultCount} pages matching the search query.",
|
||||
resultCount,
|
||||
).replace('${resultCount}', resultCount);
|
||||
};
|
||||
const _displayNextItem = (
|
||||
@@ -138,7 +145,7 @@ const _displayNextItem = (
|
||||
else _finishSearch(resultCount);
|
||||
};
|
||||
// Helper function used by query() to order search results.
|
||||
// Each input is an array of [docname, title, anchor, descr, score, filename].
|
||||
// Each input is an array of [docname, title, anchor, descr, score, filename, kind].
|
||||
// Order the results by score (in opposite order of appearance, since the
|
||||
// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically.
|
||||
const _orderResultsByScoreThenName = (a, b) => {
|
||||
@@ -248,6 +255,7 @@ const Search = {
|
||||
searchSummary.classList.add("search-summary");
|
||||
searchSummary.innerText = "";
|
||||
const searchList = document.createElement("ul");
|
||||
searchList.setAttribute("role", "list");
|
||||
searchList.classList.add("search");
|
||||
|
||||
const out = document.getElementById("search-results");
|
||||
@@ -318,7 +326,7 @@ const Search = {
|
||||
const indexEntries = Search._index.indexentries;
|
||||
|
||||
// Collect multiple result groups to be sorted separately and then ordered.
|
||||
// Each is an array of [docname, title, anchor, descr, score, filename].
|
||||
// Each is an array of [docname, title, anchor, descr, score, filename, kind].
|
||||
const normalResults = [];
|
||||
const nonMainIndexResults = [];
|
||||
|
||||
@@ -337,6 +345,7 @@ const Search = {
|
||||
null,
|
||||
score + boost,
|
||||
filenames[file],
|
||||
SearchResultKind.title,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -354,6 +363,7 @@ const Search = {
|
||||
null,
|
||||
score,
|
||||
filenames[file],
|
||||
SearchResultKind.index,
|
||||
];
|
||||
if (isMain) {
|
||||
normalResults.push(result);
|
||||
@@ -475,6 +485,7 @@ const Search = {
|
||||
descr,
|
||||
score,
|
||||
filenames[match[0]],
|
||||
SearchResultKind.object,
|
||||
]);
|
||||
};
|
||||
Object.keys(objects).forEach((prefix) =>
|
||||
@@ -502,9 +513,11 @@ const Search = {
|
||||
// perform the search on the required terms
|
||||
searchTerms.forEach((word) => {
|
||||
const files = [];
|
||||
// find documents, if any, containing the query word in their text/title term indices
|
||||
// use Object.hasOwnProperty to avoid mismatching against prototype properties
|
||||
const arr = [
|
||||
{ files: terms[word], score: Scorer.term },
|
||||
{ files: titleTerms[word], score: Scorer.title },
|
||||
{ files: terms.hasOwnProperty(word) ? terms[word] : undefined, score: Scorer.term },
|
||||
{ files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, score: Scorer.title },
|
||||
];
|
||||
// add support for partial matches
|
||||
if (word.length > 2) {
|
||||
@@ -536,8 +549,9 @@ const Search = {
|
||||
|
||||
// set score for the word in each file
|
||||
recordFiles.forEach((file) => {
|
||||
if (!scoreMap.has(file)) scoreMap.set(file, {});
|
||||
scoreMap.get(file)[word] = record.score;
|
||||
if (!scoreMap.has(file)) scoreMap.set(file, new Map());
|
||||
const fileScores = scoreMap.get(file);
|
||||
fileScores.set(word, record.score);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -576,7 +590,7 @@ const Search = {
|
||||
break;
|
||||
|
||||
// select one (max) score for the file.
|
||||
const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w]));
|
||||
const score = Math.max(...wordList.map((w) => scoreMap.get(file).get(w)));
|
||||
// add result to the result list
|
||||
results.push([
|
||||
docNames[file],
|
||||
@@ -585,6 +599,7 @@ const Search = {
|
||||
null,
|
||||
score,
|
||||
filenames[file],
|
||||
SearchResultKind.text,
|
||||
]);
|
||||
}
|
||||
return results;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="writer-html5" lang="en" data-content_root="./">
|
||||
<head>
|
||||
@@ -5,19 +7,15 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>About Inkycal — inkycal 2.0.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
|
||||
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="_static/js/html5shiv.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/js/theme.js"></script>
|
||||
<link rel="author" title="About these documents" href="#" />
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
@@ -82,7 +80,7 @@
|
||||
|
||||
<section id="about-inkycal">
|
||||
<h1>About Inkycal<a class="headerlink" href="#about-inkycal" title="Link to this heading"></a></h1>
|
||||
<img align="center" src="https://raw.githubusercontent.com/aceisace/Inkycal/assets/Repo/logo.png" width="800" alt="inkycal logo"><p>Inkycal is a python3 software for selected E-Paper displays.
|
||||
<img align="center" src="https://raw.githubusercontent.com/aceinnolab/Inkycal/assets/Repo/logo.png" width="800" alt="inkycal logo"><p>Inkycal is a python3 software for selected E-Paper displays.
|
||||
It’s open-source (non-commercially), fully modular, user-friendly and even runs
|
||||
well even on the Raspberry Pi Zero. Inkycal even has a web-UI which takes
|
||||
care of adding your details! No more editing files, Yay :partying_face:</p>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="writer-html5" lang="en" data-content_root="./">
|
||||
<head>
|
||||
@@ -5,19 +7,15 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Developer documentation — inkycal 2.0.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
|
||||
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="_static/js/html5shiv.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/js/theme.js"></script>
|
||||
<link rel="author" title="About these documents" href="about.html" />
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="writer-html5" lang="en" data-content_root="./">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Index — inkycal 2.0.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
|
||||
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="_static/js/html5shiv.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/js/theme.js"></script>
|
||||
<link rel="author" title="About these documents" href="about.html" />
|
||||
<link rel="index" title="Index" href="#" />
|
||||
@@ -255,10 +253,6 @@
|
||||
|
||||
<h2 id="P">P</h2>
|
||||
<table style="width: 100%" class="indextable genindextable"><tr>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="inkycal.html#inkycal.modules.inky_image.Inkyimage.preview">preview() (inkycal.modules.inky_image.Inkyimage static method)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="inkycal.html#inkycal.main.Inkycal.process_module">process_module() (inkycal.main.Inkycal method)</a>
|
||||
</li>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="writer-html5" lang="en" data-content_root="./">
|
||||
<head>
|
||||
@@ -5,19 +7,15 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Inkycal documentation — inkycal 2.0.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
|
||||
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="_static/js/html5shiv.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/js/theme.js"></script>
|
||||
<link rel="author" title="About these documents" href="about.html" />
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="writer-html5" lang="en" data-content_root="./">
|
||||
<head>
|
||||
@@ -5,19 +7,15 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Inkycal — inkycal 2.0.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
|
||||
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="_static/js/html5shiv.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/js/theme.js"></script>
|
||||
<link rel="author" title="About these documents" href="about.html" />
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
@@ -87,7 +85,6 @@
|
||||
<li class="toctree-l3"><a class="reference internal" href="#inkycal.modules.inky_image.Inkyimage.flip"><code class="docutils literal notranslate"><span class="pre">Inkyimage.flip()</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="#inkycal.modules.inky_image.Inkyimage.load"><code class="docutils literal notranslate"><span class="pre">Inkyimage.load()</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="#inkycal.modules.inky_image.Inkyimage.merge"><code class="docutils literal notranslate"><span class="pre">Inkyimage.merge()</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="#inkycal.modules.inky_image.Inkyimage.preview"><code class="docutils literal notranslate"><span class="pre">Inkyimage.preview()</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="#inkycal.modules.inky_image.Inkyimage.remove_alpha"><code class="docutils literal notranslate"><span class="pre">Inkyimage.remove_alpha()</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="#inkycal.modules.inky_image.Inkyimage.resize"><code class="docutils literal notranslate"><span class="pre">Inkyimage.resize()</span></code></a></li>
|
||||
</ul>
|
||||
@@ -131,7 +128,7 @@
|
||||
Copyright by aceinnolab</p>
|
||||
<dl class="py class">
|
||||
<dt class="sig sig-object py" id="inkycal.main.Inkycal">
|
||||
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">inkycal.main.</span></span><span class="sig-name descname"><span class="pre">Inkycal</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">settings_path</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">render</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">bool</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">True</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">use_pi_sugar</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">bool</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">shutdown_after_run</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">bool</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.main.Inkycal" title="Link to this definition"></a></dt>
|
||||
<em class="property"><span class="k"><span class="pre">class</span></span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">inkycal.main.</span></span><span class="sig-name descname"><span class="pre">Inkycal</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">settings_path</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">render</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">bool</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">True</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">use_pi_sugar</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">bool</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">shutdown_after_run</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">bool</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.main.Inkycal" title="Link to this definition"></a></dt>
|
||||
<dd><p>Inkycal main class</p>
|
||||
<p>Main class of Inkycal, test and run the main Inkycal program.</p>
|
||||
<dl class="simple">
|
||||
@@ -188,7 +185,7 @@ checks if the images could be generated correctly.</p>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="inkycal.main.Inkycal.run">
|
||||
<em class="property"><span class="pre">async</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">run</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">run_once</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.main.Inkycal.run" title="Link to this definition"></a></dt>
|
||||
<em class="property"><span class="k"><span class="pre">async</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">run</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">run_once</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.main.Inkycal.run" title="Link to this definition"></a></dt>
|
||||
<dd><p>Runs main program in nonstop mode or a single iteration based on the run_once flag.</p>
|
||||
<dl class="simple">
|
||||
<dt>Args:</dt><dd><dl class="simple">
|
||||
@@ -295,7 +292,7 @@ printed fonts of this function:</p>
|
||||
</dd>
|
||||
</dl>
|
||||
<p>The extracted timezone can be used to show the local time instead of UTC. e.g.</p>
|
||||
<div class="doctest highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">>>> </span><span class="kn">import</span> <span class="nn">arrow</span>
|
||||
<div class="doctest highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">>>> </span><span class="kn">import</span><span class="w"> </span><span class="nn">arrow</span>
|
||||
<span class="gp">>>> </span><span class="nb">print</span><span class="p">(</span><span class="n">arrow</span><span class="o">.</span><span class="n">now</span><span class="p">())</span> <span class="c1"># returns non-timezone-aware time</span>
|
||||
<span class="gp">>>> </span><span class="nb">print</span><span class="p">(</span><span class="n">arrow</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">tz</span><span class="o">=</span><span class="n">get_system_tz</span><span class="p">()))</span> <span class="c1"># prints timezone aware time.</span>
|
||||
</pre></div>
|
||||
@@ -379,12 +376,12 @@ maximum of 90% of the size of the full height of the text-box.</p></li>
|
||||
Copyright by aceinnolab</p>
|
||||
<dl class="py class">
|
||||
<dt class="sig sig-object py" id="inkycal.modules.ical_parser.iCalendar">
|
||||
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">inkycal.modules.ical_parser.</span></span><span class="sig-name descname"><span class="pre">iCalendar</span></span><a class="headerlink" href="#inkycal.modules.ical_parser.iCalendar" title="Link to this definition"></a></dt>
|
||||
<em class="property"><span class="k"><span class="pre">class</span></span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">inkycal.modules.ical_parser.</span></span><span class="sig-name descname"><span class="pre">iCalendar</span></span><a class="headerlink" href="#inkycal.modules.ical_parser.iCalendar" title="Link to this definition"></a></dt>
|
||||
<dd><p>iCalendar parsing moudule for inkycal.
|
||||
Parses events from given iCalendar URLs / paths</p>
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="inkycal.modules.ical_parser.iCalendar.all_day">
|
||||
<em class="property"><span class="pre">static</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">all_day</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">event</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.ical_parser.iCalendar.all_day" title="Link to this definition"></a></dt>
|
||||
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">all_day</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">event</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.ical_parser.iCalendar.all_day" title="Link to this definition"></a></dt>
|
||||
<dd><p>Check if an event is an all day event.
|
||||
Returns True if event is all day, else False</p>
|
||||
</dd></dl>
|
||||
@@ -407,7 +404,7 @@ Returns a list of events sorted by date</p>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="inkycal.modules.ical_parser.iCalendar.get_system_tz">
|
||||
<em class="property"><span class="pre">static</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">get_system_tz</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.ical_parser.iCalendar.get_system_tz" title="Link to this definition"></a></dt>
|
||||
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">get_system_tz</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.ical_parser.iCalendar.get_system_tz" title="Link to this definition"></a></dt>
|
||||
<dd><p>Get the timezone set by the system</p>
|
||||
</dd></dl>
|
||||
|
||||
@@ -450,7 +447,7 @@ images.</p>
|
||||
<p>Copyright by aceinnolab</p>
|
||||
<dl class="py class">
|
||||
<dt class="sig sig-object py" id="inkycal.modules.inky_image.Inkyimage">
|
||||
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">inkycal.modules.inky_image.</span></span><span class="sig-name descname"><span class="pre">Inkyimage</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">image</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.inky_image.Inkyimage" title="Link to this definition"></a></dt>
|
||||
<em class="property"><span class="k"><span class="pre">class</span></span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">inkycal.modules.inky_image.</span></span><span class="sig-name descname"><span class="pre">Inkyimage</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">image</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.inky_image.Inkyimage" title="Link to this definition"></a></dt>
|
||||
<dd><p>Custom Imgae class written for commonly used image operations.</p>
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="inkycal.modules.inky_image.Inkyimage.autoflip">
|
||||
@@ -510,7 +507,7 @@ file-format, i.e. is not an image</p></li>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="inkycal.modules.inky_image.Inkyimage.merge">
|
||||
<em class="property"><span class="pre">static</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">merge</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">image1</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">image2</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.inky_image.Inkyimage.merge" title="Link to this definition"></a></dt>
|
||||
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">merge</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">image1</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">image2</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.inky_image.Inkyimage.merge" title="Link to this definition"></a></dt>
|
||||
<dd><p>Merges two images into one.</p>
|
||||
<p>Replaces white pixels of the first image with transparent ones. Then pastes
|
||||
the first image on the second one.</p>
|
||||
@@ -527,12 +524,6 @@ the first image on the second one.</p>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="inkycal.modules.inky_image.Inkyimage.preview">
|
||||
<em class="property"><span class="pre">static</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">preview</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">image</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.inky_image.Inkyimage.preview" title="Link to this definition"></a></dt>
|
||||
<dd><p>Previews an image on gpicview (only works on Rapsbian with Desktop).</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="inkycal.modules.inky_image.Inkyimage.remove_alpha">
|
||||
<span class="sig-name descname"><span class="pre">remove_alpha</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.modules.inky_image.Inkyimage.remove_alpha" title="Link to this definition"></a></dt>
|
||||
|
||||
BIN
docs/objects.inv
BIN
docs/objects.inv
Binary file not shown.
@@ -1,22 +1,20 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="writer-html5" lang="en" data-content_root="./">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Python Module Index — inkycal 2.0.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
|
||||
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="_static/js/html5shiv.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/js/theme.js"></script>
|
||||
<link rel="author" title="About these documents" href="about.html" />
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="writer-html5" lang="en" data-content_root="./">
|
||||
<head>
|
||||
@@ -5,19 +7,15 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Quickstart — inkycal 2.0.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
|
||||
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="_static/js/html5shiv.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/js/theme.js"></script>
|
||||
<link rel="author" title="About these documents" href="about.html" />
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
@@ -102,7 +100,7 @@ pip3<span class="w"> </span>install<span class="w"> </span>-e<span class="w"> </
|
||||
</section>
|
||||
<section id="creating-settings-file">
|
||||
<h2>Creating settings file<a class="headerlink" href="#creating-settings-file" title="Link to this heading"></a></h2>
|
||||
<p>Please navigate to the <a class="reference external" href="https://aceisace.eu.pythonanywhere.com/index">WEB-UI</a> to create your settings file.</p>
|
||||
<p>Please navigate to the <a class="reference external" href="https://inkycal.aceinnolab.com">WEB-UI</a> to create your settings file.</p>
|
||||
<p>Copy the generated settings file to the Raspberry Pi
|
||||
more coming soon..</p>
|
||||
</section>
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="writer-html5" lang="en" data-content_root="./">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Search — inkycal 2.0.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=b86133f3" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=e59714d7" />
|
||||
|
||||
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="_static/js/html5shiv.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/js/theme.js"></script>
|
||||
<script src="_static/searchtools.js"></script>
|
||||
<script src="_static/language_data.js"></script>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
# About Inkycal
|
||||
<img align="center" src="https://raw.githubusercontent.com/aceisace/Inkycal/assets/Repo/logo.png" width="800" alt="inkycal logo">
|
||||
<img align="center" src="https://raw.githubusercontent.com/aceinnolab/Inkycal/assets/Repo/logo.png" width="800" alt="inkycal logo">
|
||||
|
||||
Inkycal is a python3 software for selected E-Paper displays.
|
||||
It's open-source (non-commercially), fully modular, user-friendly and even runs
|
||||
|
||||
@@ -17,7 +17,7 @@ pip3 install -e ./
|
||||
```
|
||||
|
||||
## Creating settings file
|
||||
Please navigate to the [WEB-UI](https://aceisace.eu.pythonanywhere.com/index) to create your settings file.
|
||||
Please navigate to the [WEB-UI](https://inkycal.aceinnolab.com) to create your settings file.
|
||||
|
||||
Copy the generated settings file to the Raspberry Pi
|
||||
more coming soon..
|
||||
|
||||
@@ -40,4 +40,4 @@ async def clear_display():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run())
|
||||
asyncio.run(dry_run())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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
2
inkycal/modules/__init__.py
Executable file → Normal 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
0
inkycal/modules/dev_module.py
Executable file → Normal file
0
inkycal/modules/ical_parser.py
Executable file → Normal file
0
inkycal/modules/ical_parser.py
Executable file → Normal file
7
inkycal/modules/inky_image.py
Executable file → Normal file
7
inkycal/modules/inky_image.py
Executable file → Normal file
@@ -169,6 +169,13 @@ class Inkyimage:
|
||||
logger.error("no height of width specified")
|
||||
return
|
||||
|
||||
current_width, current_height = self.image.size
|
||||
|
||||
# Skip if dimensions are the same
|
||||
if width == current_width and height == current_height:
|
||||
logger.info(f"Image already correct size ({width}x{height}), skipping resize")
|
||||
return
|
||||
|
||||
image = self.image
|
||||
|
||||
if width:
|
||||
|
||||
0
inkycal/modules/inkycal_agenda.py
Executable file → Normal file
0
inkycal/modules/inkycal_agenda.py
Executable file → Normal file
0
inkycal/modules/inkycal_calendar.py
Executable file → Normal file
0
inkycal/modules/inkycal_calendar.py
Executable file → Normal 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
405
inkycal/modules/inkycal_github.py
Normal file
405
inkycal/modules/inkycal_github.py
Normal 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
0
inkycal/modules/inkycal_image.py
Executable file → Normal file
0
inkycal/modules/inkycal_jokes.py
Executable file → Normal file
0
inkycal/modules/inkycal_jokes.py
Executable file → Normal file
0
inkycal/modules/inkycal_server.py
Executable file → Normal file
0
inkycal/modules/inkycal_server.py
Executable file → Normal file
0
inkycal/modules/inkycal_slideshow.py
Executable file → Normal file
0
inkycal/modules/inkycal_slideshow.py
Executable file → Normal file
0
inkycal/modules/inkycal_stocks.py
Executable file → Normal file
0
inkycal/modules/inkycal_stocks.py
Executable file → Normal 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
0
inkycal/modules/inkycal_tindie.py
Executable file → Normal file
379
inkycal/modules/inkycal_today.py
Normal file
379
inkycal/modules/inkycal_today.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
241
inkycal/modules/super_productivity_utils.py
Normal file
241
inkycal/modules/super_productivity_utils.py
Normal 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
3
inkycal/modules/template.py
Executable file → Normal 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)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"name": "Calendar",
|
||||
"name": "Today",
|
||||
"config": {
|
||||
"size": [
|
||||
528,
|
||||
|
||||
@@ -1,54 +1,52 @@
|
||||
appdirs==1.4.4
|
||||
arrow==1.3.0
|
||||
asyncio==3.4.3
|
||||
beautifulsoup4==4.12.3
|
||||
certifi==2024.7.4
|
||||
beautifulsoup4==4.13.4
|
||||
certifi==2025.8.3
|
||||
cfgv==3.4.0
|
||||
charset-normalizer==3.3.2
|
||||
charset-normalizer==3.4.3
|
||||
colorzero==2.0
|
||||
cycler==0.12.1
|
||||
distlib==0.3.8
|
||||
distlib==0.4.0
|
||||
feedparser==6.0.11
|
||||
filelock==3.13.1
|
||||
fonttools==4.48.1
|
||||
frozendict==2.4.0
|
||||
gpiozero==2.0
|
||||
html2text==2020.1.16
|
||||
filelock==3.18.0
|
||||
fonttools==4.59.0
|
||||
frozendict==2.4.6
|
||||
gpiozero==2.0.1
|
||||
html2text==2025.4.15
|
||||
html5lib==1.1
|
||||
htmlwebshot==0.1.2
|
||||
icalendar==5.0.11
|
||||
identify==2.5.34
|
||||
idna==3.7
|
||||
kiwisolver==1.4.5
|
||||
lgpio==0.0.0.2
|
||||
matplotlib==3.7.1
|
||||
multitasking==0.0.11
|
||||
nodeenv==1.8.0
|
||||
numpy==1.26.2
|
||||
packaging==23.2
|
||||
pandas==2.2.0
|
||||
peewee==3.17.1
|
||||
pillow==10.3.0
|
||||
platformdirs==4.2.0
|
||||
pre-commit==3.6.1
|
||||
pyparsing==3.1.1
|
||||
python-dateutil==2.8.2
|
||||
python-dotenv==1.0.1
|
||||
pytz==2024.1
|
||||
PyYAML==6.0.1
|
||||
recurring-ical-events==2.1.2
|
||||
requests==2.32.3
|
||||
icalendar==6.3.1
|
||||
identify==2.6.13
|
||||
idna==3.10
|
||||
kiwisolver==1.4.9
|
||||
matplotlib==3.10.5
|
||||
multitasking==0.0.12
|
||||
nodeenv==1.9.1
|
||||
numpy==2.3.2
|
||||
packaging==25.0
|
||||
pandas==2.3.1
|
||||
peewee==3.18.2
|
||||
pillow==11.3.0
|
||||
platformdirs==4.3.8
|
||||
pre-commit==4.3.0
|
||||
pyparsing==3.2.3
|
||||
python-dateutil==2.9.0
|
||||
python-dotenv==1.1.1
|
||||
pytz==2025.2
|
||||
PyYAML==6.0.2
|
||||
recurring-ical-events==3.8.0
|
||||
requests==2.32.4
|
||||
sgmllib3k==1.0.0
|
||||
six==1.16.0
|
||||
soupsieve==2.5
|
||||
todoist-api-python==2.1.3
|
||||
types-python-dateutil==2.8.19.20240106
|
||||
typing_extensions==4.9.0
|
||||
tzdata==2024.1
|
||||
tzlocal==5.2
|
||||
urllib3==2.2.2
|
||||
virtualenv==20.25.0
|
||||
six==1.17.0
|
||||
soupsieve==2.7
|
||||
todoist-api-python==3.1.0
|
||||
types-python-dateutil==2.9.0.20250809
|
||||
typing_extensions==4.14.1
|
||||
tzdata==2025.2
|
||||
tzlocal==5.3.1
|
||||
urllib3==2.5.0
|
||||
virtualenv==20.34.0
|
||||
webencodings==0.5.1
|
||||
x-wr-timezone==0.0.6
|
||||
x-wr-timezone==2.0.1
|
||||
xkcd==2.4.2
|
||||
yfinance==0.2.40
|
||||
yfinance==0.2.65
|
||||
|
||||
2
setup.py
2
setup.py
@@ -17,7 +17,7 @@ __version__ = "2.0.4"
|
||||
__description__ = "Inkycal is a python3 software for syncing icalendar events, weather and news on selected E-Paper displays"
|
||||
__packages__ = ["inkycal"]
|
||||
__author__ = "aceinnolab"
|
||||
__author_email__ = "aceisace63@yahoo.com"
|
||||
__author_email__ = "inkycal@aceinnolab.com"
|
||||
__url__ = "https://github.com/aceinnolab/Inkycal"
|
||||
|
||||
__install_requires__ = required
|
||||
|
||||
649
test_display.py
Normal file
649
test_display.py
Normal 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())
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
0
tests/test_ical_parser.py
Executable file → Normal file
3
tests/test_inkycal_agenda.py
Executable file → Normal file
3
tests/test_inkycal_agenda.py
Executable file → Normal 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
3
tests/test_inkycal_calendar.py
Executable file → Normal 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
3
tests/test_inkycal_feeds.py
Executable file → Normal 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
3
tests/test_inkycal_image.py
Executable file → Normal 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
3
tests/test_inkycal_jokes.py
Executable file → Normal 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
9
tests/test_inkycal_slideshow.py
Executable file → Normal 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
0
tests/test_inkycal_stocks.py
Executable file → Normal 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
3
tests/test_inkycal_tindie.py
Executable file → Normal 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()
|
||||
|
||||
86
tests/test_inkycal_today.py
Normal file
86
tests/test_inkycal_today.py
Normal 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()
|
||||
@@ -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
1
tests/test_inkycal_weather.py
Executable file → Normal 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
3
tests/test_inkycal_webshot.py
Executable file → Normal 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')
|
||||
|
||||
Reference in New Issue
Block a user