internal data sources (clock, static text), connection indicator, rendering fixes

DataSourceConfig refactored to enum: External/Clock/StaticText. Clock
generates formatted time via chrono, static text emits configured string.

ESP32: connection status indicator (green/red dot bottom-right), per-widget
clear before redraw, RenderEvent enum for local + server messages.

Polling uses DataUpdate instead of ScreenUpdate to avoid wiping widget state.
Empty mappings passthrough raw source data for internal sources.
This commit is contained in:
2026-06-19 11:26:49 +02:00
parent b448fa15fe
commit a51d22649a
25 changed files with 630 additions and 214 deletions

View File

@@ -1,13 +1,16 @@
use anyhow::Result;
use application::DataProjection;
use chrono::Utc;
use chrono_tz::Tz;
use config_sqlite::SqliteConfigStore;
use domain::{
BroadcastPort, ConfigRepository, DataSource, DataSourcePort, DataSourceType, Value, WidgetState,
BroadcastPort, ConfigRepository, DataSource, DataSourceConfig, DataSourcePort, DataSourceType,
Value, WidgetState,
};
use http_json::HttpJsonAdapter;
use media_adapter::MediaAdapter;
use rss_adapter::RssAdapter;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use std::time::Duration;
use tcp_server::TcpBroadcaster;
@@ -117,14 +120,6 @@ async fn poll_loop(
}
};
let layout = match config.get_layout().await {
Ok(l) => l,
Err(e) => {
warn!(error = %e, "failed to fetch layout");
continue;
}
};
let changed: Vec<(u16, WidgetState)> = projection
.apply_poll_result(source.id, &result, &widgets)
.await;
@@ -137,9 +132,7 @@ async fn poll_loop(
Some((*id, hint, state.clone()))
})
.collect();
if let Some(l) = &layout
&& let Err(e) = broadcaster.push_screen_update(l, &with_hints).await
{
if let Err(e) = broadcaster.push_data_update(&with_hints).await {
warn!(error = %e, "failed to push update");
}
info!(source = %source.name, count = changed.len(), "pushed widget updates");
@@ -166,8 +159,33 @@ async fn poll_source(
.poll(source)
.await
.map_err(|e| anyhow::anyhow!("{e}")),
DataSourceType::Clock => Ok(generate_clock(&source.config)),
DataSourceType::StaticText => Ok(generate_static_text(&source.config)),
DataSourceType::Webhook => Err(anyhow::anyhow!(
"webhook sources are push-based, not polled"
)),
}
}
fn generate_clock(config: &DataSourceConfig) -> Value {
let (fmt, tz_name) = match config {
DataSourceConfig::Clock { format, timezone } => (format.as_str(), timezone.as_str()),
_ => ("%H:%M:%S", "UTC"),
};
let tz: Tz = tz_name.parse().unwrap_or(chrono_tz::UTC);
let now = Utc::now().with_timezone(&tz);
let formatted = now.format(fmt).to_string();
let mut map = BTreeMap::new();
map.insert("time".into(), Value::String(formatted));
Value::Object(map)
}
fn generate_static_text(config: &DataSourceConfig) -> Value {
let text = match config {
DataSourceConfig::StaticText { text } => text.clone(),
_ => String::new(),
};
let mut map = BTreeMap::new();
map.insert("text".into(), Value::String(text));
Value::Object(map)
}