One-click and nightly firmware updates from ESP Screens

Screens now report their ESPHome name and IP address, so the add-on can
match each screen to its YAML profile and OTA address itself. The screen
list shows an update badge with a Bijwerken button, an update-all action
and a nightly checkbox; updates run one screen at a time, wait for the
new version to report back and stop the round on the first failure.

App 0.2.16, firmware 0.2.17.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-13 10:14:04 +02:00
parent 437b8c55b4
commit 437e3a5046
18 changed files with 875 additions and 23 deletions
+23
View File
@@ -12,6 +12,10 @@ import time
import yaml
from core import installation_yaml
class LenientLoader(yaml.SafeLoader):
"""Reads profile metadata without resolving !secret or !include."""
LenientLoader.add_multi_constructor('!', lambda loader, suffix, node: None)
class Firmware:
def __init__(self, root, data):
self.root, self.data = Path(root).resolve(), Path(data)
@@ -25,6 +29,25 @@ class Firmware:
return [{'file': p.name} for p in sorted(self.root.glob('*.yaml'))
if p.name != 'secrets.yaml' and p.is_file() and not p.is_symlink()]
def profile_names(self):
"""{file: {'node': esphome name, 'friendly': friendly name}} for every readable profile."""
found = {}
for entry in self.profiles():
try:
data = yaml.load((self.root / entry['file']).read_text(), Loader=LenientLoader)
except (OSError, yaml.YAMLError, UnicodeError):
continue
block = data.get('esphome') if isinstance(data, dict) else None
if not isinstance(block, dict):
continue
substitutions = data.get('substitutions') if isinstance(data.get('substitutions'), dict) else {}
def resolve(value):
if isinstance(value, str) and value.startswith('${') and value.endswith('}'):
value = substitutions.get(value[2:-1])
return value if isinstance(value, str) else None
found[entry['file']] = {'node': resolve(block.get('name')), 'friendly': resolve(block.get('friendly_name'))}
return found
def ports(self):
return sorted(set(glob.glob('/dev/serial/by-id/*') or glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')))