diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index 8acb810e..9641a761 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -24,7 +24,8 @@ jobs: TINDIE_USERNAME: ${{ secrets.TINDIE_USERNAME }} with: # Set the base_image to the desired Raspberry Pi OS version - base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2024-03-15/2024-03-15-raspios-bookworm-armhf-lite.img.xz + # note: version 2023-12-11 seems to have issues with the kernel and gpio + base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf-lite.img.xz image_additional_mb: 3072 # enlarge free space to 3 GB optimize_image: true commands: | @@ -69,10 +70,11 @@ jobs: echo $CWD # increase swap-size - sudo dphys-swapfile swapoff - sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=512/' /etc/dphys-swapfile - sudo dphys-swapfile setup - sudo dphys-swapfile swapon + # temporarily disabled due to unmounting issues + # sudo dphys-swapfile swapoff + # sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=512/' /etc/dphys-swapfile + # sudo dphys-swapfile setup + # sudo dphys-swapfile swapon # enable SPI sudo sed -i s/#dtparam=spi=on/dtparam=spi=on/ /boot/config.txt @@ -85,7 +87,8 @@ jobs: sudo chown -R inky:inky /home/inky/Inkycal # make all users require a password for sudo commands (improves security) - echo 'ALL ALL=(ALL:ALL) PASSWD: ALL' | sudo tee -a /etc/sudoers.d/010_require_sudo_password + # temporarily disabled to allow pisugar support + # echo 'ALL ALL=(ALL:ALL) PASSWD: ALL' | sudo tee -a /etc/sudoers.d/010_require_sudo_password # allow some time to unmount sleep 10 diff --git a/Changelog.md b/Changelog.md index cc5fc696..e817e025 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,13 +1,47 @@ -# E-Paper-Calendar Software Changelog +# Inkycal Software Changelog All significant changes will be documented in this file. The order is from latest to oldest and structured in the following way: * Version name with date of publishing -* Sections with either 'added', 'fixed', 'updated' and 'changed' +* Sections with either 'added', 'fixed', 'updated', 'changed' or 'removed' to describe the changes ## [2.0.3] 2024 +### Changed +- Updated dependencies to the most-recent supported version +- Unified logging all over the library. Print statements are now rare. This makes it easier to identify why Inkycal isn't working without having to look up the logs +- Inkycal now makes use of a JSON-Cache to make it more resilient against resets etc. For example, the slideshow module will remember the last index even after a shutdown +- Inkycal now uses a list of supported displays instead of having to look up each driver in the driver directory +- Renamed tests according to python standards, starting with `test_..`, allowing unittest/pytest to automatically discover and run these tests. + +### Fixed +- Fixed an annoying vertical alignment issue causing some characters to look chopped off +- Fixed the alignment of the red-circle on the calendar module +- Fixed weekday-names not translating in the weather module +- Fixed python 3.11 issues with numpy on Raspberry Pi OS + ### Added -* Added fullscreen weather module -* Own OWM API abstraction as a replacement for PyOWM module +- Added long-awaited support of PiSugar v1/2/3. Still a bit experimental (no calibration handling), but works for most part. If PiSugar support is enabled, Inkycal will set the new alarm before shutting down the system, increasing battery life. Please note that around 70 updates were possible with the 1200mAh PiSugar 3 board, so one update a day to three should be max to get at least one month battery life. +- Added Webshot module which can be used to display a webpage. Works on InkycalOS-Lite too and does not need a GUI. +- Added XKCD module +- Added Tindie module +- Added support for much longer update-intervals than the previous max of once every 60 minutes +- Added Material-UI icons font +- Added dedicated Pipeline for unittests directly on Raspberry Pi OS to ensure Inkycal can run reliably on Raspberry Pi OS +- Added Feature-request and PR template +- Added support for 5.83" display (v2) +- Added support for 12.48" display on 64-bit systems +- Added Inkycal fullweather-module +- Added `settings.py file (not to be confused with `settings.json`) to set VCOM and other internal variables + + +## [2.0.3] 2023 +### Changed +- Switched from pyowm to custom wrapper as pyowm only works up to python3.9, which is now outdated. +- Updated dependencies to the most-recent supported version + +### Fixed +- Fixed python 3.11 issues with numpy on Raspberry Pi OS +- Fixed compatibility issues with Pillow when switching from v9.x to v10.x, particularly font width and height operations +- Renamed tests according to python standards, starting with `test_..`, allowing unittest/pytest to automatically discover and run these tests. ## [2.0.2] 2022 diff --git a/README.md b/README.md index 6663b974..20a9dd2e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Welcome to inkycal v2.0.3! +# Welcome to inkycal v2.0.4!

@@ -29,7 +29,7 @@ ready-to-flash version of Inkycal called InkycalOS-Lite with everything pre-inst via [GitHub Sponsors](https://github.com/sponsors/aceisace). This helps keep up maintenance costs, implement new features and fixing bugs. Please choose the one-time sponsor option and select the one with the plug-and-play version of Inkycal. Then, send your email-address to which InkycalOS-Lite should be sent. -Alternatively, you can also use the paypal.me link and send the same amount as Github sponsors to get access to +Alternatively, you can also use the PayPal.me link and send the same amount as GitHub sponsors to get access to InkycalOS-Lite! ## Main features @@ -42,10 +42,13 @@ following built-in modules are supported: * Image - Display an Image from URL or local file path. * Slideshow - Cycle through images in a given folder and show them on the E-Paper. * Feeds - Synchronise RSS/ATOM feeds from your favorite providers. -* Stocks - Display stocks using Tickers from Yahoo! Finance. +* Stocks - Display stocks using Tickers from Yahoo! Finance. Special thanks to @worstface * Weather - Show current weather, daily or hourly weather forecasts from openweathermap. * Todoist - Synchronise with Todoist app or website to show todos. * iCanHazDad - Display a random joke from [iCanHazDad.com](iCanhazdad.com). +* Webshot - Display a website as an image. Special thanks to @worstface +* Tindie - Show the latest orders from your Tindie store. +* XKCD - Show XKCD comics. Special thanks to @worstface ## Quickstart @@ -56,7 +59,8 @@ Watch the one-minute video on getting started with Inkycal: ## Hardware guide Before you can start, please ensure you have one of the supported displays and of the supported Raspberry -Pi: `|4|3A|3B|3B+|2B|ZeroW|ZeroWH|Zero2W|`. We personally recommend the Raspberry Pi Zero W as this is relatively cheaper, uses +Pi: `|4|3A|3B|3B+|2B|ZeroW|ZeroWH|Zero2W|`. We personally recommend the Raspberry Pi Zero W as this is relatively +cheaper, uses less power and is perfect to fit in a small photo frame once you have assembled everything. **Serial** displays are usually cheaper, but slower. Their main advantage is ease of use, like being able to communicate @@ -74,26 +78,27 @@ grayscale levels, which does not compare to the 256 grayscales of LCDs, but far links below may or may not contain the required driver board. Please ensure you get the correct driver board for the display!** -| type | vendor | Where to buy | -|---------------------------------------------------------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 7.5" Inkycal (plug-and-play) | Aceinnolab (author) |  [Buy on Tindie](https://www.tindie.com/products/aceisace4444/inkycal-build-v1/) Pre-configured version of Inkycal with custom frame and a web-ui. You do not need to buy anything extra. Includes Raspberry Pi Zero W, 7.5" e-paper, microSD card, driver board, custom packaging and 1m of cable. Comes pre-assembled for plug-and-play. | -| Inkycal frame (kit -> requires wires, 7.5" Display and Zero W with microSD card | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-frame-custom-driver-board-only/) Ultraslim frame with custom-made front and backcover inkl. ultraslim driver board). You will need a Raspberry Pi, microSD card and a 7.5" e-paper display | -| Driver board | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/universal-e-paper-driver-board-for-24-pin-spi/) Ultraslim, 24-pin SPI driver board for many serial e-paper displays. | -| `[serial]` 12.48" (1304×984px) display | waveshare / gooddisplay |  Search for `Waveshare 12.48" E-Paper 1304×984` on amazon or similar | -| `[serial]` 7.5" (640x384px) -> v1 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 640x384` on amazon or similar | -| `[serial]` 7.5" (800x480px) -> v2 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x480` on amazon or similar | -| `[serial]` 7.5" (880x528px) -> v3 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x528` on amazon or similar | -| `[serial]` 5.83" (400x300px) display | waveshare / gooddisplay | Search for `Waveshare 5.83" E-Paper 400x300` on amazon or similar | -| `[serial]` 4.2" (400x300px)display | waveshare / gooddisplay | Search for `Waveshare 4.2" E-Paper 400x300` on amazon or similar | | -| `[parallel]` 10.3" (1872×1404px) display | waveshare / gooddisplay |  Search for `Waveshare 10.3" E-Paper 1872×1404` on amazon or similar | -| `[parallel]` 9.7" (1200×825px) display | waveshare / gooddisplay | Search for `Waveshare 9.7" E-Paper 1200×825` on amazon or similar | -| `[parallel]` 7.8" (1872×1404px) display | waveshare / gooddisplay |  Search for `Waveshare 7.8" E-Paper 1872×1404` on amazon or similar | -| Raspberry Pi Zero W | Raspberry Pi |  Search for `Raspberry Pi Zero W` on amazon or similar | -| MicroSD card | Sandisk |  Search for `MicroSD card 8GB` on amazon or similar | +| type | vendor | Where to buy | +|---------------------------------------------------------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 12.48" Inkycal (plug-and-play) | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-1248-build/) Pre-configured version of Inkycal with matte black aluminium designer frame and a web-ui. You do not need to buy anything extra. Includes Raspberry Pi Zero W, 12.48" e-paper, microSD card, driver board, custom packaging and 1m of cable. Comes pre-assembled for plug-and-play. | +| 7.5" Inkycal (plug-and-play) | Aceinnolab (author) |  [Buy on Tindie](https://www.tindie.com/products/aceisace4444/inkycal-build-v1/) Pre-configured version of Inkycal with custom frame and a web-ui. You do not need to buy anything extra. Includes Raspberry Pi Zero W, 7.5" e-paper, microSD card, driver board, custom packaging and 1m of cable. Comes pre-assembled for plug-and-play. | +| Inkycal frame (kit -> requires wires, 7.5" Display and Zero W with microSD card | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-frame-custom-driver-board-only/) Ultraslim frame with custom-made front and backcover inkl. ultraslim driver board). You will need a Raspberry Pi, microSD card and a 7.5" e-paper display | +| Driver board | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/universal-e-paper-driver-board-for-24-pin-spi/) Ultraslim, 24-pin SPI driver board for many serial e-paper displays. | +| `[serial]` 12.48" (1304×984px) display | waveshare / gooddisplay |  Search for `Waveshare 12.48" E-Paper 1304×984` on amazon or similar | +| `[serial]` 7.5" (640x384px) -> v1 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 640x384` on amazon or similar | +| `[serial]` 7.5" (800x480px) -> v2 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x480` on amazon or similar | +| `[serial]` 7.5" (880x528px) -> v3 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x528` on amazon or similar | +| `[serial]` 5.83" (400x300px) display | waveshare / gooddisplay | Search for `Waveshare 5.83" E-Paper 400x300` on amazon or similar | +| `[serial]` 4.2" (400x300px)display | waveshare / gooddisplay | Search for `Waveshare 4.2" E-Paper 400x300` on amazon or similar | | +| `[parallel]` 10.3" (1872×1404px) display | waveshare / gooddisplay |  Search for `Waveshare 10.3" E-Paper 1872×1404` on amazon or similar | +| `[parallel]` 9.7" (1200×825px) display | waveshare / gooddisplay | Search for `Waveshare 9.7" E-Paper 1200×825` on amazon or similar | +| `[parallel]` 7.8" (1872×1404px) display | waveshare / gooddisplay |  Search for `Waveshare 7.8" E-Paper 1872×1404` on amazon or similar | +| Raspberry Pi Zero W | Raspberry Pi |  Search for `Raspberry Pi Zero W` on amazon or similar | +| MicroSD card | Sandisk |  Search for `MicroSD card 8GB` on amazon or similar | ## Configuring the Raspberry Pi -Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager](https://rptl.io/imager). -Use the following settings: + +Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager](https://rptl.io/imager). Please use this version of [Raspberry Pi OS - bookworm](https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf-lite.img.xz) as the latest release is known to have some issues with the latest kernel update. | option | value | |:--------------------------|:---------------------------:| @@ -163,13 +168,19 @@ top of the repo to get access to Inkycal-OS-Lite. Alternatively, you can also us amount as GitHub sponsors to get access to InkycalOS-Lite! This will help keep this project growing and cover the ongoing expenses too! Win-win for everyone! 🎊 +### Bonus: PiSugar support +The PiSugar is a battery pack for the Raspberry Pi Zero W. It can be used to power the Raspberry Pi and the e-paper, allowing battery life up to several weeks. +If you have a PiSugar board, please see the wiki page on how to install the PiSugar driver and configure Inkycal to work with it: +[PiSugar support](https://github.com/aceinnolab/Inkycal/wiki/PiSugar-support) + + ### Manual installation Run the following steps to install Inkycal. Do **not** use sudo for this, except where explicitly specified. ```bash # Raspberry Pi specific section start -sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev +sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev git clone https://github.com/WiringPi/WiringPi cd WiringPi ./build diff --git a/clear_display.py b/clear_display.py deleted file mode 100644 index be726f3f..00000000 --- a/clear_display.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Clears the display of any content. -""" -from inkycal import Inkycal - -print("loading Inkycal and display driver...") -inky = Inkycal(render=True) # Initialise Inkycal -print("clearing display...") -inky.calibrate(cycles=1) # Calibrate the display -print("clear complete...") - -print("finished!") diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js index 138ae34f..d5f566df 100644 --- a/docs/_static/documentation_options.js +++ b/docs/_static/documentation_options.js @@ -1,5 +1,5 @@ const DOCUMENTATION_OPTIONS = { - VERSION: '2.0.3', + VERSION: '2.0.4', LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/_static/searchtools.js b/docs/_static/searchtools.js index 92da3f8b..b08d58c9 100644 --- a/docs/_static/searchtools.js +++ b/docs/_static/searchtools.js @@ -178,7 +178,7 @@ const Search = { htmlToText: (htmlString, anchor) => { const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); - for (const removalQuery of [".headerlinks", "script", "style"]) { + for (const removalQuery of [".headerlink", "script", "style"]) { htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); } if (anchor) { @@ -328,13 +328,14 @@ const Search = { for (const [title, foundTitles] of Object.entries(allTitles)) { if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { for (const [file, id] of foundTitles) { - let score = Math.round(100 * queryLower.length / title.length) + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles normalResults.push([ docNames[file], titles[file] !== title ? `${titles[file]} > ${title}` : title, id !== null ? "#" + id : "", null, - score, + score + boost, filenames[file], ]); } diff --git a/docs/about.html b/docs/about.html index 2fa7e792..1adec597 100644 --- a/docs/about.html +++ b/docs/about.html @@ -4,7 +4,7 @@ - About Inkycal — inkycal 2.0.3 documentation + About Inkycal — inkycal 2.0.4 documentation @@ -15,7 +15,7 @@ - + diff --git a/docs/dev_doc.html b/docs/dev_doc.html index 269304c3..6282926d 100644 --- a/docs/dev_doc.html +++ b/docs/dev_doc.html @@ -4,7 +4,7 @@ - Developer documentation — inkycal 2.0.3 documentation + Developer documentation — inkycal 2.0.4 documentation @@ -15,7 +15,7 @@ - + diff --git a/docs/genindex.html b/docs/genindex.html index 4bca78f3..de6517a3 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -3,7 +3,7 @@ - Index — inkycal 2.0.3 documentation + Index — inkycal 2.0.4 documentation @@ -14,7 +14,7 @@ - + @@ -129,6 +129,10 @@

D

+
@@ -253,6 +257,10 @@

P

+
@@ -285,10 +293,6 @@

S

T

-
@@ -130,7 +131,7 @@ Copyright by aceinnolab

-class inkycal.main.Inkycal(settings_path: str = None, render: bool = True)
+class inkycal.main.Inkycal(settings_path: str = None, render: bool = True, use_pi_sugar: bool = False, shutdown_after_run: bool = False)

Inkycal main class

Main class of Inkycal, test and run the main Inkycal program.

@@ -157,35 +158,21 @@
countdown(interval_mins: int = None) int
-

Returns the remaining time in seconds until next display update.

+

Returns the remaining time in seconds until the next display update based on the interval.

-
Args:
    -
  • -
    interval_mins = int -> the interval in minutes for the update

    if no interval is given, the value from the settings file is used.

    +
    Args:
    +
    interval_mins (int): The interval in minutes for the update. If none is given, the value

    from the settings file is used.

    -
  • -
-
Returns:
    -
  • int -> the remaining time in seconds until next update

  • -
+
Returns:

int: The remaining time in seconds until the next update.

-
-async run()
-

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 scheduled update.

-
- -
-
-test()
+
+dry_run()

Tests if Inkycal can run without issues.

Attempts to import module names from settings file. Loads the config for each module and initializes the module. Tries to run the module and @@ -193,6 +180,28 @@

Generated images can be found in the /images folder of Inkycal.

+
+
+process_module(number) bool
+

Process individual module to generate images and handle exceptions.

+
+ +
+
+async run(run_once=False)
+

Runs main program in nonstop mode or a single iteration based on the run_once flag.

+
+
Args:
+
run_once (bool): If True, runs the updating process once and stops. If False,

runs indefinitely.

+
+
+
+
+

Uses an infinity loop to run Inkycal nonstop or a single time based on run_once. +Inkycal generates the image from all modules, assembles them in one image, +refreshes the E-Paper and then sleeps until the next scheduled update or exits.

+
+
@@ -232,14 +241,14 @@
-inkycal.custom.functions.draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1))
+inkycal.custom.functions.draw_border(image: <module 'PIL.Image' from '/home/runner/work/Inkycal/Inkycal/venv/lib/python3.11/site-packages/PIL/Image.py'>, xy: ~typing.Tuple[int, int], size: ~typing.Tuple[int, int], radius: int = 5, thickness: int = 1, shrinkage: ~typing.Tuple[int, int] = (0.1, 0.1)) None

Draws a border at given coordinates.

Args:
  • image: The image on which the border should be drawn (usually im_black or -im_colour.

  • +im_colour).

  • xy: Tuple representing the top-left corner of the border e.g. (32, 100) -where 32 is the x co-ordinate and 100 is the y-coordinate.

  • +where 32 is the x-coordinate and 100 is the y-coordinate.

  • size: Size of the border as a tuple -> (width, height).

  • radius: Radius of the corners, where 0 = plain rectangle, 5 = round corners.

  • thickness: Thickness of the border in pixels.

  • @@ -288,14 +297,14 @@

    The extracted timezone can be used to show the local time instead of UTC. e.g.

    >>> import arrow
     >>> print(arrow.now()) # returns non-timezone-aware time
    ->>> print(arrow.now(tz=get_system_tz()) # prints timezone aware time.
    +>>> print(arrow.now(tz=get_system_tz())) # prints timezone aware time.
     
-inkycal.custom.functions.internet_available()
+inkycal.custom.functions.internet_available() bool

checks if the internet is available.

Attempts to connect to google.com with a timeout of 5 seconds to check if the network can be reached.

@@ -315,7 +324,7 @@
-inkycal.custom.functions.text_wrap(text, font=None, max_width=None)
+inkycal.custom.functions.text_wrap(text: str, font=None, max_width=None)

Splits a very long text into smaller parts

Splits a long text to smaller lines which can fit in a line with max_width. Uses a Font object for more accurate calculations.

@@ -334,7 +343,7 @@
-inkycal.custom.functions.write(image, xy, box_size, text, font=None, **kwargs)
+inkycal.custom.functions.write(image: <module 'PIL.Image' from '/home/runner/work/Inkycal/Inkycal/venv/lib/python3.11/site-packages/PIL/Image.py'>, xy: ~typing.Tuple[int, int], box_size: ~typing.Tuple[int, int], text: str, font=None, **kwargs)

Writes text on an image.

Writes given text at given position on the specified image.

diff --git a/docs/objects.inv b/docs/objects.inv index df0d12b8..e54dc411 100644 Binary files a/docs/objects.inv and b/docs/objects.inv differ diff --git a/docs/py-modindex.html b/docs/py-modindex.html index 063dd06e..9bb91e7a 100644 --- a/docs/py-modindex.html +++ b/docs/py-modindex.html @@ -3,7 +3,7 @@ - Python Module Index — inkycal 2.0.3 documentation + Python Module Index — inkycal 2.0.4 documentation @@ -14,7 +14,7 @@ - + diff --git a/docs/quickstart.html b/docs/quickstart.html index d09f5baa..be927d04 100644 --- a/docs/quickstart.html +++ b/docs/quickstart.html @@ -4,7 +4,7 @@ - Quickstart — inkycal 2.0.3 documentation + Quickstart — inkycal 2.0.4 documentation @@ -15,7 +15,7 @@ - + diff --git a/docs/search.html b/docs/search.html index 495e5fbd..5923959d 100644 --- a/docs/search.html +++ b/docs/search.html @@ -3,7 +3,7 @@ - Search — inkycal 2.0.3 documentation + Search — inkycal 2.0.4 documentation @@ -15,7 +15,7 @@ - + diff --git a/docs/searchindex.js b/docs/searchindex.js index 669170d9..0015f4d1 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"About Inkycal": [[0, "about-inkycal"]], "Contents:": [[2, null]], "Creating settings file": [[4, "creating-settings-file"]], "Custom functions": [[3, "module-inkycal.custom.functions"]], "Developer documentation": [[1, "developer-documentation"]], "Display": [[3, "module-inkycal.display.Display"]], "Helper classes": [[3, "module-inkycal.modules.ical_parser"]], "Indices and tables": [[2, "indices-and-tables"]], "Inkycal": [[3, "module-inkycal.main"]], "Inkycal documentation": [[2, "inkycal-documentation"]], "Installing Inkycal": [[4, "installing-inkycal"]], "Quickstart": [[4, "quickstart"]]}, "docnames": ["about", "dev_doc", "index", "inkycal", "quickstart"], "envversion": {"sphinx": 61, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["about.md", "dev_doc.md", "index.rst", "inkycal.rst", "quickstart.md"], "indexentries": {"all_day() (inkycal.modules.ical_parser.icalendar static method)": [[3, "inkycal.modules.ical_parser.iCalendar.all_day", false]], "auto_fontsize() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.auto_fontsize", false]], "autoflip() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.autoflip", false]], "calibrate() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.calibrate", false]], "clear() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.clear", false]], "clear_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.clear_events", false]], "countdown() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.countdown", false]], "draw_border() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.draw_border", false]], "flip() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.flip", false]], "get_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.get_events", false]], "get_fonts() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.get_fonts", false]], "get_system_tz() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.get_system_tz", false]], "get_system_tz() (inkycal.modules.ical_parser.icalendar static method)": [[3, "inkycal.modules.ical_parser.iCalendar.get_system_tz", false]], "icalendar (class in inkycal.modules.ical_parser)": [[3, "inkycal.modules.ical_parser.iCalendar", false]], "image_to_palette() (in module inkycal.modules.inky_image)": [[3, "inkycal.modules.inky_image.image_to_palette", false]], "inkycal (class in inkycal.main)": [[3, "inkycal.main.Inkycal", false]], "inkycal.custom.functions": [[3, "module-inkycal.custom.functions", false]], "inkycal.display.display": [[3, "module-inkycal.display.Display", false]], "inkycal.main": [[3, "module-inkycal.main", false]], "inkycal.modules.ical_parser": [[3, "module-inkycal.modules.ical_parser", false]], "inkycal.modules.inky_image": [[3, "module-inkycal.modules.inky_image", false]], "inkyimage (class in inkycal.modules.inky_image)": [[3, "inkycal.modules.inky_image.Inkyimage", false]], "internet_available() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.internet_available", false]], "load() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.load", false]], "load_from_file() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.load_from_file", false]], "load_url() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.load_url", false]], "merge() (inkycal.modules.inky_image.inkyimage static method)": [[3, "inkycal.modules.inky_image.Inkyimage.merge", false]], "module": [[3, "module-inkycal.custom.functions", false], [3, "module-inkycal.display.Display", false], [3, "module-inkycal.main", false], [3, "module-inkycal.modules.ical_parser", false], [3, "module-inkycal.modules.inky_image", false]], "preview() (inkycal.modules.inky_image.inkyimage static method)": [[3, "inkycal.modules.inky_image.Inkyimage.preview", false]], "remove_alpha() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.remove_alpha", false]], "resize() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.resize", false]], "run() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.run", false]], "show_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.show_events", false]], "sort() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.sort", false]], "test() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.test", false]], "text_wrap() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.text_wrap", false]], "write() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.write", false]]}, "objects": {"inkycal": [[3, 0, 0, "-", "main"]], "inkycal.custom": [[3, 0, 0, "-", "functions"]], "inkycal.custom.functions": [[3, 1, 1, "", "auto_fontsize"], [3, 1, 1, "", "draw_border"], [3, 1, 1, "", "get_fonts"], [3, 1, 1, "", "get_system_tz"], [3, 1, 1, "", "internet_available"], [3, 1, 1, "", "text_wrap"], [3, 1, 1, "", "write"]], "inkycal.display": [[3, 0, 0, "-", "Display"]], "inkycal.main": [[3, 2, 1, "", "Inkycal"]], "inkycal.main.Inkycal": [[3, 3, 1, "", "calibrate"], [3, 3, 1, "", "countdown"], [3, 3, 1, "", "run"], [3, 3, 1, "", "test"]], "inkycal.modules": [[3, 0, 0, "-", "ical_parser"], [3, 0, 0, "-", "inky_image"]], "inkycal.modules.ical_parser": [[3, 2, 1, "", "iCalendar"]], "inkycal.modules.ical_parser.iCalendar": [[3, 3, 1, "", "all_day"], [3, 3, 1, "", "clear_events"], [3, 3, 1, "", "get_events"], [3, 3, 1, "", "get_system_tz"], [3, 3, 1, "", "load_from_file"], [3, 3, 1, "", "load_url"], [3, 3, 1, "", "show_events"], [3, 3, 1, "", "sort"]], "inkycal.modules.inky_image": [[3, 2, 1, "", "Inkyimage"], [3, 1, 1, "", "image_to_palette"]], "inkycal.modules.inky_image.Inkyimage": [[3, 3, 1, "", "autoflip"], [3, 3, 1, "", "clear"], [3, 3, 1, "", "flip"], [3, 3, 1, "", "load"], [3, 3, 1, "", "merge"], [3, 3, 1, "", "preview"], [3, 3, 1, "", "remove_alpha"], [3, 3, 1, "", "resize"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "function", "Python function"], "2": ["py", "class", "Python class"], "3": ["py", "method", "Python method"]}, "objtypes": {"0": "py:module", "1": "py:function", "2": "py:class", "3": "py:method"}, "terms": {"": [0, 3], "0": 3, "1": 3, "10": 3, "100": 3, "11": 3, "16": 3, "16grai": 3, "180": 3, "2": 3, "20": 3, "270": 3, "3": 3, "32": 3, "360": 3, "5": 3, "7": 3, "80": 3, "9": 3, "90": 3, "A": 3, "In": 3, "It": 0, "No": 0, "OR": 3, "The": [0, 3], "Then": 3, "To": 3, "_": 0, "about": 2, "access": 3, "accur": 3, "aceinnolab": [3, 4], "aceisac": 0, "actual": 3, "ad": 0, "add": 3, "adjust": 3, "after": 3, "agenda": 0, "align": 3, "aliv": 0, "all": [0, 3], "all_dai": 3, "allow": 3, "alpha": 3, "also": 0, "amount": 0, "an": 3, "angl": 3, "anti": 3, "anyth": 0, "arg": 3, "arrow": 3, "assembl": 3, "async": 3, "atom": 0, "attempt": 3, "attribut": 3, "auto_fonts": [2, 3], "autofit": 3, "autoflip": 3, "automat": 3, "avail": 3, "awar": 3, "band": 3, "befor": 3, "begin": 3, "behind": 0, "below": 3, "black": 3, "blend": 0, "bool": 3, "boot": 3, "border": 3, "box": 3, "box_siz": 3, "built": 0, "bw": 3, "bwr": 3, "bwy": 3, "calcul": 3, "calendar": 0, "calibr": 3, "can": [0, 3], "care": [0, 3], "case": 0, "caus": 3, "cd": 4, "center": 3, "chang": 3, "check": 3, "choos": 3, "chunk": 3, "class": 2, "clear": 3, "clear_ev": 3, "clockwis": 3, "clone": 4, "co": 3, "coffe": 0, "colour": 3, "com": [3, 4], "come": 4, "commerci": 0, "commonli": 3, "commun": 0, "compat": 0, "config": 3, "connect": 3, "contain": 3, "coordin": 3, "copi": 4, "copyright": 3, "corner": 3, "correct": 3, "correctli": 3, "could": 3, "countdown": 3, "cours": 0, "creat": [0, 1, 2, 3], "current": 3, "custom": 2, "cycl": 3, "dai": 3, "dashboard": 0, "date": 3, "dd": 3, "decim": 3, "default": 3, "defin": 3, "desir": 3, "desktop": 3, "detail": 0, "develop": [0, 2], "dictionari": 3, "directli": 4, "discord": 0, "displai": [0, 2], "dither": 3, "do": 3, "doesn": [0, 3], "don": 0, "donat": 0, "download": [3, 4], "draw": 3, "draw_bord": [2, 3], "drawn": 3, "driver": 3, "e": [0, 3, 4], "each": 3, "eas": 3, "edit": 0, "effort": 0, "els": 3, "en": 3, "end": 3, "environ": 0, "epap": 3, "epaper_model": 3, "establish": 3, "etc": 0, "even": 0, "event": [0, 3], "exampl": 3, "except": 3, "extract": 3, "face": 0, "fals": 3, "feed": 0, "fetch": 0, "few": 0, "file": [0, 2, 3], "filenotfounderror": 3, "filepath": 3, "fill": 3, "fill_height": 3, "fill_width": 3, "first": 3, "fit": 3, "flip": 3, "fmt": 3, "folder": [3, 4], "follow": 3, "font": 3, "fontfil": 3, "fontnam": 3, "fontsiz": 3, "forecast": 0, "form": 0, "format": 3, "found": 3, "free": 0, "friendli": 0, "from": [0, 3], "full": [0, 3], "fulli": 0, "function": 2, "g": 3, "gener": [3, 4], "get": [0, 3], "get_ev": 3, "get_font": [2, 3], "get_system_tz": [2, 3], "git": 4, "github": 4, "given": 3, "go": 4, "googl": [0, 3], "gpicview": 3, "grai": 3, "greater": 3, "ha": [0, 3], "handl": 3, "have": [0, 3], "height": 3, "height_shrink_percentag": 3, "help": 0, "helper": 2, "hh": 3, "home": 3, "horizont": 3, "hour": 0, "htpp": 3, "http": [3, 4], "i": [0, 1, 3], "ical_pars": 3, "icalendar": [0, 2, 3], "idea": 0, "im_black": 3, "im_colour": 3, "imag": 3, "image1": 3, "image2": 3, "image_to_palett": [2, 3], "imagefont": 3, "imga": 3, "import": 3, "improv": 3, "increas": 3, "index": 2, "infin": 3, "info": 3, "inform": 0, "initi": 3, "inky_imag": 3, "inkyimag": [2, 3], "input": 3, "instal": 2, "instanc": 3, "instead": 3, "int": 3, "integ": 3, "internet": 3, "internet_avail": [2, 3], "interv": 3, "interval_min": 3, "invest": 0, "io": 3, "issu": 3, "its": 0, "joke": 0, "json": 3, "keep": 0, "kwarg": 3, "larg": 0, "latest": [0, 3], "layout": 3, "left": 3, "lib": 3, "line": 3, "list": 3, "liter": 3, "load": 3, "load_from_fil": 3, "load_url": 3, "local": 3, "logo": 3, "long": 3, "look": [0, 3], "loop": 3, "made": 3, "mai": 0, "main": [0, 3], "mainli": [0, 1], "map": 3, "max_height": 3, "max_width": 3, "maximum": 3, "mean": 0, "merg": 3, "minut": 3, "miss": 0, "mm": 3, "mmm": 3, "mode": 3, "model": 3, "modifi": 3, "modul": [0, 1, 2, 3], "modular": 0, "monthli": 0, "more": [0, 3, 4], "moudul": 3, "much": 3, "multipl": 3, "name": 3, "navig": 4, "need": 0, "network": 3, "new": [0, 3], "next": [0, 3], "nice_p": 3, "non": [0, 3], "none": 3, "nonstop": 3, "noob": 0, "noth": 0, "now": 3, "number": 3, "object": 3, "one": 3, "ones": 3, "onli": 3, "open": 0, "oper": 3, "optim": 3, "option": 3, "order": 3, "ordin": 3, "organis": 0, "oserror": 3, "other": [0, 3], "output": 3, "own": 0, "packag": 3, "page": 2, "palett": 3, "paper": [0, 3], "paramet": 3, "pars": 3, "part": 3, "parti": [0, 1], "password": 3, "past": 3, "path": 3, "path1": 3, "path2": 3, "percentag": 3, "phone": 0, "pi": [0, 3, 4], "pil": 3, "pinch": 0, "pip3": 4, "pixel": 3, "plain": 3, "pleas": [0, 4], "png": 3, "point": 3, "posit": 3, "possibl": 3, "present": 3, "preview": 3, "previous": 3, "print": 3, "program": 3, "project": [0, 3], "protect": 3, "provid": 0, "py": 3, "python3": [0, 3], "quickstart": 2, "radiu": 3, "rais": 3, "rapsbian": 3, "raspberri": [0, 4], "raw": 3, "re": 0, "reach": 3, "readabl": 3, "readthedoc": 3, "rectangl": 3, "red": 3, "reduc": 3, "refresh": 3, "remain": 3, "remov": 3, "remove_alpha": 3, "render": 3, "replac": 3, "repo": 4, "repres": 3, "requir": 3, "resiz": 3, "return": 3, "rgba": 3, "right": 3, "rotat": 3, "round": 3, "rss": 0, "run": [0, 3], "runner": 3, "sampl": 3, "save": 3, "scale": 3, "schedul": 3, "search": [2, 3], "second": 3, "see": 3, "select": [0, 3], "set": [0, 2, 3], "settings_path": 3, "sever": 0, "shade": 3, "share": 0, "should": 3, "show": [0, 3], "show_ev": 3, "shown": 3, "shrink": 3, "shrinkag": 3, "singl": 3, "site": 3, "size": 3, "sleep": 3, "smaller": 3, "smile": 0, "softwar": 0, "solid": 3, "some": 0, "someth": [0, 3], "soon": 4, "sort": 3, "sourc": 0, "specifi": 3, "split": 3, "stai": 0, "start": 3, "static": 3, "str": 3, "string": 3, "student": 0, "support": [0, 3], "sync": 0, "synchronis": 0, "system": 3, "sytax": 3, "t": [0, 3], "take": [0, 3], "test": 3, "text": 3, "text_wrap": [2, 3], "than": 3, "thank": 0, "them": [0, 3], "thi": [0, 1, 3], "thick": 3, "third": [0, 1], "time": [0, 3], "timelin": 3, "timeline_end": 3, "timeline_start": 3, "timeout": 3, "timezon": 3, "token": 3, "too": 0, "top": 3, "transpar": 3, "tri": 3, "true": 3, "truetyp": 3, "tupl": 3, "two": 3, "type": 3, "typeerror": 3, "tz": 3, "u": 0, "ui": [0, 4], "univers": 0, "until": 3, "up": 0, "updat": 3, "url": 3, "url1": 3, "url2": 3, "us": 3, "user": 0, "usernam": 3, "usual": 3, "utc": 3, "valid": 3, "valu": 3, "valueerror": 3, "venv": 3, "veri": 3, "vertic": 3, "via": [0, 4], "wa": [0, 3], "wai": 3, "we": 0, "weather": 0, "web": [0, 4], "week": 0, "welcom": 0, "well": 0, "what": 0, "when": 3, "where": 3, "which": [0, 3], "white": 3, "who": 1, "width": 3, "width_shrink_percentag": 3, "wish": 1, "without": [0, 3], "work": [0, 3], "write": [0, 2, 3], "written": 3, "x": 3, "xy": 3, "y": 3, "yai": 0, "yellow": 3, "you": 0, "your": [0, 3, 4], "yy": 3, "zero": 0}, "titles": ["About Inkycal", "Developer documentation", "Inkycal documentation", "Inkycal", "Quickstart"], "titleterms": {"about": 0, "class": 3, "content": 2, "creat": 4, "custom": 3, "develop": 1, "displai": 3, "document": [1, 2], "file": 4, "function": 3, "helper": 3, "indic": 2, "inkyc": [0, 2, 3, 4], "instal": 4, "quickstart": 4, "set": 4, "tabl": 2}}) \ No newline at end of file +Search.setIndex({"alltitles": {"About Inkycal": [[0, null]], "Contents:": [[2, null]], "Creating settings file": [[4, "creating-settings-file"]], "Custom functions": [[3, "module-inkycal.custom.functions"]], "Developer documentation": [[1, null]], "Display": [[3, "module-inkycal.display.Display"]], "Helper classes": [[3, "module-inkycal.modules.ical_parser"]], "Indices and tables": [[2, "indices-and-tables"]], "Inkycal": [[3, null]], "Inkycal documentation": [[2, null]], "Installing Inkycal": [[4, "installing-inkycal"]], "Quickstart": [[4, null]]}, "docnames": ["about", "dev_doc", "index", "inkycal", "quickstart"], "envversion": {"sphinx": 62, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["about.md", "dev_doc.md", "index.rst", "inkycal.rst", "quickstart.md"], "indexentries": {"all_day() (inkycal.modules.ical_parser.icalendar static method)": [[3, "inkycal.modules.ical_parser.iCalendar.all_day", false]], "auto_fontsize() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.auto_fontsize", false]], "autoflip() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.autoflip", false]], "calibrate() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.calibrate", false]], "clear() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.clear", false]], "clear_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.clear_events", false]], "countdown() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.countdown", false]], "draw_border() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.draw_border", false]], "dry_run() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.dry_run", false]], "flip() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.flip", false]], "get_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.get_events", false]], "get_fonts() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.get_fonts", false]], "get_system_tz() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.get_system_tz", false]], "get_system_tz() (inkycal.modules.ical_parser.icalendar static method)": [[3, "inkycal.modules.ical_parser.iCalendar.get_system_tz", false]], "icalendar (class in inkycal.modules.ical_parser)": [[3, "inkycal.modules.ical_parser.iCalendar", false]], "image_to_palette() (in module inkycal.modules.inky_image)": [[3, "inkycal.modules.inky_image.image_to_palette", false]], "inkycal (class in inkycal.main)": [[3, "inkycal.main.Inkycal", false]], "inkycal.custom.functions": [[3, "module-inkycal.custom.functions", false]], "inkycal.display.display": [[3, "module-inkycal.display.Display", false]], "inkycal.main": [[3, "module-inkycal.main", false]], "inkycal.modules.ical_parser": [[3, "module-inkycal.modules.ical_parser", false]], "inkycal.modules.inky_image": [[3, "module-inkycal.modules.inky_image", false]], "inkyimage (class in inkycal.modules.inky_image)": [[3, "inkycal.modules.inky_image.Inkyimage", false]], "internet_available() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.internet_available", false]], "load() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.load", false]], "load_from_file() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.load_from_file", false]], "load_url() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.load_url", false]], "merge() (inkycal.modules.inky_image.inkyimage static method)": [[3, "inkycal.modules.inky_image.Inkyimage.merge", false]], "module": [[3, "module-inkycal.custom.functions", false], [3, "module-inkycal.display.Display", false], [3, "module-inkycal.main", false], [3, "module-inkycal.modules.ical_parser", false], [3, "module-inkycal.modules.inky_image", false]], "preview() (inkycal.modules.inky_image.inkyimage static method)": [[3, "inkycal.modules.inky_image.Inkyimage.preview", false]], "process_module() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.process_module", false]], "remove_alpha() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.remove_alpha", false]], "resize() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.resize", false]], "run() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.run", false]], "show_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.show_events", false]], "sort() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.sort", false]], "text_wrap() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.text_wrap", false]], "write() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.write", false]]}, "objects": {"inkycal": [[3, 0, 0, "-", "main"]], "inkycal.custom": [[3, 0, 0, "-", "functions"]], "inkycal.custom.functions": [[3, 1, 1, "", "auto_fontsize"], [3, 1, 1, "", "draw_border"], [3, 1, 1, "", "get_fonts"], [3, 1, 1, "", "get_system_tz"], [3, 1, 1, "", "internet_available"], [3, 1, 1, "", "text_wrap"], [3, 1, 1, "", "write"]], "inkycal.display": [[3, 0, 0, "-", "Display"]], "inkycal.main": [[3, 2, 1, "", "Inkycal"]], "inkycal.main.Inkycal": [[3, 3, 1, "", "calibrate"], [3, 3, 1, "", "countdown"], [3, 3, 1, "", "dry_run"], [3, 3, 1, "", "process_module"], [3, 3, 1, "", "run"]], "inkycal.modules": [[3, 0, 0, "-", "ical_parser"], [3, 0, 0, "-", "inky_image"]], "inkycal.modules.ical_parser": [[3, 2, 1, "", "iCalendar"]], "inkycal.modules.ical_parser.iCalendar": [[3, 3, 1, "", "all_day"], [3, 3, 1, "", "clear_events"], [3, 3, 1, "", "get_events"], [3, 3, 1, "", "get_system_tz"], [3, 3, 1, "", "load_from_file"], [3, 3, 1, "", "load_url"], [3, 3, 1, "", "show_events"], [3, 3, 1, "", "sort"]], "inkycal.modules.inky_image": [[3, 2, 1, "", "Inkyimage"], [3, 1, 1, "", "image_to_palette"]], "inkycal.modules.inky_image.Inkyimage": [[3, 3, 1, "", "autoflip"], [3, 3, 1, "", "clear"], [3, 3, 1, "", "flip"], [3, 3, 1, "", "load"], [3, 3, 1, "", "merge"], [3, 3, 1, "", "preview"], [3, 3, 1, "", "remove_alpha"], [3, 3, 1, "", "resize"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "function", "Python function"], "2": ["py", "class", "Python class"], "3": ["py", "method", "Python method"]}, "objtypes": {"0": "py:module", "1": "py:function", "2": "py:class", "3": "py:method"}, "terms": {"": [0, 3], "0": 3, "1": 3, "10": 3, "100": 3, "11": 3, "16": 3, "16grai": 3, "180": 3, "2": 3, "20": 3, "270": 3, "3": 3, "32": 3, "360": 3, "5": 3, "7": 3, "80": 3, "9": 3, "90": 3, "A": 3, "If": 3, "In": 3, "It": 0, "No": 0, "OR": 3, "The": [0, 3], "Then": 3, "To": 3, "_": 0, "about": 2, "access": 3, "accur": 3, "aceinnolab": [3, 4], "aceisac": 0, "actual": 3, "ad": 0, "add": 3, "adjust": 3, "after": 3, "agenda": 0, "align": 3, "aliv": 0, "all": [0, 3], "all_dai": 3, "allow": 3, "alpha": 3, "also": 0, "amount": 0, "an": 3, "angl": 3, "anti": 3, "anyth": 0, "arg": 3, "arrow": 3, "assembl": 3, "async": 3, "atom": 0, "attempt": 3, "attribut": 3, "auto_fonts": [2, 3], "autofit": 3, "autoflip": 3, "automat": 3, "avail": 3, "awar": 3, "band": 3, "base": 3, "befor": 3, "begin": 3, "behind": 0, "below": 3, "black": 3, "blend": 0, "bool": 3, "boot": 3, "border": 3, "box": 3, "box_siz": 3, "built": 0, "bw": 3, "bwr": 3, "bwy": 3, "calcul": 3, "calendar": 0, "calibr": 3, "can": [0, 3], "care": [0, 3], "case": 0, "caus": 3, "cd": 4, "center": 3, "chang": 3, "check": 3, "choos": 3, "chunk": 3, "class": 2, "clear": 3, "clear_ev": 3, "clockwis": 3, "clone": 4, "co": 3, "coffe": 0, "colour": 3, "com": [3, 4], "come": 4, "commerci": 0, "commonli": 3, "commun": 0, "compat": 0, "config": 3, "connect": 3, "contain": 3, "coordin": 3, "copi": 4, "copyright": 3, "corner": 3, "correct": 3, "correctli": 3, "could": 3, "countdown": 3, "cours": 0, "creat": [0, 1, 2, 3], "current": 3, "custom": 2, "cycl": 3, "dai": 3, "dashboard": 0, "date": 3, "dd": 3, "decim": 3, "default": 3, "defin": 3, "desir": 3, "desktop": 3, "detail": 0, "develop": [0, 2], "dictionari": 3, "directli": 4, "discord": 0, "displai": [0, 2], "dither": 3, "do": 3, "doesn": [0, 3], "don": 0, "donat": 0, "download": [3, 4], "draw": 3, "draw_bord": [2, 3], "drawn": 3, "driver": 3, "dry_run": 3, "e": [0, 3, 4], "each": 3, "eas": 3, "edit": 0, "effort": 0, "els": 3, "en": 3, "end": 3, "environ": 0, "epap": 3, "epaper_model": 3, "establish": 3, "etc": 0, "even": 0, "event": [0, 3], "exampl": 3, "except": 3, "exit": 3, "extract": 3, "face": 0, "fals": 3, "feed": 0, "fetch": 0, "few": 0, "file": [0, 2, 3], "filenotfounderror": 3, "filepath": 3, "fill": 3, "fill_height": 3, "fill_width": 3, "first": 3, "fit": 3, "flag": 3, "flip": 3, "fmt": 3, "folder": [3, 4], "follow": 3, "font": 3, "fontfil": 3, "fontnam": 3, "fontsiz": 3, "forecast": 0, "form": 0, "format": 3, "found": 3, "free": 0, "friendli": 0, "from": [0, 3], "full": [0, 3], "fulli": 0, "function": 2, "g": 3, "gener": [3, 4], "get": [0, 3], "get_ev": 3, "get_font": [2, 3], "get_system_tz": [2, 3], "git": 4, "github": 4, "given": 3, "go": 4, "googl": [0, 3], "gpicview": 3, "grai": 3, "greater": 3, "ha": [0, 3], "handl": 3, "have": [0, 3], "height": 3, "height_shrink_percentag": 3, "help": 0, "helper": 2, "hh": 3, "home": 3, "horizont": 3, "hour": 0, "htpp": 3, "http": [3, 4], "i": [0, 1, 3], "ical_pars": 3, "icalendar": [0, 2, 3], "idea": 0, "im_black": 3, "im_colour": 3, "imag": 3, "image1": 3, "image2": 3, "image_to_palett": [2, 3], "imagefont": 3, "imga": 3, "import": 3, "improv": 3, "increas": 3, "indefinit": 3, "index": 2, "individu": 3, "infin": 3, "info": 3, "inform": 0, "initi": 3, "inky_imag": 3, "inkyimag": [2, 3], "input": 3, "instal": 2, "instanc": 3, "instead": 3, "int": 3, "integ": 3, "internet": 3, "internet_avail": [2, 3], "interv": 3, "interval_min": 3, "invest": 0, "io": 3, "issu": 3, "iter": 3, "its": 0, "joke": 0, "json": 3, "keep": 0, "kwarg": 3, "larg": 0, "latest": [0, 3], "layout": 3, "left": 3, "lib": 3, "line": 3, "list": 3, "liter": 3, "load": 3, "load_from_fil": 3, "load_url": 3, "local": 3, "logo": 3, "long": 3, "look": [0, 3], "loop": 3, "made": 3, "mai": 0, "main": [0, 3], "mainli": [0, 1], "map": 3, "max_height": 3, "max_width": 3, "maximum": 3, "mean": 0, "merg": 3, "minut": 3, "miss": 0, "mm": 3, "mmm": 3, "mode": 3, "model": 3, "modifi": 3, "modul": [0, 1, 2, 3], "modular": 0, "monthli": 0, "more": [0, 3, 4], "moudul": 3, "much": 3, "multipl": 3, "name": 3, "navig": 4, "need": 0, "network": 3, "new": [0, 3], "next": [0, 3], "nice_p": 3, "non": [0, 3], "none": 3, "nonstop": 3, "noob": 0, "noth": 0, "now": 3, "number": 3, "object": 3, "onc": 3, "one": 3, "ones": 3, "onli": 3, "open": 0, "oper": 3, "optim": 3, "option": 3, "order": 3, "ordin": 3, "organis": 0, "oserror": 3, "other": [0, 3], "output": 3, "own": 0, "packag": 3, "page": 2, "palett": 3, "paper": [0, 3], "paramet": 3, "pars": 3, "part": 3, "parti": [0, 1], "password": 3, "past": 3, "path": 3, "path1": 3, "path2": 3, "percentag": 3, "phone": 0, "pi": [0, 3, 4], "pil": 3, "pinch": 0, "pip3": 4, "pixel": 3, "plain": 3, "pleas": [0, 4], "png": 3, "point": 3, "posit": 3, "possibl": 3, "present": 3, "preview": 3, "previous": 3, "print": 3, "process": 3, "process_modul": 3, "program": 3, "project": [0, 3], "protect": 3, "provid": 0, "py": 3, "python3": [0, 3], "quickstart": 2, "radiu": 3, "rais": 3, "rapsbian": 3, "raspberri": [0, 4], "raw": 3, "re": 0, "reach": 3, "readabl": 3, "readthedoc": 3, "rectangl": 3, "red": 3, "reduc": 3, "refresh": 3, "remain": 3, "remov": 3, "remove_alpha": 3, "render": 3, "replac": 3, "repo": 4, "repres": 3, "requir": 3, "resiz": 3, "return": 3, "rgba": 3, "right": 3, "rotat": 3, "round": 3, "rss": 0, "run": [0, 3], "run_onc": 3, "runner": 3, "sampl": 3, "save": 3, "scale": 3, "schedul": 3, "search": [2, 3], "second": 3, "see": 3, "select": [0, 3], "set": [0, 2, 3], "settings_path": 3, "sever": 0, "shade": 3, "share": 0, "should": 3, "show": [0, 3], "show_ev": 3, "shown": 3, "shrink": 3, "shrinkag": 3, "shutdown_after_run": 3, "singl": 3, "site": 3, "size": 3, "sleep": 3, "smaller": 3, "smile": 0, "softwar": 0, "solid": 3, "some": 0, "someth": [0, 3], "soon": 4, "sort": 3, "sourc": 0, "specifi": 3, "split": 3, "stai": 0, "start": 3, "static": 3, "stop": 3, "str": 3, "string": 3, "student": 0, "support": [0, 3], "sync": 0, "synchronis": 0, "system": 3, "sytax": 3, "t": [0, 3], "take": [0, 3], "test": 3, "text": 3, "text_wrap": [2, 3], "than": 3, "thank": 0, "them": [0, 3], "thi": [0, 1, 3], "thick": 3, "third": [0, 1], "time": [0, 3], "timelin": 3, "timeline_end": 3, "timeline_start": 3, "timeout": 3, "timezon": 3, "token": 3, "too": 0, "top": 3, "transpar": 3, "tri": 3, "true": 3, "truetyp": 3, "tupl": 3, "two": 3, "type": 3, "typeerror": 3, "tz": 3, "u": 0, "ui": [0, 4], "univers": 0, "until": 3, "up": 0, "updat": 3, "url": 3, "url1": 3, "url2": 3, "us": 3, "use_pi_sugar": 3, "user": 0, "usernam": 3, "usual": 3, "utc": 3, "valid": 3, "valu": 3, "valueerror": 3, "venv": 3, "veri": 3, "vertic": 3, "via": [0, 4], "wa": [0, 3], "wai": 3, "we": 0, "weather": 0, "web": [0, 4], "week": 0, "welcom": 0, "well": 0, "what": 0, "when": 3, "where": 3, "which": [0, 3], "white": 3, "who": 1, "width": 3, "width_shrink_percentag": 3, "wish": 1, "without": [0, 3], "work": [0, 3], "write": [0, 2, 3], "written": 3, "x": 3, "xy": 3, "y": 3, "yai": 0, "yellow": 3, "you": 0, "your": [0, 3, 4], "yy": 3, "zero": 0}, "titles": ["About Inkycal", "Developer documentation", "Inkycal documentation", "Inkycal", "Quickstart"], "titleterms": {"about": 0, "class": 3, "content": 2, "creat": 4, "custom": 3, "develop": 1, "displai": 3, "document": [1, 2], "file": 4, "function": 3, "helper": 3, "indic": 2, "inkyc": [0, 2, 3, 4], "instal": 4, "quickstart": 4, "set": 4, "tabl": 2}}) \ No newline at end of file diff --git a/docsource/conf.py b/docsource/conf.py index 790c661a..b4e96aa7 100644 --- a/docsource/conf.py +++ b/docsource/conf.py @@ -22,7 +22,7 @@ author = 'aceinnolab' # The full version, including alpha/beta/rc tags -release = '2.0.3' +release = '2.0.4' # -- General configuration --------------------------------------------------- diff --git a/fonts/MaterialIcons/MaterialIcons.ttf b/fonts/MaterialIcons/MaterialIcons.ttf new file mode 100644 index 00000000..9d09b0fe Binary files /dev/null and b/fonts/MaterialIcons/MaterialIcons.ttf differ diff --git a/icons/ui-icons/home_temp.png b/fonts/ui-icons/home_temp.png similarity index 100% rename from icons/ui-icons/home_temp.png rename to fonts/ui-icons/home_temp.png diff --git a/icons/ui-icons/humidity.bmp b/fonts/ui-icons/humidity.bmp similarity index 100% rename from icons/ui-icons/humidity.bmp rename to fonts/ui-icons/humidity.bmp diff --git a/icons/ui-icons/outline_thermostat_white_48dp.bmp b/fonts/ui-icons/outline_thermostat_white_48dp.bmp similarity index 100% rename from icons/ui-icons/outline_thermostat_white_48dp.bmp rename to fonts/ui-icons/outline_thermostat_white_48dp.bmp diff --git a/icons/ui-icons/rain-chance.bmp b/fonts/ui-icons/rain-chance.bmp similarity index 100% rename from icons/ui-icons/rain-chance.bmp rename to fonts/ui-icons/rain-chance.bmp diff --git a/icons/ui-icons/uv.bmp b/fonts/ui-icons/uv.bmp similarity index 100% rename from icons/ui-icons/uv.bmp rename to fonts/ui-icons/uv.bmp diff --git a/icons/ui-icons/wind.bmp b/fonts/ui-icons/wind.bmp similarity index 100% rename from icons/ui-icons/wind.bmp rename to fonts/ui-icons/wind.bmp diff --git a/inky_run.py b/inky_run.py index e2fff6a5..b0b6b319 100644 --- a/inky_run.py +++ b/inky_run.py @@ -1,7 +1,43 @@ +"""Basic Inkycal run script. + +Assumes that the settings.json file is in the /boot directory. +set render=True to render the display, set render=False to only run the modules. +""" import asyncio + from inkycal import Inkycal -inky = Inkycal(render=True) # Initialise Inkycal -# If your settings.json file is not in /boot, use the full path: inky = Inkycal('path/to/settings.json', render=True) -inky.test() # test if Inkycal can be run correctly, running this will show a bit of info for each module -asyncio.run(inky.run()) # If there were no issues, you can run Inkycal nonstop + +async def run(): + """Run Inkycal nonstop. Default mode.""" + # create an instance of Inkycal + # If your settings.json file is not in /boot, use the full path: + # inky = Inkycal('path/to/settings.json', render=True) + + # when using experimental PiSugar support: + # inky = Inkycal(render=True, use_pi_sugar=True, shutdown_after_run=False) + inky = Inkycal(render=True) + await inky.run() # If there were no issues, you can run Inkycal nonstop + + +async def dry_run(): + """Useful for checking if the settings.json file is okay, without actually touching the display""" + # create an instance of Inkycal + # If your settings.json file is not in /boot, use the full path: + # inky = Inkycal('path/to/settings.json', render=True) + inky = Inkycal(render=False) + await inky.run(run_once=True) # dry-run without rendering anything on the display + + +async def clear_display(): + """Calibrate the display if you see some ghosting""" + print("loading Inkycal and display driver...") + inky = Inkycal(render=True) # Initialise Inkycal + print("clearing display...") + inky.calibrate(cycles=1) # Calibrate the display + print("clear complete...") + print("finished!") + + +if __name__ == "__main__": + asyncio.run(run()) diff --git a/inkycal/__init__.py b/inkycal/__init__.py index 0c6a2446..edb39260 100644 --- a/inkycal/__init__.py +++ b/inkycal/__init__.py @@ -1,20 +1,15 @@ -# Display class (for driving E-Paper displays) -from inkycal.display import Display - # Default modules import inkycal.modules.inkycal_agenda import inkycal.modules.inkycal_calendar -import inkycal.modules.inkycal_weather import inkycal.modules.inkycal_feeds -import inkycal.modules.inkycal_todoist +import inkycal.modules.inkycal_fullweather import inkycal.modules.inkycal_image import inkycal.modules.inkycal_jokes import inkycal.modules.inkycal_slideshow import inkycal.modules.inkycal_stocks +import inkycal.modules.inkycal_todoist +import inkycal.modules.inkycal_weather import inkycal.modules.inkycal_webshot import inkycal.modules.inkycal_xkcd -import inkycal.modules.inkycal_fullweather - -# Main file +from inkycal.display import Display from inkycal.main import Inkycal -import inkycal.modules.inkycal_stocks diff --git a/inkycal/custom/functions.py b/inkycal/custom/functions.py index 327095cf..2c39b764 100644 --- a/inkycal/custom/functions.py +++ b/inkycal/custom/functions.py @@ -8,29 +8,25 @@ import os import time import traceback +from typing import Tuple import arrow -import PIL import requests import tzlocal from PIL import Image from PIL import ImageDraw from PIL import ImageFont -logs = logging.getLogger(__name__) -logs.setLevel(level=logging.INFO) +from inkycal.settings import Settings -# Get the path to the Inkycal folder -top_level = "/".join(os.path.dirname(os.path.abspath(os.path.dirname(__file__))).split("/")[:-1]) +logger = logging.getLogger(__name__) -# Get path of 'fonts' and 'images' folders within Inkycal folder -fonts_location = os.path.join(top_level, "fonts/") -image_folder = os.path.join(top_level, "image_folder/") +settings = Settings() # Get available fonts within fonts folder fonts = {} -for path, dirs, files in os.walk(fonts_location): +for path, dirs, files in os.walk(settings.FONT_PATH): for _ in files: if _.endswith(".otf"): name = _.split(".otf")[0] @@ -39,7 +35,7 @@ if _.endswith(".ttf"): name = _.split(".ttf")[0] fonts[name] = os.path.join(path, _) -logs.debug(f"Found fonts: {json.dumps(fonts, indent=4, sort_keys=True)}") +logger.debug(f"Found fonts: {json.dumps(fonts, indent=4, sort_keys=True)}") available_fonts = [key for key, values in fonts.items()] @@ -77,16 +73,16 @@ def get_system_tz() -> str: >>> import arrow >>> print(arrow.now()) # returns non-timezone-aware time - >>> print(arrow.now(tz=get_system_tz()) # prints timezone aware time. + >>> print(arrow.now(tz=get_system_tz())) # prints timezone aware time. """ try: local_tz = tzlocal.get_localzone().key - logs.debug(f"Local system timezone is {local_tz}.") + logger.debug(f"Local system timezone is {local_tz}.") except: - logs.error("System timezone could not be parsed!") - logs.error("Please set timezone manually!. Falling back to UTC...") + logger.error("System timezone could not be parsed!") + logger.error("Please set timezone manually!. Falling back to UTC...") local_tz = "UTC" - logs.debug(f"The time is {arrow.now(tz=local_tz).format('YYYY-MM-DD HH:mm:ss ZZ')}.") + logger.debug(f"The time is {arrow.now(tz=local_tz).format('YYYY-MM-DD HH:mm:ss ZZ')}.") return local_tz @@ -115,7 +111,7 @@ def auto_fontsize(font, max_height): return font -def write(image, xy, box_size, text, font=None, **kwargs): +def write(image: Image, xy: Tuple[int, int], box_size: Tuple[int, int], text: str, font=None, **kwargs): """Writes text on an image. Writes given text at given position on the specified image. @@ -165,7 +161,7 @@ def write(image, xy, box_size, text, font=None, **kwargs): text_bbox = font.getbbox(text) text_width = text_bbox[2] - text_bbox[0] text_bbox_height = font.getbbox("hg") - text_height = text_bbox_height[3] - text_bbox_height[1] + text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1]) while text_width < int(box_width * fill_width) and text_height < int(box_height * fill_height): size += 1 @@ -173,23 +169,23 @@ def write(image, xy, box_size, text, font=None, **kwargs): text_bbox = font.getbbox(text) text_width = text_bbox[2] - text_bbox[0] text_bbox_height = font.getbbox("hg") - text_height = text_bbox_height[3] - text_bbox_height[1] + text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1]) text_bbox = font.getbbox(text) text_width = text_bbox[2] - text_bbox[0] text_bbox_height = font.getbbox("hg") - text_height = text_bbox_height[3] - text_bbox_height[1] + text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1]) # Truncate text if text is too long, so it can fit inside the box if (text_width, text_height) > (box_width, box_height): - logs.debug(("truncating {}".format(text))) + logger.debug(("truncating {}".format(text))) while (text_width, text_height) > (box_width, box_height): text = text[0:-1] text_bbox = font.getbbox(text) text_width = text_bbox[2] - text_bbox[0] text_bbox_height = font.getbbox("hg") - text_height = text_bbox_height[3] - text_bbox_height[1] - logs.debug(text) + text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1]) + logger.debug(text) # Align text to desired position if alignment == "center" or None: @@ -199,10 +195,13 @@ def write(image, xy, box_size, text, font=None, **kwargs): elif alignment == "right": x = int(box_width - text_width) + # Vertical centering + y = int((box_height / 2) - (text_height / 2)) + # Draw the text in the text-box draw = ImageDraw.Draw(image) space = Image.new('RGBA', (box_width, box_height)) - ImageDraw.Draw(space).text((x, 0), text, fill=colour, font=font) + ImageDraw.Draw(space).text((x, y), text, fill=colour, font=font) # Uncomment following two lines, comment out above two lines to show # red text-box with white text (debugging purposes) @@ -217,7 +216,7 @@ def write(image, xy, box_size, text, font=None, **kwargs): image.paste(space, xy, space) -def text_wrap(text, font=None, max_width=None): +def text_wrap(text: str, font=None, max_width=None): """Splits a very long text into smaller parts Splits a long text to smaller lines which can fit in a line with max_width. @@ -253,7 +252,7 @@ def text_wrap(text, font=None, max_width=None): return lines -def internet_available(): +def internet_available() -> bool: """checks if the internet is available. Attempts to connect to google.com with a timeout of 5 seconds to check @@ -278,15 +277,16 @@ def internet_available(): return False -def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)): +def draw_border(image: Image, xy: Tuple[int, int], size: Tuple[int, int], radius: int = 5, thickness: int = 1, + shrinkage: Tuple[int, int] = (0.1, 0.1)) -> None: """Draws a border at given coordinates. Args: - image: The image on which the border should be drawn (usually im_black or - im_colour. + im_colour). - xy: Tuple representing the top-left corner of the border e.g. (32, 100) - where 32 is the x co-ordinate and 100 is the y-coordinate. + where 32 is the x-coordinate and 100 is the y-coordinate. - size: Size of the border as a tuple -> (width, height). @@ -324,6 +324,7 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)): c5, c6 = ((x + width) - diameter, (y + height) - diameter), (x + width, y + height) c7, c8 = (x, (y + height) - diameter), (x + diameter, y + height) + # Draw lines and arcs, creating a square with round corners draw = ImageDraw.Draw(image) draw.line((p1, p2), fill=colour, width=thickness) @@ -338,7 +339,7 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)): draw.arc((c7, c8), 90, 180, fill=colour, width=thickness) -def draw_border_2(im: PIL.Image, xy: tuple, size: tuple, radius: int): +def draw_border_2(im: Image, xy: Tuple[int, int], size: Tuple[int, int], radius: int): draw = ImageDraw.Draw(im) x, y = xy diff --git a/inkycal/custom/openweathermap_wrapper.py b/inkycal/custom/openweathermap_wrapper.py index 6cd44053..779c5bf5 100644 --- a/inkycal/custom/openweathermap_wrapper.py +++ b/inkycal/custom/openweathermap_wrapper.py @@ -41,18 +41,9 @@ def get_json_from_url(request_url): class OpenWeatherMap: - def __init__( - self, - api_key: str, - city_id: int = None, - lat: float = None, - lon: float = None, - api_version: API_VERSIONS = "2.5", - temp_unit: TEMP_UNITS = "celsius", - wind_unit: WIND_UNITS = "meters_sec", - language: str = "en", - tz_name: str = "UTC", - ) -> None: + def __init__(self, api_key: str, city_id: int = None, lat: float = None, lon: float = None, + api_version: API_VERSIONS = "2.5", temp_unit: TEMP_UNITS = "celsius", + wind_unit: WIND_UNITS = "meters_sec", language: str = "en", tz_name: str = "UTC") -> None: self.api_key = api_key self.temp_unit = temp_unit self.wind_unit = wind_unit @@ -106,7 +97,7 @@ def get_current_weather(self) -> Dict: current_weather["temp_feels_like"] = self.get_converted_temperature(current_data["main"]["feels_like"]) current_weather["min_temp"] = self.get_converted_temperature(current_data["main"]["temp_min"]) current_weather["max_temp"] = self.get_converted_temperature(current_data["main"]["temp_max"]) - current_weather["humidity"] = current_data["main"]["humidity"] # OWM Unit: % rH + current_weather["humidity"] = current_data["main"]["humidity"] # OWM Unit: % rH current_weather["wind"] = self.get_converted_windspeed( current_data["wind"]["speed"] ) # OWM Unit Default: meter/sec, Metric: meter/sec @@ -161,10 +152,10 @@ def get_weather_forecast(self) -> List[Dict]: forecast["wind"]["speed"] ), # OWM Unit Default: meter/sec, Metric: meter/sec, Imperial: miles/hour "wind_gust": self.get_converted_windspeed(forecast["wind"]["gust"]), - "pressure": forecast["main"]["pressure"], # OWM Unit: hPa - "humidity": forecast["main"]["humidity"], # OWM Unit: % rH + "pressure": forecast["main"]["pressure"], # OWM Unit: hPa + "humidity": forecast["main"]["humidity"], # OWM Unit: % rH "precip_probability": forecast["pop"] - * 100.0, # OWM value is unitless, directly converting to % scale + * 100.0, # OWM value is unitless, directly converting to % scale "icon": forecast["weather"][0]["icon"], "datetime": datetime.fromtimestamp(forecast["dt"], tz=self.tz_zone), } @@ -187,7 +178,7 @@ def get_forecast_for_day(self, days_from_today: int) -> Dict: :return: Forecast dictionary """ - # Make sure hourly forecasts are up to date + # Make sure hourly forecasts are up-to-date _ = self.get_weather_forecast() # Calculate the start and end times for the specified number of days from now @@ -207,7 +198,7 @@ def get_forecast_for_day(self, days_from_today: int) -> Dict: ] # In case the next available forecast is already for the next day, use that one for the less than 3 remaining hours of today - if forecasts == []: + if not forecasts: forecasts.append(self.hourly_forecasts[0]) # Get rain and temperatures for that day diff --git a/inkycal/display/display.py b/inkycal/display/display.py index 89fdf4c8..bdb0818d 100644 --- a/inkycal/display/display.py +++ b/inkycal/display/display.py @@ -1,14 +1,12 @@ """ Inkycal ePaper driving functions -Copyright by aceisace +Copyright by aceinnolab """ -import os from importlib import import_module import PIL from PIL import Image -from inkycal.custom import top_level from inkycal.display.supported_models import supported_models @@ -199,9 +197,7 @@ def get_display_names(cls) -> list: >>> Display.get_display_names() """ - driver_files = top_level + '/inkycal/display/drivers/' - drivers = [i for i in os.listdir(driver_files) if i.endswith(".py") and not i.startswith("__") and "_" in i] - return drivers + return list(supported_models.keys()) if __name__ == '__main__': diff --git a/inkycal/display/drivers/10_in_3.py b/inkycal/display/drivers/10_in_3.py index 834e2df5..ee37e927 100644 --- a/inkycal/display/drivers/10_in_3.py +++ b/inkycal/display/drivers/10_in_3.py @@ -2,22 +2,18 @@ 10.3" driver class Copyright by aceinnolab """ +import os from subprocess import run from PIL import Image -from inkycal.custom import image_folder, top_level +from inkycal.settings import Settings # Display resolution EPD_WIDTH = 1872 EPD_HEIGHT = 1404 -# Please insert VCOM of your display. The Minus sign before is not required -VCOM = "2.0" - -driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/' - -command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}' +settings = Settings() class EPD: @@ -40,8 +36,8 @@ def display(self, command): def getbuffer(self, image): """ad-hoc""" image = image.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT) - image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP') - command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}' + image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), "BMP") + command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(settings.IMAGE_FOLDER, "canvas.bmp")}' print(command) return command diff --git a/inkycal/display/drivers/7_in_8.py b/inkycal/display/drivers/7_in_8.py index fe4d2437..9e700c71 100644 --- a/inkycal/display/drivers/7_in_8.py +++ b/inkycal/display/drivers/7_in_8.py @@ -2,20 +2,16 @@ 7.8" parallel driver class Copyright by aceinnolab """ +import os from subprocess import run -from inkycal.custom import image_folder, top_level +from inkycal.settings import Settings # Display resolution EPD_WIDTH = 1872 EPD_HEIGHT = 1404 -# Please insert VCOM of your display. The Minus sign before is not required -VCOM = "2.0" - -driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/' - -command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}' +settings = Settings() class EPD: @@ -38,8 +34,8 @@ def display(self, command): def getbuffer(self, image): """ad-hoc""" image = image.rotate(90, expand=True) - image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP') - command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}' + image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), 'BMP') + command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(settings.IMAGE_FOLDER, "canvas.bmp")}' print(command) return command diff --git a/inkycal/display/drivers/9_in_7.py b/inkycal/display/drivers/9_in_7.py index 8c6ec0f7..f801dcf9 100644 --- a/inkycal/display/drivers/9_in_7.py +++ b/inkycal/display/drivers/9_in_7.py @@ -2,20 +2,16 @@ 9.7" driver class Copyright by aceinnolab """ +import os from subprocess import run -from inkycal.custom import image_folder, top_level +from inkycal.settings import Settings # Display resolution EPD_WIDTH = 1200 EPD_HEIGHT = 825 -# Please insert VCOM of your display. The Minus sign before is not required -VCOM = "2.0" - -driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/' - -command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}' +settings = Settings() class EPD: @@ -38,8 +34,8 @@ def display(self, command): def getbuffer(self, image): """ad-hoc""" image = image.rotate(90, expand=True) - image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP') - command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}' + image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), "BMP") + command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(settings.IMAGE_FOLDER, "canvas.bmp")}' print(command) return command diff --git a/inkycal/display/drivers/epd_13_in_3.py b/inkycal/display/drivers/epd_13_in_3.py new file mode 100644 index 00000000..4ed8a24b --- /dev/null +++ b/inkycal/display/drivers/epd_13_in_3.py @@ -0,0 +1,527 @@ +""" +* | File : epd13in3k.py +* | Author : Waveshare team +* | Function : Electronic paper driver +* | Info : +*---------------- +* | This version: V1.0 +* | Date : 2023-09-08 +# | Info : python demo +----------------------------------------------------------------------------- +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import logging + +from inkycal.display.drivers import epdconfig + +# Display resolution +EPD_WIDTH = 960 +EPD_HEIGHT = 680 + +GRAY1 = 0xff # white +GRAY2 = 0xC0 +GRAY3 = 0x80 # gray +GRAY4 = 0x00 # Blackest + +logger = logging.getLogger(__name__) + + +class EPD: + def __init__(self): + self.reset_pin = epdconfig.RST_PIN + self.dc_pin = epdconfig.DC_PIN + self.busy_pin = epdconfig.BUSY_PIN + self.cs_pin = epdconfig.CS_PIN + self.width = EPD_WIDTH + self.height = EPD_HEIGHT + self.GRAY1 = GRAY1 # white + self.GRAY2 = GRAY2 + self.GRAY3 = GRAY3 # gray + self.GRAY4 = GRAY4 # Blackest + + self.Lut_Partial = [ + 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x2A, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x15, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x01, 0x01, 0x00, + 0x0A, 0x00, 0x05, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x01, + 0x22, 0x22, 0x22, 0x22, 0x22, + 0x17, 0x41, 0xA8, 0x32, 0x18, + 0x00, 0x00, ] + + self.LUT_DATA_4Gray = [ + 0x80, 0x48, 0x4A, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0A, 0x48, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x88, 0x48, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xA8, 0x48, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0x23, 0x17, 0x02, 0x00, + 0x05, 0x01, 0x05, 0x01, 0x02, + 0x08, 0x02, 0x01, 0x04, 0x04, + 0x00, 0x02, 0x00, 0x02, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, + 0x22, 0x22, 0x22, 0x22, 0x22, + 0x17, 0x41, 0xA8, 0x32, 0x30, + 0x00, 0x00, ] + + if (epdconfig.module_init() != 0): + return -1 + + # Hardware reset + def reset(self): + epdconfig.digital_write(self.reset_pin, 1) + epdconfig.delay_ms(20) + epdconfig.digital_write(self.reset_pin, 0) + epdconfig.delay_ms(2) + epdconfig.digital_write(self.reset_pin, 1) + epdconfig.delay_ms(20) + + def send_command(self, command): + epdconfig.digital_write(self.dc_pin, 0) + epdconfig.digital_write(self.cs_pin, 0) + epdconfig.spi_writebyte([command]) + epdconfig.digital_write(self.cs_pin, 1) + + def send_data(self, data): + epdconfig.digital_write(self.dc_pin, 1) + epdconfig.digital_write(self.cs_pin, 0) + epdconfig.spi_writebyte([data]) + epdconfig.digital_write(self.cs_pin, 1) + + def send_data2(self, data): + epdconfig.digital_write(self.dc_pin, 1) + epdconfig.digital_write(self.cs_pin, 0) + epdconfig.SPI.writebytes2(data) + epdconfig.digital_write(self.cs_pin, 1) + + def ReadBusy(self): + logger.debug("e-Paper busy") + busy = epdconfig.digital_read(self.busy_pin) + while (busy == 1): + busy = epdconfig.digital_read(self.busy_pin) + epdconfig.delay_ms(20) + epdconfig.delay_ms(20) + logger.debug("e-Paper busy release") + + def TurnOnDisplay(self): + self.send_command(0x22) # Display Update Control + self.send_data(0xF7) + self.send_command(0x20) # Activate Display Update Sequence + self.ReadBusy() + + def TurnOnDisplay_Part(self): + self.send_command(0x22) # Display Update Control + self.send_data(0xCF) + self.send_command(0x20) # Activate Display Update Sequence + self.ReadBusy() + + def TurnOnDisplay_4GRAY(self): + self.send_command(0x22) # Display Update Control + self.send_data(0xC7) + self.send_command(0x20) # Activate Display Update Sequence + self.ReadBusy() + + def Lut(self, LUT): + self.send_command(0x32) + for i in range(105): + self.send_data(LUT[i]) + + self.send_command(0x03) + self.send_data(LUT[105]) + + self.send_command(0x04) + self.send_data(LUT[106]) + self.send_data(LUT[107]) + self.send_data(LUT[108]) + + self.send_command(0x2C) + self.send_data(LUT[109]) + + def init(self): + + # EPD hardware init start + self.reset() + self.ReadBusy() + + self.send_command(0x12) # SWRESET + self.ReadBusy() + + self.send_command(0x0C) + self.send_data(0xAE) + self.send_data(0xC7) + self.send_data(0xC3) + self.send_data(0xC0) + self.send_data(0x80) + + self.send_command(0x01) + self.send_data(0xA7) + self.send_data(0x02) + self.send_data(0x00) + + self.send_command(0x11) + self.send_data(0x03) + + self.send_command(0x44) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0xBF) + self.send_data(0x03) + + self.send_command(0x45) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0xA7) + self.send_data(0x02) + + self.send_command(0x3C) + self.send_data(0x05) + + self.send_command(0x18) + self.send_data(0x80) + + self.send_command(0x4E) + self.send_data(0x00) + self.send_data(0x00) + + self.send_command(0x4F) + self.send_data(0x00) + self.send_data(0x00) + + # EPD hardware init end + return 0 + + def init_Part(self): + self.reset() + + self.send_command(0x3C) + self.send_data(0x80) + + self.Lut(self.Lut_Partial) + + self.send_command(0x37) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0x40) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0x00) + + self.send_command(0x3C) + self.send_data(0x80) + + self.send_command(0x22) + self.send_data(0xC0) + self.send_command(0x20) + + self.ReadBusy() + + def init_4GRAY(self): + self.reset() + + self.ReadBusy() + self.send_command(0x12) + self.ReadBusy() + + self.send_command(0x0C) + self.send_data(0xAE) + self.send_data(0xC7) + self.send_data(0xC3) + self.send_data(0xC0) + self.send_data(0x80) + + self.send_command(0x01) + self.send_data(0xA7) + self.send_data(0x02) + self.send_data(0x00) + + self.send_command(0x11) + self.send_data(0x03) + + self.send_command(0x44) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0xBF) + self.send_data(0x03) + + self.send_command(0x45) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0xA7) + self.send_data(0x02) + + self.send_command(0x3C) + self.send_data(0x00) + + self.send_command(0x18) + self.send_data(0x80) + + self.send_command(0x4E) + self.send_data(0x00) + self.send_data(0x00) + + self.send_command(0x4F) + self.send_data(0x00) + self.send_data(0x00) + + self.Lut(self.LUT_DATA_4Gray) + + self.ReadBusy() + + def getbuffer(self, image): + # logger.debug("bufsiz = ",int(self.width/8) * self.height) + buf = [0xFF] * (int(self.width / 8) * self.height) + image_monocolor = image.convert('1') + imwidth, imheight = image_monocolor.size + pixels = image_monocolor.load() + # logger.debug("imwidth = %d, imheight = %d",imwidth,imheight) + if imwidth == self.width and imheight == self.height: + logger.debug("Horizontal") + for y in range(imheight): + for x in range(imwidth): + # Set the bits for the column of pixels at the current position. + if pixels[x, y] == 0: + buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8)) + elif imwidth == self.height and imheight == self.width: + logger.debug("Vertical") + for y in range(imheight): + for x in range(imwidth): + newx = y + newy = self.height - x - 1 + if pixels[x, y] == 0: + buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8)) + return buf + + def getbuffer_4Gray(self, image): + # logger.debug("bufsiz = ",int(self.width/8) * self.height) + buf = [0xFF] * (int(self.width / 4) * self.height) + image_monocolor = image.convert('L') + imwidth, imheight = image_monocolor.size + pixels = image_monocolor.load() + i = 0 + # logger.debug("imwidth = %d, imheight = %d",imwidth,imheight) + if (imwidth == self.width and imheight == self.height): + logger.debug("Vertical") + for y in range(imheight): + for x in range(imwidth): + # Set the bits for the column of pixels at the current position. + if (pixels[x, y] == 0xC0): + pixels[x, y] = 0x80 + elif (pixels[x, y] == 0x80): + pixels[x, y] = 0x40 + i = i + 1 + if (i % 4 == 0): + buf[int((x + (y * self.width)) / 4)] = ( + (pixels[x - 3, y] & 0xc0) | (pixels[x - 2, y] & 0xc0) >> 2 | ( + pixels[x - 1, y] & 0xc0) >> 4 | (pixels[x, y] & 0xc0) >> 6) + + elif (imwidth == self.height and imheight == self.width): + logger.debug("Horizontal") + for x in range(imwidth): + for y in range(imheight): + newx = y + newy = self.height - x - 1 + if (pixels[x, y] == 0xC0): + pixels[x, y] = 0x80 + elif (pixels[x, y] == 0x80): + pixels[x, y] = 0x40 + i = i + 1 + if (i % 4 == 0): + buf[int((newx + (newy * self.width)) / 4)] = ( + (pixels[x, y - 3] & 0xc0) | (pixels[x, y - 2] & 0xc0) >> 2 | ( + pixels[x, y - 1] & 0xc0) >> 4 | (pixels[x, y] & 0xc0) >> 6) + return buf + + def Clear(self): + buf = [0xFF] * (int(self.width / 8) * self.height) + self.send_command(0x24) + self.send_data2(buf) + + self.TurnOnDisplay() + + def display(self, image): + self.send_command(0x24) + self.send_data2(image) + + self.TurnOnDisplay() + + def display_Base(self, image): + self.send_command(0x24) + self.send_data2(image) + + self.send_command(0x26) + self.send_data2(image) + + self.TurnOnDisplay() + + def display_Base_color(self, color): + if (self.width % 8 == 0): + Width = self.width // 8 + else: + Width = self.width // 8 + 1 + Height = self.height + self.send_command(0x24) # Write Black and White image to RAM + for j in range(Height): + for i in range(Width): + self.send_data(color) + + self.send_command(0x26) # Write Black and White image to RAM + for j in range(Height): + for i in range(Width): + self.send_data(color) + # self.TurnOnDisplay() + + def display_Partial(self, Image, Xstart, Ystart, Xend, Yend): + if ((Xstart % 8 + Xend % 8 == 8 & Xstart % 8 > Xend % 8) | Xstart % 8 + Xend % 8 == 0 | ( + Xend - Xstart) % 8 == 0): + Xstart = Xstart // 8 + Xend = Xend // 8 + else: + Xstart = Xstart // 8 + if Xend % 8 == 0: + Xend = Xend // 8 + else: + Xend = Xend // 8 + 1 + + if (self.width % 8 == 0): + Width = self.width // 8 + else: + Width = self.width // 8 + 1 + Height = self.height + + Xend -= 1 + Yend -= 1 + + self.send_command(0x44) + self.send_data((Xstart * 8) & 0xff) + self.send_data((Xstart >> 5) & 0x01) + self.send_data((Xend * 8) & 0xff) + self.send_data((Xend >> 5) & 0x01) + self.send_command(0x45) + self.send_data(Ystart & 0xff) + self.send_data((Ystart >> 8) & 0x01) + self.send_data(Yend & 0xff) + self.send_data((Yend >> 8) & 0x01) + + self.send_command(0x4E) + self.send_data((Xstart * 8) & 0xff) + self.send_data((Xstart >> 5) & 0x01) + self.send_command(0x4F) + self.send_data(Ystart & 0xff) + self.send_data((Ystart >> 8) & 0x01) + + self.send_command(0x24) + for j in range(Height): + for i in range(Width): + if ((j > Ystart - 1) & (j < (Yend + 1)) & (i > Xstart - 1) & (i < (Xend + 1))): + self.send_data(Image[i + j * Width]) + self.TurnOnDisplay_Part() + + def display_4Gray(self, image): + self.send_command(0x24) + for i in range(0, 81600): + temp3 = 0 + for j in range(0, 2): + temp1 = image[i * 2 + j] + for k in range(0, 2): + temp2 = temp1 & 0xC0 + if (temp2 == 0xC0): + temp3 |= 0x00 + elif (temp2 == 0x00): + temp3 |= 0x01 + elif (temp2 == 0x80): + temp3 |= 0x01 + else: # 0x40 + temp3 |= 0x00 + temp3 <<= 1 + + temp1 <<= 2 + temp2 = temp1 & 0xC0 + if (temp2 == 0xC0): + temp3 |= 0x00 + elif (temp2 == 0x00): + temp3 |= 0x01 + elif (temp2 == 0x80): + temp3 |= 0x01 + else: # 0x40 + temp3 |= 0x00 + if (j != 1 or k != 1): + temp3 <<= 1 + temp1 <<= 2 + self.send_data(temp3) + + self.send_command(0x26) + for i in range(0, 81600): + temp3 = 0 + for j in range(0, 2): + temp1 = image[i * 2 + j] + for k in range(0, 2): + temp2 = temp1 & 0xC0 + if (temp2 == 0xC0): + temp3 |= 0x00 + elif (temp2 == 0x00): + temp3 |= 0x01 + elif (temp2 == 0x80): + temp3 |= 0x00 + else: # 0x40 + temp3 |= 0x01 + temp3 <<= 1 + + temp1 <<= 2 + temp2 = temp1 & 0xC0 + if (temp2 == 0xC0): + temp3 |= 0x00 + elif (temp2 == 0x00): + temp3 |= 0x01 + elif (temp2 == 0x80): + temp3 |= 0x00 + else: # 0x40 + temp3 |= 0x01 + if (j != 1 or k != 1): + temp3 <<= 1 + temp1 <<= 2 + self.send_data(temp3) + + self.TurnOnDisplay_4GRAY() + + def sleep(self): + self.send_command(0x10) # DEEP_SLEEP + self.send_data(0x03) + + epdconfig.delay_ms(2000) + epdconfig.module_exit() diff --git a/inkycal/display/drivers/epd_13_in_3_colour.py b/inkycal/display/drivers/epd_13_in_3_colour.py new file mode 100644 index 00000000..663d88fe --- /dev/null +++ b/inkycal/display/drivers/epd_13_in_3_colour.py @@ -0,0 +1,299 @@ +""" +* | File : epd13in3b.py +* | Author : Waveshare team +* | Function : Electronic paper driver +* | Info : +*---------------- +* | This version: V1.0 +* | Date : 2024-04-08 +# | Info : python demo +----------------------------------------------------------------------------- +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import logging + +from inkycal.display.drivers import epdconfig + +# Display resolution +EPD_WIDTH = 960 +EPD_HEIGHT = 680 + +GRAY1 = 0xff # white +GRAY2 = 0xC0 +GRAY3 = 0x80 # gray +GRAY4 = 0x00 # Blackest + +logger = logging.getLogger(__name__) + + +class EPD: + def __init__(self): + self.reset_pin = epdconfig.RST_PIN + self.dc_pin = epdconfig.DC_PIN + self.busy_pin = epdconfig.BUSY_PIN + self.cs_pin = epdconfig.CS_PIN + self.width = EPD_WIDTH + self.height = EPD_HEIGHT + if (epdconfig.module_init() != 0): + return -1 + + # Hardware reset + def reset(self): + epdconfig.digital_write(self.reset_pin, 1) + epdconfig.delay_ms(20) + epdconfig.digital_write(self.reset_pin, 0) + epdconfig.delay_ms(2) + epdconfig.digital_write(self.reset_pin, 1) + epdconfig.delay_ms(20) + + def send_command(self, command): + epdconfig.digital_write(self.dc_pin, 0) + epdconfig.digital_write(self.cs_pin, 0) + epdconfig.spi_writebyte([command]) + epdconfig.digital_write(self.cs_pin, 1) + + def send_data(self, data): + epdconfig.digital_write(self.dc_pin, 1) + epdconfig.digital_write(self.cs_pin, 0) + epdconfig.spi_writebyte([data]) + epdconfig.digital_write(self.cs_pin, 1) + + def send_data2(self, data): + epdconfig.digital_write(self.dc_pin, 1) + epdconfig.digital_write(self.cs_pin, 0) + epdconfig.SPI.writebytes2(data) + epdconfig.digital_write(self.cs_pin, 1) + + def ReadBusy(self): + logger.debug("e-Paper busy") + busy = epdconfig.digital_read(self.busy_pin) + while (busy == 1): + busy = epdconfig.digital_read(self.busy_pin) + epdconfig.delay_ms(20) + epdconfig.delay_ms(20) + logger.debug("e-Paper busy release") + + def TurnOnDisplay(self): + self.send_command(0x22) # Display Update Control + self.send_data(0xF7) + self.send_command(0x20) # Activate Display Update Sequence + self.ReadBusy() + + def TurnOnDisplay_Part(self): + self.send_command(0x22) # Display Update Control + self.send_data(0xFF) + self.send_command(0x20) # Activate Display Update Sequence + self.ReadBusy() + + def init(self): + # EPD hardware init start + self.reset() + self.ReadBusy() + + self.send_command(0x12) # SWRESET + self.ReadBusy() + + self.send_command(0x0C) + self.send_data(0xAE) + self.send_data(0xC7) + self.send_data(0xC3) + self.send_data(0xC0) + self.send_data(0x80) + + self.send_command(0x01) + self.send_data(0xA7) + self.send_data(0x02) + self.send_data(0x00) + + self.send_command(0x11) + self.send_data(0x03) + + self.send_command(0x44) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0xBF) + self.send_data(0x03) + + self.send_command(0x45) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0xA7) + self.send_data(0x02) + + self.send_command(0x3C) + self.send_data(0x01) + + self.send_command(0x18) + self.send_data(0x80) + + self.send_command(0x4E) + self.send_data(0x00) + self.send_data(0x00) + + self.send_command(0x4F) + self.send_data(0x00) + self.send_data(0x00) + self.ReadBusy() + + # EPD hardware init end + return 0 + + def getbuffer(self, image): + # logger.debug("bufsiz = ",int(self.width/8) * self.height) + buf = [0xFF] * (int(self.width / 8) * self.height) + image_monocolor = image.convert('1') + imwidth, imheight = image_monocolor.size + pixels = image_monocolor.load() + # logger.debug("imwidth = %d, imheight = %d",imwidth,imheight) + if imwidth == self.width and imheight == self.height: + logger.debug("Horizontal") + for y in range(imheight): + for x in range(imwidth): + # Set the bits for the column of pixels at the current position. + if pixels[x, y] == 0: + buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8)) + elif imwidth == self.height and imheight == self.width: + logger.debug("Vertical") + for y in range(imheight): + for x in range(imwidth): + newx = y + newy = self.height - x - 1 + if pixels[x, y] == 0: + buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8)) + return buf + + def Clear(self): + self.send_command(0x24) + self.send_data2([0xFF] * (int(self.width / 8) * self.height)) + self.send_command(0x26) + self.send_data2([0x00] * (int(self.width / 8) * self.height)) + + self.TurnOnDisplay() + + def Clear_Base(self): + self.send_command(0x24) + self.send_data2([0xFF] * (int(self.width / 8) * self.height)) + self.send_command(0x26) + self.send_data2([0x00] * (int(self.width / 8) * self.height)) + + self.TurnOnDisplay() + self.send_command(0x26) + self.send_data2([0xFF] * (int(self.width / 8) * self.height)) + + def display(self, blackimage, ryimage): + if (self.width % 8 == 0): + Width = self.width // 8 + else: + Width = self.width // 8 + 1 + Height = self.height + if (blackimage != None): + self.send_command(0x24) + self.send_data2(blackimage) + if (ryimage != None): + for j in range(Height): + for i in range(Width): + ryimage[i + j * Width] = ~ryimage[i + j * Width] + self.send_command(0x26) + self.send_data2(ryimage) + + self.TurnOnDisplay() + + def display_Base(self, blackimage, ryimage): + if (self.width % 8 == 0): + Width = self.width // 8 + else: + Width = self.width // 8 + 1 + Height = self.height + if (blackimage != None): + self.send_command(0x24) + self.send_data2(blackimage) + if (ryimage != None): + for j in range(Height): + for i in range(Width): + ryimage[i + j * Width] = ~ryimage[i + j * Width] + self.send_command(0x26) + self.send_data2(ryimage) + + self.TurnOnDisplay() + + self.send_command(0x26) + self.send_data2(blackimage) + + def display_Partial(self, Image, Xstart, Ystart, Xend, Yend): + if ((Xstart % 8 + Xend % 8 == 8 & Xstart % 8 > Xend % 8) | Xstart % 8 + Xend % 8 == 0 | ( + Xend - Xstart) % 8 == 0): + Xstart = Xstart // 8 + Xend = Xend // 8 + else: + Xstart = Xstart // 8 + if Xend % 8 == 0: + Xend = Xend // 8 + else: + Xend = Xend // 8 + 1 + + if (self.width % 8 == 0): + Width = self.width // 8 + else: + Width = self.width // 8 + 1 + Height = self.height + + Xend -= 1 + Yend -= 1 + + self.send_command(0x3C) + self.send_data(0x80) + + self.send_command(0x44) + self.send_data((Xstart * 8) & 0xff) + self.send_data((Xstart >> 5) & 0x01) + self.send_data((Xend * 8) & 0xff) + self.send_data((Xend >> 5) & 0x01) + self.send_command(0x45) + self.send_data(Ystart & 0xff) + self.send_data((Ystart >> 8) & 0x01) + self.send_data(Yend & 0xff) + self.send_data((Yend >> 8) & 0x01) + + self.send_command(0x4E) + self.send_data((Xstart * 8) & 0xff) + self.send_data((Xstart >> 5) & 0x01) + self.send_command(0x4F) + self.send_data(Ystart & 0xff) + self.send_data((Ystart >> 8) & 0x01) + + self.send_command(0x24) + for j in range(Height): + for i in range(Width): + if ((j > Ystart - 1) & (j < (Yend + 1)) & (i > Xstart - 1) & (i < (Xend + 1))): + self.send_data(Image[i + j * Width]) + self.TurnOnDisplay_Part() + + self.send_command(0x26) + for j in range(Height): + for i in range(Width): + if ((j > Ystart - 1) & (j < (Yend + 1)) & (i > Xstart - 1) & (i < (Xend + 1))): + self.send_data(Image[i + j * Width]) + + def sleep(self): + self.send_command(0x10) # DEEP_SLEEP + self.send_data(0x03) + + epdconfig.delay_ms(2000) + epdconfig.module_exit() diff --git a/inkycal/display/drivers/epdconfig.py b/inkycal/display/drivers/epdconfig.py index 2eaddf4f..2a5e8190 100644 --- a/inkycal/display/drivers/epdconfig.py +++ b/inkycal/display/drivers/epdconfig.py @@ -28,8 +28,6 @@ """ import logging -import os -import subprocess import sys import time @@ -128,4 +126,3 @@ def module_exit(self, cleanup=False): for func in [x for x in dir(implementation) if not x.startswith('_')]: setattr(sys.modules[__name__], func, getattr(implementation, func)) - diff --git a/inkycal/display/supported_models.py b/inkycal/display/supported_models.py index cbf535c8..f8a9fb4a 100644 --- a/inkycal/display/supported_models.py +++ b/inkycal/display/supported_models.py @@ -1,4 +1,6 @@ supported_models = { + "epd_13_in_3": (960, 680), + "epd_13_in_3_colour": (960, 680), "epd_12_in_48": (1304, 984), "epd_7_in_5_colour": (640, 384), "9_in_7": (1200, 825), diff --git a/inkycal/loggers.py b/inkycal/loggers.py new file mode 100644 index 00000000..3a59c335 --- /dev/null +++ b/inkycal/loggers.py @@ -0,0 +1,35 @@ +"""Logging configuration for Inkycal.""" +import logging +import os +from logging.handlers import RotatingFileHandler + +from inkycal.settings import Settings + +# On the console, set a logger to show only important logs +# (level ERROR or higher) +stream_handler = logging.StreamHandler() +stream_handler.setLevel(logging.INFO) + +settings = Settings() + +if not os.path.exists(settings.LOG_PATH): + os.mkdir(settings.LOG_PATH) + + +# Save all logs to a file, which contains more detailed output +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s | %(name)s | %(levelname)s: %(message)s', + datefmt='%d-%m-%Y %H:%M:%S', + handlers=[ + stream_handler, # add stream handler from above + RotatingFileHandler( # log to a file too + settings.INKYCAL_LOG_PATH, # file to log + maxBytes=2*1024*1024, # 2MB max filesize + backupCount=5 # create max 5 log files + ) + ] +) + +# Show less logging for PIL module +logging.getLogger("PIL").setLevel(logging.WARNING) \ No newline at end of file diff --git a/inkycal/main.py b/inkycal/main.py index cc4473b4..cef0e11a 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -6,44 +6,22 @@ import asyncio import glob import hashlib -from logging.handlers import RotatingFileHandler +import os.path import numpy +from inkycal import loggers # noqa from inkycal.custom import * from inkycal.display import Display from inkycal.modules.inky_image import Inkyimage as Images - -# On the console, set a logger to show only important logs -# (level ERROR or higher) -stream_handler = logging.StreamHandler() -stream_handler.setLevel(logging.ERROR) - -if not os.path.exists(f'{top_level}/logs'): - os.mkdir(f'{top_level}/logs') - -# Save all logs to a file, which contains more detailed output -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s | %(name)s | %(levelname)s: %(message)s', - datefmt='%d-%m-%Y %H:%M:%S', - handlers=[ - stream_handler, # add stream handler from above - RotatingFileHandler( # log to a file too - f'{top_level}/logs/inkycal.log', # file to log - maxBytes=2097152, # 2MB max filesize - backupCount=5 # create max 5 log files - ) - ] -) - -# Show less logging for PIL module -logging.getLogger("PIL").setLevel(logging.WARNING) +from inkycal.utils import JSONCache logger = logging.getLogger(__name__) +settings = Settings() + +CACHE_NAME = "inkycal_main" -# TODO: autostart -> supervisor? class Inkycal: """Inkycal main class @@ -60,43 +38,61 @@ class Inkycal: to improve rendering on E-Papers. Set this to False for 9.7" E-Paper. """ - def __init__(self, settings_path: str or None = None, render: bool = True): - """Initialise Inkycal""" + def __init__(self, settings_path: str or None = None, render: bool = True, use_pi_sugar: bool = False, + shutdown_after_run: bool = False) -> None: + """Initialise Inkycal - # Get the release version from setup.py - with open(f'{top_level}/setup.py') as setup_file: - for line in setup_file: - if line.startswith('__version__'): - self._release = line.split("=")[-1].replace("'", "").replace('"', "").replace(" ", "") - break + Args: + settings_path (str): + The full path to your settings.json file. If no path was specified, will look in the /boot directory. + render (bool): + Show the image on the E-Paper display. + use_pi_sugar (bool): + Use PiSugar board (all revisions). Default is False. + shutdown_after_run (bool): + Shutdown the system after the run is complete. Will only work with PiSugar enabled. + + """ + self._release = "2.0.4" + + logger.info(f"Inkycal v{self._release} booting up...") self.render = render self.info = None + logger.info("Checking if a settings file is present...") # load settings file - throw an error if file could not be found if settings_path: + logger.info(f"Custom location for settings.json file specified: {settings_path}") try: - with open(settings_path) as settings_file: - settings = json.load(settings_file) - self.settings = settings + with open(settings_path, mode="r") as settings_file: + self.settings = json.load(settings_file) except FileNotFoundError: raise FileNotFoundError( f"No settings.json file could be found in the specified location: {settings_path}") else: - try: - with open('/boot/settings.json') as settings_file: - settings = json.load(settings_file) - self.settings = settings - - except FileNotFoundError: - raise SettingsFileNotFoundError + found = False + for location in settings.SETTINGS_JSON_PATHS: + if os.path.exists(location): + logger.info(f"Found settings.json file in {location}") + with open(location, mode="r") as settings_file: + self.settings = json.load(settings_file) + found = True + break + if not found: + raise SettingsFileNotFoundError(f"No settings.json file could be found in {settings.SETTINGS_JSON_PATHS} and no explicit path was specified.") self.disable_calibration = self.settings.get('disable_calibration', False) + if self.disable_calibration: + logger.info("Calibration disabled. Please proceed with caution to prevent ghosting.") + + if not os.path.exists(settings.IMAGE_FOLDER): + os.mkdir(settings.IMAGE_FOLDER) - if not os.path.exists(image_folder): - os.mkdir(image_folder) + if not os.path.exists(settings.CACHE_PATH): + os.mkdir(settings.CACHE_PATH) # Option to use epaper image optimisation, reduces colours self.optimize = True @@ -109,10 +105,10 @@ def __init__(self, settings_path: str or None = None, render: bool = True): if self.render: # Init Display class with model in settings file # from inkycal.display import Display - self.Display = Display(settings["model"]) + self.Display = Display(self.settings["model"]) # check if colours can be rendered - self.supports_colour = True if 'colour' in settings['model'] else False + self.supports_colour = True if 'colour' in self.settings['model'] else False # get calibration hours self._calibration_hours = self.settings['calibration_hours'] @@ -122,7 +118,7 @@ def __init__(self, settings_path: str or None = None, render: bool = True): # Load and initialise modules specified in the settings file self._module_number = 1 - for module in settings['modules']: + for module in self.settings['modules']: module_name = module['name'] try: loader = f'from inkycal.modules import {module_name}' @@ -131,10 +127,9 @@ def __init__(self, settings_path: str or None = None, render: bool = True): setup = f'self.module_{self._module_number} = {module_name}({module})' # print(setup) exec(setup) - logger.info(('name : {name} size : {width}x{height} px'.format( - name=module_name, - width=module['config']['size'][0], - height=module['config']['size'][1]))) + width = module['config']['size'][0] + height = module['config']['size'][1] + logger.info(f'name : {module_name} size : {width}x{height} px') self._module_number += 1 @@ -146,58 +141,85 @@ def __init__(self, settings_path: str or None = None, render: bool = True): except: logger.exception(f"Exception: {traceback.format_exc()}.") - # Path to store images - self.image_folder = image_folder - # Remove old hashes - self._remove_hashes(self.image_folder) + self._remove_hashes(settings.IMAGE_FOLDER) + + # set up cache + if not os.path.exists(os.path.join(settings.CACHE_PATH, CACHE_NAME)): + if not os.path.exists(settings.CACHE_PATH): + os.mkdir(settings.CACHE_PATH) + self.cache = JSONCache(CACHE_NAME) + self.cache_data = self.cache.read() + + self.counter = 0 if "counter" not in self.cache_data else int(self.cache_data["counter"]) + + self.use_pi_sugar = use_pi_sugar + self.battery_capacity = 100 + self.shutdown_after_run = use_pi_sugar and shutdown_after_run + + if self.use_pi_sugar: + logger.info("PiSugar support enabled.") + from inkycal.utils import PiSugar + self.pisugar = PiSugar() + + self.battery_capacity = self.pisugar.get_battery() + logger.info(f"PiSugar battery capacity: {self.battery_capacity}%") + + if self.battery_capacity < 20: + logger.warning("Battery capacity is below 20%!") + + logger.info("Setting system time to PiSugar time...") + if self.pisugar.rtc_pi2rtc(): + logger.info("RTC time updates successfully") + else: + logger.warning("RTC time could not be set!") + + print( + f"Using PiSigar model: {self.pisugar.get_model()}. Current PiSugar time: {self.pisugar.get_rtc_time()}") + + if self.shutdown_after_run: + logger.warning("Shutdown after run enabled. System will shutdown after the run is complete.") # Give an OK message - print('loaded inkycal') + logger.info('Inkycal initialised successfully!') - def countdown(self, interval_mins: int or None = None) -> int: - """Returns the remaining time in seconds until next display update. + def countdown(self, interval_mins: int = None) -> int: + """Returns the remaining time in seconds until the next display update based on the interval. Args: - - interval_mins = int -> the interval in minutes for the update - if no interval is given, the value from the settings file is used. + interval_mins (int): The interval in minutes for the update. If none is given, the value + from the settings file is used. Returns: - - int -> the remaining time in seconds until next update + int: The remaining time in seconds until the next update. """ - - # Check if empty, if empty, use value from settings file + # Default to settings if no interval is provided if interval_mins is None: interval_mins = self.settings["update_interval"] - # Find out at which minutes the update should happen + # Get the current time now = arrow.now() - if interval_mins <= 60: - update_timings = [(60 - interval_mins * updates) for updates in range(60 // interval_mins)][::-1] - # Calculate time in minutes until next update - minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute + # Calculate the next update time + # Finding the total minutes from the start of the day + minutes_since_midnight = now.hour * 60 + now.minute - # Print the remaining time in minutes until next update - print(f'{minutes} minutes left until next refresh') + # Finding the next interval point + minutes_to_next_interval = ( + minutes_since_midnight // interval_mins + 1) * interval_mins - minutes_since_midnight + seconds_to_next_interval = minutes_to_next_interval * 60 - now.second - # Calculate time in seconds until next update - remaining_time = minutes * 60 + (60 - now.second) - - # Return seconds until next update - return remaining_time + # Logging the remaining time in appropriate units + hours_to_next_interval = minutes_to_next_interval // 60 + remaining_minutes = minutes_to_next_interval % 60 + if hours_to_next_interval > 0: + print(f'{hours_to_next_interval} hours and {remaining_minutes} minutes left until next refresh') else: - # Calculate time in minutes until next update using the range of 24 hours in steps of every full hour - update_timings = [(60 * 24 - interval_mins * updates) for updates in range(60 * 24 // interval_mins)][::-1] - minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute - remaining_time = minutes * 60 + (60 - now.second) - - print(f'{round(minutes / 60, 1)} hours left until next refresh') + print(f'{remaining_minutes} minutes left until next refresh') - # Return seconds until next update - return remaining_time + return seconds_to_next_interval - def test(self): + def dry_run(self): """Tests if Inkycal can run without issues. Attempts to import module names from settings file. Loads the config @@ -206,8 +228,6 @@ def test(self): Generated images can be found in the /images folder of Inkycal. """ - - logger.info(f"Inkycal version: v{self._release}") logger.info(f'Selected E-paper display: {self.settings["model"]}') # store module numbers in here @@ -218,20 +238,13 @@ def test(self): for number in range(1, self._module_number): name = eval(f"self.module_{number}.name") - module = eval(f'self.module_{number}') - print(f'generating image(s) for {name}...', end="") - try: - black, colour = module.generate_image() - if self.show_border: - draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5) - black.save(f"{self.image_folder}module{number}_black.png", "PNG") - colour.save(f"{self.image_folder}module{number}_colour.png", "PNG") - print("OK!") - except Exception: + success = self.process_module(number) + if success: + logger.debug(f'Image of module {name} generated successfully') + else: + logger.warning(f'Generating image of module {name} failed!') errors.append(number) self.info += f"module {number}: Error! " - logger.exception("Error!") - logger.exception(f"Exception: {traceback.format_exc()}.") if errors: logger.error('Error/s in modules:', *errors) @@ -277,98 +290,89 @@ def _needs_image_update(self, _list): print("Refresh needed: {a}".format(a=res)) return res - async def run(self): - """Runs main program in nonstop mode. + async def run(self, run_once=False): + """Runs main program in nonstop mode or a single iteration based on the run_once flag. - Uses an infinity loop to run Inkycal nonstop. Inkycal generates the image - from all modules, assembles them in one image, refreshed the E-Paper and - then sleeps until the next scheduled update. - """ + Args: + run_once (bool): If True, runs the updating process once and stops. If False, + runs indefinitely. + Uses an infinity loop to run Inkycal nonstop or a single time based on run_once. + Inkycal generates the image from all modules, assembles them in one image, + refreshes the E-Paper and then sleeps until the next scheduled update or exits. + """ # Get the time of initial run runtime = arrow.now() # Function to flip images upside down upside_down = lambda image: image.rotate(180, expand=True) - # Count the number of times without any errors - counter = 0 - - print(f'Inkycal version: v{self._release}') - print(f'Selected E-paper display: {self.settings["model"]}') + logger.info(f'Inkycal version: v{self._release}') + logger.info(f'Selected E-paper display: {self.settings["model"]}') while True: + logger.info("Starting new cycle...") current_time = arrow.now(tz=get_system_tz()) - print(f"Date: {current_time.format('D MMM YY')} | " - f"Time: {current_time.format('HH:mm')}") - print('Generating images for all modules...', end='') + logger.info(f"Timestamp: {current_time.format('HH:mm:ss DD.MM.YYYY')}") + self.cache_data["counter"] = self.counter - errors = [] # store module numbers in here + errors = [] # Store module numbers in here - # short info for info-section + # Short info for info-section if not self.settings.get('image_hash', False): self.info = f"{current_time.format('D MMM @ HH:mm')} " else: self.info = "" for number in range(1, self._module_number): - - # name = eval(f"self.module_{number}.name") - module = eval(f'self.module_{number}') - - try: - black, colour = module.generate_image() - if self.show_border: - draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5) - black.save(f"{self.image_folder}module{number}_black.png", "PNG") - colour.save(f"{self.image_folder}module{number}_colour.png", "PNG") - self.info += f"module {number}: OK " - except Exception as e: + success = self.process_module(number) + if not success: errors.append(number) - self.info += f"module {number}: Error! " - logger.exception("Error!") - logger.exception(f"Exception: {traceback.format_exc()}.") + self.info += f"im {number}: X " if errors: logger.error("Error/s in modules:", *errors) - counter = 0 + self.counter = 0 + self.cache_data["counter"] = 0 else: - counter += 1 - logger.info("successful") + self.counter += 1 + self.cache_data["counter"] += 1 + logger.info("All images generated successfully!") del errors + if self.battery_capacity < 20: + self.info += "Low battery! " + # Assemble image from each module - add info section if specified self._assemble() # Check if image should be rendered if self.render: + logger.info("Attempting to render image on display...") display = self.Display - self._calibration_check() if self._calibration_state: - # after calibration, we have to forcefully rewrite the screen - self._remove_hashes(self.image_folder) + # After calibration, we have to forcefully rewrite the screen + self._remove_hashes(settings.IMAGE_FOLDER) if self.supports_colour: - im_black = Image.open(f"{self.image_folder}canvas.png") - im_colour = Image.open(f"{self.image_folder}canvas_colour.png") + im_black = Image.open(os.path.join(settings.IMAGE_FOLDER, "canvas.png")) + im_colour = Image.open(os.path.join(settings.IMAGE_FOLDER, "canvas_colour.png")) # Flip the image by 180° if required if self.settings['orientation'] == 180: im_black = upside_down(im_black) im_colour = upside_down(im_colour) - # render the image on the display + # Render the image on the display if not self.settings.get('image_hash', False) or self._needs_image_update([ - (f"{self.image_folder}/canvas.png.hash", im_black), - (f"{self.image_folder}/canvas_colour.png.hash", im_colour) + (f"{settings.IMAGE_FOLDER}/canvas.png.hash", im_black), + (f"{settings.IMAGE_FOLDER}/canvas_colour.png.hash", im_colour) ]): - # render the image on the display display.render(im_black, im_colour) # Part for black-white ePapers - elif not self.supports_colour: - + else: im_black = self._merge_bands() # Flip the image by 180° if required @@ -376,14 +380,34 @@ async def run(self): im_black = upside_down(im_black) if not self.settings.get('image_hash', False) or self._needs_image_update([ - (f"{self.image_folder}/canvas.png.hash", im_black), - ]): + (f"{settings.IMAGE_FOLDER}/canvas.png.hash", im_black), ]): display.render(im_black) - print(f'\nNo errors since {counter} display updates \n' - f'program started {runtime.humanize()}') + logger.info(f'No errors since {self.counter} display updates') + logger.info(f'program started {runtime.humanize()}') + + # store the cache data + self.cache.write(self.cache_data) + + # Exit the loop if run_once is True + if run_once: + break # Exit the loop after one full cycle if run_once is True sleep_time = self.countdown() + + if self.use_pi_sugar: + sleep_time_rtc = arrow.now(tz=get_system_tz()).shift(seconds=sleep_time) + result = self.pisugar.rtc_alarm_set(sleep_time_rtc, 127) + if result: + logger.info(f"Alarm set for {sleep_time_rtc.format('HH:mm:ss')}") + if self.shutdown_after_run: + logger.warning("System shutdown in 5 seconds!") + time.sleep(5) + self._shutdown_system() + break + else: + logger.warning(f"Failed to set alarm for {sleep_time_rtc.format('HH:mm:ss')}") + await asyncio.sleep(sleep_time) @staticmethod @@ -392,7 +416,8 @@ def _merge_bands(): returns the merged image """ - im1_path, im2_path = image_folder + 'canvas.png', image_folder + 'canvas_colour.png' + im1_path = os.path.join(settings.IMAGE_FOLDER, "canvas.png") + im2_path = os.path.join(settings.IMAGE_FOLDER, "canvas_colour.png") # If there is an image for black and colour, merge them if os.path.exists(im1_path) and os.path.exists(im2_path): @@ -430,8 +455,8 @@ def _assemble(self): for number in range(1, self._module_number): # get the path of the current module's generated images - im1_path = f"{self.image_folder}module{number}_black.png" - im2_path = f"{self.image_folder}module{number}_colour.png" + im1_path = os.path.join(settings.IMAGE_FOLDER, f"module{number}_black.png") + im2_path = os.path.join(settings.IMAGE_FOLDER, f"module{number}_colour.png") # Check if there is an image for the black band if os.path.exists(im1_path): @@ -501,8 +526,8 @@ def _assemble(self): im_black = self._optimize_im(im_black) im_colour = self._optimize_im(im_colour) - im_black.save(self.image_folder + 'canvas.png', 'PNG') - im_colour.save(self.image_folder + 'canvas_colour.png', 'PNG') + im_black.save(os.path.join(settings.IMAGE_FOLDER, "canvas.png"), "PNG") + im_colour.save(os.path.join(settings.IMAGE_FOLDER, "canvas_colour.png"), 'PNG') # Additionally, combine the two images with color def clear_white(img): @@ -531,7 +556,7 @@ def black_to_colour(img): im_colour = black_to_colour(im_colour) im_colour.paste(im_black, (0, 0), im_black) - im_colour.save(image_folder + 'full-screen.png', 'PNG') + im_colour.save(os.path.join(settings.IMAGE_FOLDER, 'full-screen.png'), 'PNG') @staticmethod def _optimize_im(image, threshold=220): @@ -574,13 +599,40 @@ def _calibration_check(self): @staticmethod def cleanup(): # clean up old images in image_folder - for _file in glob.glob(f"{image_folder}*.png"): + if len(glob.glob(settings.IMAGE_FOLDER)) <= 1: + return + for _file in glob.glob(settings.IMAGE_FOLDER): try: os.remove(_file) except: logger.error(f"could not remove file: {_file}") pass + def process_module(self, number) -> bool or Exception: + """Process individual module to generate images and handle exceptions.""" + module = eval(f'self.module_{number}') + try: + black, colour = module.generate_image() + if self.show_border: + draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5) + black.save(os.path.join(settings.IMAGE_FOLDER, f"module{number}_black.png"), "PNG") + colour.save(os.path.join(settings.IMAGE_FOLDER, f"module{number}_colour.png"), "PNG") + return True + except Exception: + logger.exception(f"Error in module {number}!") + return False + + def _shutdown_system(self): + """Shutdown the system""" + import subprocess + from time import sleep + try: + logger.info("Shutting down OS in 5 seconds...") + sleep(5) + subprocess.run(["sudo", "shutdown", "-h", "now"], check=True) + except subprocess.CalledProcessError: + logger.warning("Failed to execute shutdown command.") + if __name__ == '__main__': print(f'running inkycal main in standalone/debug mode') diff --git a/inkycal/modules/dev_module.py b/inkycal/modules/dev_module.py index 13d62758..53d8db3c 100755 --- a/inkycal/modules/dev_module.py +++ b/inkycal/modules/dev_module.py @@ -156,7 +156,7 @@ def __init__(self, config): # -----------------------------------------------------------------------# # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') ############################################################################# # Validation of module specific parameters (optional) # diff --git a/inkycal/modules/inky_image.py b/inkycal/modules/inky_image.py index 67f4a14f..5795f0e4 100755 --- a/inkycal/modules/inky_image.py +++ b/inkycal/modules/inky_image.py @@ -27,7 +27,7 @@ def __init__(self, image=None): self.image = image # give an OK message - logger.info(f"{__name__} loaded") + logger.debug(f"{__name__} loaded") def load(self, path: str) -> None: """loads an image from a URL or filepath. @@ -59,7 +59,7 @@ def load(self, path: str) -> None: logger.error("Invalid Image file provided", exc_info=True) raise Exception("Please check if the path points to an image file.") - logger.info(f"width: {image.width}, height: {image.height}") + logger.debug(f"width: {image.width}, height: {image.height}") image.convert(mode="RGBA") # convert to a more suitable format self.image = image diff --git a/inkycal/modules/inkycal_agenda.py b/inkycal/modules/inkycal_agenda.py index 5b7f5d09..1508a66e 100755 --- a/inkycal/modules/inkycal_agenda.py +++ b/inkycal/modules/inkycal_agenda.py @@ -2,9 +2,7 @@ Inkycal Agenda Module Copyright by aceinnolab """ - -import arrow - +import arrow # noqa from inkycal.custom import * from inkycal.modules.ical_parser import iCalendar from inkycal.modules.template import inkycal_module @@ -77,8 +75,10 @@ def __init__(self, config): # Additional config self.timezone = get_system_tz() + self.icon_font = ImageFont.truetype(fonts['MaterialIcons'], size=self.fontsize) + # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def generate_image(self): """Generate image for this module""" @@ -88,7 +88,7 @@ def generate_image(self): im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') @@ -203,10 +203,10 @@ def generate_image(self): write(im_black, (x_time, line_pos[cursor][1]), (time_width, line_height), time, font=self.font, alignment='right') - if parser.all_day(_): + else: write(im_black, (x_time, line_pos[cursor][1]), - (time_width, line_height), "all day", - font=self.font, alignment='right') + (time_width, line_height), "\ue878", + font=self.icon_font, alignment='right') write(im_black, (x_event, line_pos[cursor][1]), (event_width, line_height), diff --git a/inkycal/modules/inkycal_calendar.py b/inkycal/modules/inkycal_calendar.py index 9c859846..146c272b 100755 --- a/inkycal/modules/inkycal_calendar.py +++ b/inkycal/modules/inkycal_calendar.py @@ -6,16 +6,16 @@ # pylint: disable=logging-fstring-interpolation import calendar as cal -import arrow -from inkycal.modules.template import inkycal_module + from inkycal.custom import * +from inkycal.modules.template import inkycal_module logger = logging.getLogger(__name__) class Calendar(inkycal_module): """Calendar class - Create monthly calendar and show events from given icalendars + Create monthly calendar and show events from given iCalendars """ name = "Calendar - Show monthly calendar with events from iCalendars" @@ -39,12 +39,12 @@ class Calendar(inkycal_module): }, "date_format": { "label": "Use an arrow-supported token for custom date formatting " - + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM", + + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM", "default": "D MMM", }, "time_format": { "label": "Use an arrow-supported token for custom time formatting " - + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm", + + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm", "default": "HH:mm", }, } @@ -61,7 +61,7 @@ def __init__(self, config): self._days_with_events = None # optional parameters - self.weekstart = config['week_starts_on'] + self.week_start = config['week_starts_on'] self.show_events = config['show_events'] self.date_format = config["date_format"] self.time_format = config['time_format'] @@ -84,7 +84,7 @@ def __init__(self, config): ) # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') @staticmethod def flatten(values): @@ -100,7 +100,7 @@ def generate_image(self): im_size = im_width, im_height events_height = 0 - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') @@ -109,7 +109,7 @@ def generate_image(self): # Allocate space for month-names, weekdays etc. month_name_height = int(im_height * 0.10) text_bbox_height = self.font.getbbox("hg") - weekdays_height = int((text_bbox_height[3] - text_bbox_height[1])* 1.25) + weekdays_height = int((abs(text_bbox_height[3]) + abs(text_bbox_height[1])) * 1.25) logger.debug(f"month_name_height: {month_name_height}") logger.debug(f"weekdays_height: {weekdays_height}") @@ -117,7 +117,7 @@ def generate_image(self): logger.debug("Allocating space for events") calendar_height = int(im_height * 0.6) events_height = ( - im_height - month_name_height - weekdays_height - calendar_height + im_height - month_name_height - weekdays_height - calendar_height ) logger.debug(f'calendar-section size: {im_width} x {calendar_height} px') logger.debug(f'events-section size: {im_width} x {events_height} px') @@ -156,13 +156,13 @@ def generate_image(self): now = arrow.now(tz=self.timezone) - # Set weekstart of calendar to specified weekstart - if self.weekstart == "Monday": + # Set week-start of calendar to specified week-start + if self.week_start == "Monday": cal.setfirstweekday(cal.MONDAY) - weekstart = now.shift(days=-now.weekday()) + week_start = now.shift(days=-now.weekday()) else: cal.setfirstweekday(cal.SUNDAY) - weekstart = now.shift(days=-now.isoweekday()) + week_start = now.shift(days=-now.isoweekday()) # Write the name of current month write( @@ -174,9 +174,9 @@ def generate_image(self): autofit=True, ) - # Set up weeknames in local language and add to main section + # Set up week-names in local language and add to main section weekday_names = [ - weekstart.shift(days=+_).format('ddd', locale=self.language) + week_start.shift(days=+_).format('ddd', locale=self.language) for _ in range(7) ] logger.debug(f'weekday names: {weekday_names}') @@ -192,7 +192,7 @@ def generate_image(self): fill_height=0.9, ) - # Create a calendar template and flatten (remove nestings) + # Create a calendar template and flatten (remove nesting) calendar_flat = self.flatten(cal.monthcalendar(now.year, now.month)) # logger.debug(f" calendar_flat: {calendar_flat}") @@ -265,7 +265,7 @@ def generate_image(self): # find out how many lines can fit at max in the event section line_spacing = 2 text_bbox_height = self.font.getbbox("hg") - line_height = text_bbox_height[3] + line_spacing + line_height = text_bbox_height[3] - text_bbox_height[1] + line_spacing max_event_lines = events_height // (line_height + line_spacing) # generate list of coordinates for each line @@ -281,7 +281,7 @@ def generate_image(self): month_start = arrow.get(now.floor('month')) month_end = arrow.get(now.ceil('month')) - # fetch events from given icalendars + # fetch events from given iCalendars self.ical = iCalendar() parser = self.ical @@ -294,14 +294,12 @@ def generate_image(self): month_events = parser.get_events(month_start, month_end, self.timezone) parser.sort() self.month_events = month_events - + # Initialize days_with_events as an empty list days_with_events = [] # Handle multi-day events by adding all days between start and end for event in month_events: - start_date = event['begin'].date() - end_date = event['end'].date() # Convert start and end dates to arrow objects with timezone start = arrow.get(event['begin'].date(), tzinfo=self.timezone) @@ -324,9 +322,7 @@ def generate_image(self): im_colour, grid[days], (icon_width, icon_height), - radius=6, - thickness=1, - shrinkage=(0.4, 0.2), + radius=6 ) # Filter upcoming events until 4 weeks in the future @@ -345,13 +341,13 @@ def generate_image(self): date_width = int(max(( self.font.getlength(events['begin'].format(self.date_format, locale=lang)) - for events in upcoming_events))* 1.1 - ) + for events in upcoming_events)) * 1.1 + ) time_width = int(max(( self.font.getlength(events['begin'].format(self.time_format, locale=lang)) - for events in upcoming_events))* 1.1 - ) + for events in upcoming_events)) * 1.1 + ) text_bbox_height = self.font.getbbox("hg") line_height = text_bbox_height[3] + line_spacing @@ -369,7 +365,8 @@ def generate_image(self): event_duration = (event['end'] - event['begin']).days if event_duration > 1: # Format the duration using Arrow's localization - days_translation = arrow.get().shift(days=event_duration).humanize(only_distance=True, locale=lang) + days_translation = arrow.get().shift(days=event_duration).humanize(only_distance=True, + locale=lang) the_name = f"{event['title']} ({days_translation})" else: the_name = event['title'] diff --git a/inkycal/modules/inkycal_feeds.py b/inkycal/modules/inkycal_feeds.py index d7bdde30..38a62949 100644 --- a/inkycal/modules/inkycal_feeds.py +++ b/inkycal/modules/inkycal_feeds.py @@ -60,7 +60,7 @@ def __init__(self, config): self.shuffle_feeds = config["shuffle_feeds"] # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def _validate(self): """Validate module-specific parameters""" @@ -75,7 +75,7 @@ def generate_image(self): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') @@ -83,8 +83,9 @@ def generate_image(self): # Check if internet is available if internet_available(): - logger.info('Connection test passed') + logger.debug('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise NetworkNotReachableError # Set some parameters for formatting feeds diff --git a/inkycal/modules/inkycal_fullweather.py b/inkycal/modules/inkycal_fullweather.py index 55ce0448..5dccc70f 100644 --- a/inkycal/modules/inkycal_fullweather.py +++ b/inkycal/modules/inkycal_fullweather.py @@ -23,16 +23,18 @@ from inkycal.custom.functions import fonts from inkycal.custom.functions import get_system_tz from inkycal.custom.functions import internet_available -from inkycal.custom.functions import top_level from inkycal.custom.inkycal_exceptions import NetworkNotReachableError from inkycal.custom.openweathermap_wrapper import OpenWeatherMap from inkycal.modules.inky_image import image_to_palette from inkycal.modules.template import inkycal_module +from inkycal.settings import Settings logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -icons_dir = os.path.join(top_level, "icons", "ui-icons") +settings = Settings() + +icons_dir = os.path.join(settings.FONT_PATH, "ui-icons") def outline(image: Image, size: int, color: tuple) -> Image: @@ -139,7 +141,7 @@ def __init__(self, config): # Check if all required parameters are present for param in self.requires: - if not param in config: + if param not in config: raise Exception(f"config is missing {param}") # required parameters @@ -237,7 +239,7 @@ def __init__(self, config): self.left_section_width = int(self.width / 4) # give an OK message - print(f"{__name__} loaded") + logger.debug(f"{__name__} loaded") def createBaseImage(self): """ diff --git a/inkycal/modules/inkycal_image.py b/inkycal/modules/inkycal_image.py index 5387b9b7..bdf94bd9 100755 --- a/inkycal/modules/inkycal_image.py +++ b/inkycal/modules/inkycal_image.py @@ -50,7 +50,7 @@ def __init__(self, config): self.dither = False # give an OK message - print(f"{__name__} loaded") + logger.debug(f"{__name__} loaded") def generate_image(self): """Generate image for this module""" @@ -71,7 +71,7 @@ def generate_image(self): # Remove background if present im.remove_alpha() - # if autoflip was enabled, flip the image + # if auto-flip was enabled, flip the image if self.autoflip: im.autoflip(self.orientation) diff --git a/inkycal/modules/inkycal_jokes.py b/inkycal/modules/inkycal_jokes.py index 5f0085e8..e35fd5eb 100755 --- a/inkycal/modules/inkycal_jokes.py +++ b/inkycal/modules/inkycal_jokes.py @@ -30,7 +30,7 @@ def __init__(self, config): config = config['config'] # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def generate_image(self): """Generate image for this module""" @@ -39,7 +39,7 @@ def generate_image(self): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'image size: {im_width} x {im_height} px') + logger.debug(f'image size: {im_width} x {im_height} px') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') @@ -47,8 +47,9 @@ def generate_image(self): # Check if internet is available if internet_available(): - logger.info('Connection test passed') + logger.debug('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise NetworkNotReachableError # Set some parameters for formatting feeds diff --git a/inkycal/modules/inkycal_server.py b/inkycal/modules/inkycal_server.py index 619c146c..4cade833 100755 --- a/inkycal/modules/inkycal_server.py +++ b/inkycal/modules/inkycal_server.py @@ -67,7 +67,7 @@ def __init__(self, config): self.path_body = config['path_body'] # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def generate_image(self): """Generate image for this module""" diff --git a/inkycal/modules/inkycal_slideshow.py b/inkycal/modules/inkycal_slideshow.py index c7481fac..926d72bf 100755 --- a/inkycal/modules/inkycal_slideshow.py +++ b/inkycal/modules/inkycal_slideshow.py @@ -8,13 +8,13 @@ # PIL has a class named Image, use alias for Inkyimage -> Images from inkycal.modules.inky_image import Inkyimage as Images, image_to_palette from inkycal.modules.template import inkycal_module +from inkycal.utils import JSONCache logger = logging.getLogger(__name__) class Slideshow(inkycal_module): - """Cycles through images in a local image folder - """ + """Cycles through images in a local image folder""" name = "Slideshow - cycle through images from a local folder" requires = { @@ -53,7 +53,7 @@ def __init__(self, config): # required parameters for param in self.requires: - if not param in config: + if param not in config: raise Exception(f'config is missing {param}') # optional parameters @@ -64,19 +64,20 @@ def __init__(self, config): # Get the full path of all png/jpg/jpeg images in the given folder all_files = glob.glob(f'{self.path}/*') - self.images = [i for i in all_files - if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')] + self.images = [i for i in all_files if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')] if not self.images: - logger.error('No images found in the given folder, please ' - 'double check your path!') + logger.error('No images found in the given folder, please double check your path!') raise Exception('No images found in the given folder path :/') + self.cache = JSONCache('inkycal_slideshow') + self.cache_data = self.cache.read() + # set a 'first run' signal self._first_run = True # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def generate_image(self): """Generate image for this module""" @@ -86,17 +87,19 @@ def generate_image(self): im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # rotates list items by 1 index - def rotate(somelist): - return somelist[1:] + somelist[:1] + def rotate(list: list): + return list[1:] + list[:1] # Switch to the next image if this is not the first run if self._first_run: self._first_run = False + self.cache_data["current_index"] = 0 else: self.images = rotate(self.images) + self.cache_data["current_index"] = (self.cache_data["current_index"] + 1) % len(self.images) # initialize custom image class im = Images() @@ -110,7 +113,7 @@ def rotate(somelist): # Remove background if present im.remove_alpha() - # if autoflip was enabled, flip the image + # if auto-flip was enabled, flip the image if self.autoflip: im.autoflip(self.orientation) @@ -123,6 +126,8 @@ def rotate(somelist): # with the images now send, clear the current image im.clear() + self.cache.write(self.cache_data) + # return images return im_black, im_colour diff --git a/inkycal/modules/inkycal_stocks.py b/inkycal/modules/inkycal_stocks.py index cf58a41a..4e7538fd 100755 --- a/inkycal/modules/inkycal_stocks.py +++ b/inkycal/modules/inkycal_stocks.py @@ -54,7 +54,7 @@ def __init__(self, config): self.tickers = config['tickers'] # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def generate_image(self): """Generate image for this module""" @@ -63,7 +63,7 @@ def generate_image(self): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'image size: {im_width} x {im_height} px') + logger.debug(f'image size: {im_width} x {im_height} px') # Create an image for black pixels and one for coloured pixels (required) im_black = Image.new('RGB', size=im_size, color='white') @@ -142,7 +142,7 @@ def generate_image(self): logger.warning(f"Failed to get '{stockName}' ticker price hint! Using " "default precision of 2 instead.") - stockHistory = yfTicker.history("30d") + stockHistory = yfTicker.history("1mo") stockHistoryLen = len(stockHistory) logger.info(f'fetched {stockHistoryLen} datapoints ...') previousQuote = (stockHistory.tail(2)['Close'].iloc[0]) diff --git a/inkycal/modules/inkycal_textfile_to_display.py b/inkycal/modules/inkycal_textfile_to_display.py index 7dc4987d..dc36ad1e 100644 --- a/inkycal/modules/inkycal_textfile_to_display.py +++ b/inkycal/modules/inkycal_textfile_to_display.py @@ -31,7 +31,7 @@ def __init__(self, config): self.make_request = True if self.filepath.startswith("https://") else False # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def _validate(self): """Validate module-specific parameters""" @@ -45,7 +45,7 @@ def generate_image(self): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') diff --git a/inkycal/modules/inkycal_tindie.py b/inkycal/modules/inkycal_tindie.py index 24b51cb4..bf2b92bf 100755 --- a/inkycal/modules/inkycal_tindie.py +++ b/inkycal/modules/inkycal_tindie.py @@ -32,7 +32,7 @@ def __init__(self, config): # self.mode = config['mode'] # unshipped_orders, shipped_orders, all_orders # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def generate_image(self): """Generate image for this module""" @@ -40,7 +40,7 @@ def generate_image(self): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'image size: {im_width} x {im_height} px') + logger.debug(f'image size: {im_width} x {im_height} px') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') @@ -50,6 +50,7 @@ def generate_image(self): if internet_available(): logger.info('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise NetworkNotReachableError # Set some parameters for formatting feeds diff --git a/inkycal/modules/inkycal_todoist.py b/inkycal/modules/inkycal_todoist.py index 55e725ee..0e955850 100644 --- a/inkycal/modules/inkycal_todoist.py +++ b/inkycal/modules/inkycal_todoist.py @@ -56,7 +56,7 @@ def __init__(self, config): self._api = TodoistAPI(config['api_key']) # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def _validate(self): """Validate module-specific parameters""" @@ -70,7 +70,7 @@ def generate_image(self): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') @@ -80,6 +80,7 @@ def generate_image(self): if internet_available(): logger.info('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise NetworkNotReachableError # Set some parameters for formatting todos diff --git a/inkycal/modules/inkycal_weather.py b/inkycal/modules/inkycal_weather.py index 7aeb8451..f5861f4a 100644 --- a/inkycal/modules/inkycal_weather.py +++ b/inkycal/modules/inkycal_weather.py @@ -2,12 +2,12 @@ Inkycal weather module Copyright by aceinnolab """ - -import arrow import decimal import logging import math +from typing import Tuple +import arrow from PIL import Image from PIL import ImageDraw from PIL import ImageFont @@ -51,7 +51,7 @@ class Weather(inkycal_module): "options": [True, False], }, - "round_windspeed": { + "round_wind_speed": { "label": "Round windspeed?", "options": [True, False], }, @@ -89,7 +89,7 @@ def __init__(self, config): # Check if all required parameters are present for param in self.requires: - if not param in config: + if param not in config: raise Exception(f'config is missing {param}') # required parameters @@ -98,15 +98,15 @@ def __init__(self, config): # optional parameters self.round_temperature = config['round_temperature'] - self.round_windspeed = config['round_windspeed'] + self.round_wind_speed = config['round_windspeed'] self.forecast_interval = config['forecast_interval'] self.hour_format = int(config['hour_format']) if config['units'] == "imperial": self.temp_unit = "fahrenheit" else: self.temp_unit = "celsius" - - if config['use_beaufort'] == True: + + if config['use_beaufort']: self.wind_unit = "beaufort" elif config['units'] == "imperial": self.wind_unit = "miles_hour" @@ -116,17 +116,17 @@ def __init__(self, config): # additional configuration self.owm = OpenWeatherMap( - api_key=self.api_key, - city_id=self.location, - wind_unit=self.wind_unit, + api_key=self.api_key, + city_id=self.location, + wind_unit=self.wind_unit, temp_unit=self.temp_unit, - language=self.locale, + language=self.locale, tz_name=self.timezone - ) - + ) + self.weatherfont = ImageFont.truetype( fonts['weathericons-regular-webfont'], size=self.fontsize) - + if self.wind_unit == "beaufort": self.windDispUnit = "bft" elif self.wind_unit == "knots": @@ -143,9 +143,7 @@ def __init__(self, config): self.tempDispUnit = "°" # give an OK message - print(f"{__name__} loaded") - - + logger.debug(f"{__name__} loaded") def generate_image(self): """Generate image for this module""" @@ -154,7 +152,7 @@ def generate_image(self): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') @@ -162,8 +160,9 @@ def generate_image(self): # Check if internet is available if internet_available(): - logger.info('Connection test passed') + logger.debug('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise NetworkNotReachableError def get_moon_phase(): @@ -190,7 +189,7 @@ def get_moon_phase(): 7: '\uf0ae' }[int(index) & 7] - def is_negative(temp:str): + def is_negative(temp: str): """Check if temp is below freezing point of water (0°C/32°F) returns True if temp below freezing point, else False""" answer = False @@ -223,12 +222,19 @@ def is_negative(temp:str): '50n': '\uf023' } - def draw_icon(image, xy, box_size, icon, rotation=None): - """Custom function to add icons of weather font on image - image = on which image should the text be added? - xy = xy-coordinates as tuple -> (x,y) - box_size = size of text-box -> (width,height) - icon = icon-unicode, looks this up in weathericons dictionary + def draw_icon(image: Image, xy: Tuple[int, int], box_size: Tuple[int, int], icon: str, rotation=None): + """Custom function to add icons of weather font on the image. + + Args: + - image: + the image on which image should the text be added + - xy: + coordinates as tuple -> (x,y) + - box_size: + size of text-box -> (width,height) + - icon: + icon-unicode, looks this up in weather-icons dictionary + """ icon_size_correction = { @@ -263,7 +269,6 @@ def draw_icon(image, xy, box_size, icon, rotation=None): '\uf0a0': 0, '\uf0a3': 0, '\uf0a7': 0, - '\uf0aa': 0, '\uf0ae': 0 } @@ -277,8 +282,7 @@ def draw_icon(image, xy, box_size, icon, rotation=None): font = ImageFont.truetype(font.path, size) text_width, text_height = font.getbbox(text)[2:] - while (text_width < int(box_width * 0.9) and - text_height < int(box_height * 0.9)): + while text_width < int(box_width * 0.9) and text_height < int(box_height * 0.9): size += 1 font = ImageFont.truetype(font.path, size) text_width, text_height = font.getbbox(text)[2:] @@ -289,8 +293,6 @@ def draw_icon(image, xy, box_size, icon, rotation=None): x = int((box_width / 2) - (text_width / 2)) y = int((box_height / 2) - (text_height / 2)) - # Draw the text in the text-box - draw = ImageDraw.Draw(image) space = Image.new('RGBA', (box_width, box_height)) ImageDraw.Draw(space).text((x, y), text, fill='black', font=font) @@ -349,17 +351,17 @@ def draw_icon(image, xy, box_size, icon, rotation=None): row3 = row2 + line_gap + row_height # Draw lines on each row and border - ############################################################################ - ## draw = ImageDraw.Draw(im_black) - ## draw.line((0, 0, im_width, 0), fill='red') - ## draw.line((0, im_height-1, im_width, im_height-1), fill='red') - ## draw.line((0, row1, im_width, row1), fill='black') - ## draw.line((0, row1+row_height, im_width, row1+row_height), fill='black') - ## draw.line((0, row2, im_width, row2), fill='black') - ## draw.line((0, row2+row_height, im_width, row2+row_height), fill='black') - ## draw.line((0, row3, im_width, row3), fill='black') - ## draw.line((0, row3+row_height, im_width, row3+row_height), fill='black') - ############################################################################ + ########################################################################### + # draw = ImageDraw.Draw(im_black) + # draw.line((0, 0, im_width, 0), fill='red') + # draw.line((0, im_height-1, im_width, im_height-1), fill='red') + # draw.line((0, row1, im_width, row1), fill='black') + # draw.line((0, row1+row_height, im_width, row1+row_height), fill='black') + # draw.line((0, row2, im_width, row2), fill='black') + # draw.line((0, row2+row_height, im_width, row2+row_height), fill='black') + # draw.line((0, row3, im_width, row3), fill='black') + # draw.line((0, row3+row_height, im_width, row3+row_height), fill='black') + ########################################################################### # Positions for current weather details weather_icon_pos = (col1, 0) @@ -378,24 +380,24 @@ def draw_icon(image, xy, box_size, icon, rotation=None): sunset_time_pos = (col3 + icon_small, row3) # Positions for forecast 1 - stamp_fc1 = (col4, row1) - icon_fc1 = (col4, row1 + row_height) - temp_fc1 = (col4, row3) + stamp_fc1 = (col4, row1) # noqa + icon_fc1 = (col4, row1 + row_height) # noqa + temp_fc1 = (col4, row3) # noqa # Positions for forecast 2 - stamp_fc2 = (col5, row1) - icon_fc2 = (col5, row1 + row_height) - temp_fc2 = (col5, row3) + stamp_fc2 = (col5, row1) # noqa + icon_fc2 = (col5, row1 + row_height) # noqa + temp_fc2 = (col5, row3) # noqa # Positions for forecast 3 - stamp_fc3 = (col6, row1) - icon_fc3 = (col6, row1 + row_height) - temp_fc3 = (col6, row3) + stamp_fc3 = (col6, row1) # noqa + icon_fc3 = (col6, row1 + row_height) # noqa + temp_fc3 = (col6, row3) # noqa # Positions for forecast 4 - stamp_fc4 = (col7, row1) - icon_fc4 = (col7, row1 + row_height) - temp_fc4 = (col7, row3) + stamp_fc4 = (col7, row1) # noqa + icon_fc4 = (col7, row1 + row_height) # noqa + temp_fc4 = (col7, row3) # noqa # Create current-weather and weather-forecast objects logging.debug('looking up location by ID') @@ -404,7 +406,7 @@ def draw_icon(image, xy, box_size, icon, rotation=None): # Set decimals dec_temp = 0 if self.round_temperature == True else 1 - dec_wind = 0 if self.round_windspeed == True else 1 + dec_wind = 0 if self.round_wind_speed == True else 1 logging.debug(f'temperature unit: {self.temp_unit}') logging.debug(f'decimals temperature: {dec_temp} | decimals wind: {dec_wind}') @@ -424,7 +426,8 @@ def draw_icon(image, xy, box_size, icon, rotation=None): fc_data['fc' + str(index + 1)] = { 'temp': f"{forecast['temp']:.{dec_temp}f}{self.tempDispUnit}", 'icon': forecast["icon"], - 'stamp': forecast["datetime"].strftime("%I %p" if self.hour_format == 12 else "%H:%M")} + 'stamp': forecast["datetime"].strftime("%I %p" if self.hour_format == 12 else "%H:%M") + } elif self.forecast_interval == 'daily': @@ -433,7 +436,7 @@ def draw_icon(image, xy, box_size, icon, rotation=None): daily_forecasts = [self.owm.get_forecast_for_day(days) for days in range(1, 5)] for index, forecast in enumerate(daily_forecasts): - fc_data['fc' + str(index +1)] = { + fc_data['fc' + str(index + 1)] = { 'temp': f'{forecast["temp_min"]:.{dec_temp}f}{self.tempDispUnit}/{forecast["temp_max"]:.{dec_temp}f}{self.tempDispUnit}', 'icon': forecast['icon'], 'stamp': forecast['datetime'].strftime("%A") @@ -513,6 +516,9 @@ def draw_icon(image, xy, box_size, icon, rotation=None): # Add the forecast data to the correct places for pos in range(1, len(fc_data) + 1): stamp = fc_data[f'fc{pos}']['stamp'] + # check if we're using daily forecasts + if "day" in stamp: + stamp = arrow.get(fc_data[f'fc{pos}']['stamp'], "dddd").format("dddd", locale=self.locale) icon = weather_icons[fc_data[f'fc{pos}']['icon']] temp = fc_data[f'fc{pos}']['temp'] diff --git a/inkycal/modules/inkycal_webshot.py b/inkycal/modules/inkycal_webshot.py index 299145d2..36557521 100644 --- a/inkycal/modules/inkycal_webshot.py +++ b/inkycal/modules/inkycal_webshot.py @@ -40,7 +40,10 @@ class Webshot(inkycal_module): }, "crop_h": { "label": "Please enter the crop height", - } + }, + "rotation": { + "label": "Please enter the rotation. Must be either 0, 90, 180 or 270", + }, } def __init__(self, config): @@ -72,8 +75,14 @@ def __init__(self, config): else: self.crop_y = 0 + self.rotation = 0 + if "rotation" in config: + self.rotation = int(config["rotation"]) + if self.rotation not in [0, 90, 180, 270]: + raise Exception("Rotation must be either 0, 90, 180 or 270") + # give an OK message - print(f'Inkycal webshot loaded') + logger.debug(f'Inkycal webshot loaded') def generate_image(self): """Generate image for this module""" @@ -89,7 +98,7 @@ def generate_image(self): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info('image size: {} x {} px'.format(im_width, im_height)) + logger.debug('image size: {} x {} px'.format(im_width, im_height)) # Create an image for black pixels and one for coloured pixels (required) im_black = Image.new('RGB', size=im_size, color='white') @@ -99,12 +108,13 @@ def generate_image(self): if internet_available(): logger.info('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise Exception('Network could not be reached :/') logger.info( f'preparing webshot from {self.url}... cropH{self.crop_h} cropW{self.crop_w} cropX{self.crop_x} cropY{self.crop_y}') - shot = WebShot() + shot = WebShot(size=(im_height, im_width)) shot.params = { "--crop-x": self.crop_x, @@ -150,11 +160,21 @@ def generate_image(self): centerPosX = int((im_width / 2) - (im.image.width / 2)) - webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY)) - im_black.paste(webshotSpaceBlack) - webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY)) - im_colour.paste(webshotSpaceColour) + if self.rotation != 0: + webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY)) + im_black.paste(webshotSpaceBlack) + im_black = im_black.rotate(self.rotation, expand=True) + + webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY)) + im_colour.paste(webshotSpaceColour) + im_colour = im_colour.rotate(self.rotation, expand=True) + else: + webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY)) + im_black.paste(webshotSpaceBlack) + + webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY)) + im_colour.paste(webshotSpaceColour) im.clear() logger.info(f'added webshot image') diff --git a/inkycal/modules/inkycal_xkcd.py b/inkycal/modules/inkycal_xkcd.py index b2ce25ee..864e15c1 100644 --- a/inkycal/modules/inkycal_xkcd.py +++ b/inkycal/modules/inkycal_xkcd.py @@ -11,6 +11,8 @@ logger = logging.getLogger(__name__) +settings = Settings() + class Xkcd(inkycal_module): name = "xkcd - Displays comics from xkcd.com by Randall Munroe" @@ -51,13 +53,13 @@ def __init__(self, config): self.scale_filter = config['filter'] # give an OK message - print(f'Inkycal XKCD loaded') + logger.debug(f'Inkycal XKCD loaded') def generate_image(self): """Generate image for this module""" # Create tmp path - tmpPath = f"{top_level}/temp" + tmpPath = settings.TEMPORARY_FOLDER if not os.path.exists(tmpPath): os.mkdir(tmpPath) @@ -66,7 +68,7 @@ def generate_image(self): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info('image size: {} x {} px'.format(im_width, im_height)) + logger.debug('image size: {} x {} px'.format(im_width, im_height)) # Create an image for black pixels and one for coloured pixels (required) im_black = Image.new('RGB', size=im_size, color='white') @@ -76,6 +78,7 @@ def generate_image(self): if internet_available(): logger.info('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise Exception('Network could not be reached :/') # Set some parameters for formatting feeds diff --git a/inkycal/settings.py b/inkycal/settings.py new file mode 100644 index 00000000..9c4b9dce --- /dev/null +++ b/inkycal/settings.py @@ -0,0 +1,22 @@ +"""Settings class +Used to initialize the settings for the application. +""" +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) + + +class Settings: + """Settings class to initialize the settings for the application. + + """ + CACHE_PATH = os.path.join(basedir, "cache") + LOG_PATH = os.path.join(basedir, "../logs") + INKYCAL_LOG_PATH = os.path.join(LOG_PATH, "inkycal.log") + FONT_PATH = os.path.join(basedir, "../fonts") + IMAGE_FOLDER = os.path.join(basedir, "../image_folder") + PARALLEL_DRIVER_PATH = os.path.join(basedir, "display", "drivers", "parallel_drivers") + TEMPORARY_FOLDER = os.path.join(basedir, "tmp") + VCOM = "2.0" + # /boot/settings.json is path on older releases, while the latter is more the more recent ones + SETTINGS_JSON_PATHS = ["/boot/settings.json", "/boot/firmware/settings.json"] diff --git a/inkycal/utils/__init__.py b/inkycal/utils/__init__.py new file mode 100644 index 00000000..e0a85ab4 --- /dev/null +++ b/inkycal/utils/__init__.py @@ -0,0 +1,2 @@ +from .pisugar import PiSugar +from .json_cache import JSONCache \ No newline at end of file diff --git a/inkycal/utils/json_cache.py b/inkycal/utils/json_cache.py new file mode 100644 index 00000000..dda173af --- /dev/null +++ b/inkycal/utils/json_cache.py @@ -0,0 +1,32 @@ +"""JSON Cache +Can be used to cache JSON data to disk. This is useful for caching data to survive reboots. +""" +import json +import os + +from inkycal.settings import Settings + +settings = Settings() + + +class JSONCache: + def __init__(self, name: str, create_if_not_exists: bool = True): + self.path = os.path.join(settings.CACHE_PATH,f"{name}.json") + + if not os.path.exists(settings.CACHE_PATH): + os.makedirs(settings.CACHE_PATH) + + if create_if_not_exists and not os.path.exists(self.path): + with open(self.path, "w", encoding="utf-8") as file: + json.dump({}, file) + + def read(self): + try: + with open(self.path, "r", encoding="utf-8") as file: + return json.load(file) + except FileNotFoundError: + return {} + + def write(self, data: dict): + with open(self.path, "w", encoding="utf-8") as file: + json.dump(data, file, indent=4, sort_keys=True) diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py new file mode 100644 index 00000000..0c899114 --- /dev/null +++ b/inkycal/utils/pisugar.py @@ -0,0 +1,147 @@ +"""PiSugar helper class for Inkycal.""" + +import logging +import subprocess + +from inkycal.settings import Settings +import arrow + +settings = Settings() + +logger = logging.getLogger(__name__) + + +class PiSugar: + + def __init__(self): + # replace "command" with actual command + self.command_template = 'echo "command" | nc -q 0 127.0.0.1 8423' + self.allowed_commands = ["get battery", "get model", "get rtc_time", "get rtc_alarm_enabled", + "get rtc_alarm_time", "get alarm_repeat", "rtc_pi2rtc", "rtc_alarm_set"] + + def _get_output(self, command, param=None): + if command not in self.allowed_commands: + logger.error(f"Command {command} not allowed") + return None + if param: + cmd = self.command_template.replace("command", f"{command} {param}") + else: + cmd = self.command_template.replace("command", command) + try: + result = subprocess.run(cmd, shell=True, text=True, capture_output=True) + if result.returncode != 0: + print(f"Command failed with {result.stderr}") + return None + output = result.stdout.strip() + return output + except Exception as e: + logger.error(f"Error executing command: {e}") + return None + + def get_battery(self) -> float or None: + """Get the battery level in percentage. + + Returns: + int or None: The battery level in percentage or None if the command fails. + """ + battery_output = self._get_output("get battery") + if battery_output: + for line in battery_output.splitlines(): + if 'battery:' in line: + return float(line.split(':')[1].strip()) + return None + + def get_model(self) -> str or None: + """Get the PiSugar model.""" + model_output = self._get_output("get model") + if model_output: + for line in model_output.splitlines(): + if 'model:' in line: + return line.split(':')[1].strip() + return None + + def get_rtc_time(self) -> arrow.arrow or None: + """Get the RTC time.""" + result = self._get_output("get rtc_time") + if result: + rtc_time = result.split("rtc_time: ")[1].strip() + return arrow.get(rtc_time) + return None + + def get_rtc_alarm_enabled(self) -> str or None: + """Get the RTC alarm enabled status.""" + result = self._get_output("get rtc_alarm_enabled") + if result: + second_line = result.splitlines()[1] + output = second_line.split('rtc_alarm_enabled: ')[1].strip() + return True if output == "true" else False + return None + + def get_rtc_alarm_time(self) -> arrow.arrow or None: + """Get the RTC alarm time.""" + result = self._get_output("get rtc_alarm_time") + if result: + alarm_time = result.split('rtc_alarm_time: ')[1].strip() + return arrow.get(alarm_time) + return None + + def get_alarm_repeat(self) -> dict or None: + """Get the alarm repeat status. + + Returns: + dict or None: A dictionary with the alarm repeating days or None if the command fails. + """ + result = self._get_output("get alarm_repeat") + if result: + repeating_days = f"{int(result.split('alarm_repeat: ')[1].strip()):8b}".strip() + data = {"Monday": False, "Tuesday": False, "Wednesday": False, "Thursday": False, "Friday": False, + "Saturday": False, "Sunday": False} + if repeating_days[0] == "1": + data["Monday"] = True + if repeating_days[1] == "1": + data["Tuesday"] = True + if repeating_days[2] == "1": + data["Wednesday"] = True + if repeating_days[3] == "1": + data["Thursday"] = True + if repeating_days[4] == "1": + data["Friday"] = True + if repeating_days[5] == "1": + data["Saturday"] = True + if repeating_days[6] == "1": + data["Sunday"] = True + return data + return None + + def rtc_pi2rtc(self) -> bool: + """Sync the Pi time to RTC. + + Returns: + bool: True if the sync was successful, False otherwise. + """ + result = self._get_output("rtc_pi2rtc") + if result: + status = result.split('rtc_pi2rtc: ')[1].strip() + if status == "done": + return True + return False + + def rtc_alarm_set(self, time: arrow.arrow, repeat:int=127) -> bool: + """Set the RTC alarm time. + + Args: + time (arrow.arrow): The alarm time in ISO 8601 format. + repeat: int representing 7-bit binary number of repeating days. e.g. 127 = 1111111 = repeat every day + + Returns: + bool: True if the alarm was set successfully, False otherwise. + """ + iso_format = time.isoformat() + result = self._get_output("rtc_alarm_set", f"{iso_format } {repeat}") + if result: + status = result.split('rtc_alarm_set: ')[1].strip() + if status == "done": + return True + return False + + diff --git a/requirements.txt b/requirements.txt index 37d5b990..de527553 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ appdirs==1.4.4 arrow==1.3.0 asyncio==3.4.3 beautifulsoup4==4.12.3 -certifi==2024.2.2 +certifi==2024.7.4 cfgv==3.4.0 charset-normalizer==3.3.2 colorzero==2.0 @@ -37,7 +37,7 @@ python-dotenv==1.0.1 pytz==2024.1 PyYAML==6.0.1 recurring-ical-events==2.1.2 -requests==2.32.0 +requests==2.32.3 sgmllib3k==1.0.0 six==1.16.0 soupsieve==2.5 @@ -46,9 +46,9 @@ types-python-dateutil==2.8.19.20240106 typing_extensions==4.9.0 tzdata==2024.1 tzlocal==5.2 -urllib3==2.2.0 +urllib3==2.2.2 virtualenv==20.25.0 webencodings==0.5.1 x-wr-timezone==0.0.6 xkcd==2.4.2 -yfinance==0.2.36 +yfinance==0.2.40 diff --git a/setup.py b/setup.py index 6064cceb..e120a94f 100644 --- a/setup.py +++ b/setup.py @@ -13,10 +13,10 @@ required = [i.split(' ')[0] for i in required] __project__ = "inkycal" -__version__ = "2.0.3" +__version__ = "2.0.4" __description__ = "Inkycal is a python3 software for syncing icalendar events, weather and news on selected E-Paper displays" __packages__ = ["inkycal"] -__author__ = "aceisace" +__author__ = "aceinnolab" __author_email__ = "aceisace63@yahoo.com" __url__ = "https://github.com/aceinnolab/Inkycal" diff --git a/tests/test_functions.py b/tests/test_functions.py index b624978d..0d8a6dd4 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -1,12 +1,22 @@ """ Test the functions in the functions module. """ +import unittest + from PIL import Image, ImageFont -from inkycal.custom import write, fonts +from inkycal.custom import write, fonts, get_system_tz + + +class TestIcalendar(unittest.TestCase): + + def test_write(self): + im = Image.new("RGB", (500, 200), "white") + font = ImageFont.truetype(fonts['NotoSans-SemiCondensed'], size=40) + write(im, (125, 75), (250, 50), "Hello World", font) + # im.show() + + def test_get_system_tz(self): + tz = get_system_tz() + assert isinstance(tz, str) -def test_write(): - im = Image.new("RGB", (500, 200), "white") - font = ImageFont.truetype(fonts['NotoSans-SemiCondensed'], size = 40) - write(im, (125,75), (250, 50), "Hello World", font) - # im.show() diff --git a/tests/test_inkycal_agenda.py b/tests/test_inkycal_agenda.py index af002ec3..685bb860 100755 --- a/tests/test_inkycal_agenda.py +++ b/tests/test_inkycal_agenda.py @@ -28,7 +28,7 @@ "padding_x": 10, "padding_y": 10, "fontsize": 12, - "language": "en" + "language": "de" } }, { @@ -37,7 +37,7 @@ "size": [500, 800], "ical_urls": sample_url, "ical_files": None, - "date_format": "ddd D MMM", + "date_format": "DD.MMMM YYYY", "time_format": "HH:mm", "padding_x": 10, "padding_y": 10, diff --git a/tests/test_inkycal_calendar.py b/tests/test_inkycal_calendar.py index cb28b9ac..434d5d83 100755 --- a/tests/test_inkycal_calendar.py +++ b/tests/test_inkycal_calendar.py @@ -20,7 +20,7 @@ { "name": "Calendar", "config": { - "size": [500, 500], + "size": [500, 600], "week_starts_on": "Monday", "show_events": True, "ical_urls": sample_url, diff --git a/tests/test_inkycal_weather.py b/tests/test_inkycal_weather.py index bcc50cef..dce32977 100755 --- a/tests/test_inkycal_weather.py +++ b/tests/test_inkycal_weather.py @@ -30,11 +30,11 @@ "forecast_interval": "daily", "units": "metric", "hour_format": "12", - "use_beaufort": True, + "use_beaufort": False, "padding_x": 10, "padding_y": 10, "fontsize": 12, - "language": "en" + "language": "de" } }, { diff --git a/tests/test_inkycal_webshot.py b/tests/test_inkycal_webshot.py index 7105a12c..073a437a 100755 --- a/tests/test_inkycal_webshot.py +++ b/tests/test_inkycal_webshot.py @@ -6,17 +6,22 @@ import unittest from inkycal.modules import Webshot +from inkycal.modules.inky_image import Inkyimage +from tests import Config logger = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) +preview = Inkyimage.preview +merge = Inkyimage.merge + tests = [ { "position": 1, "name": "Webshot", "config": { - "size": [400, 100], - "url": "https://github.com", + "size": [400, 200], + "url": "https://aceinnolab.com", "palette": "bwr", "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" } @@ -25,9 +30,10 @@ "position": 1, "name": "Webshot", "config": { - "size": [400, 200], - "url": "https://github.com", + "size": [400, 400], + "url": "https://aceinnolab.com", "palette": "bwy", + "rotation": 0, "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" } }, @@ -35,9 +41,10 @@ "position": 1, "name": "Webshot", "config": { - "size": [400, 300], - "url": "https://github.com", + "size": [400, 600], + "url": "https://aceinnolab.com", "palette": "bw", + "rotation": 90, "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" } }, @@ -45,9 +52,10 @@ "position": 1, "name": "Webshot", "config": { - "size": [400, 400], - "url": "https://github.com", + "size": [400, 800], + "url": "https://aceinnolab.com", "palette": "bwr", + "rotation": 180, "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" } } @@ -60,6 +68,7 @@ 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() + im_black, im_colour = module.generate_image() + if Config.USE_PREVIEW: + preview(merge(im_black, im_colour)) logger.info('OK') - diff --git a/tests/test_main.py b/tests/test_main.py index ceb834c5..e8c30afb 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -21,9 +21,9 @@ def test_init(self): assert inkycal.settings["info_section_height"] == 70 assert inkycal.settings["border_around_modules"] is True - def test_run(self): + def test_dry_run(self): inkycal = Inkycal(self.settings_path, render=False) - inkycal.test() + inkycal.dry_run() def test_countdown(self): inkycal = Inkycal(self.settings_path, render=False)