aceinnolab / Inkycal

Create awesome e-paper dashboards within minutes! Modularity? Check! Python3? Check? Works on Raspberry Pi Zero W? Check! Support for own modules? Check!
https://aceinnolab.github.io/Inkycal/
GNU General Public License v3.0
1.16k stars 125 forks source link

Add official support for inkycal_server module #93

Closed Atrejoe closed 1 year ago

Atrejoe commented 4 years ago

The image module is not yet available as a configurable panel, fitting in besides other existing panels (calender, agenda, rss, weather).

It would be nice if it was.

It may be wise to tackle #94 first

aceisace commented 4 years ago

@Atrejoe I've more or less finished the refacotring of all inkycal_modules except for inkycal_image. Since I'll be working on inkycal_image next, I'd like to ask you to specify the requirements for this modules. Think it might be wise to create a separate module for the image from your service?

aceisace commented 3 years ago

@Atrejoe I've got a few days to work on the official support of the inkcal_server module in Inkycal. I just created an account on your inkycal_server service to get a better grasp of the module. It seems as though the format to request an image is the following: https://inkycal.robertsirre.nl/panel/{panel_id}

In python, this could be implemented in the following way for example:

panel_id = "some_top-secret-id" ## (only required parameter?, any optional ones?)

def get_image_from_url():
## gets the image from the request url
...

image = get_image_from_url(f"https://inkycal.robertsirre.nl/panel/{panel_id}")
## f"https://inkycal.robertsirre.nl/panel/{panel_id}" effectly turns to: 
## "https://inkycal.robertsirre.nl/panel/some_top-secret-id"

There is a minor issue with this approach on 3-colour-displays; since release 2.0.0 requires two images, one for the black pixels, one for white pixels, ideally, there should be 2 images.

But I'll try using numpy for a fast and efficient way to split the image into two images, filtered by colour, so they can be rendered correctly.

Does the url pattern path the desired request URL? Are there any additional required and/or optional parameters worth integrating into this module for the next release? Thanks in advance!

Atrejoe commented 3 years ago

Hi @aceisace,

TLDR: Minor (optional) changes to the image module: support POST, detect colors and dimensions to reduce load. InkCal-server specific client config : nice to have

I hope to save you some of your valuable time with this:

First get the definitions for the feature straight for the rest of the post:

In general the current image component on the client could support three generic methods of processing images:

  1. get image by file path (outside of scope for the rest of this post)
  2. get image by GET url (feature already exists, as least, developed by you, tuned by me, available in branch dev_ver2_0_image_panel)
  3. for advanced usage: get image by POST to url, with parameters (feature was added previously in https://github.com/aceisace/Inky-Calendar/commit/8fa7c075a87d7707e0a03276f0568020b3f209f4#diff-5af0e282319406abf4004162c5c33c9f2af59f12a7a6a10800b19eed0c2abfd8R41, see line 41. I'd say easy to re-implement any time)

The client (already) takes care of :

When using the server, this would result in two possible scenario's:

  1. n/a

  2. Users with an account on InkyCal use the GET url (most users): Fill in the url of the panel that the user has configured (so https://inkycal.robertsirre.nl/panel/{panel_id}), this is the most user-friendly option, as this panel id could be stable over time, adjustment the panel options is done on the server. For maximum flexibility and the most stable client config, use a panel-of-panels option, which allows composition of other panels. Configuration of the panel is very much analogue to the current client configuration options: image

  3. Users that do not wish to have an account (zero of a few users), for some reason, could opt to create their custom POST url, based on the API docs: https://inkycal.robertsirre.nl/swagger/index.html

Advantages of having a (Inkycal-)server-specific client config:

When having a dedicated client config requires having a dedicated module, this module could just inherit from the Image module and have different Init arguments:

Conclusion: this may be an interesting option: simplicity of configuration is always nice.

Possible advantages of having a (Inkycal-)server specific client module:

I think checking the necessity for resizing could already be achieved, as the log say:

...
inkycal has been running without any errors for 7 display updates
Programm started 4 hours ago
10 Minutes left until next refresh
Generating images for all modules...
INFO:inkycal_image:image size: 528 x 836 px
DEBUG:inkycal_image:identified url
DEBUG:inkycal_image:('image-width:', 528)
DEBUG:inkycal_image:('image-height:', 880)
DEBUG:inkycal_image:('resizing width from', 528, 'to') //<-- why resize, it's already ok
DEBUG:inkycal_image:528
Printing t0 bwr
OK
...

So in the case it already detects the image has the correct dimensions and could (maybe already does) determine resizing is not required.

For image quantizing: I don't know how costly it is, but I think is runs just fine now on the client. The regular image panels need to do this anyway, so why bother skipping this step on the client?

For processing pre-split images: I don't know how costly it is, but I think is runs just fine now on the client. The regular image panels need to do this anyway, so why bother skipping this step on the client?

Having pre-split image would require

Furthermore: presplitting would create a dependency between the client and the server, if for some reason the client want to send pre-split image differently to the panel, or when a new e-ink panel would support multi-colored images, both server and client needs to be changed. A wrong server-implementation would jeopardize the stability of the client.

Conclusion: I think this is not worth the effort, the assumptions could be detectable (number of colors, image dimensions), making the images split on the server vs on the client does not justify the developer effort.

aceisace commented 3 years ago

@Atrejoe I've pushed a inky_image helper module for working with images and easier manipulating of common things like flipping, loading, resizing, splitting colours etc.

For the inkycal_server module, I tried to understand code logic, i.e. how the get and post requests are formatted and handled:

path = 'https://inkycal.robertsirre.nl/panel/calendar/{model}?width={width}&height={height}'

inkycal_image_path_body = [
    'https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics',
    'https://www.calendarlabs.com/ical-calendar/ics/101/Netherlands_Holidays.ics'
]

im = Image.open(requests.post(path, json=path_body, stream=True).raw)

# then {model} is substituted with the corresponding displays name, {width} and {height} are replaced too (still the case?)
'https://inkycal.robertsirre.nl/panel/calendar/{model}?width={width}&height={height}'

Taking a look at the swagger page shows a several options for get and one for post:

..demos removed
GET  -> ​ ​/panel​/image​/{model}
Returns an image panel with a user-specified url
GET  -> ​/panel​/calendar​/{model}​/url
Returns a calendar panel for a single calendar
GET  -> ​​/panel​/weather​/{model}​/forecast​/{token}​/{city}
Returns a weather forecast panel for a single calendar
POST -> ​​/panel​/calendar​/{model}
Returns a calendar panel for multiple calendars
GET -> ​/panel​/{id}
Returns a panel

So without the demo, there are 5 options, namely:

An inkycal module can have required parameters and optional parameters. In the above URLs, there are 4 parameters meant to be substituted:

Which of these are required and which ones are optional? For example, in the web-ui, this is how I see things:

Inkycal-server: dropdown-menu (required): 5 panels, as shown above optional fields: input field: model input field : token input field: city input field: ID

Ideally it would be nice to have just panel by ID since it's the easiest one to implement, but it seems that adding more panels is not (yet) possible. (only shows Calendar panel as an option).

Could you help clarify a few things? Thanks in advance.

Atrejoe commented 3 years ago

Hi @aceisace ,

Thanks for having a look at the server API.

Before anything else: I recommend using the regular InkyImage module, combined with a user defined panel in InkyCal-server to be obtained by id

so: given a pre-configured panel on InkyCal server (which may contain a combination of calendars weather or future components and can be changed without changing the configuration in the Raspberry PI), with the url of:

https://inkycal.robertsirre.nl/panel/MY-GUID-HERE

the inkycal-config could be:

{
    "model": "epd_7_in_5_v3_colour",
    "update_interval": 20,
    "orientation": 0,
    "info_section": false,
    "info_section_height": null,
    "calibration_hours": [0,12,18],
    "modules": [
        {
            "position": 1,
            "name": "Inkyimage",
            "config": {
                "size": [528,880],
                "path": "https://inkycal.robertsirre.nl/panel/MY-GUID-HERE",
                "rotation": "0",
                "layout": "fill",
                "padding_x": 0,
                "padding_y": 0,
                "fontsize": 12,
                "language": "en",
                "colours": "bwr"
            }
        }
    ]
}

For this a version of the configuration UI cóuld be built, explaining the use of InkyCal server, maybe validating the provided Guid. Hell, maybe in the future we'll do OAuth, where InkyCal web UI pull list possible panels based on the authenticated user in InkyCal server.

There could also be a first advanced scenario: mixing InkyCalender modules, like combining the iCanHazDad jokes module with the same preconfigured Inky-Cal server panel:

{
    "model": "epd_7_in_5_v3",
    "update_interval": 60,
    "orientation": 0,
    "info_section": false,
    "info_section_height": null,
    "calibration_hours": [0,12,18],
    "modules": [
        {
            "position": 1,
            "name": "Jokes",
            "config": {
                "size": [528,440],
                "padding_x": 10,
                "padding_y": 10,
                "fontsize": 12,
                "language": "en"
            }
        },
        {
            "position": 2,
            "name": "Inkyimage",
            "config": {
                "size": [528,440], //height has been cut in half
                "path": "https://inkycal.robertsirre.nl/panel/MY-GUID-HERE&width=528&height=440", //width and height could also have been substituted here
                "rotation": "0",
                "layout": "fill",
                "padding_x": 10,
                "padding_y": 10,
                "fontsize": 12,
                "language": "en"
            }
        }
    ]
}

Another option is not to use a pre-configured panel, but use the anonymous API (so not to have to create an account at InkyCal server).

In this case obtaining the panel can be done using GET or POST, providing a method body in some cases. The image module (that also allows to specify POST options) previously worked fine for this.

Say you want to get a rendered panel with multiple calenders:

url = 'https://inkycal.robertsirre.nl/panel/calendar/{model}?width={width}&height={height}'

body = [
    'https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics',
    'https://www.calendarlabs.com/ical-calendar/ics/101/Netherlands_Holidays.ics'
]

Let's say you want to combine this with ICanHasDad jokes too:

{
    "model": "epd_7_in_5_v3",
    "update_interval": 60,
    "orientation": 0,
    "info_section": false,
    "info_section_height": null,
    "calibration_hours": [0,12,18],
    "modules": [
        {
            "position": 1,
            "name": "Jokes",
            "config": {
                "size": [528,440],
                "padding_x": 10,
                "padding_y": 10,
                "fontsize": 12,
                "language": "en"
            }
        },
        {
            "position": 2,
            "name": "Inkyimage",
            "config": {
                "size": [528,440],
                "path": "https://inkycal.robertsirre.nl/panel/calendar/{model}?width={width}&height={height}", 
                //model, width and height (the variables InkyCal already knows) could also have been substituted here to result in:
                //https://inkycal.robertsirre.nl/panel/calendar/epd_7_in_5_v3?width=528&height=440", 
                // Recently font-size and language also have become common panel attributes. Inkycal-server could be changed to support this too, so they too could be substituted in the panel request.
                // Same goes for InkyImage colours property

                //by providing a body, InkyImage module switches to POST to obtain the image:
                "body" : [
                    "https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics",
                    "https://www.calendarlabs.com/ical-calendar/ics/101/Netherlands_Holidays.ics"
                ]
                "rotation": "0",
                "layout": "fill",
                "padding_x": 10,
                "padding_y": 10,
                "fontsize": 12,
                "language": "en"
            }
        }
    ]
}

What I do not recommend: building configuration UI for specific panels in Inkycal Server in the Web UI: these modules are subject to change and expansion. Supporting them would become a burden. Furthermore, similar to what you are doing, I intend to support drop-in extensions to allow custom modules.

aceisace commented 3 years ago

@Atrejoe I think I get the basic idea of this module now, thanks for all the explanations. So the module can be run on 2 parameters only:

If post data is not empty, split on each comma, convert to list and use POST. else use GET request from custom image module to fetch the image.

In regards to the API URLs, it makes more sense to use width and height instead of the e-paper display name to allow using this like any other inkycal module.

{
            "position": 2,
            "name": "Inkyimage",
            "config": {
                "size": [528,440],
               # "path": "https://inkycal.robertsirre.nl/panel/calendar/{model}?width={width}&height={height}", # use width and height only,
                "path": "https://inkycal.robertsirre.nl/panel/calendar/width={width}&height={height}", # use width and height only,

I'll write up the module with the specified parameters and will adapt it as you see fit : )

aceisace commented 3 years ago

@Atrejoe I drafted the inkycal_server module, this is how it looks like currently:

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
Inkycal-server module for Inkycal Project
by Aterju (https://inkycal.robertsirre.nl/)
Copyright by aceisace
"""

import requests

from inkycal.modules.template import inkycal_module
from inkycal.custom import *

from inkycal.modules.inky_image import Inkyimage as Images

filename = os.path.basename(__file__).split('.py')[0]
logger = logging.getLogger(filename)

class Inkyserver(inkycal_module):
  """Displays an image from URL or local path
  """

  name = "Inykcal Server - fetches an image from Inkycal-server - (https://inkycal.robertsirre.nl/)"

  requires = {

    "path":{
      "label": "Which URL should be used to get the image?"
      },

    "palette": {
      "label":"Which palette should be used to convert the images?",
      "options": ['bw', 'bwr', 'bwy']
      }

    }

  optional = {

    "path_body":{
        "label":"Send this data to the server via POST. Use a comma to "
                "separate multiple items",
        },
    "dither":{
      "label": "Dither images before sending to E-Paper? Default is False.",
      "options": [False, True],
      }

    }

  def __init__(self, config):
    """Initialize module"""

    super().__init__(config)

    config = config['config']

    # required parameters
    for param in self.requires:
      if not param in config:
        raise Exception(f'config is missing {param}')

    # optional parameters
    self.path = config['path']
    self.palette = config['palette']
    self.dither = config['dither']

    # convert path_body to list, if not already
    if config['path_body'] and isinstance(config['path_body'], str):
      self.path_body = config['path_body'].split(',')
    else:
      self.path_body = config['path_body']

    # give an OK message
    print(f'{filename} loaded')

  def generate_image(self):
    """Generate image for this module"""

    # Define new image size with respect to padding
    im_width = int(self.width - (2 * self.padding_left))
    im_height = int(self.height - (2 * self.padding_top))
    im_size = im_width, im_height

    logger.info(f'Image size: {im_size}')

    # replace width and height of url
    print(self.path)
    self.path = self.path.format(width=im_width, height=im_height)
    print(f"modified path: {self.path}")

    # initialize custom image class
    im = Images()

    # when no path_body is provided, use plain GET
    if not self.path_body:

      # use the image at the first index
      im.load(self.path)

    # else use POST request
    else:
      # Get the response image
      response = Image.open(requests.post(
                            self.path, json=self.path_body, stream=True).raw)

      # initialize custom image class with response
      im = Images(response)

    # resize the image to respect padding
    im.resize( width=im_width, height=im_height )

    # convert image to given palette
    im_black, im_colour = im.to_palette(self.palette, dither=self.dither)

    # with the images now send, clear the current image
    im.clear()

    # return images
    return im_black, im_colour

if __name__ == '__main__':
  print(f'running {filename} in standalone/debug mode')

But to get this working with your server, you'd have to make some changes to the API, for example, instead of the model, you could use the palette (bwr, bw, bwy). And the width and height parameter also has to be present.

Omitting the test urls, the URLs for your API may look like this:

..demos removed
GET  -> ​ ​/panel​/image​/width={width}&height={height}&palette={palette}
Returns an image panel with a user-specified url

GET  -> ​/panel​/calendar​/width={width}&height={height}&palette={palette}​/url
Returns a calendar panel for a single calendar

GET  -> ​​/panel​/weather​/width={width}&height={height}&palette={palette}​/forecast​/{token}​/{city}
Returns a weather forecast panel for a single calendar

POST -> ​​/panel​/calendar​/width={width}&height={height}&palette={palette}
Returns a calendar panel for multiple calendars

GET -> ​/panel​/width={width}&height={height}&palette={palette}
Returns a panel

In regards to the weather panel, instead of GET, the POST method seems more suitable to send the api-key and location. As there is no special config for weather, it makes sense to use POST method for that, e.g.:

GET_URL = /panel​/weather​/width={width}&height={height}&palette={palette}​ path_body = ['mysecretapikey', 'Location']

Does this seem suitable for you or do you suggest some changes?

github-actions[bot] commented 1 year ago

Marking this issue as stale due to inactivity

github-actions[bot] commented 1 year ago

Marking this issue as stale due to inactivity