Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -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
|
||||
|
@@ -8,29 +8,25 @@ import logging
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from typing import Tuple
|
||||
|
||||
import arrow
|
||||
import PIL
|
||||
import requests
|
||||
import tzlocal
|
||||
from PIL import Image
|
||||
from PIL import ImageDraw
|
||||
from PIL import ImageFont
|
||||
|
||||
logs = logging.getLogger(__name__)
|
||||
logs.setLevel(level=logging.INFO)
|
||||
from inkycal.settings import Settings
|
||||
|
||||
# Get the path to the Inkycal folder
|
||||
top_level = "/".join(os.path.dirname(os.path.abspath(os.path.dirname(__file__))).split("/")[:-1])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Get path of 'fonts' and 'images' folders within Inkycal folder
|
||||
fonts_location = os.path.join(top_level, "fonts/")
|
||||
image_folder = os.path.join(top_level, "image_folder/")
|
||||
settings = Settings()
|
||||
|
||||
# Get available fonts within fonts folder
|
||||
fonts = {}
|
||||
|
||||
for path, dirs, files in os.walk(fonts_location):
|
||||
for path, dirs, files in os.walk(settings.FONT_PATH):
|
||||
for _ in files:
|
||||
if _.endswith(".otf"):
|
||||
name = _.split(".otf")[0]
|
||||
@@ -39,7 +35,7 @@ for path, dirs, files in os.walk(fonts_location):
|
||||
if _.endswith(".ttf"):
|
||||
name = _.split(".ttf")[0]
|
||||
fonts[name] = os.path.join(path, _)
|
||||
logs.debug(f"Found fonts: {json.dumps(fonts, indent=4, sort_keys=True)}")
|
||||
logger.debug(f"Found fonts: {json.dumps(fonts, indent=4, sort_keys=True)}")
|
||||
available_fonts = [key for key, values in fonts.items()]
|
||||
|
||||
|
||||
@@ -77,16 +73,16 @@ def get_system_tz() -> str:
|
||||
|
||||
>>> import arrow
|
||||
>>> print(arrow.now()) # returns non-timezone-aware time
|
||||
>>> print(arrow.now(tz=get_system_tz()) # prints timezone aware time.
|
||||
>>> print(arrow.now(tz=get_system_tz())) # prints timezone aware time.
|
||||
"""
|
||||
try:
|
||||
local_tz = tzlocal.get_localzone().key
|
||||
logs.debug(f"Local system timezone is {local_tz}.")
|
||||
logger.debug(f"Local system timezone is {local_tz}.")
|
||||
except:
|
||||
logs.error("System timezone could not be parsed!")
|
||||
logs.error("Please set timezone manually!. Falling back to UTC...")
|
||||
logger.error("System timezone could not be parsed!")
|
||||
logger.error("Please set timezone manually!. Falling back to UTC...")
|
||||
local_tz = "UTC"
|
||||
logs.debug(f"The time is {arrow.now(tz=local_tz).format('YYYY-MM-DD HH:mm:ss ZZ')}.")
|
||||
logger.debug(f"The time is {arrow.now(tz=local_tz).format('YYYY-MM-DD HH:mm:ss ZZ')}.")
|
||||
return local_tz
|
||||
|
||||
|
||||
@@ -115,7 +111,7 @@ def auto_fontsize(font, max_height):
|
||||
return font
|
||||
|
||||
|
||||
def write(image, xy, box_size, text, font=None, **kwargs):
|
||||
def write(image: Image, xy: Tuple[int, int], box_size: Tuple[int, int], text: str, font=None, **kwargs):
|
||||
"""Writes text on an image.
|
||||
|
||||
Writes given text at given position on the specified image.
|
||||
@@ -165,7 +161,7 @@ def write(image, xy, box_size, text, font=None, **kwargs):
|
||||
text_bbox = font.getbbox(text)
|
||||
text_width = text_bbox[2] - text_bbox[0]
|
||||
text_bbox_height = font.getbbox("hg")
|
||||
text_height = text_bbox_height[3] - text_bbox_height[1]
|
||||
text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1])
|
||||
|
||||
while text_width < int(box_width * fill_width) and text_height < int(box_height * fill_height):
|
||||
size += 1
|
||||
@@ -173,23 +169,23 @@ def write(image, xy, box_size, text, font=None, **kwargs):
|
||||
text_bbox = font.getbbox(text)
|
||||
text_width = text_bbox[2] - text_bbox[0]
|
||||
text_bbox_height = font.getbbox("hg")
|
||||
text_height = text_bbox_height[3] - text_bbox_height[1]
|
||||
text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1])
|
||||
|
||||
text_bbox = font.getbbox(text)
|
||||
text_width = text_bbox[2] - text_bbox[0]
|
||||
text_bbox_height = font.getbbox("hg")
|
||||
text_height = text_bbox_height[3] - text_bbox_height[1]
|
||||
text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1])
|
||||
|
||||
# Truncate text if text is too long, so it can fit inside the box
|
||||
if (text_width, text_height) > (box_width, box_height):
|
||||
logs.debug(("truncating {}".format(text)))
|
||||
logger.debug(("truncating {}".format(text)))
|
||||
while (text_width, text_height) > (box_width, box_height):
|
||||
text = text[0:-1]
|
||||
text_bbox = font.getbbox(text)
|
||||
text_width = text_bbox[2] - text_bbox[0]
|
||||
text_bbox_height = font.getbbox("hg")
|
||||
text_height = text_bbox_height[3] - text_bbox_height[1]
|
||||
logs.debug(text)
|
||||
text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1])
|
||||
logger.debug(text)
|
||||
|
||||
# Align text to desired position
|
||||
if alignment == "center" or None:
|
||||
@@ -199,10 +195,13 @@ def write(image, xy, box_size, text, font=None, **kwargs):
|
||||
elif alignment == "right":
|
||||
x = int(box_width - text_width)
|
||||
|
||||
# Vertical centering
|
||||
y = int((box_height / 2) - (text_height / 2))
|
||||
|
||||
# Draw the text in the text-box
|
||||
draw = ImageDraw.Draw(image)
|
||||
space = Image.new('RGBA', (box_width, box_height))
|
||||
ImageDraw.Draw(space).text((x, 0), text, fill=colour, font=font)
|
||||
ImageDraw.Draw(space).text((x, y), text, fill=colour, font=font)
|
||||
|
||||
# Uncomment following two lines, comment out above two lines to show
|
||||
# red text-box with white text (debugging purposes)
|
||||
@@ -217,7 +216,7 @@ def write(image, xy, box_size, text, font=None, **kwargs):
|
||||
image.paste(space, xy, space)
|
||||
|
||||
|
||||
def text_wrap(text, font=None, max_width=None):
|
||||
def text_wrap(text: str, font=None, max_width=None):
|
||||
"""Splits a very long text into smaller parts
|
||||
|
||||
Splits a long text to smaller lines which can fit in a line with max_width.
|
||||
@@ -253,7 +252,7 @@ def text_wrap(text, font=None, max_width=None):
|
||||
return lines
|
||||
|
||||
|
||||
def internet_available():
|
||||
def internet_available() -> bool:
|
||||
"""checks if the internet is available.
|
||||
|
||||
Attempts to connect to google.com with a timeout of 5 seconds to check
|
||||
@@ -278,15 +277,16 @@ def internet_available():
|
||||
return False
|
||||
|
||||
|
||||
def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
|
||||
def draw_border(image: Image, xy: Tuple[int, int], size: Tuple[int, int], radius: int = 5, thickness: int = 1,
|
||||
shrinkage: Tuple[int, int] = (0.1, 0.1)) -> None:
|
||||
"""Draws a border at given coordinates.
|
||||
|
||||
Args:
|
||||
- image: The image on which the border should be drawn (usually im_black or
|
||||
im_colour.
|
||||
im_colour).
|
||||
|
||||
- xy: Tuple representing the top-left corner of the border e.g. (32, 100)
|
||||
where 32 is the x co-ordinate and 100 is the y-coordinate.
|
||||
where 32 is the x-coordinate and 100 is the y-coordinate.
|
||||
|
||||
- size: Size of the border as a tuple -> (width, height).
|
||||
|
||||
@@ -324,6 +324,7 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
|
||||
c5, c6 = ((x + width) - diameter, (y + height) - diameter), (x + width, y + height)
|
||||
c7, c8 = (x, (y + height) - diameter), (x + diameter, y + height)
|
||||
|
||||
|
||||
# Draw lines and arcs, creating a square with round corners
|
||||
draw = ImageDraw.Draw(image)
|
||||
draw.line((p1, p2), fill=colour, width=thickness)
|
||||
@@ -338,7 +339,7 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
|
||||
draw.arc((c7, c8), 90, 180, fill=colour, width=thickness)
|
||||
|
||||
|
||||
def draw_border_2(im: PIL.Image, xy: tuple, size: tuple, radius: int):
|
||||
def draw_border_2(im: Image, xy: Tuple[int, int], size: Tuple[int, int], radius: int):
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
||||
x, y = xy
|
||||
|
@@ -41,18 +41,9 @@ def get_json_from_url(request_url):
|
||||
|
||||
|
||||
class OpenWeatherMap:
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str,
|
||||
city_id: int = None,
|
||||
lat: float = None,
|
||||
lon: float = None,
|
||||
api_version: API_VERSIONS = "2.5",
|
||||
temp_unit: TEMP_UNITS = "celsius",
|
||||
wind_unit: WIND_UNITS = "meters_sec",
|
||||
language: str = "en",
|
||||
tz_name: str = "UTC",
|
||||
) -> None:
|
||||
def __init__(self, api_key: str, city_id: int = None, lat: float = None, lon: float = None,
|
||||
api_version: API_VERSIONS = "2.5", temp_unit: TEMP_UNITS = "celsius",
|
||||
wind_unit: WIND_UNITS = "meters_sec", language: str = "en", tz_name: str = "UTC") -> None:
|
||||
self.api_key = api_key
|
||||
self.temp_unit = temp_unit
|
||||
self.wind_unit = wind_unit
|
||||
@@ -106,7 +97,7 @@ class OpenWeatherMap:
|
||||
current_weather["temp_feels_like"] = self.get_converted_temperature(current_data["main"]["feels_like"])
|
||||
current_weather["min_temp"] = self.get_converted_temperature(current_data["main"]["temp_min"])
|
||||
current_weather["max_temp"] = self.get_converted_temperature(current_data["main"]["temp_max"])
|
||||
current_weather["humidity"] = current_data["main"]["humidity"] # OWM Unit: % rH
|
||||
current_weather["humidity"] = current_data["main"]["humidity"] # OWM Unit: % rH
|
||||
current_weather["wind"] = self.get_converted_windspeed(
|
||||
current_data["wind"]["speed"]
|
||||
) # OWM Unit Default: meter/sec, Metric: meter/sec
|
||||
@@ -161,10 +152,10 @@ class OpenWeatherMap:
|
||||
forecast["wind"]["speed"]
|
||||
), # OWM Unit Default: meter/sec, Metric: meter/sec, Imperial: miles/hour
|
||||
"wind_gust": self.get_converted_windspeed(forecast["wind"]["gust"]),
|
||||
"pressure": forecast["main"]["pressure"], # OWM Unit: hPa
|
||||
"humidity": forecast["main"]["humidity"], # OWM Unit: % rH
|
||||
"pressure": forecast["main"]["pressure"], # OWM Unit: hPa
|
||||
"humidity": forecast["main"]["humidity"], # OWM Unit: % rH
|
||||
"precip_probability": forecast["pop"]
|
||||
* 100.0, # OWM value is unitless, directly converting to % scale
|
||||
* 100.0, # OWM value is unitless, directly converting to % scale
|
||||
"icon": forecast["weather"][0]["icon"],
|
||||
"datetime": datetime.fromtimestamp(forecast["dt"], tz=self.tz_zone),
|
||||
}
|
||||
@@ -187,7 +178,7 @@ class OpenWeatherMap:
|
||||
:return:
|
||||
Forecast dictionary
|
||||
"""
|
||||
# Make sure hourly forecasts are up to date
|
||||
# Make sure hourly forecasts are up-to-date
|
||||
_ = self.get_weather_forecast()
|
||||
|
||||
# Calculate the start and end times for the specified number of days from now
|
||||
@@ -207,7 +198,7 @@ class OpenWeatherMap:
|
||||
]
|
||||
|
||||
# In case the next available forecast is already for the next day, use that one for the less than 3 remaining hours of today
|
||||
if forecasts == []:
|
||||
if not forecasts:
|
||||
forecasts.append(self.hourly_forecasts[0])
|
||||
|
||||
# Get rain and temperatures for that day
|
||||
|
@@ -1,14 +1,12 @@
|
||||
"""
|
||||
Inkycal ePaper driving functions
|
||||
Copyright by aceisace
|
||||
Copyright by aceinnolab
|
||||
"""
|
||||
import os
|
||||
from importlib import import_module
|
||||
|
||||
import PIL
|
||||
from PIL import Image
|
||||
|
||||
from inkycal.custom import top_level
|
||||
from inkycal.display.supported_models import supported_models
|
||||
|
||||
|
||||
@@ -199,9 +197,7 @@ class Display:
|
||||
|
||||
>>> Display.get_display_names()
|
||||
"""
|
||||
driver_files = top_level + '/inkycal/display/drivers/'
|
||||
drivers = [i for i in os.listdir(driver_files) if i.endswith(".py") and not i.startswith("__") and "_" in i]
|
||||
return drivers
|
||||
return list(supported_models.keys())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@@ -2,22 +2,18 @@
|
||||
10.3" driver class
|
||||
Copyright by aceinnolab
|
||||
"""
|
||||
import os
|
||||
from subprocess import run
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from inkycal.custom import image_folder, top_level
|
||||
from inkycal.settings import Settings
|
||||
|
||||
# Display resolution
|
||||
EPD_WIDTH = 1872
|
||||
EPD_HEIGHT = 1404
|
||||
|
||||
# Please insert VCOM of your display. The Minus sign before is not required
|
||||
VCOM = "2.0"
|
||||
|
||||
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
|
||||
|
||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
||||
settings = Settings()
|
||||
|
||||
|
||||
class EPD:
|
||||
@@ -40,8 +36,8 @@ class EPD:
|
||||
def getbuffer(self, image):
|
||||
"""ad-hoc"""
|
||||
image = image.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
|
||||
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
|
||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
||||
image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), "BMP")
|
||||
command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(settings.IMAGE_FOLDER, "canvas.bmp")}'
|
||||
print(command)
|
||||
return command
|
||||
|
||||
|
@@ -2,20 +2,16 @@
|
||||
7.8" parallel driver class
|
||||
Copyright by aceinnolab
|
||||
"""
|
||||
import os
|
||||
from subprocess import run
|
||||
|
||||
from inkycal.custom import image_folder, top_level
|
||||
from inkycal.settings import Settings
|
||||
|
||||
# Display resolution
|
||||
EPD_WIDTH = 1872
|
||||
EPD_HEIGHT = 1404
|
||||
|
||||
# Please insert VCOM of your display. The Minus sign before is not required
|
||||
VCOM = "2.0"
|
||||
|
||||
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
|
||||
|
||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
||||
settings = Settings()
|
||||
|
||||
|
||||
class EPD:
|
||||
@@ -38,8 +34,8 @@ class EPD:
|
||||
def getbuffer(self, image):
|
||||
"""ad-hoc"""
|
||||
image = image.rotate(90, expand=True)
|
||||
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
|
||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
||||
image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), 'BMP')
|
||||
command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(settings.IMAGE_FOLDER, "canvas.bmp")}'
|
||||
print(command)
|
||||
return command
|
||||
|
||||
|
@@ -2,20 +2,16 @@
|
||||
9.7" driver class
|
||||
Copyright by aceinnolab
|
||||
"""
|
||||
import os
|
||||
from subprocess import run
|
||||
|
||||
from inkycal.custom import image_folder, top_level
|
||||
from inkycal.settings import Settings
|
||||
|
||||
# Display resolution
|
||||
EPD_WIDTH = 1200
|
||||
EPD_HEIGHT = 825
|
||||
|
||||
# Please insert VCOM of your display. The Minus sign before is not required
|
||||
VCOM = "2.0"
|
||||
|
||||
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
|
||||
|
||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
||||
settings = Settings()
|
||||
|
||||
|
||||
class EPD:
|
||||
@@ -38,8 +34,8 @@ class EPD:
|
||||
def getbuffer(self, image):
|
||||
"""ad-hoc"""
|
||||
image = image.rotate(90, expand=True)
|
||||
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
|
||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
||||
image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), "BMP")
|
||||
command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(settings.IMAGE_FOLDER, "canvas.bmp")}'
|
||||
print(command)
|
||||
return command
|
||||
|
||||
|
527
inkycal/display/drivers/epd_13_in_3.py
Normal file
527
inkycal/display/drivers/epd_13_in_3.py
Normal file
@@ -0,0 +1,527 @@
|
||||
"""
|
||||
* | File : epd13in3k.py
|
||||
* | Author : Waveshare team
|
||||
* | Function : Electronic paper driver
|
||||
* | Info :
|
||||
*----------------
|
||||
* | This version: V1.0
|
||||
* | Date : 2023-09-08
|
||||
# | Info : python demo
|
||||
-----------------------------------------------------------------------------
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from inkycal.display.drivers import epdconfig
|
||||
|
||||
# Display resolution
|
||||
EPD_WIDTH = 960
|
||||
EPD_HEIGHT = 680
|
||||
|
||||
GRAY1 = 0xff # white
|
||||
GRAY2 = 0xC0
|
||||
GRAY3 = 0x80 # gray
|
||||
GRAY4 = 0x00 # Blackest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EPD:
|
||||
def __init__(self):
|
||||
self.reset_pin = epdconfig.RST_PIN
|
||||
self.dc_pin = epdconfig.DC_PIN
|
||||
self.busy_pin = epdconfig.BUSY_PIN
|
||||
self.cs_pin = epdconfig.CS_PIN
|
||||
self.width = EPD_WIDTH
|
||||
self.height = EPD_HEIGHT
|
||||
self.GRAY1 = GRAY1 # white
|
||||
self.GRAY2 = GRAY2
|
||||
self.GRAY3 = GRAY3 # gray
|
||||
self.GRAY4 = GRAY4 # Blackest
|
||||
|
||||
self.Lut_Partial = [
|
||||
0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x2A, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x15, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x01, 0x01, 0x01, 0x00,
|
||||
0x0A, 0x00, 0x05, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0x01,
|
||||
0x22, 0x22, 0x22, 0x22, 0x22,
|
||||
0x17, 0x41, 0xA8, 0x32, 0x18,
|
||||
0x00, 0x00, ]
|
||||
|
||||
self.LUT_DATA_4Gray = [
|
||||
0x80, 0x48, 0x4A, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x0A, 0x48, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x88, 0x48, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xA8, 0x48, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x07, 0x23, 0x17, 0x02, 0x00,
|
||||
0x05, 0x01, 0x05, 0x01, 0x02,
|
||||
0x08, 0x02, 0x01, 0x04, 0x04,
|
||||
0x00, 0x02, 0x00, 0x02, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x01,
|
||||
0x22, 0x22, 0x22, 0x22, 0x22,
|
||||
0x17, 0x41, 0xA8, 0x32, 0x30,
|
||||
0x00, 0x00, ]
|
||||
|
||||
if (epdconfig.module_init() != 0):
|
||||
return -1
|
||||
|
||||
# Hardware reset
|
||||
def reset(self):
|
||||
epdconfig.digital_write(self.reset_pin, 1)
|
||||
epdconfig.delay_ms(20)
|
||||
epdconfig.digital_write(self.reset_pin, 0)
|
||||
epdconfig.delay_ms(2)
|
||||
epdconfig.digital_write(self.reset_pin, 1)
|
||||
epdconfig.delay_ms(20)
|
||||
|
||||
def send_command(self, command):
|
||||
epdconfig.digital_write(self.dc_pin, 0)
|
||||
epdconfig.digital_write(self.cs_pin, 0)
|
||||
epdconfig.spi_writebyte([command])
|
||||
epdconfig.digital_write(self.cs_pin, 1)
|
||||
|
||||
def send_data(self, data):
|
||||
epdconfig.digital_write(self.dc_pin, 1)
|
||||
epdconfig.digital_write(self.cs_pin, 0)
|
||||
epdconfig.spi_writebyte([data])
|
||||
epdconfig.digital_write(self.cs_pin, 1)
|
||||
|
||||
def send_data2(self, data):
|
||||
epdconfig.digital_write(self.dc_pin, 1)
|
||||
epdconfig.digital_write(self.cs_pin, 0)
|
||||
epdconfig.SPI.writebytes2(data)
|
||||
epdconfig.digital_write(self.cs_pin, 1)
|
||||
|
||||
def ReadBusy(self):
|
||||
logger.debug("e-Paper busy")
|
||||
busy = epdconfig.digital_read(self.busy_pin)
|
||||
while (busy == 1):
|
||||
busy = epdconfig.digital_read(self.busy_pin)
|
||||
epdconfig.delay_ms(20)
|
||||
epdconfig.delay_ms(20)
|
||||
logger.debug("e-Paper busy release")
|
||||
|
||||
def TurnOnDisplay(self):
|
||||
self.send_command(0x22) # Display Update Control
|
||||
self.send_data(0xF7)
|
||||
self.send_command(0x20) # Activate Display Update Sequence
|
||||
self.ReadBusy()
|
||||
|
||||
def TurnOnDisplay_Part(self):
|
||||
self.send_command(0x22) # Display Update Control
|
||||
self.send_data(0xCF)
|
||||
self.send_command(0x20) # Activate Display Update Sequence
|
||||
self.ReadBusy()
|
||||
|
||||
def TurnOnDisplay_4GRAY(self):
|
||||
self.send_command(0x22) # Display Update Control
|
||||
self.send_data(0xC7)
|
||||
self.send_command(0x20) # Activate Display Update Sequence
|
||||
self.ReadBusy()
|
||||
|
||||
def Lut(self, LUT):
|
||||
self.send_command(0x32)
|
||||
for i in range(105):
|
||||
self.send_data(LUT[i])
|
||||
|
||||
self.send_command(0x03)
|
||||
self.send_data(LUT[105])
|
||||
|
||||
self.send_command(0x04)
|
||||
self.send_data(LUT[106])
|
||||
self.send_data(LUT[107])
|
||||
self.send_data(LUT[108])
|
||||
|
||||
self.send_command(0x2C)
|
||||
self.send_data(LUT[109])
|
||||
|
||||
def init(self):
|
||||
|
||||
# EPD hardware init start
|
||||
self.reset()
|
||||
self.ReadBusy()
|
||||
|
||||
self.send_command(0x12) # SWRESET
|
||||
self.ReadBusy()
|
||||
|
||||
self.send_command(0x0C)
|
||||
self.send_data(0xAE)
|
||||
self.send_data(0xC7)
|
||||
self.send_data(0xC3)
|
||||
self.send_data(0xC0)
|
||||
self.send_data(0x80)
|
||||
|
||||
self.send_command(0x01)
|
||||
self.send_data(0xA7)
|
||||
self.send_data(0x02)
|
||||
self.send_data(0x00)
|
||||
|
||||
self.send_command(0x11)
|
||||
self.send_data(0x03)
|
||||
|
||||
self.send_command(0x44)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0xBF)
|
||||
self.send_data(0x03)
|
||||
|
||||
self.send_command(0x45)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0xA7)
|
||||
self.send_data(0x02)
|
||||
|
||||
self.send_command(0x3C)
|
||||
self.send_data(0x05)
|
||||
|
||||
self.send_command(0x18)
|
||||
self.send_data(0x80)
|
||||
|
||||
self.send_command(0x4E)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
|
||||
self.send_command(0x4F)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
|
||||
# EPD hardware init end
|
||||
return 0
|
||||
|
||||
def init_Part(self):
|
||||
self.reset()
|
||||
|
||||
self.send_command(0x3C)
|
||||
self.send_data(0x80)
|
||||
|
||||
self.Lut(self.Lut_Partial)
|
||||
|
||||
self.send_command(0x37)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x40)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
|
||||
self.send_command(0x3C)
|
||||
self.send_data(0x80)
|
||||
|
||||
self.send_command(0x22)
|
||||
self.send_data(0xC0)
|
||||
self.send_command(0x20)
|
||||
|
||||
self.ReadBusy()
|
||||
|
||||
def init_4GRAY(self):
|
||||
self.reset()
|
||||
|
||||
self.ReadBusy()
|
||||
self.send_command(0x12)
|
||||
self.ReadBusy()
|
||||
|
||||
self.send_command(0x0C)
|
||||
self.send_data(0xAE)
|
||||
self.send_data(0xC7)
|
||||
self.send_data(0xC3)
|
||||
self.send_data(0xC0)
|
||||
self.send_data(0x80)
|
||||
|
||||
self.send_command(0x01)
|
||||
self.send_data(0xA7)
|
||||
self.send_data(0x02)
|
||||
self.send_data(0x00)
|
||||
|
||||
self.send_command(0x11)
|
||||
self.send_data(0x03)
|
||||
|
||||
self.send_command(0x44)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0xBF)
|
||||
self.send_data(0x03)
|
||||
|
||||
self.send_command(0x45)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0xA7)
|
||||
self.send_data(0x02)
|
||||
|
||||
self.send_command(0x3C)
|
||||
self.send_data(0x00)
|
||||
|
||||
self.send_command(0x18)
|
||||
self.send_data(0x80)
|
||||
|
||||
self.send_command(0x4E)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
|
||||
self.send_command(0x4F)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
|
||||
self.Lut(self.LUT_DATA_4Gray)
|
||||
|
||||
self.ReadBusy()
|
||||
|
||||
def getbuffer(self, image):
|
||||
# logger.debug("bufsiz = ",int(self.width/8) * self.height)
|
||||
buf = [0xFF] * (int(self.width / 8) * self.height)
|
||||
image_monocolor = image.convert('1')
|
||||
imwidth, imheight = image_monocolor.size
|
||||
pixels = image_monocolor.load()
|
||||
# logger.debug("imwidth = %d, imheight = %d",imwidth,imheight)
|
||||
if imwidth == self.width and imheight == self.height:
|
||||
logger.debug("Horizontal")
|
||||
for y in range(imheight):
|
||||
for x in range(imwidth):
|
||||
# Set the bits for the column of pixels at the current position.
|
||||
if pixels[x, y] == 0:
|
||||
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
|
||||
elif imwidth == self.height and imheight == self.width:
|
||||
logger.debug("Vertical")
|
||||
for y in range(imheight):
|
||||
for x in range(imwidth):
|
||||
newx = y
|
||||
newy = self.height - x - 1
|
||||
if pixels[x, y] == 0:
|
||||
buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8))
|
||||
return buf
|
||||
|
||||
def getbuffer_4Gray(self, image):
|
||||
# logger.debug("bufsiz = ",int(self.width/8) * self.height)
|
||||
buf = [0xFF] * (int(self.width / 4) * self.height)
|
||||
image_monocolor = image.convert('L')
|
||||
imwidth, imheight = image_monocolor.size
|
||||
pixels = image_monocolor.load()
|
||||
i = 0
|
||||
# logger.debug("imwidth = %d, imheight = %d",imwidth,imheight)
|
||||
if (imwidth == self.width and imheight == self.height):
|
||||
logger.debug("Vertical")
|
||||
for y in range(imheight):
|
||||
for x in range(imwidth):
|
||||
# Set the bits for the column of pixels at the current position.
|
||||
if (pixels[x, y] == 0xC0):
|
||||
pixels[x, y] = 0x80
|
||||
elif (pixels[x, y] == 0x80):
|
||||
pixels[x, y] = 0x40
|
||||
i = i + 1
|
||||
if (i % 4 == 0):
|
||||
buf[int((x + (y * self.width)) / 4)] = (
|
||||
(pixels[x - 3, y] & 0xc0) | (pixels[x - 2, y] & 0xc0) >> 2 | (
|
||||
pixels[x - 1, y] & 0xc0) >> 4 | (pixels[x, y] & 0xc0) >> 6)
|
||||
|
||||
elif (imwidth == self.height and imheight == self.width):
|
||||
logger.debug("Horizontal")
|
||||
for x in range(imwidth):
|
||||
for y in range(imheight):
|
||||
newx = y
|
||||
newy = self.height - x - 1
|
||||
if (pixels[x, y] == 0xC0):
|
||||
pixels[x, y] = 0x80
|
||||
elif (pixels[x, y] == 0x80):
|
||||
pixels[x, y] = 0x40
|
||||
i = i + 1
|
||||
if (i % 4 == 0):
|
||||
buf[int((newx + (newy * self.width)) / 4)] = (
|
||||
(pixels[x, y - 3] & 0xc0) | (pixels[x, y - 2] & 0xc0) >> 2 | (
|
||||
pixels[x, y - 1] & 0xc0) >> 4 | (pixels[x, y] & 0xc0) >> 6)
|
||||
return buf
|
||||
|
||||
def Clear(self):
|
||||
buf = [0xFF] * (int(self.width / 8) * self.height)
|
||||
self.send_command(0x24)
|
||||
self.send_data2(buf)
|
||||
|
||||
self.TurnOnDisplay()
|
||||
|
||||
def display(self, image):
|
||||
self.send_command(0x24)
|
||||
self.send_data2(image)
|
||||
|
||||
self.TurnOnDisplay()
|
||||
|
||||
def display_Base(self, image):
|
||||
self.send_command(0x24)
|
||||
self.send_data2(image)
|
||||
|
||||
self.send_command(0x26)
|
||||
self.send_data2(image)
|
||||
|
||||
self.TurnOnDisplay()
|
||||
|
||||
def display_Base_color(self, color):
|
||||
if (self.width % 8 == 0):
|
||||
Width = self.width // 8
|
||||
else:
|
||||
Width = self.width // 8 + 1
|
||||
Height = self.height
|
||||
self.send_command(0x24) # Write Black and White image to RAM
|
||||
for j in range(Height):
|
||||
for i in range(Width):
|
||||
self.send_data(color)
|
||||
|
||||
self.send_command(0x26) # Write Black and White image to RAM
|
||||
for j in range(Height):
|
||||
for i in range(Width):
|
||||
self.send_data(color)
|
||||
# self.TurnOnDisplay()
|
||||
|
||||
def display_Partial(self, Image, Xstart, Ystart, Xend, Yend):
|
||||
if ((Xstart % 8 + Xend % 8 == 8 & Xstart % 8 > Xend % 8) | Xstart % 8 + Xend % 8 == 0 | (
|
||||
Xend - Xstart) % 8 == 0):
|
||||
Xstart = Xstart // 8
|
||||
Xend = Xend // 8
|
||||
else:
|
||||
Xstart = Xstart // 8
|
||||
if Xend % 8 == 0:
|
||||
Xend = Xend // 8
|
||||
else:
|
||||
Xend = Xend // 8 + 1
|
||||
|
||||
if (self.width % 8 == 0):
|
||||
Width = self.width // 8
|
||||
else:
|
||||
Width = self.width // 8 + 1
|
||||
Height = self.height
|
||||
|
||||
Xend -= 1
|
||||
Yend -= 1
|
||||
|
||||
self.send_command(0x44)
|
||||
self.send_data((Xstart * 8) & 0xff)
|
||||
self.send_data((Xstart >> 5) & 0x01)
|
||||
self.send_data((Xend * 8) & 0xff)
|
||||
self.send_data((Xend >> 5) & 0x01)
|
||||
self.send_command(0x45)
|
||||
self.send_data(Ystart & 0xff)
|
||||
self.send_data((Ystart >> 8) & 0x01)
|
||||
self.send_data(Yend & 0xff)
|
||||
self.send_data((Yend >> 8) & 0x01)
|
||||
|
||||
self.send_command(0x4E)
|
||||
self.send_data((Xstart * 8) & 0xff)
|
||||
self.send_data((Xstart >> 5) & 0x01)
|
||||
self.send_command(0x4F)
|
||||
self.send_data(Ystart & 0xff)
|
||||
self.send_data((Ystart >> 8) & 0x01)
|
||||
|
||||
self.send_command(0x24)
|
||||
for j in range(Height):
|
||||
for i in range(Width):
|
||||
if ((j > Ystart - 1) & (j < (Yend + 1)) & (i > Xstart - 1) & (i < (Xend + 1))):
|
||||
self.send_data(Image[i + j * Width])
|
||||
self.TurnOnDisplay_Part()
|
||||
|
||||
def display_4Gray(self, image):
|
||||
self.send_command(0x24)
|
||||
for i in range(0, 81600):
|
||||
temp3 = 0
|
||||
for j in range(0, 2):
|
||||
temp1 = image[i * 2 + j]
|
||||
for k in range(0, 2):
|
||||
temp2 = temp1 & 0xC0
|
||||
if (temp2 == 0xC0):
|
||||
temp3 |= 0x00
|
||||
elif (temp2 == 0x00):
|
||||
temp3 |= 0x01
|
||||
elif (temp2 == 0x80):
|
||||
temp3 |= 0x01
|
||||
else: # 0x40
|
||||
temp3 |= 0x00
|
||||
temp3 <<= 1
|
||||
|
||||
temp1 <<= 2
|
||||
temp2 = temp1 & 0xC0
|
||||
if (temp2 == 0xC0):
|
||||
temp3 |= 0x00
|
||||
elif (temp2 == 0x00):
|
||||
temp3 |= 0x01
|
||||
elif (temp2 == 0x80):
|
||||
temp3 |= 0x01
|
||||
else: # 0x40
|
||||
temp3 |= 0x00
|
||||
if (j != 1 or k != 1):
|
||||
temp3 <<= 1
|
||||
temp1 <<= 2
|
||||
self.send_data(temp3)
|
||||
|
||||
self.send_command(0x26)
|
||||
for i in range(0, 81600):
|
||||
temp3 = 0
|
||||
for j in range(0, 2):
|
||||
temp1 = image[i * 2 + j]
|
||||
for k in range(0, 2):
|
||||
temp2 = temp1 & 0xC0
|
||||
if (temp2 == 0xC0):
|
||||
temp3 |= 0x00
|
||||
elif (temp2 == 0x00):
|
||||
temp3 |= 0x01
|
||||
elif (temp2 == 0x80):
|
||||
temp3 |= 0x00
|
||||
else: # 0x40
|
||||
temp3 |= 0x01
|
||||
temp3 <<= 1
|
||||
|
||||
temp1 <<= 2
|
||||
temp2 = temp1 & 0xC0
|
||||
if (temp2 == 0xC0):
|
||||
temp3 |= 0x00
|
||||
elif (temp2 == 0x00):
|
||||
temp3 |= 0x01
|
||||
elif (temp2 == 0x80):
|
||||
temp3 |= 0x00
|
||||
else: # 0x40
|
||||
temp3 |= 0x01
|
||||
if (j != 1 or k != 1):
|
||||
temp3 <<= 1
|
||||
temp1 <<= 2
|
||||
self.send_data(temp3)
|
||||
|
||||
self.TurnOnDisplay_4GRAY()
|
||||
|
||||
def sleep(self):
|
||||
self.send_command(0x10) # DEEP_SLEEP
|
||||
self.send_data(0x03)
|
||||
|
||||
epdconfig.delay_ms(2000)
|
||||
epdconfig.module_exit()
|
299
inkycal/display/drivers/epd_13_in_3_colour.py
Normal file
299
inkycal/display/drivers/epd_13_in_3_colour.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
* | File : epd13in3b.py
|
||||
* | Author : Waveshare team
|
||||
* | Function : Electronic paper driver
|
||||
* | Info :
|
||||
*----------------
|
||||
* | This version: V1.0
|
||||
* | Date : 2024-04-08
|
||||
# | Info : python demo
|
||||
-----------------------------------------------------------------------------
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from inkycal.display.drivers import epdconfig
|
||||
|
||||
# Display resolution
|
||||
EPD_WIDTH = 960
|
||||
EPD_HEIGHT = 680
|
||||
|
||||
GRAY1 = 0xff # white
|
||||
GRAY2 = 0xC0
|
||||
GRAY3 = 0x80 # gray
|
||||
GRAY4 = 0x00 # Blackest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EPD:
|
||||
def __init__(self):
|
||||
self.reset_pin = epdconfig.RST_PIN
|
||||
self.dc_pin = epdconfig.DC_PIN
|
||||
self.busy_pin = epdconfig.BUSY_PIN
|
||||
self.cs_pin = epdconfig.CS_PIN
|
||||
self.width = EPD_WIDTH
|
||||
self.height = EPD_HEIGHT
|
||||
if (epdconfig.module_init() != 0):
|
||||
return -1
|
||||
|
||||
# Hardware reset
|
||||
def reset(self):
|
||||
epdconfig.digital_write(self.reset_pin, 1)
|
||||
epdconfig.delay_ms(20)
|
||||
epdconfig.digital_write(self.reset_pin, 0)
|
||||
epdconfig.delay_ms(2)
|
||||
epdconfig.digital_write(self.reset_pin, 1)
|
||||
epdconfig.delay_ms(20)
|
||||
|
||||
def send_command(self, command):
|
||||
epdconfig.digital_write(self.dc_pin, 0)
|
||||
epdconfig.digital_write(self.cs_pin, 0)
|
||||
epdconfig.spi_writebyte([command])
|
||||
epdconfig.digital_write(self.cs_pin, 1)
|
||||
|
||||
def send_data(self, data):
|
||||
epdconfig.digital_write(self.dc_pin, 1)
|
||||
epdconfig.digital_write(self.cs_pin, 0)
|
||||
epdconfig.spi_writebyte([data])
|
||||
epdconfig.digital_write(self.cs_pin, 1)
|
||||
|
||||
def send_data2(self, data):
|
||||
epdconfig.digital_write(self.dc_pin, 1)
|
||||
epdconfig.digital_write(self.cs_pin, 0)
|
||||
epdconfig.SPI.writebytes2(data)
|
||||
epdconfig.digital_write(self.cs_pin, 1)
|
||||
|
||||
def ReadBusy(self):
|
||||
logger.debug("e-Paper busy")
|
||||
busy = epdconfig.digital_read(self.busy_pin)
|
||||
while (busy == 1):
|
||||
busy = epdconfig.digital_read(self.busy_pin)
|
||||
epdconfig.delay_ms(20)
|
||||
epdconfig.delay_ms(20)
|
||||
logger.debug("e-Paper busy release")
|
||||
|
||||
def TurnOnDisplay(self):
|
||||
self.send_command(0x22) # Display Update Control
|
||||
self.send_data(0xF7)
|
||||
self.send_command(0x20) # Activate Display Update Sequence
|
||||
self.ReadBusy()
|
||||
|
||||
def TurnOnDisplay_Part(self):
|
||||
self.send_command(0x22) # Display Update Control
|
||||
self.send_data(0xFF)
|
||||
self.send_command(0x20) # Activate Display Update Sequence
|
||||
self.ReadBusy()
|
||||
|
||||
def init(self):
|
||||
# EPD hardware init start
|
||||
self.reset()
|
||||
self.ReadBusy()
|
||||
|
||||
self.send_command(0x12) # SWRESET
|
||||
self.ReadBusy()
|
||||
|
||||
self.send_command(0x0C)
|
||||
self.send_data(0xAE)
|
||||
self.send_data(0xC7)
|
||||
self.send_data(0xC3)
|
||||
self.send_data(0xC0)
|
||||
self.send_data(0x80)
|
||||
|
||||
self.send_command(0x01)
|
||||
self.send_data(0xA7)
|
||||
self.send_data(0x02)
|
||||
self.send_data(0x00)
|
||||
|
||||
self.send_command(0x11)
|
||||
self.send_data(0x03)
|
||||
|
||||
self.send_command(0x44)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0xBF)
|
||||
self.send_data(0x03)
|
||||
|
||||
self.send_command(0x45)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0xA7)
|
||||
self.send_data(0x02)
|
||||
|
||||
self.send_command(0x3C)
|
||||
self.send_data(0x01)
|
||||
|
||||
self.send_command(0x18)
|
||||
self.send_data(0x80)
|
||||
|
||||
self.send_command(0x4E)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
|
||||
self.send_command(0x4F)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.ReadBusy()
|
||||
|
||||
# EPD hardware init end
|
||||
return 0
|
||||
|
||||
def getbuffer(self, image):
|
||||
# logger.debug("bufsiz = ",int(self.width/8) * self.height)
|
||||
buf = [0xFF] * (int(self.width / 8) * self.height)
|
||||
image_monocolor = image.convert('1')
|
||||
imwidth, imheight = image_monocolor.size
|
||||
pixels = image_monocolor.load()
|
||||
# logger.debug("imwidth = %d, imheight = %d",imwidth,imheight)
|
||||
if imwidth == self.width and imheight == self.height:
|
||||
logger.debug("Horizontal")
|
||||
for y in range(imheight):
|
||||
for x in range(imwidth):
|
||||
# Set the bits for the column of pixels at the current position.
|
||||
if pixels[x, y] == 0:
|
||||
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
|
||||
elif imwidth == self.height and imheight == self.width:
|
||||
logger.debug("Vertical")
|
||||
for y in range(imheight):
|
||||
for x in range(imwidth):
|
||||
newx = y
|
||||
newy = self.height - x - 1
|
||||
if pixels[x, y] == 0:
|
||||
buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8))
|
||||
return buf
|
||||
|
||||
def Clear(self):
|
||||
self.send_command(0x24)
|
||||
self.send_data2([0xFF] * (int(self.width / 8) * self.height))
|
||||
self.send_command(0x26)
|
||||
self.send_data2([0x00] * (int(self.width / 8) * self.height))
|
||||
|
||||
self.TurnOnDisplay()
|
||||
|
||||
def Clear_Base(self):
|
||||
self.send_command(0x24)
|
||||
self.send_data2([0xFF] * (int(self.width / 8) * self.height))
|
||||
self.send_command(0x26)
|
||||
self.send_data2([0x00] * (int(self.width / 8) * self.height))
|
||||
|
||||
self.TurnOnDisplay()
|
||||
self.send_command(0x26)
|
||||
self.send_data2([0xFF] * (int(self.width / 8) * self.height))
|
||||
|
||||
def display(self, blackimage, ryimage):
|
||||
if (self.width % 8 == 0):
|
||||
Width = self.width // 8
|
||||
else:
|
||||
Width = self.width // 8 + 1
|
||||
Height = self.height
|
||||
if (blackimage != None):
|
||||
self.send_command(0x24)
|
||||
self.send_data2(blackimage)
|
||||
if (ryimage != None):
|
||||
for j in range(Height):
|
||||
for i in range(Width):
|
||||
ryimage[i + j * Width] = ~ryimage[i + j * Width]
|
||||
self.send_command(0x26)
|
||||
self.send_data2(ryimage)
|
||||
|
||||
self.TurnOnDisplay()
|
||||
|
||||
def display_Base(self, blackimage, ryimage):
|
||||
if (self.width % 8 == 0):
|
||||
Width = self.width // 8
|
||||
else:
|
||||
Width = self.width // 8 + 1
|
||||
Height = self.height
|
||||
if (blackimage != None):
|
||||
self.send_command(0x24)
|
||||
self.send_data2(blackimage)
|
||||
if (ryimage != None):
|
||||
for j in range(Height):
|
||||
for i in range(Width):
|
||||
ryimage[i + j * Width] = ~ryimage[i + j * Width]
|
||||
self.send_command(0x26)
|
||||
self.send_data2(ryimage)
|
||||
|
||||
self.TurnOnDisplay()
|
||||
|
||||
self.send_command(0x26)
|
||||
self.send_data2(blackimage)
|
||||
|
||||
def display_Partial(self, Image, Xstart, Ystart, Xend, Yend):
|
||||
if ((Xstart % 8 + Xend % 8 == 8 & Xstart % 8 > Xend % 8) | Xstart % 8 + Xend % 8 == 0 | (
|
||||
Xend - Xstart) % 8 == 0):
|
||||
Xstart = Xstart // 8
|
||||
Xend = Xend // 8
|
||||
else:
|
||||
Xstart = Xstart // 8
|
||||
if Xend % 8 == 0:
|
||||
Xend = Xend // 8
|
||||
else:
|
||||
Xend = Xend // 8 + 1
|
||||
|
||||
if (self.width % 8 == 0):
|
||||
Width = self.width // 8
|
||||
else:
|
||||
Width = self.width // 8 + 1
|
||||
Height = self.height
|
||||
|
||||
Xend -= 1
|
||||
Yend -= 1
|
||||
|
||||
self.send_command(0x3C)
|
||||
self.send_data(0x80)
|
||||
|
||||
self.send_command(0x44)
|
||||
self.send_data((Xstart * 8) & 0xff)
|
||||
self.send_data((Xstart >> 5) & 0x01)
|
||||
self.send_data((Xend * 8) & 0xff)
|
||||
self.send_data((Xend >> 5) & 0x01)
|
||||
self.send_command(0x45)
|
||||
self.send_data(Ystart & 0xff)
|
||||
self.send_data((Ystart >> 8) & 0x01)
|
||||
self.send_data(Yend & 0xff)
|
||||
self.send_data((Yend >> 8) & 0x01)
|
||||
|
||||
self.send_command(0x4E)
|
||||
self.send_data((Xstart * 8) & 0xff)
|
||||
self.send_data((Xstart >> 5) & 0x01)
|
||||
self.send_command(0x4F)
|
||||
self.send_data(Ystart & 0xff)
|
||||
self.send_data((Ystart >> 8) & 0x01)
|
||||
|
||||
self.send_command(0x24)
|
||||
for j in range(Height):
|
||||
for i in range(Width):
|
||||
if ((j > Ystart - 1) & (j < (Yend + 1)) & (i > Xstart - 1) & (i < (Xend + 1))):
|
||||
self.send_data(Image[i + j * Width])
|
||||
self.TurnOnDisplay_Part()
|
||||
|
||||
self.send_command(0x26)
|
||||
for j in range(Height):
|
||||
for i in range(Width):
|
||||
if ((j > Ystart - 1) & (j < (Yend + 1)) & (i > Xstart - 1) & (i < (Xend + 1))):
|
||||
self.send_data(Image[i + j * Width])
|
||||
|
||||
def sleep(self):
|
||||
self.send_command(0x10) # DEEP_SLEEP
|
||||
self.send_data(0x03)
|
||||
|
||||
epdconfig.delay_ms(2000)
|
||||
epdconfig.module_exit()
|
@@ -28,8 +28,6 @@ THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
@@ -128,4 +126,3 @@ implementation = RaspberryPi()
|
||||
|
||||
for func in [x for x in dir(implementation) if not x.startswith('_')]:
|
||||
setattr(sys.modules[__name__], func, getattr(implementation, func))
|
||||
|
||||
|
@@ -1,4 +1,6 @@
|
||||
supported_models = {
|
||||
"epd_13_in_3": (960, 680),
|
||||
"epd_13_in_3_colour": (960, 680),
|
||||
"epd_12_in_48": (1304, 984),
|
||||
"epd_7_in_5_colour": (640, 384),
|
||||
"9_in_7": (1200, 825),
|
||||
|
35
inkycal/loggers.py
Normal file
35
inkycal/loggers.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Logging configuration for Inkycal."""
|
||||
import logging
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
from inkycal.settings import Settings
|
||||
|
||||
# On the console, set a logger to show only important logs
|
||||
# (level ERROR or higher)
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(logging.INFO)
|
||||
|
||||
settings = Settings()
|
||||
|
||||
if not os.path.exists(settings.LOG_PATH):
|
||||
os.mkdir(settings.LOG_PATH)
|
||||
|
||||
|
||||
# Save all logs to a file, which contains more detailed output
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s | %(name)s | %(levelname)s: %(message)s',
|
||||
datefmt='%d-%m-%Y %H:%M:%S',
|
||||
handlers=[
|
||||
stream_handler, # add stream handler from above
|
||||
RotatingFileHandler( # log to a file too
|
||||
settings.INKYCAL_LOG_PATH, # file to log
|
||||
maxBytes=2*1024*1024, # 2MB max filesize
|
||||
backupCount=5 # create max 5 log files
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# Show less logging for PIL module
|
||||
logging.getLogger("PIL").setLevel(logging.WARNING)
|
368
inkycal/main.py
368
inkycal/main.py
@@ -6,44 +6,22 @@ Copyright by aceinnolab
|
||||
import asyncio
|
||||
import glob
|
||||
import hashlib
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os.path
|
||||
|
||||
import numpy
|
||||
|
||||
from inkycal import loggers # noqa
|
||||
from inkycal.custom import *
|
||||
from inkycal.display import Display
|
||||
from inkycal.modules.inky_image import Inkyimage as Images
|
||||
|
||||
# On the console, set a logger to show only important logs
|
||||
# (level ERROR or higher)
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(logging.ERROR)
|
||||
|
||||
if not os.path.exists(f'{top_level}/logs'):
|
||||
os.mkdir(f'{top_level}/logs')
|
||||
|
||||
# Save all logs to a file, which contains more detailed output
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s | %(name)s | %(levelname)s: %(message)s',
|
||||
datefmt='%d-%m-%Y %H:%M:%S',
|
||||
handlers=[
|
||||
stream_handler, # add stream handler from above
|
||||
RotatingFileHandler( # log to a file too
|
||||
f'{top_level}/logs/inkycal.log', # file to log
|
||||
maxBytes=2097152, # 2MB max filesize
|
||||
backupCount=5 # create max 5 log files
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# Show less logging for PIL module
|
||||
logging.getLogger("PIL").setLevel(logging.WARNING)
|
||||
from inkycal.utils import JSONCache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
settings = Settings()
|
||||
|
||||
CACHE_NAME = "inkycal_main"
|
||||
|
||||
# TODO: autostart -> supervisor?
|
||||
|
||||
class Inkycal:
|
||||
"""Inkycal main class
|
||||
@@ -60,43 +38,61 @@ class Inkycal:
|
||||
to improve rendering on E-Papers. Set this to False for 9.7" E-Paper.
|
||||
"""
|
||||
|
||||
def __init__(self, settings_path: str or None = None, render: bool = True):
|
||||
"""Initialise Inkycal"""
|
||||
def __init__(self, settings_path: str or None = None, render: bool = True, use_pi_sugar: bool = False,
|
||||
shutdown_after_run: bool = False) -> None:
|
||||
"""Initialise Inkycal
|
||||
|
||||
# Get the release version from setup.py
|
||||
with open(f'{top_level}/setup.py') as setup_file:
|
||||
for line in setup_file:
|
||||
if line.startswith('__version__'):
|
||||
self._release = line.split("=")[-1].replace("'", "").replace('"', "").replace(" ", "")
|
||||
break
|
||||
Args:
|
||||
settings_path (str):
|
||||
The full path to your settings.json file. If no path was specified, will look in the /boot directory.
|
||||
render (bool):
|
||||
Show the image on the E-Paper display.
|
||||
use_pi_sugar (bool):
|
||||
Use PiSugar board (all revisions). Default is False.
|
||||
shutdown_after_run (bool):
|
||||
Shutdown the system after the run is complete. Will only work with PiSugar enabled.
|
||||
|
||||
"""
|
||||
self._release = "2.0.4"
|
||||
|
||||
logger.info(f"Inkycal v{self._release} booting up...")
|
||||
|
||||
self.render = render
|
||||
self.info = None
|
||||
|
||||
logger.info("Checking if a settings file is present...")
|
||||
# load settings file - throw an error if file could not be found
|
||||
if settings_path:
|
||||
logger.info(f"Custom location for settings.json file specified: {settings_path}")
|
||||
try:
|
||||
with open(settings_path) as settings_file:
|
||||
settings = json.load(settings_file)
|
||||
self.settings = settings
|
||||
with open(settings_path, mode="r") as settings_file:
|
||||
self.settings = json.load(settings_file)
|
||||
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(
|
||||
f"No settings.json file could be found in the specified location: {settings_path}")
|
||||
|
||||
else:
|
||||
try:
|
||||
with open('/boot/settings.json') as settings_file:
|
||||
settings = json.load(settings_file)
|
||||
self.settings = settings
|
||||
|
||||
except FileNotFoundError:
|
||||
raise SettingsFileNotFoundError
|
||||
found = False
|
||||
for location in settings.SETTINGS_JSON_PATHS:
|
||||
if os.path.exists(location):
|
||||
logger.info(f"Found settings.json file in {location}")
|
||||
with open(location, mode="r") as settings_file:
|
||||
self.settings = json.load(settings_file)
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
raise SettingsFileNotFoundError(f"No settings.json file could be found in {settings.SETTINGS_JSON_PATHS} and no explicit path was specified.")
|
||||
|
||||
self.disable_calibration = self.settings.get('disable_calibration', False)
|
||||
if self.disable_calibration:
|
||||
logger.info("Calibration disabled. Please proceed with caution to prevent ghosting.")
|
||||
|
||||
if not os.path.exists(image_folder):
|
||||
os.mkdir(image_folder)
|
||||
if not os.path.exists(settings.IMAGE_FOLDER):
|
||||
os.mkdir(settings.IMAGE_FOLDER)
|
||||
|
||||
if not os.path.exists(settings.CACHE_PATH):
|
||||
os.mkdir(settings.CACHE_PATH)
|
||||
|
||||
# Option to use epaper image optimisation, reduces colours
|
||||
self.optimize = True
|
||||
@@ -109,10 +105,10 @@ class Inkycal:
|
||||
if self.render:
|
||||
# Init Display class with model in settings file
|
||||
# from inkycal.display import Display
|
||||
self.Display = Display(settings["model"])
|
||||
self.Display = Display(self.settings["model"])
|
||||
|
||||
# check if colours can be rendered
|
||||
self.supports_colour = True if 'colour' in settings['model'] else False
|
||||
self.supports_colour = True if 'colour' in self.settings['model'] else False
|
||||
|
||||
# get calibration hours
|
||||
self._calibration_hours = self.settings['calibration_hours']
|
||||
@@ -122,7 +118,7 @@ class Inkycal:
|
||||
|
||||
# Load and initialise modules specified in the settings file
|
||||
self._module_number = 1
|
||||
for module in settings['modules']:
|
||||
for module in self.settings['modules']:
|
||||
module_name = module['name']
|
||||
try:
|
||||
loader = f'from inkycal.modules import {module_name}'
|
||||
@@ -131,10 +127,9 @@ class Inkycal:
|
||||
setup = f'self.module_{self._module_number} = {module_name}({module})'
|
||||
# print(setup)
|
||||
exec(setup)
|
||||
logger.info(('name : {name} size : {width}x{height} px'.format(
|
||||
name=module_name,
|
||||
width=module['config']['size'][0],
|
||||
height=module['config']['size'][1])))
|
||||
width = module['config']['size'][0]
|
||||
height = module['config']['size'][1]
|
||||
logger.info(f'name : {module_name} size : {width}x{height} px')
|
||||
|
||||
self._module_number += 1
|
||||
|
||||
@@ -146,58 +141,85 @@ class Inkycal:
|
||||
except:
|
||||
logger.exception(f"Exception: {traceback.format_exc()}.")
|
||||
|
||||
# Path to store images
|
||||
self.image_folder = image_folder
|
||||
|
||||
# Remove old hashes
|
||||
self._remove_hashes(self.image_folder)
|
||||
self._remove_hashes(settings.IMAGE_FOLDER)
|
||||
|
||||
# set up cache
|
||||
if not os.path.exists(os.path.join(settings.CACHE_PATH, CACHE_NAME)):
|
||||
if not os.path.exists(settings.CACHE_PATH):
|
||||
os.mkdir(settings.CACHE_PATH)
|
||||
self.cache = JSONCache(CACHE_NAME)
|
||||
self.cache_data = self.cache.read()
|
||||
|
||||
self.counter = 0 if "counter" not in self.cache_data else int(self.cache_data["counter"])
|
||||
|
||||
self.use_pi_sugar = use_pi_sugar
|
||||
self.battery_capacity = 100
|
||||
self.shutdown_after_run = use_pi_sugar and shutdown_after_run
|
||||
|
||||
if self.use_pi_sugar:
|
||||
logger.info("PiSugar support enabled.")
|
||||
from inkycal.utils import PiSugar
|
||||
self.pisugar = PiSugar()
|
||||
|
||||
self.battery_capacity = self.pisugar.get_battery()
|
||||
logger.info(f"PiSugar battery capacity: {self.battery_capacity}%")
|
||||
|
||||
if self.battery_capacity < 20:
|
||||
logger.warning("Battery capacity is below 20%!")
|
||||
|
||||
logger.info("Setting system time to PiSugar time...")
|
||||
if self.pisugar.rtc_pi2rtc():
|
||||
logger.info("RTC time updates successfully")
|
||||
else:
|
||||
logger.warning("RTC time could not be set!")
|
||||
|
||||
print(
|
||||
f"Using PiSigar model: {self.pisugar.get_model()}. Current PiSugar time: {self.pisugar.get_rtc_time()}")
|
||||
|
||||
if self.shutdown_after_run:
|
||||
logger.warning("Shutdown after run enabled. System will shutdown after the run is complete.")
|
||||
|
||||
# Give an OK message
|
||||
print('loaded inkycal')
|
||||
logger.info('Inkycal initialised successfully!')
|
||||
|
||||
def countdown(self, interval_mins: int or None = None) -> int:
|
||||
"""Returns the remaining time in seconds until next display update.
|
||||
def countdown(self, interval_mins: int = None) -> int:
|
||||
"""Returns the remaining time in seconds until the next display update based on the interval.
|
||||
|
||||
Args:
|
||||
- interval_mins = int -> the interval in minutes for the update
|
||||
if no interval is given, the value from the settings file is used.
|
||||
interval_mins (int): The interval in minutes for the update. If none is given, the value
|
||||
from the settings file is used.
|
||||
|
||||
Returns:
|
||||
- int -> the remaining time in seconds until next update
|
||||
int: The remaining time in seconds until the next update.
|
||||
"""
|
||||
|
||||
# Check if empty, if empty, use value from settings file
|
||||
# Default to settings if no interval is provided
|
||||
if interval_mins is None:
|
||||
interval_mins = self.settings["update_interval"]
|
||||
|
||||
# Find out at which minutes the update should happen
|
||||
# Get the current time
|
||||
now = arrow.now()
|
||||
if interval_mins <= 60:
|
||||
update_timings = [(60 - interval_mins * updates) for updates in range(60 // interval_mins)][::-1]
|
||||
|
||||
# Calculate time in minutes until next update
|
||||
minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute
|
||||
# Calculate the next update time
|
||||
# Finding the total minutes from the start of the day
|
||||
minutes_since_midnight = now.hour * 60 + now.minute
|
||||
|
||||
# Print the remaining time in minutes until next update
|
||||
print(f'{minutes} minutes left until next refresh')
|
||||
# Finding the next interval point
|
||||
minutes_to_next_interval = (
|
||||
minutes_since_midnight // interval_mins + 1) * interval_mins - minutes_since_midnight
|
||||
seconds_to_next_interval = minutes_to_next_interval * 60 - now.second
|
||||
|
||||
# Calculate time in seconds until next update
|
||||
remaining_time = minutes * 60 + (60 - now.second)
|
||||
|
||||
# Return seconds until next update
|
||||
return remaining_time
|
||||
# Logging the remaining time in appropriate units
|
||||
hours_to_next_interval = minutes_to_next_interval // 60
|
||||
remaining_minutes = minutes_to_next_interval % 60
|
||||
if hours_to_next_interval > 0:
|
||||
print(f'{hours_to_next_interval} hours and {remaining_minutes} minutes left until next refresh')
|
||||
else:
|
||||
# Calculate time in minutes until next update using the range of 24 hours in steps of every full hour
|
||||
update_timings = [(60 * 24 - interval_mins * updates) for updates in range(60 * 24 // interval_mins)][::-1]
|
||||
minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute
|
||||
remaining_time = minutes * 60 + (60 - now.second)
|
||||
print(f'{remaining_minutes} minutes left until next refresh')
|
||||
|
||||
print(f'{round(minutes / 60, 1)} hours left until next refresh')
|
||||
return seconds_to_next_interval
|
||||
|
||||
# Return seconds until next update
|
||||
return remaining_time
|
||||
|
||||
def test(self):
|
||||
def dry_run(self):
|
||||
"""Tests if Inkycal can run without issues.
|
||||
|
||||
Attempts to import module names from settings file. Loads the config
|
||||
@@ -206,8 +228,6 @@ class Inkycal:
|
||||
|
||||
Generated images can be found in the /images folder of Inkycal.
|
||||
"""
|
||||
|
||||
logger.info(f"Inkycal version: v{self._release}")
|
||||
logger.info(f'Selected E-paper display: {self.settings["model"]}')
|
||||
|
||||
# store module numbers in here
|
||||
@@ -218,20 +238,13 @@ class Inkycal:
|
||||
|
||||
for number in range(1, self._module_number):
|
||||
name = eval(f"self.module_{number}.name")
|
||||
module = eval(f'self.module_{number}')
|
||||
print(f'generating image(s) for {name}...', end="")
|
||||
try:
|
||||
black, colour = module.generate_image()
|
||||
if self.show_border:
|
||||
draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5)
|
||||
black.save(f"{self.image_folder}module{number}_black.png", "PNG")
|
||||
colour.save(f"{self.image_folder}module{number}_colour.png", "PNG")
|
||||
print("OK!")
|
||||
except Exception:
|
||||
success = self.process_module(number)
|
||||
if success:
|
||||
logger.debug(f'Image of module {name} generated successfully')
|
||||
else:
|
||||
logger.warning(f'Generating image of module {name} failed!')
|
||||
errors.append(number)
|
||||
self.info += f"module {number}: Error! "
|
||||
logger.exception("Error!")
|
||||
logger.exception(f"Exception: {traceback.format_exc()}.")
|
||||
|
||||
if errors:
|
||||
logger.error('Error/s in modules:', *errors)
|
||||
@@ -277,98 +290,89 @@ class Inkycal:
|
||||
print("Refresh needed: {a}".format(a=res))
|
||||
return res
|
||||
|
||||
async def run(self):
|
||||
"""Runs main program in nonstop mode.
|
||||
async def run(self, run_once=False):
|
||||
"""Runs main program in nonstop mode or a single iteration based on the run_once flag.
|
||||
|
||||
Uses an infinity loop to run Inkycal nonstop. Inkycal generates the image
|
||||
from all modules, assembles them in one image, refreshed the E-Paper and
|
||||
then sleeps until the next scheduled update.
|
||||
Args:
|
||||
run_once (bool): If True, runs the updating process once and stops. If False,
|
||||
runs indefinitely.
|
||||
|
||||
Uses an infinity loop to run Inkycal nonstop or a single time based on run_once.
|
||||
Inkycal generates the image from all modules, assembles them in one image,
|
||||
refreshes the E-Paper and then sleeps until the next scheduled update or exits.
|
||||
"""
|
||||
|
||||
# Get the time of initial run
|
||||
runtime = arrow.now()
|
||||
|
||||
# Function to flip images upside down
|
||||
upside_down = lambda image: image.rotate(180, expand=True)
|
||||
|
||||
# Count the number of times without any errors
|
||||
counter = 0
|
||||
|
||||
print(f'Inkycal version: v{self._release}')
|
||||
print(f'Selected E-paper display: {self.settings["model"]}')
|
||||
logger.info(f'Inkycal version: v{self._release}')
|
||||
logger.info(f'Selected E-paper display: {self.settings["model"]}')
|
||||
|
||||
while True:
|
||||
logger.info("Starting new cycle...")
|
||||
current_time = arrow.now(tz=get_system_tz())
|
||||
print(f"Date: {current_time.format('D MMM YY')} | "
|
||||
f"Time: {current_time.format('HH:mm')}")
|
||||
print('Generating images for all modules...', end='')
|
||||
logger.info(f"Timestamp: {current_time.format('HH:mm:ss DD.MM.YYYY')}")
|
||||
self.cache_data["counter"] = self.counter
|
||||
|
||||
errors = [] # store module numbers in here
|
||||
errors = [] # Store module numbers in here
|
||||
|
||||
# short info for info-section
|
||||
# Short info for info-section
|
||||
if not self.settings.get('image_hash', False):
|
||||
self.info = f"{current_time.format('D MMM @ HH:mm')} "
|
||||
else:
|
||||
self.info = ""
|
||||
|
||||
for number in range(1, self._module_number):
|
||||
|
||||
# name = eval(f"self.module_{number}.name")
|
||||
module = eval(f'self.module_{number}')
|
||||
|
||||
try:
|
||||
black, colour = module.generate_image()
|
||||
if self.show_border:
|
||||
draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5)
|
||||
black.save(f"{self.image_folder}module{number}_black.png", "PNG")
|
||||
colour.save(f"{self.image_folder}module{number}_colour.png", "PNG")
|
||||
self.info += f"module {number}: OK "
|
||||
except Exception as e:
|
||||
success = self.process_module(number)
|
||||
if not success:
|
||||
errors.append(number)
|
||||
self.info += f"module {number}: Error! "
|
||||
logger.exception("Error!")
|
||||
logger.exception(f"Exception: {traceback.format_exc()}.")
|
||||
self.info += f"im {number}: X "
|
||||
|
||||
if errors:
|
||||
logger.error("Error/s in modules:", *errors)
|
||||
counter = 0
|
||||
self.counter = 0
|
||||
self.cache_data["counter"] = 0
|
||||
else:
|
||||
counter += 1
|
||||
logger.info("successful")
|
||||
self.counter += 1
|
||||
self.cache_data["counter"] += 1
|
||||
logger.info("All images generated successfully!")
|
||||
del errors
|
||||
|
||||
if self.battery_capacity < 20:
|
||||
self.info += "Low battery! "
|
||||
|
||||
# Assemble image from each module - add info section if specified
|
||||
self._assemble()
|
||||
|
||||
# Check if image should be rendered
|
||||
if self.render:
|
||||
logger.info("Attempting to render image on display...")
|
||||
display = self.Display
|
||||
|
||||
self._calibration_check()
|
||||
if self._calibration_state:
|
||||
# after calibration, we have to forcefully rewrite the screen
|
||||
self._remove_hashes(self.image_folder)
|
||||
# After calibration, we have to forcefully rewrite the screen
|
||||
self._remove_hashes(settings.IMAGE_FOLDER)
|
||||
|
||||
if self.supports_colour:
|
||||
im_black = Image.open(f"{self.image_folder}canvas.png")
|
||||
im_colour = Image.open(f"{self.image_folder}canvas_colour.png")
|
||||
im_black = Image.open(os.path.join(settings.IMAGE_FOLDER, "canvas.png"))
|
||||
im_colour = Image.open(os.path.join(settings.IMAGE_FOLDER, "canvas_colour.png"))
|
||||
|
||||
# Flip the image by 180° if required
|
||||
if self.settings['orientation'] == 180:
|
||||
im_black = upside_down(im_black)
|
||||
im_colour = upside_down(im_colour)
|
||||
|
||||
# render the image on the display
|
||||
# Render the image on the display
|
||||
if not self.settings.get('image_hash', False) or self._needs_image_update([
|
||||
(f"{self.image_folder}/canvas.png.hash", im_black),
|
||||
(f"{self.image_folder}/canvas_colour.png.hash", im_colour)
|
||||
(f"{settings.IMAGE_FOLDER}/canvas.png.hash", im_black),
|
||||
(f"{settings.IMAGE_FOLDER}/canvas_colour.png.hash", im_colour)
|
||||
]):
|
||||
# render the image on the display
|
||||
display.render(im_black, im_colour)
|
||||
|
||||
# Part for black-white ePapers
|
||||
elif not self.supports_colour:
|
||||
|
||||
else:
|
||||
im_black = self._merge_bands()
|
||||
|
||||
# Flip the image by 180° if required
|
||||
@@ -376,14 +380,34 @@ class Inkycal:
|
||||
im_black = upside_down(im_black)
|
||||
|
||||
if not self.settings.get('image_hash', False) or self._needs_image_update([
|
||||
(f"{self.image_folder}/canvas.png.hash", im_black),
|
||||
]):
|
||||
(f"{settings.IMAGE_FOLDER}/canvas.png.hash", im_black), ]):
|
||||
display.render(im_black)
|
||||
|
||||
print(f'\nNo errors since {counter} display updates \n'
|
||||
f'program started {runtime.humanize()}')
|
||||
logger.info(f'No errors since {self.counter} display updates')
|
||||
logger.info(f'program started {runtime.humanize()}')
|
||||
|
||||
# store the cache data
|
||||
self.cache.write(self.cache_data)
|
||||
|
||||
# Exit the loop if run_once is True
|
||||
if run_once:
|
||||
break # Exit the loop after one full cycle if run_once is True
|
||||
|
||||
sleep_time = self.countdown()
|
||||
|
||||
if self.use_pi_sugar:
|
||||
sleep_time_rtc = arrow.now(tz=get_system_tz()).shift(seconds=sleep_time)
|
||||
result = self.pisugar.rtc_alarm_set(sleep_time_rtc, 127)
|
||||
if result:
|
||||
logger.info(f"Alarm set for {sleep_time_rtc.format('HH:mm:ss')}")
|
||||
if self.shutdown_after_run:
|
||||
logger.warning("System shutdown in 5 seconds!")
|
||||
time.sleep(5)
|
||||
self._shutdown_system()
|
||||
break
|
||||
else:
|
||||
logger.warning(f"Failed to set alarm for {sleep_time_rtc.format('HH:mm:ss')}")
|
||||
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
@staticmethod
|
||||
@@ -392,7 +416,8 @@ class Inkycal:
|
||||
returns the merged image
|
||||
"""
|
||||
|
||||
im1_path, im2_path = image_folder + 'canvas.png', image_folder + 'canvas_colour.png'
|
||||
im1_path = os.path.join(settings.IMAGE_FOLDER, "canvas.png")
|
||||
im2_path = os.path.join(settings.IMAGE_FOLDER, "canvas_colour.png")
|
||||
|
||||
# If there is an image for black and colour, merge them
|
||||
if os.path.exists(im1_path) and os.path.exists(im2_path):
|
||||
@@ -430,8 +455,8 @@ class Inkycal:
|
||||
for number in range(1, self._module_number):
|
||||
|
||||
# get the path of the current module's generated images
|
||||
im1_path = f"{self.image_folder}module{number}_black.png"
|
||||
im2_path = f"{self.image_folder}module{number}_colour.png"
|
||||
im1_path = os.path.join(settings.IMAGE_FOLDER, f"module{number}_black.png")
|
||||
im2_path = os.path.join(settings.IMAGE_FOLDER, f"module{number}_colour.png")
|
||||
|
||||
# Check if there is an image for the black band
|
||||
if os.path.exists(im1_path):
|
||||
@@ -501,8 +526,8 @@ class Inkycal:
|
||||
im_black = self._optimize_im(im_black)
|
||||
im_colour = self._optimize_im(im_colour)
|
||||
|
||||
im_black.save(self.image_folder + 'canvas.png', 'PNG')
|
||||
im_colour.save(self.image_folder + 'canvas_colour.png', 'PNG')
|
||||
im_black.save(os.path.join(settings.IMAGE_FOLDER, "canvas.png"), "PNG")
|
||||
im_colour.save(os.path.join(settings.IMAGE_FOLDER, "canvas_colour.png"), 'PNG')
|
||||
|
||||
# Additionally, combine the two images with color
|
||||
def clear_white(img):
|
||||
@@ -531,7 +556,7 @@ class Inkycal:
|
||||
im_colour = black_to_colour(im_colour)
|
||||
|
||||
im_colour.paste(im_black, (0, 0), im_black)
|
||||
im_colour.save(image_folder + 'full-screen.png', 'PNG')
|
||||
im_colour.save(os.path.join(settings.IMAGE_FOLDER, 'full-screen.png'), 'PNG')
|
||||
|
||||
@staticmethod
|
||||
def _optimize_im(image, threshold=220):
|
||||
@@ -574,13 +599,40 @@ class Inkycal:
|
||||
@staticmethod
|
||||
def cleanup():
|
||||
# clean up old images in image_folder
|
||||
for _file in glob.glob(f"{image_folder}*.png"):
|
||||
if len(glob.glob(settings.IMAGE_FOLDER)) <= 1:
|
||||
return
|
||||
for _file in glob.glob(settings.IMAGE_FOLDER):
|
||||
try:
|
||||
os.remove(_file)
|
||||
except:
|
||||
logger.error(f"could not remove file: {_file}")
|
||||
pass
|
||||
|
||||
def process_module(self, number) -> bool or Exception:
|
||||
"""Process individual module to generate images and handle exceptions."""
|
||||
module = eval(f'self.module_{number}')
|
||||
try:
|
||||
black, colour = module.generate_image()
|
||||
if self.show_border:
|
||||
draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5)
|
||||
black.save(os.path.join(settings.IMAGE_FOLDER, f"module{number}_black.png"), "PNG")
|
||||
colour.save(os.path.join(settings.IMAGE_FOLDER, f"module{number}_colour.png"), "PNG")
|
||||
return True
|
||||
except Exception:
|
||||
logger.exception(f"Error in module {number}!")
|
||||
return False
|
||||
|
||||
def _shutdown_system(self):
|
||||
"""Shutdown the system"""
|
||||
import subprocess
|
||||
from time import sleep
|
||||
try:
|
||||
logger.info("Shutting down OS in 5 seconds...")
|
||||
sleep(5)
|
||||
subprocess.run(["sudo", "shutdown", "-h", "now"], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
logger.warning("Failed to execute shutdown command.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(f'running inkycal main in standalone/debug mode')
|
||||
|
@@ -156,7 +156,7 @@ class Simple(inkycal_module):
|
||||
# -----------------------------------------------------------------------#
|
||||
|
||||
# give an OK message
|
||||
print(f'{__name__} loaded')
|
||||
logger.debug(f'{__name__} loaded')
|
||||
|
||||
#############################################################################
|
||||
# Validation of module specific parameters (optional) #
|
||||
|
@@ -27,7 +27,7 @@ class Inkyimage:
|
||||
self.image = image
|
||||
|
||||
# give an OK message
|
||||
logger.info(f"{__name__} loaded")
|
||||
logger.debug(f"{__name__} loaded")
|
||||
|
||||
def load(self, path: str) -> None:
|
||||
"""loads an image from a URL or filepath.
|
||||
@@ -59,7 +59,7 @@ class Inkyimage:
|
||||
logger.error("Invalid Image file provided", exc_info=True)
|
||||
raise Exception("Please check if the path points to an image file.")
|
||||
|
||||
logger.info(f"width: {image.width}, height: {image.height}")
|
||||
logger.debug(f"width: {image.width}, height: {image.height}")
|
||||
|
||||
image.convert(mode="RGBA") # convert to a more suitable format
|
||||
self.image = image
|
||||
|
@@ -2,9 +2,7 @@
|
||||
Inkycal Agenda Module
|
||||
Copyright by aceinnolab
|
||||
"""
|
||||
|
||||
import arrow
|
||||
|
||||
import arrow # noqa
|
||||
from inkycal.custom import *
|
||||
from inkycal.modules.ical_parser import iCalendar
|
||||
from inkycal.modules.template import inkycal_module
|
||||
@@ -77,8 +75,10 @@ class Agenda(inkycal_module):
|
||||
# Additional config
|
||||
self.timezone = get_system_tz()
|
||||
|
||||
self.icon_font = ImageFont.truetype(fonts['MaterialIcons'], size=self.fontsize)
|
||||
|
||||
# give an OK message
|
||||
print(f'{__name__} loaded')
|
||||
logger.debug(f'{__name__} loaded')
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
@@ -88,7 +88,7 @@ class Agenda(inkycal_module):
|
||||
im_height = int(self.height - (2 * self.padding_top))
|
||||
im_size = im_width, im_height
|
||||
|
||||
logger.info(f'Image size: {im_size}')
|
||||
logger.debug(f'Image size: {im_size}')
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels
|
||||
im_black = Image.new('RGB', size=im_size, color='white')
|
||||
@@ -203,10 +203,10 @@ class Agenda(inkycal_module):
|
||||
write(im_black, (x_time, line_pos[cursor][1]),
|
||||
(time_width, line_height), time,
|
||||
font=self.font, alignment='right')
|
||||
if parser.all_day(_):
|
||||
else:
|
||||
write(im_black, (x_time, line_pos[cursor][1]),
|
||||
(time_width, line_height), "all day",
|
||||
font=self.font, alignment='right')
|
||||
(time_width, line_height), "\ue878",
|
||||
font=self.icon_font, alignment='right')
|
||||
|
||||
write(im_black, (x_event, line_pos[cursor][1]),
|
||||
(event_width, line_height),
|
||||
|
@@ -6,16 +6,16 @@ Copyright by aceinnolab
|
||||
# pylint: disable=logging-fstring-interpolation
|
||||
|
||||
import calendar as cal
|
||||
import arrow
|
||||
from inkycal.modules.template import inkycal_module
|
||||
|
||||
from inkycal.custom import *
|
||||
from inkycal.modules.template import inkycal_module
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Calendar(inkycal_module):
|
||||
"""Calendar class
|
||||
Create monthly calendar and show events from given icalendars
|
||||
Create monthly calendar and show events from given iCalendars
|
||||
"""
|
||||
|
||||
name = "Calendar - Show monthly calendar with events from iCalendars"
|
||||
@@ -39,12 +39,12 @@ class Calendar(inkycal_module):
|
||||
},
|
||||
"date_format": {
|
||||
"label": "Use an arrow-supported token for custom date formatting "
|
||||
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM",
|
||||
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM",
|
||||
"default": "D MMM",
|
||||
},
|
||||
"time_format": {
|
||||
"label": "Use an arrow-supported token for custom time formatting "
|
||||
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
|
||||
+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
|
||||
"default": "HH:mm",
|
||||
},
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class Calendar(inkycal_module):
|
||||
self._days_with_events = None
|
||||
|
||||
# optional parameters
|
||||
self.weekstart = config['week_starts_on']
|
||||
self.week_start = config['week_starts_on']
|
||||
self.show_events = config['show_events']
|
||||
self.date_format = config["date_format"]
|
||||
self.time_format = config['time_format']
|
||||
@@ -84,7 +84,7 @@ class Calendar(inkycal_module):
|
||||
)
|
||||
|
||||
# give an OK message
|
||||
print(f'{__name__} loaded')
|
||||
logger.debug(f'{__name__} loaded')
|
||||
|
||||
@staticmethod
|
||||
def flatten(values):
|
||||
@@ -100,7 +100,7 @@ class Calendar(inkycal_module):
|
||||
im_size = im_width, im_height
|
||||
events_height = 0
|
||||
|
||||
logger.info(f'Image size: {im_size}')
|
||||
logger.debug(f'Image size: {im_size}')
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels
|
||||
im_black = Image.new('RGB', size=im_size, color='white')
|
||||
@@ -109,7 +109,7 @@ class Calendar(inkycal_module):
|
||||
# Allocate space for month-names, weekdays etc.
|
||||
month_name_height = int(im_height * 0.10)
|
||||
text_bbox_height = self.font.getbbox("hg")
|
||||
weekdays_height = int((text_bbox_height[3] - text_bbox_height[1])* 1.25)
|
||||
weekdays_height = int((abs(text_bbox_height[3]) + abs(text_bbox_height[1])) * 1.25)
|
||||
logger.debug(f"month_name_height: {month_name_height}")
|
||||
logger.debug(f"weekdays_height: {weekdays_height}")
|
||||
|
||||
@@ -117,7 +117,7 @@ class Calendar(inkycal_module):
|
||||
logger.debug("Allocating space for events")
|
||||
calendar_height = int(im_height * 0.6)
|
||||
events_height = (
|
||||
im_height - month_name_height - weekdays_height - calendar_height
|
||||
im_height - month_name_height - weekdays_height - calendar_height
|
||||
)
|
||||
logger.debug(f'calendar-section size: {im_width} x {calendar_height} px')
|
||||
logger.debug(f'events-section size: {im_width} x {events_height} px')
|
||||
@@ -156,13 +156,13 @@ class Calendar(inkycal_module):
|
||||
|
||||
now = arrow.now(tz=self.timezone)
|
||||
|
||||
# Set weekstart of calendar to specified weekstart
|
||||
if self.weekstart == "Monday":
|
||||
# Set week-start of calendar to specified week-start
|
||||
if self.week_start == "Monday":
|
||||
cal.setfirstweekday(cal.MONDAY)
|
||||
weekstart = now.shift(days=-now.weekday())
|
||||
week_start = now.shift(days=-now.weekday())
|
||||
else:
|
||||
cal.setfirstweekday(cal.SUNDAY)
|
||||
weekstart = now.shift(days=-now.isoweekday())
|
||||
week_start = now.shift(days=-now.isoweekday())
|
||||
|
||||
# Write the name of current month
|
||||
write(
|
||||
@@ -174,9 +174,9 @@ class Calendar(inkycal_module):
|
||||
autofit=True,
|
||||
)
|
||||
|
||||
# Set up weeknames in local language and add to main section
|
||||
# Set up week-names in local language and add to main section
|
||||
weekday_names = [
|
||||
weekstart.shift(days=+_).format('ddd', locale=self.language)
|
||||
week_start.shift(days=+_).format('ddd', locale=self.language)
|
||||
for _ in range(7)
|
||||
]
|
||||
logger.debug(f'weekday names: {weekday_names}')
|
||||
@@ -192,7 +192,7 @@ class Calendar(inkycal_module):
|
||||
fill_height=0.9,
|
||||
)
|
||||
|
||||
# Create a calendar template and flatten (remove nestings)
|
||||
# Create a calendar template and flatten (remove nesting)
|
||||
calendar_flat = self.flatten(cal.monthcalendar(now.year, now.month))
|
||||
# logger.debug(f" calendar_flat: {calendar_flat}")
|
||||
|
||||
@@ -265,7 +265,7 @@ class Calendar(inkycal_module):
|
||||
# find out how many lines can fit at max in the event section
|
||||
line_spacing = 2
|
||||
text_bbox_height = self.font.getbbox("hg")
|
||||
line_height = text_bbox_height[3] + line_spacing
|
||||
line_height = text_bbox_height[3] - text_bbox_height[1] + line_spacing
|
||||
max_event_lines = events_height // (line_height + line_spacing)
|
||||
|
||||
# generate list of coordinates for each line
|
||||
@@ -281,7 +281,7 @@ class Calendar(inkycal_module):
|
||||
month_start = arrow.get(now.floor('month'))
|
||||
month_end = arrow.get(now.ceil('month'))
|
||||
|
||||
# fetch events from given icalendars
|
||||
# fetch events from given iCalendars
|
||||
self.ical = iCalendar()
|
||||
parser = self.ical
|
||||
|
||||
@@ -294,14 +294,12 @@ class Calendar(inkycal_module):
|
||||
month_events = parser.get_events(month_start, month_end, self.timezone)
|
||||
parser.sort()
|
||||
self.month_events = month_events
|
||||
|
||||
|
||||
# Initialize days_with_events as an empty list
|
||||
days_with_events = []
|
||||
|
||||
# Handle multi-day events by adding all days between start and end
|
||||
for event in month_events:
|
||||
start_date = event['begin'].date()
|
||||
end_date = event['end'].date()
|
||||
|
||||
# Convert start and end dates to arrow objects with timezone
|
||||
start = arrow.get(event['begin'].date(), tzinfo=self.timezone)
|
||||
@@ -324,9 +322,7 @@ class Calendar(inkycal_module):
|
||||
im_colour,
|
||||
grid[days],
|
||||
(icon_width, icon_height),
|
||||
radius=6,
|
||||
thickness=1,
|
||||
shrinkage=(0.4, 0.2),
|
||||
radius=6
|
||||
)
|
||||
|
||||
# Filter upcoming events until 4 weeks in the future
|
||||
@@ -345,13 +341,13 @@ class Calendar(inkycal_module):
|
||||
|
||||
date_width = int(max((
|
||||
self.font.getlength(events['begin'].format(self.date_format, locale=lang))
|
||||
for events in upcoming_events))* 1.1
|
||||
)
|
||||
for events in upcoming_events)) * 1.1
|
||||
)
|
||||
|
||||
time_width = int(max((
|
||||
self.font.getlength(events['begin'].format(self.time_format, locale=lang))
|
||||
for events in upcoming_events))* 1.1
|
||||
)
|
||||
for events in upcoming_events)) * 1.1
|
||||
)
|
||||
|
||||
text_bbox_height = self.font.getbbox("hg")
|
||||
line_height = text_bbox_height[3] + line_spacing
|
||||
@@ -369,7 +365,8 @@ class Calendar(inkycal_module):
|
||||
event_duration = (event['end'] - event['begin']).days
|
||||
if event_duration > 1:
|
||||
# Format the duration using Arrow's localization
|
||||
days_translation = arrow.get().shift(days=event_duration).humanize(only_distance=True, locale=lang)
|
||||
days_translation = arrow.get().shift(days=event_duration).humanize(only_distance=True,
|
||||
locale=lang)
|
||||
the_name = f"{event['title']} ({days_translation})"
|
||||
else:
|
||||
the_name = event['title']
|
||||
|
@@ -60,7 +60,7 @@ class Feeds(inkycal_module):
|
||||
self.shuffle_feeds = config["shuffle_feeds"]
|
||||
|
||||
# give an OK message
|
||||
print(f'{__name__} loaded')
|
||||
logger.debug(f'{__name__} loaded')
|
||||
|
||||
def _validate(self):
|
||||
"""Validate module-specific parameters"""
|
||||
@@ -75,7 +75,7 @@ class Feeds(inkycal_module):
|
||||
im_width = int(self.width - (2 * self.padding_left))
|
||||
im_height = int(self.height - (2 * self.padding_top))
|
||||
im_size = im_width, im_height
|
||||
logger.info(f'Image size: {im_size}')
|
||||
logger.debug(f'Image size: {im_size}')
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels
|
||||
im_black = Image.new('RGB', size=im_size, color='white')
|
||||
@@ -83,8 +83,9 @@ class Feeds(inkycal_module):
|
||||
|
||||
# Check if internet is available
|
||||
if internet_available():
|
||||
logger.info('Connection test passed')
|
||||
logger.debug('Connection test passed')
|
||||
else:
|
||||
logger.error("Network not reachable. Please check your connection.")
|
||||
raise NetworkNotReachableError
|
||||
|
||||
# Set some parameters for formatting feeds
|
||||
|
@@ -23,16 +23,18 @@ from icons.weather_icons.weather_icons import get_weather_icon
|
||||
from inkycal.custom.functions import fonts
|
||||
from inkycal.custom.functions import get_system_tz
|
||||
from inkycal.custom.functions import internet_available
|
||||
from inkycal.custom.functions import top_level
|
||||
from inkycal.custom.inkycal_exceptions import NetworkNotReachableError
|
||||
from inkycal.custom.openweathermap_wrapper import OpenWeatherMap
|
||||
from inkycal.modules.inky_image import image_to_palette
|
||||
from inkycal.modules.template import inkycal_module
|
||||
from inkycal.settings import Settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
icons_dir = os.path.join(top_level, "icons", "ui-icons")
|
||||
settings = Settings()
|
||||
|
||||
icons_dir = os.path.join(settings.FONT_PATH, "ui-icons")
|
||||
|
||||
|
||||
def outline(image: Image, size: int, color: tuple) -> Image:
|
||||
@@ -139,7 +141,7 @@ class Fullweather(inkycal_module):
|
||||
|
||||
# Check if all required parameters are present
|
||||
for param in self.requires:
|
||||
if not param in config:
|
||||
if param not in config:
|
||||
raise Exception(f"config is missing {param}")
|
||||
|
||||
# required parameters
|
||||
@@ -237,7 +239,7 @@ class Fullweather(inkycal_module):
|
||||
self.left_section_width = int(self.width / 4)
|
||||
|
||||
# give an OK message
|
||||
print(f"{__name__} loaded")
|
||||
logger.debug(f"{__name__} loaded")
|
||||
|
||||
def createBaseImage(self):
|
||||
"""
|
||||
|
@@ -50,7 +50,7 @@ class Inkyimage(inkycal_module):
|
||||
self.dither = False
|
||||
|
||||
# give an OK message
|
||||
print(f"{__name__} loaded")
|
||||
logger.debug(f"{__name__} loaded")
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
@@ -71,7 +71,7 @@ class Inkyimage(inkycal_module):
|
||||
# Remove background if present
|
||||
im.remove_alpha()
|
||||
|
||||
# if autoflip was enabled, flip the image
|
||||
# if auto-flip was enabled, flip the image
|
||||
if self.autoflip:
|
||||
im.autoflip(self.orientation)
|
||||
|
||||
|
@@ -30,7 +30,7 @@ class Jokes(inkycal_module):
|
||||
config = config['config']
|
||||
|
||||
# give an OK message
|
||||
print(f'{__name__} loaded')
|
||||
logger.debug(f'{__name__} loaded')
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
@@ -39,7 +39,7 @@ class Jokes(inkycal_module):
|
||||
im_width = int(self.width - (2 * self.padding_left))
|
||||
im_height = int(self.height - (2 * self.padding_top))
|
||||
im_size = im_width, im_height
|
||||
logger.info(f'image size: {im_width} x {im_height} px')
|
||||
logger.debug(f'image size: {im_width} x {im_height} px')
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels
|
||||
im_black = Image.new('RGB', size=im_size, color='white')
|
||||
@@ -47,8 +47,9 @@ class Jokes(inkycal_module):
|
||||
|
||||
# Check if internet is available
|
||||
if internet_available():
|
||||
logger.info('Connection test passed')
|
||||
logger.debug('Connection test passed')
|
||||
else:
|
||||
logger.error("Network not reachable. Please check your connection.")
|
||||
raise NetworkNotReachableError
|
||||
|
||||
# Set some parameters for formatting feeds
|
||||
|
@@ -67,7 +67,7 @@ class Inkyserver(inkycal_module):
|
||||
self.path_body = config['path_body']
|
||||
|
||||
# give an OK message
|
||||
print(f'{__name__} loaded')
|
||||
logger.debug(f'{__name__} loaded')
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
|
@@ -8,13 +8,13 @@ from inkycal.custom import *
|
||||
# PIL has a class named Image, use alias for Inkyimage -> Images
|
||||
from inkycal.modules.inky_image import Inkyimage as Images, image_to_palette
|
||||
from inkycal.modules.template import inkycal_module
|
||||
from inkycal.utils import JSONCache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Slideshow(inkycal_module):
|
||||
"""Cycles through images in a local image folder
|
||||
"""
|
||||
"""Cycles through images in a local image folder"""
|
||||
name = "Slideshow - cycle through images from a local folder"
|
||||
|
||||
requires = {
|
||||
@@ -53,7 +53,7 @@ class Slideshow(inkycal_module):
|
||||
|
||||
# required parameters
|
||||
for param in self.requires:
|
||||
if not param in config:
|
||||
if param not in config:
|
||||
raise Exception(f'config is missing {param}')
|
||||
|
||||
# optional parameters
|
||||
@@ -64,19 +64,20 @@ class Slideshow(inkycal_module):
|
||||
|
||||
# Get the full path of all png/jpg/jpeg images in the given folder
|
||||
all_files = glob.glob(f'{self.path}/*')
|
||||
self.images = [i for i in all_files
|
||||
if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
|
||||
self.images = [i for i in all_files if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
|
||||
|
||||
if not self.images:
|
||||
logger.error('No images found in the given folder, please '
|
||||
'double check your path!')
|
||||
logger.error('No images found in the given folder, please double check your path!')
|
||||
raise Exception('No images found in the given folder path :/')
|
||||
|
||||
self.cache = JSONCache('inkycal_slideshow')
|
||||
self.cache_data = self.cache.read()
|
||||
|
||||
# set a 'first run' signal
|
||||
self._first_run = True
|
||||
|
||||
# give an OK message
|
||||
print(f'{__name__} loaded')
|
||||
logger.debug(f'{__name__} loaded')
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
@@ -86,17 +87,19 @@ class Slideshow(inkycal_module):
|
||||
im_height = int(self.height - (2 * self.padding_top))
|
||||
im_size = im_width, im_height
|
||||
|
||||
logger.info(f'Image size: {im_size}')
|
||||
logger.debug(f'Image size: {im_size}')
|
||||
|
||||
# rotates list items by 1 index
|
||||
def rotate(somelist):
|
||||
return somelist[1:] + somelist[:1]
|
||||
def rotate(list: list):
|
||||
return list[1:] + list[:1]
|
||||
|
||||
# Switch to the next image if this is not the first run
|
||||
if self._first_run:
|
||||
self._first_run = False
|
||||
self.cache_data["current_index"] = 0
|
||||
else:
|
||||
self.images = rotate(self.images)
|
||||
self.cache_data["current_index"] = (self.cache_data["current_index"] + 1) % len(self.images)
|
||||
|
||||
# initialize custom image class
|
||||
im = Images()
|
||||
@@ -110,7 +113,7 @@ class Slideshow(inkycal_module):
|
||||
# Remove background if present
|
||||
im.remove_alpha()
|
||||
|
||||
# if autoflip was enabled, flip the image
|
||||
# if auto-flip was enabled, flip the image
|
||||
if self.autoflip:
|
||||
im.autoflip(self.orientation)
|
||||
|
||||
@@ -123,6 +126,8 @@ class Slideshow(inkycal_module):
|
||||
# with the images now send, clear the current image
|
||||
im.clear()
|
||||
|
||||
self.cache.write(self.cache_data)
|
||||
|
||||
# return images
|
||||
return im_black, im_colour
|
||||
|
||||
|
@@ -54,7 +54,7 @@ class Stocks(inkycal_module):
|
||||
self.tickers = config['tickers']
|
||||
|
||||
# give an OK message
|
||||
print(f'{__name__} loaded')
|
||||
logger.debug(f'{__name__} loaded')
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
@@ -63,7 +63,7 @@ class Stocks(inkycal_module):
|
||||
im_width = int(self.width - (2 * self.padding_left))
|
||||
im_height = int(self.height - (2 * self.padding_top))
|
||||
im_size = im_width, im_height
|
||||
logger.info(f'image size: {im_width} x {im_height} px')
|
||||
logger.debug(f'image size: {im_width} x {im_height} px')
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels (required)
|
||||
im_black = Image.new('RGB', size=im_size, color='white')
|
||||
@@ -142,7 +142,7 @@ class Stocks(inkycal_module):
|
||||
logger.warning(f"Failed to get '{stockName}' ticker price hint! Using "
|
||||
"default precision of 2 instead.")
|
||||
|
||||
stockHistory = yfTicker.history("30d")
|
||||
stockHistory = yfTicker.history("1mo")
|
||||
stockHistoryLen = len(stockHistory)
|
||||
logger.info(f'fetched {stockHistoryLen} datapoints ...')
|
||||
previousQuote = (stockHistory.tail(2)['Close'].iloc[0])
|
||||
|
@@ -31,7 +31,7 @@ class TextToDisplay(inkycal_module):
|
||||
self.make_request = True if self.filepath.startswith("https://") else False
|
||||
|
||||
# give an OK message
|
||||
print(f'{__name__} loaded')
|
||||
logger.debug(f'{__name__} loaded')
|
||||
|
||||
def _validate(self):
|
||||
"""Validate module-specific parameters"""
|
||||
@@ -45,7 +45,7 @@ class TextToDisplay(inkycal_module):
|
||||
im_width = int(self.width - (2 * self.padding_left))
|
||||
im_height = int(self.height - (2 * self.padding_top))
|
||||
im_size = im_width, im_height
|
||||
logger.info(f'Image size: {im_size}')
|
||||
logger.debug(f'Image size: {im_size}')
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels
|
||||
im_black = Image.new('RGB', size=im_size, color='white')
|
||||
|
@@ -32,7 +32,7 @@ class Tindie(inkycal_module):
|
||||
# self.mode = config['mode'] # unshipped_orders, shipped_orders, all_orders
|
||||
|
||||
# give an OK message
|
||||
print(f'{__name__} loaded')
|
||||
logger.debug(f'{__name__} loaded')
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
@@ -40,7 +40,7 @@ class Tindie(inkycal_module):
|
||||
im_width = int(self.width - (2 * self.padding_left))
|
||||
im_height = int(self.height - (2 * self.padding_top))
|
||||
im_size = im_width, im_height
|
||||
logger.info(f'image size: {im_width} x {im_height} px')
|
||||
logger.debug(f'image size: {im_width} x {im_height} px')
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels
|
||||
im_black = Image.new('RGB', size=im_size, color='white')
|
||||
@@ -50,6 +50,7 @@ class Tindie(inkycal_module):
|
||||
if internet_available():
|
||||
logger.info('Connection test passed')
|
||||
else:
|
||||
logger.error("Network not reachable. Please check your connection.")
|
||||
raise NetworkNotReachableError
|
||||
|
||||
# Set some parameters for formatting feeds
|
||||
|
@@ -56,7 +56,7 @@ class Todoist(inkycal_module):
|
||||
self._api = TodoistAPI(config['api_key'])
|
||||
|
||||
# give an OK message
|
||||
print(f'{__name__} loaded')
|
||||
logger.debug(f'{__name__} loaded')
|
||||
|
||||
def _validate(self):
|
||||
"""Validate module-specific parameters"""
|
||||
@@ -70,7 +70,7 @@ class Todoist(inkycal_module):
|
||||
im_width = int(self.width - (2 * self.padding_left))
|
||||
im_height = int(self.height - (2 * self.padding_top))
|
||||
im_size = im_width, im_height
|
||||
logger.info(f'Image size: {im_size}')
|
||||
logger.debug(f'Image size: {im_size}')
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels
|
||||
im_black = Image.new('RGB', size=im_size, color='white')
|
||||
@@ -80,6 +80,7 @@ class Todoist(inkycal_module):
|
||||
if internet_available():
|
||||
logger.info('Connection test passed')
|
||||
else:
|
||||
logger.error("Network not reachable. Please check your connection.")
|
||||
raise NetworkNotReachableError
|
||||
|
||||
# Set some parameters for formatting todos
|
||||
|
@@ -2,12 +2,12 @@
|
||||
Inkycal weather module
|
||||
Copyright by aceinnolab
|
||||
"""
|
||||
|
||||
import arrow
|
||||
import decimal
|
||||
import logging
|
||||
import math
|
||||
from typing import Tuple
|
||||
|
||||
import arrow
|
||||
from PIL import Image
|
||||
from PIL import ImageDraw
|
||||
from PIL import ImageFont
|
||||
@@ -51,7 +51,7 @@ class Weather(inkycal_module):
|
||||
"options": [True, False],
|
||||
},
|
||||
|
||||
"round_windspeed": {
|
||||
"round_wind_speed": {
|
||||
"label": "Round windspeed?",
|
||||
"options": [True, False],
|
||||
},
|
||||
@@ -89,7 +89,7 @@ class Weather(inkycal_module):
|
||||
|
||||
# Check if all required parameters are present
|
||||
for param in self.requires:
|
||||
if not param in config:
|
||||
if param not in config:
|
||||
raise Exception(f'config is missing {param}')
|
||||
|
||||
# required parameters
|
||||
@@ -98,15 +98,15 @@ class Weather(inkycal_module):
|
||||
|
||||
# optional parameters
|
||||
self.round_temperature = config['round_temperature']
|
||||
self.round_windspeed = config['round_windspeed']
|
||||
self.round_wind_speed = config['round_windspeed']
|
||||
self.forecast_interval = config['forecast_interval']
|
||||
self.hour_format = int(config['hour_format'])
|
||||
if config['units'] == "imperial":
|
||||
self.temp_unit = "fahrenheit"
|
||||
else:
|
||||
self.temp_unit = "celsius"
|
||||
|
||||
if config['use_beaufort'] == True:
|
||||
|
||||
if config['use_beaufort']:
|
||||
self.wind_unit = "beaufort"
|
||||
elif config['units'] == "imperial":
|
||||
self.wind_unit = "miles_hour"
|
||||
@@ -116,17 +116,17 @@ class Weather(inkycal_module):
|
||||
# additional configuration
|
||||
|
||||
self.owm = OpenWeatherMap(
|
||||
api_key=self.api_key,
|
||||
city_id=self.location,
|
||||
wind_unit=self.wind_unit,
|
||||
api_key=self.api_key,
|
||||
city_id=self.location,
|
||||
wind_unit=self.wind_unit,
|
||||
temp_unit=self.temp_unit,
|
||||
language=self.locale,
|
||||
language=self.locale,
|
||||
tz_name=self.timezone
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
self.weatherfont = ImageFont.truetype(
|
||||
fonts['weathericons-regular-webfont'], size=self.fontsize)
|
||||
|
||||
|
||||
if self.wind_unit == "beaufort":
|
||||
self.windDispUnit = "bft"
|
||||
elif self.wind_unit == "knots":
|
||||
@@ -143,9 +143,7 @@ class Weather(inkycal_module):
|
||||
self.tempDispUnit = "°"
|
||||
|
||||
# give an OK message
|
||||
print(f"{__name__} loaded")
|
||||
|
||||
|
||||
logger.debug(f"{__name__} loaded")
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
@@ -154,7 +152,7 @@ class Weather(inkycal_module):
|
||||
im_width = int(self.width - (2 * self.padding_left))
|
||||
im_height = int(self.height - (2 * self.padding_top))
|
||||
im_size = im_width, im_height
|
||||
logger.info(f'Image size: {im_size}')
|
||||
logger.debug(f'Image size: {im_size}')
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels
|
||||
im_black = Image.new('RGB', size=im_size, color='white')
|
||||
@@ -162,8 +160,9 @@ class Weather(inkycal_module):
|
||||
|
||||
# Check if internet is available
|
||||
if internet_available():
|
||||
logger.info('Connection test passed')
|
||||
logger.debug('Connection test passed')
|
||||
else:
|
||||
logger.error("Network not reachable. Please check your connection.")
|
||||
raise NetworkNotReachableError
|
||||
|
||||
def get_moon_phase():
|
||||
@@ -190,7 +189,7 @@ class Weather(inkycal_module):
|
||||
7: '\uf0ae'
|
||||
}[int(index) & 7]
|
||||
|
||||
def is_negative(temp:str):
|
||||
def is_negative(temp: str):
|
||||
"""Check if temp is below freezing point of water (0°C/32°F)
|
||||
returns True if temp below freezing point, else False"""
|
||||
answer = False
|
||||
@@ -223,12 +222,19 @@ class Weather(inkycal_module):
|
||||
'50n': '\uf023'
|
||||
}
|
||||
|
||||
def draw_icon(image, xy, box_size, icon, rotation=None):
|
||||
"""Custom function to add icons of weather font on image
|
||||
image = on which image should the text be added?
|
||||
xy = xy-coordinates as tuple -> (x,y)
|
||||
box_size = size of text-box -> (width,height)
|
||||
icon = icon-unicode, looks this up in weathericons dictionary
|
||||
def draw_icon(image: Image, xy: Tuple[int, int], box_size: Tuple[int, int], icon: str, rotation=None):
|
||||
"""Custom function to add icons of weather font on the image.
|
||||
|
||||
Args:
|
||||
- image:
|
||||
the image on which image should the text be added
|
||||
- xy:
|
||||
coordinates as tuple -> (x,y)
|
||||
- box_size:
|
||||
size of text-box -> (width,height)
|
||||
- icon:
|
||||
icon-unicode, looks this up in weather-icons dictionary
|
||||
|
||||
"""
|
||||
|
||||
icon_size_correction = {
|
||||
@@ -263,7 +269,6 @@ class Weather(inkycal_module):
|
||||
'\uf0a0': 0,
|
||||
'\uf0a3': 0,
|
||||
'\uf0a7': 0,
|
||||
'\uf0aa': 0,
|
||||
'\uf0ae': 0
|
||||
}
|
||||
|
||||
@@ -277,8 +282,7 @@ class Weather(inkycal_module):
|
||||
font = ImageFont.truetype(font.path, size)
|
||||
text_width, text_height = font.getbbox(text)[2:]
|
||||
|
||||
while (text_width < int(box_width * 0.9) and
|
||||
text_height < int(box_height * 0.9)):
|
||||
while text_width < int(box_width * 0.9) and text_height < int(box_height * 0.9):
|
||||
size += 1
|
||||
font = ImageFont.truetype(font.path, size)
|
||||
text_width, text_height = font.getbbox(text)[2:]
|
||||
@@ -289,8 +293,6 @@ class Weather(inkycal_module):
|
||||
x = int((box_width / 2) - (text_width / 2))
|
||||
y = int((box_height / 2) - (text_height / 2))
|
||||
|
||||
# Draw the text in the text-box
|
||||
draw = ImageDraw.Draw(image)
|
||||
space = Image.new('RGBA', (box_width, box_height))
|
||||
ImageDraw.Draw(space).text((x, y), text, fill='black', font=font)
|
||||
|
||||
@@ -349,17 +351,17 @@ class Weather(inkycal_module):
|
||||
row3 = row2 + line_gap + row_height
|
||||
|
||||
# Draw lines on each row and border
|
||||
############################################################################
|
||||
## draw = ImageDraw.Draw(im_black)
|
||||
## draw.line((0, 0, im_width, 0), fill='red')
|
||||
## draw.line((0, im_height-1, im_width, im_height-1), fill='red')
|
||||
## draw.line((0, row1, im_width, row1), fill='black')
|
||||
## draw.line((0, row1+row_height, im_width, row1+row_height), fill='black')
|
||||
## draw.line((0, row2, im_width, row2), fill='black')
|
||||
## draw.line((0, row2+row_height, im_width, row2+row_height), fill='black')
|
||||
## draw.line((0, row3, im_width, row3), fill='black')
|
||||
## draw.line((0, row3+row_height, im_width, row3+row_height), fill='black')
|
||||
############################################################################
|
||||
###########################################################################
|
||||
# draw = ImageDraw.Draw(im_black)
|
||||
# draw.line((0, 0, im_width, 0), fill='red')
|
||||
# draw.line((0, im_height-1, im_width, im_height-1), fill='red')
|
||||
# draw.line((0, row1, im_width, row1), fill='black')
|
||||
# draw.line((0, row1+row_height, im_width, row1+row_height), fill='black')
|
||||
# draw.line((0, row2, im_width, row2), fill='black')
|
||||
# draw.line((0, row2+row_height, im_width, row2+row_height), fill='black')
|
||||
# draw.line((0, row3, im_width, row3), fill='black')
|
||||
# draw.line((0, row3+row_height, im_width, row3+row_height), fill='black')
|
||||
###########################################################################
|
||||
|
||||
# Positions for current weather details
|
||||
weather_icon_pos = (col1, 0)
|
||||
@@ -378,24 +380,24 @@ class Weather(inkycal_module):
|
||||
sunset_time_pos = (col3 + icon_small, row3)
|
||||
|
||||
# Positions for forecast 1
|
||||
stamp_fc1 = (col4, row1)
|
||||
icon_fc1 = (col4, row1 + row_height)
|
||||
temp_fc1 = (col4, row3)
|
||||
stamp_fc1 = (col4, row1) # noqa
|
||||
icon_fc1 = (col4, row1 + row_height) # noqa
|
||||
temp_fc1 = (col4, row3) # noqa
|
||||
|
||||
# Positions for forecast 2
|
||||
stamp_fc2 = (col5, row1)
|
||||
icon_fc2 = (col5, row1 + row_height)
|
||||
temp_fc2 = (col5, row3)
|
||||
stamp_fc2 = (col5, row1) # noqa
|
||||
icon_fc2 = (col5, row1 + row_height) # noqa
|
||||
temp_fc2 = (col5, row3) # noqa
|
||||
|
||||
# Positions for forecast 3
|
||||
stamp_fc3 = (col6, row1)
|
||||
icon_fc3 = (col6, row1 + row_height)
|
||||
temp_fc3 = (col6, row3)
|
||||
stamp_fc3 = (col6, row1) # noqa
|
||||
icon_fc3 = (col6, row1 + row_height) # noqa
|
||||
temp_fc3 = (col6, row3) # noqa
|
||||
|
||||
# Positions for forecast 4
|
||||
stamp_fc4 = (col7, row1)
|
||||
icon_fc4 = (col7, row1 + row_height)
|
||||
temp_fc4 = (col7, row3)
|
||||
stamp_fc4 = (col7, row1) # noqa
|
||||
icon_fc4 = (col7, row1 + row_height) # noqa
|
||||
temp_fc4 = (col7, row3) # noqa
|
||||
|
||||
# Create current-weather and weather-forecast objects
|
||||
logging.debug('looking up location by ID')
|
||||
@@ -404,7 +406,7 @@ class Weather(inkycal_module):
|
||||
|
||||
# Set decimals
|
||||
dec_temp = 0 if self.round_temperature == True else 1
|
||||
dec_wind = 0 if self.round_windspeed == True else 1
|
||||
dec_wind = 0 if self.round_wind_speed == True else 1
|
||||
|
||||
logging.debug(f'temperature unit: {self.temp_unit}')
|
||||
logging.debug(f'decimals temperature: {dec_temp} | decimals wind: {dec_wind}')
|
||||
@@ -424,7 +426,8 @@ class Weather(inkycal_module):
|
||||
fc_data['fc' + str(index + 1)] = {
|
||||
'temp': f"{forecast['temp']:.{dec_temp}f}{self.tempDispUnit}",
|
||||
'icon': forecast["icon"],
|
||||
'stamp': forecast["datetime"].strftime("%I %p" if self.hour_format == 12 else "%H:%M")}
|
||||
'stamp': forecast["datetime"].strftime("%I %p" if self.hour_format == 12 else "%H:%M")
|
||||
}
|
||||
|
||||
elif self.forecast_interval == 'daily':
|
||||
|
||||
@@ -433,7 +436,7 @@ class Weather(inkycal_module):
|
||||
daily_forecasts = [self.owm.get_forecast_for_day(days) for days in range(1, 5)]
|
||||
|
||||
for index, forecast in enumerate(daily_forecasts):
|
||||
fc_data['fc' + str(index +1)] = {
|
||||
fc_data['fc' + str(index + 1)] = {
|
||||
'temp': f'{forecast["temp_min"]:.{dec_temp}f}{self.tempDispUnit}/{forecast["temp_max"]:.{dec_temp}f}{self.tempDispUnit}',
|
||||
'icon': forecast['icon'],
|
||||
'stamp': forecast['datetime'].strftime("%A")
|
||||
@@ -513,6 +516,9 @@ class Weather(inkycal_module):
|
||||
# Add the forecast data to the correct places
|
||||
for pos in range(1, len(fc_data) + 1):
|
||||
stamp = fc_data[f'fc{pos}']['stamp']
|
||||
# check if we're using daily forecasts
|
||||
if "day" in stamp:
|
||||
stamp = arrow.get(fc_data[f'fc{pos}']['stamp'], "dddd").format("dddd", locale=self.locale)
|
||||
|
||||
icon = weather_icons[fc_data[f'fc{pos}']['icon']]
|
||||
temp = fc_data[f'fc{pos}']['temp']
|
||||
|
@@ -40,7 +40,10 @@ class Webshot(inkycal_module):
|
||||
},
|
||||
"crop_h": {
|
||||
"label": "Please enter the crop height",
|
||||
}
|
||||
},
|
||||
"rotation": {
|
||||
"label": "Please enter the rotation. Must be either 0, 90, 180 or 270",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, config):
|
||||
@@ -72,8 +75,14 @@ class Webshot(inkycal_module):
|
||||
else:
|
||||
self.crop_y = 0
|
||||
|
||||
self.rotation = 0
|
||||
if "rotation" in config:
|
||||
self.rotation = int(config["rotation"])
|
||||
if self.rotation not in [0, 90, 180, 270]:
|
||||
raise Exception("Rotation must be either 0, 90, 180 or 270")
|
||||
|
||||
# give an OK message
|
||||
print(f'Inkycal webshot loaded')
|
||||
logger.debug(f'Inkycal webshot loaded')
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
@@ -89,7 +98,7 @@ class Webshot(inkycal_module):
|
||||
im_width = int(self.width - (2 * self.padding_left))
|
||||
im_height = int(self.height - (2 * self.padding_top))
|
||||
im_size = im_width, im_height
|
||||
logger.info('image size: {} x {} px'.format(im_width, im_height))
|
||||
logger.debug('image size: {} x {} px'.format(im_width, im_height))
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels (required)
|
||||
im_black = Image.new('RGB', size=im_size, color='white')
|
||||
@@ -99,12 +108,13 @@ class Webshot(inkycal_module):
|
||||
if internet_available():
|
||||
logger.info('Connection test passed')
|
||||
else:
|
||||
logger.error("Network not reachable. Please check your connection.")
|
||||
raise Exception('Network could not be reached :/')
|
||||
|
||||
logger.info(
|
||||
f'preparing webshot from {self.url}... cropH{self.crop_h} cropW{self.crop_w} cropX{self.crop_x} cropY{self.crop_y}')
|
||||
|
||||
shot = WebShot()
|
||||
shot = WebShot(size=(im_height, im_width))
|
||||
|
||||
shot.params = {
|
||||
"--crop-x": self.crop_x,
|
||||
@@ -150,11 +160,21 @@ class Webshot(inkycal_module):
|
||||
|
||||
centerPosX = int((im_width / 2) - (im.image.width / 2))
|
||||
|
||||
webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY))
|
||||
im_black.paste(webshotSpaceBlack)
|
||||
|
||||
webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY))
|
||||
im_colour.paste(webshotSpaceColour)
|
||||
if self.rotation != 0:
|
||||
webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY))
|
||||
im_black.paste(webshotSpaceBlack)
|
||||
im_black = im_black.rotate(self.rotation, expand=True)
|
||||
|
||||
webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY))
|
||||
im_colour.paste(webshotSpaceColour)
|
||||
im_colour = im_colour.rotate(self.rotation, expand=True)
|
||||
else:
|
||||
webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY))
|
||||
im_black.paste(webshotSpaceBlack)
|
||||
|
||||
webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY))
|
||||
im_colour.paste(webshotSpaceColour)
|
||||
|
||||
im.clear()
|
||||
logger.info(f'added webshot image')
|
||||
|
@@ -11,6 +11,8 @@ from inkycal.modules.template import inkycal_module
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
settings = Settings()
|
||||
|
||||
|
||||
class Xkcd(inkycal_module):
|
||||
name = "xkcd - Displays comics from xkcd.com by Randall Munroe"
|
||||
@@ -51,13 +53,13 @@ class Xkcd(inkycal_module):
|
||||
self.scale_filter = config['filter']
|
||||
|
||||
# give an OK message
|
||||
print(f'Inkycal XKCD loaded')
|
||||
logger.debug(f'Inkycal XKCD loaded')
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
|
||||
# Create tmp path
|
||||
tmpPath = f"{top_level}/temp"
|
||||
tmpPath = settings.TEMPORARY_FOLDER
|
||||
|
||||
if not os.path.exists(tmpPath):
|
||||
os.mkdir(tmpPath)
|
||||
@@ -66,7 +68,7 @@ class Xkcd(inkycal_module):
|
||||
im_width = int(self.width - (2 * self.padding_left))
|
||||
im_height = int(self.height - (2 * self.padding_top))
|
||||
im_size = im_width, im_height
|
||||
logger.info('image size: {} x {} px'.format(im_width, im_height))
|
||||
logger.debug('image size: {} x {} px'.format(im_width, im_height))
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels (required)
|
||||
im_black = Image.new('RGB', size=im_size, color='white')
|
||||
@@ -76,6 +78,7 @@ class Xkcd(inkycal_module):
|
||||
if internet_available():
|
||||
logger.info('Connection test passed')
|
||||
else:
|
||||
logger.error("Network not reachable. Please check your connection.")
|
||||
raise Exception('Network could not be reached :/')
|
||||
|
||||
# Set some parameters for formatting feeds
|
||||
|
22
inkycal/settings.py
Normal file
22
inkycal/settings.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Settings class
|
||||
Used to initialize the settings for the application.
|
||||
"""
|
||||
import os
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
class Settings:
|
||||
"""Settings class to initialize the settings for the application.
|
||||
|
||||
"""
|
||||
CACHE_PATH = os.path.join(basedir, "cache")
|
||||
LOG_PATH = os.path.join(basedir, "../logs")
|
||||
INKYCAL_LOG_PATH = os.path.join(LOG_PATH, "inkycal.log")
|
||||
FONT_PATH = os.path.join(basedir, "../fonts")
|
||||
IMAGE_FOLDER = os.path.join(basedir, "../image_folder")
|
||||
PARALLEL_DRIVER_PATH = os.path.join(basedir, "display", "drivers", "parallel_drivers")
|
||||
TEMPORARY_FOLDER = os.path.join(basedir, "tmp")
|
||||
VCOM = "2.0"
|
||||
# /boot/settings.json is path on older releases, while the latter is more the more recent ones
|
||||
SETTINGS_JSON_PATHS = ["/boot/settings.json", "/boot/firmware/settings.json"]
|
2
inkycal/utils/__init__.py
Normal file
2
inkycal/utils/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .pisugar import PiSugar
|
||||
from .json_cache import JSONCache
|
32
inkycal/utils/json_cache.py
Normal file
32
inkycal/utils/json_cache.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""JSON Cache
|
||||
Can be used to cache JSON data to disk. This is useful for caching data to survive reboots.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
from inkycal.settings import Settings
|
||||
|
||||
settings = Settings()
|
||||
|
||||
|
||||
class JSONCache:
|
||||
def __init__(self, name: str, create_if_not_exists: bool = True):
|
||||
self.path = os.path.join(settings.CACHE_PATH,f"{name}.json")
|
||||
|
||||
if not os.path.exists(settings.CACHE_PATH):
|
||||
os.makedirs(settings.CACHE_PATH)
|
||||
|
||||
if create_if_not_exists and not os.path.exists(self.path):
|
||||
with open(self.path, "w", encoding="utf-8") as file:
|
||||
json.dump({}, file)
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
with open(self.path, "r", encoding="utf-8") as file:
|
||||
return json.load(file)
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
def write(self, data: dict):
|
||||
with open(self.path, "w", encoding="utf-8") as file:
|
||||
json.dump(data, file, indent=4, sort_keys=True)
|
147
inkycal/utils/pisugar.py
Normal file
147
inkycal/utils/pisugar.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""PiSugar helper class for Inkycal."""
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from inkycal.settings import Settings
|
||||
import arrow
|
||||
|
||||
settings = Settings()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PiSugar:
|
||||
|
||||
def __init__(self):
|
||||
# replace "command" with actual command
|
||||
self.command_template = 'echo "command" | nc -q 0 127.0.0.1 8423'
|
||||
self.allowed_commands = ["get battery", "get model", "get rtc_time", "get rtc_alarm_enabled",
|
||||
"get rtc_alarm_time", "get alarm_repeat", "rtc_pi2rtc", "rtc_alarm_set"]
|
||||
|
||||
def _get_output(self, command, param=None):
|
||||
if command not in self.allowed_commands:
|
||||
logger.error(f"Command {command} not allowed")
|
||||
return None
|
||||
if param:
|
||||
cmd = self.command_template.replace("command", f"{command} {param}")
|
||||
else:
|
||||
cmd = self.command_template.replace("command", command)
|
||||
try:
|
||||
result = subprocess.run(cmd, shell=True, text=True, capture_output=True)
|
||||
if result.returncode != 0:
|
||||
print(f"Command failed with {result.stderr}")
|
||||
return None
|
||||
output = result.stdout.strip()
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing command: {e}")
|
||||
return None
|
||||
|
||||
def get_battery(self) -> float or None:
|
||||
"""Get the battery level in percentage.
|
||||
|
||||
Returns:
|
||||
int or None: The battery level in percentage or None if the command fails.
|
||||
"""
|
||||
battery_output = self._get_output("get battery")
|
||||
if battery_output:
|
||||
for line in battery_output.splitlines():
|
||||
if 'battery:' in line:
|
||||
return float(line.split(':')[1].strip())
|
||||
return None
|
||||
|
||||
def get_model(self) -> str or None:
|
||||
"""Get the PiSugar model."""
|
||||
model_output = self._get_output("get model")
|
||||
if model_output:
|
||||
for line in model_output.splitlines():
|
||||
if 'model:' in line:
|
||||
return line.split(':')[1].strip()
|
||||
return None
|
||||
|
||||
def get_rtc_time(self) -> arrow.arrow or None:
|
||||
"""Get the RTC time."""
|
||||
result = self._get_output("get rtc_time")
|
||||
if result:
|
||||
rtc_time = result.split("rtc_time: ")[1].strip()
|
||||
return arrow.get(rtc_time)
|
||||
return None
|
||||
|
||||
def get_rtc_alarm_enabled(self) -> str or None:
|
||||
"""Get the RTC alarm enabled status."""
|
||||
result = self._get_output("get rtc_alarm_enabled")
|
||||
if result:
|
||||
second_line = result.splitlines()[1]
|
||||
output = second_line.split('rtc_alarm_enabled: ')[1].strip()
|
||||
return True if output == "true" else False
|
||||
return None
|
||||
|
||||
def get_rtc_alarm_time(self) -> arrow.arrow or None:
|
||||
"""Get the RTC alarm time."""
|
||||
result = self._get_output("get rtc_alarm_time")
|
||||
if result:
|
||||
alarm_time = result.split('rtc_alarm_time: ')[1].strip()
|
||||
return arrow.get(alarm_time)
|
||||
return None
|
||||
|
||||
def get_alarm_repeat(self) -> dict or None:
|
||||
"""Get the alarm repeat status.
|
||||
|
||||
Returns:
|
||||
dict or None: A dictionary with the alarm repeating days or None if the command fails.
|
||||
"""
|
||||
result = self._get_output("get alarm_repeat")
|
||||
if result:
|
||||
repeating_days = f"{int(result.split('alarm_repeat: ')[1].strip()):8b}".strip()
|
||||
data = {"Monday": False, "Tuesday": False, "Wednesday": False, "Thursday": False, "Friday": False,
|
||||
"Saturday": False, "Sunday": False}
|
||||
if repeating_days[0] == "1":
|
||||
data["Monday"] = True
|
||||
if repeating_days[1] == "1":
|
||||
data["Tuesday"] = True
|
||||
if repeating_days[2] == "1":
|
||||
data["Wednesday"] = True
|
||||
if repeating_days[3] == "1":
|
||||
data["Thursday"] = True
|
||||
if repeating_days[4] == "1":
|
||||
data["Friday"] = True
|
||||
if repeating_days[5] == "1":
|
||||
data["Saturday"] = True
|
||||
if repeating_days[6] == "1":
|
||||
data["Sunday"] = True
|
||||
return data
|
||||
return None
|
||||
|
||||
def rtc_pi2rtc(self) -> bool:
|
||||
"""Sync the Pi time to RTC.
|
||||
|
||||
Returns:
|
||||
bool: True if the sync was successful, False otherwise.
|
||||
"""
|
||||
result = self._get_output("rtc_pi2rtc")
|
||||
if result:
|
||||
status = result.split('rtc_pi2rtc: ')[1].strip()
|
||||
if status == "done":
|
||||
return True
|
||||
return False
|
||||
|
||||
def rtc_alarm_set(self, time: arrow.arrow, repeat:int=127) -> bool:
|
||||
"""Set the RTC alarm time.
|
||||
|
||||
Args:
|
||||
time (arrow.arrow): The alarm time in ISO 8601 format.
|
||||
repeat: int representing 7-bit binary number of repeating days. e.g. 127 = 1111111 = repeat every day
|
||||
|
||||
Returns:
|
||||
bool: True if the alarm was set successfully, False otherwise.
|
||||
"""
|
||||
iso_format = time.isoformat()
|
||||
result = self._get_output("rtc_alarm_set", f"{iso_format } {repeat}")
|
||||
if result:
|
||||
status = result.split('rtc_alarm_set: ')[1].strip()
|
||||
if status == "done":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
Reference in New Issue
Block a user