Initial commit: Electron + React touchscreen kiosk dashboard for Home Assistant
This commit is contained in:
229
electron/services/ScreenManager.ts
Normal file
229
electron/services/ScreenManager.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// DBus environment for GNOME Wayland - needed to communicate with session services
|
||||
const DBUS_ENV = {
|
||||
XDG_RUNTIME_DIR: '/run/user/1000',
|
||||
DBUS_SESSION_BUS_ADDRESS: 'unix:path=/run/user/1000/bus',
|
||||
};
|
||||
|
||||
export class ScreenManager {
|
||||
private window: BrowserWindow;
|
||||
private idleTimeout: number = 300000; // 5 minutes default
|
||||
private idleTimer: NodeJS.Timeout | null = null;
|
||||
private isScreenOn: boolean = true;
|
||||
private lastActivity: number = Date.now();
|
||||
private screenControlAvailable: boolean = true;
|
||||
private isWayland: boolean = false;
|
||||
|
||||
constructor(window: BrowserWindow) {
|
||||
this.window = window;
|
||||
this.detectDisplayServer();
|
||||
this.setupActivityListeners();
|
||||
this.startIdleMonitor();
|
||||
}
|
||||
|
||||
private detectDisplayServer(): void {
|
||||
// Check if running under Wayland
|
||||
this.isWayland = !!(process.env.WAYLAND_DISPLAY || process.env.XDG_SESSION_TYPE === 'wayland');
|
||||
console.log(`ScreenManager: Display server detected - ${this.isWayland ? 'Wayland' : 'X11'}`);
|
||||
}
|
||||
|
||||
private setupActivityListeners(): void {
|
||||
// Monitor keyboard and mouse activity
|
||||
this.window.webContents.on('before-input-event', () => {
|
||||
this.resetIdleTimer();
|
||||
});
|
||||
|
||||
// Also listen for any cursor/pointer events via IPC from renderer
|
||||
// Touch events on Wayland may not trigger before-input-event
|
||||
}
|
||||
|
||||
// Called from renderer when touch/click detected
|
||||
public handleUserActivity(): void {
|
||||
this.lastActivity = Date.now();
|
||||
// Always try to wake the screen on touch/click - user is actively interacting
|
||||
if (!this.isScreenOn) {
|
||||
this.wakeScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private startIdleMonitor(): void {
|
||||
this.idleTimer = setInterval(() => {
|
||||
const idleTime = Date.now() - this.lastActivity;
|
||||
if (idleTime >= this.idleTimeout && this.isScreenOn && this.screenControlAvailable) {
|
||||
this.sleepScreen();
|
||||
}
|
||||
}, 30000); // Check every 30 seconds (not 10) to reduce spam
|
||||
}
|
||||
|
||||
private resetIdleTimer(): void {
|
||||
this.lastActivity = Date.now();
|
||||
if (!this.isScreenOn) {
|
||||
this.wakeScreen();
|
||||
}
|
||||
}
|
||||
|
||||
setIdleTimeout(timeout: number): void {
|
||||
this.idleTimeout = timeout;
|
||||
}
|
||||
|
||||
async wakeScreen(): Promise<void> {
|
||||
// Always attempt to wake - the screen may have been externally put to sleep by GNOME
|
||||
if (!this.screenControlAvailable) {
|
||||
this.isScreenOn = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (process.platform === 'linux') {
|
||||
if (this.isWayland) {
|
||||
// On GNOME Wayland, we need multiple approaches because:
|
||||
// 1. The screensaver/lock screen blocks DBus SetActive calls
|
||||
// 2. We need to wake DPMS separately from screensaver
|
||||
// 3. Simulating input is the most reliable way to wake
|
||||
|
||||
let woke = false;
|
||||
|
||||
// Method 1: Simulate mouse movement with ydotool (most reliable on Wayland)
|
||||
// This bypasses all the GNOME screensaver/DPMS complications
|
||||
try {
|
||||
// ydotool syntax: mousemove <x> <y> (relative movement)
|
||||
await execAsync('ydotool mousemove 1 0 && ydotool mousemove -- -1 0', { env: { ...process.env, ...DBUS_ENV } });
|
||||
console.log('ScreenManager: Screen woke via ydotool mouse movement');
|
||||
woke = true;
|
||||
} catch (e) {
|
||||
console.log('ScreenManager: ydotool failed:', e);
|
||||
}
|
||||
|
||||
// Method 2: loginctl unlock-session (unlocks GNOME lock screen)
|
||||
if (!woke) {
|
||||
try {
|
||||
await execAsync('loginctl unlock-session', { env: { ...process.env, ...DBUS_ENV } });
|
||||
console.log('ScreenManager: Session unlocked via loginctl');
|
||||
woke = true;
|
||||
} catch (e) {
|
||||
console.log('ScreenManager: loginctl unlock failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: gnome-screensaver-command (older GNOME)
|
||||
if (!woke) {
|
||||
try {
|
||||
await execAsync('gnome-screensaver-command --deactivate', { env: { ...process.env, ...DBUS_ENV } });
|
||||
console.log('ScreenManager: Screen woke via gnome-screensaver-command');
|
||||
woke = true;
|
||||
} catch {
|
||||
console.log('ScreenManager: gnome-screensaver-command not available');
|
||||
}
|
||||
}
|
||||
|
||||
// Method 4: DBus SetActive (works when screen is blanked but not locked)
|
||||
if (!woke) {
|
||||
try {
|
||||
await execAsync('dbus-send --session --dest=org.gnome.ScreenSaver --type=method_call /org/gnome/ScreenSaver org.gnome.ScreenSaver.SetActive boolean:false', { env: { ...process.env, ...DBUS_ENV } });
|
||||
console.log('ScreenManager: Screen woke via dbus-send');
|
||||
woke = true;
|
||||
} catch {
|
||||
console.log('ScreenManager: dbus-send SetActive failed');
|
||||
}
|
||||
}
|
||||
|
||||
// Method 5: Wake DPMS via wlr-randr or gnome-randr
|
||||
try {
|
||||
await execAsync('gnome-randr modify --on DP-1 2>/dev/null || wlr-randr --output DP-1 --on 2>/dev/null || true', { env: { ...process.env, ...DBUS_ENV } });
|
||||
} catch {
|
||||
// Ignore - display names vary
|
||||
}
|
||||
|
||||
if (!woke) {
|
||||
console.log('ScreenManager: All wake methods failed');
|
||||
}
|
||||
} else {
|
||||
// X11 methods
|
||||
try {
|
||||
await execAsync('xset dpms force on');
|
||||
} catch {
|
||||
try {
|
||||
await execAsync('xdotool key shift');
|
||||
} catch {
|
||||
console.log('ScreenManager: No X11 screen control available, disabling feature');
|
||||
this.screenControlAvailable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (process.platform === 'win32') {
|
||||
await execAsync(
|
||||
'powershell -Command "(Add-Type -MemberDefinition \'[DllImport(\\"user32.dll\\")]public static extern int SendMessage(int hWnd,int hMsg,int wParam,int lParam);\' -Name a -Pas)::SendMessage(-1,0x0112,0xF170,-1)"'
|
||||
);
|
||||
} else if (process.platform === 'darwin') {
|
||||
await execAsync('caffeinate -u -t 1');
|
||||
}
|
||||
|
||||
this.isScreenOn = true;
|
||||
this.lastActivity = Date.now();
|
||||
this.window.webContents.send('screen:woke');
|
||||
} catch (error) {
|
||||
console.error('Failed to wake screen:', error);
|
||||
this.isScreenOn = true; // Assume screen is on to prevent retry loops
|
||||
}
|
||||
}
|
||||
|
||||
async sleepScreen(): Promise<void> {
|
||||
if (!this.isScreenOn) return;
|
||||
if (!this.screenControlAvailable) return;
|
||||
|
||||
try {
|
||||
if (process.platform === 'linux') {
|
||||
if (this.isWayland) {
|
||||
// Use busctl with DBUS session for GNOME on Wayland
|
||||
try {
|
||||
await execAsync('busctl --user call org.gnome.ScreenSaver /org/gnome/ScreenSaver org.gnome.ScreenSaver SetActive b true', { env: { ...process.env, ...DBUS_ENV } });
|
||||
console.log('ScreenManager: Screen slept via GNOME ScreenSaver DBus');
|
||||
} catch {
|
||||
try {
|
||||
// Fallback: Try wlopm
|
||||
await execAsync('wlopm --off \\*', { env: { ...process.env, ...DBUS_ENV } });
|
||||
} catch {
|
||||
// No working method - disable screen control
|
||||
console.log('ScreenManager: Wayland screen sleep not available, disabling feature');
|
||||
this.screenControlAvailable = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await execAsync('xset dpms force off');
|
||||
} catch {
|
||||
console.log('ScreenManager: X11 screen sleep not available, disabling feature');
|
||||
this.screenControlAvailable = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (process.platform === 'win32') {
|
||||
await execAsync(
|
||||
'powershell -Command "(Add-Type -MemberDefinition \'[DllImport(\\"user32.dll\\")]public static extern int SendMessage(int hWnd,int hMsg,int wParam,int lParam);\' -Name a -Pas)::SendMessage(-1,0x0112,0xF170,2)"'
|
||||
);
|
||||
} else if (process.platform === 'darwin') {
|
||||
await execAsync('pmset displaysleepnow');
|
||||
}
|
||||
|
||||
this.isScreenOn = false;
|
||||
this.window.webContents.send('screen:slept');
|
||||
} catch (error) {
|
||||
console.error('Failed to sleep screen:', error);
|
||||
// Disable feature to prevent spam
|
||||
this.screenControlAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.idleTimer) {
|
||||
clearInterval(this.idleTimer);
|
||||
this.idleTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user