1786298825
This commit is contained in:
226
Cargo.lock
generated
226
Cargo.lock
generated
@@ -346,6 +346,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "2.0.1"
|
||||
@@ -684,6 +690,35 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie_store"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"document-features",
|
||||
"idna",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "copypasta"
|
||||
version = "0.10.2"
|
||||
@@ -849,12 +884,14 @@ dependencies = [
|
||||
"dotenvy",
|
||||
"hass-rs",
|
||||
"paste",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slint",
|
||||
"slint-build",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"trekstor",
|
||||
"ureq",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -870,6 +907,12 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
@@ -950,6 +993,15 @@ dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
@@ -1445,6 +1497,17 @@ dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
@@ -2513,6 +2576,12 @@ version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
@@ -2788,6 +2857,12 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
@@ -3459,6 +3534,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@@ -3841,6 +3922,20 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rowan"
|
||||
version = "0.16.1"
|
||||
@@ -3922,6 +4017,41 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
@@ -4352,6 +4482,12 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "svgtypes"
|
||||
version = "0.16.1"
|
||||
@@ -4521,6 +4657,36 @@ dependencies = [
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.12.0"
|
||||
@@ -4855,12 +5021,49 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "unty"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"cookie_store",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ureq-proto",
|
||||
"utf8-zero",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -4879,7 +5082,7 @@ version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d46cf96c5f498d36b7a9693bc6a7075c0bb9303189d61b2249b0dc3d309c07de"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"data-url",
|
||||
"flate2",
|
||||
"fontdb",
|
||||
@@ -4901,6 +5104,12 @@ dependencies = [
|
||||
"xmlwriter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8-zero"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -5074,6 +5283,15 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
@@ -5694,6 +5912,12 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.4"
|
||||
|
||||
@@ -14,6 +14,8 @@ url = "2.5.8"
|
||||
chrono = "0.4.45"
|
||||
thiserror = "2.0.20"
|
||||
paste = "1.0.15"
|
||||
ureq = { version = "3.4.0", default-features = false, features = ["json", "rustls"] }
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
dev = ["slint/backend-winit-x11"]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#[allow(clippy::all)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AppError {
|
||||
#[error("HASS_URL is not set")]
|
||||
|
||||
@@ -44,7 +44,7 @@ impl HassEntityExt for HassEntity {
|
||||
impl From<&HassEntity> for LightData {
|
||||
fn from(value: &HassEntity) -> Self {
|
||||
Self {
|
||||
id: value.entity_id.clone().into(),
|
||||
id: value.entity_id.to_shared_string(),
|
||||
name: value
|
||||
.friendly_name()
|
||||
.replace(" Lights", "")
|
||||
@@ -61,7 +61,7 @@ impl From<&HassEntity> for ThermometerData {
|
||||
let v: f32 = value.state.parse().unwrap_or_default();
|
||||
|
||||
Self {
|
||||
id: value.entity_id.clone().into(),
|
||||
id: value.entity_id.to_shared_string(),
|
||||
name: value.friendly_name().into(),
|
||||
value: format!("{:.1}", v).into(),
|
||||
unit: value.unit_of_measurement().into(),
|
||||
@@ -71,21 +71,19 @@ impl From<&HassEntity> for ThermometerData {
|
||||
|
||||
impl From<&HassEntity> for DateTimeData {
|
||||
fn from(value: &HassEntity) -> Self {
|
||||
let mut data = Self {
|
||||
id: value.entity_id.to_shared_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Ok(datetime) =
|
||||
chrono::NaiveDateTime::parse_from_str(&value.state, "%Y-%m-%dT%H:%M:%S")
|
||||
{
|
||||
return Self {
|
||||
id: value.entity_id.clone().into(),
|
||||
date: datetime.format("%a %m %b").to_shared_string(),
|
||||
time: datetime.format("%H:%M").to_shared_string(),
|
||||
};
|
||||
data.date = datetime.format("%a %m %b").to_shared_string();
|
||||
data.time = datetime.format("%H:%M").to_shared_string();
|
||||
}
|
||||
|
||||
Self {
|
||||
id: value.entity_id.clone().into(),
|
||||
date: "0-0-0000".into(),
|
||||
time: "00:00".into(),
|
||||
}
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,64 +137,6 @@ impl From<&HassEntity> for WeatherData {
|
||||
|
||||
let label = pretty_label(&value.state).to_shared_string();
|
||||
|
||||
let attribution = value
|
||||
.attributes
|
||||
.get("attribution")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_shared_string();
|
||||
|
||||
let cloud_coverage = value
|
||||
.attributes
|
||||
.get("cloud_coverage")
|
||||
.and_then(|x| x.as_i64())
|
||||
.unwrap_or_default() as i32;
|
||||
|
||||
let dew_point = value
|
||||
.attributes
|
||||
.get("dew_point")
|
||||
.and_then(|x| x.as_f64())
|
||||
.unwrap_or_default() as f32;
|
||||
|
||||
let friendly_name = value
|
||||
.attributes
|
||||
.get("friendly_name")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_shared_string();
|
||||
|
||||
let humidity = value
|
||||
.attributes
|
||||
.get("humidity")
|
||||
.and_then(|x| x.as_i64())
|
||||
.unwrap_or_default() as i32;
|
||||
|
||||
let precipitation_unit = value
|
||||
.attributes
|
||||
.get("precipitation_unit")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_shared_string();
|
||||
|
||||
let pressure = value
|
||||
.attributes
|
||||
.get("pressure")
|
||||
.and_then(|x| x.as_f64())
|
||||
.unwrap_or_default() as f32;
|
||||
|
||||
let pressure_unit = value
|
||||
.attributes
|
||||
.get("pressure_unit")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_shared_string();
|
||||
|
||||
let supported_features = value
|
||||
.attributes
|
||||
.get("supported_features")
|
||||
.and_then(|x| x.as_i64())
|
||||
.unwrap_or_default() as i32;
|
||||
|
||||
let temperature = value
|
||||
.attributes
|
||||
.get("temperature")
|
||||
@@ -210,57 +150,24 @@ impl From<&HassEntity> for WeatherData {
|
||||
.unwrap_or_default()
|
||||
.to_shared_string();
|
||||
|
||||
let uv_index = value
|
||||
.attributes
|
||||
.get("uv_index")
|
||||
.and_then(|x| x.as_f64())
|
||||
.unwrap_or_default() as f32;
|
||||
|
||||
let visibility_unit = value
|
||||
.attributes
|
||||
.get("visibility_unit")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_shared_string();
|
||||
|
||||
let wind_bearing = value
|
||||
.attributes
|
||||
.get("wind_bearing")
|
||||
.and_then(|x| x.as_f64())
|
||||
.unwrap_or_default() as f32;
|
||||
|
||||
let wind_speed = value
|
||||
.attributes
|
||||
.get("wind_speed")
|
||||
.and_then(|x| x.as_f64())
|
||||
.unwrap_or_default() as f32;
|
||||
|
||||
let wind_speed_unit = value
|
||||
.attributes
|
||||
.get("wind_speed_unit")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_shared_string();
|
||||
|
||||
Self {
|
||||
icon_index,
|
||||
label,
|
||||
attribution,
|
||||
cloud_coverage,
|
||||
dew_point,
|
||||
friendly_name,
|
||||
humidity,
|
||||
precipitation_unit,
|
||||
pressure,
|
||||
pressure_unit,
|
||||
supported_features,
|
||||
temperature,
|
||||
temperature_unit,
|
||||
uv_index,
|
||||
visibility_unit,
|
||||
wind_bearing,
|
||||
wind_speed,
|
||||
wind_speed_unit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// weird spot for this but okay
|
||||
impl From<&crate::home_assistant::CalendarEvent> for CalendarEventData {
|
||||
fn from(value: &crate::home_assistant::CalendarEvent) -> Self {
|
||||
Self {
|
||||
calendar: value.calendar.to_shared_string(),
|
||||
date: value.date.to_shared_string(),
|
||||
id: value.entity_id.to_shared_string(),
|
||||
summary: value.summary.to_shared_string(),
|
||||
time: value.time.to_shared_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
use chrono::{DateTime, Local, NaiveDate};
|
||||
use dotenvy::var;
|
||||
use hass_rs::{HassClient, HassEntity};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
use tokio::{
|
||||
sync::mpsc::{UnboundedReceiver, UnboundedSender},
|
||||
time::sleep,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::AppError,
|
||||
@@ -114,6 +119,18 @@ pub async fn hass(
|
||||
initial_state.sort_by(|a, b| a.entity_id.cmp(&b.entity_id));
|
||||
s2c_tx.send(EventMessage::InitialState(initial_state)).ok();
|
||||
|
||||
tokio::spawn({
|
||||
let s2c_tx = s2c_tx.clone();
|
||||
async move {
|
||||
loop {
|
||||
let ha_client = HaClient::from_environment().unwrap();
|
||||
let events = ha_client.fetch_calendar_events().unwrap();
|
||||
s2c_tx.send(EventMessage::CalendarUpdated(events)).ok();
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut event_receiver = client
|
||||
.subscribe_event("state_changed")
|
||||
.await
|
||||
@@ -162,3 +179,143 @@ fn toggle_action(current_state: &str, on_state: &str) -> &'static str {
|
||||
"turn_on"
|
||||
}
|
||||
}
|
||||
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(12);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HaClient {
|
||||
pub agent: ureq::Agent,
|
||||
pub base_url: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct HaCalendar {
|
||||
pub entity_id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct HaCalendarEvent {
|
||||
pub uid: String,
|
||||
pub summary: String,
|
||||
pub start: HaCalendarTime,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct HaCalendarTime {
|
||||
#[serde(rename = "dateTime")]
|
||||
pub date_time: Option<String>,
|
||||
pub date: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CalendarEvent {
|
||||
pub entity_id: String,
|
||||
pub summary: String,
|
||||
pub date: String,
|
||||
pub time: String,
|
||||
pub calendar: String,
|
||||
pub sort_epoch: i64,
|
||||
}
|
||||
|
||||
impl HaClient {
|
||||
pub fn from_environment() -> Result<Self, String> {
|
||||
let base_url = std::env::var("HASS_URL")
|
||||
.map_err(|_| "HASS_URL is not configured".to_string())?
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
let token =
|
||||
std::env::var("HASS_TOKEN").map_err(|_| "HASS_TOKEN is not configured".to_string())?;
|
||||
let agent = ureq::Agent::new_with_config(
|
||||
ureq::config::Config::builder()
|
||||
.timeout_send_request(Some(REQUEST_TIMEOUT))
|
||||
.timeout_recv_response(Some(REQUEST_TIMEOUT))
|
||||
.timeout_send_body(Some(REQUEST_TIMEOUT))
|
||||
.build(),
|
||||
);
|
||||
Ok(Self {
|
||||
agent,
|
||||
base_url,
|
||||
token,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_calendars(&self) -> Result<Vec<HaCalendar>, String> {
|
||||
self.agent
|
||||
.get(&format!("{}/api/calendars", self.base_url))
|
||||
.header("Authorization", &format!("Bearer {}", self.token))
|
||||
.header("Accept", "application/json")
|
||||
.call()
|
||||
.map_err(|error| format!("calendars: {error}"))?
|
||||
.body_mut()
|
||||
.read_json()
|
||||
.map_err(|error| format!("calendars: invalid response: {error}"))
|
||||
}
|
||||
|
||||
fn get_calendar_events(
|
||||
&self,
|
||||
entity_id: &str,
|
||||
start: &str,
|
||||
end: &str,
|
||||
) -> Result<Vec<HaCalendarEvent>, String> {
|
||||
self.agent
|
||||
.get(&format!("{}/api/calendars/{entity_id}", self.base_url))
|
||||
.query("start", start)
|
||||
.query("end", end)
|
||||
.header("Authorization", &format!("Bearer {}", self.token))
|
||||
.header("Accept", "application/json")
|
||||
.call()
|
||||
.map_err(|error| format!("{entity_id}: {error}"))?
|
||||
.body_mut()
|
||||
.read_json()
|
||||
.map_err(|error| format!("{entity_id}: invalid response: {error}"))
|
||||
}
|
||||
|
||||
pub fn fetch_calendar_events(&self) -> Result<Vec<CalendarEvent>, String> {
|
||||
let now = Local::now();
|
||||
let start = now.to_rfc3339();
|
||||
let end = (now + chrono::Duration::days(30)).to_rfc3339();
|
||||
let mut upcoming = Vec::new();
|
||||
|
||||
for calendar in self.get_calendars()? {
|
||||
let Ok(events) = self.get_calendar_events(&calendar.entity_id, &start, &end) else {
|
||||
continue;
|
||||
};
|
||||
for event in events {
|
||||
let Some((sort_epoch, date, time)) = format_calendar_time(&event.start) else {
|
||||
continue;
|
||||
};
|
||||
upcoming.push(CalendarEvent {
|
||||
entity_id: event.uid,
|
||||
summary: event.summary,
|
||||
date,
|
||||
time,
|
||||
calendar: calendar.name.clone(),
|
||||
sort_epoch,
|
||||
});
|
||||
}
|
||||
}
|
||||
upcoming.sort_by_key(|event| event.sort_epoch);
|
||||
upcoming.truncate(7);
|
||||
Ok(upcoming)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_calendar_time(value: &HaCalendarTime) -> Option<(i64, String, String)> {
|
||||
if let Some(date_time) = value.date_time.as_deref() {
|
||||
let parsed = DateTime::parse_from_rfc3339(date_time).ok()?;
|
||||
return Some((
|
||||
parsed.timestamp(),
|
||||
parsed.format("%a %d %b").to_string().to_uppercase(),
|
||||
parsed.format("%H:%M").to_string().to_uppercase(),
|
||||
));
|
||||
}
|
||||
let date = NaiveDate::parse_from_str(value.date.as_deref()?, "%Y-%m-%d").ok()?;
|
||||
let sort_epoch = date.and_hms_opt(0, 0, 0)?.and_utc().timestamp();
|
||||
Some((
|
||||
sort_epoch,
|
||||
date.format("%a %d %b").to_string().to_uppercase(),
|
||||
"ALL DAY".into(),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use hass_rs::HassEntity;
|
||||
|
||||
use crate::home_assistant::CalendarEvent;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CommandMessage {
|
||||
ToggleLight { entity_id: String },
|
||||
@@ -14,4 +16,5 @@ pub enum EventMessage {
|
||||
InitialState(Vec<HassEntity>),
|
||||
EntityUpdated(HassEntity),
|
||||
EntityRemoved(HassEntity),
|
||||
CalendarUpdated(Vec<CalendarEvent>),
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ slint::include_modules!();
|
||||
|
||||
use crate::{
|
||||
ha_ext::*,
|
||||
home_assistant::CalendarEvent,
|
||||
messages::{CommandMessage, EventMessage},
|
||||
};
|
||||
|
||||
@@ -96,6 +97,18 @@ pub fn apply_thermostats(app: &AppWindow, entities: &[HassEntity]) {
|
||||
});
|
||||
}
|
||||
|
||||
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 = entities.iter().map(Into::into).collect::<Vec<_>>();
|
||||
app.set_calendar_events(ModelRc::new(VecModel::<CalendarEventData>::from(entities)));
|
||||
}) {
|
||||
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);
|
||||
@@ -161,6 +174,9 @@ pub async fn update_listener(
|
||||
EventMessage::EntityRemoved(hass_entity) => {
|
||||
apply_state(&app_weak, vec![hass_entity]);
|
||||
}
|
||||
EventMessage::CalendarUpdated(events) => {
|
||||
apply_calendars(&app_weak, events);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,41 @@
|
||||
import { FilledButton, SnackBar } from "material/material.slint";
|
||||
import { Palette } from "std-widgets.slint";
|
||||
import {
|
||||
FilledButton,
|
||||
SnackBar,
|
||||
ListTile,
|
||||
MaterialText,
|
||||
MaterialPalette,
|
||||
MaterialTypography,
|
||||
} from "material/material.slint";
|
||||
import { Icons } from "material/ui/icons/icons.slint";
|
||||
|
||||
export struct CalendarEventData {
|
||||
id: string,
|
||||
date: string,
|
||||
time: string,
|
||||
summary: string,
|
||||
calendar: string,
|
||||
}
|
||||
|
||||
export component Calendar inherits Rectangle {
|
||||
in property <[CalendarEventData]> calendar-events;
|
||||
|
||||
VerticalLayout {
|
||||
// ..
|
||||
for event[index] in calendar-events: ListTile {
|
||||
avatar_icon: Icons.calendar_today;
|
||||
avatar_foreground: Palette.foreground;
|
||||
text: event.date;
|
||||
supporting-text: event.summary;
|
||||
height: 72px;
|
||||
|
||||
MaterialText {
|
||||
text: event.time;
|
||||
color: MaterialPalette.on_surface;
|
||||
overflow: elide;
|
||||
style: MaterialTypography.title_medium;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Palette, HorizontalBox, VerticalBox } from "std-widgets.slint";
|
||||
import { Icons } from "material/ui/icons/icons.slint";
|
||||
import {
|
||||
ListTile,
|
||||
Vertical,
|
||||
@@ -7,26 +8,12 @@ import {
|
||||
MaterialPalette,
|
||||
FilledButton,
|
||||
} from "material/material.slint";
|
||||
import { Icons } from "material/ui/icons/icons.slint";
|
||||
|
||||
export struct WeatherData {
|
||||
icon-index: int,
|
||||
label: string,
|
||||
temperature: float,
|
||||
dew_point: float,
|
||||
temperature_unit: string,
|
||||
humidity: int,
|
||||
cloud_coverage: int,
|
||||
uv_index: float,
|
||||
pressure: float,
|
||||
pressure_unit: string,
|
||||
wind_bearing: float,
|
||||
wind_speed: float,
|
||||
wind_speed_unit: string,
|
||||
visibility_unit: string,
|
||||
precipitation_unit: string,
|
||||
attribution: string,
|
||||
friendly_name: string,
|
||||
supported_features: int,
|
||||
}
|
||||
|
||||
export struct ThermostatData {
|
||||
@@ -114,7 +101,6 @@ export component Climate inherits Rectangle {
|
||||
HorizontalLayout {
|
||||
Text {
|
||||
text: thermostat.target;
|
||||
// color: MaterialPalette.primary;
|
||||
font-size: 40px;
|
||||
vertical-alignment: center;
|
||||
horizontal-alignment: right;
|
||||
@@ -126,7 +112,6 @@ export component Climate inherits Rectangle {
|
||||
padding-top: 2px;
|
||||
Text {
|
||||
text: thermostat.unit;
|
||||
// color: MaterialPalette.primary;
|
||||
font-size: 14px;
|
||||
horizontal-alignment: left;
|
||||
vertical-alignment: top;
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
} from "std-widgets.slint";
|
||||
|
||||
import { Climate, WeatherData, ThermostatData } from "./climate.slint";
|
||||
import { CalendarEventData } from "./calendar.slint";
|
||||
|
||||
import { Calendar } from "calendar.slint";
|
||||
import { Energy } from "energy.slint";
|
||||
import {
|
||||
@@ -132,6 +134,8 @@ export component AppWindow inherits Window {
|
||||
callback set_temperature(string, float);
|
||||
callback toggle_thermostat(string);
|
||||
|
||||
in property <[CalendarEventData]> calendar-events;
|
||||
|
||||
in property <[ThermometerData]> thermometers;
|
||||
|
||||
in property <[LightData]> lights;
|
||||
@@ -177,7 +181,11 @@ export component AppWindow inherits Window {
|
||||
set_temperature(entity_id, value) => { root.set_temperature(entity_id, value); }
|
||||
toggle_thermostat(entity_id) => { toggle_thermostat(entity_id) }
|
||||
}
|
||||
if tab-bar.current-index == 1: Calendar { }
|
||||
|
||||
if tab-bar.current-index == 1: Calendar {
|
||||
calendar-events: calendar-events;
|
||||
}
|
||||
|
||||
if tab-bar.current-index == 2: Energy { }
|
||||
|
||||
Rectangle {
|
||||
|
||||
Reference in New Issue
Block a user