🏡 index : ~doyle/shalom.git

author Jordan Doyle <jordan@doyle.la> 2023-10-30 23:12:32.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2023-10-30 23:12:32.0 +00:00:00
commit
5db8867b6e0219d29c5b68e9bd491d12a5799223 [patch]
tree
88f623a1d105d58191cd53a463f6ff7a0fdb5b3b
parent
1d584771d0461934b2af2ceb005808eae59fbe5e
download
5db8867b6e0219d29c5b68e9bd491d12a5799223.tar.gz

First iteration of loading rooms from home assistant



Diff

 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<MaybeTlsStream<TcpStream>>;
#[derive(Clone, Debug)]
pub struct Client {
    sender: mpsc::Sender<(
        HassRequestKind,
        oneshot::Sender<Yoke<&'static RawValue, String>>,
    )>,
}

pub enum ClientMessage {
    // Ready,
impl Client {
    pub async fn request<T: for<'a> Yokeable<'a>>(
        &self,
        request: HassRequestKind,
    ) -> Yoke<T, String>
    where
        for<'a> <T as Yokeable<'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<ClientMessage> {
    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<u64, oneshot::Sender<Yoke<&'static RawValue, String>>> =
            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<ClientMessage
                            eprintln!("rtt: {}", OffsetDateTime::now_utc() - ts);
                        }
                        Message::Text(payload) => {
                            handle_hass_response(&config, serde_json::from_str(&payload).unwrap(), &mut connection).await;
                            let yoked_payload: Yoke<HassResponse, String> = 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<ClientMessage
                        _ => {}
                    }
                }
                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<ClientMessage
        }
    });

    recv
    ready_recv.await.unwrap();

    Client { sender }
}

async fn handle_hass_response(
    config: &HomeAssistantConfig,
    v: HassResponse,
    socket: &mut SocketStream,
) {
    #[allow(clippy::match_same_arms)]
    match v {
        HassResponse::AuthRequired => {
            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<u64>,
    #[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<u64>,
    #[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<Area<'a>>);

    #[derive(Deserialize, Debug)]
    pub struct Area<'a> {
        #[serde(borrow)]
        pub aliases: Vec<Cow<'a, str>>,
        #[serde(borrow)]
        pub area_id: Cow<'a, str>,
        #[serde(borrow)]
        pub name: Cow<'a, str>,
        #[serde(borrow)]
        pub picture: Option<Cow<'a, str>>,
    }
}
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<ActiveContextMenu>,
    homepage: ActivePage,
    oracle: Option<Arc<Oracle>>,
}

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<Self::Message> {
        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<Oracle>),
    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<Intern<str>, Room>,
}

impl Oracle {
    pub async fn new(hass_client: crate::hass_client::Client) -> Self {
        let (rooms,) = tokio::join!(
            hass_client.request::<AreaRegistryList<'_>>(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<Item = &'_ Room> + '_ {
        self.rooms.values()
    }
}

#[derive(Debug)]
pub struct Room {
    pub name: Intern<str>,
}
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<M> {
    oracle: Arc<Oracle>,
    open_page: fn(ActivePage) -> M,
}

impl<M> Omni<M> {
    pub fn new(open_page: fn(ActivePage) -> M) -> Self {
        Self { open_page }
    pub fn new(oracle: Arc<Oracle>, open_page: fn(ActivePage) -> M) -> Self {
        Self { oracle, open_page }
    }
}

@@ -43,18 +46,14 @@ impl<M: Clone> Component<M, Renderer> for Omni<M> {
            // .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<M: Clone> Component<M, Renderer> for Omni<M> {
    }
}

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<image::Handle>,
    caption: &'a str,
) -> ImageCard<'a, M> {
pub fn image_card<'a, M: 'a>(handle: impl Into<image::Handle>, caption: &str) -> ImageCard<'a, M> {
    let image_handle = handle.into();

    ImageCard {