Merge remote-tracking branch 'origin/main'

This commit is contained in:
Ace 2024-08-13 04:07:26 +02:00
commit 8376e26f9c
67 changed files with 1769 additions and 541 deletions

View File

@ -24,7 +24,8 @@ jobs:
TINDIE_USERNAME: ${{ secrets.TINDIE_USERNAME }}
with:
# Set the base_image to the desired Raspberry Pi OS version
base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2024-03-15/2024-03-15-raspios-bookworm-armhf-lite.img.xz
# 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
optimize_image: true
commands: |
@ -69,10 +70,11 @@ jobs:
echo $CWD
# increase swap-size
sudo dphys-swapfile swapoff
sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=512/' /etc/dphys-swapfile
sudo dphys-swapfile setup
sudo dphys-swapfile swapon
# temporarily disabled due to unmounting issues
# sudo dphys-swapfile swapoff
# sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=512/' /etc/dphys-swapfile
# sudo dphys-swapfile setup
# sudo dphys-swapfile swapon
# enable SPI
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
# 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
sleep 10

View File

@ -1,13 +1,47 @@
# E-Paper-Calendar Software Changelog
# Inkycal Software Changelog
All significant changes will be documented in this file.
The order is from latest to oldest and structured in the following way:
* 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
### 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 fullscreen weather module
* Own OWM API abstraction as a replacement for PyOWM 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.
- 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

View File

@ -1,4 +1,4 @@
# Welcome to inkycal v2.0.3!
# Welcome to inkycal v2.0.4!
<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>
@ -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
features and fixing bugs. Please choose the one-time sponsor option and select the one with the plug-and-play version of
Inkycal. Then, send your email-address to which InkycalOS-Lite should be sent.
Alternatively, you can also use the paypal.me link and send the same amount as Github sponsors to get access to
Alternatively, you can also use the PayPal.me link and send the same amount as GitHub sponsors to get access to
InkycalOS-Lite!
## Main features
@ -42,10 +42,13 @@ following built-in modules are supported:
* 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.
* 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.
* Todoist - Synchronise with Todoist app or website to show todos.
* 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
@ -56,7 +59,8 @@ Watch the one-minute video on getting started with Inkycal:
## Hardware guide
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.
**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
display!**
| 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. |
| Inkycal frame (kit -> requires wires, 7.5" Display and Zero W with microSD card | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-frame-custom-driver-board-only/) Ultraslim frame with custom-made front and backcover inkl. ultraslim driver board). You will need a Raspberry Pi, microSD card and a 7.5" e-paper display |
| Driver board | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/universal-e-paper-driver-board-for-24-pin-spi/) Ultraslim, 24-pin SPI driver board for many serial e-paper displays. |
| `[serial]` 12.48" (1304×984px) display | waveshare / gooddisplay |  Search for `Waveshare 12.48" E-Paper 1304×984` on amazon or similar |
| `[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" (800x480px) -> v2 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x480` 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]` 5.83" (400x300px) display | waveshare / gooddisplay | Search for `Waveshare 5.83" E-Paper 400x300` on amazon or similar |
| `[serial]` 4.2" (400x300px)display | waveshare / gooddisplay | Search for `Waveshare 4.2" 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 |
| `[parallel]` 9.7" (1200×825px) display | waveshare / gooddisplay | Search for `Waveshare 9.7" E-Paper 1200×825` 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 |
| 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 |
| type | vendor | Where to buy |
|---------------------------------------------------------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 12.48" Inkycal (plug-and-play) | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-1248-build/) Pre-configured version of Inkycal with matte black aluminium designer frame and a web-ui. You do not need to buy anything extra. Includes Raspberry Pi Zero W, 12.48" e-paper, microSD card, driver board, custom packaging and 1m of cable. Comes pre-assembled for plug-and-play. |
| 7.5" Inkycal (plug-and-play) | Aceinnolab (author) |  [Buy on Tindie](https://www.tindie.com/products/aceisace4444/inkycal-build-v1/) Pre-configured version of Inkycal with custom frame and a web-ui. You do not need to buy anything extra. Includes Raspberry Pi Zero W, 7.5" e-paper, microSD card, driver board, custom packaging and 1m of cable. Comes pre-assembled for plug-and-play. |
| Inkycal frame (kit -> requires wires, 7.5" Display and Zero W with microSD card | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-frame-custom-driver-board-only/) Ultraslim frame with custom-made front and backcover inkl. ultraslim driver board). You will need a Raspberry Pi, microSD card and a 7.5" e-paper display |
| Driver board | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/universal-e-paper-driver-board-for-24-pin-spi/) Ultraslim, 24-pin SPI driver board for many serial e-paper displays. |
| `[serial]` 12.48" (1304×984px) display | waveshare / gooddisplay |  Search for `Waveshare 12.48" E-Paper 1304×984` on amazon or similar |
| `[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" (800x480px) -> v2 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x480` 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]` 5.83" (400x300px) display | waveshare / gooddisplay | Search for `Waveshare 5.83" E-Paper 400x300` on amazon or similar |
| `[serial]` 4.2" (400x300px)display | waveshare / gooddisplay | Search for `Waveshare 4.2" 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 |
| `[parallel]` 9.7" (1200×825px) display | waveshare / gooddisplay | Search for `Waveshare 9.7" E-Paper 1200×825` 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 |
| 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
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 |
|:--------------------------|:---------------------------:|
@ -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!
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
Run the following steps to install Inkycal. Do **not** use sudo for this, except where explicitly specified.
```bash
# Raspberry Pi specific section start
sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev
sudo apt-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
cd WiringPi
./build

View File

@ -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!")

View File

@ -1,5 +1,5 @@
const DOCUMENTATION_OPTIONS = {
VERSION: '2.0.3',
VERSION: '2.0.4',
LANGUAGE: 'en',
COLLAPSE_INDEX: false,
BUILDER: 'html',

View File

@ -178,7 +178,7 @@ const Search = {
htmlToText: (htmlString, anchor) => {
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() });
}
if (anchor) {
@ -328,13 +328,14 @@ const Search = {
for (const [title, foundTitles] of Object.entries(allTitles)) {
if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) {
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([
docNames[file],
titles[file] !== title ? `${titles[file]} > ${title}` : title,
id !== null ? "#" + id : "",
null,
score,
score + boost,
filenames[file],
]);
}

View File

@ -4,7 +4,7 @@
<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" />
<title>About Inkycal &mdash; inkycal 2.0.3 documentation</title>
<title>About Inkycal &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
@ -15,7 +15,7 @@
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=664ffad9"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>

View File

@ -4,7 +4,7 @@
<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" />
<title>Developer documentation &mdash; inkycal 2.0.3 documentation</title>
<title>Developer documentation &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
@ -15,7 +15,7 @@
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=664ffad9"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Index &mdash; inkycal 2.0.3 documentation</title>
<title>Index &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
@ -14,7 +14,7 @@
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=664ffad9"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>
@ -129,6 +129,10 @@
<table style="width: 100%" class="indextable genindextable"><tr>
<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>
</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>
</ul></td>
</tr></table>
@ -253,6 +257,10 @@
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="inkycal.html#inkycal.modules.inky_image.Inkyimage.preview">preview() (inkycal.modules.inky_image.Inkyimage static method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="inkycal.html#inkycal.main.Inkycal.process_module">process_module() (inkycal.main.Inkycal method)</a>
</li>
</ul></td>
</tr></table>
@ -285,10 +293,6 @@
<h2 id="T">T</h2>
<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>
<li><a href="inkycal.html#inkycal.custom.functions.text_wrap">text_wrap() (in module inkycal.custom.functions)</a>
</li>

View File

@ -4,7 +4,7 @@
<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" />
<title>Inkycal documentation &mdash; inkycal 2.0.3 documentation</title>
<title>Inkycal documentation &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
@ -15,7 +15,7 @@
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=664ffad9"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>

View File

@ -4,7 +4,7 @@
<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" />
<title>Inkycal &mdash; inkycal 2.0.3 documentation</title>
<title>Inkycal &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
@ -15,7 +15,7 @@
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=664ffad9"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/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-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.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.test"><code class="docutils literal notranslate"><span class="pre">Inkycal.test()</span></code></a></li>
</ul>
</li>
</ul>
@ -130,7 +131,7 @@
Copyright by aceinnolab</p>
<dl class="py class">
<dt class="sig sig-object py" id="inkycal.main.Inkycal">
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">inkycal.main.</span></span><span class="sig-name descname"><span class="pre">Inkycal</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">settings_path</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">render</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">bool</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">True</span></span></em><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>
<p>Main class of Inkycal, test and run the main Inkycal program.</p>
<dl class="simple">
@ -157,35 +158,21 @@ cycles. After a refresh cycle, a new image is generated and shown.</p>
<dl class="py method">
<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">&#x2192;</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">
<dt>Args:</dt><dd><ul class="simple">
<li><dl class="simple">
<dt>interval_mins = int -&gt; the interval in minutes for the update</dt><dd><p>if no interval is given, the value from the settings file is used.</p>
<dt>Args:</dt><dd><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>
</dd>
</dl>
</li>
</ul>
</dd>
<dt>Returns:</dt><dd><ul class="simple">
<li><p>int -&gt; the remaining time in seconds until next update</p></li>
</ul>
<dt>Returns:</dt><dd><p>int: The remaining time in seconds until the next update.</p>
</dd>
</dl>
</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><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.</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>
<dt class="sig sig-object py" id="inkycal.main.Inkycal.dry_run">
<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>Tests if Inkycal can run without issues.</p>
<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
@ -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>
</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">&#x2192;</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>
</section>
@ -232,14 +241,14 @@ which the given font should be scaled to.</p></li>
<dl class="py function">
<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">&lt;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'&gt;,</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">&#x2192;</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>
<dl 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
im_colour.</p></li>
im_colour).</p></li>
<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 -&gt; (width, height).</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>
@ -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>
<div class="doctest highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="kn">import</span> <span class="nn">arrow</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">print</span><span class="p">(</span><span class="n">arrow</span><span class="o">.</span><span class="n">now</span><span class="p">())</span> <span class="c1"># returns non-timezone-aware time</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">print</span><span class="p">(</span><span class="n">arrow</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">tz</span><span class="o">=</span><span class="n">get_system_tz</span><span class="p">())</span> <span class="c1"># prints timezone aware time.</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">print</span><span class="p">(</span><span class="n">arrow</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">tz</span><span class="o">=</span><span class="n">get_system_tz</span><span class="p">()))</span> <span class="c1"># prints timezone aware time.</span>
</pre></div>
</div>
</dd></dl>
<dl class="py function">
<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">&#x2192;</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>
<p>Attempts to connect to google.com with a timeout of 5 seconds to check
if the network can be reached.</p>
@ -315,7 +324,7 @@ if the network can be reached.</p>
<dl class="py function">
<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>
<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>
@ -334,7 +343,7 @@ splitting the text into the next chunk.</p></li>
<dl class="py function">
<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">&lt;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'&gt;,</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>
<p>Writes given text at given position on the specified image.</p>
<dl class="simple">

Binary file not shown.

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Python Module Index &mdash; inkycal 2.0.3 documentation</title>
<title>Python Module Index &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
@ -14,7 +14,7 @@
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=664ffad9"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>

View File

@ -4,7 +4,7 @@
<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" />
<title>Quickstart &mdash; inkycal 2.0.3 documentation</title>
<title>Quickstart &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
@ -15,7 +15,7 @@
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=664ffad9"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Search &mdash; inkycal 2.0.3 documentation</title>
<title>Search &mdash; inkycal 2.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=80d5e7a1" />
<link rel="stylesheet" type="text/css" href="_static/css/theme.css?v=19f00094" />
@ -15,7 +15,7 @@
<script src="_static/jquery.js?v=5d32c60e"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js?v=2cd50e6c"></script>
<script src="_static/documentation_options.js?v=664ffad9"></script>
<script src="_static/documentation_options.js?v=adc66a14"></script>
<script src="_static/doctools.js?v=9a2dae69"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/js/theme.js"></script>

File diff suppressed because one or more lines are too long

View File

@ -22,7 +22,7 @@ copyright = '2018-2023, aceinnolab'
author = 'aceinnolab'
# The full version, including alpha/beta/rc tags
release = '2.0.3'
release = '2.0.4'
# -- General configuration ---------------------------------------------------

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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
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)
inky.test() # test if Inkycal can be run correctly, running this will show a bit of info for each module
asyncio.run(inky.run()) # If there were no issues, you can run Inkycal nonstop
async def run():
"""Run Inkycal nonstop. Default mode."""
# 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())

View File

@ -1,20 +1,15 @@
# Display class (for driving E-Paper displays)
from inkycal.display import Display
# Default modules
import inkycal.modules.inkycal_agenda
import inkycal.modules.inkycal_calendar
import inkycal.modules.inkycal_weather
import inkycal.modules.inkycal_feeds
import inkycal.modules.inkycal_todoist
import inkycal.modules.inkycal_fullweather
import inkycal.modules.inkycal_image
import inkycal.modules.inkycal_jokes
import inkycal.modules.inkycal_slideshow
import inkycal.modules.inkycal_stocks
import inkycal.modules.inkycal_todoist
import inkycal.modules.inkycal_weather
import inkycal.modules.inkycal_webshot
import inkycal.modules.inkycal_xkcd
import inkycal.modules.inkycal_fullweather
# Main file
from inkycal.display import Display
from inkycal.main import Inkycal
import inkycal.modules.inkycal_stocks

View File

@ -8,29 +8,25 @@ import logging
import os
import time
import traceback
from typing import Tuple
import arrow
import PIL
import requests
import tzlocal
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
logs = logging.getLogger(__name__)
logs.setLevel(level=logging.INFO)
from inkycal.settings import Settings
# Get the path to the Inkycal folder
top_level = "/".join(os.path.dirname(os.path.abspath(os.path.dirname(__file__))).split("/")[:-1])
logger = logging.getLogger(__name__)
# Get path of 'fonts' and 'images' folders within Inkycal folder
fonts_location = os.path.join(top_level, "fonts/")
image_folder = os.path.join(top_level, "image_folder/")
settings = Settings()
# Get available fonts within fonts folder
fonts = {}
for path, dirs, files in os.walk(fonts_location):
for path, dirs, files in os.walk(settings.FONT_PATH):
for _ in files:
if _.endswith(".otf"):
name = _.split(".otf")[0]
@ -39,7 +35,7 @@ for path, dirs, files in os.walk(fonts_location):
if _.endswith(".ttf"):
name = _.split(".ttf")[0]
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()]
@ -77,16 +73,16 @@ def get_system_tz() -> str:
>>> import arrow
>>> 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:
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:
logs.error("System timezone could not be parsed!")
logs.error("Please set timezone manually!. Falling back to UTC...")
logger.error("System timezone could not be parsed!")
logger.error("Please set timezone manually!. Falling back to 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
@ -115,7 +111,7 @@ def auto_fontsize(font, max_height):
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 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_width = text_bbox[2] - text_bbox[0]
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):
size += 1
@ -173,23 +169,23 @@ def write(image, xy, box_size, text, font=None, **kwargs):
text_bbox = font.getbbox(text)
text_width = text_bbox[2] - text_bbox[0]
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_width = text_bbox[2] - text_bbox[0]
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
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):
text = text[0:-1]
text_bbox = font.getbbox(text)
text_width = text_bbox[2] - text_bbox[0]
text_bbox_height = font.getbbox("hg")
text_height = text_bbox_height[3] - text_bbox_height[1]
logs.debug(text)
text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1])
logger.debug(text)
# Align text to desired position
if alignment == "center" or None:
@ -199,10 +195,13 @@ def write(image, xy, box_size, text, font=None, **kwargs):
elif alignment == "right":
x = int(box_width - text_width)
# Vertical centering
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))
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
# 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)
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 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
def internet_available():
def internet_available() -> bool:
"""checks if the internet is available.
Attempts to connect to google.com with a timeout of 5 seconds to check
@ -278,15 +277,16 @@ def internet_available():
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.
Args:
- 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)
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).
@ -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)
c7, c8 = (x, (y + height) - diameter), (x + diameter, y + height)
# Draw lines and arcs, creating a square with round corners
draw = ImageDraw.Draw(image)
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)
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)
x, y = xy

View File

@ -41,18 +41,9 @@ def get_json_from_url(request_url):
class OpenWeatherMap:
def __init__(
self,
api_key: str,
city_id: int = None,
lat: float = None,
lon: float = None,
api_version: API_VERSIONS = "2.5",
temp_unit: TEMP_UNITS = "celsius",
wind_unit: WIND_UNITS = "meters_sec",
language: str = "en",
tz_name: str = "UTC",
) -> None:
def __init__(self, api_key: str, city_id: int = None, lat: float = None, lon: float = None,
api_version: API_VERSIONS = "2.5", temp_unit: TEMP_UNITS = "celsius",
wind_unit: WIND_UNITS = "meters_sec", language: str = "en", tz_name: str = "UTC") -> None:
self.api_key = api_key
self.temp_unit = temp_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["min_temp"] = self.get_converted_temperature(current_data["main"]["temp_min"])
current_weather["max_temp"] = self.get_converted_temperature(current_data["main"]["temp_max"])
current_weather["humidity"] = current_data["main"]["humidity"] # OWM Unit: % rH
current_weather["humidity"] = current_data["main"]["humidity"] # OWM Unit: % rH
current_weather["wind"] = self.get_converted_windspeed(
current_data["wind"]["speed"]
) # OWM Unit Default: meter/sec, Metric: meter/sec
@ -161,10 +152,10 @@ class OpenWeatherMap:
forecast["wind"]["speed"]
), # OWM Unit Default: meter/sec, Metric: meter/sec, Imperial: miles/hour
"wind_gust": self.get_converted_windspeed(forecast["wind"]["gust"]),
"pressure": forecast["main"]["pressure"], # OWM Unit: hPa
"humidity": forecast["main"]["humidity"], # OWM Unit: % rH
"pressure": forecast["main"]["pressure"], # OWM Unit: hPa
"humidity": forecast["main"]["humidity"], # OWM Unit: % rH
"precip_probability": forecast["pop"]
* 100.0, # OWM value is unitless, directly converting to % scale
* 100.0, # OWM value is unitless, directly converting to % scale
"icon": forecast["weather"][0]["icon"],
"datetime": datetime.fromtimestamp(forecast["dt"], tz=self.tz_zone),
}
@ -187,7 +178,7 @@ class OpenWeatherMap:
:return:
Forecast dictionary
"""
# Make sure hourly forecasts are up to date
# Make sure hourly forecasts are up-to-date
_ = self.get_weather_forecast()
# 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
if forecasts == []:
if not forecasts:
forecasts.append(self.hourly_forecasts[0])
# Get rain and temperatures for that day

View File

@ -1,14 +1,12 @@
"""
Inkycal ePaper driving functions
Copyright by aceisace
Copyright by aceinnolab
"""
import os
from importlib import import_module
import PIL
from PIL import Image
from inkycal.custom import top_level
from inkycal.display.supported_models import supported_models
@ -199,9 +197,7 @@ class Display:
>>> Display.get_display_names()
"""
driver_files = top_level + '/inkycal/display/drivers/'
drivers = [i for i in os.listdir(driver_files) if i.endswith(".py") and not i.startswith("__") and "_" in i]
return drivers
return list(supported_models.keys())
if __name__ == '__main__':

View File

@ -2,22 +2,18 @@
10.3" driver class
Copyright by aceinnolab
"""
import os
from subprocess import run
from PIL import Image
from inkycal.custom import image_folder, top_level
from inkycal.settings import Settings
# Display resolution
EPD_WIDTH = 1872
EPD_HEIGHT = 1404
# Please insert VCOM of your display. The Minus sign before is not required
VCOM = "2.0"
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
settings = Settings()
class EPD:
@ -40,8 +36,8 @@ class EPD:
def getbuffer(self, image):
"""ad-hoc"""
image = image.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), "BMP")
command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(settings.IMAGE_FOLDER, "canvas.bmp")}'
print(command)
return command

View File

@ -2,20 +2,16 @@
7.8" parallel driver class
Copyright by aceinnolab
"""
import os
from subprocess import run
from inkycal.custom import image_folder, top_level
from inkycal.settings import Settings
# Display resolution
EPD_WIDTH = 1872
EPD_HEIGHT = 1404
# Please insert VCOM of your display. The Minus sign before is not required
VCOM = "2.0"
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
settings = Settings()
class EPD:
@ -38,8 +34,8 @@ class EPD:
def getbuffer(self, image):
"""ad-hoc"""
image = image.rotate(90, expand=True)
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), 'BMP')
command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(settings.IMAGE_FOLDER, "canvas.bmp")}'
print(command)
return command

View File

@ -2,20 +2,16 @@
9.7" driver class
Copyright by aceinnolab
"""
import os
from subprocess import run
from inkycal.custom import image_folder, top_level
from inkycal.settings import Settings
# Display resolution
EPD_WIDTH = 1200
EPD_HEIGHT = 825
# Please insert VCOM of your display. The Minus sign before is not required
VCOM = "2.0"
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
settings = Settings()
class EPD:
@ -38,8 +34,8 @@ class EPD:
def getbuffer(self, image):
"""ad-hoc"""
image = image.rotate(90, expand=True)
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), "BMP")
command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(settings.IMAGE_FOLDER, "canvas.bmp")}'
print(command)
return command

View 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()

View 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()

View File

@ -28,8 +28,6 @@ THE SOFTWARE.
"""
import logging
import os
import subprocess
import sys
import time
@ -128,4 +126,3 @@ implementation = RaspberryPi()
for func in [x for x in dir(implementation) if not x.startswith('_')]:
setattr(sys.modules[__name__], func, getattr(implementation, func))

View File

@ -1,4 +1,6 @@
supported_models = {
"epd_13_in_3": (960, 680),
"epd_13_in_3_colour": (960, 680),
"epd_12_in_48": (1304, 984),
"epd_7_in_5_colour": (640, 384),
"9_in_7": (1200, 825),

35
inkycal/loggers.py Normal file
View 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)

View File

@ -6,44 +6,22 @@ Copyright by aceinnolab
import asyncio
import glob
import hashlib
from logging.handlers import RotatingFileHandler
import os.path
import numpy
from inkycal import loggers # noqa
from inkycal.custom import *
from inkycal.display import Display
from inkycal.modules.inky_image import Inkyimage as Images
# 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)
from inkycal.utils import JSONCache
logger = logging.getLogger(__name__)
settings = Settings()
CACHE_NAME = "inkycal_main"
# TODO: autostart -> supervisor?
class Inkycal:
"""Inkycal main class
@ -60,43 +38,61 @@ class Inkycal:
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):
"""Initialise Inkycal"""
def __init__(self, settings_path: str or None = None, render: bool = True, use_pi_sugar: bool = False,
shutdown_after_run: bool = False) -> None:
"""Initialise Inkycal
# Get the release version from setup.py
with open(f'{top_level}/setup.py') as setup_file:
for line in setup_file:
if line.startswith('__version__'):
self._release = line.split("=")[-1].replace("'", "").replace('"', "").replace(" ", "")
break
Args:
settings_path (str):
The full path to your settings.json file. If no path was specified, will look in the /boot directory.
render (bool):
Show the image on the E-Paper display.
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.info = None
logger.info("Checking if a settings file is present...")
# load settings file - throw an error if file could not be found
if settings_path:
logger.info(f"Custom location for settings.json file specified: {settings_path}")
try:
with open(settings_path) as settings_file:
settings = json.load(settings_file)
self.settings = settings
with open(settings_path, mode="r") as settings_file:
self.settings = json.load(settings_file)
except FileNotFoundError:
raise FileNotFoundError(
f"No settings.json file could be found in the specified location: {settings_path}")
else:
try:
with open('/boot/settings.json') as settings_file:
settings = json.load(settings_file)
self.settings = settings
except FileNotFoundError:
raise SettingsFileNotFoundError
found = False
for location in settings.SETTINGS_JSON_PATHS:
if os.path.exists(location):
logger.info(f"Found settings.json file in {location}")
with open(location, mode="r") as settings_file:
self.settings = json.load(settings_file)
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)
if self.disable_calibration:
logger.info("Calibration disabled. Please proceed with caution to prevent ghosting.")
if not os.path.exists(image_folder):
os.mkdir(image_folder)
if not os.path.exists(settings.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
self.optimize = True
@ -109,10 +105,10 @@ class Inkycal:
if self.render:
# Init Display class with model in settings file
# from inkycal.display import Display
self.Display = Display(settings["model"])
self.Display = Display(self.settings["model"])
# 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
self._calibration_hours = self.settings['calibration_hours']
@ -122,7 +118,7 @@ class Inkycal:
# Load and initialise modules specified in the settings file
self._module_number = 1
for module in settings['modules']:
for module in self.settings['modules']:
module_name = module['name']
try:
loader = f'from inkycal.modules import {module_name}'
@ -131,10 +127,9 @@ class Inkycal:
setup = f'self.module_{self._module_number} = {module_name}({module})'
# print(setup)
exec(setup)
logger.info(('name : {name} size : {width}x{height} px'.format(
name=module_name,
width=module['config']['size'][0],
height=module['config']['size'][1])))
width = module['config']['size'][0]
height = module['config']['size'][1]
logger.info(f'name : {module_name} size : {width}x{height} px')
self._module_number += 1
@ -146,58 +141,85 @@ class Inkycal:
except:
logger.exception(f"Exception: {traceback.format_exc()}.")
# Path to store images
self.image_folder = image_folder
# 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
print('loaded inkycal')
logger.info('Inkycal initialised successfully!')
def countdown(self, interval_mins: int or None = None) -> int:
"""Returns the remaining time in seconds until next display update.
def countdown(self, interval_mins: int = None) -> int:
"""Returns the remaining time in seconds until the next display update based on the interval.
Args:
- interval_mins = int -> the interval in minutes for the update
if no interval is given, the value from the settings file is used.
interval_mins (int): The interval in minutes for the update. If none is given, the value
from the settings file is used.
Returns:
- int -> the remaining time in seconds until next update
int: The remaining time in seconds until the next update.
"""
# Check if empty, if empty, use value from settings file
# Default to settings if no interval is provided
if interval_mins is None:
interval_mins = self.settings["update_interval"]
# Find out at which minutes the update should happen
# Get the current time
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
minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute
# Calculate the next update time
# 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
print(f'{minutes} minutes left until next refresh')
# Finding the next interval point
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
remaining_time = minutes * 60 + (60 - now.second)
# Return seconds until next update
return remaining_time
# Logging the remaining time in appropriate units
hours_to_next_interval = minutes_to_next_interval // 60
remaining_minutes = minutes_to_next_interval % 60
if hours_to_next_interval > 0:
print(f'{hours_to_next_interval} hours and {remaining_minutes} minutes left until next refresh')
else:
# Calculate time in minutes until next update using the range of 24 hours in steps of every full hour
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'{remaining_minutes} minutes left until next refresh')
print(f'{round(minutes / 60, 1)} hours left until next refresh')
return seconds_to_next_interval
# Return seconds until next update
return remaining_time
def test(self):
def dry_run(self):
"""Tests if Inkycal can run without issues.
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.
"""
logger.info(f"Inkycal version: v{self._release}")
logger.info(f'Selected E-paper display: {self.settings["model"]}')
# store module numbers in here
@ -218,20 +238,13 @@ class Inkycal:
for number in range(1, self._module_number):
name = eval(f"self.module_{number}.name")
module = eval(f'self.module_{number}')
print(f'generating image(s) for {name}...', end="")
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")
print("OK!")
except Exception:
success = self.process_module(number)
if success:
logger.debug(f'Image of module {name} generated successfully')
else:
logger.warning(f'Generating image of module {name} failed!')
errors.append(number)
self.info += f"module {number}: Error! "
logger.exception("Error!")
logger.exception(f"Exception: {traceback.format_exc()}.")
if errors:
logger.error('Error/s in modules:', *errors)
@ -277,98 +290,89 @@ class Inkycal:
print("Refresh needed: {a}".format(a=res))
return res
async def run(self):
"""Runs main program in nonstop mode.
async def run(self, run_once=False):
"""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
from all modules, assembles them in one image, refreshed the E-Paper and
then sleeps until the next scheduled update.
Args:
run_once (bool): If True, runs the updating process once and stops. If False,
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
runtime = arrow.now()
# Function to flip images upside down
upside_down = lambda image: image.rotate(180, expand=True)
# Count the number of times without any errors
counter = 0
print(f'Inkycal version: v{self._release}')
print(f'Selected E-paper display: {self.settings["model"]}')
logger.info(f'Inkycal version: v{self._release}')
logger.info(f'Selected E-paper display: {self.settings["model"]}')
while True:
logger.info("Starting new cycle...")
current_time = arrow.now(tz=get_system_tz())
print(f"Date: {current_time.format('D MMM YY')} | "
f"Time: {current_time.format('HH:mm')}")
print('Generating images for all modules...', end='')
logger.info(f"Timestamp: {current_time.format('HH:mm:ss DD.MM.YYYY')}")
self.cache_data["counter"] = self.counter
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):
self.info = f"{current_time.format('D MMM @ HH:mm')} "
else:
self.info = ""
for number in range(1, self._module_number):
# name = eval(f"self.module_{number}.name")
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:
success = self.process_module(number)
if not success:
errors.append(number)
self.info += f"module {number}: Error! "
logger.exception("Error!")
logger.exception(f"Exception: {traceback.format_exc()}.")
self.info += f"im {number}: X "
if errors:
logger.error("Error/s in modules:", *errors)
counter = 0
self.counter = 0
self.cache_data["counter"] = 0
else:
counter += 1
logger.info("successful")
self.counter += 1
self.cache_data["counter"] += 1
logger.info("All images generated successfully!")
del errors
if self.battery_capacity < 20:
self.info += "Low battery! "
# Assemble image from each module - add info section if specified
self._assemble()
# Check if image should be rendered
if self.render:
logger.info("Attempting to render image on display...")
display = self.Display
self._calibration_check()
if self._calibration_state:
# after calibration, we have to forcefully rewrite the screen
self._remove_hashes(self.image_folder)
# After calibration, we have to forcefully rewrite the screen
self._remove_hashes(settings.IMAGE_FOLDER)
if self.supports_colour:
im_black = Image.open(f"{self.image_folder}canvas.png")
im_colour = Image.open(f"{self.image_folder}canvas_colour.png")
im_black = Image.open(os.path.join(settings.IMAGE_FOLDER, "canvas.png"))
im_colour = Image.open(os.path.join(settings.IMAGE_FOLDER, "canvas_colour.png"))
# Flip the image by 180° if required
if self.settings['orientation'] == 180:
im_black = upside_down(im_black)
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([
(f"{self.image_folder}/canvas.png.hash", im_black),
(f"{self.image_folder}/canvas_colour.png.hash", im_colour)
(f"{settings.IMAGE_FOLDER}/canvas.png.hash", im_black),
(f"{settings.IMAGE_FOLDER}/canvas_colour.png.hash", im_colour)
]):
# render the image on the display
display.render(im_black, im_colour)
# Part for black-white ePapers
elif not self.supports_colour:
else:
im_black = self._merge_bands()
# Flip the image by 180° if required
@ -376,14 +380,34 @@ class Inkycal:
im_black = upside_down(im_black)
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)
print(f'\nNo errors since {counter} display updates \n'
f'program started {runtime.humanize()}')
logger.info(f'No errors since {self.counter} display updates')
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()
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)
@staticmethod
@ -392,7 +416,8 @@ class Inkycal:
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 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):
# get the path of the current module's generated images
im1_path = f"{self.image_folder}module{number}_black.png"
im2_path = f"{self.image_folder}module{number}_colour.png"
im1_path = os.path.join(settings.IMAGE_FOLDER, f"module{number}_black.png")
im2_path = os.path.join(settings.IMAGE_FOLDER, f"module{number}_colour.png")
# Check if there is an image for the black band
if os.path.exists(im1_path):
@ -501,8 +526,8 @@ class Inkycal:
im_black = self._optimize_im(im_black)
im_colour = self._optimize_im(im_colour)
im_black.save(self.image_folder + 'canvas.png', 'PNG')
im_colour.save(self.image_folder + 'canvas_colour.png', 'PNG')
im_black.save(os.path.join(settings.IMAGE_FOLDER, "canvas.png"), "PNG")
im_colour.save(os.path.join(settings.IMAGE_FOLDER, "canvas_colour.png"), 'PNG')
# Additionally, combine the two images with color
def clear_white(img):
@ -531,7 +556,7 @@ class Inkycal:
im_colour = black_to_colour(im_colour)
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
def _optimize_im(image, threshold=220):
@ -574,13 +599,40 @@ class Inkycal:
@staticmethod
def cleanup():
# 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:
os.remove(_file)
except:
logger.error(f"could not remove file: {_file}")
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__':
print(f'running inkycal main in standalone/debug mode')

View File

@ -156,7 +156,7 @@ class Simple(inkycal_module):
# -----------------------------------------------------------------------#
# give an OK message
print(f'{__name__} loaded')
logger.debug(f'{__name__} loaded')
#############################################################################
# Validation of module specific parameters (optional) #

View File

@ -27,7 +27,7 @@ class Inkyimage:
self.image = image
# give an OK message
logger.info(f"{__name__} loaded")
logger.debug(f"{__name__} loaded")
def load(self, path: str) -> None:
"""loads an image from a URL or filepath.
@ -59,7 +59,7 @@ class Inkyimage:
logger.error("Invalid Image file provided", exc_info=True)
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
self.image = image

View File

@ -2,9 +2,7 @@
Inkycal Agenda Module
Copyright by aceinnolab
"""
import arrow
import arrow # noqa
from inkycal.custom import *
from inkycal.modules.ical_parser import iCalendar
from inkycal.modules.template import inkycal_module
@ -77,8 +75,10 @@ class Agenda(inkycal_module):
# Additional config
self.timezone = get_system_tz()
self.icon_font = ImageFont.truetype(fonts['MaterialIcons'], size=self.fontsize)
# give an OK message
print(f'{__name__} loaded')
logger.debug(f'{__name__} loaded')
def generate_image(self):
"""Generate image for this module"""
@ -88,7 +88,7 @@ class Agenda(inkycal_module):
im_height = int(self.height - (2 * self.padding_top))
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
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]),
(time_width, line_height), time,
font=self.font, alignment='right')
if parser.all_day(_):
else:
write(im_black, (x_time, line_pos[cursor][1]),
(time_width, line_height), "all day",
font=self.font, alignment='right')
(time_width, line_height), "\ue878",
font=self.icon_font, alignment='right')
write(im_black, (x_event, line_pos[cursor][1]),
(event_width, line_height),

View File

@ -6,16 +6,16 @@ Copyright by aceinnolab
# pylint: disable=logging-fstring-interpolation
import calendar as cal
import arrow
from inkycal.modules.template import inkycal_module
from inkycal.custom import *
from inkycal.modules.template import inkycal_module
logger = logging.getLogger(__name__)
class Calendar(inkycal_module):
"""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"
@ -39,12 +39,12 @@ class Calendar(inkycal_module):
},
"date_format": {
"label": "Use an arrow-supported token for custom date formatting "
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM",
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM",
"default": "D MMM",
},
"time_format": {
"label": "Use an arrow-supported token for custom time formatting "
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
"default": "HH:mm",
},
}
@ -61,7 +61,7 @@ class Calendar(inkycal_module):
self._days_with_events = None
# optional parameters
self.weekstart = config['week_starts_on']
self.week_start = config['week_starts_on']
self.show_events = config['show_events']
self.date_format = config["date_format"]
self.time_format = config['time_format']
@ -84,7 +84,7 @@ class Calendar(inkycal_module):
)
# give an OK message
print(f'{__name__} loaded')
logger.debug(f'{__name__} loaded')
@staticmethod
def flatten(values):
@ -100,7 +100,7 @@ class Calendar(inkycal_module):
im_size = im_width, im_height
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
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.
month_name_height = int(im_height * 0.10)
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"weekdays_height: {weekdays_height}")
@ -117,7 +117,7 @@ class Calendar(inkycal_module):
logger.debug("Allocating space for events")
calendar_height = int(im_height * 0.6)
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'events-section size: {im_width} x {events_height} px')
@ -156,13 +156,13 @@ class Calendar(inkycal_module):
now = arrow.now(tz=self.timezone)
# Set weekstart of calendar to specified weekstart
if self.weekstart == "Monday":
# Set week-start of calendar to specified week-start
if self.week_start == "Monday":
cal.setfirstweekday(cal.MONDAY)
weekstart = now.shift(days=-now.weekday())
week_start = now.shift(days=-now.weekday())
else:
cal.setfirstweekday(cal.SUNDAY)
weekstart = now.shift(days=-now.isoweekday())
week_start = now.shift(days=-now.isoweekday())
# Write the name of current month
write(
@ -174,9 +174,9 @@ class Calendar(inkycal_module):
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 = [
weekstart.shift(days=+_).format('ddd', locale=self.language)
week_start.shift(days=+_).format('ddd', locale=self.language)
for _ in range(7)
]
logger.debug(f'weekday names: {weekday_names}')
@ -192,7 +192,7 @@ class Calendar(inkycal_module):
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))
# 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
line_spacing = 2
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)
# generate list of coordinates for each line
@ -281,7 +281,7 @@ class Calendar(inkycal_module):
month_start = arrow.get(now.floor('month'))
month_end = arrow.get(now.ceil('month'))
# fetch events from given icalendars
# fetch events from given iCalendars
self.ical = iCalendar()
parser = self.ical
@ -294,14 +294,12 @@ class Calendar(inkycal_module):
month_events = parser.get_events(month_start, month_end, self.timezone)
parser.sort()
self.month_events = month_events
# Initialize days_with_events as an empty list
days_with_events = []
# Handle multi-day events by adding all days between start and end
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
start = arrow.get(event['begin'].date(), tzinfo=self.timezone)
@ -324,9 +322,7 @@ class Calendar(inkycal_module):
im_colour,
grid[days],
(icon_width, icon_height),
radius=6,
thickness=1,
shrinkage=(0.4, 0.2),
radius=6
)
# Filter upcoming events until 4 weeks in the future
@ -345,13 +341,13 @@ class Calendar(inkycal_module):
date_width = int(max((
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((
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")
line_height = text_bbox_height[3] + line_spacing
@ -369,7 +365,8 @@ class Calendar(inkycal_module):
event_duration = (event['end'] - event['begin']).days
if event_duration > 1:
# 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})"
else:
the_name = event['title']

View File

@ -60,7 +60,7 @@ class Feeds(inkycal_module):
self.shuffle_feeds = config["shuffle_feeds"]
# give an OK message
print(f'{__name__} loaded')
logger.debug(f'{__name__} loaded')
def _validate(self):
"""Validate module-specific parameters"""
@ -75,7 +75,7 @@ class Feeds(inkycal_module):
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'Image size: {im_size}')
logger.debug(f'Image size: {im_size}')
# Create an image for black pixels and one for coloured pixels
im_black = Image.new('RGB', size=im_size, color='white')
@ -83,8 +83,9 @@ class Feeds(inkycal_module):
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
logger.debug('Connection test passed')
else:
logger.error("Network not reachable. Please check your connection.")
raise NetworkNotReachableError
# Set some parameters for formatting feeds

View File

@ -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 get_system_tz
from inkycal.custom.functions import internet_available
from inkycal.custom.functions import top_level
from inkycal.custom.inkycal_exceptions import NetworkNotReachableError
from inkycal.custom.openweathermap_wrapper import OpenWeatherMap
from inkycal.modules.inky_image import image_to_palette
from inkycal.modules.template import inkycal_module
from inkycal.settings import Settings
logger = logging.getLogger(__name__)
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:
@ -139,7 +141,7 @@ class Fullweather(inkycal_module):
# Check if all required parameters are present
for param in self.requires:
if not param in config:
if param not in config:
raise Exception(f"config is missing {param}")
# required parameters
@ -237,7 +239,7 @@ class Fullweather(inkycal_module):
self.left_section_width = int(self.width / 4)
# give an OK message
print(f"{__name__} loaded")
logger.debug(f"{__name__} loaded")
def createBaseImage(self):
"""

View File

@ -50,7 +50,7 @@ class Inkyimage(inkycal_module):
self.dither = False
# give an OK message
print(f"{__name__} loaded")
logger.debug(f"{__name__} loaded")
def generate_image(self):
"""Generate image for this module"""
@ -71,7 +71,7 @@ class Inkyimage(inkycal_module):
# Remove background if present
im.remove_alpha()
# if autoflip was enabled, flip the image
# if auto-flip was enabled, flip the image
if self.autoflip:
im.autoflip(self.orientation)

View File

@ -30,7 +30,7 @@ class Jokes(inkycal_module):
config = config['config']
# give an OK message
print(f'{__name__} loaded')
logger.debug(f'{__name__} loaded')
def generate_image(self):
"""Generate image for this module"""
@ -39,7 +39,7 @@ class Jokes(inkycal_module):
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.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
im_black = Image.new('RGB', size=im_size, color='white')
@ -47,8 +47,9 @@ class Jokes(inkycal_module):
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
logger.debug('Connection test passed')
else:
logger.error("Network not reachable. Please check your connection.")
raise NetworkNotReachableError
# Set some parameters for formatting feeds

View File

@ -67,7 +67,7 @@ class Inkyserver(inkycal_module):
self.path_body = config['path_body']
# give an OK message
print(f'{__name__} loaded')
logger.debug(f'{__name__} loaded')
def generate_image(self):
"""Generate image for this module"""

View File

@ -8,13 +8,13 @@ from inkycal.custom import *
# 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.template import inkycal_module
from inkycal.utils import JSONCache
logger = logging.getLogger(__name__)
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"
requires = {
@ -53,7 +53,7 @@ class Slideshow(inkycal_module):
# required parameters
for param in self.requires:
if not param in config:
if param not in config:
raise Exception(f'config is missing {param}')
# optional parameters
@ -64,19 +64,20 @@ class Slideshow(inkycal_module):
# Get the full path of all png/jpg/jpeg images in the given folder
all_files = glob.glob(f'{self.path}/*')
self.images = [i for i in all_files
if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
self.images = [i for i in all_files if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
if not self.images:
logger.error('No images found in the given folder, please '
'double check your path!')
logger.error('No images found in the given folder, please double check your 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
self._first_run = True
# give an OK message
print(f'{__name__} loaded')
logger.debug(f'{__name__} loaded')
def generate_image(self):
"""Generate image for this module"""
@ -86,17 +87,19 @@ class Slideshow(inkycal_module):
im_height = int(self.height - (2 * self.padding_top))
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
def rotate(somelist):
return somelist[1:] + somelist[:1]
def rotate(list: list):
return list[1:] + list[:1]
# Switch to the next image if this is not the first run
if self._first_run:
self._first_run = False
self.cache_data["current_index"] = 0
else:
self.images = rotate(self.images)
self.cache_data["current_index"] = (self.cache_data["current_index"] + 1) % len(self.images)
# initialize custom image class
im = Images()
@ -110,7 +113,7 @@ class Slideshow(inkycal_module):
# Remove background if present
im.remove_alpha()
# if autoflip was enabled, flip the image
# if auto-flip was enabled, flip the image
if self.autoflip:
im.autoflip(self.orientation)
@ -123,6 +126,8 @@ class Slideshow(inkycal_module):
# with the images now send, clear the current image
im.clear()
self.cache.write(self.cache_data)
# return images
return im_black, im_colour

View File

@ -54,7 +54,7 @@ class Stocks(inkycal_module):
self.tickers = config['tickers']
# give an OK message
print(f'{__name__} loaded')
logger.debug(f'{__name__} loaded')
def generate_image(self):
"""Generate image for this module"""
@ -63,7 +63,7 @@ class Stocks(inkycal_module):
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.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)
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 "
"default precision of 2 instead.")
stockHistory = yfTicker.history("30d")
stockHistory = yfTicker.history("1mo")
stockHistoryLen = len(stockHistory)
logger.info(f'fetched {stockHistoryLen} datapoints ...')
previousQuote = (stockHistory.tail(2)['Close'].iloc[0])

View File

@ -31,7 +31,7 @@ class TextToDisplay(inkycal_module):
self.make_request = True if self.filepath.startswith("https://") else False
# give an OK message
print(f'{__name__} loaded')
logger.debug(f'{__name__} loaded')
def _validate(self):
"""Validate module-specific parameters"""
@ -45,7 +45,7 @@ class TextToDisplay(inkycal_module):
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'Image size: {im_size}')
logger.debug(f'Image size: {im_size}')
# Create an image for black pixels and one for coloured pixels
im_black = Image.new('RGB', size=im_size, color='white')

View File

@ -32,7 +32,7 @@ class Tindie(inkycal_module):
# self.mode = config['mode'] # unshipped_orders, shipped_orders, all_orders
# give an OK message
print(f'{__name__} loaded')
logger.debug(f'{__name__} loaded')
def generate_image(self):
"""Generate image for this module"""
@ -40,7 +40,7 @@ class Tindie(inkycal_module):
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.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
im_black = Image.new('RGB', size=im_size, color='white')
@ -50,6 +50,7 @@ class Tindie(inkycal_module):
if internet_available():
logger.info('Connection test passed')
else:
logger.error("Network not reachable. Please check your connection.")
raise NetworkNotReachableError
# Set some parameters for formatting feeds

View File

@ -56,7 +56,7 @@ class Todoist(inkycal_module):
self._api = TodoistAPI(config['api_key'])
# give an OK message
print(f'{__name__} loaded')
logger.debug(f'{__name__} loaded')
def _validate(self):
"""Validate module-specific parameters"""
@ -70,7 +70,7 @@ class Todoist(inkycal_module):
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'Image size: {im_size}')
logger.debug(f'Image size: {im_size}')
# Create an image for black pixels and one for coloured pixels
im_black = Image.new('RGB', size=im_size, color='white')
@ -80,6 +80,7 @@ class Todoist(inkycal_module):
if internet_available():
logger.info('Connection test passed')
else:
logger.error("Network not reachable. Please check your connection.")
raise NetworkNotReachableError
# Set some parameters for formatting todos

View File

@ -2,12 +2,12 @@
Inkycal weather module
Copyright by aceinnolab
"""
import arrow
import decimal
import logging
import math
from typing import Tuple
import arrow
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
@ -51,7 +51,7 @@ class Weather(inkycal_module):
"options": [True, False],
},
"round_windspeed": {
"round_wind_speed": {
"label": "Round windspeed?",
"options": [True, False],
},
@ -89,7 +89,7 @@ class Weather(inkycal_module):
# Check if all required parameters are present
for param in self.requires:
if not param in config:
if param not in config:
raise Exception(f'config is missing {param}')
# required parameters
@ -98,15 +98,15 @@ class Weather(inkycal_module):
# optional parameters
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.hour_format = int(config['hour_format'])
if config['units'] == "imperial":
self.temp_unit = "fahrenheit"
else:
self.temp_unit = "celsius"
if config['use_beaufort'] == True:
if config['use_beaufort']:
self.wind_unit = "beaufort"
elif config['units'] == "imperial":
self.wind_unit = "miles_hour"
@ -116,17 +116,17 @@ class Weather(inkycal_module):
# additional configuration
self.owm = OpenWeatherMap(
api_key=self.api_key,
city_id=self.location,
wind_unit=self.wind_unit,
api_key=self.api_key,
city_id=self.location,
wind_unit=self.wind_unit,
temp_unit=self.temp_unit,
language=self.locale,
language=self.locale,
tz_name=self.timezone
)
)
self.weatherfont = ImageFont.truetype(
fonts['weathericons-regular-webfont'], size=self.fontsize)
if self.wind_unit == "beaufort":
self.windDispUnit = "bft"
elif self.wind_unit == "knots":
@ -143,9 +143,7 @@ class Weather(inkycal_module):
self.tempDispUnit = "°"
# give an OK message
print(f"{__name__} loaded")
logger.debug(f"{__name__} loaded")
def generate_image(self):
"""Generate image for this module"""
@ -154,7 +152,7 @@ class Weather(inkycal_module):
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'Image size: {im_size}')
logger.debug(f'Image size: {im_size}')
# Create an image for black pixels and one for coloured pixels
im_black = Image.new('RGB', size=im_size, color='white')
@ -162,8 +160,9 @@ class Weather(inkycal_module):
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
logger.debug('Connection test passed')
else:
logger.error("Network not reachable. Please check your connection.")
raise NetworkNotReachableError
def get_moon_phase():
@ -190,7 +189,7 @@ class Weather(inkycal_module):
7: '\uf0ae'
}[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)
returns True if temp below freezing point, else False"""
answer = False
@ -223,12 +222,19 @@ class Weather(inkycal_module):
'50n': '\uf023'
}
def draw_icon(image, xy, box_size, icon, rotation=None):
"""Custom function to add icons of weather font on image
image = on which image should the text be added?
xy = xy-coordinates as tuple -> (x,y)
box_size = size of text-box -> (width,height)
icon = icon-unicode, looks this up in weathericons dictionary
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 the image.
Args:
- image:
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 = {
@ -263,7 +269,6 @@ class Weather(inkycal_module):
'\uf0a0': 0,
'\uf0a3': 0,
'\uf0a7': 0,
'\uf0aa': 0,
'\uf0ae': 0
}
@ -277,8 +282,7 @@ class Weather(inkycal_module):
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getbbox(text)[2:]
while (text_width < int(box_width * 0.9) and
text_height < int(box_height * 0.9)):
while text_width < int(box_width * 0.9) and text_height < int(box_height * 0.9):
size += 1
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getbbox(text)[2:]
@ -289,8 +293,6 @@ class Weather(inkycal_module):
x = int((box_width / 2) - (text_width / 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))
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
# Draw lines on each row and border
############################################################################
## draw = ImageDraw.Draw(im_black)
## 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, row1, im_width, row1), 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+row_height, im_width, row2+row_height), 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 = ImageDraw.Draw(im_black)
# 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, row1, im_width, row1), 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+row_height, im_width, row2+row_height), fill='black')
# draw.line((0, row3, im_width, row3), fill='black')
# draw.line((0, row3+row_height, im_width, row3+row_height), fill='black')
###########################################################################
# Positions for current weather details
weather_icon_pos = (col1, 0)
@ -378,24 +380,24 @@ class Weather(inkycal_module):
sunset_time_pos = (col3 + icon_small, row3)
# Positions for forecast 1
stamp_fc1 = (col4, row1)
icon_fc1 = (col4, row1 + row_height)
temp_fc1 = (col4, row3)
stamp_fc1 = (col4, row1) # noqa
icon_fc1 = (col4, row1 + row_height) # noqa
temp_fc1 = (col4, row3) # noqa
# Positions for forecast 2
stamp_fc2 = (col5, row1)
icon_fc2 = (col5, row1 + row_height)
temp_fc2 = (col5, row3)
stamp_fc2 = (col5, row1) # noqa
icon_fc2 = (col5, row1 + row_height) # noqa
temp_fc2 = (col5, row3) # noqa
# Positions for forecast 3
stamp_fc3 = (col6, row1)
icon_fc3 = (col6, row1 + row_height)
temp_fc3 = (col6, row3)
stamp_fc3 = (col6, row1) # noqa
icon_fc3 = (col6, row1 + row_height) # noqa
temp_fc3 = (col6, row3) # noqa
# Positions for forecast 4
stamp_fc4 = (col7, row1)
icon_fc4 = (col7, row1 + row_height)
temp_fc4 = (col7, row3)
stamp_fc4 = (col7, row1) # noqa
icon_fc4 = (col7, row1 + row_height) # noqa
temp_fc4 = (col7, row3) # noqa
# Create current-weather and weather-forecast objects
logging.debug('looking up location by ID')
@ -404,7 +406,7 @@ class Weather(inkycal_module):
# Set decimals
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'decimals temperature: {dec_temp} | decimals wind: {dec_wind}')
@ -424,7 +426,8 @@ class Weather(inkycal_module):
fc_data['fc' + str(index + 1)] = {
'temp': f"{forecast['temp']:.{dec_temp}f}{self.tempDispUnit}",
'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':
@ -433,7 +436,7 @@ class Weather(inkycal_module):
daily_forecasts = [self.owm.get_forecast_for_day(days) for days in range(1, 5)]
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}',
'icon': forecast['icon'],
'stamp': forecast['datetime'].strftime("%A")
@ -513,6 +516,9 @@ class Weather(inkycal_module):
# Add the forecast data to the correct places
for pos in range(1, len(fc_data) + 1):
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']]
temp = fc_data[f'fc{pos}']['temp']

View File

@ -40,7 +40,10 @@ class Webshot(inkycal_module):
},
"crop_h": {
"label": "Please enter the crop height",
}
},
"rotation": {
"label": "Please enter the rotation. Must be either 0, 90, 180 or 270",
},
}
def __init__(self, config):
@ -72,8 +75,14 @@ class Webshot(inkycal_module):
else:
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
print(f'Inkycal webshot loaded')
logger.debug(f'Inkycal webshot loaded')
def generate_image(self):
"""Generate image for this module"""
@ -89,7 +98,7 @@ class Webshot(inkycal_module):
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.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)
im_black = Image.new('RGB', size=im_size, color='white')
@ -99,12 +108,13 @@ class Webshot(inkycal_module):
if internet_available():
logger.info('Connection test passed')
else:
logger.error("Network not reachable. Please check your connection.")
raise Exception('Network could not be reached :/')
logger.info(
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 = {
"--crop-x": self.crop_x,
@ -150,11 +160,21 @@ class Webshot(inkycal_module):
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))
im_colour.paste(webshotSpaceColour)
if self.rotation != 0:
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()
logger.info(f'added webshot image')

View File

@ -11,6 +11,8 @@ from inkycal.modules.template import inkycal_module
logger = logging.getLogger(__name__)
settings = Settings()
class Xkcd(inkycal_module):
name = "xkcd - Displays comics from xkcd.com by Randall Munroe"
@ -51,13 +53,13 @@ class Xkcd(inkycal_module):
self.scale_filter = config['filter']
# give an OK message
print(f'Inkycal XKCD loaded')
logger.debug(f'Inkycal XKCD loaded')
def generate_image(self):
"""Generate image for this module"""
# Create tmp path
tmpPath = f"{top_level}/temp"
tmpPath = settings.TEMPORARY_FOLDER
if not os.path.exists(tmpPath):
os.mkdir(tmpPath)
@ -66,7 +68,7 @@ class Xkcd(inkycal_module):
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.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)
im_black = Image.new('RGB', size=im_size, color='white')
@ -76,6 +78,7 @@ class Xkcd(inkycal_module):
if internet_available():
logger.info('Connection test passed')
else:
logger.error("Network not reachable. Please check your connection.")
raise Exception('Network could not be reached :/')
# Set some parameters for formatting feeds

22
inkycal/settings.py Normal file
View 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"]

View File

@ -0,0 +1,2 @@
from .pisugar import PiSugar
from .json_cache import JSONCache

View 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
View 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

View File

@ -2,7 +2,7 @@ appdirs==1.4.4
arrow==1.3.0
asyncio==3.4.3
beautifulsoup4==4.12.3
certifi==2024.2.2
certifi==2024.7.4
cfgv==3.4.0
charset-normalizer==3.3.2
colorzero==2.0
@ -37,7 +37,7 @@ python-dotenv==1.0.1
pytz==2024.1
PyYAML==6.0.1
recurring-ical-events==2.1.2
requests==2.32.0
requests==2.32.3
sgmllib3k==1.0.0
six==1.16.0
soupsieve==2.5
@ -46,9 +46,9 @@ types-python-dateutil==2.8.19.20240106
typing_extensions==4.9.0
tzdata==2024.1
tzlocal==5.2
urllib3==2.2.0
urllib3==2.2.2
virtualenv==20.25.0
webencodings==0.5.1
x-wr-timezone==0.0.6
xkcd==2.4.2
yfinance==0.2.36
yfinance==0.2.40

View File

@ -13,10 +13,10 @@ with open('requirements.txt') as f:
required = [i.split(' ')[0] for i in required]
__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"
__packages__ = ["inkycal"]
__author__ = "aceisace"
__author__ = "aceinnolab"
__author_email__ = "aceisace63@yahoo.com"
__url__ = "https://github.com/aceinnolab/Inkycal"

View File

@ -1,12 +1,22 @@
"""
Test the functions in the functions module.
"""
import unittest
from PIL import Image, ImageFont
from inkycal.custom import write, fonts
from inkycal.custom import write, fonts, get_system_tz
def test_write():
im = Image.new("RGB", (500, 200), "white")
font = ImageFont.truetype(fonts['NotoSans-SemiCondensed'], size = 40)
write(im, (125,75), (250, 50), "Hello World", font)
# im.show()
class TestIcalendar(unittest.TestCase):
def test_write(self):
im = Image.new("RGB", (500, 200), "white")
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)

View File

@ -28,7 +28,7 @@ tests = [
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
"language": "de"
}
},
{
@ -37,7 +37,7 @@ tests = [
"size": [500, 800],
"ical_urls": sample_url,
"ical_files": None,
"date_format": "ddd D MMM",
"date_format": "DD.MMMM YYYY",
"time_format": "HH:mm",
"padding_x": 10,
"padding_y": 10,

View File

@ -20,7 +20,7 @@ tests = [
{
"name": "Calendar",
"config": {
"size": [500, 500],
"size": [500, 600],
"week_starts_on": "Monday",
"show_events": True,
"ical_urls": sample_url,

View File

@ -30,11 +30,11 @@ tests = [
"forecast_interval": "daily",
"units": "metric",
"hour_format": "12",
"use_beaufort": True,
"use_beaufort": False,
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
"language": "de"
}
},
{

View File

@ -6,38 +6,23 @@ import logging
import unittest
from inkycal.modules import Webshot
from inkycal.modules.inky_image import Inkyimage
from tests import Config
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
preview = Inkyimage.preview
merge = Inkyimage.merge
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,
"name": "Webshot",
"config": {
"size": [400, 200],
"url": "https://github.com",
"palette": "bwy",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"position": 1,
"name": "Webshot",
"config": {
"size": [400, 300],
"url": "https://github.com",
"palette": "bw",
"url": "https://aceinnolab.com",
"palette": "bwr",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
@ -46,8 +31,31 @@ tests = [
"name": "Webshot",
"config": {
"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",
"rotation": 180,
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
}
@ -60,6 +68,7 @@ class TestWebshot(unittest.TestCase):
for test in tests:
logger.info(f'test {tests.index(test) + 1} generating image..')
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')

View File

@ -21,9 +21,9 @@ class TestMain(unittest.TestCase):
assert inkycal.settings["info_section_height"] == 70
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.test()
inkycal.dry_run()
def test_countdown(self):
inkycal = Inkycal(self.settings_path, render=False)