Self-serving app: eet.energy Solar Panel

Hi,

as mentioned in this post (URL), I have set up my Tidbyt to serve some apps locally in my network with the help of my 24/7 server.

There are 2 more apps which can be found here (URL) and here (URL) for reference.

Let’s start with the main question: how can you self-serve apps to your Tidbyt.

First off, you have to install all the necessary tools that you need to develop apps - this can be found here: Tidbyt | Dev

Then you need a server that can run LaunchAgents via a crontab or similar (I’m using a Mac mini as the server and LaunchControl to write and run LaunchAgents on this server). I took some pointers from here (Pixlet install on linux/RaspberryPi). There are a few threads on the community discussion board that talk about setting up bash scripts and crontabs which I read and then came up with the following solution.

You need to write a bash script which serves the app on a regular basis:

#!/bin/bash
while true; do
	python3 /path/to/folder/eet_json_data_write.py 
	pixlet render /path/to/folder/tidbyt/EET_Monitor.star
	pixlet push --api-token=“[YOUR API TOKEN HERE]” /path/to/folder/EET_Monitor.webp
		wait_period=$(($wait_period+30))
		if [ $wait_period -ge 50400 ];then
			break
		else
			sleep 30
		fi
done

First, this script runs a python script that converts the data which is grabbed from the solar panel’s Raspberry Pi from CSV to json. For this to work you need to install the eet.energy SDK (Welcome to solmate-sdk’s documentation! — solmate-sdk stable documentation).

The underlying script (eet_json_data_write.py) looks like this:

import json
import solmate_sdk
import requests

client = solmate_sdk.SolMateAPIClient(“[ADD API HERE]")
client.quickstart()
keys = client.get_live_values().keys()

# This is the same path to your URL file that you will need in the .star script - see below
path = ‘/path/to/folder'

# Convert the data to a JSON string
json_data = json.dumps([client.get_live_values()])

# write the live values to the JSON file
with open(f"{path}/{client.serialnum}.json", "w") as jsonfile:
    jsonfile.write(json_data)

The above mentioned bash script code renders the .star file called “EET_Monitor” every 30 seconds for 14 hours (10800 / 30+30 / 60 = 14). I named the script “eet_render.sh” and saved it in a specific folder (same as the .star and .webp files).

Now this script needs to be started at a certain time automatically by the server in order to show up on your Tidbyt. This can be done with a crontab or a LaunchAgent.

So I set up a LaunchAgent which looked like this: (Never mind the red label on the top)

If you prefer the XML code:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>EnvironmentVariables</key>
	<dict>
		<key>PATH</key>
		<string>/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin</string>
	</dict>
	<key>KeepAlive</key>
	<false/>
	<key>Label</key>
	<string>com.localhost.eet.push</string>
	<key>ProgramArguments</key>
	<array>
		<string>/bin/sh</string>
		<string>/path/to/folder/eet_render.sh</string>
	</array>
	<key>StartCalendarInterval</key>
	<dict>
		<key>Hour</key>
		<integer>6</integer>
		<key>Minute</key>
		<integer>30</integer>
	</dict>
</dict>
</plist>

This LaunchAgent starts every day at 6:30am and starts the eet_render.sh script which then runs for 14 hours and stops automatically.

Of course, the PATH key might be different for you depending on your setup (my installations are all standard, so if you haven’t tinkered with the setup it should work).

There is one small quirk: as I’m rendering several apps they sometimes “conflict”, meaning that they override each other leading to only short times on the screen of the Tidbyt. I haven’t figured out how to solve that but it’s basically only a nuisance not a showstopper.

And now for the pixlet code. As mentioned, for this code I have used some code from marcusb’s SolarEdge montiro which can be found on Github (community/apps/solaredgemonitor at main · tidbyt/community · GitHubl) to get started with the coding and help me with the rendering.

Note that you have to have the json data somewhere on a webserver (can be your local server of course).

See screenshot at the end of the post.

"""
Applet: EET.energy Monitor
Summary: PV system monitor
Description: Energy production and consumption monitor for your EET.energy solar panels.
Author: Joerg (based on marcusb's SolarEdge monitor) using ChatGPT for some Python elements
"""

load("cache.star", "cache")
load("encoding/base64.star", "base64")
load("encoding/json.star", "json")
load("encoding/csv.star", "csv")
load("http.star", "http")
load("humanize.star", "humanize")
load("render.star", "render")
load("schema.star", "schema")

url = “[ADD the path and file to your json data here - needs to be an http URL]"

SOLAR_PANEL = base64.decode("""
iVBORw0KGgoAAAANSUhEUgAAABUAAAAQCAYAAAD52jQlAAAArElEQVQ4ja2T2xGFIAxETxyrcOhM
y7M0xzb2fjA4kYcPrvsFCSzhBExCZDLDAHwuxZ5ovEq+MfIaWkYSqt3ikdLmlkG38dcmx1U9v2h8
721mVeaNRgkwpjCzbytTWABO43i4VDMe44lYqlaS4sb5ttKWiu5faQoL+7ae5pLKd+4ntQVPlCMo
mHpmOcNWLGc7+ES+uFevmELJNcU8ugG+rRKOx9/XoKph40P8rR8wcGBXI4UlEQAAAABJRU5ErkJg
gg==
""")
SOLAR_PANEL_OFF = base64.decode("""
iVBORw0KGgoAAAANSUhEUgAAABUAAAAQCAYAAAD52jQlAAAAd0lEQVQ4je2TwQrAIAxDo+y7e+iP
d5cJtY0iustgOWklsb4i8OsTKqxoZrZkLoX6r5FBRAAAqkrX7XIWXFmX3rijFDqTiEBVuz1D1bW+
yjKFBASJqX96ZDiqRbbVH5yyTKGrilxbzaOrwLtdAs+gdgdEAwcf4lg3XnREWPIOZLAAAAAASUVO
RK5CYII=
""")
#Battery above 63%
BAT3 = base64.decode("""
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAABAAAAEAAAAAA0VXHyAAAAW0lEQVQ4EWNgGGjAiMsB/4EAWY4RCJD5MDaGILJGmCZsYjADMGhkxeiS2ORY0BXB+EobfVC8cM9/C4ZrQWqZYBrIpUcNGAyBiBF72BILTBE2OawJCZtCmCFUpwEq0yQFPkZb0AAAAABJRU5ErkJggg==
""")
#Battery above 38% but below 63%
BAT2 = base64.decode("""
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAABAAAAEAAAAAA0VXHyAAAAXElEQVQ4EWNgGGjAiMsB/4EAWY4RCJD5MDaGILJGmCZsYjADMGhkxeiS2OSY0BWRyh94A1hwOfn/JwbUWOBjwAhwkN6B98LAuwAjDLElFpgibHJYYwGbQpghVKcBTSIjBeToJT4AAAAASUVORK5CYII=
""")
#Battery below 38%
BAT1 = base64.decode("""
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAABAAAAEAAAAAA0VXHyAAAAXElEQVQ4EWNgGGjAiMsB/4EAWY4RCJD5MDaGILJGmCZsYjADMGhkxeiS2OSY0BWRyh81gIGBBVegvZVRQUkHwk/uYEQ5SO/AByKGD7AlFpgibHJYwwCbQpghVKcBFN4kBa7idPkAAAAASUVORK5CYII=
""")
HOUSE = base64.decode("""
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAb0lEQVQ4jcWQQQrAMAgE19KX9dL/
v2h7arDRFT1VCIHEGVYBUbxu+ntUCybnkgBPJDu83jsSBbckGUyC7yklOnYUaEkSWwlcPwHQPGxm
ls2fJQj9anmVAADOTlOVLhXsQJXuUB/d+l/w2UE1q/p7AIUnlBV3qmXkAAAAAElFTkSuQmCC
""")
DOTS_LTR = base64.decode("""
R0lGODlhCgAFAIABAA31VAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJDwABACwAAAAACgAFAAAC
CYyPmWAc7pRMBQAh+QQJDwABACwAAAAACgAFAAACCIyPqWAcrmIsACH5BAkPAAEALAAAAAAKAAUA
AAIIjI+pAda8oioAOw==
""")
DOTS_RTL = base64.decode("""
R0lGODlhCgAFAIABAP/RGwAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJDwABACwAAAAACgAFAAAC
CIyPqQHWvKIqACH5BAkPAAEALAAAAAAKAAUAAAIIjI+pYByuYiwAIfkECQ8AAQAsAAAAAAoABQAA
AgmMj5lgHO6UTAUAOw==
""")

def main(config):
    response = http.get(url)
    alldata = response.json()
    for datapoints in alldata:
        pvpower = datapoints['pv_power']
        inject = datapoints['inject_power']
        battery_state = datapoints['battery_state']
        battery_flow = datapoints['battery_flow']
        temp = datapoints['temperature']
        timestamp = datapoints['timestamp']
    conv_batterystate = float(battery_state) * 100
 
    if battery_state > 0.8:
        BATT = BAT3
    else:
        if battery_state < 0.4:
            BATT = BAT1
        else:
            BATT = BAT2
    
    points = []
    flows = []
    img = SOLAR_PANEL_OFF if pvpower == 0 else SOLAR_PANEL
    points.append((img, pvpower, "W"))
    if pvpower > 20:
        dir = 1
    else:
        dir = 0
    flows.append(dir)
    
    points.append((HOUSE, inject, "W"))
    if battery_flow > 0:
        if battery_state < 1:
            dir = 1
        else:
            dir = -1
    else:
        if battery_state > 0.4:
            dir = -1
        else:
            dir = 0
    flows.append(dir)
    
    points.append((BATT, conv_batterystate, "%"))
    
    columns = []
    for p in points:
        img, power, unit = p
        columns.append(
            render.Column(
                main_align = "space_between",
                cross_align = "center",
                children = [
                    render.Image(src = img),
                    render.Text(
                        content = format_power(power),
                        height = 8,
                        font = "tb-8",
                        color = "#ffd11a",
                    ),
                    render.Text(
                        content = unit,
                        height = 8,
                        font = "tb-8",
                        color = "#ffd11a",
                    ),
                ],
            ),
        )
    dots = [render.Box(width = 18)]
    for dir in flows:
        if dir:
            el = render.Image(src = DOTS_LTR if dir == 1 else DOTS_RTL)
        else:
            el = render.Box(width = 10)
        dots.append(
            render.Stack(children = [render.Box(width = 23), el]),
        )
    return render.Root(
        child = render.Stack(
            children = [
                render.Row(
                    expanded = True,
                    main_align = "space_between",
                    children = columns,
                ),
                render.Column(
                    children = [
                        render.Box(height = 8),
                        render.Row(expanded = True, children = dots),
                    ],
                ),
            ],
        ),
    )

def format_power(p):
    if p:
        return humanize.float("#,###.", p)
    else:
        return "0"

Screenshot should be self-explanatory. The solar panel is greyed out when there is no sun and/or the panel doesn’t deliver any power.
The battery icon changes to red underneath 40% and to green above 80%.