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(-)
@@ -35,6 +35,17 @@
[[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 @@
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 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
dependencies = [
"ahash",
"ahash 0.8.3",
"allocator-api2",
]
@@ -1276,6 +1290,16 @@
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[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]]
@@ -2422,6 +2446,7 @@
dependencies = [
"iced",
"image",
"internment",
"itertools",
"keyframe",
"once_cell",
@@ -2431,6 +2456,7 @@
"tokio",
"tokio-tungstenite",
"toml",
"yoke",
]
[[package]]
@@ -2563,6 +2589,12 @@
"bitflags 1.3.2",
"num-traits",
]
[[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"
@@ -2631,6 +2663,18 @@
"proc-macro2",
"quote",
"unicode-ident",
]
[[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]]
@@ -3715,12 +3759,57 @@
version = "0.1.6"
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"
@@ -9,11 +9,13 @@
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"] }
@@ -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 {
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 @@
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(_) => {
@@ -46,6 +103,17 @@
}
_ => {}
}
}
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,89 +122,76 @@
}
});
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 => {
}
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 {
pub fn to_request(&self) -> Message {
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>>,
}
}
@@ -18,6 +18,7 @@
use crate::{
config::Config,
oracle::Oracle,
theme::{Icon, Image},
widgets::{context_menu::ContextMenu, mouse_area::mouse_area},
};
@@ -26,6 +27,7 @@
page: ActivePage,
context_menu: Option<ActiveContextMenu>,
homepage: ActivePage,
oracle: Option<Arc<Oracle>>,
}
impl Application for Shalom {
@@ -39,6 +41,7 @@
page: ActivePage::Loading,
context_menu: None,
homepage: ActivePage::Room("Living Room"),
oracle: None,
};
@@ -49,10 +52,11 @@
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 @@
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 @@
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 @@
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 @@
#[derive(Debug, Clone)]
pub enum Message {
Loaded,
Loaded(Arc<Oracle>),
CloseContextMenu,
ChangePage(ActivePage),
OpenContextMenu(ActiveContextMenu),
@@ -1,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>,
}
@@ -1,3 +1,5 @@
use std::sync::Arc;
use iced::{
advanced::graphics::core::Element,
font::{Stretch, Weight},
@@ -6,15 +8,16 @@
};
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 @@
};
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,]
@@ -62,6 +61,16 @@
.padding(40),
)
.into()
}
}
fn determine_image(name: &str) -> Image {
match name {
"Kitchen" => Image::Kitchen,
"Bathroom" => Image::Bathroom,
"Bedroom" => Image::Bedroom,
"Dining Room" => Image::DiningRoom,
_ => Image::LivingRoom,
}
}
@@ -19,10 +19,7 @@
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 {