1786095613

This commit is contained in:
2026-08-07 11:40:13 +02:00
parent 7d9bda13e9
commit 7e7a93e87b
6 changed files with 192 additions and 28 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
HA_DOMAIN=home.local.avii.nl
HA_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkNzc4MTBkOWY0NjY0NWVlOGNlZjY4MzZlYWNiOWFlNyIsImlhdCI6MTc4NjA5MDcwNiwiZXhwIjoyMTAxNDUwNzA2fQ.jGZKkOzKQ6qEtMfvrfwL-L9cN5-AVB1VAECgLqAjDKM

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target
.env

8
Cargo.lock generated
View File

@@ -898,12 +898,14 @@ dependencies = [
name = "dashboard"
version = "0.1.0"
dependencies = [
"dotenvy",
"embedded-graphics",
"embedded-graphics-core",
"evdev",
"framebuffer",
"hass-rs",
"linfb",
"serde_json",
"slint",
"slint-build",
"tokio",
@@ -1026,6 +1028,12 @@ dependencies = [
"libloading",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "downcast-rs"
version = "1.0.4"

View File

@@ -4,12 +4,14 @@ version = "0.1.0"
edition = "2024"
[dependencies]
dotenvy = "0.15.7"
embedded-graphics = "0.8.2"
embedded-graphics-core = "0.4.1"
evdev = "0.13.2"
framebuffer = "0.3.1"
hass-rs = "0.5.0"
linfb = { version = "0.2.1", default-features = false }
serde_json = "1.0.151"
slint = { version = "1.17.1", default-features = false, features = ["std", "compat-1-2", "libm", "renderer-software", "unsafe-single-threaded"] }
tokio = { version = "1.53.1", features = ["full"] }

View File

@@ -1,36 +1,192 @@
use hass_rs::{HassClient, HassEntity};
use slint::{ModelRc, VecModel};
use std::{
collections::HashMap,
env::var,
sync::{Arc, Mutex},
thread::sleep,
time::Duration,
};
slint::include_modules!();
mod rgba;
#[cfg(not(feature = "dev"))]
mod trekstor;
slint::include_modules!();
// #[derive(Clone, Debug, PartialEq)]
// struct LightState {
// entity_id: String,
// name: String,
// on: bool,
// available: bool,
// }
struct Database {
client: HassClient,
entities: HashMap<String, HassEntity>,
}
impl Database {
pub async fn update_light(&mut self, light: &str, checked: bool) {
self.client
.call_service(
"light".to_string(),
if checked {
"turn_on".to_string()
} else {
"turn_off".to_string()
},
Some(serde_json::json!({
"entity_id": light.to_string()
})),
)
.await
.ok();
}
pub fn get_lights(&self) -> Vec<LightData> {
let mut hidden = vec![];
let lights = self
.entities
.iter()
.filter_map(|(id, e)| {
if id.starts_with("light.") {
if let Some(children) = e.attributes["entity_id"].as_array() {
for child in children {
hidden.push(child.as_str().unwrap().to_string());
}
}
return Some(LightData {
available: e.state != "unavailable",
entity_id: e.entity_id.clone().into(),
name: e.attributes["friendly_name"].as_str().unwrap().into(),
on: e.state == "on",
});
}
None
})
.collect::<Vec<_>>();
let mut lights = lights
.iter()
.filter(|e| {
!e.entity_id.ends_with("screen") && !hidden.contains(&e.entity_id.to_string())
})
.map(|e| e.to_owned())
.collect::<Vec<_>>();
lights.sort_by(|a, b| a.name.cmp(&b.name));
lights
}
}
type DB = Arc<Mutex<Database>>;
#[tokio::main]
async fn main() -> Result<(), slint::PlatformError> {
dotenvy::dotenv().ok();
#[cfg(not(feature = "dev"))]
slint::platform::set_platform(Box::new(crate::trekstor::Trekstor::new()))
.expect("set platform");
let main_window = MainWindow::new()?;
let lights = vec![LightData {
available: true,
entity_id: "light.living_room".into(),
name: "Living Room Lights".into(),
on: false,
}];
// start home_assistant webscoket
let db = home_assisstant_websocket_client().await;
main_window.set_lights(ModelRc::new(VecModel::from(lights)));
let db_weak = db.clone();
let weak_window = main_window.as_weak();
main_window.on_toggle_light(move |light, checked| {
let db_weaker = db_weak.clone();
slint::spawn_local(async move {
db_weaker
.lock()
.unwrap()
.update_light(&light.entity_id.to_string(), checked)
.await;
})
.ok();
// let weak_window = main_window.as_weak();
// slint::spawn_local(async move {
// sleep(Duration::from_secs(5));
// weak_window.upgrade_in_event_loop(move |ui| {
// ui.set_text("GoodBye!".into());
// })
// })
// .unwrap();
let lights = db_weak.lock().unwrap().get_lights();
weak_window
.upgrade_in_event_loop(move |ui| {
ui.set_lights(ModelRc::new(VecModel::from(lights)));
})
.ok();
// println!("{} {}", entity_id, checked);
});
let weak_window = main_window.as_weak();
tokio::spawn(async move {
loop {
let lights = db.lock().unwrap().get_lights();
weak_window
.upgrade_in_event_loop(move |ui| {
ui.set_lights(ModelRc::new(VecModel::from(lights)));
})
.ok();
sleep(Duration::from_secs(1));
}
});
main_window.run()
}
// todo: add channel to send actions to HA, maybe on the db object? prolly not, right?
async fn home_assisstant_websocket_client() -> DB {
let hass_ws_url = var("HASS_WS_URL").expect("HASS_WS_URL not set in environment");
let hass_token = var("HASS_TOKEN").expect("HA_TOKEN not set in environment");
let mut client = HassClient::new(&hass_ws_url)
.await
.expect("Failed to connect");
client
.auth_with_longlivedtoken(&hass_token)
.await
.expect("Not able to authenticate");
let db = Arc::new(Mutex::new(Database {
client,
entities: Default::default(),
}));
let mut event_receiver = db
.lock()
.unwrap()
.client
.subscribe_event("state_changed")
.await
.expect("Failed to subscribe");
let states = db
.lock()
.unwrap()
.client
.get_states()
.await
.expect("unable to get states");
states.iter().for_each(|entity| {
db.lock()
.expect("Database poisoned")
.entities
.insert(entity.entity_id.clone(), entity.clone());
});
let db_weak = db.clone();
tokio::spawn(async move {
while let Some(message) = event_receiver.recv().await {
if let Some(entity) = &message.event.data.new_state {
db_weak
.lock()
.expect("Database poisoned")
.entities
.insert(entity.entity_id.clone(), entity.clone());
}
}
});
db
}

View File

@@ -18,11 +18,7 @@ export struct LightData {
component LightSwitch inherits Horizontal {
in property <LightData> light;
callback toggled(string);
TouchArea {
clicked => { if root.light.available { root.toggled(root.light.entity_id); } }
}
callback toggled(LightData, bool);
alignment: end;
label := Text {
@@ -33,8 +29,9 @@ component LightSwitch inherits Horizontal {
font-weight: root.light.on ? 700 : 500;
}
switch := Switch {
enabled: root.light.on;
checked: root.light.available;
enabled: root.light.available;
checked: root.light.on;
checked_state_changed(checked) => { root.toggled(root.light, checked) }
}
}
@@ -48,15 +45,13 @@ export component MainWindow inherits Window {
default-font-family: "Roboto";
in property <[LightData]> lights;
callback toggle-light(string);
callback toggle-light(LightData, bool);
in-out property <int> current-page: 0;
in property <string> next-event-label: "NEXT EVENT";
in property <string> next-event-title: "LOADING CALENDAR";
in property <string> next-event-time: "6:00 pm";
callback user-activity();
Rectangle {
x: 0;
y: 0;
@@ -78,7 +73,7 @@ export component MainWindow inherits Window {
y: 5px;
width: parent.width - 78px;
height: 78px;
clicked => { root.user-activity(); root.current-page = 2; }
clicked => { root.current-page = 2; }
}
Text {
@@ -136,7 +131,7 @@ export component MainWindow inherits Window {
for light_data in root.lights: LightSwitch {
light: light_data;
toggled(entity_id) => { root.user-activity(); root.toggle-light(entity_id); }
toggled(light, checked) => { root.toggle-light(light, checked); }
}
}
}