From 5db8867b6e0219d29c5b68e9bd491d12a5799223 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Mon, 30 Oct 2023 23:12:32 +0000 Subject: [PATCH] First iteration of loading rooms from home assistant --- Cargo.lock | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- shalom/Cargo.toml | 4 +++- shalom/src/hass_client.rs | 199 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------ shalom/src/main.rs | 20 ++++++++++++++------ shalom/src/oracle.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++++- shalom/src/pages/omni.rs | 39 ++++++++++++++++++++++++--------------- shalom/src/widgets/image_card.rs | 5 +---- 7 files changed, 306 insertions(+), 100 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9b5d1f8..5a73ab8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,17 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" @@ -979,6 +990,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.7", +] [[package]] name = "hashbrown" @@ -986,7 +1000,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" dependencies = [ - "ahash", + "ahash 0.8.3", "allocator-api2", ] @@ -1279,6 +1293,16 @@ dependencies = [ ] [[package]] +name = "internment" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e976188335292f66a1533fd41d5c2ce24b32dc2c000569b8dccf4e57f489806" +dependencies = [ + "hashbrown 0.12.3", + "parking_lot 0.12.1", +] + +[[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2422,6 +2446,7 @@ version = "0.1.0" dependencies = [ "iced", "image", + "internment", "itertools", "keyframe", "once_cell", @@ -2431,6 +2456,7 @@ dependencies = [ "tokio", "tokio-tungstenite", "toml", + "yoke", ] [[package]] @@ -2565,6 +2591,12 @@ dependencies = [ ] [[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2634,6 +2666,18 @@ dependencies = [ ] [[package]] +name = "synstructure" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "285ba80e733fac80aa4270fbcdf83772a79b80aa35c97075320abfee4a915b06" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", + "unicode-xid", +] + +[[package]] name = "sys-locale" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3717,12 +3761,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" [[package]] +name = "yoke" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e38c508604d6bbbd292dadb3c02559aa7fff6b654a078a36217cad871636e4" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5e19fb6ed40002bab5403ffa37e53e0e56f914a4450c8765f533018db1db35f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", + "synstructure", +] + +[[package]] name = "zeno" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" [[package]] +name = "zerofrom" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655b0814c5c0b19ade497851070c640773304939a6c0fd5f5fb43da0696d05b7" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6a647510471d372f2e6c2e6b7219e44d8c574d24fdc11c610a61455782f18c3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", + "synstructure", +] + +[[package]] name = "zune-inflate" version = "0.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/shalom/Cargo.toml b/shalom/Cargo.toml index 99447ad..9906bc1 100644 --- a/shalom/Cargo.toml +++ b/shalom/Cargo.toml @@ -9,11 +9,13 @@ edition = "2021" iced = { version = "0.10", features = ["tokio", "svg", "lazy", "advanced", "image"] } image = "0.24" once_cell = "1.18" +internment = "0.7.4" itertools = "0.11" keyframe = "1.1" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +serde_json = { version = "1.0", features = ["raw_value"] } tokio = { version = "1.33", features = ["net", "sync", "rt", "macros", "time", "fs"] } tokio-tungstenite = "0.20" toml = "0.8" time = { version = "0.3", features = ["std"] } +yoke = { version = "0.7", features = ["derive"] } diff --git a/shalom/src/hass_client.rs b/shalom/src/hass_client.rs index 4c23919..8086984 100644 --- a/shalom/src/hass_client.rs +++ b/shalom/src/hass_client.rs @@ -1,33 +1,60 @@ -use std::{fs::File, io::Write, time::Duration}; +#![allow(clippy::forget_non_drop)] + +use std::{collections::HashMap, time::Duration}; use iced::futures::{SinkExt, StreamExt}; use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; use time::OffsetDateTime; -use tokio::{net::TcpStream, sync::mpsc}; -use tokio_tungstenite::{tungstenite::Message, MaybeTlsStream, WebSocketStream}; +use tokio::sync::{mpsc, oneshot}; +use tokio_tungstenite::tungstenite::Message; +use yoke::{Yoke, Yokeable}; use crate::config::HomeAssistantConfig; -type SocketStream = WebSocketStream>; +#[derive(Clone, Debug)] +pub struct Client { + sender: mpsc::Sender<( + HassRequestKind, + oneshot::Sender>, + )>, +} -pub enum ClientMessage { - // Ready, +impl Client { + pub async fn request Yokeable<'a>>( + &self, + request: HassRequestKind, + ) -> Yoke + where + for<'a> >::Output: Deserialize<'a>, + { + let (send, recv) = oneshot::channel(); + self.sender.send((request, send)).await.unwrap(); + let resp = recv.await.unwrap(); + + resp.map_project(move |value, _| serde_json::from_str(value.get()).unwrap()) + } } -pub async fn create(config: HomeAssistantConfig) -> mpsc::Receiver { - let (_send, recv) = mpsc::channel(10); +pub async fn create(config: HomeAssistantConfig) -> Client { + let (sender, mut recv) = mpsc::channel(10); let uri = format!("ws://{}/api/websocket", config.uri); let (mut connection, _response) = tokio_tungstenite::connect_async(&uri).await.unwrap(); + let (ready_send, ready_recv) = oneshot::channel(); + let mut ready_send = Some(ready_send); + tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(10)); + let mut counter: u64 = 0; + let mut pending: HashMap>> = + HashMap::new(); loop { tokio::select! { Some(message) = connection.next() => { let message = message.unwrap(); - eprintln!("recv: {message:?}"); #[allow(clippy::match_same_arms)] match message { @@ -38,7 +65,37 @@ pub async fn create(config: HomeAssistantConfig) -> mpsc::Receiver { - handle_hass_response(&config, serde_json::from_str(&payload).unwrap(), &mut connection).await; + let yoked_payload: Yoke = Yoke::attach_to_cart(payload, |s| serde_json::from_str(s).unwrap()); + + let payload: &HassResponse = yoked_payload.get(); + + match payload.type_ { + HassResponseType::AuthRequired => { + let payload = HassRequest { + id: None, + inner: HassRequestKind::Auth { + access_token: config.token.clone(), + } + } + .to_request(); + + connection + .send(payload) + .await + .unwrap(); + } + HassResponseType::AuthInvalid => { + eprintln!("invalid auth"); + } + HassResponseType::AuthOk => { + ready_send.take().unwrap().send(()).unwrap(); + } + HassResponseType::Result => { + let id = payload.id.unwrap(); + let payload = yoked_payload.map_project(move |yk, _| yk.result.unwrap()); + pending.remove(&id).unwrap().send(payload).unwrap(); + } + } } Message::Close(_) => { // eprintln!("Reconnecting..."); @@ -47,6 +104,17 @@ pub async fn create(config: HomeAssistantConfig) -> mpsc::Receiver {} } } + Some((inner, reply)) = recv.recv() => { + counter += 1; + let counter = counter; + + connection.send(HassRequest { + id: Some(counter), + inner, + }.to_request()).await.unwrap(); + + pending.insert(counter, reply); + } _ = interval.tick() => { connection.send(Message::Ping(OffsetDateTime::now_utc().unix_timestamp_nanos().to_be_bytes().to_vec())).await.unwrap(); } @@ -54,85 +122,50 @@ pub async fn create(config: HomeAssistantConfig) -> mpsc::Receiver { - socket - .send( - HassRequest::Auth { - access_token: config.token.clone(), - } - .to_request(), - ) - .await - .unwrap(); - } - HassResponse::AuthOk => { - // Lists: [aliases, area_id, name, picture] - // socket - // .send(HassRequest::AreaRegistry { id: 3 }.to_request()) - // .await - // .unwrap(); - - // Lists: [area_id, entity_id] - // socket.send(HassRequest::EntityRegistry { id: 3 }.to_request()) - // .await - // .unwrap(); - - // Lists: versions, area id, manufacturer, etc - // socket.send(HassRequest::DeviceRegistry { id: 3 }.to_request()) - // .await - // .unwrap(); - } - HassResponse::AuthInvalid => {} - HassResponse::Result(value) => { - File::create("test") - .unwrap() - .write_all(&serde_json::to_vec(&value).unwrap()) - .unwrap(); - } - } +#[derive(Deserialize, Yokeable)] +struct HassResponse<'a> { + id: Option, + #[serde(rename = "type")] + type_: HassResponseType, + #[serde(borrow)] + result: Option<&'a RawValue>, } -#[derive(Deserialize)] -#[serde(rename_all = "snake_case", tag = "type", content = "result")] -#[allow(clippy::enum_variant_names)] -enum HassResponse { +#[derive(Deserialize, Copy, Clone)] +#[serde(rename_all = "snake_case")] +pub enum HassResponseType { AuthRequired, AuthOk, AuthInvalid, - Result(serde_json::Value), + Result, +} + +#[derive(Serialize)] +struct HassRequest { + #[serde(skip_serializing_if = "Option::is_none")] + id: Option, + #[serde(flatten)] + inner: HassRequestKind, } #[derive(Serialize)] #[serde(rename_all = "snake_case", tag = "type")] -enum HassRequest { +pub enum HassRequestKind { Auth { access_token: String, }, - GetStates { - id: u32, - }, + GetStates, #[serde(rename = "config/area_registry/list")] - AreaRegistry { - id: u32, - }, + AreaRegistry, #[serde(rename = "config/entity_registry/list")] - EntityRegistry { - id: u32, - }, + EntityRegistry, #[serde(rename = "config/device_registry/list")] - DeviceRegistry { - id: u32, - }, + DeviceRegistry, } impl HassRequest { @@ -140,3 +173,25 @@ impl HassRequest { Message::text(serde_json::to_string(&self).unwrap()) } } + +pub mod responses { + use std::borrow::Cow; + + use serde::Deserialize; + use yoke::Yokeable; + + #[derive(Deserialize, Yokeable, Debug)] + pub struct AreaRegistryList<'a>(#[serde(borrow)] pub Vec>); + + #[derive(Deserialize, Debug)] + pub struct Area<'a> { + #[serde(borrow)] + pub aliases: Vec>, + #[serde(borrow)] + pub area_id: Cow<'a, str>, + #[serde(borrow)] + pub name: Cow<'a, str>, + #[serde(borrow)] + pub picture: Option>, + } +} diff --git a/shalom/src/main.rs b/shalom/src/main.rs index 6906c64..46254b2 100644 --- a/shalom/src/main.rs +++ b/shalom/src/main.rs @@ -18,6 +18,7 @@ use iced::{ use crate::{ config::Config, + oracle::Oracle, theme::{Icon, Image}, widgets::{context_menu::ContextMenu, mouse_area::mouse_area}, }; @@ -26,6 +27,7 @@ pub struct Shalom { page: ActivePage, context_menu: Option, homepage: ActivePage, + oracle: Option>, } impl Application for Shalom { @@ -39,6 +41,7 @@ impl Application for Shalom { page: ActivePage::Loading, context_menu: None, homepage: ActivePage::Room("Living Room"), + oracle: None, }; // this is only best-effort to try and prevent blocking when loading @@ -49,10 +52,11 @@ impl Application for Shalom { async { let config = load_config().await; let client = hass_client::create(config.home_assistant).await; + let oracle = Oracle::new(client.clone()).await; - Arc::new(client) + Arc::new(oracle) }, - |_client| Message::Loaded, + Message::Loaded, ); (this, command) @@ -64,8 +68,9 @@ impl Application for Shalom { fn update(&mut self, message: Self::Message) -> Command { match message { - Message::Loaded => { + Message::Loaded(oracle) => { self.page = self.homepage.clone(); + self.oracle = Some(oracle); } Message::CloseContextMenu => { self.context_menu = None; @@ -87,7 +92,10 @@ impl Application for Shalom { ActivePage::Room(room) => { Element::from(pages::room::Room::new(room, Message::OpenContextMenu)) } - ActivePage::Omni => Element::from(pages::omni::Omni::new(Message::ChangePage)), + ActivePage::Omni => Element::from(pages::omni::Omni::new( + self.oracle.clone().unwrap(), + Message::ChangePage, + )), }; let mut content = Column::new().push(scrollable(page_content)); @@ -152,7 +160,7 @@ impl Application for Shalom { stretch: Stretch::Condensed, ..Font::with_name("Helvetica Neue") }), - row![vertical_slider(0..=100, 0, |_v| Message::Loaded).height(200)] + row![vertical_slider(0..=100, 0, |_v| Message::CloseContextMenu).height(200)] .align_items(Alignment::Center) ]) .width(Length::Fill) @@ -175,7 +183,7 @@ async fn load_config() -> Config { #[derive(Debug, Clone)] pub enum Message { - Loaded, + Loaded(Arc), CloseContextMenu, ChangePage(ActivePage), OpenContextMenu(ActiveContextMenu), diff --git a/shalom/src/oracle.rs b/shalom/src/oracle.rs index dde3cea..3a0c878 100644 --- a/shalom/src/oracle.rs +++ b/shalom/src/oracle.rs @@ -1 +1,47 @@ -pub struct Oracle {} +use std::collections::BTreeMap; + +use internment::Intern; + +use crate::hass_client::{responses::AreaRegistryList, HassRequestKind}; + +#[derive(Debug)] +pub struct Oracle { + client: crate::hass_client::Client, + rooms: BTreeMap, Room>, +} + +impl Oracle { + pub async fn new(hass_client: crate::hass_client::Client) -> Self { + let (rooms,) = tokio::join!( + hass_client.request::>(HassRequestKind::AreaRegistry) + ); + + let rooms = rooms + .get() + .0 + .iter() + .map(|room| { + ( + Intern::from(room.area_id.as_ref()), + Room { + name: Intern::from(room.name.as_ref()), + }, + ) + }) + .collect(); + + Self { + client: hass_client, + rooms, + } + } + + pub fn rooms(&self) -> impl Iterator + '_ { + self.rooms.values() + } +} + +#[derive(Debug)] +pub struct Room { + pub name: Intern, +} diff --git a/shalom/src/pages/omni.rs b/shalom/src/pages/omni.rs index 233a012..0ea17be 100644 --- a/shalom/src/pages/omni.rs +++ b/shalom/src/pages/omni.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use iced::{ advanced::graphics::core::Element, font::{Stretch, Weight}, @@ -6,15 +8,16 @@ use iced::{ }; use itertools::Itertools; -use crate::{theme::Image, widgets::image_card, ActivePage}; +use crate::{oracle::Oracle, theme::Image, widgets::image_card, ActivePage}; pub struct Omni { + oracle: Arc, open_page: fn(ActivePage) -> M, } impl Omni { - pub fn new(open_page: fn(ActivePage) -> M) -> Self { - Self { open_page } + pub fn new(oracle: Arc, open_page: fn(ActivePage) -> M) -> Self { + Self { oracle, open_page } } } @@ -43,18 +46,14 @@ impl Component for Omni { // .width(Length::FillPortion(1)) }; - let rooms = [ - room("Living Room", Image::LivingRoom), - room("Kitchen", Image::Kitchen), - room("Bathroom", Image::Bathroom), - room("Bedroom", Image::Bedroom), - room("Dining Room", Image::DiningRoom), - ] - .into_iter() - .chunks(2) - .into_iter() - .map(|children| children.into_iter().fold(Row::new().spacing(10), Row::push)) - .fold(Column::new().spacing(10), Column::push); + let rooms = self + .oracle + .rooms() + .map(|r| room(r.name.as_ref(), determine_image(&r.name))) + .chunks(2) + .into_iter() + .map(|children| children.into_iter().fold(Row::new().spacing(10), Row::push)) + .fold(Column::new().spacing(10), Column::push); scrollable( column![header("Cameras"), header("Rooms"), rooms,] @@ -65,6 +64,16 @@ impl Component for Omni { } } +fn determine_image(name: &str) -> Image { + match name { + "Kitchen" => Image::Kitchen, + "Bathroom" => Image::Bathroom, + "Bedroom" => Image::Bedroom, + "Dining Room" => Image::DiningRoom, + _ => Image::LivingRoom, + } +} + #[derive(Default, Hash)] pub struct State {} diff --git a/shalom/src/widgets/image_card.rs b/shalom/src/widgets/image_card.rs index da515ec..6959f54 100644 --- a/shalom/src/widgets/image_card.rs +++ b/shalom/src/widgets/image_card.rs @@ -19,10 +19,7 @@ use iced::{ Point, Rectangle, Renderer, Size, Theme, Vector, }; -pub fn image_card<'a, M: 'a>( - handle: impl Into, - caption: &'a str, -) -> ImageCard<'a, M> { +pub fn image_card<'a, M: 'a>(handle: impl Into, caption: &str) -> ImageCard<'a, M> { let image_handle = handle.into(); ImageCard { -- libgit2 1.7.2