Tidbyt Retro Display - sending a message in an image

Hi All,

I own a Tidbyt display - https://tidbyt.com/
There is a dev platform and an api. I decided to go with the api. It basically accepts a post request with an image (it can be animated).

I wanted a way to send a message to it. So some googling later… I came up with a local API, to use the Tidbyt API, from home assistant to send a message. Mind you I am NOT a developer, my code is based off knowledge of python and other examples online. I did it simply because I have never built an API before and I LOVE tinkering and finding unnecessary projects for myself.

It would be a heck of a lot easier if the Tidbyt API had an improvement to do what my api is doing or BETTER YET… home assistant integration (others request) that allowed sending both an image and text :wink:

My Python script does this:

  • Flask API to accept a POST call with 2 parameters: Text and Color
  • Logic to figure out the size of the text, text wrapping, determining if color is valid and assigning a default value if not
  • PIL library used to build the image based on the text and color from API with a black background
  • Base 64 encoded
  • Sent off to Tidbyt API on Flask API POST

Config in home assistant:

  tidbyt:
    url: http://[LOCAL IP HERE]:5000/msg?text={{text}}&color={{color}}
    method: POST

Script in home assistant:

service: rest_command.tidbyt
data:
  text: '{{ states(''input_text.tidbyt_text'') }}'
  color: '{{ states(''input_select.tidbyt_color'') }}'

2022-03-21 13_18_49-Window

Obviously the heart of the operation is my python code. Keep in mind this is running on my windows machine that stays on, just as a POC. I figured I would post it here and maybe it would encourage a proper solution.

I do not provide an installation ID in my code, so all messages are just temporary messages that are temporarily inserted into the cycle and disappear. But they can be inserted into the cycle of applets being shown on the device.

from flask import Flask, abort, jsonify, request, make_response, url_for

from PIL import Image, ImageDraw, ImageFont
from string import ascii_letters
import textwrap

from colour import Color

import base64
from io import BytesIO

import http.client
 
def pil_base64(image):
  img_buffer = BytesIO()
  image.save(img_buffer, format='gif')
  byte_data = img_buffer.getvalue()
  base64_str = base64.b64encode(byte_data)
  return base64_str

def determine_text_size(text: str):
    if len(text) < 5:
        size=20
    elif len(text) < 6:
        size=17
    elif len(text) < 7:
        size=15
    elif len(text) < 8:
        size=12
    elif len(text) < 9:
        size=11
    elif len(text) < 10:
        size=10
    elif len(text) < 12: 
        size=9
    elif len(text) < 14:
        size=15
    elif len(text) < 16:
        size=12
    elif len(text) < 18:
        size=11
    elif len(text) < 20:
        size=10
    elif len(text) <= 34 :
        size=9
    elif len(text) > 34:
        size=14
    return size

def check_color(color):
    if color is None:
        color='red'
    elif color =='':
        color='red'

    try:
        # Converting 'deep sky blue' to 'deepskyblue'
        color = color.replace(" ", "")
        Color(color)
        # if everything goes fine then return True
        return color
    except ValueError: # The color code was not found
        return "red"

def create_image(text: str, size: int, color: str):    
    # Create Image
    img = Image.new('RGB', (64, 32), color = 'black')
    # Load custom font
    font = ImageFont.truetype(font='SourceCodePro-Bold.ttf', size=size)
    # Create DrawText object
    draw = ImageDraw.Draw(im=img)    
    # Calculate the average length of a single character of our font.
    avg_char_width = sum(font.getsize(char)[0] for char in ascii_letters) / len(ascii_letters)
    # Translate this average length into a character count
    max_char_count = int(img.size[0] / avg_char_width)
    # Create a wrapped text object using scaled character count
    text = textwrap.fill(text=text, width=max_char_count)
    ## Add text to the image
    draw.text(xy=(img.size[0]/2, img.size[1] / 2 - .5), text=text, font=font, fill= color, anchor='mm')
    return img

app = Flask(__name__)


@app.errorhandler(400)
def bad_request(error):
    return make_response(jsonify( { 'error': 'Bad request' } ), 400)

@app.errorhandler(404)
def not_found(error):
    return make_response(jsonify( { 'error': 'Not found' } ), 404)
    
@app.route('/msg', methods = ['POST'])
def data():

    text = request.args.get('text')
    if len(text) > 34:
        text='TOO BIG'
    size=determine_text_size(text)
    color = request.args.get('color')
    color = check_color(color)
    img =  create_image(text, size, color) 
    #img.show()
    
    #encode image
    image_base64=pil_base64(img)
    image_encoded=image_base64.decode('utf-8')
    
    #API: token 
    headers = {
        'Content-Type': "application/json",
        'Authorization': "Bearer [TOKEN HERE]"
        }

    #API: Image-base64 encoded, installationID if you want to save applet, background deploy
    payload = "{\n\"image\":\"" + image_encoded +"\",\n  \"installationID\": \"\",\n  \"background\": false\n}"

    #API: Set address
    conn = http.client.HTTPSConnection("api.tidbyt.com")
    #API: Post to Tidbyt API DeviceID, payload and header
    conn.request("POST", "/v0/devices/[DEVICEID HERE]/push", payload, headers)

    #API: Response captured    
    res = conn.getresponse()
    data = res.read()

    #Local API Response        
    return jsonify({"color":color,"size":size,"Text":text,"Tidbyt-reply":data.decode("utf-8")})

if __name__ == '__main__':
    app.run('[LOCAL IP HERE]',debug=True)

4 Likes

@systemsNinja you should package this and get it to be installable via HACS

5 Likes