Merge branch 'main' into weather_scaling

This commit is contained in:
mrbwburns 2023-11-26 17:16:03 +01:00 committed by GitHub
commit a7c3edc60e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1360 additions and 894 deletions

View File

@ -6,6 +6,7 @@
"target": "development"
},
// This is the settings.json mount
"mounts": ["source=/c/temp/settings_test.json,target=/boot/settings.json,type=bind,consistency=cached"],

View File

@ -1,26 +1,26 @@
# Contributor Covenant Code of Conduct
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
Examples of unacceptable behavior include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct that could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
@ -30,17 +30,16 @@ Project maintainers have the right and responsibility to remove, edit, or reject
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at aceisace63@yahoo.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [this email](inkycal@aceinnolab.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq.

View File

@ -1,31 +1,49 @@
# Inkycal Contribution Policy
# Contributing to Inkycal
Thanks for willing to contribute to Inkycal
We welcome all sort of contributions, for example:
* giving support via the Discord server
* submitting hotfixes for existing bugs
* giving ideas for new features
* financial contributions (while Inkycal is still dependent on them. These go towards new hardware, displays and a bit of coffee)
Welcome to Inkycal! We are excited that you are considering contributing to our project. Before you get started, please take a moment to read through our contribution guidelines.
# Third party modules
So you had a great idea for an inkycal-module? Awesome! In fact, there is already a repo sepcfifically created for that purpose: [inkycal-modules-template](https://github.com/aceisace/inkycal-modules-template). Just fork that repo, add your module and give me a shout via Discord, Github or Email. If it is really unique and convincing, chances are, if you agree, that it will be available as default module in a future release. Please do not attempt to have it merged straight into main. We try not to touch main except for new releases to keep things consistent, stable and easy-to-maintain.
## Code of Conduct
# Code contributions (PRs, hotfixes, Critical improvements)
So you found a bug in Inkycal and tested out a bugfix? Kudos! Please fork the Inkycal repo, add your changes in there and create a PR targeting main. For all other PRs, please target a different branch.
This project and everyone participating in it are governed by our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report any unacceptable behavior.
Don't forget to add your name in the file `CONTRIBUTORS.md` of the corresponding branch. Thank You!
## How Can I Contribute?
# Submitting Issues
### Reporting Bugs
Please only submit reproducible issues with clear instructions on how to reproduce them.
Before submitting a bug report, check if the issue is already reported in the [Issues](https://github.com/aceinnolab/Inkycal/issues) section. If not, please open a new issue with a detailed description of the problem, including steps to reproduce it.
When you are submitting a new issue, please supply the following information:
### Suggesting Enhancements
### Release version
* are you using main or a different branch. In most cases, this is main
We welcome suggestions for new features or enhancements. Use the [Issues](https://github.com/aceinnolab/Inkycal/issues) section to submit your ideas, and provide as much detail as possible.
### Expected behavior and actual behavior
* what were you expecting to happen and what did really happen?
### Third party modules
So you had a great idea for an inkycal-module? Awesome! In fact, there is already a repo sepcfifically created for that purpose: [inkycal-modules-template](https://github.com/aceisace/inkycal-modules-template). Just fork that repo, add your module and give me a shout via Discord, Github or Email.
### Pull Requests
1. Fork the repository and create a new branch for your feature or bug fix.
2. Make your changes and test thoroughly.
3. Ensure your code follows our coding standards.
4. Update the documentation if necessary.
5. Add your name in the file `CONTRIBUTORS.md`.
6. Open a pull request, referencing any related issues.
## Code Standards
Follow our coding standards to maintain consistency across the project. Check the existing codebase to understand the style and conventions.
## Testing
Ensure that your changes are thoroughly tested. If applicable, provide test cases to cover your code.
## License
By contributing, you agree that your contributions will be licensed under the [LICENSE](https://github.com/aceinnolab/Inkycal/blob/main/LICENSE) file of this project.
## Thank You
Thank you for considering contributing to Inkycal! Your help is invaluable, and we appreciate your time and effort.
Happy coding!
### Steps to reproduce the behavior
* How can the devs re-create the same problem you were having?

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE]: "
labels: ''
assignees: aceisace
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,29 @@
# Pull Request
## Description
_Briefly describe the purpose of this pull request_
## Changes Made
_Describe the changes you made in this PR_
## Related Issues
_Reference any related issues here. Use the format "Fixes #<issue_number>" if this PR fixes an issue._
## How to Test
_Provide step-by-step instructions or commands on how to test your changes_
## Screenshots (if applicable)
_Include screenshots or GIFs that demonstrate the changes (if applicable)_
## Checklist
_Place an 'x' in the checkboxes that apply.
If you're unsure about any of them, don't hesitate to ask._
- [ ] I have read the [contribution guidelines](https://github.com/aceinnolab/Inkycal/blob/main/.github/CONTRIBUTING.md)
- [ ] My code follows the project's coding standards
- [ ] I have tested my changes
- [ ] I have updated the documentation
- [ ] My changes do not introduce new warnings or errors
## Additional Notes
_Any additional information or context you want to provide_

View File

@ -4,6 +4,9 @@ on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test-on-rpi-os:
@ -24,8 +27,8 @@ jobs:
TODOIST_API_KEY: ${{ secrets.TODOIST_API_KEY }}
with:
# Set the base_image to the desired Raspberry Pi OS version
base_image: https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf-lite.img.xz
image_additional_mb: 1500 # enlarge free space to 1.5 GB
base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-10-10/2023-10-10-raspios-bookworm-armhf-lite.img.xz
image_additional_mb: 2560 # enlarge free space to 2.5 GB
optimize_image: true
user: inky
commands: |
@ -39,7 +42,7 @@ jobs:
sudo apt-get update -y
python --version
sudo apt-get install -y python3-pip
sudo apt-get install zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev -y
sudo apt-get install zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf -y
echo $PWD && ls
git clone --branch main --single-branch https://github.com/aceinnolab/Inkycal
cd Inkycal
@ -49,6 +52,6 @@ jobs:
pip install wheel
pip install -e ./
pip install RPi.GPIO==0.7.1 spidev==3.5
cd inkycal/tests
wget https://raw.githubusercontent.com/aceinnolab/Inkycal/assets/tests/settings.json
for f in *.py; do python3 "$f"; done
pip install pytest
python -m pytest

View File

@ -23,7 +23,7 @@ jobs:
with:
# Set the base_image to the desired Raspberry Pi OS version
base_image: https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf-lite.img.xz
image_additional_mb: 1500 # enlarge free space to 1.5 GB
image_additional_mb: 2560 # enlarge free space to 2 GB
optimize_image: true
user: inky
commands: |
@ -37,7 +37,7 @@ jobs:
sudo apt-get update -y
python --version
sudo apt-get install -y python3-pip
sudo apt-get install zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev -y
sudo apt-get install zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf -y
echo $PWD && ls
git clone --branch main --single-branch https://github.com/aceinnolab/Inkycal
cd Inkycal
@ -47,9 +47,9 @@ jobs:
pip install wheel
pip install -e ./
pip install RPi.GPIO==0.7.1 spidev==3.5
cd inkycal/tests
wget https://raw.githubusercontent.com/aceinnolab/Inkycal/assets/tests/settings.json
for f in *.py; do python3 "$f"; done
pip install pytest
python -m pytest
# install deps for 12.48" display
wget http://www.airspayce.com/mikem/bcm2835/bcm2835-1.71.tar.gz

2
.gitignore vendored
View File

@ -146,7 +146,7 @@ dmypy.json
/logs
# inkycal tests
/inkycal/tests/tmp/
/tests/tmp/
!/inkycal/tests/*.py
/docsource/._build/

167
README.md
View File

@ -1,9 +1,10 @@
# Welcome to inkycal v2.0.2!
# Welcome to inkycal v2.0.3!
<p align="center">
<img src="https://raw.githubusercontent.com/aceisace/Inkycal/assets/Repo/logo.png" width="900">
<img src="https://raw.githubusercontent.com/aceisace/Inkycal/assets/Repo/logo.png" width="900" alt="aceinnolab logo">
</p>
<p align="center">
<img src="https://github.com/aceinnolab/Inkycal/blob/c1c274878ba81ddaee6186561e6ea892da54cd6a/Repo/inkycal-featured-gif.gif" width="900">
<img src="https://github.com/aceinnolab/Inkycal/blob/c1c274878ba81ddaee6186561e6ea892da54cd6a/Repo/inkycal-featured-gif.gif" width="900" alt="featured-image">
</p>
<p align="center">
@ -12,13 +13,28 @@
<a href="https://github.com/aceinnolab/Inkycal/releases"><img alt="Version" src="https://img.shields.io/github/release/aceisace/Inkycal.svg"/></a>
<a href="https://github.com/aceinnolab/Inkycal/blob/main/LICENSE"><img alt="Licence" src="https://img.shields.io/github/license/aceisace/Inkycal.svg" /></a>
<a href="https://github.com/aceinnolab/Inkycal/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/aceisace/Inkycal?color=yellow"></a>
<a href="https://github.com/aceinnolab/Inkycal"><img alt="python" src="https://img.shields.io/badge/python-3.9-lightorange"></a>
<a href="https://github.com/aceinnolab/Inkycal"><img alt="python" src="https://img.shields.io/badge/python-3.11-lightorange"></a>
</p>
Inkycal is a software written in python for selected E-Paper displays. It converts these displays into useful information dashboards. It's open-source, free for personal use, fully modular and user-friendly. Despite all this, Inkycal can run well even on the Raspberry Pi Zero. Oh, and it's open for third-party modules! Hooray!
Inkycal is a software written in python for selected E-Paper displays. It converts these displays into useful
information dashboards. It's open-source, free for personal use, fully modular and user-friendly. Despite all this,
Inkycal can run well even on the Raspberry Pi Zero. Oh, and it's open for third-party modules! Hooray!
## ⚠️ Warning: long installation time expected!
Starting october 2023, Raspberry Pi OS is now based on Debian bookworm and uses python 3.11 instead of 3.9 as the
default version. Inkycal has been updated to work with python3.11, but the installation of numpy can take a very long
time, in some cases even hours. If you do not want to wait this long to install Inkycal, you can also get a
ready-to-flash version of Inkycal called InkycalOS-Lite with everything pre-installed for you by sponsoring
via [Github Sponsors](https://github.com/sponsors/aceisace). This helps keep up maintenance costs, implement new
features and fixing bugs. Please choose the one-time sponsor option and select the one with the plug-and-play version of
Inkycal. Then, send your email-address to which InkycalOS-Lite should be sent.
## Main features
Inkycal is fully modular, you can mix and match any modules you like and configure them on the web-ui. For now, these following built-in modules are supported:
Inkycal is fully modular, you can mix and match any modules you like and configure them on the web-ui. For now, these
following built-in modules are supported:
* Calendar - Monthly Calendar with option to sync events from iCalendars, e.g. Google.
* Agenda - Agenda showing upcoming events from given iCalendar URLs.
* Image - Display an Image from URL or local file path.
@ -29,21 +45,38 @@ Inkycal is fully modular, you can mix and match any modules you like and configu
* Todoist - Synchronise with Todoist app or website to show todos.
* iCanHazDad - Display a random joke from [iCanHazDad.com](iCanhazdad.com).
## Quickstart
Watch the one-minute video on getting started with Inkycal:
[![Inkycal quickstart](https://img.youtube.com/vi/IiIv_nWE5KI/0.jpg)](https://www.youtube.com/watch?v=IiIv_nWE5KI)
## 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+|0W|0WH|`. 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 via SPI. A single update will cause flickering (fully normal on e-paper displays) ranging from a few seconds to half an minute. We recommend these for users who want to get started quickly and for more compact setups, e.g. fitting inside a photo frame. The resolution of these displays ranges from low to medium. Usually, these displays support 2-3 colours, but no colours in between, e.g. fully black, fully red/yellow and fully-white.
Before you can start, please ensure you have one of the supported displays and of the supported Raspberry
Pi: `|4|3A|3B|3B+|2B|0W|0WH|02W|`. 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.
**Parallel** displays on the other hand do not understand SPI and require their own dedicated driver boards individually configured for these displays. Flickering also takes place here, but an update only takes about one to a few seconds. The resolution is much better than serial e-paper displays, but the cost is also higher. These also have 16 different grayscale levels, which does not compare to the 256 grayscales of LCDs, but far better than serial displays.
**Serial** displays are usually cheaper, but slower. Their main advantage is ease of use, like being able to communicate
via SPI. A single update will cause flickering (fully normal on e-paper displays) ranging from a few seconds to half an
minute. We recommend these for users who want to get started quickly and for more compact setups, e.g. fitting inside a
photo frame. The resolution of these displays ranges from low to medium. Usually, these displays support 2-3 colours,
but no colours in between, e.g. fully black, fully red/yellow and fully-white.
**❗Important note: e-paper displays cannot be simply connected to the Raspberry Pi, but require a driver board. The links below may or may not contain the required driver board. Please ensure you get the correct driver board for the display!**
**Parallel** displays on the other hand do not understand SPI and require their own dedicated driver boards individually
configured for these displays. Flickering also takes place here, but an update only takes about one to a few seconds.
The resolution is much better than serial e-paper displays, but the cost is also higher. These also have 16 different
grayscale levels, which does not compare to the 256 grayscales of LCDs, but far better than serial displays.
**❗Important note: e-paper displays cannot be simply connected to the Raspberry Pi, but require a driver board. The
links below may or may not contain the required driver board. Please ensure you get the correct driver board for the
display!**
| type | vendor | affiliate links to product |
| -- | -- | -- |
|-------------------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 7.5" Inkycal (plug-and-play) | Author of Inkycal |  [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 | Author of Inkycal | coming soon (ultraslim frame with custom-made front and backcover inkl. ultraslim driver board). You will need a Raspberry Pi and a 7.5" e-paper display |
| `[serial]` 12.48" (1304×984px) display | waveshare / gooddisplay | <a target="_blank" href="https://www.amazon.de/gp/search?ie=UTF8&tag=aceisace-21&linkCode=ur2&linkId=7e08c6110a1a5b3511ead10db2fd909a&camp=1638&creative=6742&index=computers&keywords=Waveshare 12.48 Inch E-Paper">Waveshare 12.48 Inch E-Paper</a>
| `[serial]` 12.48" (1304×984px) display | waveshare / gooddisplay |  <a target="_blank" href="https://www.amazon.de/gp/search?ie=UTF8&tag=aceisace-21&linkCode=ur2&linkId=7e08c6110a1a5b3511ead10db2fd909a&camp=1638&creative=6742&index=computers&keywords=Waveshare 12.48 Inch E-Paper">Waveshare 12.48 Inch E-Paper</a> |
| `[serial]` 7.5" (640x384px) -> v1 display | waveshare / gooddisplay | <a target="_blank" href="https://www.amazon.de/gp/search?ie=UTF8&tag=aceisace-21&linkCode=ur2&linkId=1bf1a6338786c0a4e7b877335afa0683&camp=1638&creative=6742&index=computers&keywords=Waveshare 7.5 Inch E-Paper">Waveshare 7.5 Inch E-Paper</a> |
| `[serial]` 7.5" (800x400px) -> v2 display | waveshare / gooddisplay | <a target="_blank" href="https://www.amazon.de/gp/search?ie=UTF8&tag=aceisace-21&linkCode=ur2&linkId=1bf1a6338786c0a4e7b877335afa0683&camp=1638&creative=6742&index=computers&keywords=Waveshare 7.5 Inch E-Paper">Waveshare 7.5 Inch E-Paper</a> |
| `[serial]` 7.5" (880x528px) -> v3 display | waveshare / gooddisplay | <a target="_blank" href="https://www.amazon.de/gp/search?ie=UTF8&tag=aceisace-21&linkCode=ur2&linkId=1bf1a6338786c0a4e7b877335afa0683&camp=1638&creative=6742&index=computers&keywords=Waveshare 7.5 Inch E-Paper">Waveshare 7.5 Inch E-Paper</a> |
@ -55,28 +88,36 @@ Before you can start, please ensure you have one of the supported displays and o
| Raspberry Pi Zero W | Raspberry Pi |  <a target="_blank" href="https://www.amazon.de/gp/search?ie=UTF8&tag=aceisace-21&linkCode=ur2&linkId=8f9c223197e1ab91b0372b1fe56ed508&camp=1638&creative=6742&index=computers&keywords=Raspberry Pi Zero W">Raspberry Pi Zero W</a> |
| MicroSD card | Sandisk |  <a target="_blank" href="https://www.amazon.de/gp/search?ie=UTF8&tag=aceisace-21&linkCode=ur2&linkId=530a2b371c40bfeca48e875fb735a4a1&camp=1638&creative=6742&index=computers&keywords=Sandisk microSD 16GB U1 A1">MicroSD card (8GB)</a> |
## Configuring the Raspberry Pi
1. Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager](https://rptl.io/imager). Use the following settings:
| option | value |
| :-- | :--: |
| hostname | inkycal |
| enable ssh | yes |
| set username and password | yes |
| username | a username you like |
| password | a password you can remember |
| set Wi-Fi | yes |
| Wi-Fi SSID | your Wi-Fi name |
| Wi-Fi password | your Wi-Fi password |
Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager](https://rptl.io/imager). Use the
following settings:
| option | value |
|:--------------------------|:---------------------------:|
| hostname | inkycal |
| enable ssh | yes |
| set username and password | yes |
| username | a username you like |
| password | a password you can remember |
| set Wi-Fi | yes |
| Wi-Fi SSID | your Wi-Fi name |
| Wi-Fi password | your Wi-Fi password |
| set timezone | your local timezone |
1. Create and download `settings.json` file for Inkycal from
the [WEB-UI](https://aceinnolab.com/inkycal/ui). Add the modules you want with the add
module button.
2. Copy the `settings.json` to the flashed microSD card in the `/boot` folder of microSD card. On Windows, this is the
only visible directory on the SD card. On Linux, copy these files to `/boot` of the microSD card.
3. Eject the microSD card from your computer now, insert it in the Raspberry Pi and power the Raspberry Pi.
4. Once the green LED has stopped blinking after ~3 minutes, you can connect to your Raspberry Pi via SSH using a SSH
Client. We suggest [Termius](https://termius.com/download/windows)
on your smartphone. Use the address: `inkycal.local` with the username and password you set earlier. For more
detailed instructions, check out the page from
the [Raspberry Pi website](https://www.raspberrypi.org/documentation/remote-access/ssh/)
5. After connecting via SSH, run the following commands, line by line:
2. Create and download `settings.json` file for Inkycal from the [WEB-UI](https://aceisace.eu.pythonanywhere.com/inkycal-config-v2-0-0). Add the modules you want with the add module button.
3. Copy the `settings.json` to the flashed microSD card in the `/boot` folder of microSD card. On Windows, this is the only visible directory on the SD card. On Linux, copy these files to `/boot` of the microSD card.
4. Eject the microSD card from your computer now, insert it in the Raspberry Pi and power the Raspberry Pi.
5. Once the green LED has stopped blinking after ~3 minutes, you can connect to your Raspberry Pi via SSH using a SSH Client. We suggest [Termius](https://termius.com/download/windows)
on your smartphone. Use the address: `inkycal.local` with the username and password you set earlier. For more detailed instructions, check out the page from the [Raspberry Pi website](https://www.raspberrypi.org/documentation/remote-access/ssh/)
6. After connecting via SSH, run the following commands, line by line:
```bash
sudo raspi-config --expand-rootfs
sudo sed -i s/#dtparam=spi=on/dtparam=spi=on/ /boot/config.txt
@ -96,22 +137,37 @@ sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=256/' /etc/dphys-swapfile
sudo dphys-swapfile setup
sudo dphys-swapfile swapon
```
These commands expand the filesystem, enable SPI and set up the correct timezone on the Raspberry Pi. When running the last command, please select the continent you live in, press enter and then select the capital of the country you live in. Lastly, press enter.
These commands expand the filesystem, enable SPI and set up the correct timezone on the Raspberry Pi. When running the
last command, please select the continent you live in, press enter and then select the capital of the country you live
in. Lastly, press enter.
7. Follow the steps in `Installation` (see below) on how to install Inkycal.
## Installing Inkycal
⚠️ Please note that although the developers try to keep the installation as simple as possible, the full installation can sometimes take hours on the Raspberry Pi Zero W and is not guaranteed to go smoothly each time. This is because installing dependencies on the zero w takes a long time and is prone to copy-paste-, permission- and configuration errors.
**Looking for a shortcut to safe a few hours?** We know about this problem and have spent a signifcant amount of time to prepare a pre-configured image with the latest version of Inkycal for the Raspberry Pi Zero. It comes with the latest version of Inkycal, is fully tested and uses the Raspberry Pi OS Lite as it's base image. You only need to copy your settings.json file, we already took care of the rest, including auto-start at boot, enabling spi and installing all dependencies in advance. Pretty neat right? Check the [sponsor button]() at the very top of the repo to get access to Inkycal-OS-Lite. This will help keep this project growing and cover the ongoing expenses too! Win-win for everyone! 🎊
⚠️ Please note that although the developers try to keep the installation as simple as possible, the full installation
can sometimes take hours on the Raspberry Pi Zero W and is not guaranteed to go smoothly each time. This is because
installing dependencies on the zero w takes a long time and is prone to copy-paste-, permission- and configuration
errors.
**Looking for a shortcut to safe a few hours?** We know about this problem and have spent a signifcant amount of time
to prepare a pre-configured image with the latest version of Inkycal for the Raspberry Pi Zero. It comes with the latest
version of Inkycal, is fully tested and uses the Raspberry Pi OS Lite as it's base image. You only need to copy your
settings.json file, we already took care of the rest, including auto-start at boot, enabling spi and installing all
dependencies in advance. Pretty neat right? Check the [sponsor button](https://github.com/sponsors/aceisace) at the very
top of the repo to get access to Inkycal-OS-Lite. This will help keep this project growing and cover the ongoing
expenses too! Win-win for everyone! 🎊
### Manual installation
Run the following steps to install Inkycal. Do **not** use sudo for this, except where explicitly specified.
```bash
# the next line is for the Raspberry Pi only
sudo apt-get install zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev
sudo apt-get install zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf
cd $HOME
git clone --branch main --single-branch https://github.com/aceisace/Inkycal
git clone --branch main --single-branch https://github.com/aceinnolab/Inkycal
cd Inkycal
python3 -m venv venv
source venv/bin/activate
@ -124,7 +180,9 @@ pip install RPi.GPIO==0.7.1 spidev==3.5
```
## Running Inkycal
To run Inkycal, type in the following command in the terminal:
```bash
cd $HOME/Inkycal
source venv/bin/activate
@ -132,16 +190,21 @@ python3 inky_run.py
```
## Running on each boot
To make inkycal run on each boot automatically, you can use crontab. Do not use sudo for this
```bash
(crontab -l ; echo "@reboot sleep 60 && cd $HOME/Inkycal && venv/bin/python inky_run.py &")| crontab -
```
## Updating Inkycal
To update Inkycal to the latest version, navigate to the Inkycal folder, then run:
```bash
git pull
```
Yep. It's actually that simple!
But, if you have made changes to Inkycal, those will be overwritten.
If that is the case, backup your modified files somewhere else if you need them. Then run:
@ -152,51 +215,69 @@ git pull
```
## Uninstalling Inkycal
We'll miss you, but we don't want to make it hard for you to leave.
Just delete the Inkycal folder, and you're good to go!
Additionally, if you want to reset your crontab file, which runs inkycal at boot, run:
```bash
crontab -r
```
## Modifying Inkycal
Inkycal now runs in a virtual environment to support more devices than just the Raspberry Pi. Therefore, to make changes to Inkycal, navigate to Inkycal, then run:
Inkycal now runs in a virtual environment to support more devices than just the Raspberry Pi. Therefore, to make changes
to Inkycal, navigate to Inkycal, then run:
```bash
cd $HOME/Inkycal && source venv/bin/activate
```
Then modify the files as needed and experiment with Inkycal.
To deactivate the virtual environment, simply run:
```bash
deactivate
```
## 3D printed frames
With your setup being complete at this stage, you may want to 3d-print a case. The following files were shared by our friendly community:
With your setup being complete at this stage, you may want to 3d-print a case. The following files were shared by our
friendly community:
[3D-printable case](https://github.com/aceinnolab/Inkycal/wiki/3D-printable-files)
## Contributing
All sorts of contributions are most welcome and appreciated. To start contributing, please follow the [Contribution Guidelines](https://github.com/aceisace/Inkycal/blob/main/.github/CONTRIBUTING.md)
The average response time for issues, PRs and emails is usually 24 hours. In some cases, it might be longer. If you want to have some faster responses, please use Discord (link below)
All sorts of contributions are most welcome and appreciated. To start contributing, please follow
the [Contribution Guidelines](https://github.com/aceisace/Inkycal/blob/main/.github/CONTRIBUTING.md)
The average response time for issues, PRs and emails is usually 24 hours. In some cases, it might be longer. If you want
to have some faster responses, please use Discord (link below)
**P.S:** Don't forget to star and/or watch the repo. For those who have done so already, thank you very much!
## Join us on Discord!
We're happy to help, to beginners and developers alike. In fact, you are more likely to get faster support on Discord than on Github.
We're happy to help, to beginners and developers alike. In fact, you are more likely to get faster support on Discord
than on Github.
<a href="https://discord.gg/sHYKeSM">
<img src="https://github.com/aceisace/Inkycal/blob/assets/Repo/discord-logo.png?raw=true" alt="Inkycal chatroom Discord" width=200>
</a>
## Sponsoring
Inkycal relies on sponsors to keep up maintainance, development and bug-fixing. Please consider sponsoring Inkycal via the sponsor button if you are happy with Inkycal.
We now offer perks depending on the amount contributed for sponsoring, ranging from pre-configured OS images for plug-and-play to development of user-suggested modules. Check out the sponsor page to find out more.
If you have been a previous sponsor, please let us know on our Dicord server or by sending an email. We'll send you the perks after confirming 💯
Inkycal relies on sponsors to keep up maintainance, development and bug-fixing. Please consider sponsoring Inkycal via
the sponsor button if you are happy with Inkycal.
We now offer perks depending on the amount contributed for sponsoring, ranging from pre-configured OS images for
plug-and-play to development of user-suggested modules. Check out the sponsor page to find out more.
If you have been a previous sponsor, please let us know on our Dicord server or by sending an email. We'll send you the
perks after confirming 💯
## As featured on
* [makeuseof - fantastic projects using an eink display](http://makeuseof.com/fantastic-projects-using-an-e-ink-display/)
* [magpi.de](https://www.magpi.de/news/maginkcal-ein-kalender-mit-epaper-display-und-raspberry-pi)
* [reddit - Inkycal](https://www.reddit.com/r/InkyCal/)

View File

@ -1,7 +1,7 @@
#!python3
from inkycal import Inkycal # Import Inkycal
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
inky.run() # If there were no issues, you can run Inkycal nonstop
asyncio.run(inky.run()) # If there were no issues, you can run Inkycal nonstop

View File

@ -10,7 +10,9 @@ import inkycal.modules.inkycal_todoist
import inkycal.modules.inkycal_image
import inkycal.modules.inkycal_jokes
import inkycal.modules.inkycal_slideshow
# import inkycal.modules.inkycal_server
import inkycal.modules.inkycal_stocks
import inkycal.modules.inkycal_webshot
import inkycal.modules.inkycal_xkcd
# Main file
from inkycal.main import Inkycal

View File

@ -1,2 +1,3 @@
from .functions import *
from .inkycal_exceptions import *
from .openweathermap_wrapper import OpenWeatherMap

View File

@ -1,15 +1,15 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Inkycal custom-functions for ease-of-use
Copyright by aceinnolab
"""
import logging
from PIL import Image, ImageDraw, ImageFont, ImageColor
from urllib.request import urlopen
import os
import time
import traceback
import requests
from PIL import ImageFont, ImageDraw, Image
logs = logging.getLogger(__name__)
logs.setLevel(level=logging.INFO)
@ -98,11 +98,13 @@ def auto_fontsize(font, max_height):
Returns:
A PIL font object with modified height.
"""
fontsize = font.getsize('hg')[1]
while font.getsize('hg')[1] <= (max_height * 0.80):
text_bbox = font.getbbox("hg")
text_height = text_bbox[3]
fontsize = text_height
while text_height <= (max_height * 0.80):
fontsize += 1
font = ImageFont.truetype(font.path, fontsize)
text_height = text_bbox[3]
return font
@ -154,21 +156,34 @@ def write(image, xy, box_size, text, font=None, **kwargs):
if autofit or (fill_width != 1.0) or (fill_height != 0.8):
size = 8
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[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]
while (text_width < int(box_width * fill_width) and
text_height < int(box_height * fill_height)):
size += 1
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[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_width, text_height = font.getsize(text)[0], font.getsize('hg')[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]
# 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)))
while (text_width, text_height) > (box_width, box_height):
text = text[0:-1]
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[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)
# Align text to desired position
@ -215,14 +230,17 @@ def text_wrap(text, font=None, max_width=None):
A list containing chunked strings of the full text.
"""
lines = []
if font.getsize(text)[0] < max_width:
text_width = font.getlength(text)
if text_width < max_width:
lines.append(text)
else:
words = text.split(' ')
i = 0
while i < len(words):
line = ''
while i < len(words) and font.getsize(line + words[i])[0] <= max_width:
while i < len(words) and font.getlength(line + words[i]) <= max_width:
line = line + words[i] + " "
i += 1
if not line:
@ -247,11 +265,13 @@ def internet_available():
>>> if internet_available():
>>> #...do something that requires internet connectivity
"""
for attempt in range(3):
try:
urlopen('https://google.com', timeout=5)
requests.get('https://google.com', timeout=5)
return True
except:
print(f"Network could not be reached: {traceback.print_exc()}")
time.sleep(5)
return False
@ -279,17 +299,17 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
colour = 'black'
# size from function paramter
# size from function parameter
width, height = int(size[0] * (1 - shrinkage[0])), int(size[1] * (1 - shrinkage[1]))
# shift cursor to move rectangle to center
offset_x, offset_y = int((size[0] - width) / 2), int((size[1] - height) / 2)
x, y, diameter = xy[0] + offset_x, xy[1] + offset_y, radius * 2
# lenght of rectangle size
# length of rectangle size
a, b = (width - diameter), (height - diameter)
# Set coordinates for staright lines
# Set coordinates for straight lines
p1, p2 = (x + radius, y), (x + radius + a, y)
p3, p4 = (x + width, y + radius), (x + width, y + radius + b)
p5, p6 = (p2[0], y + height), (p1[0], y + height)
@ -313,3 +333,12 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
draw.arc((c3, c4), 270, 360, fill=colour, width=thickness)
draw.arc((c5, c6), 0, 90, fill=colour, width=thickness)
draw.arc((c7, c8), 90, 180, fill=colour, width=thickness)
def draw_border_2(im: Image, xy: tuple, size: tuple, radius: int):
draw = ImageDraw.Draw(im)
x, y = xy
w, h = size
draw.rounded_rectangle(xy=(x, y, x + w, y + h), outline="black", radius=radius)

View File

@ -0,0 +1,43 @@
import logging
from enum import Enum
import requests
import json
logger = logging.getLogger(__name__)
class WEATHER_OPTIONS(Enum):
CURRENT_WEATHER = "weather"
class FORECAST_INTERVAL(Enum):
THREE_HOURS = "3h"
FIVE_DAYS = "5d"
class OpenWeatherMap:
def __init__(self, api_key:str, city_id:int, units:str) -> None:
self.api_key = api_key
self.city_id = city_id
assert (units in ["metric", "imperial"] )
self.units = units
self._api_version = "2.5"
self._base_url = f"https://api.openweathermap.org/data/{self._api_version}"
def get_current_weather(self) -> dict:
current_weather_url = f"{self._base_url}/weather?id={self.city_id}&appid={self.api_key}&units={self.units}"
response = requests.get(current_weather_url)
if not response.ok:
raise AssertionError(f"Failure getting the current weather: code {response.status_code}. Reason: {response.text}")
data = json.loads(response.text)
return data
def get_weather_forecast(self) -> dict:
forecast_url = f"{self._base_url}/forecast?id={self.city_id}&appid={self.api_key}&units={self.units}"
response = requests.get(forecast_url)
if not response.ok:
raise AssertionError(f"Failure getting the current weather: code {response.status_code}. Reason: {response.text}")
data = json.loads(response.text)["list"]
return data

View File

@ -4,6 +4,7 @@ Copyright by aceisace
"""
import os
import logging
import traceback
from importlib import import_module
from PIL import Image
@ -43,7 +44,7 @@ class Display:
except FileNotFoundError:
raise Exception('SPI could not be found. Please check if SPI is enabled')
def render(self, im_black: Image.Image, im_colour=Image.Image or None) -> None:
def render(self, im_black: Image, im_colour=Image or None) -> None:
"""Renders an image on the selected E-Paper display.
Initlializes the E-Paper display, sends image data and executes command
@ -66,7 +67,6 @@ class Display:
Rendering black-white on coloured E-Paper displays:
>>> sample_image = Image.open('path/to/file.png')
>>> display = Display('my_coloured_display')
>>> display.render(sample_image, sample_image)
@ -82,14 +82,7 @@ class Display:
epaper = self._epaper
if not self.supports_colour:
print('Initialising..', end='')
epaper.init()
print('Updating display......', end='')
epaper.display(epaper.getbuffer(im_black))
print('Done')
elif self.supports_colour:
if self.supports_colour:
if not im_colour:
raise Exception('im_colour is required for coloured epaper displays')
print('Initialising..', end='')
@ -97,6 +90,12 @@ class Display:
print('Updating display......', end='')
epaper.display(epaper.getbuffer(im_black), epaper.getbuffer(im_colour))
print('Done')
else:
print('Initialising..', end='')
epaper.init()
print('Updating display......', end='')
epaper.display(epaper.getbuffer(im_black))
print('Done')
print('Sending E-Paper to deep sleep...', end='')
epaper.sleep()
@ -173,9 +172,10 @@ class Display:
try:
driver = import_driver(model_name)
return driver.EPD_WIDTH, driver.EPD_HEIGHT
except Exception as e:
except:
logging.error(f'Failed to load driver for ${model_name}. Check spelling?')
raise e;
print(traceback.format_exc())
raise AssertionError("Could not import driver")
@classmethod
def get_display_names(cls) -> list:

View File

@ -35,6 +35,8 @@ from inkycal.display.drivers import epdconfig
EPD_WIDTH = 800
EPD_HEIGHT = 480
logger = logging.getLogger(__name__)
class EPD:
def __init__(self):
@ -48,11 +50,11 @@ class EPD:
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
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(200)
epdconfig.delay_ms(20)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
@ -66,14 +68,21 @@ class EPD:
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):
logging.debug("e-Paper busy")
logger.debug("e-Paper busy")
self.send_command(0x71)
busy = epdconfig.digital_read(self.busy_pin)
while (busy == 0):
self.send_command(0x71)
busy = epdconfig.digital_read(self.busy_pin)
epdconfig.delay_ms(200)
epdconfig.delay_ms(20)
logger.debug("e-Paper busy release")
def init(self):
if (epdconfig.module_init() != 0):
@ -81,6 +90,12 @@ class EPD:
# EPD hardware init start
self.reset()
self.send_command(0x06) # btst
self.send_data(0x17)
self.send_data(0x17)
self.send_data(0x28) # If an exception is displayed, try using 0x38
self.send_data(0x17)
self.send_command(0x01) # POWER SETTING
self.send_data(0x07)
self.send_data(0x07) # VGH=20V,VGL=-20V
@ -114,47 +129,39 @@ class EPD:
return 0
def getbuffer(self, image):
# logging.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()
# logging.debug("imwidth = %d, imheight = %d",imwidth,imheight)
img = image
imwidth, imheight = img.size
if (imwidth == self.width and imheight == self.height):
logging.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] == 0:
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
img = img.convert('1')
elif (imwidth == self.height and imheight == self.width):
logging.debug("Horizontal")
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))
# image has correct dimensions, but needs to be rotated
img = img.rotate(90, expand=True).convert('1')
else:
logger.warning("Wrong image dimensions: must be " + str(self.width) + "x" + str(self.height))
# return a blank buffer
return [0x00] * (int(self.width / 8) * self.height)
buf = bytearray(img.tobytes('raw'))
# The bytes need to be inverted, because in the PIL world 0=black and 1=white, but
# in the e-paper world 0=white and 1=black.
for i in range(len(buf)):
buf[i] ^= 0xFF
return buf
def display(self, image):
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(~image[i]);
self.send_data2(image)
self.send_command(0x12)
epdconfig.delay_ms(100)
self.ReadBusy()
def Clear(self):
buf = [0x00] * (int(self.width / 8) * self.height)
self.send_command(0x10)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0x00)
self.send_data2(buf)
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0x00)
self.send_data2(buf)
self.send_command(0x12)
epdconfig.delay_ms(100)
self.ReadBusy()
@ -166,5 +173,6 @@ class EPD:
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0XA5)
epdconfig.delay_ms(2000)
epdconfig.module_exit()
### END OF FILE ###

View File

@ -1,6 +1,3 @@
#!python3
# -*- coding: utf-8 -*-
"""
Main class for inkycal Project
Copyright by aceinnolab
@ -9,11 +6,12 @@ Copyright by aceinnolab
import glob
import hashlib
import json
import traceback
from logging.handlers import RotatingFileHandler
import arrow
import numpy
import asyncio
from inkycal.custom import *
from inkycal.display import Display
@ -27,7 +25,6 @@ stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.ERROR)
if not os.path.exists(f'{top_level}/logs'):
os.mkdir(f'{top_level}/logs')
@ -37,9 +34,7 @@ logging.basicConfig(
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
@ -71,15 +66,18 @@ class Inkycal:
to improve rendering on E-Papers. Set this to False for 9.7" E-Paper.
"""
def __init__(self, settings_path=None, render=True):
def __init__(self, settings_path:str or None=None, render:bool=True):
"""Initialise Inkycal"""
self._release = '2.0.2'
# 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
# Check if render was set correctly
if render not in [True, False]:
raise Exception(f'render must be True or False, not "{render}"')
self.render = render
self.info = None
# load settings file - throw an error if file could not be found
if settings_path:
@ -89,7 +87,7 @@ class Inkycal:
self.settings = settings
except FileNotFoundError:
raise SettingsFileNotFoundError
raise FileNotFoundError(f"No settings.json file could be found in the specified location: {settings_path}")
else:
try:
@ -106,6 +104,8 @@ class Inkycal:
# Option to use epaper image optimisation, reduces colours
self.optimize = True
self.show_border = self.settings.get('border_around_modules', False)
# Load drivers if image should be rendered
if self.render:
# Init Display class with model in settings file
@ -121,7 +121,7 @@ class Inkycal:
# init calibration state
self._calibration_state = False
# Load and intialize modules specified in the settings file
# Load and initialise modules specified in the settings file
self._module_number = 1
for module in settings['modules']:
module_name = module['name']
@ -168,10 +168,10 @@ class Inkycal:
update_timings = [(60 - int(interval_mins) * updates) for updates in
range(60 // int(interval_mins))][::-1]
# Calculate time in mins until next update
# Calculate time in minutes until next update
minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute
# Print the remaining time in mins until next update
# Print the remaining time in minutes until next update
print(f'{minutes} minutes left until next refresh')
# Calculate time in seconds until next update
@ -205,6 +205,8 @@ class Inkycal:
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!')
@ -259,12 +261,12 @@ class Inkycal:
return res
def run(self):
async def run(self):
"""Runs main program in nonstop mode.
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 sheduled update.
then sleeps until the next scheduled update.
"""
# Get the time of initial run
@ -300,6 +302,8 @@ class Inkycal:
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 "
@ -327,7 +331,7 @@ class Inkycal:
self._calibration_check()
if self._calibration_state:
# after calibration we have to forcefully rewrite the screen
# after calibration, we have to forcefully rewrite the screen
self._remove_hashes(self.image_folder)
if self.supports_colour:
@ -365,7 +369,7 @@ class Inkycal:
f'program started {runtime.humanize()}')
sleep_time = self.countdown()
time.sleep(sleep_time)
await asyncio.sleep(sleep_time)
@staticmethod
def _merge_bands():
@ -536,7 +540,7 @@ class Inkycal:
self.Display.calibrate()
def _calibration_check(self):
"""Calibration sheduler
"""Calibration scheduler
uses calibration hours from settings file to check if calibration is due"""
now = arrow.now()
# print('hour:', now.hour, 'hours:', self._calibration_hours)
@ -547,187 +551,6 @@ class Inkycal:
else:
self._calibration_state = False
@classmethod
def add_module(cls, filepath):
"""registers a third party module for inkycal.
Uses the full filepath of the third party module to check if it is inside
the correct folder, then checks if it's an inkycal module. Lastly, the
init files in /inkycal and /inkycal/modules are updated to allow using
the new module.
Args:
- filepath: The full filepath of the third party module. Modules should be
in Inkycal/inkycal/modules.
Usage:
- download a third-party module. The exact link is provided by the
developer of that module and starts with
`https://raw.githubusercontent.com/...`
enter the following in bash to download a module::
$ cd Inkycal/inkycal/modules #navigate to modules folder in inkycal
$ wget https://raw.githubusercontent.com/... #download the module
then register it with this function::
>>> from inkycal import Inkycal
>>> Inkycal.add_module('/full/path/to/the/module/in/inkycal/modules.py')
"""
module_folder = top_level + '/inkycal/modules'
if module_folder in filepath:
filename = filepath.split('.py')[0].split('/')[-1]
# Extract name of class from given module and validate if it's an inkycal
# module
with open(filepath, mode='r') as module:
module_content = module.read().splitlines()
for line in module_content:
if '(inkycal_module):' in line:
classname = line.split(' ')[-1].split('(')[0]
break
if not classname:
raise TypeError("your module doesn't seem to be a correct inkycal module.."
"Please check your module again.")
# Check if filename or classname exists in init of module folder
with open(module_folder + '/__init__.py', mode='r') as file:
module_init = file.read().splitlines()
print('checking module init file..')
for line in module_init:
if filename in line:
raise Exception(
"A module with this filename already exists! \n"
"Please consider renaming your module and try again."
)
if classname in line:
raise Exception(
"A module with this classname already exists! \n"
"Please consider renaming your class and try again."
)
print('OK!')
# Check if filename or classname exists in init of inkycal folder
with open(top_level + '/inkycal/__init__.py', mode='r') as file:
inkycal_init = file.read().splitlines()
print('checking inkycal init file..')
for line in inkycal_init:
if filename in line:
raise Exception(
"A module with this filename already exists! \n"
"Please consider renaming your module and try again."
)
if classname in line:
raise Exception(
"A module with this classname already exists! \n"
"Please consider renaming your class and try again."
)
print('OK')
# If all checks have passed, add the module in the module init file
with open(module_folder + '/__init__.py', mode='a') as file:
file.write(f'from .{filename} import {classname} # Added by module adder')
# If all checks have passed, add the module in the inkycal init file
with open(top_level + '/inkycal/__init__.py', mode='a') as file:
file.write(f'import inkycal.modules.{filename} # Added by module adder')
print(f"Your module '{filename}' with class '{classname}' has been added "
"successfully! Hooray!")
return
# Check if module is inside the modules folder
raise Exception(f"Your module should be in {module_folder} "
f"but is currently in {filepath}")
@classmethod
def remove_module(cls, filename, remove_file=True):
"""unregisters an inkycal module.
Looks for given filename.py in /modules folder, removes entries of that
module in init files inside /inkycal and /inkycal/modules
Args:
- filename: The filename (with .py ending) of the module which should be
unregistered. e.g. `'mymodule.py'`
- remove_file: ->bool (True/False). If set to True, the module is deleted
after unregistering it, else it remains in the /modules folder
Usage:
- Look for the module in Inkycal/inkycal/modules which should be removed.
Only the filename (with .py) is required, not the full path.
Use this function to unregister the module from inkycal::
>>> from inkycal import Inkycal
>>> Inkycal.remove_module('mymodule.py')
"""
module_folder = top_level + '/inkycal/modules'
# Check if module is inside the modules folder and extract classname
try:
with open(f"{module_folder}/{filename}", mode='r') as file:
module_content = file.read().splitlines()
for line in module_content:
if '(inkycal_module):' in line:
classname = line.split(' ')[-1].split('(')[0]
break
if not classname:
print('The module you are trying to remove is not an inkycal module.. '
'Not removing it.')
return
except FileNotFoundError:
print(f"No module named {filename} found in {module_folder}")
return
filename = filename.split('.py')[0]
# Create a memory backup of /modules init file
with open(module_folder + '/__init__.py', mode='r') as file:
module_init = file.read().splitlines()
print('removing line from module_init')
# Remove lines that contain classname
with open(module_folder + '/__init__.py', mode='w') as file:
for line in module_init:
if not classname in line:
file.write(line + '\n')
else:
print('found, removing')
# Create a memory backup of inkycal init file
with open(f"{top_level}/inkycal/__init__.py", mode='r') as file:
inkycal_init = file.read().splitlines()
print('removing line from inkycal init')
# Remove lines that contain classname
with open(f"{top_level}/inkycal/__init__.py", mode='w') as file:
for line in inkycal_init:
if filename in line:
print('found, removing')
else:
file.write(line + '\n')
# remove the file of the third party module if it exists and remove_file
# was set to True (default)
if os.path.exists(f"{module_folder}/{filename}.py") and remove_file is True:
print('deleting module file')
os.remove(f"{module_folder}/{filename}.py")
print(f"Your module '{filename}' with class '{classname}' was removed.")
if __name__ == '__main__':
print(f'running inkycal main in standalone/debug mode')

View File

@ -8,3 +8,5 @@ from .inkycal_jokes import Jokes
from .inkycal_stocks import Stocks
from .inkycal_slideshow import Slideshow
from .inkycal_textfile_to_display import TextToDisplay
from .inkycal_webshot import Webshot
from .inkycal_xkcd import Xkcd

View File

@ -1,5 +1,3 @@
#!python3
"""
Inkycal iCalendar parsing module
Copyright by aceinnolab
@ -119,7 +117,7 @@ class iCalendar:
events = (
{
'title': events.get('SUMMARY').lstrip(),
'title': events.get('SUMMARY').lstrip() if events.get('SUMMARY') else "",
'begin': arrow.get(events.get('DTSTART').dt).to(timezone) if (
arrow.get(events.get('dtstart').dt).format('HH:mm') != '00:00')

View File

@ -83,12 +83,11 @@ class Inkyimage:
@staticmethod
def preview(image):
""""Previews an image on gpicview (only works on Rapsbian with Desktop).
"""
path = '/home/pi/Desktop/'
image.save(path + 'temp.png')
os.system("gpicview " + path + 'temp.png')
os.system('rm ' + path + 'temp.png')
"""Previews an image on gpicview (only works on Rapsbian with Desktop)."""
path = '~/temp'
image.save(path + '/temp.png')
os.system("gpicview " + path + '/temp.png')
os.system('rm ' + path + '/temp.png')
def _image_loaded(self):
"""returns True if image was loaded"""

View File

@ -1,5 +1,3 @@
#!python3
"""
Inkycal Agenda Module
Copyright by aceinnolab
@ -98,7 +96,9 @@ class Agenda(inkycal_module):
# Calculate the max number of lines that can fit on the image
line_spacing = 1
line_height = int(self.font.getsize('hg')[1]) + line_spacing
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
line_width = im_width
max_lines = im_height // line_height
logger.debug(f'max lines: {max_lines}')
@ -109,9 +109,11 @@ class Agenda(inkycal_module):
# Create a list of dates for the next days
agenda_events = [
{'begin': today.shift(days=+_),
{
'begin': today.shift(days=+_),
'title': today.shift(days=+_).format(
self.date_format, locale=self.language)}
self.date_format, locale=self.language)
}
for _ in range(max_lines)]
# Load icalendar from config
@ -133,8 +135,8 @@ class Agenda(inkycal_module):
# parser.show_events()
# Set the width for date, time and event titles
date_width = int(max([self.font.getsize(
dates['begin'].format(self.date_format, locale=self.language))[0]
date_width = int(max([self.font.getlength(
dates['begin'].format(self.date_format, locale=self.language))
for dates in agenda_events]) * 1.2)
logger.debug(f'date_width: {date_width}')
@ -147,8 +149,9 @@ class Agenda(inkycal_module):
logger.info('Managed to parse events from urls')
# Find out how much space the event times take
time_width = int(max([self.font.getsize(
events['begin'].format(self.time_format, locale=self.language))[0]
time_width = int(max([self.font.getlength(
events['begin'].format(self.time_format, locale=self.language))
for events in upcoming_events]) * 1.2)
logger.debug(f'time_width: {time_width}')
@ -224,7 +227,3 @@ class Agenda(inkycal_module):
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone mode')

View File

@ -1,5 +1,3 @@
#!python3
"""
Inkycal Calendar Module
Copyright by aceinnolab
@ -110,7 +108,8 @@ class Calendar(inkycal_module):
# Allocate space for month-names, weekdays etc.
month_name_height = int(im_height * 0.10)
weekdays_height = int(self.font.getsize('hg')[1] * 1.25)
text_bbox_height = self.font.getbbox("hg")
weekdays_height = int((text_bbox_height[3] - text_bbox_height[1])* 1.25)
logger.debug(f"month_name_height: {month_name_height}")
logger.debug(f"weekdays_height: {weekdays_height}")
@ -182,15 +181,15 @@ class Calendar(inkycal_module):
]
logger.debug(f'weekday names: {weekday_names}')
for idx, weekday in enumerate(weekday_pos):
for index, weekday in enumerate(weekday_pos):
write(
im_black,
weekday,
(icon_width, weekdays_height),
weekday_names[idx],
weekday_names[index],
font=self.font,
autofit=True,
fill_height=1.0,
fill_height=0.9,
)
# Create a calendar template and flatten (remove nestings)
@ -207,6 +206,10 @@ class Calendar(inkycal_module):
# remove zeros from calendar since they are not required
calendar_flat = [num for num in calendar_flat if num != 0]
# ensure all numbers have the same size
fontsize_numbers = int(min(icon_width, icon_height) * 0.5)
number_font = ImageFont.truetype(self.font.path, fontsize_numbers)
# Add the numbers on the correct positions
for number in calendar_flat:
if number != int(now.day):
@ -215,9 +218,7 @@ class Calendar(inkycal_module):
grid[number],
(icon_width, icon_height),
str(number),
font=self.num_font,
fill_height=0.5,
fill_width=0.5,
font=number_font,
)
# Draw a red/black circle with the current day of month in white
@ -262,10 +263,10 @@ class Calendar(inkycal_module):
from inkycal.modules.ical_parser import iCalendar
# find out how many lines can fit at max in the event section
line_spacing = 0
max_event_lines = events_height // (
self.font.getsize('hg')[1] + line_spacing
)
line_spacing = 2
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
max_event_lines = events_height // (line_height + line_spacing)
# generate list of coordinates for each line
events_offset = im_height - events_height
@ -294,13 +295,26 @@ class Calendar(inkycal_module):
parser.sort()
self.month_events = month_events
# find out on which days of this month events are taking place
days_with_events = [
int(events['begin'].format('D')) for events in 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)
end = arrow.get(event['end'].date(), tzinfo=self.timezone)
# Use arrow's range function for generating dates
for day in arrow.Arrow.range('day', start, end):
day_num = int(day.format('D')) # get day number using arrow's format method
if day_num not in days_with_events:
days_with_events.append(day_num)
# remove duplicates (more than one event in a single day)
list(set(days_with_events)).sort()
days_with_events = sorted(set(days_with_events))
self._days_with_events = days_with_events
# Draw a border with specified parameters around days with events
@ -329,31 +343,18 @@ class Calendar(inkycal_module):
# Find out how much space (width) the date format requires
lang = self.language
date_width = int(
max(
(
self.font.getsize(
events['begin'].format(self.date_format, locale=lang)
)[0]
for events in upcoming_events
)
)
* 1.1
date_width = int(max((
self.font.getlength(events['begin'].format(self.date_format, locale=lang))
for events in upcoming_events))* 1.1
)
time_width = int(
max(
(
self.font.getsize(
events['begin'].format(self.time_format, locale=lang)
)[0]
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
)
line_height = self.font.getsize('hg')[1] + line_spacing
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
event_width_s = im_width - date_width - time_width
event_width_l = im_width - date_width
@ -365,6 +366,12 @@ class Calendar(inkycal_module):
cursor = 0
for event in upcoming_events:
if cursor < len(event_lines):
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)
the_name = f"{event['title']} ({days_translation})"
else:
the_name = event['title']
the_date = event['begin'].format(self.date_format, locale=lang)
the_time = event['begin'].format(self.time_format, locale=lang)
@ -411,19 +418,16 @@ class Calendar(inkycal_module):
cursor += 1
else:
symbol = '- '
while self.font.getsize(symbol)[0] < im_width * 0.9:
while self.font.getlength(symbol) < im_width * 0.9:
symbol += ' -'
write(
im_black,
event_lines[0],
(im_width, self.font.getsize(symbol)[1]),
(im_width, line_height),
symbol,
font=self.font,
)
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone mode')

View File

@ -1,5 +1,3 @@
#!python3
"""
Feeds module for InkyCal Project
Copyright by aceinnolab
@ -91,9 +89,11 @@ class Feeds(inkycal_module):
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
max_lines = (im_height // (line_height + line_spacing))
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
@ -149,7 +149,3 @@ class Feeds(inkycal_module):
# return images
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone/debug mode')

View File

@ -1,5 +1,3 @@
#!python3
"""
iCanHazDadJoke module for InkyCal Project
Special thanks to Erik Fredericks (@efredericks) for the template!
@ -54,10 +52,11 @@ class Jokes(inkycal_module):
raise NetworkNotReachableError
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_spacing = 5
text_bbox = self.font.getbbox("hg")
line_height = text_bbox[3] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
max_lines = (im_height // (line_height + line_spacing))
logger.debug(f"max_lines: {max_lines}")
@ -97,7 +96,3 @@ class Jokes(inkycal_module):
# Return images for black and colour channels
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone/debug mode')

View File

@ -1,4 +1,3 @@
#!python3
"""
Stocks Module for Inkycal Project
@ -10,26 +9,18 @@ Version 0.1: Migration to Inkycal 2.0.0b
by https://github.com/worstface
"""
import os
import logging
from inkycal.modules.template import inkycal_module
from inkycal.custom import write, internet_available
import os
from PIL import Image
from matplotlib import pyplot
from inkycal.custom import write, internet_available
from inkycal.modules.template import inkycal_module
try:
import yfinance as yf
except ImportError:
print('yfinance is not installed! Please install with:')
print('pip3 install yfinance')
try:
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
except ImportError:
print('matplotlib is not installed! Please install with:')
print('pip3 install matplotlib')
logger = logging.getLogger(__name__)
@ -79,26 +70,24 @@ class Stocks(inkycal_module):
im_colour = Image.new('RGB', size=im_size, color='white')
# Create tmp path
tmpPath = '/tmp/inkycal_stocks/'
tmpPath = 'temp/'
try:
if not os.path.exists(tmpPath):
print(f"Creating tmp directory {tmpPath}")
os.mkdir(tmpPath)
print(f"Successfully created tmp directory {tmpPath} ")
except OSError:
print(f"Creation of tmp directory {tmpPath} failed")
# Check if internet is available
if internet_available() == True:
if internet_available():
logger.info('Connection test passed')
else:
raise Exception('Network could not be reached :/')
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
text_bbox = self.font.getbbox("hg")
line_height = text_bbox[3] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
max_lines = (im_height // (line_height + line_spacing))
logger.debug(f"max_lines: {max_lines}")
@ -211,7 +200,7 @@ class Stocks(inkycal_module):
else:
parsed_tickers_colour.append("")
if (_ < len(tickerCount)):
if _ < len(tickerCount):
parsed_tickers.append("")
parsed_tickers_colour.append("")
@ -232,9 +221,10 @@ class Stocks(inkycal_module):
logger.info(f'chartSpace is...{im_width} {im_height}')
logger.info(f'open chart ...{chartPath}')
chartImage = Image.open(chartPath)
chartImage.thumbnail((im_width / 4, line_height * 4), Image.BICUBIC)
chartImage.thumbnail((int(im_width / 4), int(line_height * 4)), Image.BICUBIC)
pyplot.close()
chartPasteX = im_width - (chartImage.width)
chartPasteX = im_width - chartImage.width
chartPasteY = line_height * 5 * _
logger.info(f'pasting chart image with index {_} to...{chartPasteX} {chartPasteY}')
@ -265,6 +255,3 @@ class Stocks(inkycal_module):
# Save image of black and colour channel in image-folder
return im_black, im_colour
if __name__ == '__main__':
print('running module in standalone/debug mode')

View File

@ -1,4 +1,3 @@
#!python3
"""
Textfile module for InkyCal Project
@ -7,44 +6,29 @@ If the content is too long, it will be truncated from the back until it fits
Copyright by aceinnolab
"""
from inkycal.modules.template import inkycal_module
from inkycal.custom import *
from urllib.request import urlopen
from inkycal.custom import *
from inkycal.modules.template import inkycal_module
logger = logging.getLogger(__name__)
class TextToDisplay(inkycal_module):
"""TextToDisplay module
"""TextToDisplay module - Display text from a local file on the display
"""
name = "Text module - Display text from a local file on the display"
requires = {
"filepath": {
"label": "Please enter a filepath or URL pointing to a .txt file",
},
}
def __init__(self, config):
"""Initialize inkycal_textfile_to_display module"""
super().__init__(config)
config = config['config']
# Check if all required parameters are present
for param in self.requires:
if param not in config:
raise Exception(f'config is missing {param}')
# required parameters
self.filepath = config["filepath"]
self.make_request = True if self.filepath.startswith("https://") else False
# give an OK message
print(f'{__name__} loaded')
@ -66,17 +50,12 @@ class TextToDisplay(inkycal_module):
im_black = Image.new('RGB', size=im_size, color='white')
im_colour = Image.new('RGB', size=im_size, color='white')
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
else:
raise NetworkNotReachableError
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_spacing = 4
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
max_lines = im_height // line_height
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
@ -87,6 +66,11 @@ class TextToDisplay(inkycal_module):
if self.make_request:
logger.info("Detected http path, making request")
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
else:
raise NetworkNotReachableError
file_content = urlopen(self.filepath).read().decode('utf-8')
else:
# Create list containing all lines
@ -111,7 +95,3 @@ class TextToDisplay(inkycal_module):
# return images
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone/debug mode')

View File

@ -1,5 +1,3 @@
#!python3
"""
Inkycal Todoist Module
Copyright by aceinnolab
@ -86,9 +84,10 @@ class Todoist(inkycal_module):
# Set some parameters for formatting todos
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
max_lines = im_height // line_height
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
@ -197,7 +196,3 @@ class Todoist(inkycal_module):
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone/debug mode')

View File

@ -12,7 +12,7 @@ import math
import decimal
import arrow
from pyowm.owm import OWM
from inkycal.custom import OpenWeatherMap
logger = logging.getLogger(__name__)
@ -95,7 +95,7 @@ class Weather(inkycal_module):
self.use_beaufort = config['use_beaufort']
# additional configuration
self.owm = OWM(self.api_key).weather_manager()
self.owm = OpenWeatherMap(api_key=self.api_key, city_id=self.location, units=config['units'])
self.timezone = get_system_tz()
self.locale = config['language']
self.weatherfont = ImageFont.truetype(
@ -104,6 +104,42 @@ class Weather(inkycal_module):
# give an OK message
print(f"{__name__} loaded")
@staticmethod
def mps_to_beaufort(meters_per_second:float) -> int:
"""Map meters per second to the beaufort scale.
Args:
meters_per_second:
float representing meters per seconds
Returns:
an integer of the beaufort scale mapping the input
"""
thresholds = [0.3, 1.6, 3.4, 5.5, 8.0, 10.8, 13.9, 17.2, 20.7, 24.5, 28.4]
return next((i for i, threshold in enumerate(thresholds) if meters_per_second < threshold), 11)
@staticmethod
def mps_to_mph(meters_per_second:float) -> float:
"""Map meters per second to miles per hour, rounded to one decimal place.
Args:
meters_per_second:
float representing meters per seconds.
Returns:
float representing the input value in miles per hour.
"""
# 1 m/s is approximately equal to 2.23694 mph
miles_per_hour = meters_per_second * 2.23694
return round(miles_per_hour, 1)
@staticmethod
def celsius_to_fahrenheit(celsius:int or float):
"""Converts the given temperate from degrees Celsius to Fahrenheit."""
fahrenheit = (celsius * 9 / 5) + 32
return fahrenheit
def generate_image(self):
"""Generate image for this module"""
@ -124,7 +160,11 @@ class Weather(inkycal_module):
raise NetworkNotReachableError
def get_moon_phase():
"""Calculate the current (approximate) moon phase"""
"""Calculate the current (approximate) moon phase
Returns:
The corresponding moonphase-icon.
"""
dec = decimal.Decimal
diff = now - arrow.get(2001, 1, 1)
@ -154,7 +194,7 @@ class Weather(inkycal_module):
return answer
# Lookup-table for weather icons and weather codes
weathericons = {
weather_icons = {
'01d': '\uf00d',
'02d': '\uf002',
'03d': '\uf013',
@ -227,26 +267,26 @@ class Weather(inkycal_module):
# Increase fontsize to fit specified height and width of text box
size = 8
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)
text_width, text_height = font.getbbox(text)[2:]
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.getsize(text)
text_width, text_height = font.getbbox(text)[2:]
text_width, text_height = font.getsize(text)
text_width, text_height = font.getbbox(text)[2:]
# Align text to desired position
x = int((box_width / 2) - (text_width / 2))
y = int((box_height / 2) - (text_height / 2) - (icon_size_correction[icon] * size) / 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)
if rotation != None:
if rotation:
space.rotate(rotation, expand=True)
# Update only region with text (add text with transparent background)
@ -350,14 +390,9 @@ class Weather(inkycal_module):
temp_fc4 = (col7, row3)
# Create current-weather and weather-forecast objects
if self.location.isdigit():
logging.debug('looking up location by ID')
weather = self.owm.weather_at_id(int(self.location)).weather
forecast = self.owm.forecast_at_id(int(self.location), '3h')
else:
logging.debug('looking up location by string')
weather = self.owm.weather_at_place(self.location).weather
forecast = self.owm.forecast_at_place(self.location, '3h')
weather = self.owm.get_current_weather()
forecast = self.owm.get_weather_forecast()
# Set decimals
dec_temp = None if self.round_temperature == True else 1
@ -369,12 +404,14 @@ class Weather(inkycal_module):
elif self.units == 'imperial':
temp_unit = 'fahrenheit'
logging.debug(f'temperature unit: {temp_unit}')
logging.debug(f'temperature unit: {self.units}')
logging.debug(f'decimals temperature: {dec_temp} | decimals wind: {dec_wind}')
# Get current time
now = arrow.utcnow()
fc_data = {}
if self.forecast_interval == 'hourly':
logger.debug("getting hourly forecasts")
@ -386,21 +423,22 @@ class Weather(inkycal_module):
else:
hour_gap = 3
# Create timings for hourly forcasts
# Create timings for hourly forecasts
forecast_timings = [now.shift(hours=+ hour_gap + _).floor('hour')
for _ in range(0, 12, 3)]
# Create forecast objects for given timings
forecasts = [forecast.get_weather_at(forecast_time.datetime) for
forecast_time in forecast_timings]
forecasts = [_ for _ in forecast if arrow.get(_["dt"]) in forecast_timings]
# Add forecast-data to fc_data dictionary
fc_data = {}
for forecast in forecasts:
temp = '{}°'.format(round(
forecast.temperature(unit=temp_unit)['temp'], ndigits=dec_temp))
if self.units == "metric":
temp = f"{round(weather['main']['temp'], ndigits=dec_temp)}°C"
else:
temp = f"{round(self.celsius_to_fahrenheit(weather['weather']['main']['temp']), ndigits=dec_temp)}°F"
icon = forecast.weather_icon_name
icon = forecast["weather"][0]["icon"]
fc_data['fc' + str(forecasts.index(forecast) + 1)] = {
'temp': temp,
'icon': icon,
@ -412,38 +450,35 @@ class Weather(inkycal_module):
logger.debug("getting daily forecasts")
def calculate_forecast(days_from_today):
def calculate_forecast(days_from_today) -> dict:
"""Get temperature range and most frequent icon code for forecast
days_from_today should be int from 1-4: e.g. 2 -> 2 days from today
"""
# Create a list containing time-objects for every 3rd hour of the day
time_range = list(arrow.Arrow.range('hour',
now.shift(days=days_from_today).floor('day'),
now.shift(days=days_from_today).ceil('day')
time_range = list(
arrow.Arrow.range('hour',
now.shift(days=days_from_today).floor('day'),now.shift(days=days_from_today).ceil('day')
))[::3]
# Get forecasts for each time-object
forecasts = [forecast.get_weather_at(_.datetime) for _ in time_range]
forecasts = [_ for _ in forecast if arrow.get(_["dt"]) in time_range]
# Get all temperatures for this day
daily_temp = [round(_.temperature(unit=temp_unit)['temp'],
ndigits=dec_temp) for _ in forecasts]
daily_temp = [round(_["main"]["temp"]) for _ in forecasts]
# Calculate min. and max. temp for this day
temp_range = f'{max(daily_temp)}°/{min(daily_temp)}°'
temp_range = f'{min(daily_temp)}°/{max(daily_temp)}°'
# Get all weather icon codes for this day
daily_icons = [_.weather_icon_name for _ in forecasts]
daily_icons = [_["weather"][0]["icon"] for _ in forecasts]
# Find most common element from all weather icon codes
status = max(set(daily_icons), key=daily_icons.count)
weekday = now.shift(days=days_from_today).format('ddd', locale=
self.locale)
weekday = now.shift(days=days_from_today).format('ddd', locale=self.locale)
return {'temp': temp_range, 'icon': status, 'stamp': weekday}
forecasts = [calculate_forecast(days) for days in range(1, 5)]
fc_data = {}
for forecast in forecasts:
fc_data['fc' + str(forecasts.index(forecast) + 1)] = {
'temp': forecast['temp'],
@ -455,13 +490,15 @@ class Weather(inkycal_module):
logger.debug((key, val))
# Get some current weather details
temperature = '{}°'.format(round(
weather.temperature(unit=temp_unit)['temp'], ndigits=dec_temp))
if dec_temp != 0:
temperature = f"{round(weather['main']['temp'])}°"
else:
temperature = f"{round(weather['main']['temp'],ndigits=dec_temp)}°"
weather_icon = weather.weather_icon_name
humidity = str(weather.humidity)
sunrise_raw = arrow.get(weather.sunrise_time()).to(self.timezone)
sunset_raw = arrow.get(weather.sunset_time()).to(self.timezone)
weather_icon = weather["weather"][0]["icon"]
humidity = str(weather["main"]["humidity"])
sunrise_raw = arrow.get(weather["sys"]["sunrise"]).to(self.timezone)
sunset_raw = arrow.get(weather["sys"]["sunset"]).to(self.timezone)
logger.debug(f'weather_icon: {weather_icon}')
@ -469,33 +506,29 @@ class Weather(inkycal_module):
logger.debug('using 12 hour format for sunrise/sunset')
sunrise = sunrise_raw.format('h:mm a')
sunset = sunset_raw.format('h:mm a')
elif self.hour_format == 24:
else:
# 24 hours format
logger.debug('using 24 hour format for sunrise/sunset')
sunrise = sunrise_raw.format('H:mm')
sunset = sunset_raw.format('H:mm')
# Format the windspeed to user preference
# Format the wind-speed to user preference
if self.use_beaufort:
logger.debug("using beaufort for wind")
wind = str(weather.wind(unit='beaufort')['speed'])
wind = str(self.mps_to_beaufort(weather["wind"]["speed"]))
else:
if self.units == 'metric':
logging.debug('getting windspeed in metric unit')
wind = str(weather.wind(unit='meters_sec')['speed']) + 'm/s'
elif self.units == 'imperial':
logging.debug('getting wind speed in meters per second')
wind = f"{weather['wind']['speed']} m/s"
else:
logging.debug('getting wind speed in imperial unit')
wind = str(weather.wind(unit='miles_hour')['speed']) + 'miles/h'
wind = f"{self.mps_to_mph(weather['wind']['speed'])} miles/h"
dec = decimal.Decimal
moonphase = get_moon_phase()
moon_phase = get_moon_phase()
# Fill weather details in col 1 (current weather icon)
draw_icon(im_colour, weather_icon_pos, (col_width, im_height),
weathericons[weather_icon])
weather_icons[weather_icon])
# Fill weather details in col 2 (temp, humidity, wind)
draw_icon(im_colour, temperature_icon_pos, (icon_small, row_height),
@ -521,7 +554,7 @@ class Weather(inkycal_module):
wind, font=self.font)
# Fill weather details in col 3 (moonphase, sunrise, sunset)
draw_icon(im_colour, moonphase_pos, (col_width, row_height), moonphase)
draw_icon(im_colour, moonphase_pos, (col_width, row_height), moon_phase)
draw_icon(im_colour, sunrise_icon_pos, (icon_small, icon_small), '\uf051')
write(im_black, sunrise_time_pos, (col_width - icon_small, row_height),
@ -535,7 +568,7 @@ class Weather(inkycal_module):
for pos in range(1, len(fc_data) + 1):
stamp = fc_data[f'fc{pos}']['stamp']
icon = weathericons[fc_data[f'fc{pos}']['icon']]
icon = weather_icons[fc_data[f'fc{pos}']['icon']]
temp = fc_data[f'fc{pos}']['temp']
write(im_black, eval(f'stamp_fc{pos}'), (col_width, row_height),
@ -548,7 +581,7 @@ class Weather(inkycal_module):
border_h = row3 + row_height
border_w = col_width - 3 # leave 3 pixels gap
# Add borders around each sub-section
# Add borders around each subsection
draw_border(im_black, (col1, row1), (col_width * 3 - 3, border_h),
shrinkage=(0, 0))

View File

@ -0,0 +1,164 @@
"""
Webshot module for Inkycal
by https://github.com/worstface
"""
from htmlwebshot import WebShot
from inkycal.custom import *
from inkycal.modules.inky_image import Inkyimage as Images
from inkycal.modules.template import inkycal_module
from tests import Config
logger = logging.getLogger(__name__)
class Webshot(inkycal_module):
name = "Webshot - Displays screenshots of webpages"
# required parameters
requires = {
"url": {
"label": "Please enter the url",
},
"palette": {
"label": "Which color palette should be used for the webshots?",
"options": ["bw", "bwr", "bwy"]
}
}
optional = {
"crop_x": {
"label": "Please enter the crop x-position",
},
"crop_y": {
"label": "Please enter the crop y-position",
},
"crop_w": {
"label": "Please enter the crop width",
},
"crop_h": {
"label": "Please enter the crop height",
}
}
def __init__(self, config):
super().__init__(config)
config = config['config']
self.url = config['url']
self.palette = config['palette']
if "crop_h" in config and isinstance(config["crop_h"], str):
self.crop_h = int(config["crop_h"])
else:
self.crop_h = 2000
if "crop_w" in config and isinstance(config["crop_w"], str):
self.crop_w = int(config["crop_w"])
else:
self.crop_w = 2000
if "crop_x" in config and isinstance(config["crop_x"], str):
self.crop_x = int(config["crop_x"])
else:
self.crop_x = 0
if "crop_y" in config and isinstance(config["crop_y"], str):
self.crop_y = int(config["crop_y"])
else:
self.crop_y = 0
# give an OK message
print(f'Inkycal webshot loaded')
def generate_image(self):
"""Generate image for this module"""
# Create tmp path
tmpFolder = Config.TEMP_PATH
if not os.path.exists(tmpFolder):
print(f"Creating tmp directory {tmpFolder}")
os.mkdir(tmpFolder)
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info('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')
im_colour = Image.new('RGB', size=im_size, color='white')
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
else:
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.params = {
"--crop-x": self.crop_x,
"--crop-y": self.crop_y,
"--crop-w": self.crop_w,
"--crop-h": self.crop_h,
}
logger.info(f'getting webshot from {self.url}...')
try:
shot.create_pic(url=self.url, output=f"{tmpFolder}/webshot.png")
except:
print(traceback.format_exc())
print("If you have not already installed wkhtmltopdf, please use: sudo apt-get install wkhtmltopdf. See here for more details: https://github.com/1Danish-00/htmlwebshot/")
raise Exception('Could not get webshot :/')
logger.info(f'got webshot...')
webshotSpaceBlack = Image.new('RGBA', (im_width, im_height), (255, 255, 255, 255))
webshotSpaceColour = Image.new('RGBA', (im_width, im_height), (255, 255, 255, 255))
im = Images()
im.load(f'{tmpFolder}/webshot.png')
im.remove_alpha()
imageAspectRatio = im_width / im_height
webshotAspectRatio = im.image.width / im.image.height
if webshotAspectRatio > imageAspectRatio:
imageScale = im_width / im.image.width
else:
imageScale = im_height / im.image.height
webshotHeight = int(im.image.height * imageScale)
im.resize(width=int(im.image.width * imageScale), height=webshotHeight)
im_webshot_black, im_webshot_colour = im.to_palette(self.palette)
webshotCenterPosY = int((im_height / 2) - (im.image.height / 2))
centerPosX = int((im_width / 2) - (im.image.width / 2))
webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY))
im_black.paste(webshotSpaceBlack)
webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY))
im_colour.paste(webshotSpaceColour)
im.clear()
logger.info(f'added webshot image')
# Save image of black and colour channel in image-folder
return im_black, im_colour

View File

@ -0,0 +1,202 @@
"""
Inkycal XKCD module
by https://github.com/worstface
"""
import xkcd
from inkycal.custom import *
from inkycal.modules.inky_image import Inkyimage as Images
from inkycal.modules.template import inkycal_module
logger = logging.getLogger(__name__)
class Xkcd(inkycal_module):
name = "xkcd - Displays comics from xkcd.com by Randall Munroe"
# required parameters
requires = {
"mode": {
"label": "Please select the mode",
"options": ["latest", "random"],
"default": "latest"
},
"palette": {
"label": "Which color palette should be used for the comic images?",
"options": ["bw", "bwr", "bwy"]
},
"alt": {
"label": "Would you like to add the alt text below the comic? If XKCD is not the only module you are showing, I recommend setting this to 'no'",
"options": ["yes", "no"],
"default": "no"
},
"filter": {
"label": "Would you like to add a scaling filter? If the is far too big to be shown in the space you've allotted for it, the module will try to find another image for you. This only applies in random mode. If XKCD is not the only module you are showing, I recommend setting this to 'no'.",
"options": ["yes", "no"],
"default": "no"
}
}
def __init__(self, config):
super().__init__(config)
config = config['config']
self.mode = config['mode']
self.palette = config['palette']
self.alt = config['alt']
self.scale_filter = config['filter']
# give an OK message
print(f'Inkycal XKCD loaded')
def generate_image(self):
"""Generate image for this module"""
# Create tmp path
tmpPath = f"{top_level}/temp"
if not os.path.exists(tmpPath):
os.mkdir(tmpPath)
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info('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')
im_colour = Image.new('RGB', size=im_size, color='white')
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
else:
raise Exception('Network could not be reached :/')
# Set some parameters for formatting feeds
line_spacing = 1
text_bbox = self.font.getbbox("hg")
line_height = text_bbox[3] + line_spacing
line_width = im_width
max_lines = im_height // (line_height + line_spacing)
logger.debug(f"max_lines: {max_lines}")
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
# Calculate line_positions
line_positions = [(0, spacing_top + _ * line_height) for _ in range(max_lines)]
logger.debug(f'line positions: {line_positions}')
logger.info(f'getting xkcd comic...')
if self.mode == 'random':
if self.scale_filter == 'no':
xkcdComic = xkcd.getRandomComic()
xkcdComic.download(output=tmpPath, outputFile='xkcdComic.png')
else:
perc = (2.1, 0.4)
url = "test variable, not a real comic"
while max(perc) > 1.75:
print("looking for another comic, old comic was: ", perc, url)
xkcdComic = xkcd.getRandomComic()
xkcdComic.download(output=tmpPath, outputFile='xkcdComic.png')
actual_size = Image.open(tmpPath + '/xkcdComic.png').size
perc = (actual_size[0] / im_width, actual_size[1] / im_height)
url = xkcdComic.getImageLink()
print("found one! perc: ", perc, url)
else:
xkcdComic = xkcd.getLatestComic()
xkcdComic.download(output=tmpPath, outputFile='xkcdComic.png')
logger.info(f'got xkcd comic...')
title_lines = []
title_lines.append(xkcdComic.getTitle())
altOffset = int(line_height * 1)
if self.alt == "yes":
alt_text = xkcdComic.getAltText() # get the alt text, too (I break it up into multiple lines later on)
# break up the alt text into lines
alt_lines = []
current_line = ""
for _ in alt_text.split(" "):
# this breaks up the alt_text into words and creates each line by adding
# one word at a time until the line is longer than the width of the module
# then it appends the line to the alt_lines array and starts testing a new line
# with the next word
text_bbox = self.font.getbbox(current_line + _ + " ")
if text_bbox[2] < im_width:
current_line = current_line + _ + " "
else:
alt_lines.append(current_line)
current_line = _ + " "
alt_lines.append(
current_line) # this adds the last line to the array (or the only line, if the alt text is really short)
altHeight = int(line_height * len(alt_lines)) + altOffset
else:
altHeight = 0 # this is added so that I don't need to add more "if alt is yes" conditionals when centering below. Now the centering code will work regardless of whether they want alttext or not
comicSpaceBlack = Image.new('RGBA', (im_width, im_height), (255, 255, 255, 255))
comicSpaceColour = Image.new('RGBA', (im_width, im_height), (255, 255, 255, 255))
im = Images()
im.load(f"{tmpPath}/xkcdComic.png")
im.remove_alpha()
imageAspectRatio = im_width / im_height
comicAspectRatio = im.image.width / im.image.height
if comicAspectRatio > imageAspectRatio:
imageScale = im_width / im.image.width
else:
imageScale = im_height / im.image.height
comicHeight = int(im.image.height * imageScale)
headerHeight = int(line_height * 3 / 2)
if comicHeight + (headerHeight + altHeight) > im_height:
comicHeight -= (headerHeight + altHeight)
im.resize(width=int(im.image.width * imageScale), height=comicHeight)
im_comic_black, im_comic_colour = im.to_palette(self.palette)
headerCenterPosY = int((im_height / 2) - ((im.image.height + headerHeight + altHeight) / 2))
comicCenterPosY = int((im_height / 2) - ((im.image.height + headerHeight + altHeight) / 2) + headerHeight)
altCenterPosY = int(
(im_height / 2) - ((im.image.height + headerHeight + altHeight) / 2) + headerHeight + im.image.height)
centerPosX = int((im_width / 2) - (im.image.width / 2))
comicSpaceBlack.paste(im_comic_black, (centerPosX, comicCenterPosY))
im_black.paste(comicSpaceBlack)
comicSpaceColour.paste(im_comic_colour, (centerPosX, comicCenterPosY))
im_colour.paste(comicSpaceColour)
im.clear()
logger.info(f'added comic image')
# Write the title on the black image
write(im_black, (0, headerCenterPosY), (line_width, line_height),
title_lines[0], font=self.font, alignment='center')
if self.alt == "yes":
# write alt_text
for _ in range(len(alt_lines)):
write(im_black, (0, altCenterPosY + _ * line_height + altOffset), (line_width, line_height),
alt_lines[_], font=self.font, alignment='left')
# Save image of black and colour channel in image-folder
return im_black, im_colour

View File

@ -1 +0,0 @@
from config import Config

View File

@ -1,56 +0,0 @@
# #!python3
# """
# inkycal_stocks unittest
# """
# import logging
# import sys
# import unittest
# from inkycal.modules import Stocks as Module
#
# from inkycal.modules.inky_image import Inkyimage
# from inkycal.tests import Config
# preview = Inkyimage.preview
# merge = Inkyimage.merge
#
# tests = [
# {
# "name": "Stocks",
# "config": {
# "size": [528, 30],
# "tickers": ['TSLA', 'AMD', 'NVDA', '^DJI', 'BTC-USD', 'EURUSD=X'],
# "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
# }
# },
# {
# "name": "Stocks",
# "config": {
# "size": [528, 50],
# "tickers": [],
# "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
# }
# }
# ]
#
#
# class module_test(unittest.TestCase):
# def test_get_config(self):
# print('getting data for web-ui...', end="")
# Module.get_config()
# print('OK')
#
# def test_generate_image(self):
# for test in tests:
# print(f'test {tests.index(test) + 1} generating image..')
# module = Module(test)
# im_black, im_colour = module.generate_image()
# print('OK')
# if Config.USE_PREVIEW:
# preview(merge(im_black, im_colour))
#
#
# if __name__ == '__main__':
# logger = logging.getLogger()
# logger.level = logging.DEBUG
# logger.addHandler(logging.StreamHandler(sys.stdout))
#
# unittest.main()

View File

@ -14,7 +14,7 @@ with open('requirements.txt') as f:
required = [i.split(' ')[0] for i in required]
__project__ = "inkycal"
__version__ = "2.0.0"
__version__ = "2.0.3"
__description__ = "Inkycal is a python3 software for syncing icalendar events, weather and news on selected E-Paper displays"
__packages__ = ["inkycal"]
__author__ = "aceisace"

1
tests/__init__.py Normal file
View File

@ -0,0 +1 @@
from .config import Config

View File

@ -1,4 +1,3 @@
#!python
"""
Tests config
"""
@ -28,6 +27,10 @@ class Config:
# inkycal_todoist_test
TODOIST_API_KEY = get("TODOIST_API_KEY")
TEMP_PATH = f"{basedir}/temp"
TEST_SETTINGS_PATH = f"{basedir}/settings.json"

View File

@ -1,6 +1,6 @@
{
"model": "epd_7_in_5_v3_colour",
"model": "image_file",
"update_interval": 5,
"orientation": 0,
"info_section": true,

View File

@ -1,56 +1,50 @@
#!python3
"""
iCalendar parser test (ical_parser)
"""
import logging
import os
import sys
import unittest
from urllib.request import urlopen
import arrow
from inkycal.modules.ical_parser import iCalendar
from inkycal.tests import Config
from tests import Config
ical = iCalendar()
test_ical = Config.TEST_ICAL_URL
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
class ical_parser_test(unittest.TestCase):
class TestIcalendar(unittest.TestCase):
def test_load_url(self):
print('testing loading via URL...', end="")
logger.info('testing loading via URL...')
ical.load_url(test_ical)
print('OK')
logger.info('OK')
def test_get_events(self):
print('testing parsing of events...', end="")
logger.info('testing parsing of events...')
ical.get_events(arrow.now(), arrow.now().shift(weeks=30))
print('OK')
logger.info('OK')
def test_sorting(self):
print('testing sorting of events...', end="")
logger.info('testing sorting of events...')
ical.sort()
print('OK')
logger.info('OK')
def test_show_events(self):
print('testing if events can be shown...', end="")
logger.info('testing if events can be shown...')
ical.show_events()
print('OK')
logger.info('OK')
def test_laod_from_file(self):
print('testing loading from file...', end="")
logger.info('testing loading from file...')
dummy = str(urlopen(test_ical, timeout=10).read().decode())
with open('dummy.ical', mode="w", encoding="utf-8") as file:
file.write(dummy)
ical.load_from_file('dummy.ical')
print('OK')
logger.info('OK')
os.remove('dummy.ical')
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@ -1,17 +1,19 @@
#!python3
"""
inkycal_agenda unittest
"""
import logging
import sys
import unittest
from inkycal.modules import Agenda as Module
from inkycal.modules import Agenda
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
sample_url = Config.SAMPLE_ICAL_URL
tests = [
@ -61,25 +63,13 @@ tests = [
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
class TestAgenda(unittest.TestCase):
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
logger.info(f'test {tests.index(test) + 1} generating image..')
module = Agenda(test)
im_black, im_colour = module.generate_image()
print('OK')
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@ -1,20 +1,21 @@
#!python3
"""
inkycal_calendar unittest
"""
import logging
import sys
import unittest
from inkycal.modules import Calendar as Module
from inkycal.modules import Calendar
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
sample_url = Config.SAMPLE_ICAL_URL
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
tests = [
{
"name": "Calendar",
@ -67,25 +68,13 @@ tests = [
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
class TestCalendar(unittest.TestCase):
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..', end="")
module = Module(test)
module = Calendar(test)
im_black, im_colour = module.generate_image()
print('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@ -1,17 +1,18 @@
#!python3
"""
inkycal_feeds unittest
"""
import logging
import sys
import unittest
from inkycal.modules import Feeds as Module
from inkycal.modules import Feeds
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
tests = [
{
"name": "Feeds",
@ -43,27 +44,14 @@ tests = [
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
class TestFeeds(unittest.TestCase):
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
logger.info(f'test {tests.index(test) + 1} generating image..')
module = Feeds(test)
im_black, im_colour = module.generate_image()
print('OK')
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
im = merge(im_black, im_colour)
im.show()
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@ -1,19 +1,16 @@
#!python3
"""
inkycal_image unittest
"""
import logging
import sys
import unittest
import requests
from PIL import Image
from inkycal.modules import Inkyimage as Module
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
@ -23,6 +20,9 @@ im = Image.open(requests.get(url, stream=True).raw)
im.save("test.png", "PNG")
test_path = "test.png"
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
tests = [
{
"name": "Inkyimage",
@ -104,25 +104,13 @@ tests = [
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
class TestInkyImage(unittest.TestCase):
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
logger.info(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@ -1,17 +1,19 @@
#!python3
"""
inkycal_jokes unittest
"""
import logging
import sys
import unittest
from inkycal.modules import Jokes as Module
from inkycal.modules import Jokes
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
tests = [
{
"name": "Jokes",
@ -46,25 +48,13 @@ tests = [
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
class TestJokes(unittest.TestCase):
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
logger.info(f'test {tests.index(test) + 1} generating image..')
module = Jokes(test)
im_black, im_colour = module.generate_image()
print('OK')
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@ -1,18 +1,16 @@
#!python3
"""
Slideshow test (inkycal_slideshow)
"""
import logging
import os
import sys
import unittest
import requests
from PIL import Image
from inkycal.modules import Slideshow as Module
from inkycal.modules import Slideshow
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
@ -31,6 +29,9 @@ for count, url in enumerate(im_urls):
test_path = "tmp"
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
tests = [
{
"name": "Slideshow",
@ -134,24 +135,20 @@ tests = [
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
class TestSlideshow(unittest.TestCase):
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
logger.info(f'test {tests.index(test) + 1} generating image..')
module = Slideshow(test)
im_black, im_colour = module.generate_image()
print('OK')
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
def test_switch_to_next_image(self):
print(f'testing switching to next images..')
module = Module(tests[0])
logger.info(f'testing switching to next images..')
module = Slideshow(tests[0])
im_black, im_colour = module.generate_image()
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
@ -164,12 +161,4 @@ class module_test(unittest.TestCase):
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
print('OK')
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()
logger.info('OK')

57
tests/test_inkycal_stocks.py Executable file
View File

@ -0,0 +1,57 @@
import logging
import unittest
from inkycal.modules import Stocks
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
tests = [
{
"position": 1,
"name": "Stocks",
"config": {
"size": [400, 100],
"tickers": ['TSLA', 'AMD', 'NVDA', '^DJI', 'BTC-USD', 'EURUSD=X'],
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"position": 1,
"name": "Stocks",
"config": {
"size": [400, 200],
"tickers": [],
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"position": 1,
"name": "Stocks",
"config": {
"size": [400, 300],
"tickers": ['TSLA', 'AMD', 'NVDA', '^DJI', 'BTC-USD', 'EURUSD=X'],
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"position": 1,
"name": "Stocks",
"config": {
"size": [400, 400],
"tickers": ['TSLA', 'AMD', 'NVDA', '^DJI', 'BTC-USD', 'EURUSD=X'],
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
}
]
class TestStocks(unittest.TestCase):
def test_generate_image(self):
for test in tests:
logger.info(f'test {tests.index(test) + 1} generating image..')
module = Stocks(test)
module.generate_image()
logger.info('OK')

View File

@ -1,16 +1,20 @@
#!python3
"""
Inkycal Text module
"""
import logging
import os
import sys
import unittest
from inkycal.modules import TextToDisplay as Module
from inkycal.modules import TextToDisplay
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
file_path = None
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
dummy_data = [
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', ' Donec feugiat facilisis neque vel blandit.',
@ -50,13 +54,15 @@ dummy_data = [
'Duis facilisis sapien est, a elementum lorem maximus ut.'
]
temp_path = f"{Config.TEMP_PATH}/temp.txt"
tests = [
{
"position": 1,
"name": "TextToFile",
"config": {
"size": [500, 100],
"filepath": file_path,
"filepath": temp_path,
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
@ -68,7 +74,7 @@ tests = [
"name": "TextToFile",
"config": {
"size": [500, 400],
"filepath": file_path,
"filepath": "https://raw.githubusercontent.com/aceinnolab/Inkycal/main/setup.py",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
@ -80,49 +86,23 @@ tests = [
class TestTextToDisplay(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
delete_file_after_parse = False
if not file_path:
delete_file_after_parse = True
print("Filepath does not exist. Creating dummy file")
tmp_path = "tmp.txt"
with open(tmp_path, mode="w", encoding="utf-8") as file:
def setUp(self):
self.temp_path = temp_path
if not os.path.exists(Config.TEMP_PATH):
os.mkdir(Config.TEMP_PATH)
with open(self.temp_path, encoding="utf-8", mode="w") as file:
file.writelines(dummy_data)
# update tests with new temp path
def test_generate_image(self):
for test in tests:
test["config"]["filepath"] = tmp_path
else:
make_request = bool(file_path.startswith("https://"))
if not make_request and not os.path.exists(file_path):
raise FileNotFoundError("Your text file could not be found")
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
logger.info(f'test {tests.index(test) + 1} generating image..')
module = TextToDisplay(test)
im_black, im_colour = module.generate_image()
print('OK')
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
im = merge(im_black, im_colour)
im.show()
if delete_file_after_parse:
print("cleaning up temp file")
os.remove("tmp.txt")
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()
def tearDown(self):
if os.path.exists(self.temp_path):
logger.info("deleting temporary file.")
os.remove(self.temp_path)

View File

@ -1,19 +1,21 @@
#!python3
"""
inkycal_todoist unittest
"""
import logging
import sys
import unittest
from inkycal.modules import Todoist as Module
from inkycal.modules import Todoist
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
api_key = Config.TODOIST_API_KEY
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
tests = [
{
"name": "Todoist",
@ -30,30 +32,16 @@ tests = [
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
class TestTodoist(unittest.TestCase):
def test_generate_image(self):
if api_key:
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
module = Todoist(test)
im_black, im_colour = module.generate_image()
print('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()
else:
print('No api key given, omitting test')
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@ -1,19 +1,21 @@
#!python3
"""
inkycal_weather unittest
"""
import logging
import sys
import unittest
from inkycal.modules import Weather as Module
from inkycal.modules import Weather
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
from tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
owm_api_key = Config.OPENWEATHERMAP_API_KEY
location = 'Stuttgart, DE'
location = '2825297'
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
tests = [
{
@ -171,26 +173,14 @@ tests = [
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
class TestWeather(unittest.TestCase):
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
logger.info(f'test {tests.index(test) + 1} generating image..')
module = Weather(test)
im_black, im_colour = module.generate_image()
print('OK')
logger.info('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()
merged = merge(im_black, im_colour)
preview(merged)

65
tests/test_inkycal_webshot.py Executable file
View File

@ -0,0 +1,65 @@
"""
Test Inkycal Webshot Module
"""
import logging
import unittest
from inkycal.modules import Webshot
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
tests = [
{
"position": 1,
"name": "Webshot",
"config": {
"size": [400, 100],
"url": "https://www.catsuthecat.com/blogs/comics/the-one-about-regeneration",
"palette": "bwr",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"position": 1,
"name": "Webshot",
"config": {
"size": [400, 200],
"url": "https://www.catsuthecat.com/blogs/comics/the-one-about-crazy-friday-nights",
"palette": "bwy",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"position": 1,
"name": "Webshot",
"config": {
"size": [400, 300],
"url": "https://www.catsuthecat.com/blogs/comics/the-one-about-teamwork",
"palette": "bw",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"position": 1,
"name": "Webshot",
"config": {
"size": [400, 400],
"url": "https://www.catsuthecat.com/blogs/comics/the-one-about-addictions-1",
"palette": "bwr",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
}
]
class TestWebshot(unittest.TestCase):
def test_generate_image(self):
for test in tests:
logger.info(f'test {tests.index(test) + 1} generating image..')
module = Webshot(test)
module.generate_image()
logger.info('OK')

View File

@ -0,0 +1,72 @@
"""
Test Inkycal XKCD Module
"""
import logging
import unittest
from inkycal.modules.inkycal_xkcd import Xkcd
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
tests = [
{
"position": 1,
"name": "XKCD",
"config": {
"size": [400, 300],
"mode": "latest",
"palette": "bwr",
"alt": "no",
"filter": "yes",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"position": 1,
"name": "XKCD",
"config": {
"size": [400, 300],
"mode": "random",
"palette": "bw",
"alt": "no",
"filter": "no",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"position": 1,
"name": "XKCD",
"config": {
"size": [400, 400],
"mode": "latest",
"palette": "bwy",
"alt": "no",
"filter": "yes",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"position": 1,
"name": "XKCD",
"config": {
"size": [400, 500],
"mode": "random",
"palette": "bwr",
"alt": "yes",
"filter": "no",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
}
]
class TestXkcd(unittest.TestCase):
def test_generate_image(self):
for test in tests:
logger.info(f'test {tests.index(test) + 1} generating image..')
xkcd = Xkcd(test)
xkcd.generate_image()
logger.info('OK')

28
tests/test_main.py Normal file
View File

@ -0,0 +1,28 @@
"""
Test main module
"""
import unittest
from inkycal import Inkycal
from tests import Config
class TestMain(unittest.TestCase):
def setUp(self):
self.settings_path = Config.TEST_SETTINGS_PATH
def test_init(self):
inkycal = Inkycal(self.settings_path, render=False)
assert inkycal.settings["model"] == "image_file"
assert inkycal.settings["update_interval"] == 5
assert inkycal.settings["orientation"] == 0
assert inkycal.settings["info_section"] == True
assert inkycal.settings["info_section_height"] == 70
assert inkycal.settings["border_around_modules"] == True
def test_run(self):
inkycal = Inkycal(self.settings_path, render=False)
inkycal.test()