1786278419
This commit is contained in:
27
Cargo.lock
generated
27
Cargo.lock
generated
@@ -57,7 +57,7 @@ dependencies = [
|
|||||||
"ndk-context",
|
"ndk-context",
|
||||||
"ndk-sys",
|
"ndk-sys",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -312,7 +312,7 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"pastey",
|
"pastey",
|
||||||
"rayon",
|
"rayon",
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.20",
|
||||||
"v_frame",
|
"v_frame",
|
||||||
"y4m",
|
"y4m",
|
||||||
]
|
]
|
||||||
@@ -851,6 +851,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"slint",
|
"slint",
|
||||||
"slint-build",
|
"slint-build",
|
||||||
|
"thiserror 2.0.20",
|
||||||
"tokio",
|
"tokio",
|
||||||
"trekstor",
|
"trekstor",
|
||||||
"url",
|
"url",
|
||||||
@@ -1636,7 +1637,7 @@ dependencies = [
|
|||||||
"parking_lot",
|
"parking_lot",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.20",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
]
|
]
|
||||||
@@ -2304,7 +2305,7 @@ dependencies = [
|
|||||||
"jni-sys 0.4.1",
|
"jni-sys 0.4.1",
|
||||||
"log",
|
"log",
|
||||||
"simd_cesu8",
|
"simd_cesu8",
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.20",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
@@ -2552,7 +2553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7755f08423275157ad1680aaecc9ccb7e0cc633da3240fea2d1522935cc15c72"
|
checksum = "7755f08423275157ad1680aaecc9ccb7e0cc633da3240fea2d1522935cc15c72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lyon_path",
|
"lyon_path",
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2662,7 +2663,7 @@ dependencies = [
|
|||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png",
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.20",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3662,7 +3663,7 @@ dependencies = [
|
|||||||
"rand",
|
"rand",
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"simd_helpers",
|
"simd_helpers",
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.20",
|
||||||
"v_frame",
|
"v_frame",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@@ -4476,11 +4477,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.19"
|
version = "2.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
|
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 2.0.19",
|
"thiserror-impl 2.0.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4496,9 +4497,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.19"
|
version = "2.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
|
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -4739,7 +4740,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"rand",
|
"rand",
|
||||||
"sha1",
|
"sha1",
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ tokio = { version = "1.53.1", features = ["full"] }
|
|||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
chrono = "0.4.45"
|
chrono = "0.4.45"
|
||||||
|
thiserror = "2.0.20"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
dev = ["slint/backend-winit-x11"]
|
dev = ["slint/backend-winit-x11"]
|
||||||
|
|||||||
@@ -1,84 +1,105 @@
|
|||||||
mod ha_ext;
|
mod ha_ext;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use dotenvy::var;
|
use dotenvy::var;
|
||||||
use hass_rs::{HassClient, HassEntity};
|
use hass_rs::{HassClient, HassEntity};
|
||||||
use serde_json::{Value, json};
|
use serde_json::json;
|
||||||
use slint::language::ColorScheme;
|
use slint::language::ColorScheme;
|
||||||
use slint::{ModelRc, VecModel, Weak};
|
use slint::{ModelRc, VecModel, Weak};
|
||||||
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
||||||
slint::include_modules!();
|
slint::include_modules!();
|
||||||
|
|
||||||
|
fn apply_theme(app: &AppWindow, state: &State) {
|
||||||
|
if let Some(illuminance) = state
|
||||||
|
.get("sensor.diyless_thermostat_3_ambient_light_level")
|
||||||
|
.as_ref()
|
||||||
|
&& let Ok(illuminance) = illuminance.state.parse::<i32>()
|
||||||
|
{
|
||||||
|
let color_scheme = if illuminance < 250 {
|
||||||
|
ColorScheme::Dark
|
||||||
|
} else {
|
||||||
|
ColorScheme::Light
|
||||||
|
};
|
||||||
|
|
||||||
|
app.set_color_scheme(color_scheme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_clock(app: &AppWindow, state: &State) {
|
||||||
|
if let Some(date_time_iso) = state.get("sensor.date_time_iso").as_ref() {
|
||||||
|
app.set_date_time(date_time_iso.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_lights(app: &AppWindow, state: &State) {
|
||||||
|
let lights = state
|
||||||
|
.domain("light")
|
||||||
|
.iter()
|
||||||
|
.filter(|light| {
|
||||||
|
!(light.entity_id.ends_with("screen") || light.entity_id.chars().any(char::is_numeric))
|
||||||
|
})
|
||||||
|
.map(Into::into)
|
||||||
|
.collect::<Vec<LightData>>();
|
||||||
|
app.set_lights(ModelRc::new(VecModel::from(lights)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_thermometers(app: &AppWindow, state: &State) {
|
||||||
|
let thermometers = state
|
||||||
|
.domain("sensor")
|
||||||
|
.iter()
|
||||||
|
.filter(|sensor| sensor.entity_id.ends_with("thermometer_temperature"))
|
||||||
|
.map(Into::into)
|
||||||
|
.collect::<Vec<ThermometerData>>();
|
||||||
|
app.set_thermometers(ModelRc::new(VecModel::from(thermometers)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_weather(app: &AppWindow, state: &State) {
|
||||||
|
if let Some(weather) = state.get("weather.forecast_home").as_ref() {
|
||||||
|
app.set_weather(weather.into());
|
||||||
|
}
|
||||||
|
app.set_is_night(
|
||||||
|
state
|
||||||
|
.get("sun.sun")
|
||||||
|
.is_some_and(|sun| sun.state == "below_horizon"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_thermostats(app: &AppWindow, state: &State) {
|
||||||
|
let thermostats = state
|
||||||
|
.domain("climate")
|
||||||
|
.iter()
|
||||||
|
.filter(|sensor| !sensor.entity_id.contains("diyless"))
|
||||||
|
.map(Into::into)
|
||||||
|
.collect::<Vec<ThermostatData>>();
|
||||||
|
|
||||||
|
app.set_thermostats(ModelRc::new(VecModel::from(thermostats)));
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_state(app: &Weak<AppWindow>, state: State) {
|
fn apply_state(app: &Weak<AppWindow>, state: State) {
|
||||||
app.upgrade_in_event_loop(move |app| {
|
if let Err(err) = app.upgrade_in_event_loop(move |app| {
|
||||||
// Light/Dark mode
|
apply_theme(&app, &state);
|
||||||
//
|
apply_clock(&app, &state);
|
||||||
if let Some(illuminance) = state
|
apply_lights(&app, &state);
|
||||||
.get("sensor.diyless_thermostat_3_ambient_light_level")
|
apply_thermometers(&app, &state);
|
||||||
.as_ref()
|
apply_weather(&app, &state);
|
||||||
&& let Ok(illuminance) = illuminance.state.parse::<i32>()
|
apply_thermostats(&app, &state);
|
||||||
{
|
}) {
|
||||||
let scheme = if illuminance < 300 {
|
eprintln!("Failed to upgrade in event loop.\n\t{:?}", err);
|
||||||
ColorScheme::Dark
|
}
|
||||||
} else {
|
}
|
||||||
ColorScheme::Light
|
|
||||||
};
|
|
||||||
|
|
||||||
app.set_color_scheme(scheme);
|
fn toggle_action(current_state: &str, on_state: &str) -> &'static str {
|
||||||
}
|
if current_state == on_state {
|
||||||
|
"turn_off"
|
||||||
// Lights
|
} else {
|
||||||
let lights = state
|
"turn_on"
|
||||||
.domain("light")
|
}
|
||||||
.iter()
|
|
||||||
.filter(|light| {
|
|
||||||
!(light.entity_id.ends_with("screen")
|
|
||||||
|| light.entity_id.chars().any(char::is_numeric))
|
|
||||||
})
|
|
||||||
.map(Into::into)
|
|
||||||
.collect::<Vec<LightData>>();
|
|
||||||
app.set_lights(ModelRc::new(VecModel::from(lights)));
|
|
||||||
|
|
||||||
// Thermometers
|
|
||||||
let thermometers = state
|
|
||||||
.domain("sensor")
|
|
||||||
.iter()
|
|
||||||
.filter(|sensor| sensor.entity_id.ends_with("thermometer_temperature"))
|
|
||||||
.map(Into::into)
|
|
||||||
.collect::<Vec<ThermometerData>>();
|
|
||||||
app.set_thermometers(ModelRc::new(VecModel::from(thermometers)));
|
|
||||||
|
|
||||||
// Clock
|
|
||||||
if let Some(date_time_iso) = state.get("sensor.date_time_iso").as_ref() {
|
|
||||||
app.set_date_time(date_time_iso.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Weather
|
|
||||||
if let Some(weather) = state.get("weather.forecast_home").as_ref() {
|
|
||||||
app.set_weather(weather.into());
|
|
||||||
}
|
|
||||||
app.set_is_night(
|
|
||||||
state
|
|
||||||
.get("sun.sun")
|
|
||||||
.is_some_and(|sun| sun.state == "below_horizon"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Thermostats
|
|
||||||
let thermostats = state
|
|
||||||
.domain("climate")
|
|
||||||
.iter()
|
|
||||||
.filter(|sensor| !sensor.entity_id.contains("diyless"))
|
|
||||||
.map(Into::into)
|
|
||||||
.collect::<Vec<ThermostatData>>();
|
|
||||||
|
|
||||||
app.set_thermostats(ModelRc::new(VecModel::from(thermostats)));
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), AppError> {
|
||||||
dotenvy::dotenv().ok();
|
dotenvy::dotenv().ok();
|
||||||
|
|
||||||
#[cfg(not(feature = "dev"))]
|
#[cfg(not(feature = "dev"))]
|
||||||
@@ -98,37 +119,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let app_weak = app.as_weak();
|
let app_weak = app.as_weak();
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
async move {
|
async move {
|
||||||
loop {
|
while let Some(S2cMessage::EntityUpdated) = s2c_rx.recv().await {
|
||||||
if let Some(S2cMessage::EntityUpdated) = s2c_rx.recv().await {
|
apply_state(&app_weak, state.clone());
|
||||||
apply_state(&app_weak, state.clone());
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Button Bindings
|
||||||
app.on_toggle_light({
|
app.on_toggle_light({
|
||||||
let state = state.clone();
|
|
||||||
let c2s_tx = c2s_tx.clone();
|
let c2s_tx = c2s_tx.clone();
|
||||||
move |entity_id| {
|
move |entity_id| {
|
||||||
let id = entity_id.to_string();
|
let id = entity_id.to_string();
|
||||||
|
|
||||||
let payload = Some(json!({
|
if let Err(err) = c2s_tx.send(C2sMessage::ToggleLight { entity_id: id }) {
|
||||||
"entity_id": id
|
eprintln!("Failed to send message to server.\n\t{:?}", err);
|
||||||
}));
|
|
||||||
|
|
||||||
if let Some(entity) = state.get(&id) {
|
|
||||||
let action = if entity.state == "on" {
|
|
||||||
"turn_off"
|
|
||||||
} else {
|
|
||||||
"turn_on"
|
|
||||||
};
|
|
||||||
c2s_tx
|
|
||||||
.send(C2sMessage::CallService(
|
|
||||||
"light".into(),
|
|
||||||
action.into(),
|
|
||||||
payload.clone(),
|
|
||||||
))
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -136,56 +140,60 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
app.on_set_temperature({
|
app.on_set_temperature({
|
||||||
let c2s_tx = c2s_tx.clone();
|
let c2s_tx = c2s_tx.clone();
|
||||||
move |entity_id, value| {
|
move |entity_id, value| {
|
||||||
let payload = Some(json!({
|
if let Err(err) = c2s_tx.send(C2sMessage::SetTemperature {
|
||||||
"entity_id": entity_id.to_string(),
|
entity_id: entity_id.to_string(),
|
||||||
"temperature": value
|
temperature: value,
|
||||||
}));
|
}) {
|
||||||
|
eprintln!("Failed to send message to server.\n\t{:?}", err);
|
||||||
c2s_tx
|
|
||||||
.send(C2sMessage::CallService(
|
|
||||||
"climate".into(),
|
|
||||||
"set_temperature".into(),
|
|
||||||
payload.clone(),
|
|
||||||
))
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on_toggle_thermostat({
|
|
||||||
let state = state.clone();
|
|
||||||
let c2s_tx = c2s_tx.clone();
|
|
||||||
move |entity_id| {
|
|
||||||
let id = entity_id.to_string();
|
|
||||||
|
|
||||||
let payload = Some(json!({
|
|
||||||
"entity_id": id
|
|
||||||
}));
|
|
||||||
|
|
||||||
if let Some(entity) = state.get(&id) {
|
|
||||||
let action = if entity.state == "heat" {
|
|
||||||
"turn_off"
|
|
||||||
} else {
|
|
||||||
"turn_on"
|
|
||||||
};
|
|
||||||
c2s_tx
|
|
||||||
.send(C2sMessage::CallService(
|
|
||||||
"climate".into(),
|
|
||||||
action.into(),
|
|
||||||
payload.clone(),
|
|
||||||
))
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.run()?;
|
app.on_toggle_thermostat({
|
||||||
|
let c2s_tx = c2s_tx.clone();
|
||||||
|
move |entity_id| {
|
||||||
|
let id = entity_id.to_string();
|
||||||
|
|
||||||
Ok(())
|
if let Err(err) = c2s_tx.send(C2sMessage::ToggleThermostat { entity_id: id }) {
|
||||||
|
eprintln!("Failed to send message to server.\n\t{:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.run().map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
enum AppError {
|
||||||
|
#[error("HASS_URL is not set")]
|
||||||
|
MissingUrl,
|
||||||
|
|
||||||
|
#[error("HASS_TOKEN is not set")]
|
||||||
|
MissingToken,
|
||||||
|
|
||||||
|
#[error("invalid HASS_URL")]
|
||||||
|
InvalidUrl,
|
||||||
|
|
||||||
|
#[error("Unsupported HASS_URL scheme: {0}")]
|
||||||
|
InvalidScheme(String),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
SlintPlatform(#[from] slint::PlatformError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
HomeAssistant(#[from] Box<hass_rs::HassError>),
|
||||||
|
|
||||||
|
#[error("Entity {0} not found")]
|
||||||
|
NoEntity(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum C2sMessage {
|
enum C2sMessage {
|
||||||
CallService(String, String, Option<Value>),
|
ToggleLight { entity_id: String },
|
||||||
|
|
||||||
|
ToggleThermostat { entity_id: String },
|
||||||
|
|
||||||
|
SetTemperature { entity_id: String, temperature: f32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -194,36 +202,36 @@ enum S2cMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
struct State(Arc<RwLock<Vec<HassEntity>>>);
|
struct State(Arc<RwLock<HashMap<String, HassEntity>>>);
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn update(&self, entity: &HassEntity) {
|
pub fn update(&self, entity: &HassEntity) {
|
||||||
let mut entities = self.0.write().unwrap();
|
self.0
|
||||||
|
.write()
|
||||||
if let Some(existing) = entities
|
.unwrap()
|
||||||
.iter_mut()
|
.insert(entity.entity_id.clone(), entity.clone());
|
||||||
.find(|e| e.entity_id == entity.entity_id)
|
|
||||||
{
|
|
||||||
*existing = entity.clone();
|
|
||||||
} else {
|
|
||||||
entities.push(entity.clone());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn domain(&self, domain: &str) -> Vec<HassEntity> {
|
pub fn domain(&self, domain: &str) -> Vec<HassEntity> {
|
||||||
let entities = self.0.read().unwrap();
|
let entities = self.0.read().unwrap();
|
||||||
|
|
||||||
entities
|
let mut entities = entities
|
||||||
.iter()
|
.values()
|
||||||
.filter(|e| e.entity_id.starts_with(domain))
|
.filter(|e| {
|
||||||
|
e.entity_id
|
||||||
|
.split_once('.')
|
||||||
|
.is_some_and(|(d, _)| d == domain)
|
||||||
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
entities.sort_by(|a, b| a.entity_id.cmp(&b.entity_id));
|
||||||
|
|
||||||
|
entities
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, entity_id: &str) -> Option<HassEntity> {
|
pub fn get(&self, entity_id: &str) -> Option<HassEntity> {
|
||||||
let entities = self.0.read().unwrap();
|
self.0.read().unwrap().get(entity_id).cloned()
|
||||||
|
|
||||||
entities.iter().find(|e| e.entity_id == entity_id).cloned()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,50 +239,111 @@ async fn hass(
|
|||||||
s2c_tx: UnboundedSender<S2cMessage>,
|
s2c_tx: UnboundedSender<S2cMessage>,
|
||||||
mut c2s_rx: UnboundedReceiver<C2sMessage>,
|
mut c2s_rx: UnboundedReceiver<C2sMessage>,
|
||||||
state: State,
|
state: State,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), AppError> {
|
||||||
let Ok(url) = var("HASS_URL") else {
|
let Ok(url) = var("HASS_URL") else {
|
||||||
return Err("NO HASS_URL SET".into());
|
return Err(AppError::MissingUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(hass_token) = var("HASS_TOKEN") else {
|
let Ok(hass_token) = var("HASS_TOKEN") else {
|
||||||
return Err("NO HASS_TOKEN SET".into());
|
return Err(AppError::MissingToken);
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(url) = url::Url::parse(&url) else {
|
let Ok(url) = url::Url::parse(&url) else {
|
||||||
return Err("INVALID HASS_URL".into());
|
return Err(AppError::InvalidUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
let url = format!("ws://{}/api/websocket", url.authority());
|
let scheme = match url.scheme() {
|
||||||
|
"http" => "ws",
|
||||||
|
"https" => "wss",
|
||||||
|
scheme => return Err(AppError::InvalidScheme(scheme.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
let mut client = HassClient::new(&url).await?;
|
let url = format!("{}://{}/api/websocket", scheme, url.authority());
|
||||||
client.auth_with_longlivedtoken(&hass_token).await?;
|
|
||||||
|
|
||||||
let states = client.get_states().await?;
|
let mut client = HassClient::new(&url).await.map_err(Box::new)?;
|
||||||
|
client
|
||||||
|
.auth_with_longlivedtoken(&hass_token)
|
||||||
|
.await
|
||||||
|
.map_err(Box::new)?;
|
||||||
|
|
||||||
|
let states = client.get_states().await.map_err(Box::new)?;
|
||||||
|
|
||||||
states.iter().for_each(|entity| {
|
states.iter().for_each(|entity| {
|
||||||
state.update(entity);
|
state.update(entity);
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut event_receiver = client.subscribe_event("state_changed").await?;
|
let mut event_receiver = client
|
||||||
tokio::spawn(async move {
|
.subscribe_event("state_changed")
|
||||||
while let Some(message) = event_receiver.recv().await {
|
.await
|
||||||
if let Some(entity) = &message.event.data.new_state {
|
.map_err(Box::new)?;
|
||||||
state.update(entity);
|
|
||||||
s2c_tx.send(S2cMessage::EntityUpdated).ok();
|
tokio::spawn({
|
||||||
|
let state = state.clone();
|
||||||
|
async move {
|
||||||
|
while let Some(message) = event_receiver.recv().await {
|
||||||
|
if let Some(entity) = &message.event.data.new_state {
|
||||||
|
state.update(entity);
|
||||||
|
if let Err(err) = s2c_tx.send(S2cMessage::EntityUpdated) {
|
||||||
|
eprintln!("Failed to send message to client.\n\t{:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn({
|
||||||
loop {
|
let state = state.clone();
|
||||||
if let Some(C2sMessage::CallService(domain, service, service_data)) =
|
async move {
|
||||||
c2s_rx.recv().await
|
while let Some(message) = c2s_rx.recv().await {
|
||||||
{
|
if let Err(err) = match message {
|
||||||
client
|
C2sMessage::ToggleLight { entity_id } => {
|
||||||
.call_service(domain, service, service_data)
|
if let Some(entity) = state.get(&entity_id) {
|
||||||
.await
|
let payload = json!({
|
||||||
.unwrap();
|
"entity_id": entity_id
|
||||||
};
|
});
|
||||||
|
let service = toggle_action(&entity.state, "on");
|
||||||
|
client
|
||||||
|
.call_service("light".into(), service.into(), Some(payload))
|
||||||
|
.await
|
||||||
|
.map_err(Box::new)
|
||||||
|
.map_err(Into::into)
|
||||||
|
} else {
|
||||||
|
Err(AppError::NoEntity(entity_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
C2sMessage::ToggleThermostat { entity_id } => {
|
||||||
|
if let Some(entity) = state.get(&entity_id) {
|
||||||
|
let payload = json!({
|
||||||
|
"entity_id": entity_id
|
||||||
|
});
|
||||||
|
let service = toggle_action(&entity.state, "heat");
|
||||||
|
client
|
||||||
|
.call_service("climate".into(), service.into(), Some(payload))
|
||||||
|
.await
|
||||||
|
.map_err(Box::new)
|
||||||
|
.map_err(Into::into)
|
||||||
|
} else {
|
||||||
|
Err(AppError::NoEntity(entity_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
C2sMessage::SetTemperature {
|
||||||
|
entity_id,
|
||||||
|
temperature,
|
||||||
|
} => {
|
||||||
|
let payload = json!({
|
||||||
|
"entity_id": entity_id,
|
||||||
|
"temperature": temperature
|
||||||
|
});
|
||||||
|
client
|
||||||
|
.call_service("climate".into(), "set_temperature".into(), Some(payload))
|
||||||
|
.await
|
||||||
|
.map_err(Box::new)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
eprintln!("Failed to execute Service Call.\n\t{:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -96,9 +96,11 @@ export component Climate inherits Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for thermostat[index] in thermostats: ListTile {
|
for thermostat[index] in thermostats: ListTile {
|
||||||
|
avatar_icon: Icons.thermostat;
|
||||||
text: thermostat.name;
|
text: thermostat.name;
|
||||||
supporting-text: thermostat.available ? thermostat.state : "UNAVAILABLE";
|
supporting-text: thermostat.available ? thermostat.state : "UNAVAILABLE";
|
||||||
height: 72px;
|
height: 72px;
|
||||||
|
enabled: false;
|
||||||
|
|
||||||
ElevatedButton {
|
ElevatedButton {
|
||||||
icon: Icons.remove;
|
icon: Icons.remove;
|
||||||
@@ -111,7 +113,7 @@ export component Climate inherits Rectangle {
|
|||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
Text {
|
Text {
|
||||||
text: thermostat.target;
|
text: thermostat.target;
|
||||||
color: MaterialPalette.primary;
|
// color: MaterialPalette.primary;
|
||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
vertical-alignment: center;
|
vertical-alignment: center;
|
||||||
horizontal-alignment: right;
|
horizontal-alignment: right;
|
||||||
@@ -123,7 +125,7 @@ export component Climate inherits Rectangle {
|
|||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
Text {
|
Text {
|
||||||
text: thermostat.unit;
|
text: thermostat.unit;
|
||||||
color: MaterialPalette.primary;
|
// color: MaterialPalette.primary;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
horizontal-alignment: left;
|
horizontal-alignment: left;
|
||||||
vertical-alignment: top;
|
vertical-alignment: top;
|
||||||
@@ -133,7 +135,7 @@ export component Climate inherits Rectangle {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: ".\{thermostat.remainder}";
|
text: ".\{thermostat.remainder}";
|
||||||
color: MaterialPalette.primary;
|
// color: MaterialPalette.primary;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
horizontal-alignment: left;
|
horizontal-alignment: left;
|
||||||
vertical-alignment: bottom;
|
vertical-alignment: bottom;
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export component ListTile {
|
|||||||
|
|
||||||
if root.avatar_text != "": MaterialText {
|
if root.avatar_text != "": MaterialText {
|
||||||
text: root.avatar_text;
|
text: root.avatar_text;
|
||||||
vertical_alignment: center;
|
vertical_alignment: bottom;
|
||||||
horizontal_alignment: center;
|
horizontal_alignment: center;
|
||||||
color: root.avatar_foreground;
|
color: root.avatar_foreground;
|
||||||
style: MaterialTypography.title_medium;
|
style: MaterialTypography.title_medium;
|
||||||
@@ -100,7 +100,7 @@ export component ListTile {
|
|||||||
states [
|
states [
|
||||||
disabled when !root.enabled: {
|
disabled when !root.enabled: {
|
||||||
state_layer.display_background: false;
|
state_layer.display_background: false;
|
||||||
title_layout.opacity: 38%;
|
// title_layout.opacity: 38%;
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user