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:
@@ -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