Files
dashboard/dashboard/src/ui.rs
2026-08-17 02:03:35 +02:00

217 lines
7.1 KiB
Rust

slint::include_modules!();
use crate::{
battery::RawBatteryData,
ha_ext::*,
home_assistant::CalendarEvent,
messages::{CommandMessage, EventMessage},
};
use hass_rs::HassEntity;
use paste::paste;
use slint::{Model, ModelRc, ToSharedString, VecModel, Weak, language::ColorScheme};
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
macro_rules! apply_model {
($app:expr, $entities:expr, $name:ident, $ty:ty, $filter:expr) => {
paste! {
if $app
.[<get_ $name>]()
.as_any()
.downcast_ref::<VecModel<$ty>>()
.is_none()
{
$app.[<set_ $name>](ModelRc::new(VecModel::<$ty>::default()));
}
let data = $app
.[<get_ $name>]();
if let Some(model) = data
.as_any()
.downcast_ref::<VecModel<$ty>>() {
for entity in $entities.iter().filter($filter) {
match model.iter().position(|item| item.id == entity.entity_id) {
Some(index) => model.set_row_data(index, entity.into()),
None => model.push(entity.into()),
}
}
}
}
};
}
pub fn apply_theme(app: &AppWindow, entities: &Vec<HassEntity>) {
for entity in entities {
if entity.entity_id == "sensor.diyless_thermostat_3_ambient_light_level"
&& let Ok(illuminance) = entity.state.parse::<i32>()
{
let mut color_scheme = app.get_color_scheme();
if illuminance < 100 {
color_scheme = ColorScheme::Dark;
} else if illuminance > 400 {
color_scheme = ColorScheme::Light;
}
app.set_color_scheme(color_scheme);
}
}
}
pub fn apply_clock(app: &AppWindow, entities: &Vec<HassEntity>) {
for entity in entities {
if entity.entity_id == "sensor.date_time_iso" {
app.set_date_time(entity.into());
}
}
}
pub fn apply_weather(app: &AppWindow, entities: &Vec<HassEntity>) {
for entity in entities {
if entity.entity_id == "weather.forecast_home" {
app.set_weather(entity.into());
}
if entity.entity_id == "sun.sun" {
app.set_is_night(entity.state == "below_horizon");
}
}
}
pub fn apply_lights(app: &AppWindow, entities: &[HassEntity]) {
apply_model!(app, entities, lights, LightData, |entity| {
entity.domain() == "light"
&& !entity.entity_id.ends_with("screen")
&& !entity.entity_id.chars().any(char::is_numeric)
});
}
pub fn apply_thermometers(app: &AppWindow, entities: &[HassEntity]) {
apply_model!(app, entities, thermometers, ThermometerData, |entity| {
entity.domain() == "sensor" && entity.entity_id.ends_with("thermometer_temperature")
});
}
pub fn apply_thermostats(app: &AppWindow, entities: &[HassEntity]) {
apply_model!(app, entities, thermostats, ThermostatData, |entity| {
entity.domain() == "climate" && !entity.entity_id.contains("diyless")
});
}
pub fn apply_calendars(app: &Weak<AppWindow>, entities: Vec<CalendarEvent>) {
if let Err(err) = app.upgrade_in_event_loop(move |app| {
let mut entities = entities.clone();
entities.sort_by_key(|a| a.sort_epoch);
let entities: Vec<CalendarEventData> = entities.iter().map(Into::into).collect::<Vec<_>>();
if let Some(event) = &entities.first() {
app.set_next_event_date(slint::format!("NEXT / {}", event.date.to_uppercase(),));
app.set_next_event_time(event.time.clone());
app.set_next_event_summary(event.summary.clone());
} else {
app.set_next_event_date("NEXT EVENT".into());
app.set_next_event_time("".into());
app.set_next_event_summary("NO UPCOMING EVENTS".into());
}
app.set_calendar_events(ModelRc::new(VecModel::<CalendarEventData>::from(entities)));
}) {
eprintln!("Failed to upgrade in event loop.\n\t{:?}", err);
}
}
pub fn apply_battery(app: &Weak<AppWindow>, batt: RawBatteryData) {
if let Err(err) = app.upgrade_in_event_loop(move |app| {
app.set_battery(batt.into());
}) {
eprintln!("Failed to upgrade in event loop.\n\t{:?}", err);
}
}
pub fn apply_state(app: &Weak<AppWindow>, entities: Vec<HassEntity>) {
if let Err(err) = app.upgrade_in_event_loop(move |app| {
apply_theme(&app, &entities);
apply_clock(&app, &entities);
apply_lights(&app, &entities);
apply_thermometers(&app, &entities);
apply_weather(&app, &entities);
apply_thermostats(&app, &entities);
}) {
eprintln!("Failed to upgrade in event loop.\n\t{:?}", err);
}
}
pub fn bind_buttons(app: &AppWindow, c2s_tx: UnboundedSender<CommandMessage>) {
// Button Bindings
app.on_toggle_light({
let c2s_tx = c2s_tx.clone();
move |entity_id| {
let id = entity_id.to_string();
if let Err(err) = c2s_tx.send(CommandMessage::ToggleLight { entity_id: id }) {
eprintln!("Failed to send message to server.\n\t{:?}", err);
}
}
});
app.on_set_temperature({
let c2s_tx = c2s_tx.clone();
move |entity_id, value| {
if let Err(err) = c2s_tx.send(CommandMessage::SetTemperature {
entity_id: entity_id.to_string(),
temperature: value,
}) {
eprintln!("Failed to send message to server.\n\t{:?}", err);
}
}
});
app.on_toggle_thermostat({
let c2s_tx = c2s_tx.clone();
move |entity_id| {
let id = entity_id.to_string();
if let Err(err) = c2s_tx.send(CommandMessage::ToggleThermostat { entity_id: id }) {
eprintln!("Failed to send message to server.\n\t{:?}", err);
}
}
});
}
pub async fn update_listener(
app_weak: Weak<AppWindow>,
mut s2c_rx: UnboundedReceiver<EventMessage>,
) {
while let Some(message) = s2c_rx.recv().await {
match message {
EventMessage::InitialState(items) => {
apply_state(&app_weak, items);
}
EventMessage::EntityUpdated(hass_entity) => {
apply_state(&app_weak, vec![hass_entity]);
}
EventMessage::EntityRemoved(hass_entity) => {
apply_state(&app_weak, vec![hass_entity]);
}
EventMessage::CalendarUpdated(events) => {
apply_calendars(&app_weak, events);
}
EventMessage::Battery(batt) => {
apply_battery(&app_weak, batt);
}
EventMessage::NoBattery => {
if let Err(err) = app_weak.upgrade_in_event_loop(move |app| {
app.set_battery(BatteryData {
state: "No Battery".to_shared_string(),
percent: 0.,
});
}) {
eprintln!("Failed to upgrade in event loop.\n\t{:?}", err);
}
}
}
}
}