1786095613
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
HA_DOMAIN=home.local.avii.nl
|
||||
HA_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkNzc4MTBkOWY0NjY0NWVlOGNlZjY4MzZlYWNiOWFlNyIsImlhdCI6MTc4NjA5MDcwNiwiZXhwIjoyMTAxNDUwNzA2fQ.jGZKkOzKQ6qEtMfvrfwL-L9cN5-AVB1VAECgLqAjDKM
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/target
|
||||
.env
|
||||
|
||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
|
||||
188
src/main.rs
188
src/main.rs
@@ -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
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user