Wake only for watched entities, registry events, light poll, 2 min keepalive (0.2.20)
The sync loop now wakes only for state changes of entities on a layout or belonging to a screen. Registries refresh on HA registry events with a 10 minute fallback instead of every 30 s. The page polls a light inventory (screens + update status) every 10 s and the full catalogues every 5 min or when the tab becomes visible. The full keepalive interval is 120 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
Uses the same message builder as ESP Screen Manager, but with synthetic states,
|
||||
so rendering can be checked without touching Home Assistant. The running
|
||||
manager restores the real layout on its next keepalive (about 25 seconds).
|
||||
manager restores the real layout on its next keepalive (about two minutes).
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
## 0.2.20 (firmware 0.2.18 blijft actueel)
|
||||
|
||||
- **Minder verbruik in rust**: de synchronisatielus wordt alleen nog wakker voor entiteiten die op een indeling staan of bij een scherm horen (Tegelinstellingen, Schermfirmware, Apparaatnaam, IP-adres, Guition schermtype), niet meer bij elke `state_changed` in Home Assistant.
|
||||
- De entity-, device- en area-registry (circa 1 MB JSON) wordt niet meer elke 30 s opgehaald, maar bij een `*_registry_updated`-event van Home Assistant (1 s gedebounced) en als vangnet elke 10 minuten.
|
||||
- De volledige keepalive naar de schermen loopt elke 2 minuten in plaats van elke 25 s. Een scherm dat offline en weer online komt krijgt de volledige indeling direct, zoals voorheen.
|
||||
- **Lichte poll**: de pagina haalt elke 10 s alleen nog schermen en updatestatus op (`/api/inventory?light=1`, enkele KB) en de volledige lijst met entiteiten, achtergronden en iconen bij openen, bij terugkeer naar het tabblad en elke 5 minuten.
|
||||
- Alleen de app verandert; firmware 0.2.18 blijft actueel.
|
||||
|
||||
## 0.2.19 (firmware 0.2.18 blijft actueel)
|
||||
|
||||
- **Sneller overzicht**: `/api/inventory` las bij elke aanroep alle ESPHome-profielen opnieuw in (tot vier keer per verzoek), waardoor de add-on op een Pi seconden per verzoek blokkeerde. Profielen worden nu per bestand gecachet op inode, wijzigingstijd en grootte; alleen een gewijzigd profiel wordt opnieuw gelezen en verwijderde profielen verdwijnen direct. Het parsen zelf gebruikt libyaml wanneer die beschikbaar is (circa tien keer sneller).
|
||||
|
||||
@@ -19,7 +19,13 @@ from zoneinfo import ZoneInfo
|
||||
|
||||
LOG = logging.getLogger('screen_manager')
|
||||
|
||||
REGISTRY_EVENTS = ('entity_registry_updated', 'device_registry_updated', 'area_registry_updated')
|
||||
SCREEN_ENTITY_NAMES = {'Tegelinstellingen', 'Schermfirmware', 'Guition schermtype', 'Apparaatnaam', 'IP-adres'}
|
||||
KEEPALIVE_SECONDS = 120
|
||||
|
||||
class HomeAssistant:
|
||||
registry_interval = 600
|
||||
|
||||
def __init__(self, session, base, token):
|
||||
self.session, self.base, self.token = session, base.rstrip('/'), token
|
||||
self.ws = None
|
||||
@@ -29,6 +35,9 @@ class HomeAssistant:
|
||||
self.registry, self.devices, self.areas = [], [], []
|
||||
self.online = False
|
||||
self.changed = asyncio.Event()
|
||||
# Entity ids the manager cares about; None wakes it for every state change.
|
||||
self.relevant = None
|
||||
self.registry_changed = asyncio.Event()
|
||||
self.setting_events = []
|
||||
self.time_zone = None
|
||||
|
||||
@@ -63,13 +72,16 @@ class HomeAssistant:
|
||||
if event.get('event_type') == 'esphome.screen_setting':
|
||||
self.setting_events.append(body)
|
||||
self.changed.set()
|
||||
if event.get('event_type') == 'state_changed':
|
||||
elif event.get('event_type') == 'state_changed':
|
||||
eid = body.get('entity_id')
|
||||
if body.get('new_state'):
|
||||
self.states[eid] = body['new_state']
|
||||
else:
|
||||
self.states.pop(eid, None)
|
||||
self.changed.set()
|
||||
if self.relevant is None or eid in self.relevant:
|
||||
self.changed.set()
|
||||
elif event.get('event_type') in REGISTRY_EVENTS:
|
||||
self.registry_changed.set()
|
||||
raise ConnectionError('Home Assistant-verbinding verbroken.')
|
||||
|
||||
async def registries(self):
|
||||
@@ -91,6 +103,8 @@ class HomeAssistant:
|
||||
reader = asyncio.create_task(self.read())
|
||||
await self.request('subscribe_events', event_type='state_changed')
|
||||
await self.request('subscribe_events', event_type='esphome.screen_setting')
|
||||
for event_type in REGISTRY_EVENTS:
|
||||
await self.request('subscribe_events', event_type=event_type)
|
||||
self.states = {s['entity_id']: s for s in await self.request('get_states')}
|
||||
await self.registries()
|
||||
try:
|
||||
@@ -100,12 +114,24 @@ class HomeAssistant:
|
||||
self.online = True
|
||||
self.changed.set()
|
||||
LOG.info('Home Assistant verbonden')
|
||||
# Refresh registry for newly paired screens and renamed entities.
|
||||
# Refresh the registry when HA reports a change (debounced), with a slow
|
||||
# fallback; a full fetch is about 1 MB of JSON and used to run every 30 s.
|
||||
fetched = time.monotonic()
|
||||
while True:
|
||||
done, _ = await asyncio.wait([reader], timeout=30)
|
||||
if done:
|
||||
waiter = asyncio.ensure_future(self.registry_changed.wait())
|
||||
try:
|
||||
done, _ = await asyncio.wait([reader, waiter], timeout=30, return_when=asyncio.FIRST_COMPLETED)
|
||||
finally:
|
||||
waiter.cancel()
|
||||
if reader in done:
|
||||
await reader
|
||||
if self.registry_changed.is_set():
|
||||
await asyncio.sleep(1)
|
||||
self.registry_changed.clear()
|
||||
elif time.monotonic() - fetched < self.registry_interval:
|
||||
continue
|
||||
await self.registries()
|
||||
fetched = time.monotonic()
|
||||
self.changed.set()
|
||||
except (ConnectionError, TimeoutError, OSError, ValueError) as error:
|
||||
LOG.warning('Home Assistant tijdelijk niet beschikbaar (%s)', type(error).__name__)
|
||||
@@ -245,6 +271,14 @@ class Manager:
|
||||
self.status[inbox] = 'Opgeslagen; wacht op synchronisatie'
|
||||
self.ha.changed.set()
|
||||
|
||||
def watched_entities(self):
|
||||
"""Entities whose state changes matter: tiles on any layout plus the screens' own diagnostics."""
|
||||
watched = {tile['entity'] for layout in self.layouts.values() for tile in layout['tiles']}
|
||||
for item in getattr(self.ha, 'registry', []):
|
||||
if item.get('platform') == 'esphome' and item.get('original_name') in SCREEN_ENTITY_NAMES:
|
||||
watched.add(item['entity_id'])
|
||||
return watched
|
||||
|
||||
async def cached(self, store, key, ttl, fetch):
|
||||
entry=store.get(key)
|
||||
if not entry or time.monotonic()-entry[0]>ttl:
|
||||
@@ -313,7 +347,9 @@ class Manager:
|
||||
layout['settings']=validate_settings(settings);self.save(inbox,layout)
|
||||
except (ValueError,TypeError): pass
|
||||
if hasattr(self.ha,'setting_events'): self.ha.setting_events.clear()
|
||||
for screen in self.inventory()[0]:
|
||||
screens = self.inventory()[0]
|
||||
self.ha.relevant = self.watched_entities()
|
||||
for screen in screens:
|
||||
inbox = screen['id']
|
||||
if not screen['online']:
|
||||
self.sent.pop(inbox, None)
|
||||
@@ -322,7 +358,7 @@ class Manager:
|
||||
if inbox not in self.layouts:
|
||||
continue
|
||||
try:
|
||||
force = time.monotonic() - self.last.get(inbox, 0) >= 25
|
||||
force = time.monotonic() - self.last.get(inbox, 0) >= KEEPALIVE_SECONDS
|
||||
# last records full keepalive, not unrelated HA state events.
|
||||
before = self.last.get(inbox, 0)
|
||||
await self.sync_one(inbox, self.layouts[inbox], force, screen)
|
||||
@@ -366,9 +402,14 @@ def create_app(manager, development=False):
|
||||
screen['layout'] = manager.layouts.get(screen['id'], {'title': 'Thuis', 'tiles': []})
|
||||
screen['delivery'] = manager.status.get(screen['id'], 'Kies je eerste tegels')
|
||||
screen['update'] = manager.updates.state_for(screen, profiles)
|
||||
builtin = [{'id': key, 'name': name, 'device': 'Ingebouwd op het scherm', 'area': '', 'state': 'ok'} for key, name in BUILTIN.items()]
|
||||
return web.json_response({'csrf': csrf, 'connected': manager.ha.online, 'screens': screens, 'entities': entities,
|
||||
'backgrounds': TILE_BACKGROUNDS, 'icons': tile_icons.editor(), 'builtin': builtin, 'updates': manager.updates.summary(screens, profiles)})
|
||||
payload = {'csrf': csrf, 'connected': manager.ha.online, 'screens': screens, 'updates': manager.updates.summary(screens, profiles)}
|
||||
if request.query.get('light') != '1':
|
||||
# The page polls the light form; entities, backgrounds and icons (~100 KB) only on demand.
|
||||
payload['entities'] = entities
|
||||
payload['backgrounds'] = TILE_BACKGROUNDS
|
||||
payload['icons'] = tile_icons.editor()
|
||||
payload['builtin'] = [{'id': key, 'name': name, 'device': 'Ingebouwd op het scherm', 'area': '', 'state': 'ok'} for key, name in BUILTIN.items()]
|
||||
return web.json_response(payload)
|
||||
async def save(request):
|
||||
manager.save(request.match_info['inbox'], await request.json())
|
||||
return web.json_response({'saved': True})
|
||||
|
||||
@@ -851,9 +851,11 @@ function renderResults() {
|
||||
),
|
||||
);
|
||||
}
|
||||
async function refresh() {
|
||||
async function refresh(full = true) {
|
||||
try {
|
||||
inventory = await (await api("inventory")).json();
|
||||
const data = await (await api(full ? "inventory" : "inventory?light=1")).json();
|
||||
// A light poll carries only screens and update status; keep the catalogues we have.
|
||||
inventory = full ? data : { ...inventory, ...data };
|
||||
$("#connection").textContent = inventory.connected
|
||||
? "● Home Assistant verbonden"
|
||||
: "Verbinding met Home Assistant herstellen…";
|
||||
@@ -1015,17 +1017,22 @@ window.addEventListener("beforeunload", (e) => {
|
||||
});
|
||||
refresh();
|
||||
// Poll only while the tab is visible; a hidden tab would otherwise keep the add-on busy.
|
||||
let pollTimer;
|
||||
let pollTimer, lastFull = Date.now();
|
||||
function poll() {
|
||||
clearTimeout(pollTimer);
|
||||
pollTimer = setTimeout(async () => {
|
||||
if (!document.hidden) await refresh();
|
||||
if (!document.hidden) {
|
||||
const full = Date.now() - lastFull >= 300000;
|
||||
if (full) lastFull = Date.now();
|
||||
await refresh(full);
|
||||
}
|
||||
poll();
|
||||
}, inventory.updates?.busy ? 3000 : 10000);
|
||||
}
|
||||
poll();
|
||||
document.addEventListener("visibilitychange", async () => {
|
||||
if (document.hidden) return;
|
||||
lastFull = Date.now();
|
||||
await refresh();
|
||||
poll();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: ESP Screen Manager
|
||||
version: "0.2.19"
|
||||
version: "0.2.20"
|
||||
slug: esp_screen_manager
|
||||
description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen.
|
||||
url: https://github.com/MaxGramser/homeassistant_espscreen
|
||||
|
||||
@@ -213,6 +213,13 @@ class UpdaterTests(unittest.IsolatedAsyncioTestCase):
|
||||
with self.assertRaises(ValueError): self.setup_manager(tmp)
|
||||
with self.assertRaises(ValueError): m.updates.set_auto('yes')
|
||||
|
||||
async def test_manager_watches_only_layout_and_screen_entities(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
m = self.setup_manager(tmp)
|
||||
m.layouts['text.screen1'] = {'title': 'Thuis', 'tiles': [{'entity': 'light.lamp'}]}
|
||||
watched = m.watched_entities()
|
||||
self.assertEqual(watched, {'light.lamp', 'text.screen1', 'text.node1', 'text.ip1', 'text.fw1', 'text.screen2', 'text.fw2'})
|
||||
|
||||
async def test_http_endpoints(self):
|
||||
from server import create_app
|
||||
from aiohttp.test_utils import TestClient, TestServer
|
||||
@@ -222,6 +229,12 @@ class UpdaterTests(unittest.IsolatedAsyncioTestCase):
|
||||
inventory = await (await client.get('/api/inventory')).json()
|
||||
self.assertEqual(inventory['updates']['target'], FIRMWARE_VERSION)
|
||||
self.assertTrue(inventory['screens'][0]['update']['available'])
|
||||
self.assertIn('entities', inventory)
|
||||
light = await (await client.get('/api/inventory?light=1')).json()
|
||||
self.assertNotIn('entities', light)
|
||||
self.assertNotIn('icons', light)
|
||||
self.assertEqual([x['id'] for x in light['screens']], [x['id'] for x in inventory['screens']])
|
||||
self.assertEqual(light['updates'], inventory['updates'])
|
||||
headers = {'X-Screen-CSRF': inventory['csrf']}
|
||||
self.assertEqual((await client.put('/api/updates', headers=headers, json={'auto': True})).status, 200)
|
||||
self.assertTrue(m.updates.auto)
|
||||
|
||||
Reference in New Issue
Block a user