Berry extension manager (#23940)
This commit is contained in:
parent
9d39901967
commit
c95063a56b
@ -34,6 +34,7 @@ be_extern_native_module(re);
|
||||
be_extern_native_module(mqtt);
|
||||
be_extern_native_module(persist);
|
||||
be_extern_native_module(autoconf);
|
||||
be_extern_native_module(extension_manager);
|
||||
be_extern_native_module(tapp);
|
||||
be_extern_native_module(light);
|
||||
be_extern_native_module(gpio);
|
||||
@ -153,6 +154,9 @@ BERRY_LOCAL const bntvmodule_t* const be_module_table[] = {
|
||||
#ifdef USE_AUTOCONF
|
||||
&be_native_module(autoconf),
|
||||
#endif // USE_AUTOCONF
|
||||
#ifdef USE_EXTENSION_MANAGER
|
||||
&be_native_module(extension_manager),
|
||||
#endif // USE_EXTENSION_MANAGER
|
||||
&be_native_module(tapp),
|
||||
&be_native_module(gpio),
|
||||
#ifdef USE_DISPLAY
|
||||
|
||||
@ -13,7 +13,7 @@ import sys
|
||||
sys.path().push('src/embedded') # allow to import from src/embedded
|
||||
|
||||
# globals that need to exist to make compilation succeed
|
||||
var globs = "path,ctypes_bytes_dyn,tasmota,ccronexpr,gpio,light,webclient,load,MD5,lv,light_state,udp,tcpserver,log,"
|
||||
var globs = "path,ctypes_bytes_dyn,tasmota,ccronexpr,gpio,light,webclient,load,MD5,lv,light_state,udp,tcpserver,log,sortedmap,"
|
||||
"lv_clock,lv_clock_icon,lv_signal_arcs,lv_signal_bars,lv_wifi_arcs_icon,lv_wifi_arcs,"
|
||||
"lv_wifi_bars_icon,lv_wifi_bars,"
|
||||
"_lvgl,"
|
||||
|
||||
7
lib/libesp32/berry_tasmota/src/be_extensions_lib.c
Normal file
7
lib/libesp32/berry_tasmota/src/be_extensions_lib.c
Normal file
@ -0,0 +1,7 @@
|
||||
/********************************************************************
|
||||
* Tasmota lib
|
||||
*
|
||||
* To use: `import autoconf`
|
||||
*
|
||||
*******************************************************************/
|
||||
#include "solidify/solidified_extension_manager.h"
|
||||
@ -90,6 +90,7 @@ class be_class_tasmota (scope: global, name: Tasmota) {
|
||||
_crons, var // list of active crons
|
||||
_ccmd, var // list of active Tasmota commands implemented in Berry
|
||||
_drivers, var // list of active drivers
|
||||
_ext, var // list of active extensions
|
||||
_wnu, var // list of closures to call when network is connected
|
||||
wire1, var // Tasmota I2C Wire1
|
||||
wire2, var // Tasmota I2C Wire2
|
||||
@ -189,6 +190,9 @@ class be_class_tasmota (scope: global, name: Tasmota) {
|
||||
run_network_up, closure(class_Tasmota_run_network_up_closure)
|
||||
add_driver, closure(class_Tasmota_add_driver_closure)
|
||||
remove_driver, closure(class_Tasmota_remove_driver_closure)
|
||||
add_extension, closure(class_Tasmota_add_extension_closure)
|
||||
read_extension_manifest, closure(class_Tasmota_read_extension_manifest_closure)
|
||||
unload_extension, closure(class_Tasmota_unload_extension_closure)
|
||||
load, closure(class_Tasmota_load_closure)
|
||||
compile, closure(class_Tasmota_compile_closure)
|
||||
wire_scan, closure(class_Tasmota_wire_scan_closure)
|
||||
|
||||
644
lib/libesp32/berry_tasmota/src/embedded/extension_manager.be
Normal file
644
lib/libesp32/berry_tasmota/src/embedded/extension_manager.be
Normal file
@ -0,0 +1,644 @@
|
||||
# extensions manager module for Berry
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
var extension_manager = module("extension_manager")
|
||||
|
||||
#@ solidify:extension_manager
|
||||
class Extension_manager
|
||||
static var EXT_FOLDER = "/.extensions/"
|
||||
static var EXT_REPO = "https://ota.tasmota.com/extensions/"
|
||||
static var EXT_REPO_MANIFEST = "extensions.jsonl"
|
||||
static var EXT_REPO_FOLDER = "tapp/"
|
||||
|
||||
#####################################################################################################
|
||||
# init - constructor
|
||||
#
|
||||
# Register as driver
|
||||
#
|
||||
def init()
|
||||
tasmota.add_driver(self)
|
||||
end
|
||||
|
||||
#####################################################################################################
|
||||
# General static helper functions
|
||||
#####################################################################################################
|
||||
#####################################################################################################
|
||||
# version_string(v)
|
||||
#
|
||||
# Convert 32 bits version to "a.b.c.d" where version is 0xAABBCCDD
|
||||
#
|
||||
# @param v: int - version in format 0xAABBCCDD
|
||||
# @return string - string in format "a.b.c.d"
|
||||
#####################################################################################################
|
||||
static def version_string(v)
|
||||
return f"v{(v >> 24) & 0xFF}.{(v >> 16) & 0xFF}.{(v >> 8) & 0xFF}.{v & 0xFF}"
|
||||
end
|
||||
|
||||
#####################################################################################################
|
||||
# tapp_name(wd)
|
||||
#
|
||||
# Takes a working directory 'wd' and extract the name of the tapp file.
|
||||
# Ex: '/.extensions/Leds_Panel.tapp#' becomes 'Leds_Panel.tapp'
|
||||
#
|
||||
# @param wv: string - the Working Dir of the tapp file like '/.extensions/Leds_Panel.tapp#'
|
||||
# @return string - the raw name of the tapp file, like 'Leds_Panel.tapp'
|
||||
#####################################################################################################
|
||||
static def tapp_name(wd)
|
||||
import string
|
||||
|
||||
var from = 0
|
||||
var to = size(wd) - 1
|
||||
# if ends with '#' or '_' remove last char
|
||||
if (wd[to] == "#") || (wd[to] == "_")
|
||||
to -= 1
|
||||
end
|
||||
# remove suffix .tapp
|
||||
if string.find(wd, ".tapp", to + 1 - size(".tapp")) >= 0
|
||||
to -= size(".tapp")
|
||||
end
|
||||
# remove anything before the last '/'
|
||||
var idx
|
||||
while (idx := string.find(wd, '/', from)) >= 0
|
||||
from = idx + 1
|
||||
end
|
||||
# final result
|
||||
return wd[from .. to]
|
||||
end
|
||||
|
||||
#####################################################################################################
|
||||
# General functions to get information about tapp files (installed or from store)
|
||||
#####################################################################################################
|
||||
#####################################################################################################
|
||||
# manifest_decode()
|
||||
#
|
||||
# Decode the manifest entry from JSONL (single line) into a validated format
|
||||
# or return 'nil' if something major went wrong (and logged)
|
||||
#
|
||||
# @param json_line: string - single JSONL entry, like '{"name":"Leds Panel","file":"Leds_Panel.tapp","version":"0x19090100","description":"[...]","author":"[...]"}
|
||||
# @return map or nil: normalized map or 'nil' if something went wrong
|
||||
# guaranteed fileds: 'name', 'description', 'file', 'version', 'author'
|
||||
static def manifest_decode(json_line)
|
||||
import json
|
||||
|
||||
# try to parse json
|
||||
var entry = json.load(json_line)
|
||||
if (entry == nil)
|
||||
log(f"EXT: unable to parse manifest line '{json_line}'", 3)
|
||||
return nil
|
||||
end
|
||||
|
||||
# check for mandatory fields
|
||||
if !entry.contains('name') || !entry.contains('file') || !entry.contains('version')
|
||||
log(f"EXT: manifest is missing 'name/file/version' in map '{entry}'")
|
||||
return nil
|
||||
end
|
||||
|
||||
# build result with mandatory fields
|
||||
var result = {
|
||||
'name': entry['name'],
|
||||
'file': entry['file'],
|
||||
'version': int(entry['version'])
|
||||
}
|
||||
# add non-critical fields
|
||||
result['description'] = entry.find('description', "[no description]")
|
||||
result['author'] = entry.find('author', "")
|
||||
return result
|
||||
end
|
||||
|
||||
#####################################################################################################
|
||||
# list_installed_ext()
|
||||
#
|
||||
# Return a map of installed tap files, by tapp file name
|
||||
# tapp_name -> path {'Leds_Panel.tapp': '/.extensions/Leds_Panel.tapp'}
|
||||
#
|
||||
# @return map: tapp_namt -> full path (or wd)
|
||||
static def list_installed_ext()
|
||||
# Read extensions in file system
|
||||
var installed_ext = {}
|
||||
|
||||
for ext_path: _class.list_extensions()
|
||||
installed_ext[_class.tapp_name(ext_path)] = ext_path
|
||||
end
|
||||
# log(f"EXT: list_installed_ext={installed_ext}")
|
||||
return installed_ext
|
||||
end
|
||||
|
||||
#####################################################################################################
|
||||
# list_extensions_in_fs()
|
||||
#
|
||||
# List all extensions in file-system, whether they are running or not
|
||||
#
|
||||
# @return sortedmap: with Name of App as key, and following map:
|
||||
# name, description, version (int), autorun (bool)
|
||||
static def list_extensions_in_fs()
|
||||
import string
|
||||
var sm = sortedmap()
|
||||
for ext: _class.list_extensions()
|
||||
var details = tasmota.read_extension_manifest(ext)
|
||||
if (details != nil)
|
||||
var name = details.find("name")
|
||||
if (name)
|
||||
sm[name] = ext
|
||||
end
|
||||
else
|
||||
log(f"EXT: unable to read details from '{ext}'", 3)
|
||||
end
|
||||
end
|
||||
return sm
|
||||
end
|
||||
|
||||
#####################################################################################################
|
||||
# list_extensions()
|
||||
|
||||
# Returns the list of enabled and disabled extensions
|
||||
# i.e. scan files in ".extensions" folder and keep files ending with ".tapp" and ".tapp_"
|
||||
#
|
||||
# @return list: list of extensions by path or by instance
|
||||
static def list_extensions()
|
||||
import path
|
||||
import string
|
||||
var l = []
|
||||
# read from fs
|
||||
for d: path.listdir(_class.EXT_FOLDER)
|
||||
if string.endswith(d, ".tapp") || string.endswith(d, ".tapp_")
|
||||
l.push(_class.EXT_FOLDER + d)
|
||||
end
|
||||
end
|
||||
# finish
|
||||
return l
|
||||
end
|
||||
|
||||
#####################################################################################################
|
||||
# Methods to install / upgrade / delete extensions from the store
|
||||
#####################################################################################################
|
||||
#####################################################################################################
|
||||
# install_from_store(tapp_fname)
|
||||
#
|
||||
# @param tapp_fname : string - name of tapp file to install from repository
|
||||
def install_from_store(tapp_fname)
|
||||
import string
|
||||
# sanitize
|
||||
tapp_fname = self.tapp_name(tapp_fname)
|
||||
# add '.tapp' extension if it is not present
|
||||
if !string.endswith(tapp_fname, ".tapp")
|
||||
tapp_fname += '.tapp'
|
||||
end
|
||||
# full url
|
||||
var ext_url = f"{self.EXT_REPO}{self.EXT_REPO_FOLDER}{tapp_fname}"
|
||||
log(f"EXT: installing from '{ext_url}'", 3)
|
||||
# load from web
|
||||
try
|
||||
var local_file = f"{self.EXT_FOLDER}{tapp_fname}"
|
||||
var cl = webclient()
|
||||
cl.begin(ext_url)
|
||||
var r = cl.GET()
|
||||
if r != 200
|
||||
log(f"EXT: return_code={r}", 2)
|
||||
return
|
||||
end
|
||||
cl.write_file(local_file)
|
||||
cl.close()
|
||||
except .. as e, m
|
||||
tasmota.log(format("CFG: exception '%s' - '%s'", e, m), 2)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
#####################################################################################################
|
||||
# Init web handlers
|
||||
#####################################################################################################
|
||||
# Displays a "Autoconf" button on the configuration page
|
||||
def web_add_button()
|
||||
import webserver
|
||||
webserver.content_send(
|
||||
"<p></p><form id=but_part_mgr style='display: block;' action='ext' method='get'><button>Extension Manager</button></form><p></p>")
|
||||
end
|
||||
|
||||
#####################################################################################################
|
||||
# This HTTP GET manager controls which web controls are displayed
|
||||
#####################################################################################################
|
||||
def page_extensions_mgr_dispatcher()
|
||||
import webserver
|
||||
if !webserver.check_privileged_access() return nil end
|
||||
|
||||
if (webserver.has_arg("store"))
|
||||
return self.page_extensions_store()
|
||||
else
|
||||
return self.page_extensions_mgr()
|
||||
end
|
||||
end
|
||||
|
||||
#####################################################################################################
|
||||
# This HTTP GET manager controls which web controls are displayed
|
||||
#####################################################################################################
|
||||
def page_extensions_mgr()
|
||||
import webserver
|
||||
import string
|
||||
|
||||
webserver.content_start('Extensions Manager')
|
||||
webserver.content_send_style()
|
||||
# webserver.content_send("<p><small> (This feature requires an internet connection)</small></p>")
|
||||
|
||||
webserver.content_send("<div style='padding:0px 5px;text-align:center;'><h3><hr>Extension Manager<hr></h3></div>")
|
||||
|
||||
webserver.content_send("<script>"
|
||||
"function loadext() {"
|
||||
"eb('store').disabled=true;"
|
||||
# "eb('store').innerHTML = '[ <span style=\"color:var(--c_btnsv);\">Loading from Store...</span> ]';"
|
||||
"x=new XMLHttpRequest();"
|
||||
"x.timeout=4000;"
|
||||
"x.onreadystatechange = () => {"
|
||||
"if(x.readyState==4){"
|
||||
"if(x.status==200){"
|
||||
"eb('inet').style.display='none';"
|
||||
"eb('store').outerHTML=x.responseText;"
|
||||
# "}else{"
|
||||
# "eb('store').innerHTML='<b>[ <span style=\"color:var(--c_btnrsthvr);\">Error loading manifest.</span> ]</b>';"
|
||||
"}"
|
||||
"}"
|
||||
"};"
|
||||
"x.open('GET','?store=');"
|
||||
"x.send();"
|
||||
"}"
|
||||
"window.onload=function(){"
|
||||
"loadext();"
|
||||
"};"
|
||||
|
||||
# scripts for Store
|
||||
"function toggleDesc(id) {"
|
||||
"var desc = document.getElementById('desc-' + id);"
|
||||
"var arrow = document.getElementById('arrow-' + id);"
|
||||
"if (desc.style.display === 'none' || desc.style.display === '') {"
|
||||
"desc.style.display = 'block';"
|
||||
"arrow.innerHTML = '▼';"
|
||||
"} else {"
|
||||
"desc.style.display = 'none';"
|
||||
"arrow.innerHTML = '▶';"
|
||||
"}"
|
||||
"}"
|
||||
# Simple filtering functions (optional enhancement)
|
||||
"function filterExtensions(query) {"
|
||||
"var items = document.getElementsByClassName('ext-store-item');"
|
||||
"query = query.toLowerCase();"
|
||||
"for (var i = 0; i < items.length; i++) {"
|
||||
"var name = items[i].getElementsByClassName('ext-name')[0].textContent.toLowerCase();"
|
||||
"var desc = items[i].getElementsByClassName('ext-desc')[0].textContent.toLowerCase();"
|
||||
"if (name.includes(query) || desc.includes(query)) {"
|
||||
"items[i].style.display = 'block';"
|
||||
"} else {"
|
||||
"items[i].style.display = 'none';"
|
||||
"}"
|
||||
"}"
|
||||
"}"
|
||||
"</script>"
|
||||
)
|
||||
|
||||
webserver.content_send("<fieldset style='padding:0 5px;'>"
|
||||
"<style>"
|
||||
# Fix for small text - the key is width: min-content on parent */
|
||||
".ext-item{width:min-content;min-width:100%;}"
|
||||
".ext-item small{display:block;word-wrap:break-word;overflow-wrap:break-word;white-space:normal;padding-right:5px;padding-top:2px;}"
|
||||
# Control bar styles
|
||||
".ext-controls{display:flex;gap:8px;align-items:center;margin-top:4px;padding:0px}"
|
||||
# Small action buttons
|
||||
".btn-small{padding:0 6px;line-height:1.8rem;font-size:0.9rem;min-width:auto;width:auto;flex-shrink:0;}"
|
||||
# form
|
||||
"form{padding-top:0px;padding-bottom:0px;}"
|
||||
# Running indicator
|
||||
".running-indicator{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:8px;background:var(--c_btnsvhvr);animation:pulse 1.5s infinite;}"
|
||||
"@keyframes pulse{0%{opacity:1;}50%{opacity:0.5;}100%{opacity:1;}}"
|
||||
|
||||
# for store
|
||||
|
||||
# /* Extension Store specific styles */
|
||||
".store-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;}"
|
||||
".store-stats{font-size:0.9em;color:var(--c_in);}"
|
||||
".ext-store-item{background:var(--c_bg);border-radius:0.3em;margin-bottom:5px;padding:4px;}"
|
||||
".ext-header{display:flex;justify-content:space-between;align-items:center;cursor:pointer;user-select:none;padding:5px;}"
|
||||
".ext-title{display:flex;align-items:center;gap:6px;flex:1;padding:0;}"
|
||||
".ext-name{font-weight:bold;}"
|
||||
".ext-version{font-size:0.8em;}"
|
||||
".ext-arrow{color:var(--c_in);font-size:0.8em;}"
|
||||
".ext-badges{padding:0;}"
|
||||
# ".ext-badges{margin-left:auto;gap:8px;align-items:center;}"
|
||||
".ext-details{width:min-content;min-width:100%;padding:0;display:none;}"
|
||||
".ext-desc{color:var(--c_in);font-size:0.8em;line-height:1.4;display:block;word-wrap:break-word;overflow-wrap:break-word;white-space:normal;padding:0 5px;}"
|
||||
|
||||
".ext-actions{display:flex;gap:8px;padding:5px;}"
|
||||
".btn-action{padding:0 12px;line-height:1.8em;font-size:0.9em;flex:1;}"
|
||||
".installed-badge{border-color:var(--c_btnhvr);padding:0px 4px;border-radius:4px;font-size:0.7em;border-width:2px;border-style:solid;margin-right:3px;}"
|
||||
# ".installed-badge{background:var(--c_btnsv);padding:2px 6px;border-radius:3px;font-size:0.7em;}"
|
||||
".update-badge{background:var(--c_btnhvr);padding:2px 6px;border-radius:4px;font-size:0.7em;margin-right:3px;animation:pulse 2s infinite;}"
|
||||
"@keyframes pulse{0%{opacity:1;}50%{opacity:0.7;}100%{opacity:1;}}"
|
||||
# ".category-filter{display:flex;gap:5px;margin-bottom:15px;overflow-x:auto;padding:5px 0;}"
|
||||
# ".cat-btn{padding:5px 12px;background:#3a3a3a;border:1px solid var(--c_frm);border-radius:15px;color:#aaa;font-size:0.9em;white-space:nowrap;cursor:pointer;transition:all 0.2s;}"
|
||||
# ".cat-btn:hover{background:#4a4a4a;color:var(--c_btntxt);}"
|
||||
# ".cat-btn.active{background:#1fa3ec;color:var(--c_btntxt);border-color:var(--c_btn);}"
|
||||
# ".search-box{width:100%;box-sizing:border-box;padding:8px;border:0;color:var(--c_txt);margin-bottom:10px;border-radius:0.3em;}"
|
||||
# ".search-box::placeholder{color:var(--c_frm);}"
|
||||
|
||||
"</style>"
|
||||
"<legend><b title='Running extensions'> Installed extensions</b></legend>")
|
||||
var installed_ext = self.list_extensions_in_fs(true)
|
||||
if size(installed_ext) > 0
|
||||
var ext_nb = 0
|
||||
while ext_nb < size(installed_ext)
|
||||
if (ext_nb > 0) webserver.content_send("<hr style='margin:2px 0 0 0;'>") end
|
||||
var ext_path = installed_ext.get_by_index(ext_nb) # ex: '/.extensions/Partition_Wizard.tapp'
|
||||
var ext_path_html = webserver.html_escape(ext_path)
|
||||
var details = tasmota.read_extension_manifest(ext_path)
|
||||
# log(f"EXT: {details=}")
|
||||
var running = tasmota._ext ? tasmota._ext.contains(ext_path) : false
|
||||
var running_indicator = running ? " <span class='running-indicator' title='Running'></span>" : ""
|
||||
var autorun = details.find("autorun", false)
|
||||
var back_green = "style='background:var(--c_btnsvhvr);'"
|
||||
var dark_blue = "style='background:var(--c_btnoff);'"
|
||||
webserver.content_send("<div class='ext-item'>")
|
||||
webserver.content_send(f"<span title='path: {ext_path_html}'><b>{webserver.html_escape(details['name'])}</b>{running_indicator}</span><br>")
|
||||
webserver.content_send(f"<small>{webserver.html_escape(details['description'])}</small>")
|
||||
|
||||
webserver.content_send("<div class='ext-controls' style='padding-top:0px;padding-bottom:0px;'>")
|
||||
webserver.content_send("<form action='/ext' method='post' class='ext-controls'>")
|
||||
webserver.content_send(f"<button type='submit' class='btn-small' {running ? back_green :: dark_blue} name='{running ? 's' :: 'r'}{ext_path_html}'>{running ? 'Running' :: 'Stopped'}</button>")
|
||||
webserver.content_send(f"<button type='submit' class='btn-small' {autorun ? '' :: dark_blue} name='{autorun ? 'a' :: 'A'}{ext_path_html}'>Auto-run: {autorun ? 'ON' :: 'OFF'}</button>")
|
||||
webserver.content_send(f"<button type='submit' class='btn-small bred' name='d{ext_path_html}' onclick='return confirm(\"Confirm deletion of {webserver.html_escape(ext_path)}\")'>Uninstall</button>")
|
||||
# webserver.content_send(f"<button type='submit' class='btn-small' style='background-color:var(--c_btnoff);border-color:var(--c_btnrst);border-width:3px;border-style:solid;' name='d{ext_path_html}' onclick='return confirm(\"Confirm deletion of {webserver.html_escape(ext_path)}\")'>Uninstall</button>")
|
||||
webserver.content_send("</form></div></div>")
|
||||
|
||||
ext_nb += 1
|
||||
end
|
||||
else
|
||||
# no installed extensions
|
||||
webserver.content_send("<div><small><i>No installed extension.</i></small></p>")
|
||||
end
|
||||
|
||||
webserver.content_send("<p></p></fieldset><p></p>")
|
||||
|
||||
webserver.content_send("<div style='padding:0px 5px;text-align:center;'><h3><hr>Online Store"
|
||||
"<hr style='margin-bottom:0;'>"
|
||||
"<span id='inet' style='font-size:small;font-weight:normal;''> (This feature requires an internet connection)</span>"
|
||||
"</h3></div>")
|
||||
# "<p><small> (This feature requires an internet connection)</small></p>")
|
||||
|
||||
webserver.content_send("<b id='store'>[ <span style='color:var(--c_btnsv);'>Loading from Store...</span> ]</b>")
|
||||
|
||||
webserver.content_button(webserver.BUTTON_MANAGEMENT) #- button back to management page -#
|
||||
webserver.content_stop()
|
||||
end
|
||||
|
||||
#####################################################################################################
|
||||
# Extension Store
|
||||
#####################################################################################################
|
||||
def page_extensions_store()
|
||||
import webserver
|
||||
import string
|
||||
import json
|
||||
|
||||
webserver.content_open(200, "text/html")
|
||||
# read manifest from ota.tasmota.com
|
||||
var item_jsonl
|
||||
try
|
||||
item_jsonl = self.load_manifest()
|
||||
except .. as e, m
|
||||
webserver.content_send("<b id='store'>[ <span style='color:var(--c_btnrst);'>Error loading manifest.</span> ]</b>")
|
||||
webserver.content_send(f"<p><small>{webserver.html_escape(m)}</small></p>")
|
||||
webserver.content_close()
|
||||
return
|
||||
end
|
||||
var item_json_count = string.count(item_jsonl, '"name":')
|
||||
|
||||
webserver.content_send("<fieldset id='store'>")
|
||||
|
||||
webserver.content_send(f"<div class='store-header'>"
|
||||
"<span>Browse Extensions</span>"
|
||||
"<span class='store-stats'>{item_json_count} available</span>"
|
||||
"</div>")
|
||||
|
||||
webserver.content_send("<input type='text' placeholder='Search extensions...' onkeyup='filterExtensions(this.value)'>"
|
||||
"<p></p>")
|
||||
|
||||
# Read extensions in file system
|
||||
# as a map tapp_name -> wd_path, ex {'Leds_Panel.tapp': '/.extensions/Leds_Panel.tapp'}
|
||||
var installed_ext = self.list_installed_ext()
|
||||
|
||||
# Now parse application manifests
|
||||
var item_idx = 1
|
||||
var json_pos = 0 # starting char to parse JSONL
|
||||
while (json_pos < size(item_jsonl)) # item_idx negative means that we have nothing more to display
|
||||
var lf_pos = string.find(item_jsonl, "\n", json_pos)
|
||||
if (lf_pos < 0) lf_pos = size(item_jsonl) end # go to end of string
|
||||
# extract the json from the line
|
||||
var json_line = item_jsonl[json_pos .. lf_pos]
|
||||
|
||||
# ex: {"name":"Leds Panel","file":"Leds_Panel.tapp","version":"0x19090100","description":"Real-time display of WS2812 LEDs in browser with smooth animations and pattern editor.","author":"Stephan Hadinger"}
|
||||
var entry = self.manifest_decode(json_line)
|
||||
|
||||
if (entry != nil)
|
||||
# entry is guaranteed to have the following fields: 'name', 'description', 'file', 'version', 'author'
|
||||
var app_version = entry['version']
|
||||
var app_version_web = self.version_string(app_version)
|
||||
var app_name_web = webserver.html_escape(entry['name'])
|
||||
var app_file = entry['file']
|
||||
var app_description_web = string.replace(webserver.html_escape(entry['description']), '\\n', '<br>')
|
||||
var app_author = entry['author']
|
||||
|
||||
# now compute the status
|
||||
var installed = false
|
||||
var installed_version
|
||||
var installed_tapp_name
|
||||
installed_tapp_name = self.tapp_name(entry['file'])
|
||||
var installed_tapp_name_web = webserver.html_escape(installed_tapp_name)
|
||||
installed = installed_ext.contains(installed_tapp_name)
|
||||
var installed_path_web
|
||||
if installed
|
||||
var installed_path = installed_ext[installed_tapp_name]
|
||||
installed_path_web = webserver.html_escape(installed_path)
|
||||
var details = tasmota.read_extension_manifest(installed_path)
|
||||
installed_version = int(details.find('version', 0))
|
||||
end
|
||||
# We have 3 options:
|
||||
# - 'installed == false', only button "Install"
|
||||
# - 'installed' and 'installed_version < app_version', buttons "Upgrade" and "Delete"
|
||||
# - else 'installed' and version more recent, 1 button "Delete"
|
||||
var upgrade = installed && (installed_version < app_version)
|
||||
|
||||
webserver.content_send(f"<div class='ext-store-item'>"
|
||||
"<div class='ext-header' onclick='toggleDesc(\"{item_idx}\")'>"
|
||||
"<div class='ext-title'>"
|
||||
"<span class='ext-name'>{app_name_web}</span>"
|
||||
"<span class='ext-version'><small>{self.version_string(app_version)}</small></span>"
|
||||
"</div>")
|
||||
if upgrade
|
||||
webserver.content_send( "<div class='ext-badges'>"
|
||||
"<span class='update-badge'>Upgrade</span>"
|
||||
"</div>")
|
||||
elif installed
|
||||
webserver.content_send( "<div class='ext-badges'>"
|
||||
"<span class='installed-badge'>Installed</span>"
|
||||
"</div>")
|
||||
end
|
||||
webserver.content_send( f"<span id='arrow-{item_idx}' class='ext-arrow'>▶</span>"
|
||||
"</div>"
|
||||
"<div id='desc-{item_idx}' class='ext-details'>"
|
||||
"<div class='ext-desc'>"
|
||||
"{app_description_web}")
|
||||
if upgrade
|
||||
webserver.content_send( f"<br>{self.version_string(installed_version)} → {app_version_web}")
|
||||
end
|
||||
webserver.content_send( "</div>"
|
||||
"<form action='/ext' method='post' class='ext-actions'>"
|
||||
"<div style='width:30%'></div>")
|
||||
if installed
|
||||
if upgrade
|
||||
webserver.content_send( f"<button type='submit' class='btn-action' name='u{installed_path_web}' onclick='return confirm(\"Confirm upgrade of {installed_path_web}\")'>Upgrade</button>")
|
||||
else
|
||||
webserver.content_send( "<button type='submit' class='btn-action' style='visibility:hidden;'></button>")
|
||||
end
|
||||
webserver.content_send( f"<button type='submit' class='btn-action bred' name='d{installed_path_web}' onclick='return confirm(\"Confirm deletion of {installed_path_web}\")'>Uninstall</button>")
|
||||
else
|
||||
webserver.content_send( f"<button type='submit' class='btn-action' style='visibility:hidden;'></button>"
|
||||
"<button type='submit' class='btn-action bgrn' name='i{installed_tapp_name_web}' onclick='return confirm(\"Confirm installation of {app_name_web}\")'>Install</button>")
|
||||
end
|
||||
webserver.content_send( "</form>"
|
||||
"</div>"
|
||||
"</div>")
|
||||
|
||||
item_idx += 1
|
||||
end
|
||||
|
||||
json_pos = lf_pos + 1
|
||||
end
|
||||
|
||||
webserver.content_send("<p></p></fieldset><p></p>")
|
||||
webserver.content_close()
|
||||
end
|
||||
|
||||
#####################################################################################################
|
||||
# Load manifest from ota.tasmota.com
|
||||
#####################################################################################################
|
||||
def load_manifest()
|
||||
try
|
||||
var arch = tasmota.arch() # architecture, ex: "esp32" - not used currently but might be useful
|
||||
var version = f"0x{tasmota.version():08X}"
|
||||
|
||||
var url = f"{self.EXT_REPO}{self.EXT_REPO_MANIFEST}?a={arch}&v={version}"
|
||||
log(f"EXT: fetching extensions manifest '{url}'", 3)
|
||||
# Add architeture and version information
|
||||
# They are not used for now but may be interesting in the future to serve
|
||||
# different content based on architecture (Ex: ESP32) and version (ex: 0x0E060001 for 14.6.0.1)
|
||||
# load the template
|
||||
var cl = webclient()
|
||||
cl.begin(url)
|
||||
var r = cl.GET()
|
||||
if r != 200
|
||||
tasmota.log(f"EXT: error fetching manifest {r}", 2)
|
||||
raise "webclient_error", f"Error fetching manifest code={r}"
|
||||
end
|
||||
var s = cl.get_string()
|
||||
cl.close()
|
||||
return s
|
||||
except .. as e, m
|
||||
tasmota.log(format("EXT: exception '%s' - '%s'", e, m), 2)
|
||||
raise e, m
|
||||
end
|
||||
end
|
||||
|
||||
#####################################################################################################
|
||||
# Web controller
|
||||
#
|
||||
# Applies the changes and restart
|
||||
#####################################################################################################
|
||||
# This HTTP POST manager handles the submitted web form data
|
||||
def page_extensions_ctl()
|
||||
import webserver
|
||||
import path
|
||||
import string
|
||||
if !webserver.check_privileged_access() return nil end
|
||||
|
||||
try
|
||||
# log(f">>> {webserver.arg_name(0)=} {webserver.arg(0)=} {webserver.arg_size()=}")
|
||||
# var redirect_to_store = false # add suffix to redirect to store
|
||||
|
||||
var btn_name = webserver.arg_name(0)
|
||||
var action = btn_name[0] # first character
|
||||
var action_path = btn_name[1..] # remove first character
|
||||
|
||||
if (action == "r") # button "Run"
|
||||
if (action_path != "")
|
||||
# log(f"EXT: run '{action_path}'")
|
||||
tasmota.load(action_path)
|
||||
end
|
||||
elif (action == "s") # button "Stop"
|
||||
# log(f"EXT: stop '{action_path}'")
|
||||
tasmota.unload_extension(action_path)
|
||||
elif (action == "a") || (action == "A") # button "Autorun", "A" enable, "a" disable
|
||||
var new_name
|
||||
if (action == "a") && string.endswith(action_path, ".tapp") # Autorun is enabled, disable it
|
||||
new_name = action_path[0..-5] + "tapp_"
|
||||
elif (action == "A") && string.endswith(action_path, ".tapp_")
|
||||
new_name = action_path[0..-6] + "tapp"
|
||||
end
|
||||
if new_name
|
||||
var success = path.rename(action_path, new_name)
|
||||
# log(f"EXT: rename '{action_path}' to '{new_name} {success=}", 3)
|
||||
if (success) # update any running extension with its new name
|
||||
if tasmota._ext.contains(action_path)
|
||||
tasmota._ext[new_name] = tasmota._ext[action_path]
|
||||
tasmota._ext.remove(action_path)
|
||||
end
|
||||
end
|
||||
else
|
||||
log(f"EXT: wrong action '{btn_name}'", 3)
|
||||
end
|
||||
elif (action == 'd') # button "Delete"
|
||||
if (action_path != "")
|
||||
# first stop if it was running
|
||||
tasmota.unload_extension(action_path)
|
||||
# then delete file
|
||||
var success = path.remove(action_path)
|
||||
# log(f"EXT: delete '{action_path}' {success=}", 3)
|
||||
end
|
||||
|
||||
# Now try the store commands
|
||||
elif (action == 'u') # Upgrade ext
|
||||
# log(f"EXT: upgrade '{action_path}'", 3)
|
||||
# first stop the app if it's running
|
||||
tasmota.unload_extension(action_path)
|
||||
self.install_from_store(self.tapp_name(action_path))
|
||||
# redirect_to_store = true
|
||||
elif (action == 'i') # Install ext
|
||||
# log(f"EXT: install '{action_path}'", 3)
|
||||
self.install_from_store(self.tapp_name(action_path))
|
||||
# redirect_to_store = true
|
||||
end
|
||||
|
||||
# var redirect_suffix = redirect_to_store ? "store=" : ""
|
||||
# webserver.redirect(f"/ext?{redirect_suffix}")
|
||||
webserver.redirect(f"/ext")
|
||||
except .. as e, m
|
||||
log(f"CFG: Exception> '{e}' - {m}", 2)
|
||||
#- display error page -#
|
||||
webserver.content_start("Parameter error") #- title of the web page -#
|
||||
webserver.content_send_style() #- send standard Tasmota styles -#
|
||||
|
||||
webserver.content_send(f"<p style='width:340px;'><b>Exception:</b><br>'{webserver.html_escape(e)}'<br>{webserver.html_escape(m)}</p>")
|
||||
|
||||
webserver.content_button(webserver.BUTTON_CONFIGURATION) #- button back to management page -#
|
||||
webserver.content_stop() #- end of web page -#
|
||||
end
|
||||
end
|
||||
|
||||
# Add HTTP POST and GET handlers
|
||||
def web_add_handler()
|
||||
import webserver
|
||||
webserver.on('/ext', / -> self.page_extensions_mgr_dispatcher(), webserver.HTTP_GET)
|
||||
webserver.on('/ext', / -> self.page_extensions_ctl(), webserver.HTTP_POST)
|
||||
end
|
||||
end
|
||||
|
||||
extension_manager.Extension_manager = Extension_manager
|
||||
extension_manager.init = def (m)
|
||||
return m.Extension_manager() # return an instance of this class
|
||||
end
|
||||
@ -599,8 +599,8 @@ class Tasmota
|
||||
# Ex: f_name = '/app.zip#autoexec'
|
||||
|
||||
# if ends with ".tapp", add "#autoexec"
|
||||
# there's a trick here, since actual prefix may be ".tapp" or "._tapp" (the later for no-autp-run)
|
||||
if string.endswith(f_name, 'tapp')
|
||||
# prefix may be ".tapp" or ".tapp_"
|
||||
if string.endswith(f_name, '.tapp') || string.endswith(f_name, '.tapp_')
|
||||
f_name += "#autoexec"
|
||||
end
|
||||
|
||||
@ -845,6 +845,95 @@ class Tasmota
|
||||
end
|
||||
end
|
||||
|
||||
######################################################################
|
||||
# add_extension
|
||||
#
|
||||
# Add an instance to the dispatchin of Berry events
|
||||
#
|
||||
# Args:
|
||||
# - `d`: instance (or driver)
|
||||
# The events will be dispatched to this instance whenever
|
||||
# it has a method with the same name of the instance
|
||||
# - `ext_path`: the path of the extension, usually a '.tapp' file
|
||||
######################################################################
|
||||
def add_extension(d, ext_path) # add ext
|
||||
if (ext_path == nil)
|
||||
ext_path = tasmota.wd
|
||||
end
|
||||
|
||||
if (type(d) != 'instance') || (type(ext_path) != 'string')
|
||||
raise "value_error", "instance and name required"
|
||||
end
|
||||
if (ext_path != nil)
|
||||
import string
|
||||
# initialize self._ext if it does not exist
|
||||
if self._ext == nil
|
||||
self._ext = sortedmap()
|
||||
end
|
||||
if string.endswith(ext_path, '#')
|
||||
ext_path = ext_path[0..-2] # remove trailing '#''
|
||||
end
|
||||
if self._ext.contains(ext_path)
|
||||
log(f"BRY: Extension '{ext_path}' already registered", 3)
|
||||
else
|
||||
self._ext[ext_path] = d
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
######################################################################
|
||||
# read_extension_manifest
|
||||
#
|
||||
# Read and parse the 'manifest.json' file in the 'wd' (working dir)
|
||||
#
|
||||
# Args:
|
||||
# - `wd`: (string) working dir indicating which .tapp file to read
|
||||
# ex: 'Partition_Wizard.tapp#'
|
||||
# Returns: map of values from JSON, or `nil` if an error occured
|
||||
#
|
||||
# Returned map is eitner `nil` if failed or a map with guaranteed content:
|
||||
# - name (string)
|
||||
# - description (string), default ""
|
||||
# - version (int), default 0
|
||||
# - min_tasmota(int), default 0
|
||||
#
|
||||
######################################################################
|
||||
def read_extension_manifest(wd_or_instance)
|
||||
var f
|
||||
var wd = wd_or_instance
|
||||
try
|
||||
import json
|
||||
import string
|
||||
|
||||
if (wd == nil) wd = tasmota.wd end # if 'wd' is nil, use the current `tasmota.wd`
|
||||
|
||||
var delimiter = ((size(wd) > 0) && (wd[-1] != '/') && (wd[-1] != '#')) ? '#' : '' # add '#' delimiter if filename
|
||||
f = open(wd + delimiter + 'manifest.json')
|
||||
var s = f.read()
|
||||
f.close()
|
||||
var j = json.load(s)
|
||||
# check if valid, 'name' is mandatory
|
||||
var name = j.find('name')
|
||||
if name
|
||||
# convert version numbers if present
|
||||
j['name'] = str(j['name'])
|
||||
j['description'] = str(j.find('description', ''))
|
||||
j['version'] = int(j.find('version', 0))
|
||||
j['min_tasmota'] = int(j.find('min_tasmota', 0))
|
||||
j['autorun'] = string.endswith(wd, ".tapp")
|
||||
return j
|
||||
else
|
||||
return nil
|
||||
end
|
||||
except .. as e, m
|
||||
log(f"BRY: error {e} {m} when reading 'manifest.json' in '{wd}'")
|
||||
if (f != nil)
|
||||
f.close()
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def remove_driver(d)
|
||||
if self._drivers
|
||||
var idx = self._drivers.find(d)
|
||||
@ -852,6 +941,31 @@ class Tasmota
|
||||
self._drivers.pop(idx)
|
||||
end
|
||||
end
|
||||
# remove ext
|
||||
if self._ext
|
||||
self._ext.remove_by_value(d)
|
||||
end
|
||||
end
|
||||
|
||||
def unload_extension(name_or_instance)
|
||||
if (self._ext == nil) return end
|
||||
var d = name_or_instance # d = driver
|
||||
|
||||
if type(name_or_instance) == 'string'
|
||||
d = self._ext.find(name_or_instance)
|
||||
end
|
||||
if type(d) == 'instance'
|
||||
import introspect
|
||||
|
||||
if introspect.contains(d, "unload")
|
||||
d.unload()
|
||||
end
|
||||
self.remove_driver(d)
|
||||
end
|
||||
# force gc of instance
|
||||
name_or_instance = nil
|
||||
d = nil
|
||||
tasmota.gc()
|
||||
end
|
||||
|
||||
# cmd high-level function
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
BIN
tasmota/berry/extensions/Leds_Panel.tapp
Normal file
BIN
tasmota/berry/extensions/Leds_Panel.tapp
Normal file
Binary file not shown.
9
tasmota/berry/extensions/Leds_Panel/autoexec.be
Normal file
9
tasmota/berry/extensions/Leds_Panel/autoexec.be
Normal file
@ -0,0 +1,9 @@
|
||||
# rm Leds_Panel.tapp; zip -j -0 Leds_Panel.tapp Leds_Panel/autoexec.be Leds_Panel/leds_panel.be Leds_Panel/manifest.json
|
||||
do # embed in `do` so we don't add anything to global namespace
|
||||
import introspect
|
||||
var leds_panel = introspect.module('leds_panel', true) # load module but don't cache
|
||||
tasmota.add_extension(leds_panel)
|
||||
end
|
||||
|
||||
# to remove:
|
||||
# tasmota.unload_extension('Leds Panel')
|
||||
1025
tasmota/berry/extensions/Leds_Panel/leds_panel.be
Normal file
1025
tasmota/berry/extensions/Leds_Panel/leds_panel.be
Normal file
File diff suppressed because it is too large
Load Diff
8
tasmota/berry/extensions/Leds_Panel/manifest.json
Normal file
8
tasmota/berry/extensions/Leds_Panel/manifest.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Leds Panel",
|
||||
"version": "0x19090100",
|
||||
"description": "Realtime display of the WS2812 leds in browser",
|
||||
"author": "Stephan Hadinger",
|
||||
"min_tasmota": "0x0F000100",
|
||||
"features": ""
|
||||
}
|
||||
BIN
tasmota/berry/extensions/Partition_Wizard.tapp
Normal file
BIN
tasmota/berry/extensions/Partition_Wizard.tapp
Normal file
Binary file not shown.
9
tasmota/berry/extensions/Partition_Wizard/autoexec.be
Normal file
9
tasmota/berry/extensions/Partition_Wizard/autoexec.be
Normal file
@ -0,0 +1,9 @@
|
||||
# rm Partition_Wizard.tapp; zip -j -0 Partition_Wizard.tapp Partition_Wizard/autoexec.be Partition_Wizard/partition_wizard.bec Partition_Wizard/manifest.json
|
||||
do # embed in `do` so we don't add anything to global namespace
|
||||
import introspect
|
||||
var partition_wizard = introspect.module('partition_wizard', true) # load module but don't cache
|
||||
tasmota.add_extension(partition_wizard)
|
||||
end
|
||||
|
||||
# to remove:
|
||||
# tasmota.unload_extension('Partition Wizard')
|
||||
8
tasmota/berry/extensions/Partition_Wizard/manifest.json
Normal file
8
tasmota/berry/extensions/Partition_Wizard/manifest.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Partition Wizard",
|
||||
"version": "0x19090100",
|
||||
"description": "Wizard for resizing partitions and converting to safeboot layout",
|
||||
"author": "Stephan Hadinger",
|
||||
"min_tasmota": "0x0F000100",
|
||||
"features": ""
|
||||
}
|
||||
@ -7,8 +7,6 @@
|
||||
# rm Partition_Wizard.tapp; zip Partition_Wizard.tapp -j -0 Partition_Wizard/autoexec.be Partition_Wizard/partition_wizard.be
|
||||
#######################################################################
|
||||
|
||||
var partition_wizard = module('partition_wizard')
|
||||
|
||||
#################################################################################
|
||||
# Partition_wizard_UI
|
||||
#
|
||||
@ -22,6 +20,11 @@ class Partition_wizard_UI
|
||||
def init()
|
||||
import persist
|
||||
|
||||
tasmota.add_driver(self)
|
||||
if tasmota.is_network_up()
|
||||
self.web_add_handler() # if init is called after the network is up, `web_add_handler` event is not fired
|
||||
end
|
||||
|
||||
if persist.find("factory_migrate") == true
|
||||
# remove marker to avoid bootloop if something goes wrong
|
||||
tasmota.log("UPL: Resuming after step 1", 2)
|
||||
@ -30,18 +33,25 @@ class Partition_wizard_UI
|
||||
|
||||
# continue the migration process 5 seconds after Wifi is connected
|
||||
def continue_after_5s()
|
||||
tasmota.remove_rule("parwiz_5s1") # first remove rule to avoid firing it again at Wifi reconnect
|
||||
tasmota.remove_rule("parwiz_5s2") # first remove rule to avoid firing it again at Wifi reconnect
|
||||
tasmota.set_timer(5000, /-> self.do_safeboot_partitioning()) # delay by 5 s
|
||||
tasmota.set_timer(5000, /-> self.do_safeboot_partitioning(), "partition_wizard_timer") # delay by 5 s
|
||||
end
|
||||
tasmota.add_rule("Wifi#Connected=1", continue_after_5s, "parwiz_5s1")
|
||||
tasmota.add_rule("Wifi#Connected==1", continue_after_5s, "parwiz_5s2")
|
||||
|
||||
tasmota.when_network_up(continue_after_5s)
|
||||
end
|
||||
end
|
||||
|
||||
#- ---------------------------------------------------------------------- -#
|
||||
# unload
|
||||
#- ---------------------------------------------------------------------- -#
|
||||
def unload()
|
||||
import webserver
|
||||
webserver.remove_route("/part_wiz", webserver.HTTP_GET)
|
||||
webserver.remove_route("/part_wiz", webserver.HTTP_POST)
|
||||
tasmota.remove_driver(self)
|
||||
tasmota.remove_timer("partition_wizard_timer")
|
||||
end
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Patch partition core since we can't chang the solidified code
|
||||
# Patch partition core since we can't change the solidified code
|
||||
# ----------------------------------------------------------------------
|
||||
def patch_partition_core(p)
|
||||
var otadata = p.otadata
|
||||
@ -881,26 +891,5 @@ class Partition_wizard_UI
|
||||
webserver.on("/part_wiz", / -> self.page_part_ctl(), webserver.HTTP_POST)
|
||||
end
|
||||
end
|
||||
partition_wizard.Partition_wizard_UI = Partition_wizard_UI
|
||||
|
||||
|
||||
#- create and register driver in Tasmota -#
|
||||
if tasmota
|
||||
import partition_core
|
||||
var partition_wizard_ui = partition_wizard.Partition_wizard_UI()
|
||||
tasmota.add_driver(partition_wizard_ui)
|
||||
## can be removed if put in 'autoexec.bat'
|
||||
partition_wizard_ui.web_add_handler()
|
||||
end
|
||||
|
||||
return partition_wizard
|
||||
|
||||
#- Example
|
||||
|
||||
import partition
|
||||
|
||||
# read
|
||||
p = partition.Partition()
|
||||
print(p)
|
||||
|
||||
-#
|
||||
return Partition_wizard_UI()
|
||||
BIN
tasmota/berry/extensions/Partition_Wizard/partition_wizard.bec
Normal file
BIN
tasmota/berry/extensions/Partition_Wizard/partition_wizard.bec
Normal file
Binary file not shown.
BIN
tasmota/berry/extensions/Wifi_Memory_Sticker.tapp
Normal file
BIN
tasmota/berry/extensions/Wifi_Memory_Sticker.tapp
Normal file
Binary file not shown.
6
tasmota/berry/extensions/Wifi_Memory_Sticker/autoexec.be
Normal file
6
tasmota/berry/extensions/Wifi_Memory_Sticker/autoexec.be
Normal file
@ -0,0 +1,6 @@
|
||||
# rm Wifi_Memory_Sticker.tapp; zip -j -0 Wifi_Memory_Sticker.tapp Wifi_Memory_Sticker/autoexec.be Wifi_Memory_Sticker/wifi_memory_sticker.be Wifi_Memory_Sticker/manifest.json
|
||||
do # embed in `do` so we don't add anything to global namespace
|
||||
import introspect
|
||||
var wifi_memory_sticker = introspect.module('wifi_memory_sticker', true) # load module but don't cache
|
||||
tasmota.add_extension(wifi_memory_sticker)
|
||||
end
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Wifi Memory Sticker",
|
||||
"version": "0x19090100",
|
||||
"description": "Display top left sticker with Wifi strength and memory heap",
|
||||
"author": "Stephan Hadinger",
|
||||
"min_tasmota": "0x0F000100",
|
||||
"features": ""
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
#######################################################################
|
||||
# Wifi Memory Sticker
|
||||
#
|
||||
# Sticker to show realtime wifi strengh and memory (top left of main page)
|
||||
|
||||
#################################################################################
|
||||
# Wifi_Memory_Sticker
|
||||
#################################################################################
|
||||
class Wifi_Memory_Sticker
|
||||
|
||||
static var HTTP_HEAD_STYLE_WIFI =
|
||||
"<style>"
|
||||
".wifi{width:18px;height:18px;position:relative}"
|
||||
".arc{padding:0;position:absolute;border:2px solid transparent;border-radius:50%;border-top-color:var(--c_frm)}"
|
||||
".a0{width:2px;height:3px;top:10px;left:11px}"
|
||||
".a1{width:6px;height:6px;top:7px;left:9px}"
|
||||
".a2{width:12px;height:12px;top:4px;left:6px}"
|
||||
".a3{width:18px;height:18px;top:1px;left:3px}"
|
||||
".arc.active{border-top-color:var(--c_ttl)}"
|
||||
"</style>"
|
||||
|
||||
def init()
|
||||
|
||||
tasmota.add_driver(self)
|
||||
end
|
||||
|
||||
def unload()
|
||||
tasmota.remove_driver(self)
|
||||
end
|
||||
|
||||
#################################################################################
|
||||
# called when displaying the left status line
|
||||
def web_status_line_left()
|
||||
import webserver
|
||||
# display wifi
|
||||
if tasmota.wifi('up')
|
||||
webserver.content_send(self.HTTP_HEAD_STYLE_WIFI)
|
||||
var rssi = tasmota.wifi('rssi')
|
||||
webserver.content_send(format("<div class='wifi' title='RSSI %d%%, %d dBm' style='padding:0 2px 0 2px;'><div class='arc a3 %s'></div><div class='arc a2 %s'></div><div class='arc a1 %s'></div><div class='arc a0 active'></div></div>",
|
||||
tasmota.wifi('quality'), rssi,
|
||||
rssi >= -55 ? "active" : "",
|
||||
rssi >= -70 ? "active" : "",
|
||||
rssi >= -85 ? "active" : ""))
|
||||
end
|
||||
# display free heap
|
||||
webserver.content_send(f"<span> {tasmota.memory('heap_free')}k</span>")
|
||||
end
|
||||
end
|
||||
|
||||
return Wifi_Memory_Sticker()
|
||||
6
tasmota/berry/leds_panel/autoexec.be
Normal file
6
tasmota/berry/leds_panel/autoexec.be
Normal file
@ -0,0 +1,6 @@
|
||||
# Led panel web app
|
||||
do # embed in `do` so we don't add anything to global namespace
|
||||
import introspect
|
||||
var leds_panel = introspect.module('leds_panel', true) # load module but don't cache
|
||||
tasmota.add_driver(leds_panel, "leds_panel")
|
||||
end
|
||||
@ -705,6 +705,7 @@ class leds_panel
|
||||
var strip # strip object
|
||||
var h, w, cell_size, cell_space
|
||||
|
||||
static var EXT_NAME = "leds_panel"
|
||||
static var SAMPLING = 100
|
||||
static var PORT = 8886 # default port 8886
|
||||
static var HTML_WIDTH = 290
|
||||
@ -826,9 +827,25 @@ class leds_panel
|
||||
self.web.on("/leds_feed", self, self.send_info_feed) # feed with leds values
|
||||
self.web.on("/leds", self, self.send_info_page) # display leds page
|
||||
|
||||
tasmota.add_driver(self)
|
||||
tasmota.add_driver(self, self.EXT_NAME) # also register as `leds_panel` extension
|
||||
end
|
||||
|
||||
#################################################################################
|
||||
# unload
|
||||
#
|
||||
# Uninstall the extension and deallocate all resources
|
||||
#################################################################################
|
||||
def unload()
|
||||
self.close() # stop server
|
||||
tasmota.remove_driver(self) # remove driver, normally already done by tasmota.unload_ext
|
||||
global.undef("webserver_async") # free `webserver_async` if it was loaded as part of this file
|
||||
end
|
||||
|
||||
#################################################################################
|
||||
# stop
|
||||
#
|
||||
# Stop server
|
||||
#################################################################################
|
||||
def close()
|
||||
tasmota.remove_driver(self)
|
||||
self.web.close()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -1220,7 +1220,8 @@
|
||||
//#define USE_IBEACON_ESP32 // Add support for Bluetooth LE passive scan of iBeacon devices using the internal ESP32 Bluetooth module
|
||||
//#define USE_WEBCAM // Add support for webcam
|
||||
|
||||
#define USE_AUTOCONF // Enable Esp32 autoconf feature, requires USE_BERRY and USE_WEBCLIENT_HTTPS (12KB Flash)
|
||||
#define USE_AUTOCONF // Enable Esp32(x) autoconf feature, requires USE_BERRY and USE_WEBCLIENT_HTTPS (12KB Flash)
|
||||
// #define USE_EXTENSION_MANAGER // Enable Esp32(x) extensions manager, requires USE_BERRY and USE_WEBCLIENT_HTTPS (9KB Flash)
|
||||
#define USE_BERRY // Enable Berry scripting language
|
||||
#define USE_BERRY_PYTHON_COMPAT // Enable by default `import python_compat`
|
||||
#define USE_BERRY_TIMEOUT 4000 // Timeout in ms, will raise an exception if running time exceeds this timeout
|
||||
|
||||
@ -58,6 +58,10 @@ const char be_berry_init_code[] =
|
||||
"do import autoconf end "
|
||||
#endif // USE_AUTOCONF
|
||||
|
||||
#ifdef USE_EXTENSION_MANAGER
|
||||
"do import extension_manager end "
|
||||
#endif
|
||||
|
||||
#ifdef USE_LVGL
|
||||
"import lv "
|
||||
"import lv_tasmota "
|
||||
|
||||
Loading…
Reference in New Issue
Block a user