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

@@ -9,13 +9,24 @@ pub enum DataSourceType {
Rss,
HttpJson,
Webhook,
Clock,
StaticText,
}
#[derive(Debug, Clone)]
pub struct DataSourceConfig {
pub url: Option<String>,
pub headers: Vec<(String, String)>,
pub api_key: Option<String>,
pub enum DataSourceConfig {
External {
url: Option<String>,
headers: Vec<(String, String)>,
api_key: Option<String>,
},
Clock {
format: String,
timezone: String,
},
StaticText {
text: String,
},
}
#[derive(Debug, Clone)]
@@ -38,18 +49,28 @@ impl DataSource {
pub fn validate(&self) -> Vec<DataSourceValidationError> {
let mut errors = Vec::new();
let is_webhook = self.source_type == DataSourceType::Webhook;
if is_webhook {
if !self.poll_interval.is_zero() {
errors.push(DataSourceValidationError::PollIntervalNotAllowed);
match self.source_type {
DataSourceType::Webhook => {
if !self.poll_interval.is_zero() {
errors.push(DataSourceValidationError::PollIntervalNotAllowed);
}
}
} else {
if self.poll_interval.is_zero() {
errors.push(DataSourceValidationError::PollIntervalRequired);
DataSourceType::Clock | DataSourceType::StaticText => {
// Internal sources: poll_interval optional, no url needed
}
if self.requires_url() && self.config.url.is_none() {
errors.push(DataSourceValidationError::UrlRequired);
_ => {
if self.poll_interval.is_zero() {
errors.push(DataSourceValidationError::PollIntervalRequired);
}
if self.requires_url() {
let has_url = matches!(
&self.config,
DataSourceConfig::External { url: Some(_), .. }
);
if !has_url {
errors.push(DataSourceValidationError::UrlRequired);
}
}
}
}

View File

@@ -35,6 +35,15 @@ impl WidgetConfig {
}
pub fn extract(&self, raw: &Value) -> WidgetState {
let has_mappings = self.mappings.iter().any(|m| !m.source_path.is_empty());
if !has_mappings {
let data = match raw {
Value::Object(map) => map.clone(),
_ => BTreeMap::new(),
};
return WidgetState { data, error: None };
}
let budget = self.max_data_size as usize;
let mut used = 0usize;
let mut data = BTreeMap::new();

View File

@@ -7,7 +7,7 @@ fn make_source(source_type: DataSourceType, url: Option<&str>, poll: Duration) -
name: "test".into(),
source_type,
poll_interval: poll,
config: DataSourceConfig {
config: DataSourceConfig::External {
url: url.map(Into::into),
headers: vec![],
api_key: None,