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

@@ -2,60 +2,128 @@ use domain::*;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum DataSourceConfigDto {
#[serde(rename = "external")]
External {
#[serde(default)]
url: Option<String>,
#[serde(default)]
api_key: Option<String>,
#[serde(default)]
headers: Vec<(String, String)>,
},
#[serde(rename = "clock")]
Clock {
#[serde(default = "default_clock_format")]
format: String,
#[serde(default = "default_timezone")]
timezone: String,
},
#[serde(rename = "static_text")]
StaticText {
#[serde(default)]
text: String,
},
}
fn default_clock_format() -> String {
"%H:%M:%S".into()
}
fn default_timezone() -> String {
"UTC".into()
}
#[derive(Serialize, Deserialize)]
pub struct DataSourceDto {
pub id: u16,
pub name: String,
pub source_type: String,
pub poll_interval_secs: u64,
pub url: Option<String>,
pub api_key: Option<String>,
pub headers: Vec<(String, String)>,
pub config: DataSourceConfigDto,
}
fn source_type_to_str(t: &DataSourceType) -> &'static str {
match t {
DataSourceType::Weather => "weather",
DataSourceType::Media => "media",
DataSourceType::Rss => "rss",
DataSourceType::HttpJson => "http_json",
DataSourceType::Webhook => "webhook",
DataSourceType::Clock => "clock",
DataSourceType::StaticText => "static_text",
}
}
fn source_type_from_str(s: &str) -> Result<DataSourceType, String> {
match s {
"weather" => Ok(DataSourceType::Weather),
"media" => Ok(DataSourceType::Media),
"rss" => Ok(DataSourceType::Rss),
"http_json" => Ok(DataSourceType::HttpJson),
"webhook" => Ok(DataSourceType::Webhook),
"clock" => Ok(DataSourceType::Clock),
"static_text" => Ok(DataSourceType::StaticText),
t => Err(format!("unknown source_type: {t}")),
}
}
impl From<&DataSource> for DataSourceDto {
fn from(ds: &DataSource) -> Self {
let config = match &ds.config {
DataSourceConfig::External {
url,
api_key,
headers,
} => DataSourceConfigDto::External {
url: url.clone(),
api_key: api_key.clone(),
headers: headers.clone(),
},
DataSourceConfig::Clock { format, timezone } => DataSourceConfigDto::Clock {
format: format.clone(),
timezone: timezone.clone(),
},
DataSourceConfig::StaticText { text } => {
DataSourceConfigDto::StaticText { text: text.clone() }
}
};
Self {
id: ds.id,
name: ds.name.clone(),
source_type: match ds.source_type {
DataSourceType::Weather => "weather",
DataSourceType::Media => "media",
DataSourceType::Rss => "rss",
DataSourceType::HttpJson => "http_json",
DataSourceType::Webhook => "webhook",
}
.into(),
source_type: source_type_to_str(&ds.source_type).into(),
poll_interval_secs: ds.poll_interval.as_secs(),
url: ds.config.url.clone(),
api_key: ds.config.api_key.clone(),
headers: ds.config.headers.clone(),
config,
}
}
}
impl DataSourceDto {
pub fn into_domain(self) -> Result<DataSource, String> {
let source_type = match self.source_type.as_str() {
"weather" => DataSourceType::Weather,
"media" => DataSourceType::Media,
"rss" => DataSourceType::Rss,
"http_json" => DataSourceType::HttpJson,
"webhook" => DataSourceType::Webhook,
t => return Err(format!("unknown source_type: {t}")),
let source_type = source_type_from_str(&self.source_type)?;
let config = match self.config {
DataSourceConfigDto::External {
url,
api_key,
headers,
} => DataSourceConfig::External {
url,
api_key,
headers,
},
DataSourceConfigDto::Clock { format, timezone } => {
DataSourceConfig::Clock { format, timezone }
}
DataSourceConfigDto::StaticText { text } => DataSourceConfig::StaticText { text },
};
Ok(DataSource {
id: self.id,
name: self.name,
source_type,
poll_interval: Duration::from_secs(self.poll_interval_secs),
config: DataSourceConfig {
url: self.url,
api_key: self.api_key,
headers: self.headers,
},
config,
})
}
}