Merge remote-tracking branch 'origin/main'
15
.github/workflows/update-os.yml
vendored
@ -24,7 +24,8 @@ jobs:
|
|||||||
TINDIE_USERNAME: ${{ secrets.TINDIE_USERNAME }}
|
TINDIE_USERNAME: ${{ secrets.TINDIE_USERNAME }}
|
||||||
with:
|
with:
|
||||||
# Set the base_image to the desired Raspberry Pi OS version
|
# 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
|
# 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
|
||||||
image_additional_mb: 3072 # enlarge free space to 3 GB
|
image_additional_mb: 3072 # enlarge free space to 3 GB
|
||||||
optimize_image: true
|
optimize_image: true
|
||||||
commands: |
|
commands: |
|
||||||
@ -69,10 +70,11 @@ jobs:
|
|||||||
echo $CWD
|
echo $CWD
|
||||||
|
|
||||||
# increase swap-size
|
# increase swap-size
|
||||||
sudo dphys-swapfile swapoff
|
# temporarily disabled due to unmounting issues
|
||||||
sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=512/' /etc/dphys-swapfile
|
# sudo dphys-swapfile swapoff
|
||||||
sudo dphys-swapfile setup
|
# sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=512/' /etc/dphys-swapfile
|
||||||
sudo dphys-swapfile swapon
|
# sudo dphys-swapfile setup
|
||||||
|
# sudo dphys-swapfile swapon
|
||||||
|
|
||||||
# enable SPI
|
# enable SPI
|
||||||
sudo sed -i s/#dtparam=spi=on/dtparam=spi=on/ /boot/config.txt
|
sudo sed -i s/#dtparam=spi=on/dtparam=spi=on/ /boot/config.txt
|
||||||
@ -85,7 +87,8 @@ jobs:
|
|||||||
sudo chown -R inky:inky /home/inky/Inkycal
|
sudo chown -R inky:inky /home/inky/Inkycal
|
||||||
|
|
||||||
# make all users require a password for sudo commands (improves security)
|
# make all users require a password for sudo commands (improves security)
|
||||||
echo 'ALL ALL=(ALL:ALL) PASSWD: ALL' | sudo tee -a /etc/sudoers.d/010_require_sudo_password
|
# temporarily disabled to allow pisugar support
|
||||||
|
# echo 'ALL ALL=(ALL:ALL) PASSWD: ALL' | sudo tee -a /etc/sudoers.d/010_require_sudo_password
|
||||||
# allow some time to unmount
|
# allow some time to unmount
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|
||||||
|
42
Changelog.md
@ -1,13 +1,47 @@
|
|||||||
# E-Paper-Calendar Software Changelog
|
# Inkycal Software Changelog
|
||||||
All significant changes will be documented in this file.
|
All significant changes will be documented in this file.
|
||||||
The order is from latest to oldest and structured in the following way:
|
The order is from latest to oldest and structured in the following way:
|
||||||
* Version name with date of publishing
|
* Version name with date of publishing
|
||||||
* Sections with either 'added', 'fixed', 'updated' and 'changed'
|
* Sections with either 'added', 'fixed', 'updated', 'changed' or 'removed' to describe the changes
|
||||||
|
|
||||||
## [2.0.3] 2024
|
## [2.0.3] 2024
|
||||||
|
### Changed
|
||||||
|
- Updated dependencies to the most-recent supported version
|
||||||
|
- Unified logging all over the library. Print statements are now rare. This makes it easier to identify why Inkycal isn't working without having to look up the logs
|
||||||
|
- Inkycal now makes use of a JSON-Cache to make it more resilient against resets etc. For example, the slideshow module will remember the last index even after a shutdown
|
||||||
|
- Inkycal now uses a list of supported displays instead of having to look up each driver in the driver directory
|
||||||
|
- Renamed tests according to python standards, starting with `test_..`, allowing unittest/pytest to automatically discover and run these tests.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed an annoying vertical alignment issue causing some characters to look chopped off
|
||||||
|
- Fixed the alignment of the red-circle on the calendar module
|
||||||
|
- Fixed weekday-names not translating in the weather module
|
||||||
|
- Fixed python 3.11 issues with numpy on Raspberry Pi OS
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
* Added fullscreen weather module
|
- Added long-awaited support of PiSugar v1/2/3. Still a bit experimental (no calibration handling), but works for most part. If PiSugar support is enabled, Inkycal will set the new alarm before shutting down the system, increasing battery life. Please note that around 70 updates were possible with the 1200mAh PiSugar 3 board, so one update a day to three should be max to get at least one month battery life.
|
||||||
* Own OWM API abstraction as a replacement for PyOWM module
|
- Added Webshot module which can be used to display a webpage. Works on InkycalOS-Lite too and does not need a GUI.
|
||||||
|
- Added XKCD module
|
||||||
|
- Added Tindie module
|
||||||
|
- Added support for much longer update-intervals than the previous max of once every 60 minutes
|
||||||
|
- Added Material-UI icons font
|
||||||
|
- Added dedicated Pipeline for unittests directly on Raspberry Pi OS to ensure Inkycal can run reliably on Raspberry Pi OS
|
||||||
|
- Added Feature-request and PR template
|
||||||
|
- Added support for 5.83" display (v2)
|
||||||
|
- Added support for 12.48" display on 64-bit systems
|
||||||
|
- Added Inkycal fullweather-module
|
||||||
|
- Added `settings.py file (not to be confused with `settings.json`) to set VCOM and other internal variables
|
||||||
|
|
||||||
|
|
||||||
|
## [2.0.3] 2023
|
||||||
|
### Changed
|
||||||
|
- Switched from pyowm to custom wrapper as pyowm only works up to python3.9, which is now outdated.
|
||||||
|
- Updated dependencies to the most-recent supported version
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed python 3.11 issues with numpy on Raspberry Pi OS
|
||||||
|
- Fixed compatibility issues with Pillow when switching from v9.x to v10.x, particularly font width and height operations
|
||||||
|
- Renamed tests according to python standards, starting with `test_..`, allowing unittest/pytest to automatically discover and run these tests.
|
||||||
|
|
||||||
|
|
||||||
## [2.0.2] 2022
|
## [2.0.2] 2022
|
||||||
|
57
README.md
@ -1,4 +1,4 @@
|
|||||||
# Welcome to inkycal v2.0.3!
|
# Welcome to inkycal v2.0.4!
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/aceinnolab/Inkycal/actions/workflows/test-on-rpi.yml"><img src="https://github.com/aceinnolab/Inkycal/actions/workflows/test-on-rpi.yml/badge.svg"></a>
|
<a href="https://github.com/aceinnolab/Inkycal/actions/workflows/test-on-rpi.yml"><img src="https://github.com/aceinnolab/Inkycal/actions/workflows/test-on-rpi.yml/badge.svg"></a>
|
||||||
@ -29,7 +29,7 @@ ready-to-flash version of Inkycal called InkycalOS-Lite with everything pre-inst
|
|||||||
via [GitHub Sponsors](https://github.com/sponsors/aceisace). This helps keep up maintenance costs, implement new
|
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
|
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.
|
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
|
Alternatively, you can also use the PayPal.me link and send the same amount as GitHub sponsors to get access to
|
||||||
InkycalOS-Lite!
|
InkycalOS-Lite!
|
||||||
|
|
||||||
## Main features
|
## Main features
|
||||||
@ -42,10 +42,13 @@ following built-in modules are supported:
|
|||||||
* Image - Display an Image from URL or local file path.
|
* Image - Display an Image from URL or local file path.
|
||||||
* Slideshow - Cycle through images in a given folder and show them on the E-Paper.
|
* Slideshow - Cycle through images in a given folder and show them on the E-Paper.
|
||||||
* Feeds - Synchronise RSS/ATOM feeds from your favorite providers.
|
* Feeds - Synchronise RSS/ATOM feeds from your favorite providers.
|
||||||
* Stocks - Display stocks using Tickers from Yahoo! Finance.
|
* Stocks - Display stocks using Tickers from Yahoo! Finance. Special thanks to @worstface
|
||||||
* Weather - Show current weather, daily or hourly weather forecasts from openweathermap.
|
* Weather - Show current weather, daily or hourly weather forecasts from openweathermap.
|
||||||
* Todoist - Synchronise with Todoist app or website to show todos.
|
* Todoist - Synchronise with Todoist app or website to show todos.
|
||||||
* iCanHazDad - Display a random joke from [iCanHazDad.com](iCanhazdad.com).
|
* iCanHazDad - Display a random joke from [iCanHazDad.com](iCanhazdad.com).
|
||||||
|
* Webshot - Display a website as an image. Special thanks to @worstface
|
||||||
|
* Tindie - Show the latest orders from your Tindie store.
|
||||||
|
* XKCD - Show XKCD comics. Special thanks to @worstface
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
@ -56,7 +59,8 @@ Watch the one-minute video on getting started with Inkycal:
|
|||||||
## Hardware guide
|
## Hardware guide
|
||||||
|
|
||||||
Before you can start, please ensure you have one of the supported displays and of the supported Raspberry
|
Before you can start, please ensure you have one of the supported displays and of the supported Raspberry
|
||||||
Pi: `|4|3A|3B|3B+|2B|ZeroW|ZeroWH|Zero2W|`. We personally recommend the Raspberry Pi Zero W as this is relatively cheaper, uses
|
Pi: `|4|3A|3B|3B+|2B|ZeroW|ZeroWH|Zero2W|`. We personally recommend the Raspberry Pi Zero W as this is relatively
|
||||||
|
cheaper, uses
|
||||||
less power and is perfect to fit in a small photo frame once you have assembled everything.
|
less power and is perfect to fit in a small photo frame once you have assembled everything.
|
||||||
|
|
||||||
**Serial** displays are usually cheaper, but slower. Their main advantage is ease of use, like being able to communicate
|
**Serial** displays are usually cheaper, but slower. Their main advantage is ease of use, like being able to communicate
|
||||||
@ -74,26 +78,27 @@ grayscale levels, which does not compare to the 256 grayscales of LCDs, but far
|
|||||||
links below may or may not contain the required driver board. Please ensure you get the correct driver board for the
|
links below may or may not contain the required driver board. Please ensure you get the correct driver board for the
|
||||||
display!**
|
display!**
|
||||||
|
|
||||||
| type | vendor | Where to buy |
|
| type | vendor | Where to buy |
|
||||||
|---------------------------------------------------------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|---------------------------------------------------------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| 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. |
|
| 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. |
|
||||||
| 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 |
|
| 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. |
|
||||||
| 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. |
|
| 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 |
|
||||||
| `[serial]` 12.48" (1304×984px) display | waveshare / gooddisplay | Search for `Waveshare 12.48" E-Paper 1304×984` on amazon or similar |
|
| 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]` 7.5" (640x384px) -> v1 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 640x384` on amazon or similar |
|
| `[serial]` 12.48" (1304×984px) display | waveshare / gooddisplay | Search for `Waveshare 12.48" E-Paper 1304×984` on amazon or similar |
|
||||||
| `[serial]` 7.5" (800x480px) -> v2 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x480` on amazon or similar |
|
| `[serial]` 7.5" (640x384px) -> v1 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 640x384` on amazon or similar |
|
||||||
| `[serial]` 7.5" (880x528px) -> v3 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x528` on amazon or similar |
|
| `[serial]` 7.5" (800x480px) -> v2 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x480` on amazon or similar |
|
||||||
| `[serial]` 5.83" (400x300px) display | waveshare / gooddisplay | Search for `Waveshare 5.83" E-Paper 400x300` on amazon or similar |
|
| `[serial]` 7.5" (880x528px) -> v3 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x528` on amazon or similar |
|
||||||
| `[serial]` 4.2" (400x300px)display | waveshare / gooddisplay | Search for `Waveshare 4.2" E-Paper 400x300` on amazon or similar | |
|
| `[serial]` 5.83" (400x300px) display | waveshare / gooddisplay | Search for `Waveshare 5.83" E-Paper 400x300` on amazon or similar |
|
||||||
| `[parallel]` 10.3" (1872×1404px) display | waveshare / gooddisplay | Search for `Waveshare 10.3" E-Paper 1872×1404` on amazon or similar |
|
| `[serial]` 4.2" (400x300px)display | waveshare / gooddisplay | Search for `Waveshare 4.2" E-Paper 400x300` on amazon or similar | |
|
||||||
| `[parallel]` 9.7" (1200×825px) display | waveshare / gooddisplay | Search for `Waveshare 9.7" E-Paper 1200×825` on amazon or similar |
|
| `[parallel]` 10.3" (1872×1404px) display | waveshare / gooddisplay | Search for `Waveshare 10.3" E-Paper 1872×1404` on amazon or similar |
|
||||||
| `[parallel]` 7.8" (1872×1404px) display | waveshare / gooddisplay | Search for `Waveshare 7.8" E-Paper 1872×1404` on amazon or similar |
|
| `[parallel]` 9.7" (1200×825px) display | waveshare / gooddisplay | Search for `Waveshare 9.7" E-Paper 1200×825` on amazon or similar |
|
||||||
| Raspberry Pi Zero W | Raspberry Pi | Search for `Raspberry Pi Zero W` on amazon or similar |
|
| `[parallel]` 7.8" (1872×1404px) display | waveshare / gooddisplay | Search for `Waveshare 7.8" E-Paper 1872×1404` on amazon or similar |
|
||||||
| MicroSD card | Sandisk | Search for `MicroSD card 8GB` on amazon or similar |
|
| Raspberry Pi Zero W | Raspberry Pi | Search for `Raspberry Pi Zero W` on amazon or similar |
|
||||||
|
| MicroSD card | Sandisk | Search for `MicroSD card 8GB` on amazon or similar |
|
||||||
|
|
||||||
## Configuring the Raspberry Pi
|
## Configuring the Raspberry Pi
|
||||||
Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager](https://rptl.io/imager).
|
|
||||||
Use the following settings:
|
Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager](https://rptl.io/imager). Please use this version of [Raspberry Pi OS - bookworm](https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf-lite.img.xz) as the latest release is known to have some issues with the latest kernel update.
|
||||||
|
|
||||||
| option | value |
|
| option | value |
|
||||||
|:--------------------------|:---------------------------:|
|
|:--------------------------|:---------------------------:|
|
||||||
@ -163,13 +168,19 @@ top of the repo to get access to Inkycal-OS-Lite. Alternatively, you can also us
|
|||||||
amount as GitHub sponsors to get access to InkycalOS-Lite!
|
amount as GitHub sponsors to get access to InkycalOS-Lite!
|
||||||
This will help keep this project growing and cover the ongoing expenses too! Win-win for everyone! 🎊
|
This will help keep this project growing and cover the ongoing expenses too! Win-win for everyone! 🎊
|
||||||
|
|
||||||
|
### Bonus: PiSugar support
|
||||||
|
The PiSugar is a battery pack for the Raspberry Pi Zero W. It can be used to power the Raspberry Pi and the e-paper, allowing battery life up to several weeks.
|
||||||
|
If you have a PiSugar board, please see the wiki page on how to install the PiSugar driver and configure Inkycal to work with it:
|
||||||
|
[PiSugar support](https://github.com/aceinnolab/Inkycal/wiki/PiSugar-support)
|
||||||
|
|
||||||
|
|
||||||
### Manual installation
|
### Manual installation
|
||||||
|
|
||||||
Run the following steps to install Inkycal. Do **not** use sudo for this, except where explicitly specified.
|
Run the following steps to install Inkycal. Do **not** use sudo for this, except where explicitly specified.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Raspberry Pi specific section start
|
# 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-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
|
||||||
git clone https://github.com/WiringPi/WiringPi
|
git clone https://github.com/WiringPi/WiringPi
|
||||||
cd WiringPi
|
cd WiringPi
|
||||||
./build
|
./build
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
"""
|
|
||||||
Clears the display of any content.
|
|
||||||
"""
|
|
||||||
from inkycal import Inkycal
|
|
||||||
|
|
||||||
print("loading Inkycal and display driver...")
|
|
||||||
inky = Inkycal(render=True) # Initialise Inkycal
|
|
||||||
print("clearing display...")
|
|
||||||
inky.calibrate(cycles=1) # Calibrate the display
|
|
||||||
print("clear complete...")
|
|
||||||
|
|
||||||
print("finished!")
|
|
2
docs/_static/documentation_options.js
vendored
@ -1,5 +1,5 @@
|
|||||||
const DOCUMENTATION_OPTIONS = {
|
const DOCUMENTATION_OPTIONS = {
|
||||||
VERSION: '2.0.3',
|
VERSION: '2.0.4',
|
||||||
LANGUAGE: 'en',
|
LANGUAGE: 'en',
|
||||||
COLLAPSE_INDEX: false,
|
COLLAPSE_INDEX: false,
|
||||||
BUILDER: 'html',
|
BUILDER: 'html',
|
||||||
|
7
docs/_static/searchtools.js
vendored
@ -178,7 +178,7 @@ const Search = {
|
|||||||
|
|
||||||
htmlToText: (htmlString, anchor) => {
|
htmlToText: (htmlString, anchor) => {
|
||||||
const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html');
|
const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html');
|
||||||
for (const removalQuery of [".headerlinks", "script", "style"]) {
|
for (const removalQuery of [".headerlink", "script", "style"]) {
|
||||||
htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() });
|
htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() });
|
||||||
}
|
}
|
||||||
if (anchor) {
|
if (anchor) {
|
||||||
@ -328,13 +328,14 @@ const Search = {
|
|||||||
for (const [title, foundTitles] of Object.entries(allTitles)) {
|
for (const [title, foundTitles] of Object.entries(allTitles)) {
|
||||||
if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) {
|
if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) {
|
||||||
for (const [file, id] of foundTitles) {
|
for (const [file, id] of foundTitles) {
|
||||||
let score = Math.round(100 * queryLower.length / title.length)
|
const score = Math.round(Scorer.title * queryLower.length / title.length);
|
||||||
|
const boost = titles[file] === title ? 1 : 0; // add a boost for document titles
|
||||||
normalResults.push([
|
normalResults.push([
|
||||||
docNames[file],
|
docNames[file],
|
||||||
titles[file] !== title ? `${titles[file]} > ${title}` : title,
|
titles[file] !== title ? `${titles[file]} > ${title}` : title,
|
||||||
id !== null ? "#" + id : "",
|
id !== null ? "#" + id : "",
|
||||||
null,
|
null,
|
||||||
score,
|
score + boost,
|
||||||
filenames[file],
|
filenames[file],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>About Inkycal — inkycal 2.0.3 documentation</title>
|
<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/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/css/theme.css?v=19f00094" />
|
||||||
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||||
<script src="_static/documentation_options.js?v=664ffad9"></script>
|
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||||
<script src="_static/js/theme.js"></script>
|
<script src="_static/js/theme.js"></script>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Developer documentation — inkycal 2.0.3 documentation</title>
|
<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/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/css/theme.css?v=19f00094" />
|
||||||
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||||
<script src="_static/documentation_options.js?v=664ffad9"></script>
|
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||||
<script src="_static/js/theme.js"></script>
|
<script src="_static/js/theme.js"></script>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Index — inkycal 2.0.3 documentation</title>
|
<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/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/css/theme.css?v=19f00094" />
|
||||||
|
|
||||||
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||||
<script src="_static/documentation_options.js?v=664ffad9"></script>
|
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||||
<script src="_static/js/theme.js"></script>
|
<script src="_static/js/theme.js"></script>
|
||||||
@ -129,6 +129,10 @@
|
|||||||
<table style="width: 100%" class="indextable genindextable"><tr>
|
<table style="width: 100%" class="indextable genindextable"><tr>
|
||||||
<td style="width: 33%; vertical-align: top;"><ul>
|
<td style="width: 33%; vertical-align: top;"><ul>
|
||||||
<li><a href="inkycal.html#inkycal.custom.functions.draw_border">draw_border() (in module inkycal.custom.functions)</a>
|
<li><a href="inkycal.html#inkycal.custom.functions.draw_border">draw_border() (in module inkycal.custom.functions)</a>
|
||||||
|
</li>
|
||||||
|
</ul></td>
|
||||||
|
<td style="width: 33%; vertical-align: top;"><ul>
|
||||||
|
<li><a href="inkycal.html#inkycal.main.Inkycal.dry_run">dry_run() (inkycal.main.Inkycal method)</a>
|
||||||
</li>
|
</li>
|
||||||
</ul></td>
|
</ul></td>
|
||||||
</tr></table>
|
</tr></table>
|
||||||
@ -253,6 +257,10 @@
|
|||||||
<table style="width: 100%" class="indextable genindextable"><tr>
|
<table style="width: 100%" class="indextable genindextable"><tr>
|
||||||
<td style="width: 33%; vertical-align: top;"><ul>
|
<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><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>
|
</li>
|
||||||
</ul></td>
|
</ul></td>
|
||||||
</tr></table>
|
</tr></table>
|
||||||
@ -285,10 +293,6 @@
|
|||||||
|
|
||||||
<h2 id="T">T</h2>
|
<h2 id="T">T</h2>
|
||||||
<table style="width: 100%" class="indextable genindextable"><tr>
|
<table style="width: 100%" class="indextable genindextable"><tr>
|
||||||
<td style="width: 33%; vertical-align: top;"><ul>
|
|
||||||
<li><a href="inkycal.html#inkycal.main.Inkycal.test">test() (inkycal.main.Inkycal method)</a>
|
|
||||||
</li>
|
|
||||||
</ul></td>
|
|
||||||
<td style="width: 33%; vertical-align: top;"><ul>
|
<td style="width: 33%; vertical-align: top;"><ul>
|
||||||
<li><a href="inkycal.html#inkycal.custom.functions.text_wrap">text_wrap() (in module inkycal.custom.functions)</a>
|
<li><a href="inkycal.html#inkycal.custom.functions.text_wrap">text_wrap() (in module inkycal.custom.functions)</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Inkycal documentation — inkycal 2.0.3 documentation</title>
|
<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/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/css/theme.css?v=19f00094" />
|
||||||
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||||
<script src="_static/documentation_options.js?v=664ffad9"></script>
|
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||||
<script src="_static/js/theme.js"></script>
|
<script src="_static/js/theme.js"></script>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Inkycal — inkycal 2.0.3 documentation</title>
|
<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/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/css/theme.css?v=19f00094" />
|
||||||
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||||
<script src="_static/documentation_options.js?v=664ffad9"></script>
|
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||||
<script src="_static/js/theme.js"></script>
|
<script src="_static/js/theme.js"></script>
|
||||||
@ -51,8 +51,9 @@
|
|||||||
<li class="toctree-l2"><a class="reference internal" href="#inkycal.main.Inkycal"><code class="docutils literal notranslate"><span class="pre">Inkycal</span></code></a><ul>
|
<li class="toctree-l2"><a class="reference internal" href="#inkycal.main.Inkycal"><code class="docutils literal notranslate"><span class="pre">Inkycal</span></code></a><ul>
|
||||||
<li class="toctree-l3"><a class="reference internal" href="#inkycal.main.Inkycal.calibrate"><code class="docutils literal notranslate"><span class="pre">Inkycal.calibrate()</span></code></a></li>
|
<li class="toctree-l3"><a class="reference internal" href="#inkycal.main.Inkycal.calibrate"><code class="docutils literal notranslate"><span class="pre">Inkycal.calibrate()</span></code></a></li>
|
||||||
<li class="toctree-l3"><a class="reference internal" href="#inkycal.main.Inkycal.countdown"><code class="docutils literal notranslate"><span class="pre">Inkycal.countdown()</span></code></a></li>
|
<li class="toctree-l3"><a class="reference internal" href="#inkycal.main.Inkycal.countdown"><code class="docutils literal notranslate"><span class="pre">Inkycal.countdown()</span></code></a></li>
|
||||||
|
<li class="toctree-l3"><a class="reference internal" href="#inkycal.main.Inkycal.dry_run"><code class="docutils literal notranslate"><span class="pre">Inkycal.dry_run()</span></code></a></li>
|
||||||
|
<li class="toctree-l3"><a class="reference internal" href="#inkycal.main.Inkycal.process_module"><code class="docutils literal notranslate"><span class="pre">Inkycal.process_module()</span></code></a></li>
|
||||||
<li class="toctree-l3"><a class="reference internal" href="#inkycal.main.Inkycal.run"><code class="docutils literal notranslate"><span class="pre">Inkycal.run()</span></code></a></li>
|
<li class="toctree-l3"><a class="reference internal" href="#inkycal.main.Inkycal.run"><code class="docutils literal notranslate"><span class="pre">Inkycal.run()</span></code></a></li>
|
||||||
<li class="toctree-l3"><a class="reference internal" href="#inkycal.main.Inkycal.test"><code class="docutils literal notranslate"><span class="pre">Inkycal.test()</span></code></a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -130,7 +131,7 @@
|
|||||||
Copyright by aceinnolab</p>
|
Copyright by aceinnolab</p>
|
||||||
<dl class="py class">
|
<dl class="py class">
|
||||||
<dt class="sig sig-object py" id="inkycal.main.Inkycal">
|
<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><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.main.Inkycal" title="Link to this definition"></a></dt>
|
<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>
|
||||||
<dd><p>Inkycal main class</p>
|
<dd><p>Inkycal main class</p>
|
||||||
<p>Main class of Inkycal, test and run the main Inkycal program.</p>
|
<p>Main class of Inkycal, test and run the main Inkycal program.</p>
|
||||||
<dl class="simple">
|
<dl class="simple">
|
||||||
@ -157,35 +158,21 @@ cycles. After a refresh cycle, a new image is generated and shown.</p>
|
|||||||
<dl class="py method">
|
<dl class="py method">
|
||||||
<dt class="sig sig-object py" id="inkycal.main.Inkycal.countdown">
|
<dt class="sig sig-object py" id="inkycal.main.Inkycal.countdown">
|
||||||
<span class="sig-name descname"><span class="pre">countdown</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">interval_mins</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">int</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><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">→</span> <span class="sig-return-typehint"><span class="pre">int</span></span></span><a class="headerlink" href="#inkycal.main.Inkycal.countdown" title="Link to this definition"></a></dt>
|
<span class="sig-name descname"><span class="pre">countdown</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">interval_mins</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">int</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><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">→</span> <span class="sig-return-typehint"><span class="pre">int</span></span></span><a class="headerlink" href="#inkycal.main.Inkycal.countdown" title="Link to this definition"></a></dt>
|
||||||
<dd><p>Returns the remaining time in seconds until next display update.</p>
|
<dd><p>Returns the remaining time in seconds until the next display update based on the interval.</p>
|
||||||
<dl class="simple">
|
<dl class="simple">
|
||||||
<dt>Args:</dt><dd><ul class="simple">
|
<dt>Args:</dt><dd><dl class="simple">
|
||||||
<li><dl class="simple">
|
<dt>interval_mins (int): The interval in minutes for the update. If none is given, the value</dt><dd><p>from the settings file is used.</p>
|
||||||
<dt>interval_mins = int -> the interval in minutes for the update</dt><dd><p>if no interval is given, the value from the settings file is used.</p>
|
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</dd>
|
</dd>
|
||||||
<dt>Returns:</dt><dd><ul class="simple">
|
<dt>Returns:</dt><dd><p>int: The remaining time in seconds until the next update.</p>
|
||||||
<li><p>int -> the remaining time in seconds until next update</p></li>
|
|
||||||
</ul>
|
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</dd></dl>
|
</dd></dl>
|
||||||
|
|
||||||
<dl class="py method">
|
<dl class="py method">
|
||||||
<dt class="sig sig-object py" id="inkycal.main.Inkycal.run">
|
<dt class="sig sig-object py" id="inkycal.main.Inkycal.dry_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><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.main.Inkycal.run" title="Link to this definition"></a></dt>
|
<span class="sig-name descname"><span class="pre">dry_run</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.main.Inkycal.dry_run" title="Link to this definition"></a></dt>
|
||||||
<dd><p>Runs main program in nonstop mode.</p>
|
|
||||||
<p>Uses an infinity loop to run Inkycal nonstop. Inkycal generates the image
|
|
||||||
from all modules, assembles them in one image, refreshed the E-Paper and
|
|
||||||
then sleeps until the next scheduled update.</p>
|
|
||||||
</dd></dl>
|
|
||||||
|
|
||||||
<dl class="py method">
|
|
||||||
<dt class="sig sig-object py" id="inkycal.main.Inkycal.test">
|
|
||||||
<span class="sig-name descname"><span class="pre">test</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.main.Inkycal.test" title="Link to this definition"></a></dt>
|
|
||||||
<dd><p>Tests if Inkycal can run without issues.</p>
|
<dd><p>Tests if Inkycal can run without issues.</p>
|
||||||
<p>Attempts to import module names from settings file. Loads the config
|
<p>Attempts to import module names from settings file. Loads the config
|
||||||
for each module and initializes the module. Tries to run the module and
|
for each module and initializes the module. Tries to run the module and
|
||||||
@ -193,6 +180,28 @@ checks if the images could be generated correctly.</p>
|
|||||||
<p>Generated images can be found in the /images folder of Inkycal.</p>
|
<p>Generated images can be found in the /images folder of Inkycal.</p>
|
||||||
</dd></dl>
|
</dd></dl>
|
||||||
|
|
||||||
|
<dl class="py method">
|
||||||
|
<dt class="sig sig-object py" id="inkycal.main.Inkycal.process_module">
|
||||||
|
<span class="sig-name descname"><span class="pre">process_module</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">number</span></span></em><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">→</span> <span class="sig-return-typehint"><span class="pre">bool</span></span></span><a class="headerlink" href="#inkycal.main.Inkycal.process_module" title="Link to this definition"></a></dt>
|
||||||
|
<dd><p>Process individual module to generate images and handle exceptions.</p>
|
||||||
|
</dd></dl>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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">
|
||||||
|
<dt>run_once (bool): If True, runs the updating process once and stops. If False,</dt><dd><p>runs indefinitely.</p>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<p>Uses an infinity loop to run Inkycal nonstop or a single time based on run_once.
|
||||||
|
Inkycal generates the image from all modules, assembles them in one image,
|
||||||
|
refreshes the E-Paper and then sleeps until the next scheduled update or exits.</p>
|
||||||
|
</dd></dl>
|
||||||
|
|
||||||
</dd></dl>
|
</dd></dl>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
@ -232,14 +241,14 @@ which the given font should be scaled to.</p></li>
|
|||||||
|
|
||||||
<dl class="py function">
|
<dl class="py function">
|
||||||
<dt class="sig sig-object py" id="inkycal.custom.functions.draw_border">
|
<dt class="sig sig-object py" id="inkycal.custom.functions.draw_border">
|
||||||
<span class="sig-prename descclassname"><span class="pre">inkycal.custom.functions.</span></span><span class="sig-name descname"><span class="pre">draw_border</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">image</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">xy</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">size</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">radius</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">5</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">thickness</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">1</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">shrinkage</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">(0.1,</span> <span class="pre">0.1)</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.custom.functions.draw_border" title="Link to this definition"></a></dt>
|
<span class="sig-prename descclassname"><span class="pre">inkycal.custom.functions.</span></span><span class="sig-name descname"><span class="pre">draw_border</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="pre">image:</span> <span class="pre"><module</span> <span class="pre">'PIL.Image'</span> <span class="pre">from</span> <span class="pre">'/home/runner/work/Inkycal/Inkycal/venv/lib/python3.11/site-packages/PIL/Image.py'>,</span> <span class="pre">xy:</span> <span class="pre">~typing.Tuple[int,</span> <span class="pre">int],</span> <span class="pre">size:</span> <span class="pre">~typing.Tuple[int,</span> <span class="pre">int],</span> <span class="pre">radius:</span> <span class="pre">int</span> <span class="pre">=</span> <span class="pre">5,</span> <span class="pre">thickness:</span> <span class="pre">int</span> <span class="pre">=</span> <span class="pre">1,</span> <span class="pre">shrinkage:</span> <span class="pre">~typing.Tuple[int,</span> <span class="pre">int]</span> <span class="pre">=</span> <span class="pre">(0.1,</span> <span class="pre">0.1)</span></em><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">→</span> <span class="sig-return-typehint"><span class="pre">None</span></span></span><a class="headerlink" href="#inkycal.custom.functions.draw_border" title="Link to this definition"></a></dt>
|
||||||
<dd><p>Draws a border at given coordinates.</p>
|
<dd><p>Draws a border at given coordinates.</p>
|
||||||
<dl class="simple">
|
<dl class="simple">
|
||||||
<dt>Args:</dt><dd><ul class="simple">
|
<dt>Args:</dt><dd><ul class="simple">
|
||||||
<li><p>image: The image on which the border should be drawn (usually im_black or
|
<li><p>image: The image on which the border should be drawn (usually im_black or
|
||||||
im_colour.</p></li>
|
im_colour).</p></li>
|
||||||
<li><p>xy: Tuple representing the top-left corner of the border e.g. (32, 100)
|
<li><p>xy: Tuple representing the top-left corner of the border e.g. (32, 100)
|
||||||
where 32 is the x co-ordinate and 100 is the y-coordinate.</p></li>
|
where 32 is the x-coordinate and 100 is the y-coordinate.</p></li>
|
||||||
<li><p>size: Size of the border as a tuple -> (width, height).</p></li>
|
<li><p>size: Size of the border as a tuple -> (width, height).</p></li>
|
||||||
<li><p>radius: Radius of the corners, where 0 = plain rectangle, 5 = round corners.</p></li>
|
<li><p>radius: Radius of the corners, where 0 = plain rectangle, 5 = round corners.</p></li>
|
||||||
<li><p>thickness: Thickness of the border in pixels.</p></li>
|
<li><p>thickness: Thickness of the border in pixels.</p></li>
|
||||||
@ -288,14 +297,14 @@ printed fonts of this function:</p>
|
|||||||
<p>The extracted timezone can be used to show the local time instead of UTC. e.g.</p>
|
<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="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="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>
|
<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>
|
</pre></div>
|
||||||
</div>
|
</div>
|
||||||
</dd></dl>
|
</dd></dl>
|
||||||
|
|
||||||
<dl class="py function">
|
<dl class="py function">
|
||||||
<dt class="sig sig-object py" id="inkycal.custom.functions.internet_available">
|
<dt class="sig sig-object py" id="inkycal.custom.functions.internet_available">
|
||||||
<span class="sig-prename descclassname"><span class="pre">inkycal.custom.functions.</span></span><span class="sig-name descname"><span class="pre">internet_available</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.custom.functions.internet_available" title="Link to this definition"></a></dt>
|
<span class="sig-prename descclassname"><span class="pre">inkycal.custom.functions.</span></span><span class="sig-name descname"><span class="pre">internet_available</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">→</span> <span class="sig-return-typehint"><span class="pre">bool</span></span></span><a class="headerlink" href="#inkycal.custom.functions.internet_available" title="Link to this definition"></a></dt>
|
||||||
<dd><p>checks if the internet is available.</p>
|
<dd><p>checks if the internet is available.</p>
|
||||||
<p>Attempts to connect to google.com with a timeout of 5 seconds to check
|
<p>Attempts to connect to google.com with a timeout of 5 seconds to check
|
||||||
if the network can be reached.</p>
|
if the network can be reached.</p>
|
||||||
@ -315,7 +324,7 @@ if the network can be reached.</p>
|
|||||||
|
|
||||||
<dl class="py function">
|
<dl class="py function">
|
||||||
<dt class="sig sig-object py" id="inkycal.custom.functions.text_wrap">
|
<dt class="sig sig-object py" id="inkycal.custom.functions.text_wrap">
|
||||||
<span class="sig-prename descclassname"><span class="pre">inkycal.custom.functions.</span></span><span class="sig-name descname"><span class="pre">text_wrap</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">text</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">font</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">max_width</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.custom.functions.text_wrap" title="Link to this definition"></a></dt>
|
<span class="sig-prename descclassname"><span class="pre">inkycal.custom.functions.</span></span><span class="sig-name descname"><span class="pre">text_wrap</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">text</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">font</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">max_width</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.custom.functions.text_wrap" title="Link to this definition"></a></dt>
|
||||||
<dd><p>Splits a very long text into smaller parts</p>
|
<dd><p>Splits a very long text into smaller parts</p>
|
||||||
<p>Splits a long text to smaller lines which can fit in a line with max_width.
|
<p>Splits a long text to smaller lines which can fit in a line with max_width.
|
||||||
Uses a Font object for more accurate calculations.</p>
|
Uses a Font object for more accurate calculations.</p>
|
||||||
@ -334,7 +343,7 @@ splitting the text into the next chunk.</p></li>
|
|||||||
|
|
||||||
<dl class="py function">
|
<dl class="py function">
|
||||||
<dt class="sig sig-object py" id="inkycal.custom.functions.write">
|
<dt class="sig sig-object py" id="inkycal.custom.functions.write">
|
||||||
<span class="sig-prename descclassname"><span class="pre">inkycal.custom.functions.</span></span><span class="sig-name descname"><span class="pre">write</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">image</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">xy</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">box_size</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">text</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">font</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="o"><span class="pre">**</span></span><span class="n"><span class="pre">kwargs</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.custom.functions.write" title="Link to this definition"></a></dt>
|
<span class="sig-prename descclassname"><span class="pre">inkycal.custom.functions.</span></span><span class="sig-name descname"><span class="pre">write</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="pre">image:</span> <span class="pre"><module</span> <span class="pre">'PIL.Image'</span> <span class="pre">from</span> <span class="pre">'/home/runner/work/Inkycal/Inkycal/venv/lib/python3.11/site-packages/PIL/Image.py'>,</span> <span class="pre">xy:</span> <span class="pre">~typing.Tuple[int,</span> <span class="pre">int],</span> <span class="pre">box_size:</span> <span class="pre">~typing.Tuple[int,</span> <span class="pre">int],</span> <span class="pre">text:</span> <span class="pre">str,</span> <span class="pre">font=None,</span> <span class="pre">**kwargs</span></em><span class="sig-paren">)</span><a class="headerlink" href="#inkycal.custom.functions.write" title="Link to this definition"></a></dt>
|
||||||
<dd><p>Writes text on an image.</p>
|
<dd><p>Writes text on an image.</p>
|
||||||
<p>Writes given text at given position on the specified image.</p>
|
<p>Writes given text at given position on the specified image.</p>
|
||||||
<dl class="simple">
|
<dl class="simple">
|
||||||
|
BIN
docs/objects.inv
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Python Module Index — inkycal 2.0.3 documentation</title>
|
<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/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/css/theme.css?v=19f00094" />
|
||||||
|
|
||||||
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||||
<script src="_static/documentation_options.js?v=664ffad9"></script>
|
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||||
<script src="_static/js/theme.js"></script>
|
<script src="_static/js/theme.js"></script>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Quickstart — inkycal 2.0.3 documentation</title>
|
<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/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/css/theme.css?v=19f00094" />
|
||||||
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||||
<script src="_static/documentation_options.js?v=664ffad9"></script>
|
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||||
<script src="_static/js/theme.js"></script>
|
<script src="_static/js/theme.js"></script>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Search — inkycal 2.0.3 documentation</title>
|
<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/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/css/theme.css?v=19f00094" />
|
||||||
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<script src="_static/jquery.js?v=5d32c60e"></script>
|
<script src="_static/jquery.js?v=5d32c60e"></script>
|
||||||
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
|
||||||
<script src="_static/documentation_options.js?v=664ffad9"></script>
|
<script src="_static/documentation_options.js?v=adc66a14"></script>
|
||||||
<script src="_static/doctools.js?v=9a2dae69"></script>
|
<script src="_static/doctools.js?v=9a2dae69"></script>
|
||||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||||
<script src="_static/js/theme.js"></script>
|
<script src="_static/js/theme.js"></script>
|
||||||
|
@ -22,7 +22,7 @@ copyright = '2018-2023, aceinnolab'
|
|||||||
author = 'aceinnolab'
|
author = 'aceinnolab'
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags
|
# The full version, including alpha/beta/rc tags
|
||||||
release = '2.0.3'
|
release = '2.0.4'
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
|
BIN
fonts/MaterialIcons/MaterialIcons.ttf
Normal file
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
44
inky_run.py
@ -1,7 +1,43 @@
|
|||||||
|
"""Basic Inkycal run script.
|
||||||
|
|
||||||
|
Assumes that the settings.json file is in the /boot directory.
|
||||||
|
set render=True to render the display, set render=False to only run the modules.
|
||||||
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from inkycal import Inkycal
|
from inkycal import Inkycal
|
||||||
|
|
||||||
inky = Inkycal(render=True) # Initialise Inkycal
|
|
||||||
# If your settings.json file is not in /boot, use the full path: inky = Inkycal('path/to/settings.json', render=True)
|
async def run():
|
||||||
inky.test() # test if Inkycal can be run correctly, running this will show a bit of info for each module
|
"""Run Inkycal nonstop. Default mode."""
|
||||||
asyncio.run(inky.run()) # If there were no issues, you can run Inkycal nonstop
|
# create an instance of Inkycal
|
||||||
|
# If your settings.json file is not in /boot, use the full path:
|
||||||
|
# inky = Inkycal('path/to/settings.json', render=True)
|
||||||
|
|
||||||
|
# when using experimental PiSugar support:
|
||||||
|
# inky = Inkycal(render=True, use_pi_sugar=True, shutdown_after_run=False)
|
||||||
|
inky = Inkycal(render=True)
|
||||||
|
await inky.run() # If there were no issues, you can run Inkycal nonstop
|
||||||
|
|
||||||
|
|
||||||
|
async def dry_run():
|
||||||
|
"""Useful for checking if the settings.json file is okay, without actually touching the display"""
|
||||||
|
# create an instance of Inkycal
|
||||||
|
# If your settings.json file is not in /boot, use the full path:
|
||||||
|
# inky = Inkycal('path/to/settings.json', render=True)
|
||||||
|
inky = Inkycal(render=False)
|
||||||
|
await inky.run(run_once=True) # dry-run without rendering anything on the display
|
||||||
|
|
||||||
|
|
||||||
|
async def clear_display():
|
||||||
|
"""Calibrate the display if you see some ghosting"""
|
||||||
|
print("loading Inkycal and display driver...")
|
||||||
|
inky = Inkycal(render=True) # Initialise Inkycal
|
||||||
|
print("clearing display...")
|
||||||
|
inky.calibrate(cycles=1) # Calibrate the display
|
||||||
|
print("clear complete...")
|
||||||
|
print("finished!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(run())
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
# Display class (for driving E-Paper displays)
|
|
||||||
from inkycal.display import Display
|
|
||||||
|
|
||||||
# Default modules
|
# Default modules
|
||||||
import inkycal.modules.inkycal_agenda
|
import inkycal.modules.inkycal_agenda
|
||||||
import inkycal.modules.inkycal_calendar
|
import inkycal.modules.inkycal_calendar
|
||||||
import inkycal.modules.inkycal_weather
|
|
||||||
import inkycal.modules.inkycal_feeds
|
import inkycal.modules.inkycal_feeds
|
||||||
import inkycal.modules.inkycal_todoist
|
import inkycal.modules.inkycal_fullweather
|
||||||
import inkycal.modules.inkycal_image
|
import inkycal.modules.inkycal_image
|
||||||
import inkycal.modules.inkycal_jokes
|
import inkycal.modules.inkycal_jokes
|
||||||
import inkycal.modules.inkycal_slideshow
|
import inkycal.modules.inkycal_slideshow
|
||||||
import inkycal.modules.inkycal_stocks
|
import inkycal.modules.inkycal_stocks
|
||||||
|
import inkycal.modules.inkycal_todoist
|
||||||
|
import inkycal.modules.inkycal_weather
|
||||||
import inkycal.modules.inkycal_webshot
|
import inkycal.modules.inkycal_webshot
|
||||||
import inkycal.modules.inkycal_xkcd
|
import inkycal.modules.inkycal_xkcd
|
||||||
import inkycal.modules.inkycal_fullweather
|
from inkycal.display import Display
|
||||||
|
|
||||||
# Main file
|
|
||||||
from inkycal.main import Inkycal
|
from inkycal.main import Inkycal
|
||||||
import inkycal.modules.inkycal_stocks
|
|
||||||
|
@ -8,29 +8,25 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import PIL
|
|
||||||
import requests
|
import requests
|
||||||
import tzlocal
|
import tzlocal
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from PIL import ImageDraw
|
from PIL import ImageDraw
|
||||||
from PIL import ImageFont
|
from PIL import ImageFont
|
||||||
|
|
||||||
logs = logging.getLogger(__name__)
|
from inkycal.settings import Settings
|
||||||
logs.setLevel(level=logging.INFO)
|
|
||||||
|
|
||||||
# Get the path to the Inkycal folder
|
logger = logging.getLogger(__name__)
|
||||||
top_level = "/".join(os.path.dirname(os.path.abspath(os.path.dirname(__file__))).split("/")[:-1])
|
|
||||||
|
|
||||||
# Get path of 'fonts' and 'images' folders within Inkycal folder
|
settings = Settings()
|
||||||
fonts_location = os.path.join(top_level, "fonts/")
|
|
||||||
image_folder = os.path.join(top_level, "image_folder/")
|
|
||||||
|
|
||||||
# Get available fonts within fonts folder
|
# Get available fonts within fonts folder
|
||||||
fonts = {}
|
fonts = {}
|
||||||
|
|
||||||
for path, dirs, files in os.walk(fonts_location):
|
for path, dirs, files in os.walk(settings.FONT_PATH):
|
||||||
for _ in files:
|
for _ in files:
|
||||||
if _.endswith(".otf"):
|
if _.endswith(".otf"):
|
||||||
name = _.split(".otf")[0]
|
name = _.split(".otf")[0]
|
||||||
@ -39,7 +35,7 @@ for path, dirs, files in os.walk(fonts_location):
|
|||||||
if _.endswith(".ttf"):
|
if _.endswith(".ttf"):
|
||||||
name = _.split(".ttf")[0]
|
name = _.split(".ttf")[0]
|
||||||
fonts[name] = os.path.join(path, _)
|
fonts[name] = os.path.join(path, _)
|
||||||
logs.debug(f"Found fonts: {json.dumps(fonts, indent=4, sort_keys=True)}")
|
logger.debug(f"Found fonts: {json.dumps(fonts, indent=4, sort_keys=True)}")
|
||||||
available_fonts = [key for key, values in fonts.items()]
|
available_fonts = [key for key, values in fonts.items()]
|
||||||
|
|
||||||
|
|
||||||
@ -77,16 +73,16 @@ def get_system_tz() -> str:
|
|||||||
|
|
||||||
>>> import arrow
|
>>> import arrow
|
||||||
>>> print(arrow.now()) # returns non-timezone-aware time
|
>>> print(arrow.now()) # returns non-timezone-aware time
|
||||||
>>> print(arrow.now(tz=get_system_tz()) # prints timezone aware time.
|
>>> print(arrow.now(tz=get_system_tz())) # prints timezone aware time.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
local_tz = tzlocal.get_localzone().key
|
local_tz = tzlocal.get_localzone().key
|
||||||
logs.debug(f"Local system timezone is {local_tz}.")
|
logger.debug(f"Local system timezone is {local_tz}.")
|
||||||
except:
|
except:
|
||||||
logs.error("System timezone could not be parsed!")
|
logger.error("System timezone could not be parsed!")
|
||||||
logs.error("Please set timezone manually!. Falling back to UTC...")
|
logger.error("Please set timezone manually!. Falling back to UTC...")
|
||||||
local_tz = "UTC"
|
local_tz = "UTC"
|
||||||
logs.debug(f"The time is {arrow.now(tz=local_tz).format('YYYY-MM-DD HH:mm:ss ZZ')}.")
|
logger.debug(f"The time is {arrow.now(tz=local_tz).format('YYYY-MM-DD HH:mm:ss ZZ')}.")
|
||||||
return local_tz
|
return local_tz
|
||||||
|
|
||||||
|
|
||||||
@ -115,7 +111,7 @@ def auto_fontsize(font, max_height):
|
|||||||
return font
|
return font
|
||||||
|
|
||||||
|
|
||||||
def write(image, xy, box_size, text, font=None, **kwargs):
|
def write(image: Image, xy: Tuple[int, int], box_size: Tuple[int, int], text: str, font=None, **kwargs):
|
||||||
"""Writes text on an image.
|
"""Writes text on an image.
|
||||||
|
|
||||||
Writes given text at given position on the specified image.
|
Writes given text at given position on the specified image.
|
||||||
@ -165,7 +161,7 @@ def write(image, xy, box_size, text, font=None, **kwargs):
|
|||||||
text_bbox = font.getbbox(text)
|
text_bbox = font.getbbox(text)
|
||||||
text_width = text_bbox[2] - text_bbox[0]
|
text_width = text_bbox[2] - text_bbox[0]
|
||||||
text_bbox_height = font.getbbox("hg")
|
text_bbox_height = font.getbbox("hg")
|
||||||
text_height = text_bbox_height[3] - text_bbox_height[1]
|
text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1])
|
||||||
|
|
||||||
while text_width < int(box_width * fill_width) and text_height < int(box_height * fill_height):
|
while text_width < int(box_width * fill_width) and text_height < int(box_height * fill_height):
|
||||||
size += 1
|
size += 1
|
||||||
@ -173,23 +169,23 @@ def write(image, xy, box_size, text, font=None, **kwargs):
|
|||||||
text_bbox = font.getbbox(text)
|
text_bbox = font.getbbox(text)
|
||||||
text_width = text_bbox[2] - text_bbox[0]
|
text_width = text_bbox[2] - text_bbox[0]
|
||||||
text_bbox_height = font.getbbox("hg")
|
text_bbox_height = font.getbbox("hg")
|
||||||
text_height = text_bbox_height[3] - text_bbox_height[1]
|
text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1])
|
||||||
|
|
||||||
text_bbox = font.getbbox(text)
|
text_bbox = font.getbbox(text)
|
||||||
text_width = text_bbox[2] - text_bbox[0]
|
text_width = text_bbox[2] - text_bbox[0]
|
||||||
text_bbox_height = font.getbbox("hg")
|
text_bbox_height = font.getbbox("hg")
|
||||||
text_height = text_bbox_height[3] - text_bbox_height[1]
|
text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1])
|
||||||
|
|
||||||
# Truncate text if text is too long, so it can fit inside the box
|
# Truncate text if text is too long, so it can fit inside the box
|
||||||
if (text_width, text_height) > (box_width, box_height):
|
if (text_width, text_height) > (box_width, box_height):
|
||||||
logs.debug(("truncating {}".format(text)))
|
logger.debug(("truncating {}".format(text)))
|
||||||
while (text_width, text_height) > (box_width, box_height):
|
while (text_width, text_height) > (box_width, box_height):
|
||||||
text = text[0:-1]
|
text = text[0:-1]
|
||||||
text_bbox = font.getbbox(text)
|
text_bbox = font.getbbox(text)
|
||||||
text_width = text_bbox[2] - text_bbox[0]
|
text_width = text_bbox[2] - text_bbox[0]
|
||||||
text_bbox_height = font.getbbox("hg")
|
text_bbox_height = font.getbbox("hg")
|
||||||
text_height = text_bbox_height[3] - text_bbox_height[1]
|
text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1])
|
||||||
logs.debug(text)
|
logger.debug(text)
|
||||||
|
|
||||||
# Align text to desired position
|
# Align text to desired position
|
||||||
if alignment == "center" or None:
|
if alignment == "center" or None:
|
||||||
@ -199,10 +195,13 @@ def write(image, xy, box_size, text, font=None, **kwargs):
|
|||||||
elif alignment == "right":
|
elif alignment == "right":
|
||||||
x = int(box_width - text_width)
|
x = int(box_width - text_width)
|
||||||
|
|
||||||
|
# Vertical centering
|
||||||
|
y = int((box_height / 2) - (text_height / 2))
|
||||||
|
|
||||||
# Draw the text in the text-box
|
# Draw the text in the text-box
|
||||||
draw = ImageDraw.Draw(image)
|
draw = ImageDraw.Draw(image)
|
||||||
space = Image.new('RGBA', (box_width, box_height))
|
space = Image.new('RGBA', (box_width, box_height))
|
||||||
ImageDraw.Draw(space).text((x, 0), text, fill=colour, font=font)
|
ImageDraw.Draw(space).text((x, y), text, fill=colour, font=font)
|
||||||
|
|
||||||
# Uncomment following two lines, comment out above two lines to show
|
# Uncomment following two lines, comment out above two lines to show
|
||||||
# red text-box with white text (debugging purposes)
|
# red text-box with white text (debugging purposes)
|
||||||
@ -217,7 +216,7 @@ def write(image, xy, box_size, text, font=None, **kwargs):
|
|||||||
image.paste(space, xy, space)
|
image.paste(space, xy, space)
|
||||||
|
|
||||||
|
|
||||||
def text_wrap(text, font=None, max_width=None):
|
def text_wrap(text: str, font=None, max_width=None):
|
||||||
"""Splits a very long text into smaller parts
|
"""Splits a very long text into smaller parts
|
||||||
|
|
||||||
Splits a long text to smaller lines which can fit in a line with max_width.
|
Splits a long text to smaller lines which can fit in a line with max_width.
|
||||||
@ -253,7 +252,7 @@ def text_wrap(text, font=None, max_width=None):
|
|||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def internet_available():
|
def internet_available() -> bool:
|
||||||
"""checks if the internet is available.
|
"""checks if the internet is available.
|
||||||
|
|
||||||
Attempts to connect to google.com with a timeout of 5 seconds to check
|
Attempts to connect to google.com with a timeout of 5 seconds to check
|
||||||
@ -278,15 +277,16 @@ def internet_available():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
|
def draw_border(image: Image, xy: Tuple[int, int], size: Tuple[int, int], radius: int = 5, thickness: int = 1,
|
||||||
|
shrinkage: Tuple[int, int] = (0.1, 0.1)) -> None:
|
||||||
"""Draws a border at given coordinates.
|
"""Draws a border at given coordinates.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
- image: The image on which the border should be drawn (usually im_black or
|
- image: The image on which the border should be drawn (usually im_black or
|
||||||
im_colour.
|
im_colour).
|
||||||
|
|
||||||
- xy: Tuple representing the top-left corner of the border e.g. (32, 100)
|
- xy: Tuple representing the top-left corner of the border e.g. (32, 100)
|
||||||
where 32 is the x co-ordinate and 100 is the y-coordinate.
|
where 32 is the x-coordinate and 100 is the y-coordinate.
|
||||||
|
|
||||||
- size: Size of the border as a tuple -> (width, height).
|
- size: Size of the border as a tuple -> (width, height).
|
||||||
|
|
||||||
@ -324,6 +324,7 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
|
|||||||
c5, c6 = ((x + width) - diameter, (y + height) - diameter), (x + width, y + height)
|
c5, c6 = ((x + width) - diameter, (y + height) - diameter), (x + width, y + height)
|
||||||
c7, c8 = (x, (y + height) - diameter), (x + diameter, y + height)
|
c7, c8 = (x, (y + height) - diameter), (x + diameter, y + height)
|
||||||
|
|
||||||
|
|
||||||
# Draw lines and arcs, creating a square with round corners
|
# Draw lines and arcs, creating a square with round corners
|
||||||
draw = ImageDraw.Draw(image)
|
draw = ImageDraw.Draw(image)
|
||||||
draw.line((p1, p2), fill=colour, width=thickness)
|
draw.line((p1, p2), fill=colour, width=thickness)
|
||||||
@ -338,7 +339,7 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
|
|||||||
draw.arc((c7, c8), 90, 180, fill=colour, width=thickness)
|
draw.arc((c7, c8), 90, 180, fill=colour, width=thickness)
|
||||||
|
|
||||||
|
|
||||||
def draw_border_2(im: PIL.Image, xy: tuple, size: tuple, radius: int):
|
def draw_border_2(im: Image, xy: Tuple[int, int], size: Tuple[int, int], radius: int):
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
x, y = xy
|
x, y = xy
|
||||||
|
@ -41,18 +41,9 @@ def get_json_from_url(request_url):
|
|||||||
|
|
||||||
|
|
||||||
class OpenWeatherMap:
|
class OpenWeatherMap:
|
||||||
def __init__(
|
def __init__(self, api_key: str, city_id: int = None, lat: float = None, lon: float = None,
|
||||||
self,
|
api_version: API_VERSIONS = "2.5", temp_unit: TEMP_UNITS = "celsius",
|
||||||
api_key: str,
|
wind_unit: WIND_UNITS = "meters_sec", language: str = "en", tz_name: str = "UTC") -> None:
|
||||||
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:
|
|
||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.temp_unit = temp_unit
|
self.temp_unit = temp_unit
|
||||||
self.wind_unit = wind_unit
|
self.wind_unit = wind_unit
|
||||||
@ -106,7 +97,7 @@ class OpenWeatherMap:
|
|||||||
current_weather["temp_feels_like"] = self.get_converted_temperature(current_data["main"]["feels_like"])
|
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["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["max_temp"] = self.get_converted_temperature(current_data["main"]["temp_max"])
|
||||||
current_weather["humidity"] = current_data["main"]["humidity"] # OWM Unit: % rH
|
current_weather["humidity"] = current_data["main"]["humidity"] # OWM Unit: % rH
|
||||||
current_weather["wind"] = self.get_converted_windspeed(
|
current_weather["wind"] = self.get_converted_windspeed(
|
||||||
current_data["wind"]["speed"]
|
current_data["wind"]["speed"]
|
||||||
) # OWM Unit Default: meter/sec, Metric: meter/sec
|
) # OWM Unit Default: meter/sec, Metric: meter/sec
|
||||||
@ -161,10 +152,10 @@ class OpenWeatherMap:
|
|||||||
forecast["wind"]["speed"]
|
forecast["wind"]["speed"]
|
||||||
), # OWM Unit Default: meter/sec, Metric: meter/sec, Imperial: miles/hour
|
), # OWM Unit Default: meter/sec, Metric: meter/sec, Imperial: miles/hour
|
||||||
"wind_gust": self.get_converted_windspeed(forecast["wind"]["gust"]),
|
"wind_gust": self.get_converted_windspeed(forecast["wind"]["gust"]),
|
||||||
"pressure": forecast["main"]["pressure"], # OWM Unit: hPa
|
"pressure": forecast["main"]["pressure"], # OWM Unit: hPa
|
||||||
"humidity": forecast["main"]["humidity"], # OWM Unit: % rH
|
"humidity": forecast["main"]["humidity"], # OWM Unit: % rH
|
||||||
"precip_probability": forecast["pop"]
|
"precip_probability": forecast["pop"]
|
||||||
* 100.0, # OWM value is unitless, directly converting to % scale
|
* 100.0, # OWM value is unitless, directly converting to % scale
|
||||||
"icon": forecast["weather"][0]["icon"],
|
"icon": forecast["weather"][0]["icon"],
|
||||||
"datetime": datetime.fromtimestamp(forecast["dt"], tz=self.tz_zone),
|
"datetime": datetime.fromtimestamp(forecast["dt"], tz=self.tz_zone),
|
||||||
}
|
}
|
||||||
@ -187,7 +178,7 @@ class OpenWeatherMap:
|
|||||||
:return:
|
:return:
|
||||||
Forecast dictionary
|
Forecast dictionary
|
||||||
"""
|
"""
|
||||||
# Make sure hourly forecasts are up to date
|
# Make sure hourly forecasts are up-to-date
|
||||||
_ = self.get_weather_forecast()
|
_ = self.get_weather_forecast()
|
||||||
|
|
||||||
# Calculate the start and end times for the specified number of days from now
|
# Calculate the start and end times for the specified number of days from now
|
||||||
@ -207,7 +198,7 @@ class OpenWeatherMap:
|
|||||||
]
|
]
|
||||||
|
|
||||||
# In case the next available forecast is already for the next day, use that one for the less than 3 remaining hours of today
|
# 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 forecasts == []:
|
if not forecasts:
|
||||||
forecasts.append(self.hourly_forecasts[0])
|
forecasts.append(self.hourly_forecasts[0])
|
||||||
|
|
||||||
# Get rain and temperatures for that day
|
# Get rain and temperatures for that day
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
Inkycal ePaper driving functions
|
Inkycal ePaper driving functions
|
||||||
Copyright by aceisace
|
Copyright by aceinnolab
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
import PIL
|
import PIL
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from inkycal.custom import top_level
|
|
||||||
from inkycal.display.supported_models import supported_models
|
from inkycal.display.supported_models import supported_models
|
||||||
|
|
||||||
|
|
||||||
@ -199,9 +197,7 @@ class Display:
|
|||||||
|
|
||||||
>>> Display.get_display_names()
|
>>> Display.get_display_names()
|
||||||
"""
|
"""
|
||||||
driver_files = top_level + '/inkycal/display/drivers/'
|
return list(supported_models.keys())
|
||||||
drivers = [i for i in os.listdir(driver_files) if i.endswith(".py") and not i.startswith("__") and "_" in i]
|
|
||||||
return drivers
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -2,22 +2,18 @@
|
|||||||
10.3" driver class
|
10.3" driver class
|
||||||
Copyright by aceinnolab
|
Copyright by aceinnolab
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
from subprocess import run
|
from subprocess import run
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from inkycal.custom import image_folder, top_level
|
from inkycal.settings import Settings
|
||||||
|
|
||||||
# Display resolution
|
# Display resolution
|
||||||
EPD_WIDTH = 1872
|
EPD_WIDTH = 1872
|
||||||
EPD_HEIGHT = 1404
|
EPD_HEIGHT = 1404
|
||||||
|
|
||||||
# Please insert VCOM of your display. The Minus sign before is not required
|
settings = Settings()
|
||||||
VCOM = "2.0"
|
|
||||||
|
|
||||||
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
|
|
||||||
|
|
||||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
|
||||||
|
|
||||||
|
|
||||||
class EPD:
|
class EPD:
|
||||||
@ -40,8 +36,8 @@ class EPD:
|
|||||||
def getbuffer(self, image):
|
def getbuffer(self, image):
|
||||||
"""ad-hoc"""
|
"""ad-hoc"""
|
||||||
image = image.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
|
image = image.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
|
||||||
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
|
image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), "BMP")
|
||||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(settings.IMAGE_FOLDER, "canvas.bmp")}'
|
||||||
print(command)
|
print(command)
|
||||||
return command
|
return command
|
||||||
|
|
||||||
|
@ -2,20 +2,16 @@
|
|||||||
7.8" parallel driver class
|
7.8" parallel driver class
|
||||||
Copyright by aceinnolab
|
Copyright by aceinnolab
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
from subprocess import run
|
from subprocess import run
|
||||||
|
|
||||||
from inkycal.custom import image_folder, top_level
|
from inkycal.settings import Settings
|
||||||
|
|
||||||
# Display resolution
|
# Display resolution
|
||||||
EPD_WIDTH = 1872
|
EPD_WIDTH = 1872
|
||||||
EPD_HEIGHT = 1404
|
EPD_HEIGHT = 1404
|
||||||
|
|
||||||
# Please insert VCOM of your display. The Minus sign before is not required
|
settings = Settings()
|
||||||
VCOM = "2.0"
|
|
||||||
|
|
||||||
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
|
|
||||||
|
|
||||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
|
||||||
|
|
||||||
|
|
||||||
class EPD:
|
class EPD:
|
||||||
@ -38,8 +34,8 @@ class EPD:
|
|||||||
def getbuffer(self, image):
|
def getbuffer(self, image):
|
||||||
"""ad-hoc"""
|
"""ad-hoc"""
|
||||||
image = image.rotate(90, expand=True)
|
image = image.rotate(90, expand=True)
|
||||||
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
|
image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), 'BMP')
|
||||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(settings.IMAGE_FOLDER, "canvas.bmp")}'
|
||||||
print(command)
|
print(command)
|
||||||
return command
|
return command
|
||||||
|
|
||||||
|
@ -2,20 +2,16 @@
|
|||||||
9.7" driver class
|
9.7" driver class
|
||||||
Copyright by aceinnolab
|
Copyright by aceinnolab
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
from subprocess import run
|
from subprocess import run
|
||||||
|
|
||||||
from inkycal.custom import image_folder, top_level
|
from inkycal.settings import Settings
|
||||||
|
|
||||||
# Display resolution
|
# Display resolution
|
||||||
EPD_WIDTH = 1200
|
EPD_WIDTH = 1200
|
||||||
EPD_HEIGHT = 825
|
EPD_HEIGHT = 825
|
||||||
|
|
||||||
# Please insert VCOM of your display. The Minus sign before is not required
|
settings = Settings()
|
||||||
VCOM = "2.0"
|
|
||||||
|
|
||||||
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
|
|
||||||
|
|
||||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
|
||||||
|
|
||||||
|
|
||||||
class EPD:
|
class EPD:
|
||||||
@ -38,8 +34,8 @@ class EPD:
|
|||||||
def getbuffer(self, image):
|
def getbuffer(self, image):
|
||||||
"""ad-hoc"""
|
"""ad-hoc"""
|
||||||
image = image.rotate(90, expand=True)
|
image = image.rotate(90, expand=True)
|
||||||
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
|
image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), "BMP")
|
||||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(settings.IMAGE_FOLDER, "canvas.bmp")}'
|
||||||
print(command)
|
print(command)
|
||||||
return command
|
return command
|
||||||
|
|
||||||
|
527
inkycal/display/drivers/epd_13_in_3.py
Normal file
@ -0,0 +1,527 @@
|
|||||||
|
"""
|
||||||
|
* | File : epd13in3k.py
|
||||||
|
* | Author : Waveshare team
|
||||||
|
* | Function : Electronic paper driver
|
||||||
|
* | Info :
|
||||||
|
*----------------
|
||||||
|
* | This version: V1.0
|
||||||
|
* | Date : 2023-09-08
|
||||||
|
# | Info : python demo
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from inkycal.display.drivers import epdconfig
|
||||||
|
|
||||||
|
# Display resolution
|
||||||
|
EPD_WIDTH = 960
|
||||||
|
EPD_HEIGHT = 680
|
||||||
|
|
||||||
|
GRAY1 = 0xff # white
|
||||||
|
GRAY2 = 0xC0
|
||||||
|
GRAY3 = 0x80 # gray
|
||||||
|
GRAY4 = 0x00 # Blackest
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EPD:
|
||||||
|
def __init__(self):
|
||||||
|
self.reset_pin = epdconfig.RST_PIN
|
||||||
|
self.dc_pin = epdconfig.DC_PIN
|
||||||
|
self.busy_pin = epdconfig.BUSY_PIN
|
||||||
|
self.cs_pin = epdconfig.CS_PIN
|
||||||
|
self.width = EPD_WIDTH
|
||||||
|
self.height = EPD_HEIGHT
|
||||||
|
self.GRAY1 = GRAY1 # white
|
||||||
|
self.GRAY2 = GRAY2
|
||||||
|
self.GRAY3 = GRAY3 # gray
|
||||||
|
self.GRAY4 = GRAY4 # Blackest
|
||||||
|
|
||||||
|
self.Lut_Partial = [
|
||||||
|
0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x2A, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x15, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x01, 0x01, 0x00,
|
||||||
|
0x0A, 0x00, 0x05, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x01,
|
||||||
|
0x22, 0x22, 0x22, 0x22, 0x22,
|
||||||
|
0x17, 0x41, 0xA8, 0x32, 0x18,
|
||||||
|
0x00, 0x00, ]
|
||||||
|
|
||||||
|
self.LUT_DATA_4Gray = [
|
||||||
|
0x80, 0x48, 0x4A, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x0A, 0x48, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x88, 0x48, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0xA8, 0x48, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x07, 0x23, 0x17, 0x02, 0x00,
|
||||||
|
0x05, 0x01, 0x05, 0x01, 0x02,
|
||||||
|
0x08, 0x02, 0x01, 0x04, 0x04,
|
||||||
|
0x00, 0x02, 0x00, 0x02, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x01,
|
||||||
|
0x22, 0x22, 0x22, 0x22, 0x22,
|
||||||
|
0x17, 0x41, 0xA8, 0x32, 0x30,
|
||||||
|
0x00, 0x00, ]
|
||||||
|
|
||||||
|
if (epdconfig.module_init() != 0):
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# Hardware reset
|
||||||
|
def reset(self):
|
||||||
|
epdconfig.digital_write(self.reset_pin, 1)
|
||||||
|
epdconfig.delay_ms(20)
|
||||||
|
epdconfig.digital_write(self.reset_pin, 0)
|
||||||
|
epdconfig.delay_ms(2)
|
||||||
|
epdconfig.digital_write(self.reset_pin, 1)
|
||||||
|
epdconfig.delay_ms(20)
|
||||||
|
|
||||||
|
def send_command(self, command):
|
||||||
|
epdconfig.digital_write(self.dc_pin, 0)
|
||||||
|
epdconfig.digital_write(self.cs_pin, 0)
|
||||||
|
epdconfig.spi_writebyte([command])
|
||||||
|
epdconfig.digital_write(self.cs_pin, 1)
|
||||||
|
|
||||||
|
def send_data(self, data):
|
||||||
|
epdconfig.digital_write(self.dc_pin, 1)
|
||||||
|
epdconfig.digital_write(self.cs_pin, 0)
|
||||||
|
epdconfig.spi_writebyte([data])
|
||||||
|
epdconfig.digital_write(self.cs_pin, 1)
|
||||||
|
|
||||||
|
def send_data2(self, data):
|
||||||
|
epdconfig.digital_write(self.dc_pin, 1)
|
||||||
|
epdconfig.digital_write(self.cs_pin, 0)
|
||||||
|
epdconfig.SPI.writebytes2(data)
|
||||||
|
epdconfig.digital_write(self.cs_pin, 1)
|
||||||
|
|
||||||
|
def ReadBusy(self):
|
||||||
|
logger.debug("e-Paper busy")
|
||||||
|
busy = epdconfig.digital_read(self.busy_pin)
|
||||||
|
while (busy == 1):
|
||||||
|
busy = epdconfig.digital_read(self.busy_pin)
|
||||||
|
epdconfig.delay_ms(20)
|
||||||
|
epdconfig.delay_ms(20)
|
||||||
|
logger.debug("e-Paper busy release")
|
||||||
|
|
||||||
|
def TurnOnDisplay(self):
|
||||||
|
self.send_command(0x22) # Display Update Control
|
||||||
|
self.send_data(0xF7)
|
||||||
|
self.send_command(0x20) # Activate Display Update Sequence
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
def TurnOnDisplay_Part(self):
|
||||||
|
self.send_command(0x22) # Display Update Control
|
||||||
|
self.send_data(0xCF)
|
||||||
|
self.send_command(0x20) # Activate Display Update Sequence
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
def TurnOnDisplay_4GRAY(self):
|
||||||
|
self.send_command(0x22) # Display Update Control
|
||||||
|
self.send_data(0xC7)
|
||||||
|
self.send_command(0x20) # Activate Display Update Sequence
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
def Lut(self, LUT):
|
||||||
|
self.send_command(0x32)
|
||||||
|
for i in range(105):
|
||||||
|
self.send_data(LUT[i])
|
||||||
|
|
||||||
|
self.send_command(0x03)
|
||||||
|
self.send_data(LUT[105])
|
||||||
|
|
||||||
|
self.send_command(0x04)
|
||||||
|
self.send_data(LUT[106])
|
||||||
|
self.send_data(LUT[107])
|
||||||
|
self.send_data(LUT[108])
|
||||||
|
|
||||||
|
self.send_command(0x2C)
|
||||||
|
self.send_data(LUT[109])
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
|
||||||
|
# EPD hardware init start
|
||||||
|
self.reset()
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
self.send_command(0x12) # SWRESET
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
self.send_command(0x0C)
|
||||||
|
self.send_data(0xAE)
|
||||||
|
self.send_data(0xC7)
|
||||||
|
self.send_data(0xC3)
|
||||||
|
self.send_data(0xC0)
|
||||||
|
self.send_data(0x80)
|
||||||
|
|
||||||
|
self.send_command(0x01)
|
||||||
|
self.send_data(0xA7)
|
||||||
|
self.send_data(0x02)
|
||||||
|
self.send_data(0x00)
|
||||||
|
|
||||||
|
self.send_command(0x11)
|
||||||
|
self.send_data(0x03)
|
||||||
|
|
||||||
|
self.send_command(0x44)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0xBF)
|
||||||
|
self.send_data(0x03)
|
||||||
|
|
||||||
|
self.send_command(0x45)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0xA7)
|
||||||
|
self.send_data(0x02)
|
||||||
|
|
||||||
|
self.send_command(0x3C)
|
||||||
|
self.send_data(0x05)
|
||||||
|
|
||||||
|
self.send_command(0x18)
|
||||||
|
self.send_data(0x80)
|
||||||
|
|
||||||
|
self.send_command(0x4E)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
|
||||||
|
self.send_command(0x4F)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
|
||||||
|
# EPD hardware init end
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def init_Part(self):
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
self.send_command(0x3C)
|
||||||
|
self.send_data(0x80)
|
||||||
|
|
||||||
|
self.Lut(self.Lut_Partial)
|
||||||
|
|
||||||
|
self.send_command(0x37)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x40)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
|
||||||
|
self.send_command(0x3C)
|
||||||
|
self.send_data(0x80)
|
||||||
|
|
||||||
|
self.send_command(0x22)
|
||||||
|
self.send_data(0xC0)
|
||||||
|
self.send_command(0x20)
|
||||||
|
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
def init_4GRAY(self):
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
self.ReadBusy()
|
||||||
|
self.send_command(0x12)
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
self.send_command(0x0C)
|
||||||
|
self.send_data(0xAE)
|
||||||
|
self.send_data(0xC7)
|
||||||
|
self.send_data(0xC3)
|
||||||
|
self.send_data(0xC0)
|
||||||
|
self.send_data(0x80)
|
||||||
|
|
||||||
|
self.send_command(0x01)
|
||||||
|
self.send_data(0xA7)
|
||||||
|
self.send_data(0x02)
|
||||||
|
self.send_data(0x00)
|
||||||
|
|
||||||
|
self.send_command(0x11)
|
||||||
|
self.send_data(0x03)
|
||||||
|
|
||||||
|
self.send_command(0x44)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0xBF)
|
||||||
|
self.send_data(0x03)
|
||||||
|
|
||||||
|
self.send_command(0x45)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0xA7)
|
||||||
|
self.send_data(0x02)
|
||||||
|
|
||||||
|
self.send_command(0x3C)
|
||||||
|
self.send_data(0x00)
|
||||||
|
|
||||||
|
self.send_command(0x18)
|
||||||
|
self.send_data(0x80)
|
||||||
|
|
||||||
|
self.send_command(0x4E)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
|
||||||
|
self.send_command(0x4F)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
|
||||||
|
self.Lut(self.LUT_DATA_4Gray)
|
||||||
|
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
def getbuffer(self, image):
|
||||||
|
# logger.debug("bufsiz = ",int(self.width/8) * self.height)
|
||||||
|
buf = [0xFF] * (int(self.width / 8) * self.height)
|
||||||
|
image_monocolor = image.convert('1')
|
||||||
|
imwidth, imheight = image_monocolor.size
|
||||||
|
pixels = image_monocolor.load()
|
||||||
|
# logger.debug("imwidth = %d, imheight = %d",imwidth,imheight)
|
||||||
|
if imwidth == self.width and imheight == self.height:
|
||||||
|
logger.debug("Horizontal")
|
||||||
|
for y in range(imheight):
|
||||||
|
for x in range(imwidth):
|
||||||
|
# Set the bits for the column of pixels at the current position.
|
||||||
|
if pixels[x, y] == 0:
|
||||||
|
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
|
||||||
|
elif imwidth == self.height and imheight == self.width:
|
||||||
|
logger.debug("Vertical")
|
||||||
|
for y in range(imheight):
|
||||||
|
for x in range(imwidth):
|
||||||
|
newx = y
|
||||||
|
newy = self.height - x - 1
|
||||||
|
if pixels[x, y] == 0:
|
||||||
|
buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8))
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def getbuffer_4Gray(self, image):
|
||||||
|
# logger.debug("bufsiz = ",int(self.width/8) * self.height)
|
||||||
|
buf = [0xFF] * (int(self.width / 4) * self.height)
|
||||||
|
image_monocolor = image.convert('L')
|
||||||
|
imwidth, imheight = image_monocolor.size
|
||||||
|
pixels = image_monocolor.load()
|
||||||
|
i = 0
|
||||||
|
# logger.debug("imwidth = %d, imheight = %d",imwidth,imheight)
|
||||||
|
if (imwidth == self.width and imheight == self.height):
|
||||||
|
logger.debug("Vertical")
|
||||||
|
for y in range(imheight):
|
||||||
|
for x in range(imwidth):
|
||||||
|
# Set the bits for the column of pixels at the current position.
|
||||||
|
if (pixels[x, y] == 0xC0):
|
||||||
|
pixels[x, y] = 0x80
|
||||||
|
elif (pixels[x, y] == 0x80):
|
||||||
|
pixels[x, y] = 0x40
|
||||||
|
i = i + 1
|
||||||
|
if (i % 4 == 0):
|
||||||
|
buf[int((x + (y * self.width)) / 4)] = (
|
||||||
|
(pixels[x - 3, y] & 0xc0) | (pixels[x - 2, y] & 0xc0) >> 2 | (
|
||||||
|
pixels[x - 1, y] & 0xc0) >> 4 | (pixels[x, y] & 0xc0) >> 6)
|
||||||
|
|
||||||
|
elif (imwidth == self.height and imheight == self.width):
|
||||||
|
logger.debug("Horizontal")
|
||||||
|
for x in range(imwidth):
|
||||||
|
for y in range(imheight):
|
||||||
|
newx = y
|
||||||
|
newy = self.height - x - 1
|
||||||
|
if (pixels[x, y] == 0xC0):
|
||||||
|
pixels[x, y] = 0x80
|
||||||
|
elif (pixels[x, y] == 0x80):
|
||||||
|
pixels[x, y] = 0x40
|
||||||
|
i = i + 1
|
||||||
|
if (i % 4 == 0):
|
||||||
|
buf[int((newx + (newy * self.width)) / 4)] = (
|
||||||
|
(pixels[x, y - 3] & 0xc0) | (pixels[x, y - 2] & 0xc0) >> 2 | (
|
||||||
|
pixels[x, y - 1] & 0xc0) >> 4 | (pixels[x, y] & 0xc0) >> 6)
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def Clear(self):
|
||||||
|
buf = [0xFF] * (int(self.width / 8) * self.height)
|
||||||
|
self.send_command(0x24)
|
||||||
|
self.send_data2(buf)
|
||||||
|
|
||||||
|
self.TurnOnDisplay()
|
||||||
|
|
||||||
|
def display(self, image):
|
||||||
|
self.send_command(0x24)
|
||||||
|
self.send_data2(image)
|
||||||
|
|
||||||
|
self.TurnOnDisplay()
|
||||||
|
|
||||||
|
def display_Base(self, image):
|
||||||
|
self.send_command(0x24)
|
||||||
|
self.send_data2(image)
|
||||||
|
|
||||||
|
self.send_command(0x26)
|
||||||
|
self.send_data2(image)
|
||||||
|
|
||||||
|
self.TurnOnDisplay()
|
||||||
|
|
||||||
|
def display_Base_color(self, color):
|
||||||
|
if (self.width % 8 == 0):
|
||||||
|
Width = self.width // 8
|
||||||
|
else:
|
||||||
|
Width = self.width // 8 + 1
|
||||||
|
Height = self.height
|
||||||
|
self.send_command(0x24) # Write Black and White image to RAM
|
||||||
|
for j in range(Height):
|
||||||
|
for i in range(Width):
|
||||||
|
self.send_data(color)
|
||||||
|
|
||||||
|
self.send_command(0x26) # Write Black and White image to RAM
|
||||||
|
for j in range(Height):
|
||||||
|
for i in range(Width):
|
||||||
|
self.send_data(color)
|
||||||
|
# self.TurnOnDisplay()
|
||||||
|
|
||||||
|
def display_Partial(self, Image, Xstart, Ystart, Xend, Yend):
|
||||||
|
if ((Xstart % 8 + Xend % 8 == 8 & Xstart % 8 > Xend % 8) | Xstart % 8 + Xend % 8 == 0 | (
|
||||||
|
Xend - Xstart) % 8 == 0):
|
||||||
|
Xstart = Xstart // 8
|
||||||
|
Xend = Xend // 8
|
||||||
|
else:
|
||||||
|
Xstart = Xstart // 8
|
||||||
|
if Xend % 8 == 0:
|
||||||
|
Xend = Xend // 8
|
||||||
|
else:
|
||||||
|
Xend = Xend // 8 + 1
|
||||||
|
|
||||||
|
if (self.width % 8 == 0):
|
||||||
|
Width = self.width // 8
|
||||||
|
else:
|
||||||
|
Width = self.width // 8 + 1
|
||||||
|
Height = self.height
|
||||||
|
|
||||||
|
Xend -= 1
|
||||||
|
Yend -= 1
|
||||||
|
|
||||||
|
self.send_command(0x44)
|
||||||
|
self.send_data((Xstart * 8) & 0xff)
|
||||||
|
self.send_data((Xstart >> 5) & 0x01)
|
||||||
|
self.send_data((Xend * 8) & 0xff)
|
||||||
|
self.send_data((Xend >> 5) & 0x01)
|
||||||
|
self.send_command(0x45)
|
||||||
|
self.send_data(Ystart & 0xff)
|
||||||
|
self.send_data((Ystart >> 8) & 0x01)
|
||||||
|
self.send_data(Yend & 0xff)
|
||||||
|
self.send_data((Yend >> 8) & 0x01)
|
||||||
|
|
||||||
|
self.send_command(0x4E)
|
||||||
|
self.send_data((Xstart * 8) & 0xff)
|
||||||
|
self.send_data((Xstart >> 5) & 0x01)
|
||||||
|
self.send_command(0x4F)
|
||||||
|
self.send_data(Ystart & 0xff)
|
||||||
|
self.send_data((Ystart >> 8) & 0x01)
|
||||||
|
|
||||||
|
self.send_command(0x24)
|
||||||
|
for j in range(Height):
|
||||||
|
for i in range(Width):
|
||||||
|
if ((j > Ystart - 1) & (j < (Yend + 1)) & (i > Xstart - 1) & (i < (Xend + 1))):
|
||||||
|
self.send_data(Image[i + j * Width])
|
||||||
|
self.TurnOnDisplay_Part()
|
||||||
|
|
||||||
|
def display_4Gray(self, image):
|
||||||
|
self.send_command(0x24)
|
||||||
|
for i in range(0, 81600):
|
||||||
|
temp3 = 0
|
||||||
|
for j in range(0, 2):
|
||||||
|
temp1 = image[i * 2 + j]
|
||||||
|
for k in range(0, 2):
|
||||||
|
temp2 = temp1 & 0xC0
|
||||||
|
if (temp2 == 0xC0):
|
||||||
|
temp3 |= 0x00
|
||||||
|
elif (temp2 == 0x00):
|
||||||
|
temp3 |= 0x01
|
||||||
|
elif (temp2 == 0x80):
|
||||||
|
temp3 |= 0x01
|
||||||
|
else: # 0x40
|
||||||
|
temp3 |= 0x00
|
||||||
|
temp3 <<= 1
|
||||||
|
|
||||||
|
temp1 <<= 2
|
||||||
|
temp2 = temp1 & 0xC0
|
||||||
|
if (temp2 == 0xC0):
|
||||||
|
temp3 |= 0x00
|
||||||
|
elif (temp2 == 0x00):
|
||||||
|
temp3 |= 0x01
|
||||||
|
elif (temp2 == 0x80):
|
||||||
|
temp3 |= 0x01
|
||||||
|
else: # 0x40
|
||||||
|
temp3 |= 0x00
|
||||||
|
if (j != 1 or k != 1):
|
||||||
|
temp3 <<= 1
|
||||||
|
temp1 <<= 2
|
||||||
|
self.send_data(temp3)
|
||||||
|
|
||||||
|
self.send_command(0x26)
|
||||||
|
for i in range(0, 81600):
|
||||||
|
temp3 = 0
|
||||||
|
for j in range(0, 2):
|
||||||
|
temp1 = image[i * 2 + j]
|
||||||
|
for k in range(0, 2):
|
||||||
|
temp2 = temp1 & 0xC0
|
||||||
|
if (temp2 == 0xC0):
|
||||||
|
temp3 |= 0x00
|
||||||
|
elif (temp2 == 0x00):
|
||||||
|
temp3 |= 0x01
|
||||||
|
elif (temp2 == 0x80):
|
||||||
|
temp3 |= 0x00
|
||||||
|
else: # 0x40
|
||||||
|
temp3 |= 0x01
|
||||||
|
temp3 <<= 1
|
||||||
|
|
||||||
|
temp1 <<= 2
|
||||||
|
temp2 = temp1 & 0xC0
|
||||||
|
if (temp2 == 0xC0):
|
||||||
|
temp3 |= 0x00
|
||||||
|
elif (temp2 == 0x00):
|
||||||
|
temp3 |= 0x01
|
||||||
|
elif (temp2 == 0x80):
|
||||||
|
temp3 |= 0x00
|
||||||
|
else: # 0x40
|
||||||
|
temp3 |= 0x01
|
||||||
|
if (j != 1 or k != 1):
|
||||||
|
temp3 <<= 1
|
||||||
|
temp1 <<= 2
|
||||||
|
self.send_data(temp3)
|
||||||
|
|
||||||
|
self.TurnOnDisplay_4GRAY()
|
||||||
|
|
||||||
|
def sleep(self):
|
||||||
|
self.send_command(0x10) # DEEP_SLEEP
|
||||||
|
self.send_data(0x03)
|
||||||
|
|
||||||
|
epdconfig.delay_ms(2000)
|
||||||
|
epdconfig.module_exit()
|
299
inkycal/display/drivers/epd_13_in_3_colour.py
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
"""
|
||||||
|
* | File : epd13in3b.py
|
||||||
|
* | Author : Waveshare team
|
||||||
|
* | Function : Electronic paper driver
|
||||||
|
* | Info :
|
||||||
|
*----------------
|
||||||
|
* | This version: V1.0
|
||||||
|
* | Date : 2024-04-08
|
||||||
|
# | Info : python demo
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from inkycal.display.drivers import epdconfig
|
||||||
|
|
||||||
|
# Display resolution
|
||||||
|
EPD_WIDTH = 960
|
||||||
|
EPD_HEIGHT = 680
|
||||||
|
|
||||||
|
GRAY1 = 0xff # white
|
||||||
|
GRAY2 = 0xC0
|
||||||
|
GRAY3 = 0x80 # gray
|
||||||
|
GRAY4 = 0x00 # Blackest
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EPD:
|
||||||
|
def __init__(self):
|
||||||
|
self.reset_pin = epdconfig.RST_PIN
|
||||||
|
self.dc_pin = epdconfig.DC_PIN
|
||||||
|
self.busy_pin = epdconfig.BUSY_PIN
|
||||||
|
self.cs_pin = epdconfig.CS_PIN
|
||||||
|
self.width = EPD_WIDTH
|
||||||
|
self.height = EPD_HEIGHT
|
||||||
|
if (epdconfig.module_init() != 0):
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# Hardware reset
|
||||||
|
def reset(self):
|
||||||
|
epdconfig.digital_write(self.reset_pin, 1)
|
||||||
|
epdconfig.delay_ms(20)
|
||||||
|
epdconfig.digital_write(self.reset_pin, 0)
|
||||||
|
epdconfig.delay_ms(2)
|
||||||
|
epdconfig.digital_write(self.reset_pin, 1)
|
||||||
|
epdconfig.delay_ms(20)
|
||||||
|
|
||||||
|
def send_command(self, command):
|
||||||
|
epdconfig.digital_write(self.dc_pin, 0)
|
||||||
|
epdconfig.digital_write(self.cs_pin, 0)
|
||||||
|
epdconfig.spi_writebyte([command])
|
||||||
|
epdconfig.digital_write(self.cs_pin, 1)
|
||||||
|
|
||||||
|
def send_data(self, data):
|
||||||
|
epdconfig.digital_write(self.dc_pin, 1)
|
||||||
|
epdconfig.digital_write(self.cs_pin, 0)
|
||||||
|
epdconfig.spi_writebyte([data])
|
||||||
|
epdconfig.digital_write(self.cs_pin, 1)
|
||||||
|
|
||||||
|
def send_data2(self, data):
|
||||||
|
epdconfig.digital_write(self.dc_pin, 1)
|
||||||
|
epdconfig.digital_write(self.cs_pin, 0)
|
||||||
|
epdconfig.SPI.writebytes2(data)
|
||||||
|
epdconfig.digital_write(self.cs_pin, 1)
|
||||||
|
|
||||||
|
def ReadBusy(self):
|
||||||
|
logger.debug("e-Paper busy")
|
||||||
|
busy = epdconfig.digital_read(self.busy_pin)
|
||||||
|
while (busy == 1):
|
||||||
|
busy = epdconfig.digital_read(self.busy_pin)
|
||||||
|
epdconfig.delay_ms(20)
|
||||||
|
epdconfig.delay_ms(20)
|
||||||
|
logger.debug("e-Paper busy release")
|
||||||
|
|
||||||
|
def TurnOnDisplay(self):
|
||||||
|
self.send_command(0x22) # Display Update Control
|
||||||
|
self.send_data(0xF7)
|
||||||
|
self.send_command(0x20) # Activate Display Update Sequence
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
def TurnOnDisplay_Part(self):
|
||||||
|
self.send_command(0x22) # Display Update Control
|
||||||
|
self.send_data(0xFF)
|
||||||
|
self.send_command(0x20) # Activate Display Update Sequence
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
# EPD hardware init start
|
||||||
|
self.reset()
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
self.send_command(0x12) # SWRESET
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
self.send_command(0x0C)
|
||||||
|
self.send_data(0xAE)
|
||||||
|
self.send_data(0xC7)
|
||||||
|
self.send_data(0xC3)
|
||||||
|
self.send_data(0xC0)
|
||||||
|
self.send_data(0x80)
|
||||||
|
|
||||||
|
self.send_command(0x01)
|
||||||
|
self.send_data(0xA7)
|
||||||
|
self.send_data(0x02)
|
||||||
|
self.send_data(0x00)
|
||||||
|
|
||||||
|
self.send_command(0x11)
|
||||||
|
self.send_data(0x03)
|
||||||
|
|
||||||
|
self.send_command(0x44)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0xBF)
|
||||||
|
self.send_data(0x03)
|
||||||
|
|
||||||
|
self.send_command(0x45)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0xA7)
|
||||||
|
self.send_data(0x02)
|
||||||
|
|
||||||
|
self.send_command(0x3C)
|
||||||
|
self.send_data(0x01)
|
||||||
|
|
||||||
|
self.send_command(0x18)
|
||||||
|
self.send_data(0x80)
|
||||||
|
|
||||||
|
self.send_command(0x4E)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
|
||||||
|
self.send_command(0x4F)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.send_data(0x00)
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
# EPD hardware init end
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def getbuffer(self, image):
|
||||||
|
# logger.debug("bufsiz = ",int(self.width/8) * self.height)
|
||||||
|
buf = [0xFF] * (int(self.width / 8) * self.height)
|
||||||
|
image_monocolor = image.convert('1')
|
||||||
|
imwidth, imheight = image_monocolor.size
|
||||||
|
pixels = image_monocolor.load()
|
||||||
|
# logger.debug("imwidth = %d, imheight = %d",imwidth,imheight)
|
||||||
|
if imwidth == self.width and imheight == self.height:
|
||||||
|
logger.debug("Horizontal")
|
||||||
|
for y in range(imheight):
|
||||||
|
for x in range(imwidth):
|
||||||
|
# Set the bits for the column of pixels at the current position.
|
||||||
|
if pixels[x, y] == 0:
|
||||||
|
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
|
||||||
|
elif imwidth == self.height and imheight == self.width:
|
||||||
|
logger.debug("Vertical")
|
||||||
|
for y in range(imheight):
|
||||||
|
for x in range(imwidth):
|
||||||
|
newx = y
|
||||||
|
newy = self.height - x - 1
|
||||||
|
if pixels[x, y] == 0:
|
||||||
|
buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8))
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def Clear(self):
|
||||||
|
self.send_command(0x24)
|
||||||
|
self.send_data2([0xFF] * (int(self.width / 8) * self.height))
|
||||||
|
self.send_command(0x26)
|
||||||
|
self.send_data2([0x00] * (int(self.width / 8) * self.height))
|
||||||
|
|
||||||
|
self.TurnOnDisplay()
|
||||||
|
|
||||||
|
def Clear_Base(self):
|
||||||
|
self.send_command(0x24)
|
||||||
|
self.send_data2([0xFF] * (int(self.width / 8) * self.height))
|
||||||
|
self.send_command(0x26)
|
||||||
|
self.send_data2([0x00] * (int(self.width / 8) * self.height))
|
||||||
|
|
||||||
|
self.TurnOnDisplay()
|
||||||
|
self.send_command(0x26)
|
||||||
|
self.send_data2([0xFF] * (int(self.width / 8) * self.height))
|
||||||
|
|
||||||
|
def display(self, blackimage, ryimage):
|
||||||
|
if (self.width % 8 == 0):
|
||||||
|
Width = self.width // 8
|
||||||
|
else:
|
||||||
|
Width = self.width // 8 + 1
|
||||||
|
Height = self.height
|
||||||
|
if (blackimage != None):
|
||||||
|
self.send_command(0x24)
|
||||||
|
self.send_data2(blackimage)
|
||||||
|
if (ryimage != None):
|
||||||
|
for j in range(Height):
|
||||||
|
for i in range(Width):
|
||||||
|
ryimage[i + j * Width] = ~ryimage[i + j * Width]
|
||||||
|
self.send_command(0x26)
|
||||||
|
self.send_data2(ryimage)
|
||||||
|
|
||||||
|
self.TurnOnDisplay()
|
||||||
|
|
||||||
|
def display_Base(self, blackimage, ryimage):
|
||||||
|
if (self.width % 8 == 0):
|
||||||
|
Width = self.width // 8
|
||||||
|
else:
|
||||||
|
Width = self.width // 8 + 1
|
||||||
|
Height = self.height
|
||||||
|
if (blackimage != None):
|
||||||
|
self.send_command(0x24)
|
||||||
|
self.send_data2(blackimage)
|
||||||
|
if (ryimage != None):
|
||||||
|
for j in range(Height):
|
||||||
|
for i in range(Width):
|
||||||
|
ryimage[i + j * Width] = ~ryimage[i + j * Width]
|
||||||
|
self.send_command(0x26)
|
||||||
|
self.send_data2(ryimage)
|
||||||
|
|
||||||
|
self.TurnOnDisplay()
|
||||||
|
|
||||||
|
self.send_command(0x26)
|
||||||
|
self.send_data2(blackimage)
|
||||||
|
|
||||||
|
def display_Partial(self, Image, Xstart, Ystart, Xend, Yend):
|
||||||
|
if ((Xstart % 8 + Xend % 8 == 8 & Xstart % 8 > Xend % 8) | Xstart % 8 + Xend % 8 == 0 | (
|
||||||
|
Xend - Xstart) % 8 == 0):
|
||||||
|
Xstart = Xstart // 8
|
||||||
|
Xend = Xend // 8
|
||||||
|
else:
|
||||||
|
Xstart = Xstart // 8
|
||||||
|
if Xend % 8 == 0:
|
||||||
|
Xend = Xend // 8
|
||||||
|
else:
|
||||||
|
Xend = Xend // 8 + 1
|
||||||
|
|
||||||
|
if (self.width % 8 == 0):
|
||||||
|
Width = self.width // 8
|
||||||
|
else:
|
||||||
|
Width = self.width // 8 + 1
|
||||||
|
Height = self.height
|
||||||
|
|
||||||
|
Xend -= 1
|
||||||
|
Yend -= 1
|
||||||
|
|
||||||
|
self.send_command(0x3C)
|
||||||
|
self.send_data(0x80)
|
||||||
|
|
||||||
|
self.send_command(0x44)
|
||||||
|
self.send_data((Xstart * 8) & 0xff)
|
||||||
|
self.send_data((Xstart >> 5) & 0x01)
|
||||||
|
self.send_data((Xend * 8) & 0xff)
|
||||||
|
self.send_data((Xend >> 5) & 0x01)
|
||||||
|
self.send_command(0x45)
|
||||||
|
self.send_data(Ystart & 0xff)
|
||||||
|
self.send_data((Ystart >> 8) & 0x01)
|
||||||
|
self.send_data(Yend & 0xff)
|
||||||
|
self.send_data((Yend >> 8) & 0x01)
|
||||||
|
|
||||||
|
self.send_command(0x4E)
|
||||||
|
self.send_data((Xstart * 8) & 0xff)
|
||||||
|
self.send_data((Xstart >> 5) & 0x01)
|
||||||
|
self.send_command(0x4F)
|
||||||
|
self.send_data(Ystart & 0xff)
|
||||||
|
self.send_data((Ystart >> 8) & 0x01)
|
||||||
|
|
||||||
|
self.send_command(0x24)
|
||||||
|
for j in range(Height):
|
||||||
|
for i in range(Width):
|
||||||
|
if ((j > Ystart - 1) & (j < (Yend + 1)) & (i > Xstart - 1) & (i < (Xend + 1))):
|
||||||
|
self.send_data(Image[i + j * Width])
|
||||||
|
self.TurnOnDisplay_Part()
|
||||||
|
|
||||||
|
self.send_command(0x26)
|
||||||
|
for j in range(Height):
|
||||||
|
for i in range(Width):
|
||||||
|
if ((j > Ystart - 1) & (j < (Yend + 1)) & (i > Xstart - 1) & (i < (Xend + 1))):
|
||||||
|
self.send_data(Image[i + j * Width])
|
||||||
|
|
||||||
|
def sleep(self):
|
||||||
|
self.send_command(0x10) # DEEP_SLEEP
|
||||||
|
self.send_data(0x03)
|
||||||
|
|
||||||
|
epdconfig.delay_ms(2000)
|
||||||
|
epdconfig.module_exit()
|
@ -28,8 +28,6 @@ THE SOFTWARE.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@ -128,4 +126,3 @@ implementation = RaspberryPi()
|
|||||||
|
|
||||||
for func in [x for x in dir(implementation) if not x.startswith('_')]:
|
for func in [x for x in dir(implementation) if not x.startswith('_')]:
|
||||||
setattr(sys.modules[__name__], func, getattr(implementation, func))
|
setattr(sys.modules[__name__], func, getattr(implementation, func))
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
supported_models = {
|
supported_models = {
|
||||||
|
"epd_13_in_3": (960, 680),
|
||||||
|
"epd_13_in_3_colour": (960, 680),
|
||||||
"epd_12_in_48": (1304, 984),
|
"epd_12_in_48": (1304, 984),
|
||||||
"epd_7_in_5_colour": (640, 384),
|
"epd_7_in_5_colour": (640, 384),
|
||||||
"9_in_7": (1200, 825),
|
"9_in_7": (1200, 825),
|
||||||
|
35
inkycal/loggers.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"""Logging configuration for Inkycal."""
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
|
from inkycal.settings import Settings
|
||||||
|
|
||||||
|
# On the console, set a logger to show only important logs
|
||||||
|
# (level ERROR or higher)
|
||||||
|
stream_handler = logging.StreamHandler()
|
||||||
|
stream_handler.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
if not os.path.exists(settings.LOG_PATH):
|
||||||
|
os.mkdir(settings.LOG_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
# Save all logs to a file, which contains more detailed output
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s | %(name)s | %(levelname)s: %(message)s',
|
||||||
|
datefmt='%d-%m-%Y %H:%M:%S',
|
||||||
|
handlers=[
|
||||||
|
stream_handler, # add stream handler from above
|
||||||
|
RotatingFileHandler( # log to a file too
|
||||||
|
settings.INKYCAL_LOG_PATH, # file to log
|
||||||
|
maxBytes=2*1024*1024, # 2MB max filesize
|
||||||
|
backupCount=5 # create max 5 log files
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show less logging for PIL module
|
||||||
|
logging.getLogger("PIL").setLevel(logging.WARNING)
|
368
inkycal/main.py
@ -6,44 +6,22 @@ Copyright by aceinnolab
|
|||||||
import asyncio
|
import asyncio
|
||||||
import glob
|
import glob
|
||||||
import hashlib
|
import hashlib
|
||||||
from logging.handlers import RotatingFileHandler
|
import os.path
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
|
from inkycal import loggers # noqa
|
||||||
from inkycal.custom import *
|
from inkycal.custom import *
|
||||||
from inkycal.display import Display
|
from inkycal.display import Display
|
||||||
from inkycal.modules.inky_image import Inkyimage as Images
|
from inkycal.modules.inky_image import Inkyimage as Images
|
||||||
|
from inkycal.utils import JSONCache
|
||||||
# On the console, set a logger to show only important logs
|
|
||||||
# (level ERROR or higher)
|
|
||||||
stream_handler = logging.StreamHandler()
|
|
||||||
stream_handler.setLevel(logging.ERROR)
|
|
||||||
|
|
||||||
if not os.path.exists(f'{top_level}/logs'):
|
|
||||||
os.mkdir(f'{top_level}/logs')
|
|
||||||
|
|
||||||
# Save all logs to a file, which contains more detailed output
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s | %(name)s | %(levelname)s: %(message)s',
|
|
||||||
datefmt='%d-%m-%Y %H:%M:%S',
|
|
||||||
handlers=[
|
|
||||||
stream_handler, # add stream handler from above
|
|
||||||
RotatingFileHandler( # log to a file too
|
|
||||||
f'{top_level}/logs/inkycal.log', # file to log
|
|
||||||
maxBytes=2097152, # 2MB max filesize
|
|
||||||
backupCount=5 # create max 5 log files
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Show less logging for PIL module
|
|
||||||
logging.getLogger("PIL").setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
CACHE_NAME = "inkycal_main"
|
||||||
|
|
||||||
# TODO: autostart -> supervisor?
|
|
||||||
|
|
||||||
class Inkycal:
|
class Inkycal:
|
||||||
"""Inkycal main class
|
"""Inkycal main class
|
||||||
@ -60,43 +38,61 @@ class Inkycal:
|
|||||||
to improve rendering on E-Papers. Set this to False for 9.7" E-Paper.
|
to improve rendering on E-Papers. Set this to False for 9.7" E-Paper.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, settings_path: str or None = None, render: bool = True):
|
def __init__(self, settings_path: str or None = None, render: bool = True, use_pi_sugar: bool = False,
|
||||||
"""Initialise Inkycal"""
|
shutdown_after_run: bool = False) -> None:
|
||||||
|
"""Initialise Inkycal
|
||||||
|
|
||||||
# Get the release version from setup.py
|
Args:
|
||||||
with open(f'{top_level}/setup.py') as setup_file:
|
settings_path (str):
|
||||||
for line in setup_file:
|
The full path to your settings.json file. If no path was specified, will look in the /boot directory.
|
||||||
if line.startswith('__version__'):
|
render (bool):
|
||||||
self._release = line.split("=")[-1].replace("'", "").replace('"', "").replace(" ", "")
|
Show the image on the E-Paper display.
|
||||||
break
|
use_pi_sugar (bool):
|
||||||
|
Use PiSugar board (all revisions). Default is False.
|
||||||
|
shutdown_after_run (bool):
|
||||||
|
Shutdown the system after the run is complete. Will only work with PiSugar enabled.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._release = "2.0.4"
|
||||||
|
|
||||||
|
logger.info(f"Inkycal v{self._release} booting up...")
|
||||||
|
|
||||||
self.render = render
|
self.render = render
|
||||||
self.info = None
|
self.info = None
|
||||||
|
|
||||||
|
logger.info("Checking if a settings file is present...")
|
||||||
# load settings file - throw an error if file could not be found
|
# load settings file - throw an error if file could not be found
|
||||||
if settings_path:
|
if settings_path:
|
||||||
|
logger.info(f"Custom location for settings.json file specified: {settings_path}")
|
||||||
try:
|
try:
|
||||||
with open(settings_path) as settings_file:
|
with open(settings_path, mode="r") as settings_file:
|
||||||
settings = json.load(settings_file)
|
self.settings = json.load(settings_file)
|
||||||
self.settings = settings
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"No settings.json file could be found in the specified location: {settings_path}")
|
f"No settings.json file could be found in the specified location: {settings_path}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
found = False
|
||||||
with open('/boot/settings.json') as settings_file:
|
for location in settings.SETTINGS_JSON_PATHS:
|
||||||
settings = json.load(settings_file)
|
if os.path.exists(location):
|
||||||
self.settings = settings
|
logger.info(f"Found settings.json file in {location}")
|
||||||
|
with open(location, mode="r") as settings_file:
|
||||||
except FileNotFoundError:
|
self.settings = json.load(settings_file)
|
||||||
raise SettingsFileNotFoundError
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
raise SettingsFileNotFoundError(f"No settings.json file could be found in {settings.SETTINGS_JSON_PATHS} and no explicit path was specified.")
|
||||||
|
|
||||||
self.disable_calibration = self.settings.get('disable_calibration', False)
|
self.disable_calibration = self.settings.get('disable_calibration', False)
|
||||||
|
if self.disable_calibration:
|
||||||
|
logger.info("Calibration disabled. Please proceed with caution to prevent ghosting.")
|
||||||
|
|
||||||
if not os.path.exists(image_folder):
|
if not os.path.exists(settings.IMAGE_FOLDER):
|
||||||
os.mkdir(image_folder)
|
os.mkdir(settings.IMAGE_FOLDER)
|
||||||
|
|
||||||
|
if not os.path.exists(settings.CACHE_PATH):
|
||||||
|
os.mkdir(settings.CACHE_PATH)
|
||||||
|
|
||||||
# Option to use epaper image optimisation, reduces colours
|
# Option to use epaper image optimisation, reduces colours
|
||||||
self.optimize = True
|
self.optimize = True
|
||||||
@ -109,10 +105,10 @@ class Inkycal:
|
|||||||
if self.render:
|
if self.render:
|
||||||
# Init Display class with model in settings file
|
# Init Display class with model in settings file
|
||||||
# from inkycal.display import Display
|
# from inkycal.display import Display
|
||||||
self.Display = Display(settings["model"])
|
self.Display = Display(self.settings["model"])
|
||||||
|
|
||||||
# check if colours can be rendered
|
# check if colours can be rendered
|
||||||
self.supports_colour = True if 'colour' in settings['model'] else False
|
self.supports_colour = True if 'colour' in self.settings['model'] else False
|
||||||
|
|
||||||
# get calibration hours
|
# get calibration hours
|
||||||
self._calibration_hours = self.settings['calibration_hours']
|
self._calibration_hours = self.settings['calibration_hours']
|
||||||
@ -122,7 +118,7 @@ class Inkycal:
|
|||||||
|
|
||||||
# Load and initialise modules specified in the settings file
|
# Load and initialise modules specified in the settings file
|
||||||
self._module_number = 1
|
self._module_number = 1
|
||||||
for module in settings['modules']:
|
for module in self.settings['modules']:
|
||||||
module_name = module['name']
|
module_name = module['name']
|
||||||
try:
|
try:
|
||||||
loader = f'from inkycal.modules import {module_name}'
|
loader = f'from inkycal.modules import {module_name}'
|
||||||
@ -131,10 +127,9 @@ class Inkycal:
|
|||||||
setup = f'self.module_{self._module_number} = {module_name}({module})'
|
setup = f'self.module_{self._module_number} = {module_name}({module})'
|
||||||
# print(setup)
|
# print(setup)
|
||||||
exec(setup)
|
exec(setup)
|
||||||
logger.info(('name : {name} size : {width}x{height} px'.format(
|
width = module['config']['size'][0]
|
||||||
name=module_name,
|
height = module['config']['size'][1]
|
||||||
width=module['config']['size'][0],
|
logger.info(f'name : {module_name} size : {width}x{height} px')
|
||||||
height=module['config']['size'][1])))
|
|
||||||
|
|
||||||
self._module_number += 1
|
self._module_number += 1
|
||||||
|
|
||||||
@ -146,58 +141,85 @@ class Inkycal:
|
|||||||
except:
|
except:
|
||||||
logger.exception(f"Exception: {traceback.format_exc()}.")
|
logger.exception(f"Exception: {traceback.format_exc()}.")
|
||||||
|
|
||||||
# Path to store images
|
|
||||||
self.image_folder = image_folder
|
|
||||||
|
|
||||||
# Remove old hashes
|
# Remove old hashes
|
||||||
self._remove_hashes(self.image_folder)
|
self._remove_hashes(settings.IMAGE_FOLDER)
|
||||||
|
|
||||||
|
# set up cache
|
||||||
|
if not os.path.exists(os.path.join(settings.CACHE_PATH, CACHE_NAME)):
|
||||||
|
if not os.path.exists(settings.CACHE_PATH):
|
||||||
|
os.mkdir(settings.CACHE_PATH)
|
||||||
|
self.cache = JSONCache(CACHE_NAME)
|
||||||
|
self.cache_data = self.cache.read()
|
||||||
|
|
||||||
|
self.counter = 0 if "counter" not in self.cache_data else int(self.cache_data["counter"])
|
||||||
|
|
||||||
|
self.use_pi_sugar = use_pi_sugar
|
||||||
|
self.battery_capacity = 100
|
||||||
|
self.shutdown_after_run = use_pi_sugar and shutdown_after_run
|
||||||
|
|
||||||
|
if self.use_pi_sugar:
|
||||||
|
logger.info("PiSugar support enabled.")
|
||||||
|
from inkycal.utils import PiSugar
|
||||||
|
self.pisugar = PiSugar()
|
||||||
|
|
||||||
|
self.battery_capacity = self.pisugar.get_battery()
|
||||||
|
logger.info(f"PiSugar battery capacity: {self.battery_capacity}%")
|
||||||
|
|
||||||
|
if self.battery_capacity < 20:
|
||||||
|
logger.warning("Battery capacity is below 20%!")
|
||||||
|
|
||||||
|
logger.info("Setting system time to PiSugar time...")
|
||||||
|
if self.pisugar.rtc_pi2rtc():
|
||||||
|
logger.info("RTC time updates successfully")
|
||||||
|
else:
|
||||||
|
logger.warning("RTC time could not be set!")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Using PiSigar model: {self.pisugar.get_model()}. Current PiSugar time: {self.pisugar.get_rtc_time()}")
|
||||||
|
|
||||||
|
if self.shutdown_after_run:
|
||||||
|
logger.warning("Shutdown after run enabled. System will shutdown after the run is complete.")
|
||||||
|
|
||||||
# Give an OK message
|
# Give an OK message
|
||||||
print('loaded inkycal')
|
logger.info('Inkycal initialised successfully!')
|
||||||
|
|
||||||
def countdown(self, interval_mins: int or None = None) -> int:
|
def countdown(self, interval_mins: int = None) -> int:
|
||||||
"""Returns the remaining time in seconds until next display update.
|
"""Returns the remaining time in seconds until the next display update based on the interval.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
- interval_mins = int -> the interval in minutes for the update
|
interval_mins (int): The interval in minutes for the update. If none is given, the value
|
||||||
if no interval is given, the value from the settings file is used.
|
from the settings file is used.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- int -> the remaining time in seconds until next update
|
int: The remaining time in seconds until the next update.
|
||||||
"""
|
"""
|
||||||
|
# Default to settings if no interval is provided
|
||||||
# Check if empty, if empty, use value from settings file
|
|
||||||
if interval_mins is None:
|
if interval_mins is None:
|
||||||
interval_mins = self.settings["update_interval"]
|
interval_mins = self.settings["update_interval"]
|
||||||
|
|
||||||
# Find out at which minutes the update should happen
|
# Get the current time
|
||||||
now = arrow.now()
|
now = arrow.now()
|
||||||
if interval_mins <= 60:
|
|
||||||
update_timings = [(60 - interval_mins * updates) for updates in range(60 // interval_mins)][::-1]
|
|
||||||
|
|
||||||
# Calculate time in minutes until next update
|
# Calculate the next update time
|
||||||
minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute
|
# Finding the total minutes from the start of the day
|
||||||
|
minutes_since_midnight = now.hour * 60 + now.minute
|
||||||
|
|
||||||
# Print the remaining time in minutes until next update
|
# Finding the next interval point
|
||||||
print(f'{minutes} minutes left until next refresh')
|
minutes_to_next_interval = (
|
||||||
|
minutes_since_midnight // interval_mins + 1) * interval_mins - minutes_since_midnight
|
||||||
|
seconds_to_next_interval = minutes_to_next_interval * 60 - now.second
|
||||||
|
|
||||||
# Calculate time in seconds until next update
|
# Logging the remaining time in appropriate units
|
||||||
remaining_time = minutes * 60 + (60 - now.second)
|
hours_to_next_interval = minutes_to_next_interval // 60
|
||||||
|
remaining_minutes = minutes_to_next_interval % 60
|
||||||
# Return seconds until next update
|
if hours_to_next_interval > 0:
|
||||||
return remaining_time
|
print(f'{hours_to_next_interval} hours and {remaining_minutes} minutes left until next refresh')
|
||||||
else:
|
else:
|
||||||
# Calculate time in minutes until next update using the range of 24 hours in steps of every full hour
|
print(f'{remaining_minutes} minutes left until next refresh')
|
||||||
update_timings = [(60 * 24 - interval_mins * updates) for updates in range(60 * 24 // interval_mins)][::-1]
|
|
||||||
minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute
|
|
||||||
remaining_time = minutes * 60 + (60 - now.second)
|
|
||||||
|
|
||||||
print(f'{round(minutes / 60, 1)} hours left until next refresh')
|
return seconds_to_next_interval
|
||||||
|
|
||||||
# Return seconds until next update
|
def dry_run(self):
|
||||||
return remaining_time
|
|
||||||
|
|
||||||
def test(self):
|
|
||||||
"""Tests if Inkycal can run without issues.
|
"""Tests if Inkycal can run without issues.
|
||||||
|
|
||||||
Attempts to import module names from settings file. Loads the config
|
Attempts to import module names from settings file. Loads the config
|
||||||
@ -206,8 +228,6 @@ class Inkycal:
|
|||||||
|
|
||||||
Generated images can be found in the /images folder of Inkycal.
|
Generated images can be found in the /images folder of Inkycal.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.info(f"Inkycal version: v{self._release}")
|
|
||||||
logger.info(f'Selected E-paper display: {self.settings["model"]}')
|
logger.info(f'Selected E-paper display: {self.settings["model"]}')
|
||||||
|
|
||||||
# store module numbers in here
|
# store module numbers in here
|
||||||
@ -218,20 +238,13 @@ class Inkycal:
|
|||||||
|
|
||||||
for number in range(1, self._module_number):
|
for number in range(1, self._module_number):
|
||||||
name = eval(f"self.module_{number}.name")
|
name = eval(f"self.module_{number}.name")
|
||||||
module = eval(f'self.module_{number}')
|
success = self.process_module(number)
|
||||||
print(f'generating image(s) for {name}...', end="")
|
if success:
|
||||||
try:
|
logger.debug(f'Image of module {name} generated successfully')
|
||||||
black, colour = module.generate_image()
|
else:
|
||||||
if self.show_border:
|
logger.warning(f'Generating image of module {name} failed!')
|
||||||
draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5)
|
|
||||||
black.save(f"{self.image_folder}module{number}_black.png", "PNG")
|
|
||||||
colour.save(f"{self.image_folder}module{number}_colour.png", "PNG")
|
|
||||||
print("OK!")
|
|
||||||
except Exception:
|
|
||||||
errors.append(number)
|
errors.append(number)
|
||||||
self.info += f"module {number}: Error! "
|
self.info += f"module {number}: Error! "
|
||||||
logger.exception("Error!")
|
|
||||||
logger.exception(f"Exception: {traceback.format_exc()}.")
|
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
logger.error('Error/s in modules:', *errors)
|
logger.error('Error/s in modules:', *errors)
|
||||||
@ -277,98 +290,89 @@ class Inkycal:
|
|||||||
print("Refresh needed: {a}".format(a=res))
|
print("Refresh needed: {a}".format(a=res))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def run(self):
|
async def run(self, run_once=False):
|
||||||
"""Runs main program in nonstop mode.
|
"""Runs main program in nonstop mode or a single iteration based on the run_once flag.
|
||||||
|
|
||||||
Uses an infinity loop to run Inkycal nonstop. Inkycal generates the image
|
Args:
|
||||||
from all modules, assembles them in one image, refreshed the E-Paper and
|
run_once (bool): If True, runs the updating process once and stops. If False,
|
||||||
then sleeps until the next scheduled update.
|
runs indefinitely.
|
||||||
|
|
||||||
|
Uses an infinity loop to run Inkycal nonstop or a single time based on run_once.
|
||||||
|
Inkycal generates the image from all modules, assembles them in one image,
|
||||||
|
refreshes the E-Paper and then sleeps until the next scheduled update or exits.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Get the time of initial run
|
# Get the time of initial run
|
||||||
runtime = arrow.now()
|
runtime = arrow.now()
|
||||||
|
|
||||||
# Function to flip images upside down
|
# Function to flip images upside down
|
||||||
upside_down = lambda image: image.rotate(180, expand=True)
|
upside_down = lambda image: image.rotate(180, expand=True)
|
||||||
|
|
||||||
# Count the number of times without any errors
|
logger.info(f'Inkycal version: v{self._release}')
|
||||||
counter = 0
|
logger.info(f'Selected E-paper display: {self.settings["model"]}')
|
||||||
|
|
||||||
print(f'Inkycal version: v{self._release}')
|
|
||||||
print(f'Selected E-paper display: {self.settings["model"]}')
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
logger.info("Starting new cycle...")
|
||||||
current_time = arrow.now(tz=get_system_tz())
|
current_time = arrow.now(tz=get_system_tz())
|
||||||
print(f"Date: {current_time.format('D MMM YY')} | "
|
logger.info(f"Timestamp: {current_time.format('HH:mm:ss DD.MM.YYYY')}")
|
||||||
f"Time: {current_time.format('HH:mm')}")
|
self.cache_data["counter"] = self.counter
|
||||||
print('Generating images for all modules...', end='')
|
|
||||||
|
|
||||||
errors = [] # store module numbers in here
|
errors = [] # Store module numbers in here
|
||||||
|
|
||||||
# short info for info-section
|
# Short info for info-section
|
||||||
if not self.settings.get('image_hash', False):
|
if not self.settings.get('image_hash', False):
|
||||||
self.info = f"{current_time.format('D MMM @ HH:mm')} "
|
self.info = f"{current_time.format('D MMM @ HH:mm')} "
|
||||||
else:
|
else:
|
||||||
self.info = ""
|
self.info = ""
|
||||||
|
|
||||||
for number in range(1, self._module_number):
|
for number in range(1, self._module_number):
|
||||||
|
success = self.process_module(number)
|
||||||
# name = eval(f"self.module_{number}.name")
|
if not success:
|
||||||
module = eval(f'self.module_{number}')
|
|
||||||
|
|
||||||
try:
|
|
||||||
black, colour = module.generate_image()
|
|
||||||
if self.show_border:
|
|
||||||
draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5)
|
|
||||||
black.save(f"{self.image_folder}module{number}_black.png", "PNG")
|
|
||||||
colour.save(f"{self.image_folder}module{number}_colour.png", "PNG")
|
|
||||||
self.info += f"module {number}: OK "
|
|
||||||
except Exception as e:
|
|
||||||
errors.append(number)
|
errors.append(number)
|
||||||
self.info += f"module {number}: Error! "
|
self.info += f"im {number}: X "
|
||||||
logger.exception("Error!")
|
|
||||||
logger.exception(f"Exception: {traceback.format_exc()}.")
|
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
logger.error("Error/s in modules:", *errors)
|
logger.error("Error/s in modules:", *errors)
|
||||||
counter = 0
|
self.counter = 0
|
||||||
|
self.cache_data["counter"] = 0
|
||||||
else:
|
else:
|
||||||
counter += 1
|
self.counter += 1
|
||||||
logger.info("successful")
|
self.cache_data["counter"] += 1
|
||||||
|
logger.info("All images generated successfully!")
|
||||||
del errors
|
del errors
|
||||||
|
|
||||||
|
if self.battery_capacity < 20:
|
||||||
|
self.info += "Low battery! "
|
||||||
|
|
||||||
# Assemble image from each module - add info section if specified
|
# Assemble image from each module - add info section if specified
|
||||||
self._assemble()
|
self._assemble()
|
||||||
|
|
||||||
# Check if image should be rendered
|
# Check if image should be rendered
|
||||||
if self.render:
|
if self.render:
|
||||||
|
logger.info("Attempting to render image on display...")
|
||||||
display = self.Display
|
display = self.Display
|
||||||
|
|
||||||
self._calibration_check()
|
self._calibration_check()
|
||||||
if self._calibration_state:
|
if self._calibration_state:
|
||||||
# after calibration, we have to forcefully rewrite the screen
|
# After calibration, we have to forcefully rewrite the screen
|
||||||
self._remove_hashes(self.image_folder)
|
self._remove_hashes(settings.IMAGE_FOLDER)
|
||||||
|
|
||||||
if self.supports_colour:
|
if self.supports_colour:
|
||||||
im_black = Image.open(f"{self.image_folder}canvas.png")
|
im_black = Image.open(os.path.join(settings.IMAGE_FOLDER, "canvas.png"))
|
||||||
im_colour = Image.open(f"{self.image_folder}canvas_colour.png")
|
im_colour = Image.open(os.path.join(settings.IMAGE_FOLDER, "canvas_colour.png"))
|
||||||
|
|
||||||
# Flip the image by 180° if required
|
# Flip the image by 180° if required
|
||||||
if self.settings['orientation'] == 180:
|
if self.settings['orientation'] == 180:
|
||||||
im_black = upside_down(im_black)
|
im_black = upside_down(im_black)
|
||||||
im_colour = upside_down(im_colour)
|
im_colour = upside_down(im_colour)
|
||||||
|
|
||||||
# render the image on the display
|
# Render the image on the display
|
||||||
if not self.settings.get('image_hash', False) or self._needs_image_update([
|
if not self.settings.get('image_hash', False) or self._needs_image_update([
|
||||||
(f"{self.image_folder}/canvas.png.hash", im_black),
|
(f"{settings.IMAGE_FOLDER}/canvas.png.hash", im_black),
|
||||||
(f"{self.image_folder}/canvas_colour.png.hash", im_colour)
|
(f"{settings.IMAGE_FOLDER}/canvas_colour.png.hash", im_colour)
|
||||||
]):
|
]):
|
||||||
# render the image on the display
|
|
||||||
display.render(im_black, im_colour)
|
display.render(im_black, im_colour)
|
||||||
|
|
||||||
# Part for black-white ePapers
|
# Part for black-white ePapers
|
||||||
elif not self.supports_colour:
|
else:
|
||||||
|
|
||||||
im_black = self._merge_bands()
|
im_black = self._merge_bands()
|
||||||
|
|
||||||
# Flip the image by 180° if required
|
# Flip the image by 180° if required
|
||||||
@ -376,14 +380,34 @@ class Inkycal:
|
|||||||
im_black = upside_down(im_black)
|
im_black = upside_down(im_black)
|
||||||
|
|
||||||
if not self.settings.get('image_hash', False) or self._needs_image_update([
|
if not self.settings.get('image_hash', False) or self._needs_image_update([
|
||||||
(f"{self.image_folder}/canvas.png.hash", im_black),
|
(f"{settings.IMAGE_FOLDER}/canvas.png.hash", im_black), ]):
|
||||||
]):
|
|
||||||
display.render(im_black)
|
display.render(im_black)
|
||||||
|
|
||||||
print(f'\nNo errors since {counter} display updates \n'
|
logger.info(f'No errors since {self.counter} display updates')
|
||||||
f'program started {runtime.humanize()}')
|
logger.info(f'program started {runtime.humanize()}')
|
||||||
|
|
||||||
|
# store the cache data
|
||||||
|
self.cache.write(self.cache_data)
|
||||||
|
|
||||||
|
# Exit the loop if run_once is True
|
||||||
|
if run_once:
|
||||||
|
break # Exit the loop after one full cycle if run_once is True
|
||||||
|
|
||||||
sleep_time = self.countdown()
|
sleep_time = self.countdown()
|
||||||
|
|
||||||
|
if self.use_pi_sugar:
|
||||||
|
sleep_time_rtc = arrow.now(tz=get_system_tz()).shift(seconds=sleep_time)
|
||||||
|
result = self.pisugar.rtc_alarm_set(sleep_time_rtc, 127)
|
||||||
|
if result:
|
||||||
|
logger.info(f"Alarm set for {sleep_time_rtc.format('HH:mm:ss')}")
|
||||||
|
if self.shutdown_after_run:
|
||||||
|
logger.warning("System shutdown in 5 seconds!")
|
||||||
|
time.sleep(5)
|
||||||
|
self._shutdown_system()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.warning(f"Failed to set alarm for {sleep_time_rtc.format('HH:mm:ss')}")
|
||||||
|
|
||||||
await asyncio.sleep(sleep_time)
|
await asyncio.sleep(sleep_time)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -392,7 +416,8 @@ class Inkycal:
|
|||||||
returns the merged image
|
returns the merged image
|
||||||
"""
|
"""
|
||||||
|
|
||||||
im1_path, im2_path = image_folder + 'canvas.png', image_folder + 'canvas_colour.png'
|
im1_path = os.path.join(settings.IMAGE_FOLDER, "canvas.png")
|
||||||
|
im2_path = os.path.join(settings.IMAGE_FOLDER, "canvas_colour.png")
|
||||||
|
|
||||||
# If there is an image for black and colour, merge them
|
# If there is an image for black and colour, merge them
|
||||||
if os.path.exists(im1_path) and os.path.exists(im2_path):
|
if os.path.exists(im1_path) and os.path.exists(im2_path):
|
||||||
@ -430,8 +455,8 @@ class Inkycal:
|
|||||||
for number in range(1, self._module_number):
|
for number in range(1, self._module_number):
|
||||||
|
|
||||||
# get the path of the current module's generated images
|
# get the path of the current module's generated images
|
||||||
im1_path = f"{self.image_folder}module{number}_black.png"
|
im1_path = os.path.join(settings.IMAGE_FOLDER, f"module{number}_black.png")
|
||||||
im2_path = f"{self.image_folder}module{number}_colour.png"
|
im2_path = os.path.join(settings.IMAGE_FOLDER, f"module{number}_colour.png")
|
||||||
|
|
||||||
# Check if there is an image for the black band
|
# Check if there is an image for the black band
|
||||||
if os.path.exists(im1_path):
|
if os.path.exists(im1_path):
|
||||||
@ -501,8 +526,8 @@ class Inkycal:
|
|||||||
im_black = self._optimize_im(im_black)
|
im_black = self._optimize_im(im_black)
|
||||||
im_colour = self._optimize_im(im_colour)
|
im_colour = self._optimize_im(im_colour)
|
||||||
|
|
||||||
im_black.save(self.image_folder + 'canvas.png', 'PNG')
|
im_black.save(os.path.join(settings.IMAGE_FOLDER, "canvas.png"), "PNG")
|
||||||
im_colour.save(self.image_folder + 'canvas_colour.png', 'PNG')
|
im_colour.save(os.path.join(settings.IMAGE_FOLDER, "canvas_colour.png"), 'PNG')
|
||||||
|
|
||||||
# Additionally, combine the two images with color
|
# Additionally, combine the two images with color
|
||||||
def clear_white(img):
|
def clear_white(img):
|
||||||
@ -531,7 +556,7 @@ class Inkycal:
|
|||||||
im_colour = black_to_colour(im_colour)
|
im_colour = black_to_colour(im_colour)
|
||||||
|
|
||||||
im_colour.paste(im_black, (0, 0), im_black)
|
im_colour.paste(im_black, (0, 0), im_black)
|
||||||
im_colour.save(image_folder + 'full-screen.png', 'PNG')
|
im_colour.save(os.path.join(settings.IMAGE_FOLDER, 'full-screen.png'), 'PNG')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _optimize_im(image, threshold=220):
|
def _optimize_im(image, threshold=220):
|
||||||
@ -574,13 +599,40 @@ class Inkycal:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def cleanup():
|
def cleanup():
|
||||||
# clean up old images in image_folder
|
# clean up old images in image_folder
|
||||||
for _file in glob.glob(f"{image_folder}*.png"):
|
if len(glob.glob(settings.IMAGE_FOLDER)) <= 1:
|
||||||
|
return
|
||||||
|
for _file in glob.glob(settings.IMAGE_FOLDER):
|
||||||
try:
|
try:
|
||||||
os.remove(_file)
|
os.remove(_file)
|
||||||
except:
|
except:
|
||||||
logger.error(f"could not remove file: {_file}")
|
logger.error(f"could not remove file: {_file}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def process_module(self, number) -> bool or Exception:
|
||||||
|
"""Process individual module to generate images and handle exceptions."""
|
||||||
|
module = eval(f'self.module_{number}')
|
||||||
|
try:
|
||||||
|
black, colour = module.generate_image()
|
||||||
|
if self.show_border:
|
||||||
|
draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5)
|
||||||
|
black.save(os.path.join(settings.IMAGE_FOLDER, f"module{number}_black.png"), "PNG")
|
||||||
|
colour.save(os.path.join(settings.IMAGE_FOLDER, f"module{number}_colour.png"), "PNG")
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f"Error in module {number}!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _shutdown_system(self):
|
||||||
|
"""Shutdown the system"""
|
||||||
|
import subprocess
|
||||||
|
from time import sleep
|
||||||
|
try:
|
||||||
|
logger.info("Shutting down OS in 5 seconds...")
|
||||||
|
sleep(5)
|
||||||
|
subprocess.run(["sudo", "shutdown", "-h", "now"], check=True)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
logger.warning("Failed to execute shutdown command.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print(f'running inkycal main in standalone/debug mode')
|
print(f'running inkycal main in standalone/debug mode')
|
||||||
|
@ -156,7 +156,7 @@ class Simple(inkycal_module):
|
|||||||
# -----------------------------------------------------------------------#
|
# -----------------------------------------------------------------------#
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
# Validation of module specific parameters (optional) #
|
# Validation of module specific parameters (optional) #
|
||||||
|
@ -27,7 +27,7 @@ class Inkyimage:
|
|||||||
self.image = image
|
self.image = image
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
logger.info(f"{__name__} loaded")
|
logger.debug(f"{__name__} loaded")
|
||||||
|
|
||||||
def load(self, path: str) -> None:
|
def load(self, path: str) -> None:
|
||||||
"""loads an image from a URL or filepath.
|
"""loads an image from a URL or filepath.
|
||||||
@ -59,7 +59,7 @@ class Inkyimage:
|
|||||||
logger.error("Invalid Image file provided", exc_info=True)
|
logger.error("Invalid Image file provided", exc_info=True)
|
||||||
raise Exception("Please check if the path points to an image file.")
|
raise Exception("Please check if the path points to an image file.")
|
||||||
|
|
||||||
logger.info(f"width: {image.width}, height: {image.height}")
|
logger.debug(f"width: {image.width}, height: {image.height}")
|
||||||
|
|
||||||
image.convert(mode="RGBA") # convert to a more suitable format
|
image.convert(mode="RGBA") # convert to a more suitable format
|
||||||
self.image = image
|
self.image = image
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
Inkycal Agenda Module
|
Inkycal Agenda Module
|
||||||
Copyright by aceinnolab
|
Copyright by aceinnolab
|
||||||
"""
|
"""
|
||||||
|
import arrow # noqa
|
||||||
import arrow
|
|
||||||
|
|
||||||
from inkycal.custom import *
|
from inkycal.custom import *
|
||||||
from inkycal.modules.ical_parser import iCalendar
|
from inkycal.modules.ical_parser import iCalendar
|
||||||
from inkycal.modules.template import inkycal_module
|
from inkycal.modules.template import inkycal_module
|
||||||
@ -77,8 +75,10 @@ class Agenda(inkycal_module):
|
|||||||
# Additional config
|
# Additional config
|
||||||
self.timezone = get_system_tz()
|
self.timezone = get_system_tz()
|
||||||
|
|
||||||
|
self.icon_font = ImageFont.truetype(fonts['MaterialIcons'], size=self.fontsize)
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -88,7 +88,7 @@ class Agenda(inkycal_module):
|
|||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
|
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -203,10 +203,10 @@ class Agenda(inkycal_module):
|
|||||||
write(im_black, (x_time, line_pos[cursor][1]),
|
write(im_black, (x_time, line_pos[cursor][1]),
|
||||||
(time_width, line_height), time,
|
(time_width, line_height), time,
|
||||||
font=self.font, alignment='right')
|
font=self.font, alignment='right')
|
||||||
if parser.all_day(_):
|
else:
|
||||||
write(im_black, (x_time, line_pos[cursor][1]),
|
write(im_black, (x_time, line_pos[cursor][1]),
|
||||||
(time_width, line_height), "all day",
|
(time_width, line_height), "\ue878",
|
||||||
font=self.font, alignment='right')
|
font=self.icon_font, alignment='right')
|
||||||
|
|
||||||
write(im_black, (x_event, line_pos[cursor][1]),
|
write(im_black, (x_event, line_pos[cursor][1]),
|
||||||
(event_width, line_height),
|
(event_width, line_height),
|
||||||
|
@ -6,16 +6,16 @@ Copyright by aceinnolab
|
|||||||
# pylint: disable=logging-fstring-interpolation
|
# pylint: disable=logging-fstring-interpolation
|
||||||
|
|
||||||
import calendar as cal
|
import calendar as cal
|
||||||
import arrow
|
|
||||||
from inkycal.modules.template import inkycal_module
|
|
||||||
from inkycal.custom import *
|
from inkycal.custom import *
|
||||||
|
from inkycal.modules.template import inkycal_module
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Calendar(inkycal_module):
|
class Calendar(inkycal_module):
|
||||||
"""Calendar class
|
"""Calendar class
|
||||||
Create monthly calendar and show events from given icalendars
|
Create monthly calendar and show events from given iCalendars
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Calendar - Show monthly calendar with events from iCalendars"
|
name = "Calendar - Show monthly calendar with events from iCalendars"
|
||||||
@ -39,12 +39,12 @@ class Calendar(inkycal_module):
|
|||||||
},
|
},
|
||||||
"date_format": {
|
"date_format": {
|
||||||
"label": "Use an arrow-supported token for custom date formatting "
|
"label": "Use an arrow-supported token for custom date formatting "
|
||||||
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM",
|
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM",
|
||||||
"default": "D MMM",
|
"default": "D MMM",
|
||||||
},
|
},
|
||||||
"time_format": {
|
"time_format": {
|
||||||
"label": "Use an arrow-supported token for custom time formatting "
|
"label": "Use an arrow-supported token for custom time formatting "
|
||||||
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
|
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
|
||||||
"default": "HH:mm",
|
"default": "HH:mm",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -61,7 +61,7 @@ class Calendar(inkycal_module):
|
|||||||
self._days_with_events = None
|
self._days_with_events = None
|
||||||
|
|
||||||
# optional parameters
|
# optional parameters
|
||||||
self.weekstart = config['week_starts_on']
|
self.week_start = config['week_starts_on']
|
||||||
self.show_events = config['show_events']
|
self.show_events = config['show_events']
|
||||||
self.date_format = config["date_format"]
|
self.date_format = config["date_format"]
|
||||||
self.time_format = config['time_format']
|
self.time_format = config['time_format']
|
||||||
@ -84,7 +84,7 @@ class Calendar(inkycal_module):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def flatten(values):
|
def flatten(values):
|
||||||
@ -100,7 +100,7 @@ class Calendar(inkycal_module):
|
|||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
events_height = 0
|
events_height = 0
|
||||||
|
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -109,7 +109,7 @@ class Calendar(inkycal_module):
|
|||||||
# Allocate space for month-names, weekdays etc.
|
# Allocate space for month-names, weekdays etc.
|
||||||
month_name_height = int(im_height * 0.10)
|
month_name_height = int(im_height * 0.10)
|
||||||
text_bbox_height = self.font.getbbox("hg")
|
text_bbox_height = self.font.getbbox("hg")
|
||||||
weekdays_height = int((text_bbox_height[3] - text_bbox_height[1])* 1.25)
|
weekdays_height = int((abs(text_bbox_height[3]) + abs(text_bbox_height[1])) * 1.25)
|
||||||
logger.debug(f"month_name_height: {month_name_height}")
|
logger.debug(f"month_name_height: {month_name_height}")
|
||||||
logger.debug(f"weekdays_height: {weekdays_height}")
|
logger.debug(f"weekdays_height: {weekdays_height}")
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ class Calendar(inkycal_module):
|
|||||||
logger.debug("Allocating space for events")
|
logger.debug("Allocating space for events")
|
||||||
calendar_height = int(im_height * 0.6)
|
calendar_height = int(im_height * 0.6)
|
||||||
events_height = (
|
events_height = (
|
||||||
im_height - month_name_height - weekdays_height - calendar_height
|
im_height - month_name_height - weekdays_height - calendar_height
|
||||||
)
|
)
|
||||||
logger.debug(f'calendar-section size: {im_width} x {calendar_height} px')
|
logger.debug(f'calendar-section size: {im_width} x {calendar_height} px')
|
||||||
logger.debug(f'events-section size: {im_width} x {events_height} px')
|
logger.debug(f'events-section size: {im_width} x {events_height} px')
|
||||||
@ -156,13 +156,13 @@ class Calendar(inkycal_module):
|
|||||||
|
|
||||||
now = arrow.now(tz=self.timezone)
|
now = arrow.now(tz=self.timezone)
|
||||||
|
|
||||||
# Set weekstart of calendar to specified weekstart
|
# Set week-start of calendar to specified week-start
|
||||||
if self.weekstart == "Monday":
|
if self.week_start == "Monday":
|
||||||
cal.setfirstweekday(cal.MONDAY)
|
cal.setfirstweekday(cal.MONDAY)
|
||||||
weekstart = now.shift(days=-now.weekday())
|
week_start = now.shift(days=-now.weekday())
|
||||||
else:
|
else:
|
||||||
cal.setfirstweekday(cal.SUNDAY)
|
cal.setfirstweekday(cal.SUNDAY)
|
||||||
weekstart = now.shift(days=-now.isoweekday())
|
week_start = now.shift(days=-now.isoweekday())
|
||||||
|
|
||||||
# Write the name of current month
|
# Write the name of current month
|
||||||
write(
|
write(
|
||||||
@ -174,9 +174,9 @@ class Calendar(inkycal_module):
|
|||||||
autofit=True,
|
autofit=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set up weeknames in local language and add to main section
|
# Set up week-names in local language and add to main section
|
||||||
weekday_names = [
|
weekday_names = [
|
||||||
weekstart.shift(days=+_).format('ddd', locale=self.language)
|
week_start.shift(days=+_).format('ddd', locale=self.language)
|
||||||
for _ in range(7)
|
for _ in range(7)
|
||||||
]
|
]
|
||||||
logger.debug(f'weekday names: {weekday_names}')
|
logger.debug(f'weekday names: {weekday_names}')
|
||||||
@ -192,7 +192,7 @@ class Calendar(inkycal_module):
|
|||||||
fill_height=0.9,
|
fill_height=0.9,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a calendar template and flatten (remove nestings)
|
# Create a calendar template and flatten (remove nesting)
|
||||||
calendar_flat = self.flatten(cal.monthcalendar(now.year, now.month))
|
calendar_flat = self.flatten(cal.monthcalendar(now.year, now.month))
|
||||||
# logger.debug(f" calendar_flat: {calendar_flat}")
|
# logger.debug(f" calendar_flat: {calendar_flat}")
|
||||||
|
|
||||||
@ -265,7 +265,7 @@ class Calendar(inkycal_module):
|
|||||||
# find out how many lines can fit at max in the event section
|
# find out how many lines can fit at max in the event section
|
||||||
line_spacing = 2
|
line_spacing = 2
|
||||||
text_bbox_height = self.font.getbbox("hg")
|
text_bbox_height = self.font.getbbox("hg")
|
||||||
line_height = text_bbox_height[3] + line_spacing
|
line_height = text_bbox_height[3] - text_bbox_height[1] + line_spacing
|
||||||
max_event_lines = events_height // (line_height + line_spacing)
|
max_event_lines = events_height // (line_height + line_spacing)
|
||||||
|
|
||||||
# generate list of coordinates for each line
|
# generate list of coordinates for each line
|
||||||
@ -281,7 +281,7 @@ class Calendar(inkycal_module):
|
|||||||
month_start = arrow.get(now.floor('month'))
|
month_start = arrow.get(now.floor('month'))
|
||||||
month_end = arrow.get(now.ceil('month'))
|
month_end = arrow.get(now.ceil('month'))
|
||||||
|
|
||||||
# fetch events from given icalendars
|
# fetch events from given iCalendars
|
||||||
self.ical = iCalendar()
|
self.ical = iCalendar()
|
||||||
parser = self.ical
|
parser = self.ical
|
||||||
|
|
||||||
@ -294,14 +294,12 @@ class Calendar(inkycal_module):
|
|||||||
month_events = parser.get_events(month_start, month_end, self.timezone)
|
month_events = parser.get_events(month_start, month_end, self.timezone)
|
||||||
parser.sort()
|
parser.sort()
|
||||||
self.month_events = month_events
|
self.month_events = month_events
|
||||||
|
|
||||||
# Initialize days_with_events as an empty list
|
# Initialize days_with_events as an empty list
|
||||||
days_with_events = []
|
days_with_events = []
|
||||||
|
|
||||||
# Handle multi-day events by adding all days between start and end
|
# Handle multi-day events by adding all days between start and end
|
||||||
for event in month_events:
|
for event in month_events:
|
||||||
start_date = event['begin'].date()
|
|
||||||
end_date = event['end'].date()
|
|
||||||
|
|
||||||
# Convert start and end dates to arrow objects with timezone
|
# Convert start and end dates to arrow objects with timezone
|
||||||
start = arrow.get(event['begin'].date(), tzinfo=self.timezone)
|
start = arrow.get(event['begin'].date(), tzinfo=self.timezone)
|
||||||
@ -324,9 +322,7 @@ class Calendar(inkycal_module):
|
|||||||
im_colour,
|
im_colour,
|
||||||
grid[days],
|
grid[days],
|
||||||
(icon_width, icon_height),
|
(icon_width, icon_height),
|
||||||
radius=6,
|
radius=6
|
||||||
thickness=1,
|
|
||||||
shrinkage=(0.4, 0.2),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Filter upcoming events until 4 weeks in the future
|
# Filter upcoming events until 4 weeks in the future
|
||||||
@ -345,13 +341,13 @@ class Calendar(inkycal_module):
|
|||||||
|
|
||||||
date_width = int(max((
|
date_width = int(max((
|
||||||
self.font.getlength(events['begin'].format(self.date_format, locale=lang))
|
self.font.getlength(events['begin'].format(self.date_format, locale=lang))
|
||||||
for events in upcoming_events))* 1.1
|
for events in upcoming_events)) * 1.1
|
||||||
)
|
)
|
||||||
|
|
||||||
time_width = int(max((
|
time_width = int(max((
|
||||||
self.font.getlength(events['begin'].format(self.time_format, locale=lang))
|
self.font.getlength(events['begin'].format(self.time_format, locale=lang))
|
||||||
for events in upcoming_events))* 1.1
|
for events in upcoming_events)) * 1.1
|
||||||
)
|
)
|
||||||
|
|
||||||
text_bbox_height = self.font.getbbox("hg")
|
text_bbox_height = self.font.getbbox("hg")
|
||||||
line_height = text_bbox_height[3] + line_spacing
|
line_height = text_bbox_height[3] + line_spacing
|
||||||
@ -369,7 +365,8 @@ class Calendar(inkycal_module):
|
|||||||
event_duration = (event['end'] - event['begin']).days
|
event_duration = (event['end'] - event['begin']).days
|
||||||
if event_duration > 1:
|
if event_duration > 1:
|
||||||
# Format the duration using Arrow's localization
|
# Format the duration using Arrow's localization
|
||||||
days_translation = arrow.get().shift(days=event_duration).humanize(only_distance=True, locale=lang)
|
days_translation = arrow.get().shift(days=event_duration).humanize(only_distance=True,
|
||||||
|
locale=lang)
|
||||||
the_name = f"{event['title']} ({days_translation})"
|
the_name = f"{event['title']} ({days_translation})"
|
||||||
else:
|
else:
|
||||||
the_name = event['title']
|
the_name = event['title']
|
||||||
|
@ -60,7 +60,7 @@ class Feeds(inkycal_module):
|
|||||||
self.shuffle_feeds = config["shuffle_feeds"]
|
self.shuffle_feeds = config["shuffle_feeds"]
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def _validate(self):
|
def _validate(self):
|
||||||
"""Validate module-specific parameters"""
|
"""Validate module-specific parameters"""
|
||||||
@ -75,7 +75,7 @@ class Feeds(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -83,8 +83,9 @@ class Feeds(inkycal_module):
|
|||||||
|
|
||||||
# Check if internet is available
|
# Check if internet is available
|
||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.debug('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise NetworkNotReachableError
|
raise NetworkNotReachableError
|
||||||
|
|
||||||
# Set some parameters for formatting feeds
|
# Set some parameters for formatting feeds
|
||||||
|
@ -23,16 +23,18 @@ from icons.weather_icons.weather_icons import get_weather_icon
|
|||||||
from inkycal.custom.functions import fonts
|
from inkycal.custom.functions import fonts
|
||||||
from inkycal.custom.functions import get_system_tz
|
from inkycal.custom.functions import get_system_tz
|
||||||
from inkycal.custom.functions import internet_available
|
from inkycal.custom.functions import internet_available
|
||||||
from inkycal.custom.functions import top_level
|
|
||||||
from inkycal.custom.inkycal_exceptions import NetworkNotReachableError
|
from inkycal.custom.inkycal_exceptions import NetworkNotReachableError
|
||||||
from inkycal.custom.openweathermap_wrapper import OpenWeatherMap
|
from inkycal.custom.openweathermap_wrapper import OpenWeatherMap
|
||||||
from inkycal.modules.inky_image import image_to_palette
|
from inkycal.modules.inky_image import image_to_palette
|
||||||
from inkycal.modules.template import inkycal_module
|
from inkycal.modules.template import inkycal_module
|
||||||
|
from inkycal.settings import Settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
icons_dir = os.path.join(top_level, "icons", "ui-icons")
|
settings = Settings()
|
||||||
|
|
||||||
|
icons_dir = os.path.join(settings.FONT_PATH, "ui-icons")
|
||||||
|
|
||||||
|
|
||||||
def outline(image: Image, size: int, color: tuple) -> Image:
|
def outline(image: Image, size: int, color: tuple) -> Image:
|
||||||
@ -139,7 +141,7 @@ class Fullweather(inkycal_module):
|
|||||||
|
|
||||||
# Check if all required parameters are present
|
# Check if all required parameters are present
|
||||||
for param in self.requires:
|
for param in self.requires:
|
||||||
if not param in config:
|
if param not in config:
|
||||||
raise Exception(f"config is missing {param}")
|
raise Exception(f"config is missing {param}")
|
||||||
|
|
||||||
# required parameters
|
# required parameters
|
||||||
@ -237,7 +239,7 @@ class Fullweather(inkycal_module):
|
|||||||
self.left_section_width = int(self.width / 4)
|
self.left_section_width = int(self.width / 4)
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f"{__name__} loaded")
|
logger.debug(f"{__name__} loaded")
|
||||||
|
|
||||||
def createBaseImage(self):
|
def createBaseImage(self):
|
||||||
"""
|
"""
|
||||||
|
@ -50,7 +50,7 @@ class Inkyimage(inkycal_module):
|
|||||||
self.dither = False
|
self.dither = False
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f"{__name__} loaded")
|
logger.debug(f"{__name__} loaded")
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -71,7 +71,7 @@ class Inkyimage(inkycal_module):
|
|||||||
# Remove background if present
|
# Remove background if present
|
||||||
im.remove_alpha()
|
im.remove_alpha()
|
||||||
|
|
||||||
# if autoflip was enabled, flip the image
|
# if auto-flip was enabled, flip the image
|
||||||
if self.autoflip:
|
if self.autoflip:
|
||||||
im.autoflip(self.orientation)
|
im.autoflip(self.orientation)
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ class Jokes(inkycal_module):
|
|||||||
config = config['config']
|
config = config['config']
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -39,7 +39,7 @@ class Jokes(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'image size: {im_width} x {im_height} px')
|
logger.debug(f'image size: {im_width} x {im_height} px')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -47,8 +47,9 @@ class Jokes(inkycal_module):
|
|||||||
|
|
||||||
# Check if internet is available
|
# Check if internet is available
|
||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.debug('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise NetworkNotReachableError
|
raise NetworkNotReachableError
|
||||||
|
|
||||||
# Set some parameters for formatting feeds
|
# Set some parameters for formatting feeds
|
||||||
|
@ -67,7 +67,7 @@ class Inkyserver(inkycal_module):
|
|||||||
self.path_body = config['path_body']
|
self.path_body = config['path_body']
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
|
@ -8,13 +8,13 @@ from inkycal.custom import *
|
|||||||
# PIL has a class named Image, use alias for Inkyimage -> Images
|
# PIL has a class named Image, use alias for Inkyimage -> Images
|
||||||
from inkycal.modules.inky_image import Inkyimage as Images, image_to_palette
|
from inkycal.modules.inky_image import Inkyimage as Images, image_to_palette
|
||||||
from inkycal.modules.template import inkycal_module
|
from inkycal.modules.template import inkycal_module
|
||||||
|
from inkycal.utils import JSONCache
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Slideshow(inkycal_module):
|
class Slideshow(inkycal_module):
|
||||||
"""Cycles through images in a local image folder
|
"""Cycles through images in a local image folder"""
|
||||||
"""
|
|
||||||
name = "Slideshow - cycle through images from a local folder"
|
name = "Slideshow - cycle through images from a local folder"
|
||||||
|
|
||||||
requires = {
|
requires = {
|
||||||
@ -53,7 +53,7 @@ class Slideshow(inkycal_module):
|
|||||||
|
|
||||||
# required parameters
|
# required parameters
|
||||||
for param in self.requires:
|
for param in self.requires:
|
||||||
if not param in config:
|
if param not in config:
|
||||||
raise Exception(f'config is missing {param}')
|
raise Exception(f'config is missing {param}')
|
||||||
|
|
||||||
# optional parameters
|
# optional parameters
|
||||||
@ -64,19 +64,20 @@ class Slideshow(inkycal_module):
|
|||||||
|
|
||||||
# Get the full path of all png/jpg/jpeg images in the given folder
|
# Get the full path of all png/jpg/jpeg images in the given folder
|
||||||
all_files = glob.glob(f'{self.path}/*')
|
all_files = glob.glob(f'{self.path}/*')
|
||||||
self.images = [i for i in all_files
|
self.images = [i for i in all_files if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
|
||||||
if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
|
|
||||||
|
|
||||||
if not self.images:
|
if not self.images:
|
||||||
logger.error('No images found in the given folder, please '
|
logger.error('No images found in the given folder, please double check your path!')
|
||||||
'double check your path!')
|
|
||||||
raise Exception('No images found in the given folder path :/')
|
raise Exception('No images found in the given folder path :/')
|
||||||
|
|
||||||
|
self.cache = JSONCache('inkycal_slideshow')
|
||||||
|
self.cache_data = self.cache.read()
|
||||||
|
|
||||||
# set a 'first run' signal
|
# set a 'first run' signal
|
||||||
self._first_run = True
|
self._first_run = True
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -86,17 +87,19 @@ class Slideshow(inkycal_module):
|
|||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
|
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# rotates list items by 1 index
|
# rotates list items by 1 index
|
||||||
def rotate(somelist):
|
def rotate(list: list):
|
||||||
return somelist[1:] + somelist[:1]
|
return list[1:] + list[:1]
|
||||||
|
|
||||||
# Switch to the next image if this is not the first run
|
# Switch to the next image if this is not the first run
|
||||||
if self._first_run:
|
if self._first_run:
|
||||||
self._first_run = False
|
self._first_run = False
|
||||||
|
self.cache_data["current_index"] = 0
|
||||||
else:
|
else:
|
||||||
self.images = rotate(self.images)
|
self.images = rotate(self.images)
|
||||||
|
self.cache_data["current_index"] = (self.cache_data["current_index"] + 1) % len(self.images)
|
||||||
|
|
||||||
# initialize custom image class
|
# initialize custom image class
|
||||||
im = Images()
|
im = Images()
|
||||||
@ -110,7 +113,7 @@ class Slideshow(inkycal_module):
|
|||||||
# Remove background if present
|
# Remove background if present
|
||||||
im.remove_alpha()
|
im.remove_alpha()
|
||||||
|
|
||||||
# if autoflip was enabled, flip the image
|
# if auto-flip was enabled, flip the image
|
||||||
if self.autoflip:
|
if self.autoflip:
|
||||||
im.autoflip(self.orientation)
|
im.autoflip(self.orientation)
|
||||||
|
|
||||||
@ -123,6 +126,8 @@ class Slideshow(inkycal_module):
|
|||||||
# with the images now send, clear the current image
|
# with the images now send, clear the current image
|
||||||
im.clear()
|
im.clear()
|
||||||
|
|
||||||
|
self.cache.write(self.cache_data)
|
||||||
|
|
||||||
# return images
|
# return images
|
||||||
return im_black, im_colour
|
return im_black, im_colour
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class Stocks(inkycal_module):
|
|||||||
self.tickers = config['tickers']
|
self.tickers = config['tickers']
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -63,7 +63,7 @@ class Stocks(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'image size: {im_width} x {im_height} px')
|
logger.debug(f'image size: {im_width} x {im_height} px')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels (required)
|
# Create an image for black pixels and one for coloured pixels (required)
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -142,7 +142,7 @@ class Stocks(inkycal_module):
|
|||||||
logger.warning(f"Failed to get '{stockName}' ticker price hint! Using "
|
logger.warning(f"Failed to get '{stockName}' ticker price hint! Using "
|
||||||
"default precision of 2 instead.")
|
"default precision of 2 instead.")
|
||||||
|
|
||||||
stockHistory = yfTicker.history("30d")
|
stockHistory = yfTicker.history("1mo")
|
||||||
stockHistoryLen = len(stockHistory)
|
stockHistoryLen = len(stockHistory)
|
||||||
logger.info(f'fetched {stockHistoryLen} datapoints ...')
|
logger.info(f'fetched {stockHistoryLen} datapoints ...')
|
||||||
previousQuote = (stockHistory.tail(2)['Close'].iloc[0])
|
previousQuote = (stockHistory.tail(2)['Close'].iloc[0])
|
||||||
|
@ -31,7 +31,7 @@ class TextToDisplay(inkycal_module):
|
|||||||
self.make_request = True if self.filepath.startswith("https://") else False
|
self.make_request = True if self.filepath.startswith("https://") else False
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def _validate(self):
|
def _validate(self):
|
||||||
"""Validate module-specific parameters"""
|
"""Validate module-specific parameters"""
|
||||||
@ -45,7 +45,7 @@ class TextToDisplay(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
|
@ -32,7 +32,7 @@ class Tindie(inkycal_module):
|
|||||||
# self.mode = config['mode'] # unshipped_orders, shipped_orders, all_orders
|
# self.mode = config['mode'] # unshipped_orders, shipped_orders, all_orders
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -40,7 +40,7 @@ class Tindie(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'image size: {im_width} x {im_height} px')
|
logger.debug(f'image size: {im_width} x {im_height} px')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -50,6 +50,7 @@ class Tindie(inkycal_module):
|
|||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.info('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise NetworkNotReachableError
|
raise NetworkNotReachableError
|
||||||
|
|
||||||
# Set some parameters for formatting feeds
|
# Set some parameters for formatting feeds
|
||||||
|
@ -56,7 +56,7 @@ class Todoist(inkycal_module):
|
|||||||
self._api = TodoistAPI(config['api_key'])
|
self._api = TodoistAPI(config['api_key'])
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def _validate(self):
|
def _validate(self):
|
||||||
"""Validate module-specific parameters"""
|
"""Validate module-specific parameters"""
|
||||||
@ -70,7 +70,7 @@ class Todoist(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -80,6 +80,7 @@ class Todoist(inkycal_module):
|
|||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.info('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise NetworkNotReachableError
|
raise NetworkNotReachableError
|
||||||
|
|
||||||
# Set some parameters for formatting todos
|
# Set some parameters for formatting todos
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
Inkycal weather module
|
Inkycal weather module
|
||||||
Copyright by aceinnolab
|
Copyright by aceinnolab
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import arrow
|
|
||||||
import decimal
|
import decimal
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
import arrow
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from PIL import ImageDraw
|
from PIL import ImageDraw
|
||||||
from PIL import ImageFont
|
from PIL import ImageFont
|
||||||
@ -51,7 +51,7 @@ class Weather(inkycal_module):
|
|||||||
"options": [True, False],
|
"options": [True, False],
|
||||||
},
|
},
|
||||||
|
|
||||||
"round_windspeed": {
|
"round_wind_speed": {
|
||||||
"label": "Round windspeed?",
|
"label": "Round windspeed?",
|
||||||
"options": [True, False],
|
"options": [True, False],
|
||||||
},
|
},
|
||||||
@ -89,7 +89,7 @@ class Weather(inkycal_module):
|
|||||||
|
|
||||||
# Check if all required parameters are present
|
# Check if all required parameters are present
|
||||||
for param in self.requires:
|
for param in self.requires:
|
||||||
if not param in config:
|
if param not in config:
|
||||||
raise Exception(f'config is missing {param}')
|
raise Exception(f'config is missing {param}')
|
||||||
|
|
||||||
# required parameters
|
# required parameters
|
||||||
@ -98,15 +98,15 @@ class Weather(inkycal_module):
|
|||||||
|
|
||||||
# optional parameters
|
# optional parameters
|
||||||
self.round_temperature = config['round_temperature']
|
self.round_temperature = config['round_temperature']
|
||||||
self.round_windspeed = config['round_windspeed']
|
self.round_wind_speed = config['round_windspeed']
|
||||||
self.forecast_interval = config['forecast_interval']
|
self.forecast_interval = config['forecast_interval']
|
||||||
self.hour_format = int(config['hour_format'])
|
self.hour_format = int(config['hour_format'])
|
||||||
if config['units'] == "imperial":
|
if config['units'] == "imperial":
|
||||||
self.temp_unit = "fahrenheit"
|
self.temp_unit = "fahrenheit"
|
||||||
else:
|
else:
|
||||||
self.temp_unit = "celsius"
|
self.temp_unit = "celsius"
|
||||||
|
|
||||||
if config['use_beaufort'] == True:
|
if config['use_beaufort']:
|
||||||
self.wind_unit = "beaufort"
|
self.wind_unit = "beaufort"
|
||||||
elif config['units'] == "imperial":
|
elif config['units'] == "imperial":
|
||||||
self.wind_unit = "miles_hour"
|
self.wind_unit = "miles_hour"
|
||||||
@ -116,17 +116,17 @@ class Weather(inkycal_module):
|
|||||||
# additional configuration
|
# additional configuration
|
||||||
|
|
||||||
self.owm = OpenWeatherMap(
|
self.owm = OpenWeatherMap(
|
||||||
api_key=self.api_key,
|
api_key=self.api_key,
|
||||||
city_id=self.location,
|
city_id=self.location,
|
||||||
wind_unit=self.wind_unit,
|
wind_unit=self.wind_unit,
|
||||||
temp_unit=self.temp_unit,
|
temp_unit=self.temp_unit,
|
||||||
language=self.locale,
|
language=self.locale,
|
||||||
tz_name=self.timezone
|
tz_name=self.timezone
|
||||||
)
|
)
|
||||||
|
|
||||||
self.weatherfont = ImageFont.truetype(
|
self.weatherfont = ImageFont.truetype(
|
||||||
fonts['weathericons-regular-webfont'], size=self.fontsize)
|
fonts['weathericons-regular-webfont'], size=self.fontsize)
|
||||||
|
|
||||||
if self.wind_unit == "beaufort":
|
if self.wind_unit == "beaufort":
|
||||||
self.windDispUnit = "bft"
|
self.windDispUnit = "bft"
|
||||||
elif self.wind_unit == "knots":
|
elif self.wind_unit == "knots":
|
||||||
@ -143,9 +143,7 @@ class Weather(inkycal_module):
|
|||||||
self.tempDispUnit = "°"
|
self.tempDispUnit = "°"
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f"{__name__} loaded")
|
logger.debug(f"{__name__} loaded")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -154,7 +152,7 @@ class Weather(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -162,8 +160,9 @@ class Weather(inkycal_module):
|
|||||||
|
|
||||||
# Check if internet is available
|
# Check if internet is available
|
||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.debug('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise NetworkNotReachableError
|
raise NetworkNotReachableError
|
||||||
|
|
||||||
def get_moon_phase():
|
def get_moon_phase():
|
||||||
@ -190,7 +189,7 @@ class Weather(inkycal_module):
|
|||||||
7: '\uf0ae'
|
7: '\uf0ae'
|
||||||
}[int(index) & 7]
|
}[int(index) & 7]
|
||||||
|
|
||||||
def is_negative(temp:str):
|
def is_negative(temp: str):
|
||||||
"""Check if temp is below freezing point of water (0°C/32°F)
|
"""Check if temp is below freezing point of water (0°C/32°F)
|
||||||
returns True if temp below freezing point, else False"""
|
returns True if temp below freezing point, else False"""
|
||||||
answer = False
|
answer = False
|
||||||
@ -223,12 +222,19 @@ class Weather(inkycal_module):
|
|||||||
'50n': '\uf023'
|
'50n': '\uf023'
|
||||||
}
|
}
|
||||||
|
|
||||||
def draw_icon(image, xy, box_size, icon, rotation=None):
|
def draw_icon(image: Image, xy: Tuple[int, int], box_size: Tuple[int, int], icon: str, rotation=None):
|
||||||
"""Custom function to add icons of weather font on image
|
"""Custom function to add icons of weather font on the image.
|
||||||
image = on which image should the text be added?
|
|
||||||
xy = xy-coordinates as tuple -> (x,y)
|
Args:
|
||||||
box_size = size of text-box -> (width,height)
|
- image:
|
||||||
icon = icon-unicode, looks this up in weathericons dictionary
|
the image on which image should the text be added
|
||||||
|
- xy:
|
||||||
|
coordinates as tuple -> (x,y)
|
||||||
|
- box_size:
|
||||||
|
size of text-box -> (width,height)
|
||||||
|
- icon:
|
||||||
|
icon-unicode, looks this up in weather-icons dictionary
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
icon_size_correction = {
|
icon_size_correction = {
|
||||||
@ -263,7 +269,6 @@ class Weather(inkycal_module):
|
|||||||
'\uf0a0': 0,
|
'\uf0a0': 0,
|
||||||
'\uf0a3': 0,
|
'\uf0a3': 0,
|
||||||
'\uf0a7': 0,
|
'\uf0a7': 0,
|
||||||
'\uf0aa': 0,
|
|
||||||
'\uf0ae': 0
|
'\uf0ae': 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,8 +282,7 @@ class Weather(inkycal_module):
|
|||||||
font = ImageFont.truetype(font.path, size)
|
font = ImageFont.truetype(font.path, size)
|
||||||
text_width, text_height = font.getbbox(text)[2:]
|
text_width, text_height = font.getbbox(text)[2:]
|
||||||
|
|
||||||
while (text_width < int(box_width * 0.9) and
|
while text_width < int(box_width * 0.9) and text_height < int(box_height * 0.9):
|
||||||
text_height < int(box_height * 0.9)):
|
|
||||||
size += 1
|
size += 1
|
||||||
font = ImageFont.truetype(font.path, size)
|
font = ImageFont.truetype(font.path, size)
|
||||||
text_width, text_height = font.getbbox(text)[2:]
|
text_width, text_height = font.getbbox(text)[2:]
|
||||||
@ -289,8 +293,6 @@ class Weather(inkycal_module):
|
|||||||
x = int((box_width / 2) - (text_width / 2))
|
x = int((box_width / 2) - (text_width / 2))
|
||||||
y = int((box_height / 2) - (text_height / 2))
|
y = int((box_height / 2) - (text_height / 2))
|
||||||
|
|
||||||
# Draw the text in the text-box
|
|
||||||
draw = ImageDraw.Draw(image)
|
|
||||||
space = Image.new('RGBA', (box_width, box_height))
|
space = Image.new('RGBA', (box_width, box_height))
|
||||||
ImageDraw.Draw(space).text((x, y), text, fill='black', font=font)
|
ImageDraw.Draw(space).text((x, y), text, fill='black', font=font)
|
||||||
|
|
||||||
@ -349,17 +351,17 @@ class Weather(inkycal_module):
|
|||||||
row3 = row2 + line_gap + row_height
|
row3 = row2 + line_gap + row_height
|
||||||
|
|
||||||
# Draw lines on each row and border
|
# Draw lines on each row and border
|
||||||
############################################################################
|
###########################################################################
|
||||||
## draw = ImageDraw.Draw(im_black)
|
# draw = ImageDraw.Draw(im_black)
|
||||||
## draw.line((0, 0, im_width, 0), fill='red')
|
# draw.line((0, 0, im_width, 0), fill='red')
|
||||||
## draw.line((0, im_height-1, im_width, im_height-1), fill='red')
|
# draw.line((0, im_height-1, im_width, im_height-1), fill='red')
|
||||||
## draw.line((0, row1, im_width, row1), fill='black')
|
# draw.line((0, row1, im_width, row1), fill='black')
|
||||||
## draw.line((0, row1+row_height, im_width, row1+row_height), fill='black')
|
# draw.line((0, row1+row_height, im_width, row1+row_height), fill='black')
|
||||||
## draw.line((0, row2, im_width, row2), fill='black')
|
# draw.line((0, row2, im_width, row2), fill='black')
|
||||||
## draw.line((0, row2+row_height, im_width, row2+row_height), fill='black')
|
# draw.line((0, row2+row_height, im_width, row2+row_height), fill='black')
|
||||||
## draw.line((0, row3, im_width, row3), fill='black')
|
# draw.line((0, row3, im_width, row3), fill='black')
|
||||||
## draw.line((0, row3+row_height, im_width, row3+row_height), fill='black')
|
# draw.line((0, row3+row_height, im_width, row3+row_height), fill='black')
|
||||||
############################################################################
|
###########################################################################
|
||||||
|
|
||||||
# Positions for current weather details
|
# Positions for current weather details
|
||||||
weather_icon_pos = (col1, 0)
|
weather_icon_pos = (col1, 0)
|
||||||
@ -378,24 +380,24 @@ class Weather(inkycal_module):
|
|||||||
sunset_time_pos = (col3 + icon_small, row3)
|
sunset_time_pos = (col3 + icon_small, row3)
|
||||||
|
|
||||||
# Positions for forecast 1
|
# Positions for forecast 1
|
||||||
stamp_fc1 = (col4, row1)
|
stamp_fc1 = (col4, row1) # noqa
|
||||||
icon_fc1 = (col4, row1 + row_height)
|
icon_fc1 = (col4, row1 + row_height) # noqa
|
||||||
temp_fc1 = (col4, row3)
|
temp_fc1 = (col4, row3) # noqa
|
||||||
|
|
||||||
# Positions for forecast 2
|
# Positions for forecast 2
|
||||||
stamp_fc2 = (col5, row1)
|
stamp_fc2 = (col5, row1) # noqa
|
||||||
icon_fc2 = (col5, row1 + row_height)
|
icon_fc2 = (col5, row1 + row_height) # noqa
|
||||||
temp_fc2 = (col5, row3)
|
temp_fc2 = (col5, row3) # noqa
|
||||||
|
|
||||||
# Positions for forecast 3
|
# Positions for forecast 3
|
||||||
stamp_fc3 = (col6, row1)
|
stamp_fc3 = (col6, row1) # noqa
|
||||||
icon_fc3 = (col6, row1 + row_height)
|
icon_fc3 = (col6, row1 + row_height) # noqa
|
||||||
temp_fc3 = (col6, row3)
|
temp_fc3 = (col6, row3) # noqa
|
||||||
|
|
||||||
# Positions for forecast 4
|
# Positions for forecast 4
|
||||||
stamp_fc4 = (col7, row1)
|
stamp_fc4 = (col7, row1) # noqa
|
||||||
icon_fc4 = (col7, row1 + row_height)
|
icon_fc4 = (col7, row1 + row_height) # noqa
|
||||||
temp_fc4 = (col7, row3)
|
temp_fc4 = (col7, row3) # noqa
|
||||||
|
|
||||||
# Create current-weather and weather-forecast objects
|
# Create current-weather and weather-forecast objects
|
||||||
logging.debug('looking up location by ID')
|
logging.debug('looking up location by ID')
|
||||||
@ -404,7 +406,7 @@ class Weather(inkycal_module):
|
|||||||
|
|
||||||
# Set decimals
|
# Set decimals
|
||||||
dec_temp = 0 if self.round_temperature == True else 1
|
dec_temp = 0 if self.round_temperature == True else 1
|
||||||
dec_wind = 0 if self.round_windspeed == True else 1
|
dec_wind = 0 if self.round_wind_speed == True else 1
|
||||||
|
|
||||||
logging.debug(f'temperature unit: {self.temp_unit}')
|
logging.debug(f'temperature unit: {self.temp_unit}')
|
||||||
logging.debug(f'decimals temperature: {dec_temp} | decimals wind: {dec_wind}')
|
logging.debug(f'decimals temperature: {dec_temp} | decimals wind: {dec_wind}')
|
||||||
@ -424,7 +426,8 @@ class Weather(inkycal_module):
|
|||||||
fc_data['fc' + str(index + 1)] = {
|
fc_data['fc' + str(index + 1)] = {
|
||||||
'temp': f"{forecast['temp']:.{dec_temp}f}{self.tempDispUnit}",
|
'temp': f"{forecast['temp']:.{dec_temp}f}{self.tempDispUnit}",
|
||||||
'icon': forecast["icon"],
|
'icon': forecast["icon"],
|
||||||
'stamp': forecast["datetime"].strftime("%I %p" if self.hour_format == 12 else "%H:%M")}
|
'stamp': forecast["datetime"].strftime("%I %p" if self.hour_format == 12 else "%H:%M")
|
||||||
|
}
|
||||||
|
|
||||||
elif self.forecast_interval == 'daily':
|
elif self.forecast_interval == 'daily':
|
||||||
|
|
||||||
@ -433,7 +436,7 @@ class Weather(inkycal_module):
|
|||||||
daily_forecasts = [self.owm.get_forecast_for_day(days) for days in range(1, 5)]
|
daily_forecasts = [self.owm.get_forecast_for_day(days) for days in range(1, 5)]
|
||||||
|
|
||||||
for index, forecast in enumerate(daily_forecasts):
|
for index, forecast in enumerate(daily_forecasts):
|
||||||
fc_data['fc' + str(index +1)] = {
|
fc_data['fc' + str(index + 1)] = {
|
||||||
'temp': f'{forecast["temp_min"]:.{dec_temp}f}{self.tempDispUnit}/{forecast["temp_max"]:.{dec_temp}f}{self.tempDispUnit}',
|
'temp': f'{forecast["temp_min"]:.{dec_temp}f}{self.tempDispUnit}/{forecast["temp_max"]:.{dec_temp}f}{self.tempDispUnit}',
|
||||||
'icon': forecast['icon'],
|
'icon': forecast['icon'],
|
||||||
'stamp': forecast['datetime'].strftime("%A")
|
'stamp': forecast['datetime'].strftime("%A")
|
||||||
@ -513,6 +516,9 @@ class Weather(inkycal_module):
|
|||||||
# Add the forecast data to the correct places
|
# Add the forecast data to the correct places
|
||||||
for pos in range(1, len(fc_data) + 1):
|
for pos in range(1, len(fc_data) + 1):
|
||||||
stamp = fc_data[f'fc{pos}']['stamp']
|
stamp = fc_data[f'fc{pos}']['stamp']
|
||||||
|
# check if we're using daily forecasts
|
||||||
|
if "day" in stamp:
|
||||||
|
stamp = arrow.get(fc_data[f'fc{pos}']['stamp'], "dddd").format("dddd", locale=self.locale)
|
||||||
|
|
||||||
icon = weather_icons[fc_data[f'fc{pos}']['icon']]
|
icon = weather_icons[fc_data[f'fc{pos}']['icon']]
|
||||||
temp = fc_data[f'fc{pos}']['temp']
|
temp = fc_data[f'fc{pos}']['temp']
|
||||||
|
@ -40,7 +40,10 @@ class Webshot(inkycal_module):
|
|||||||
},
|
},
|
||||||
"crop_h": {
|
"crop_h": {
|
||||||
"label": "Please enter the crop height",
|
"label": "Please enter the crop height",
|
||||||
}
|
},
|
||||||
|
"rotation": {
|
||||||
|
"label": "Please enter the rotation. Must be either 0, 90, 180 or 270",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
@ -72,8 +75,14 @@ class Webshot(inkycal_module):
|
|||||||
else:
|
else:
|
||||||
self.crop_y = 0
|
self.crop_y = 0
|
||||||
|
|
||||||
|
self.rotation = 0
|
||||||
|
if "rotation" in config:
|
||||||
|
self.rotation = int(config["rotation"])
|
||||||
|
if self.rotation not in [0, 90, 180, 270]:
|
||||||
|
raise Exception("Rotation must be either 0, 90, 180 or 270")
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'Inkycal webshot loaded')
|
logger.debug(f'Inkycal webshot loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -89,7 +98,7 @@ class Webshot(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info('image size: {} x {} px'.format(im_width, im_height))
|
logger.debug('image size: {} x {} px'.format(im_width, im_height))
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels (required)
|
# Create an image for black pixels and one for coloured pixels (required)
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -99,12 +108,13 @@ class Webshot(inkycal_module):
|
|||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.info('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise Exception('Network could not be reached :/')
|
raise Exception('Network could not be reached :/')
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f'preparing webshot from {self.url}... cropH{self.crop_h} cropW{self.crop_w} cropX{self.crop_x} cropY{self.crop_y}')
|
f'preparing webshot from {self.url}... cropH{self.crop_h} cropW{self.crop_w} cropX{self.crop_x} cropY{self.crop_y}')
|
||||||
|
|
||||||
shot = WebShot()
|
shot = WebShot(size=(im_height, im_width))
|
||||||
|
|
||||||
shot.params = {
|
shot.params = {
|
||||||
"--crop-x": self.crop_x,
|
"--crop-x": self.crop_x,
|
||||||
@ -150,11 +160,21 @@ class Webshot(inkycal_module):
|
|||||||
|
|
||||||
centerPosX = int((im_width / 2) - (im.image.width / 2))
|
centerPosX = int((im_width / 2) - (im.image.width / 2))
|
||||||
|
|
||||||
webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY))
|
|
||||||
im_black.paste(webshotSpaceBlack)
|
|
||||||
|
|
||||||
webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY))
|
if self.rotation != 0:
|
||||||
im_colour.paste(webshotSpaceColour)
|
webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY))
|
||||||
|
im_black.paste(webshotSpaceBlack)
|
||||||
|
im_black = im_black.rotate(self.rotation, expand=True)
|
||||||
|
|
||||||
|
webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY))
|
||||||
|
im_colour.paste(webshotSpaceColour)
|
||||||
|
im_colour = im_colour.rotate(self.rotation, expand=True)
|
||||||
|
else:
|
||||||
|
webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY))
|
||||||
|
im_black.paste(webshotSpaceBlack)
|
||||||
|
|
||||||
|
webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY))
|
||||||
|
im_colour.paste(webshotSpaceColour)
|
||||||
|
|
||||||
im.clear()
|
im.clear()
|
||||||
logger.info(f'added webshot image')
|
logger.info(f'added webshot image')
|
||||||
|
@ -11,6 +11,8 @@ from inkycal.modules.template import inkycal_module
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
|
||||||
class Xkcd(inkycal_module):
|
class Xkcd(inkycal_module):
|
||||||
name = "xkcd - Displays comics from xkcd.com by Randall Munroe"
|
name = "xkcd - Displays comics from xkcd.com by Randall Munroe"
|
||||||
@ -51,13 +53,13 @@ class Xkcd(inkycal_module):
|
|||||||
self.scale_filter = config['filter']
|
self.scale_filter = config['filter']
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'Inkycal XKCD loaded')
|
logger.debug(f'Inkycal XKCD loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
|
|
||||||
# Create tmp path
|
# Create tmp path
|
||||||
tmpPath = f"{top_level}/temp"
|
tmpPath = settings.TEMPORARY_FOLDER
|
||||||
|
|
||||||
if not os.path.exists(tmpPath):
|
if not os.path.exists(tmpPath):
|
||||||
os.mkdir(tmpPath)
|
os.mkdir(tmpPath)
|
||||||
@ -66,7 +68,7 @@ class Xkcd(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info('image size: {} x {} px'.format(im_width, im_height))
|
logger.debug('image size: {} x {} px'.format(im_width, im_height))
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels (required)
|
# Create an image for black pixels and one for coloured pixels (required)
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -76,6 +78,7 @@ class Xkcd(inkycal_module):
|
|||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.info('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise Exception('Network could not be reached :/')
|
raise Exception('Network could not be reached :/')
|
||||||
|
|
||||||
# Set some parameters for formatting feeds
|
# Set some parameters for formatting feeds
|
||||||
|
22
inkycal/settings.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""Settings class
|
||||||
|
Used to initialize the settings for the application.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
"""Settings class to initialize the settings for the application.
|
||||||
|
|
||||||
|
"""
|
||||||
|
CACHE_PATH = os.path.join(basedir, "cache")
|
||||||
|
LOG_PATH = os.path.join(basedir, "../logs")
|
||||||
|
INKYCAL_LOG_PATH = os.path.join(LOG_PATH, "inkycal.log")
|
||||||
|
FONT_PATH = os.path.join(basedir, "../fonts")
|
||||||
|
IMAGE_FOLDER = os.path.join(basedir, "../image_folder")
|
||||||
|
PARALLEL_DRIVER_PATH = os.path.join(basedir, "display", "drivers", "parallel_drivers")
|
||||||
|
TEMPORARY_FOLDER = os.path.join(basedir, "tmp")
|
||||||
|
VCOM = "2.0"
|
||||||
|
# /boot/settings.json is path on older releases, while the latter is more the more recent ones
|
||||||
|
SETTINGS_JSON_PATHS = ["/boot/settings.json", "/boot/firmware/settings.json"]
|
2
inkycal/utils/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .pisugar import PiSugar
|
||||||
|
from .json_cache import JSONCache
|
32
inkycal/utils/json_cache.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""JSON Cache
|
||||||
|
Can be used to cache JSON data to disk. This is useful for caching data to survive reboots.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from inkycal.settings import Settings
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
|
||||||
|
class JSONCache:
|
||||||
|
def __init__(self, name: str, create_if_not_exists: bool = True):
|
||||||
|
self.path = os.path.join(settings.CACHE_PATH,f"{name}.json")
|
||||||
|
|
||||||
|
if not os.path.exists(settings.CACHE_PATH):
|
||||||
|
os.makedirs(settings.CACHE_PATH)
|
||||||
|
|
||||||
|
if create_if_not_exists and not os.path.exists(self.path):
|
||||||
|
with open(self.path, "w", encoding="utf-8") as file:
|
||||||
|
json.dump({}, file)
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
try:
|
||||||
|
with open(self.path, "r", encoding="utf-8") as file:
|
||||||
|
return json.load(file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def write(self, data: dict):
|
||||||
|
with open(self.path, "w", encoding="utf-8") as file:
|
||||||
|
json.dump(data, file, indent=4, sort_keys=True)
|
147
inkycal/utils/pisugar.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
"""PiSugar helper class for Inkycal."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from inkycal.settings import Settings
|
||||||
|
import arrow
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PiSugar:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# replace "command" with actual command
|
||||||
|
self.command_template = 'echo "command" | nc -q 0 127.0.0.1 8423'
|
||||||
|
self.allowed_commands = ["get battery", "get model", "get rtc_time", "get rtc_alarm_enabled",
|
||||||
|
"get rtc_alarm_time", "get alarm_repeat", "rtc_pi2rtc", "rtc_alarm_set"]
|
||||||
|
|
||||||
|
def _get_output(self, command, param=None):
|
||||||
|
if command not in self.allowed_commands:
|
||||||
|
logger.error(f"Command {command} not allowed")
|
||||||
|
return None
|
||||||
|
if param:
|
||||||
|
cmd = self.command_template.replace("command", f"{command} {param}")
|
||||||
|
else:
|
||||||
|
cmd = self.command_template.replace("command", command)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, shell=True, text=True, capture_output=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Command failed with {result.stderr}")
|
||||||
|
return None
|
||||||
|
output = result.stdout.strip()
|
||||||
|
return output
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing command: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_battery(self) -> float or None:
|
||||||
|
"""Get the battery level in percentage.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int or None: The battery level in percentage or None if the command fails.
|
||||||
|
"""
|
||||||
|
battery_output = self._get_output("get battery")
|
||||||
|
if battery_output:
|
||||||
|
for line in battery_output.splitlines():
|
||||||
|
if 'battery:' in line:
|
||||||
|
return float(line.split(':')[1].strip())
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_model(self) -> str or None:
|
||||||
|
"""Get the PiSugar model."""
|
||||||
|
model_output = self._get_output("get model")
|
||||||
|
if model_output:
|
||||||
|
for line in model_output.splitlines():
|
||||||
|
if 'model:' in line:
|
||||||
|
return line.split(':')[1].strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_rtc_time(self) -> arrow.arrow or None:
|
||||||
|
"""Get the RTC time."""
|
||||||
|
result = self._get_output("get rtc_time")
|
||||||
|
if result:
|
||||||
|
rtc_time = result.split("rtc_time: ")[1].strip()
|
||||||
|
return arrow.get(rtc_time)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_rtc_alarm_enabled(self) -> str or None:
|
||||||
|
"""Get the RTC alarm enabled status."""
|
||||||
|
result = self._get_output("get rtc_alarm_enabled")
|
||||||
|
if result:
|
||||||
|
second_line = result.splitlines()[1]
|
||||||
|
output = second_line.split('rtc_alarm_enabled: ')[1].strip()
|
||||||
|
return True if output == "true" else False
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_rtc_alarm_time(self) -> arrow.arrow or None:
|
||||||
|
"""Get the RTC alarm time."""
|
||||||
|
result = self._get_output("get rtc_alarm_time")
|
||||||
|
if result:
|
||||||
|
alarm_time = result.split('rtc_alarm_time: ')[1].strip()
|
||||||
|
return arrow.get(alarm_time)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_alarm_repeat(self) -> dict or None:
|
||||||
|
"""Get the alarm repeat status.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict or None: A dictionary with the alarm repeating days or None if the command fails.
|
||||||
|
"""
|
||||||
|
result = self._get_output("get alarm_repeat")
|
||||||
|
if result:
|
||||||
|
repeating_days = f"{int(result.split('alarm_repeat: ')[1].strip()):8b}".strip()
|
||||||
|
data = {"Monday": False, "Tuesday": False, "Wednesday": False, "Thursday": False, "Friday": False,
|
||||||
|
"Saturday": False, "Sunday": False}
|
||||||
|
if repeating_days[0] == "1":
|
||||||
|
data["Monday"] = True
|
||||||
|
if repeating_days[1] == "1":
|
||||||
|
data["Tuesday"] = True
|
||||||
|
if repeating_days[2] == "1":
|
||||||
|
data["Wednesday"] = True
|
||||||
|
if repeating_days[3] == "1":
|
||||||
|
data["Thursday"] = True
|
||||||
|
if repeating_days[4] == "1":
|
||||||
|
data["Friday"] = True
|
||||||
|
if repeating_days[5] == "1":
|
||||||
|
data["Saturday"] = True
|
||||||
|
if repeating_days[6] == "1":
|
||||||
|
data["Sunday"] = True
|
||||||
|
return data
|
||||||
|
return None
|
||||||
|
|
||||||
|
def rtc_pi2rtc(self) -> bool:
|
||||||
|
"""Sync the Pi time to RTC.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the sync was successful, False otherwise.
|
||||||
|
"""
|
||||||
|
result = self._get_output("rtc_pi2rtc")
|
||||||
|
if result:
|
||||||
|
status = result.split('rtc_pi2rtc: ')[1].strip()
|
||||||
|
if status == "done":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def rtc_alarm_set(self, time: arrow.arrow, repeat:int=127) -> bool:
|
||||||
|
"""Set the RTC alarm time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time (arrow.arrow): The alarm time in ISO 8601 format.
|
||||||
|
repeat: int representing 7-bit binary number of repeating days. e.g. 127 = 1111111 = repeat every day
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the alarm was set successfully, False otherwise.
|
||||||
|
"""
|
||||||
|
iso_format = time.isoformat()
|
||||||
|
result = self._get_output("rtc_alarm_set", f"{iso_format } {repeat}")
|
||||||
|
if result:
|
||||||
|
status = result.split('rtc_alarm_set: ')[1].strip()
|
||||||
|
if status == "done":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
@ -2,7 +2,7 @@ appdirs==1.4.4
|
|||||||
arrow==1.3.0
|
arrow==1.3.0
|
||||||
asyncio==3.4.3
|
asyncio==3.4.3
|
||||||
beautifulsoup4==4.12.3
|
beautifulsoup4==4.12.3
|
||||||
certifi==2024.2.2
|
certifi==2024.7.4
|
||||||
cfgv==3.4.0
|
cfgv==3.4.0
|
||||||
charset-normalizer==3.3.2
|
charset-normalizer==3.3.2
|
||||||
colorzero==2.0
|
colorzero==2.0
|
||||||
@ -37,7 +37,7 @@ python-dotenv==1.0.1
|
|||||||
pytz==2024.1
|
pytz==2024.1
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
recurring-ical-events==2.1.2
|
recurring-ical-events==2.1.2
|
||||||
requests==2.32.0
|
requests==2.32.3
|
||||||
sgmllib3k==1.0.0
|
sgmllib3k==1.0.0
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
soupsieve==2.5
|
soupsieve==2.5
|
||||||
@ -46,9 +46,9 @@ types-python-dateutil==2.8.19.20240106
|
|||||||
typing_extensions==4.9.0
|
typing_extensions==4.9.0
|
||||||
tzdata==2024.1
|
tzdata==2024.1
|
||||||
tzlocal==5.2
|
tzlocal==5.2
|
||||||
urllib3==2.2.0
|
urllib3==2.2.2
|
||||||
virtualenv==20.25.0
|
virtualenv==20.25.0
|
||||||
webencodings==0.5.1
|
webencodings==0.5.1
|
||||||
x-wr-timezone==0.0.6
|
x-wr-timezone==0.0.6
|
||||||
xkcd==2.4.2
|
xkcd==2.4.2
|
||||||
yfinance==0.2.36
|
yfinance==0.2.40
|
||||||
|
4
setup.py
@ -13,10 +13,10 @@ with open('requirements.txt') as f:
|
|||||||
required = [i.split(' ')[0] for i in required]
|
required = [i.split(' ')[0] for i in required]
|
||||||
|
|
||||||
__project__ = "inkycal"
|
__project__ = "inkycal"
|
||||||
__version__ = "2.0.3"
|
__version__ = "2.0.4"
|
||||||
__description__ = "Inkycal is a python3 software for syncing icalendar events, weather and news on selected E-Paper displays"
|
__description__ = "Inkycal is a python3 software for syncing icalendar events, weather and news on selected E-Paper displays"
|
||||||
__packages__ = ["inkycal"]
|
__packages__ = ["inkycal"]
|
||||||
__author__ = "aceisace"
|
__author__ = "aceinnolab"
|
||||||
__author_email__ = "aceisace63@yahoo.com"
|
__author_email__ = "aceisace63@yahoo.com"
|
||||||
__url__ = "https://github.com/aceinnolab/Inkycal"
|
__url__ = "https://github.com/aceinnolab/Inkycal"
|
||||||
|
|
||||||
|
@ -1,12 +1,22 @@
|
|||||||
"""
|
"""
|
||||||
Test the functions in the functions module.
|
Test the functions in the functions module.
|
||||||
"""
|
"""
|
||||||
|
import unittest
|
||||||
|
|
||||||
from PIL import Image, ImageFont
|
from PIL import Image, ImageFont
|
||||||
from inkycal.custom import write, fonts
|
|
||||||
|
from inkycal.custom import write, fonts, get_system_tz
|
||||||
|
|
||||||
|
|
||||||
def test_write():
|
class TestIcalendar(unittest.TestCase):
|
||||||
im = Image.new("RGB", (500, 200), "white")
|
|
||||||
font = ImageFont.truetype(fonts['NotoSans-SemiCondensed'], size = 40)
|
def test_write(self):
|
||||||
write(im, (125,75), (250, 50), "Hello World", font)
|
im = Image.new("RGB", (500, 200), "white")
|
||||||
# im.show()
|
font = ImageFont.truetype(fonts['NotoSans-SemiCondensed'], size=40)
|
||||||
|
write(im, (125, 75), (250, 50), "Hello World", font)
|
||||||
|
# im.show()
|
||||||
|
|
||||||
|
def test_get_system_tz(self):
|
||||||
|
tz = get_system_tz()
|
||||||
|
assert isinstance(tz, str)
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ tests = [
|
|||||||
"padding_x": 10,
|
"padding_x": 10,
|
||||||
"padding_y": 10,
|
"padding_y": 10,
|
||||||
"fontsize": 12,
|
"fontsize": 12,
|
||||||
"language": "en"
|
"language": "de"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -37,7 +37,7 @@ tests = [
|
|||||||
"size": [500, 800],
|
"size": [500, 800],
|
||||||
"ical_urls": sample_url,
|
"ical_urls": sample_url,
|
||||||
"ical_files": None,
|
"ical_files": None,
|
||||||
"date_format": "ddd D MMM",
|
"date_format": "DD.MMMM YYYY",
|
||||||
"time_format": "HH:mm",
|
"time_format": "HH:mm",
|
||||||
"padding_x": 10,
|
"padding_x": 10,
|
||||||
"padding_y": 10,
|
"padding_y": 10,
|
||||||
|
@ -20,7 +20,7 @@ tests = [
|
|||||||
{
|
{
|
||||||
"name": "Calendar",
|
"name": "Calendar",
|
||||||
"config": {
|
"config": {
|
||||||
"size": [500, 500],
|
"size": [500, 600],
|
||||||
"week_starts_on": "Monday",
|
"week_starts_on": "Monday",
|
||||||
"show_events": True,
|
"show_events": True,
|
||||||
"ical_urls": sample_url,
|
"ical_urls": sample_url,
|
||||||
|
@ -30,11 +30,11 @@ tests = [
|
|||||||
"forecast_interval": "daily",
|
"forecast_interval": "daily",
|
||||||
"units": "metric",
|
"units": "metric",
|
||||||
"hour_format": "12",
|
"hour_format": "12",
|
||||||
"use_beaufort": True,
|
"use_beaufort": False,
|
||||||
"padding_x": 10,
|
"padding_x": 10,
|
||||||
"padding_y": 10,
|
"padding_y": 10,
|
||||||
"fontsize": 12,
|
"fontsize": 12,
|
||||||
"language": "en"
|
"language": "de"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -6,38 +6,23 @@ import logging
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from inkycal.modules import Webshot
|
from inkycal.modules import Webshot
|
||||||
|
from inkycal.modules.inky_image import Inkyimage
|
||||||
|
from tests import Config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
preview = Inkyimage.preview
|
||||||
|
merge = Inkyimage.merge
|
||||||
|
|
||||||
tests = [
|
tests = [
|
||||||
{
|
|
||||||
"position": 1,
|
|
||||||
"name": "Webshot",
|
|
||||||
"config": {
|
|
||||||
"size": [400, 100],
|
|
||||||
"url": "https://github.com",
|
|
||||||
"palette": "bwr",
|
|
||||||
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"name": "Webshot",
|
"name": "Webshot",
|
||||||
"config": {
|
"config": {
|
||||||
"size": [400, 200],
|
"size": [400, 200],
|
||||||
"url": "https://github.com",
|
"url": "https://aceinnolab.com",
|
||||||
"palette": "bwy",
|
"palette": "bwr",
|
||||||
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": 1,
|
|
||||||
"name": "Webshot",
|
|
||||||
"config": {
|
|
||||||
"size": [400, 300],
|
|
||||||
"url": "https://github.com",
|
|
||||||
"palette": "bw",
|
|
||||||
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
|
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -46,8 +31,31 @@ tests = [
|
|||||||
"name": "Webshot",
|
"name": "Webshot",
|
||||||
"config": {
|
"config": {
|
||||||
"size": [400, 400],
|
"size": [400, 400],
|
||||||
"url": "https://github.com",
|
"url": "https://aceinnolab.com",
|
||||||
|
"palette": "bwy",
|
||||||
|
"rotation": 0,
|
||||||
|
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": 1,
|
||||||
|
"name": "Webshot",
|
||||||
|
"config": {
|
||||||
|
"size": [400, 600],
|
||||||
|
"url": "https://aceinnolab.com",
|
||||||
|
"palette": "bw",
|
||||||
|
"rotation": 90,
|
||||||
|
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": 1,
|
||||||
|
"name": "Webshot",
|
||||||
|
"config": {
|
||||||
|
"size": [400, 800],
|
||||||
|
"url": "https://aceinnolab.com",
|
||||||
"palette": "bwr",
|
"palette": "bwr",
|
||||||
|
"rotation": 180,
|
||||||
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
|
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,6 +68,7 @@ class TestWebshot(unittest.TestCase):
|
|||||||
for test in tests:
|
for test in tests:
|
||||||
logger.info(f'test {tests.index(test) + 1} generating image..')
|
logger.info(f'test {tests.index(test) + 1} generating image..')
|
||||||
module = Webshot(test)
|
module = Webshot(test)
|
||||||
module.generate_image()
|
im_black, im_colour = module.generate_image()
|
||||||
|
if Config.USE_PREVIEW:
|
||||||
|
preview(merge(im_black, im_colour))
|
||||||
logger.info('OK')
|
logger.info('OK')
|
||||||
|
|
||||||
|
@ -21,9 +21,9 @@ class TestMain(unittest.TestCase):
|
|||||||
assert inkycal.settings["info_section_height"] == 70
|
assert inkycal.settings["info_section_height"] == 70
|
||||||
assert inkycal.settings["border_around_modules"] is True
|
assert inkycal.settings["border_around_modules"] is True
|
||||||
|
|
||||||
def test_run(self):
|
def test_dry_run(self):
|
||||||
inkycal = Inkycal(self.settings_path, render=False)
|
inkycal = Inkycal(self.settings_path, render=False)
|
||||||
inkycal.test()
|
inkycal.dry_run()
|
||||||
|
|
||||||
def test_countdown(self):
|
def test_countdown(self):
|
||||||
inkycal = Inkycal(self.settings_path, render=False)
|
inkycal = Inkycal(self.settings_path, render=False)
|
||||||
|