Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions shell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pkgs.mkShell {
libXi
openssl
alsa-lib
python3
];

shellHook = ''
Expand Down
3 changes: 3 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub enum ConfigError {
pub struct Config {
pub token: Option<String>,
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).
Expand All @@ -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(),
Expand Down
49 changes: 47 additions & 2 deletions src/home_assistant.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -108,3 +111,45 @@ 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);
Comment on lines +117 to +126

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The close listener binds to 0.0.0.0, exposing POST /close-hass on all interfaces with no authentication. This allows any host that can reach the device to remotely navigate the kiosk UI. If Home Assistant is the only intended caller, bind to 127.0.0.1/localhost by default and/or require a shared secret (header/token) before accepting the request.

Copilot uses AI. Check for mistakes.

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;
};
Comment on lines +128 to +135

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The listener processes connections sequentially and does a blocking stream.read(...) without a timeout. A client that connects and never sends data can stall the entire listener (preventing legitimate close requests). Consider setting a short read timeout and/or handling each connection in a separate thread/task so one slow client can’t block the service.

Copilot uses AI. Check for mistakes.
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") {
Comment on lines +138 to +145

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request routing is currently based on first_line.starts_with("POST /close-hass"), which will also match unintended paths like /close-hass-foo and doesn’t validate the HTTP method/path tokens. Parse the first line into METHOD path VERSION and require an exact POST + /close-hass match (and similarly scope OPTIONS handling to the same path).

Suggested change
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") {
let mut parts = first_line.split_whitespace();
let request_line = match (parts.next(), parts.next(), parts.next(), parts.next()) {
(Some(method), Some(path), Some(version), None) => Some((method, path, version)),
_ => None,
};
if let Some(("POST", "/close-hass", _version)) = request_line {
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 let Some(("OPTIONS", "/close-hass", _version)) = request_line {

Copilot uses AI. Check for mistakes.
// 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");
}
}
}
22 changes: 22 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
&& let Some(window) = weak.upgrade()
{
window.invoke_close_hass_remote();
}
},
);
std::mem::forget(timer);
Comment thread
gmglbn-0 marked this conversation as resolved.
Outdated
}
}
6 changes: 6 additions & 0 deletions ui/main_window.slint
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
70 changes: 38 additions & 32 deletions ui/pages/home_assistant.slint
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,88 @@ import { Button, Palette } from "std-widgets.slint";

export component HomeAssistant inherits Rectangle {
in-out property <string> 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";

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The troubleshooting text says the page can be closed with POST /close-hass, but the listener is on a configurable port (hass_api_port, default 8321). Consider including the full endpoint (e.g., POST http://localhost:<port>/close-hass) or at least mention the port/config key so the hint is actionable when debugging.

Suggested change
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";
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 http://localhost:<hass_api_port>/close-hass (default port 8321; configurable via hass_api_port)";

Copilot uses AI. Check for mistakes.
font-size: 13px;
color: #999999;
horizontal-alignment: center;
wrap: word-wrap;
Expand Down
Loading