From f00f8959fe89e89d96f7f89f87f4db0642495673 Mon Sep 17 00:00:00 2001 From: Kita Lembrik Date: Mon, 20 Apr 2026 01:11:24 +0400 Subject: [PATCH 1/4] feat: allow HASS to close its own page via POST /close-hass and make Chromium fullscreen - Add HTTP listener on configurable port (default 8321) accepting POST /close-hass - Add public Slint function close-hass-remote() for Rust to trigger page navigation - Replace --window-size with --start-fullscreen for Chromium - Update HA page UI to show 'Home Assistant Opened!' status with troubleshooting hints - Add hass_api_port to Config - Add python3 to shell.nix for skia-bindings build --- shell.nix | 1 + src/config.rs | 3 ++ src/home_assistant.rs | 50 ++++++++++++++++++++++++- src/main.rs | 22 +++++++++++ ui/main_window.slint | 6 +++ ui/pages/home_assistant.slint | 70 +++++++++++++++++++---------------- 6 files changed, 118 insertions(+), 34 deletions(-) diff --git a/shell.nix b/shell.nix index d31055d..2ef9082 100644 --- a/shell.nix +++ b/shell.nix @@ -19,6 +19,7 @@ pkgs.mkShell { libXi openssl alsa-lib + python3 ]; shellHook = '' diff --git a/src/config.rs b/src/config.rs index 34a8fc5..f2e17f8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,6 +20,8 @@ pub enum ConfigError { pub struct Config { pub token: Option, pub home_assistant_url: String, + /// Port for the HTTP listener that accepts `POST /close-hass` from HASS. + pub hass_api_port: u16, pub cashcode_serial_port: String, pub cctalk_serial_port: String, /// Override the value for specific coin positions (channels). @@ -36,6 +38,7 @@ impl Default for Config { Self { token: None, home_assistant_url: "https://ha.hackem.cc/web-dramma/0?BrowserID=dramma".to_string(), + hass_api_port: 8321, cashcode_serial_port: "/dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller_D-if00-port0" .to_string(), diff --git a/src/home_assistant.rs b/src/home_assistant.rs index 143ab35..257e3ab 100644 --- a/src/home_assistant.rs +++ b/src/home_assistant.rs @@ -1,5 +1,8 @@ use log::{error, info}; +use std::io::{Read, Write}; +use std::net::TcpListener; use std::process::{Child, Command}; +use std::sync::mpsc::Sender; use std::sync::{Arc, Mutex}; /// Manages a Chromium subprocess for displaying Home Assistant @@ -30,7 +33,7 @@ impl ChromiumManager { // Try chromium first, then chromium-browser as fallback (different Debian versions) let command_result = Command::new("chromium") .arg("--app=".to_string() + url) - .arg("--window-size=1280,940") + .arg("--start-fullscreen") .arg("--window-position=0,0") .arg("--disable-infobars") .arg("--noerrdialogs") @@ -50,7 +53,7 @@ impl ChromiumManager { // Fallback to chromium-browser Command::new("chromium-browser") .arg("--app=".to_string() + url) - .arg("--window-size=1280,940") + .arg("--start-fullscreen") .arg("--window-position=0,0") .arg("--disable-infobars") .arg("--noerrdialogs") @@ -108,3 +111,46 @@ impl Drop for ChromiumManager { self.close(); } } + +/// Starts a simple HTTP listener for remote control from Home Assistant. +/// When a `POST /close-hass` request is received, sends a signal through `tx`. +pub fn start_close_listener(port: u16, tx: Sender<()>) { + let addr = format!("0.0.0.0:{}", port); + let listener = match TcpListener::bind(&addr) { + Ok(l) => l, + Err(e) => { + error!("Failed to bind HASS close listener on {}: {}", addr, e); + return; + } + }; + info!("šŸ  Home Assistant close listener on port {}", port); + + for stream in listener.incoming() { + let Ok(mut stream) = stream else { + continue; + }; + let mut buf = [0u8; 512]; + let Ok(n) = stream.read(&mut buf) else { + continue; + }; + let request = String::from_utf8_lossy(&buf[..n]); + let first_line = request.lines().next().unwrap_or(""); + + if first_line.starts_with("POST /close-hass") { + info!("šŸ  Received remote close-hass request"); + let _ = tx.send(()); + let _ = stream.write_all( + b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: 2\r\n\r\nOK", + ); + } else if first_line.starts_with("OPTIONS") { + // CORS preflight + let _ = stream.write_all( + b"HTTP/1.1 204 No Content\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: POST, OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type\r\n\r\n", + ); + } else { + let _ = stream.write_all( + b"HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot Found", + ); + } + } +} diff --git a/src/main.rs b/src/main.rs index 1c50bd3..a8358b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -623,5 +623,27 @@ mod home_assistant_handler { info!("Hiding Home Assistant page, closing Chromium"); chromium_hide.close(); }); + + // HTTP listener so HASS can POST /close-hass to dismiss its own page + let (tx, rx) = std::sync::mpsc::channel::<()>(); + let port = config.hass_api_port; + thread::spawn(move || { + home_assistant::start_close_listener(port, tx); + }); + + let weak = app.as_weak(); + let timer = slint::Timer::default(); + timer.start( + slint::TimerMode::Repeated, + std::time::Duration::from_millis(200), + move || { + if rx.try_recv().is_ok() { + if let Some(window) = weak.upgrade() { + window.invoke_close_hass_remote(); + } + } + }, + ); + std::mem::forget(timer); } } diff --git a/ui/main_window.slint b/ui/main_window.slint index 04d6349..8546397 100644 --- a/ui/main_window.slint +++ b/ui/main_window.slint @@ -47,6 +47,12 @@ export component MainWindow inherits Window { callback fetch-usernames(); // fetches available-usernames for autocomplete callback confetti-started(); // tells rust to start confetti dismiss timer + /// Called from Rust when HASS sends a POST /close-hass request. + public function close-hass-remote() { + root.hide-home-assistant(); + root.current-page = Page.Main; + } + title: "Donation Machine"; width: 1280px; height: 1024px; diff --git a/ui/pages/home_assistant.slint b/ui/pages/home_assistant.slint index ac4b6a1..10e43d5 100644 --- a/ui/pages/home_assistant.slint +++ b/ui/pages/home_assistant.slint @@ -2,82 +2,88 @@ import { Button, Palette } from "std-widgets.slint"; export component HomeAssistant inherits Rectangle { in-out property home-assistant-url: ""; - + callback back-clicked(); - + background: Palette.background; - + VerticalLayout { alignment: start; padding: 16px; spacing: 16px; - + // back button HorizontalLayout { alignment: start; - + Button { text: "← Back"; width: 150px; height: 60px; - + clicked => { root.back-clicked(); } } } - - // hass iframe placeholder - // until slint supports html iframes + + // status panel behind fullscreen Chromium Rectangle { background: #1a1a1a; border-width: 2px; border-color: #333333; border-radius: 8px; vertical-stretch: 1; - + VerticalLayout { alignment: center; padding: 32px; - + spacing: 12px; + + Text { + text: "šŸ "; + font-size: 64px; + horizontal-alignment: center; + } + Text { - text: "Home Assistant"; + text: "Home Assistant Opened!"; font-size: 28px; font-weight: 700; - color: #ffffff; + color: #4caf50; horizontal-alignment: center; } - - Rectangle { - height: 20px; - } - + Text { - text: root.home-assistant-url != "" ? "Connected to: " + root.home-assistant-url : "No URL configured"; + text: "Chromium is running in fullscreen"; font-size: 16px; color: #cccccc; horizontal-alignment: center; - wrap: word-wrap; } - - Rectangle { - height: 20px; + + Rectangle { height: 8px; } + + Text { + text: root.home-assistant-url != "" ? root.home-assistant-url : "No URL configured"; + font-size: 13px; + color: #888888; + horizontal-alignment: center; + wrap: word-wrap; } - + + Rectangle { height: 24px; } + Text { - text: "āš ļø Web view integration needed"; + text: "If you see this screen instead of Home Assistant:"; font-size: 14px; + font-weight: 600; color: #ff9800; horizontal-alignment: center; } - - Rectangle { - height: 10px; - } - + Text { - text: "if hass did not started,\nconfigure the iframe URL in .config/dramma.toml\nor install chromium huh"; - font-size: 12px; + text: "• Make sure chromium is installed\n• Check .config/dramma.toml has the correct home_assistant_url\n• You can close this page with POST /close-hass"; + font-size: 13px; color: #999999; horizontal-alignment: center; wrap: word-wrap; From c9f4464b555b30faedb9fe165b1a502df44f16ac Mon Sep 17 00:00:00 2001 From: Kita Lembrik Date: Mon, 20 Apr 2026 01:28:43 +0400 Subject: [PATCH 2/4] fix for clippy --- src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index a8358b7..ae61e5d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -637,10 +637,10 @@ mod home_assistant_handler { slint::TimerMode::Repeated, std::time::Duration::from_millis(200), move || { - if rx.try_recv().is_ok() { - if let Some(window) = weak.upgrade() { - window.invoke_close_hass_remote(); - } + if rx.try_recv().is_ok() + && let Some(window) = weak.upgrade() + { + window.invoke_close_hass_remote(); } }, ); From 948ad31272dd6e57c852fbd8b5cddfcd68dabf17 Mon Sep 17 00:00:00 2001 From: Kita Lembrik Date: Mon, 20 Apr 2026 01:32:09 +0400 Subject: [PATCH 3/4] fix for fmt --- src/home_assistant.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/home_assistant.rs b/src/home_assistant.rs index 257e3ab..6d12ad7 100644 --- a/src/home_assistant.rs +++ b/src/home_assistant.rs @@ -148,9 +148,8 @@ pub fn start_close_listener(port: u16, tx: Sender<()>) { b"HTTP/1.1 204 No Content\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: POST, OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type\r\n\r\n", ); } else { - let _ = stream.write_all( - b"HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot Found", - ); + let _ = + stream.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot Found"); } } } From 6a26e1eba08646e59c21fbc19dc10f680d3eafb1 Mon Sep 17 00:00:00 2001 From: Kita Lembrik Date: Sun, 26 Apr 2026 12:06:58 +0400 Subject: [PATCH 4/4] Update src/main.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index ae61e5d..d0cb415 100644 --- a/src/main.rs +++ b/src/main.rs @@ -632,18 +632,15 @@ mod home_assistant_handler { }); let weak = app.as_weak(); - let timer = slint::Timer::default(); - timer.start( - slint::TimerMode::Repeated, - std::time::Duration::from_millis(200), - move || { - if rx.try_recv().is_ok() - && let Some(window) = weak.upgrade() - { - window.invoke_close_hass_remote(); - } - }, - ); - std::mem::forget(timer); + thread::spawn(move || { + while rx.recv().is_ok() { + let weak = weak.clone(); + let _ = slint::invoke_from_event_loop(move || { + if let Some(window) = weak.upgrade() { + window.invoke_close_hass_remote(); + } + }); + } + }); } }