Show live values on the mockup, blink a screen to identify it, try an alert, copy and share layouts (0.2.74)
The tiles draw what Home Assistant reports (a new /api/states answers for the tiles on the page); the ··· menu gets Identify (the screen's own show_alert with flash), Copy layout from another screen, Export and Import as JSON; the Alerts page opens with Try it (/api/alerts/test, one screen or all); the library filters by room and hides what is placed, with lit icons for what is on; ⌘K searches screens, entities and actions; the Update badge lists what's new from the changelog (copied into the image) and a running update shows a progress bar with the ESPHome stage and the last log line. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,12 @@ A new editor, built as a Vue app, with the same firmware.
|
||||
- **Pages side by side.** The pages of a screen stand next to each other, at the screen's own shape (square for a Guition, 4:3 for a CYD), the way you swipe through them. Dragging from the library onto a page and between cells works as before, with mouse and touch; drop past the last page for a new one.
|
||||
- **The top bar is part of the mockup.** Tap the bar on any page to name the screen and choose its items; the drawer shows every item as the screen draws it. Adding the time, the date, the dial or an entity happens in the same drawer.
|
||||
- **Screen settings as a tab** next to Layout, with the same rows as the settings page on the screen. **Read current data** and **Override YAML** live in the ··· menu of the screen; New screen, Firmware & USB, Alerts and the app Settings are pages of their own, reached from the sidebar.
|
||||
- **Live values on the mockup.** The tiles show what Home Assistant reports right now: the temperature with its unit, On or Off with a lit icon, the heating's current temperature and setpoint, the volume, a cover's position, the song that plays. Sliders and switches on the cards stand where they stand on the screen. The page asks every eight seconds while it is open.
|
||||
- **Identify.** The ··· menu of a screen has Identify: the screen shows a short card and blinks its backlight, so with three screens you know which is which. Needs firmware 0.2.31 or newer, like every alert.
|
||||
- **Try an alert.** The Alerts page opens with Try it: fill in the same seven fields an automation sends, choose one screen or all of them, and Send. The result says how many screens showed it and which fields were left empty.
|
||||
- **Copy, export, import.** The ··· menu copies the layout of another screen onto this one, exports the layout as a JSON file (and to the clipboard), and imports one. An imported layout is trimmed to what the screen's firmware holds, and nothing is sent before you press Save & send.
|
||||
- **A smarter library.** A room filter and Hide placed next to the domain chips; a lit icon for a light, switch or fan that is on, a grey one for an entity Home Assistant can't reach. ⌘K (Ctrl+K) opens a search over screens, entities and actions: open a screen, add an entity to the page you have open, save, identify, export.
|
||||
- **Updates with content.** The Update badge has What's new: the changelog lines since that screen's firmware. A running update shows a progress bar with its stage (building, writing over Wi-Fi, waiting for the screen, checking it stays up) and the last line of the ESPHome log; Settings → Firmware updates shows the log's tail.
|
||||
- **Dark mode follows your browser**, as Home Assistant's own Auto theme does.
|
||||
- Same add-on API, same data, same firmware: nothing to update on the screens. The page is now built with Vue and Vite (`web/`); its files carry a hash of their content, so an update never runs an old script against a new page.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ for existing profiles: check, build or reinstall via USB, the IP address (OTA) o
|
||||
Download. Existing ESPHome profiles in the HA config folder are
|
||||
found automatically.
|
||||
|
||||
**Settings** in the sidebar has the nightly firmware updates, the **Alerts** cheatsheet
|
||||
**Settings** in the sidebar has the nightly firmware updates (the Update badge of a screen lists what's new, a running update shows its progress), the **Alerts** cheatsheet with a form to try an alert
|
||||
(an alert on one screen, or on every screen with the `esp_screens_show_alert` event)
|
||||
and **Claude**: install the ESP Screens skill for Claude Code in Home Assistant, or
|
||||
download it for claude.ai, and ask Claude for the alert automation.
|
||||
@@ -27,6 +27,7 @@ to configure; firmware 0.2.32 or newer renders them.
|
||||
|
||||
After pairing via the HA ESPHome integration, choose the tiles in ESP Screens.
|
||||
Tap a tile and its settings open in a drawer on the right, with the mockup still in view: click behavior, larger values, mini-sliders, a wider tile, backgrounds.
|
||||
The mockup shows what Home Assistant reports right now: values, On or Off, positions, the song that plays. **Identify** in the ··· menu blinks a screen so you know which one it is; the same menu copies the layout of another screen, or exports and imports one as JSON. ⌘K searches screens, entities and actions.
|
||||
**Screen settings**, the tab next to Layout, has brightness and standby. **Read current data** in the ··· menu of a screen helps with
|
||||
missing attributes or an offline screen. New cards require firmware 0.2.0.
|
||||
The ··· menu of a screen also has **Override YAML** for a persistent, hardware-specific
|
||||
|
||||
@@ -5,6 +5,8 @@ ARG BUILD_ARCH
|
||||
LABEL io.hass.version="${BUILD_VERSION}" io.hass.type="app" io.hass.arch="${BUILD_ARCH}"
|
||||
RUN pip install --no-cache-dir 'aiohttp>=3.12,<4' PyYAML
|
||||
COPY app /app
|
||||
# The Update badge shows what's new from the changelog (app 0.2.74).
|
||||
COPY CHANGELOG.md /app/CHANGELOG.md
|
||||
WORKDIR /app
|
||||
ENTRYPOINT []
|
||||
CMD ["python3", "/app/server.py"]
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""What's new for the Update badge (app 0.2.74): the CHANGELOG sections since a screen's firmware.
|
||||
|
||||
The Dockerfile copies CHANGELOG.md next to this file; a development checkout has it one folder up.
|
||||
Each `## <app> (firmware <fw>)` section gives its bullet lines as plain text, newest first.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
HEADER = re.compile(r'^## (\d+\.\d+\.\d+) \(firmware (\d+\.\d+\.\d+)\)')
|
||||
BOLD = re.compile(r'\*\*(.+?)\*\*')
|
||||
CODE = re.compile(r'`([^`]*)`')
|
||||
LINK = re.compile(r'\[([^\]]+)\]\([^)]+\)')
|
||||
|
||||
|
||||
def load(path=None, limit=20):
|
||||
"""[{app, firmware, lines}] for the latest sections; [] without a changelog."""
|
||||
candidates = [Path(path)] if path else [Path(__file__).with_name('CHANGELOG.md'), Path(__file__).parent.parent / 'CHANGELOG.md']
|
||||
for candidate in candidates:
|
||||
if candidate.exists():
|
||||
return parse(candidate.read_text(), limit)
|
||||
return []
|
||||
|
||||
|
||||
def parse(text, limit=20):
|
||||
sections = []
|
||||
for line in text.splitlines():
|
||||
match = HEADER.match(line)
|
||||
if match:
|
||||
if len(sections) >= limit:
|
||||
break
|
||||
sections.append({'app': match.group(1), 'firmware': match.group(2), 'lines': []})
|
||||
elif sections and line.startswith('- '):
|
||||
sections[-1]['lines'].append(plain(line[2:]))
|
||||
return sections
|
||||
|
||||
|
||||
def plain(markdown):
|
||||
text = BOLD.sub(r'\1', markdown)
|
||||
text = CODE.sub(r'\1', text)
|
||||
text = LINK.sub(r'\1', text)
|
||||
return text.strip()
|
||||
@@ -1623,6 +1623,48 @@ def create_app(manager, development=False):
|
||||
manager.registry_index().get(t['entity']),getattr(manager.ha,'state_words',None)),
|
||||
'attributes':state_message(i,t,manager.ha.states)['a'],
|
||||
'options':t.get('options',{})} for i,t in enumerate(layout['tiles'])]})
|
||||
async def states(request):
|
||||
"""Live values for the editor's mockup (app 0.2.74): the state, Home Assistant's word and the attributes a
|
||||
card shows, for the tiles on the page, saved or not. At most sixty entities per request."""
|
||||
index = manager.registry_index()
|
||||
result = {}
|
||||
for eid in request.query.getall('entity', [])[:60]:
|
||||
if not isinstance(eid, str) or eid not in manager.ha.states:
|
||||
continue
|
||||
state = manager.ha.states.get(eid, {})
|
||||
entry = index.get(eid)
|
||||
message = state_message(0, {'entity': eid, 'name': ''}, manager.ha.states,
|
||||
precision=header_bar.precision_of(entry) if eid.startswith('sensor.') else None)
|
||||
result[eid] = {'state': message['state'], 'a': message['a'],
|
||||
'word': state_word(eid, state.get('state'), state.get('attributes'), entry, getattr(manager.ha, 'state_words', None))}
|
||||
return web.json_response({'states': result})
|
||||
def one_alert_target(inbox):
|
||||
screen = manager.screen(inbox)
|
||||
if screen is None:
|
||||
raise ValueError('Unknown screen.')
|
||||
ready, skipped = alert_targets([screen])
|
||||
if not ready:
|
||||
raise ValueError(f"{screen['name']} can't show an alert: {skipped[0][1] if skipped else 'not ready'}.")
|
||||
return ready[0]
|
||||
async def identify(request):
|
||||
"""Identify (app 0.2.74): the screen shows a short card and blinks its backlight, so you know which one it is."""
|
||||
screen = one_alert_target(request.match_info['inbox'])
|
||||
data, _ = alert_data({'title': f"This is {screen['name']}", 'subtitle': 'Identify, from ESP Screens', 'icon': 'bell-ring',
|
||||
'color': 'blue', 'button_text': 'OK', 'timeout': 8, 'flash': True})
|
||||
await manager.ha.call(alert_service(screen['node']), data)
|
||||
return web.json_response({'ok': True})
|
||||
async def test_alert(request):
|
||||
"""Alerts → Try it (app 0.2.74): one alert to one screen or to every screen, with the fields an automation sends."""
|
||||
body = await request.json()
|
||||
if not isinstance(body, dict):
|
||||
raise ValueError('Invalid alert.')
|
||||
data = body.get('data') if isinstance(body.get('data'), dict) else {}
|
||||
if body.get('screen') == 'all':
|
||||
return web.json_response(await manager.broadcast(BROADCAST_SHOW, data))
|
||||
screen = one_alert_target(str(body.get('screen', '')))
|
||||
service, unusable = alert_data(data)
|
||||
await manager.ha.call(alert_service(screen['node']), service)
|
||||
return web.json_response({'sent': 1, 'failed': 0, 'skipped': 0, 'unusable': unusable})
|
||||
async def install_claude_skill(request):
|
||||
"""Settings → Claude → Install: writes the skill into Home Assistant's configuration folder, only on request."""
|
||||
return web.json_response(claude_skill.install(manager.skill_dir))
|
||||
@@ -1667,6 +1709,9 @@ def create_app(manager, development=False):
|
||||
app.router.add_get('/', index)
|
||||
app.router.add_get('/api/inventory', inventory)
|
||||
app.router.add_get('/api/capabilities', capabilities)
|
||||
app.router.add_get('/api/states', states)
|
||||
app.router.add_post('/api/screens/{inbox}/identify', identify)
|
||||
app.router.add_post('/api/alerts/test', test_alert)
|
||||
app.router.add_get('/api/entity-actions', entity_actions)
|
||||
app.router.add_post('/api/header-preview', header_preview)
|
||||
app.router.add_post('/api/claude-skill', install_claude_skill)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<title>ESP Screens</title>
|
||||
<script type="module" crossorigin src="./assets/index-DOjlT4hj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-D7h6hKdu.css">
|
||||
<script type="module" crossorigin src="./assets/index-BuQ5a76G.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-PqOTdpJB.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import time
|
||||
import changelog
|
||||
from core import FIRMWARE_VERSION
|
||||
|
||||
LOG = logging.getLogger('screen_manager')
|
||||
@@ -31,6 +32,8 @@ class Updater:
|
||||
self.manager, self.path = manager, Path(path)
|
||||
self.auto, self.hosts, self.results, self.last_round = False, {}, {}, None
|
||||
self.task, self.current, self.queue, self.phase = None, None, [], None
|
||||
# What's new since a screen's firmware, for the Update badge (app 0.2.74).
|
||||
self.changelog = changelog.load()
|
||||
if self.path.exists():
|
||||
raw = json.loads(self.path.read_text())
|
||||
if raw.get('version') != 1:
|
||||
@@ -103,7 +106,7 @@ class Updater:
|
||||
|
||||
def summary(self, screens=None, profiles=None):
|
||||
return {'auto': self.auto, 'target': FIRMWARE_VERSION, 'busy': self.current,
|
||||
'pending': len(self.pending(screens, profiles)), 'last_round': self.last_round}
|
||||
'pending': len(self.pending(screens, profiles)), 'last_round': self.last_round, 'changelog': self.changelog}
|
||||
|
||||
def pending(self, screens=None, profiles=None):
|
||||
# Callers that already hold the inventory and profile list pass them in; the inventory
|
||||
|
||||
Reference in New Issue
Block a user