Merge branch 'main' into weather_scaling
This commit is contained in:
commit
a7c3edc60e
@ -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"],
|
||||
|
||||
|
35
.github/CODE_OF_CONDUCT.md
vendored
35
.github/CODE_OF_CONDUCT.md
vendored
@ -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.
|
||||
|
60
.github/CONTRIBUTING.md
vendored
60
.github/CONTRIBUTING.md
vendored
@ -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?
|
||||
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
29
.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
vendored
Normal file
29
.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
vendored
Normal 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_
|
13
.github/workflows/test-on-rpi.yml
vendored
13
.github/workflows/test-on-rpi.yml
vendored
@ -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
|
||||
|
8
.github/workflows/update-os.yml
vendored
8
.github/workflows/update-os.yml
vendored
@ -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
2
.gitignore
vendored
@ -146,7 +146,7 @@ dmypy.json
|
||||
/logs
|
||||
|
||||
# inkycal tests
|
||||
/inkycal/tests/tmp/
|
||||
/tests/tmp/
|
||||
!/inkycal/tests/*.py
|
||||
/docsource/._build/
|
||||
|
||||
|
167
README.md
167
README.md
@ -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:
|
||||
|
||||
[](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/)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,2 +1,3 @@
|
||||
from .functions import *
|
||||
from .inkycal_exceptions import *
|
||||
from .openweathermap_wrapper import OpenWeatherMap
|
@ -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)
|
43
inkycal/custom/openweathermap_wrapper.py
Normal file
43
inkycal/custom/openweathermap_wrapper.py
Normal 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
|
||||
|
@ -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:
|
||||
|
@ -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 ###
|
227
inkycal/main.py
227
inkycal/main.py
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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"""
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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))
|
||||
|
||||
|
164
inkycal/modules/inkycal_webshot.py
Normal file
164
inkycal/modules/inkycal_webshot.py
Normal 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
|
202
inkycal/modules/inkycal_xkcd.py
Normal file
202
inkycal/modules/inkycal_xkcd.py
Normal 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
|
@ -1 +0,0 @@
|
||||
from config import Config
|
@ -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()
|
2
setup.py
2
setup.py
@ -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
1
tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .config import Config
|
@ -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"
|
||||
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
|
||||
{
|
||||
"model": "epd_7_in_5_v3_colour",
|
||||
"model": "image_file",
|
||||
"update_interval": 5,
|
||||
"orientation": 0,
|
||||
"info_section": true,
|
@ -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()
|
@ -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()
|
@ -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()
|
@ -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()
|
@ -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()
|
@ -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()
|
@ -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
57
tests/test_inkycal_stocks.py
Executable 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')
|
||||
|
@ -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)
|
@ -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()
|
@ -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
65
tests/test_inkycal_webshot.py
Executable 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')
|
||||
|
72
tests/test_inkycal_xkcd.py
Normal file
72
tests/test_inkycal_xkcd.py
Normal 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
28
tests/test_main.py
Normal 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()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user