73 Commits
Author SHA1 Message Date
leukipp e195a56739 build: update version 2025-10-10 21:29:38 +02:00
leukipp d86a5385a8 docs: update readme 2025-10-10 19:11:29 +02:00
leukipp 3ce1265c8a docs: update hardware 2025-10-10 19:10:43 +02:00
leukipp c2bf551834 ref: update images 2025-10-10 19:08:29 +02:00
leukipp 785a97721a build: update dependencies 2025-10-09 14:19:57 +02:00
leukipp 05c9d98b61 feat: add mqtt version diagnostic #91 2025-10-09 13:43:20 +02:00
leukipp 9c26d9d8bb ref: add mqtt attributes 2025-10-09 13:09:30 +02:00
leukipp ad7d3fd3ff feat: early app updates #96 2025-10-08 20:47:59 +02:00
leukipp f666ed57ab ref: widget focus handling 2025-10-08 20:13:48 +02:00
leukipp c0b1a6e9ce feat: add mqtt error diagnostic #91 2025-10-07 21:48:49 +02:00
leukipp 19e51e349c fix: use mqtt last will #84 2025-10-07 09:59:48 +02:00
leukipp 5d012bd637 feat: extended theme switching #87 2025-10-07 09:17:57 +02:00
leukipp 01c7411ddc feat: use box for mqtt zoom #90 2025-10-06 19:17:35 +02:00
leukipp 3e4d96536f feat: dynamic status bar #94 2025-10-06 18:55:51 +02:00
leukipp 4bcb2c4e89 feat: auto on-screen keyboard #85 2025-09-30 21:26:39 +02:00
leukipp b2b87888e4 ref: keyboard window status handling 2025-09-30 17:31:00 +02:00
leukipp 164ef2838c docs: add sponsor 2025-09-29 10:20:40 +02:00
leukipp ae2633800d ref: allow electron flags 2025-09-29 10:10:12 +02:00
leukipp 552f0e19a4 feat: mqtt audio volume control #82 2025-09-26 13:47:15 +02:00
leukipp 890c84d69d Merge branch 'develop' of https://github.com/leukipp/touchkio into develop 2025-09-21 22:47:10 +02:00
leukipp ac69dee387 ref: improve error handling 2025-09-21 22:47:02 +02:00
leukipp ac62dc17ce build: update version 2025-09-19 22:07:45 +02:00
leukipp f2f80e7d52 fix: mqtt config removal #79 2025-09-19 22:07:14 +02:00
leukipp 10e7ff078a build: update version 2025-09-19 16:49:12 +02:00
leukipp e677fff160 docs: update readme 2025-09-19 16:36:57 +02:00
leukipp 63db26611e build: update dependencies 2025-09-19 15:57:13 +02:00
leukipp 5ec6577b5a fix: option to ignore certificate errors #76 2025-09-19 15:52:09 +02:00
leukipp 0359742e70 docs: update readme 2025-09-11 08:55:00 +02:00
leukipp b70f4074e5 docs: add hardware 2025-09-10 16:43:24 +02:00
leukipp a4c300407b ref: remove unsupported mqtt config 2025-09-10 16:00:26 +02:00
leukipp 758f64ced4 ref: add cpu thermal zone 2025-09-10 15:49:22 +02:00
leukipp 7ab803d4ca ref: app update summary 2025-09-09 20:50:56 +02:00
leukipp 092697850d build: compress zip 2025-09-09 19:59:30 +02:00
leukipp 422e92dd89 build: update version 2025-09-09 19:41:09 +02:00
leukipp a15c0fc91d docs: update readme 2025-09-09 19:33:33 +02:00
leukipp 358fa3ca9c ref: use display status callback 2025-09-09 17:25:56 +02:00
leukipp 10722fa217 build: update build.json 2025-09-08 21:31:01 +02:00
leukipp 7267fafb97 ref: add console logs 2025-09-08 20:03:23 +02:00
leukipp b5e95936dc ref: obfuscate logged password 2025-09-08 18:58:29 +02:00
leukipp 047b38a41b feat: extended logging #73 2025-09-08 17:31:42 +02:00
leukipp a93fbb2ad3 build: update dependencies 2025-09-08 17:18:16 +02:00
leukipp 0b927a3ef6 fix: touch event handling for inactive screens #61 2025-09-08 16:23:04 +02:00
leukipp d35cf27de9 fix: update last active only on touch #71 2025-09-08 09:56:27 +02:00
leukipp 19348b4b60 build: disable zip update 2025-09-06 20:27:01 +02:00
leukipp ce1eed391d build: update version 2025-09-06 20:08:16 +02:00
leukipp fe6fe92656 docs: update readme 2025-09-06 20:07:41 +02:00
leukipp 3aea938cfb fix: validate arguments file #69 2025-09-06 20:03:49 +02:00
leukipp c39428fa42 build: update build.json 2025-09-06 19:48:02 +02:00
leukipp 7caa5bfc8a ref: update images 2025-09-06 19:22:19 +02:00
leukipp 5f69961a9f feat: mqtt app update #70 2025-09-06 19:21:51 +02:00
leukipp f44d998016 build: improve update logic 2025-09-06 19:07:57 +02:00
leukipp 3ea65a4af8 build: update dependencies 2025-09-06 19:04:16 +02:00
leukipp 2ad49c3167 feat: add network address mqtt sensor #66 2025-09-03 22:13:45 +02:00
leukipp ef2093a25e feat: check sudo rights for mqtt #62 2025-09-03 16:01:47 +02:00
leukipp 2737eabd8a fix: mqtt keyboard state #65 2025-09-01 17:22:26 +02:00
leukipp b8f809c50e build: update version 2025-08-31 22:04:56 +02:00
leukipp c3e625a8f6 docs: update readme 2025-08-31 22:01:18 +02:00
leukipp 7c15707e3b docs: add hardware 2025-08-31 21:06:57 +02:00
leukipp dbd3756793 ref: update images 2025-08-31 20:33:07 +02:00
leukipp 85fe5a910a fix: update service file #62 2025-08-31 20:31:38 +02:00
leukipp d78088e6af feat: side panel pager #46 2025-08-31 19:24:50 +02:00
leukipp bf77d9b2a0 fix: mqtt memory leak #63 2025-08-31 16:09:39 +02:00
leukipp f4ca7bc412 feat: basic keyboard navigation #46 2025-08-31 11:41:12 +02:00
leukipp d0ea377ead feat: add page zoom buttons #48 2025-08-31 09:24:30 +02:00
leukipp 9a829f72e3 build: update dependencies 2025-08-31 08:48:24 +02:00
leukipp 482a56c9b9 build: update dependencies 2025-08-27 20:03:26 +02:00
leukipp 32409983b2 fix: remember zoom level on refresh #52 2025-08-27 20:02:59 +02:00
leukipp fe0667919e feat: check online status #62 2025-08-27 18:30:15 +02:00
leukipp 0037a892cd ref: display status fallback 2025-08-27 18:26:40 +02:00
leukipp ad22dd3215 feat: kde wayland support #57 2025-08-26 17:15:21 +02:00
leukipp 619179c3c8 fix: event based communication #39 #48 #55 2025-08-25 18:52:25 +02:00
afsy-d 7e5e669b09 Added hardware information (#56)
* Update HARDWARE.md

Added CM4 light 2GB information

* Update HARDWARE.md

Updated information regarding CM4
2025-08-25 13:31:51 +02:00
Tom Carpenter 437c4ab1c5 Make Webview Zoom Changeable from Home Assistant (#52)
* Add Page Zoom Control

Adds MQTT discovery for a "Page Zoom" input to control the `WEBVIEW.viewZoom` parameter.

The MQTT value has been configured to be in % which is then converted to a scale factor when assigned or read from `WEBVIEW.viewZoom`.

* Correct Type for page_zoom publishConfig

Accidentally replaced "number" with "zoom" when changing paramters.

* Add updateZoom Functionality to webview

Add a setter function for the viewZoom property that updates the zoom factor for all views when the property is changed.
2025-08-25 13:31:28 +02:00
18 changed files with 3168 additions and 1051 deletions
+1
View File
@@ -0,0 +1 @@
github: leukipp
+2 -1
View File
@@ -1,3 +1,4 @@
node_modules/ node_modules/
docs/ docs/
out/ out/
build.json
+54 -15
View File
@@ -27,25 +27,64 @@ If you are running Linux with a graphical user interface (Wayland or X11), you s
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | 🟩 | | Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | 🟩 |
### HDMI ### HDMI
| Device | System | Display | Status | | Device | System | Display | Status |
| ------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------ | | ------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------ |
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [LAFVIN 5" Touch Display (800x480)](https://www.amazon.de/gp/product/B0BWJ8YP7S) | 🟨 | | Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [LAFVIN Touch Display 5" (800x480)](https://www.amazon.de/gp/product/B0BWJ8YP7S) | 🟨 |
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 8" CAPLCD (768x1024)](https://www.waveshare.com/wiki/8inch_768x1024_LCD) | 🟨 | | Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 7EP-CAPLCD 7" (1280x800)](https://www.waveshare.com/7ep-caplcd.htm) | 🟨 |
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 7" 7EP-CAPLCD (1280x800)](https://www.waveshare.com/7ep-caplcd.htm) | 🟨 | | Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare CAPLCD 8" (768x1024)](https://www.waveshare.com/wiki/8inch_768x1024_LCD) | 🟨 |
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Hannspree HT225 21.5" (1920x1080)](https://www.hannspree.eu/product/HT-225-HPB) | 🟨 | | Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Hannspree HT225 21.5" (1920x1080)](https://www.hannspree.eu/product/HT-225-HPB) | 🟨 |
| Raspberry Pi 400 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [UPerfect Vertical Touch 15.6" (1920x1080)](https://uperfect.com/products/uperfect-y-vertical-monitor-15-6) | 🟨 | | Raspberry Pi CM 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Lenovo ThinkCentre 23.8" (1920x1080)](https://psref.lenovo.com/Detail/ThinkCentre_Tiny_In_One_24_Gen_5?M=12NBGAT1EU) | 🟨 |
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [GeeekPi 10.1" Capacitive Touch (1280x800)](https://www.amazon.nl/dp/B0DHV6DZC1) | 🟨 | | Raspberry Pi 400 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [UPerfect Vertical Touch 15.6" (1920x1080)](https://uperfect.com/products/uperfect-y-vertical-monitor-15-6) | 🟨 |
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Prechen Portable Touch 18.5" FHD (1920x1080)](https://www.amazon.de/dp/B0CT2KLDBQ) | 🟨 | | Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [GeeekPi Capacitive Touch 10.1" (1280x800)](https://www.amazon.nl/dp/B0DHV6DZC1) | 🟨 |
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Akntzcs Portable Touch 16" HD (1920x1200)](https://www.amazon.com/dp/B0CTGW6MQ6) | 🟨 | | Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [ELECROW Portable Monitor 10.1" (1280x800)](https://www.amazon.co.uk/dp/B0BHHQLKPY) | 🟨 |
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | Generic Non-Touch | 🟨 | | Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare Capacitive Touch 14" (2160x1440)](https://www.waveshare.com/14inch-2160x1440-lcd.htm) | 🟨 |
| Generic PC (x64) | Debian KDE (64-bit), X11 | Generic Non-Touch | 🟧 | | Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Akntzcs Portable Touch HD 16" (1920x1200)](https://www.amazon.com/dp/B0CTGW6MQ6) | 🟨 |
| Generic PC (x64) | Ubuntu GNOME (64-bit), X11 | Generic Non-Touch | 🟧 | | Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Prechen Portable Touch FHD 18.5" (1920x1080)](https://www.amazon.de/dp/B0CT2KLDBQ) | 🟨 |
| Generic PC (x64) | Ubuntu GNOME (64-bit), Wayland | Generic Non-Touch | 🟥 | | Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | Generic Non-Touch | 🟨 |
| Generic PC (x64) | Debian KDE (64-bit), Wayland, X11 | Generic Non-Touch | 🟧 |
| Generic PC (x64) | Ubuntu XFCE (64-bit), X11 | Generic Non-Touch | 🟧 |
| Generic PC (x64) | Ubuntu GNOME (64-bit), X11 | Generic Non-Touch | 🟧 |
| Generic PC (x64) | Ubuntu GNOME (64-bit), Wayland | Generic Non-Touch | 🟥 |
## Features
**Minimal features** are designed to run on any system without issues:
- A webview kiosk window launched in fullscreen mode and loading the specified `--web-url` website should not cause any problems.
- When connecting to an internal HTTPS site with a self-signed certificate, ensure the specified FQDN matches. Using the `--ignore-certificate-errors` flag is [not recommended](https://github.com/leukipp/touchkio/issues/76) for browsing external sites.
**Extended features** become available when the `--mqtt-*` arguments are provided and the hardware is supported:
- The following commands are currently implemented to modify the display status. Make sure that one of these works for your display when you run it directly on the terminal, otherwise the MQTT switch will not work either.
- `wlopm --[on,off] \*` (Raspberry Pi OS, Wayland)
- `kscreen-doctor --dpms [on,off]` (Debian KDE, Wayland)
- `xset dpms force [on,off]` (Generic, X11)
- If your hardware is not fully compatible there should be no crashes, but you may miss some sensors.
- On some Debian based systems (e.g. Ubuntu GNOME), the display status control is only available when using X11 (`xset`).
- There are certain HDMI screens (e.g. Viewsonic) where the display status command doesn't work as expected.
- The screen [immediately turns on again](https://github.com/leukipp/touchkio/issues/38), which may be caused by incompatible HDMI cables being used.
- On Raspberry Pi OS the display command may fail with `ERROR: Setting power mode for output '[DSI-*,HDMI-*]' failed`.
- This can happen if you have `wayvnc` running for remote access and is caused by a [known bug](https://github.com/leukipp/touchkio/issues/78).
- Certain commands from the MQTT integration may require elevated privileges to work correctly.
- Ensure that your local user has the [necessary permissions](https://github.com/leukipp/touchkio/issues/39) to run `sudo` without being prompted for a password.
Hardware support is verified during application startup and can be seen in the terminal or in the log file under the `Supported` section.
The necessary requirements for MQTT sensors to work are listed here:
| Name | Requirements | References |
| -------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| App (Update) | Requires `sudo` rights, `.deb` install and `touchkio.service` running . | [#70](https://github.com/leukipp/touchkio/issues/70), [#77](https://github.com/leukipp/touchkio/issues/77) |
| Display (Status) | Working `wlopm`, `kscreen-doctor` or `xset` command. | [#38](https://github.com/leukipp/touchkio/issues/38), [#57](https://github.com/leukipp/touchkio/issues/57) |
| Display (Brightness) | Device under `/sys/class/backlight/*/brightness` exists. | [#30](https://github.com/leukipp/touchkio/issues/30), [#32](https://github.com/leukipp/touchkio/issues/32) |
| Keyboard | Raspberry Pi OS (Wayland) with `squeekboard` running. | [#7](https://github.com/leukipp/touchkio/issues/7), [#85](https://github.com/leukipp/touchkio/issues/85) |
| Battery | Device under `/sys/class/power_supply/*/capacity` exists. | [#33](https://github.com/leukipp/touchkio/issues/33) |
| Volume | Device (non-dummy) `pactl get-default-sink` exists. | [#82](https://github.com/leukipp/touchkio/issues/82) |
| Reboot | Requires password-less `sudo` rights. | [#39](https://github.com/leukipp/touchkio/issues/39) |
| Shutdown | Requires password-less `sudo` rights. | [#39](https://github.com/leukipp/touchkio/issues/39) |
## Contributions ## Contributions
In case your hardware is not listed above don't worry, give it a try. In case your hardware is not listed above don't worry, give it a try.
Running `touchkio --web-url=https://demo.home-assistant.io` will most likely just work. Running `touchkio --web-url=https://demo.home-assistant.io` will most likely just work.
The only problems that may arise are when controlling the display or keyboard via the Home Assistant integration. The only problems that may arise are when controlling the display via the Home Assistant integration.
- If you encounter any problems, please create a new [issue](https://github.com/leukipp/touchkio/issues). - If you encounter any problems, please create a new [issue](https://github.com/leukipp/touchkio/issues).
- If you encounter any problems and are able to fix it yourself, feel free to create a [pull request](https://github.com/leukipp/touchkio/pulls). - If you encounter any problems and are able to fix it yourself, feel free to create a [pull request](https://github.com/leukipp/touchkio/pulls).
+82 -40
View File
@@ -1,4 +1,10 @@
# TouchKio # TouchKio
![build](https://img.shields.io/github/actions/workflow/status/leukipp/touchkio/release.yaml?style=flat-square)
![date](https://img.shields.io/github/release-date/leukipp/touchkio?style=flat-square)
![platform](https://img.shields.io/badge/platform-%20arm64%20|%20x64%20-teal?style=flat-square)
![downloads](https://img.shields.io/github/downloads/leukipp/touchkio/total?style=flat-square)
[![sponsor](https://img.shields.io/github/sponsors/leukipp?color=red&logo=github&style=flat-square)](https://github.com/sponsors/leukipp)
**TouchKio** is a Node.js application that utilizes Electron to create a kiosk mode window specifically designed for a Home Assistant dashboard. **TouchKio** is a Node.js application that utilizes Electron to create a kiosk mode window specifically designed for a Home Assistant dashboard.
This tool is packaged as a **.deb** file, making it easy to launch the kiosk application on any Debian based Linux [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) (e.g. **Raspberry Pi**) equipped with a **DSI or HDMI** Touch Display. This tool is packaged as a **.deb** file, making it easy to launch the kiosk application on any Debian based Linux [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) (e.g. **Raspberry Pi**) equipped with a **DSI or HDMI** Touch Display.
Additional releases for other Linux systems are available as **.zip** file. Additional releases for other Linux systems are available as **.zip** file.
@@ -12,18 +18,21 @@ Moreover, the device running the **kiosk application** also offers several **Hom
- [x] Fast and easy setup. - [x] Fast and easy setup.
- [x] Remember login credentials. - [x] Remember login credentials.
- [x] Touch optimized web browsing. - [x] Touch optimized web browsing.
- [x] Dynamic window status bar layout.
- [x] Side panel widget for kiosk control. - [x] Side panel widget for kiosk control.
- [x] Navigation bar to switch between pages. - [x] Navigation bar to switch between pages.
- [x] Configurable browser zoom and theme support. - [x] Adjustable browser zoom and theme support.
- [x] Extended touch screen wake-up functionality. - [x] Extended touch screen wake-up functionality.
- [x] Remote controllable via MQTT. - [x] Remote controllable via MQTT.
- [x] Manage kiosk window status.
- [x] Toggle the on-screen keyboard. - [x] Toggle the on-screen keyboard.
- [x] Touch display power and brightness. - [x] Touch display power and brightness.
- [x] Manage kiosk window status and zoom.
- [x] Show network interfaces and addresses.
- [x] List all available system package upgrades.
- [x] Multi-webpage switching and url navigation. - [x] Multi-webpage switching and url navigation.
- [x] Monitor temperature, processor and memory usage. - [x] Audio volume control for connected devices.
- [x] Execute system reboot and shutdown commands. - [x] Execute system reboot and shutdown commands.
- [x] Show available system package upgrades. - [x] Monitor battery, temperature, processor and memory usage.
The kiosk application can be executed with command line arguments to load a **Home Assistant dashboard in fullscreen** mode. The kiosk application can be executed with command line arguments to load a **Home Assistant dashboard in fullscreen** mode.
Additionally, a **MQTT endpoint** can be defined, allowing the application to provide controls and sensors for the Linux device and the connected Touch Display. Additionally, a **MQTT endpoint** can be defined, allowing the application to provide controls and sensors for the Linux device and the connected Touch Display.
@@ -43,7 +52,7 @@ For a comprehensive guide on setting up MQTT with Home Assistant, please refer t
## Installation ## Installation
On the first run of the application, you may encounter a **setup procedure** and the Home Assistant **login screen**. On the first run of the application, you may encounter a **setup procedure** and the Home Assistant **login screen**.
It's recommended to create a **dedicated user** (local access only) for your kiosk device. It's recommended to create a dedicated Home Assistant user (local access only) for your kiosk device.
You might also need a physical keyboard or remote VNC access to input these credentials once. You might also need a physical keyboard or remote VNC access to input these credentials once.
If your hardware is [supported](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) you may be able to activate the on-screen keyboard using the side [widget](https://github.com/leukipp/touchkio/issues/16). If your hardware is [supported](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) you may be able to activate the on-screen keyboard using the side [widget](https://github.com/leukipp/touchkio/issues/16).
@@ -56,7 +65,6 @@ bash <(wget -qO- https://raw.githubusercontent.com/leukipp/touchkio/main/install
``` ```
Make sure that you run this with your **standard user** and not with root (sudo). Make sure that you run this with your **standard user** and not with root (sudo).
If you are paranoid, or smart, or both, have a look into the [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script before executing external code on your machine. If you are paranoid, or smart, or both, have a look into the [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script before executing external code on your machine.
This command can also be re-run to **update** an existing installation to the latest version.
The systemd **user service** is enabled by default and the kiosk application should start automatically the next time you boot. The systemd **user service** is enabled by default and the kiosk application should start automatically the next time you boot.
If you need manual control use: If you need manual control use:
@@ -70,9 +78,8 @@ systemctl --user start|stop|status|restart touchkio.service
When connected via SSH, it's necessary to export the display variables first, as outlined in the [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development) section. When connected via SSH, it's necessary to export the display variables first, as outlined in the [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development) section.
The [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script mentioned above performs the following tasks (and you just have to do it manually): The [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script mentioned above performs the following tasks (and you just have to do it manually):
- [Download](https://github.com/leukipp/touchkio/releases/latest) the latest version file that is suitable for your architecture (arm64 or x64). - [Download](https://github.com/leukipp/touchkio/releases/latest) the latest version file that is suitable for your architecture (arm64 or x64).
- Debian (**deb**): Open a terminal and execute the following command to install the application, e.g: - Debian (**deb**): Open a terminal and execute the following command to install the application, e.g: `sudo apt install ./touchkio_1.x.x_arm64.deb && touchkio --setup`
`sudo apt install ./touchkio_1.x.x_arm64.deb && touchkio --setup` - Others (**zip**): Extract the archive and run the binary, e.g: `unzip touchkio-linux-x64-1.x.x.zip && cd touchkio-linux-x64 && ./touchkio --setup`
- Others (**zip**): Extract the archive and run the binary, e.g: `unzip touchkio-linux-x64-1.x.x.zip && cd touchkio-linux-x64 && ./touchkio --setup`
- If you just want to load a Home Assistant dashboard without further control you are good to go, e.g: `touchkio --web-url=https://demo.home-assistant.io` - If you just want to load a Home Assistant dashboard without further control you are good to go, e.g: `touchkio --web-url=https://demo.home-assistant.io`
- The `--web-url` doesn't necessarily need to be a Home Assistant url, any kind of website can be shown in kiosk mode. - The `--web-url` doesn't necessarily need to be a Home Assistant url, any kind of website can be shown in kiosk mode.
- Only when using the MQTT integration via `--mqtt-*`, a running Home Assistant instance is required. - Only when using the MQTT integration via `--mqtt-*`, a running Home Assistant instance is required.
@@ -89,12 +96,21 @@ yarn build
</div></details> </div></details>
#### Update
If you have already installed TouchKio and want to upgrade to the latest version, simply install the newer version over the existing one.
The [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script can also be run to update an existing installation to the **latest version**.
The setup procedure can be skipped to use the existing default arguments from the configuration file:
```bash
bash <(wget -qO- https://raw.githubusercontent.com/leukipp/touchkio/main/install.sh) update
```
## Configuration ## Configuration
Running `touchkio --setup` will prompt you to enter arguments that will be used when the application starts without any specified arguments. Running `touchkio --setup` will prompt you to enter arguments that will be used when the application starts without any specified arguments.
These default arguments are stored in `~/.config/touchkio/Arguments.json`, where they can also be modified. These default arguments are stored in `~/.config/touchkio/Arguments.json`, where they can also be modified.
### WEB ### WEB
The available arguments to control the kiosk application via terminal are as follows: The available arguments to control the kiosk application via terminal are as follows:
| Name | Default | Description | | Name | Default | Description |
| ------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- | | ------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| `--web-url` (Required) | - | Url of the Home Assistant instance<a id="ref1"></a><sup><a href="#foot1">[1]</a></sup> (HTTP(S)://IP:PORT) | | `--web-url` (Required) | - | Url of the Home Assistant instance<a id="ref1"></a><sup><a href="#foot1">[1]</a></sup> (HTTP(S)://IP:PORT) |
@@ -109,16 +125,16 @@ For example:
touchkio --web-url=http://192.168.1.42:8123 --web-theme=light --web-zoom=1.0 touchkio --web-url=http://192.168.1.42:8123 --web-theme=light --web-zoom=1.0
``` ```
> <a href="#ref1">↩ </a><a id="foot1"></a>**[1]:** Not necessarily a Home Assistant Url. > <a id="foot1"></a><a href="#ref1">[1]</a>: This doesn't necessarily have to be a Home Assistant Url.
> You can load multiple pages by separating them with a comma: > You can configure multiple pages by separating them with a comma:
> `touchkio --web-url=https://demo.home-assistant.io,https://imvbh.github.io/FlipClock`. > `touchkio --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app`.
In the `~/.config/touchkio/Arguments.json` file: In the `~/.config/touchkio/Arguments.json` file:
```json ```json
{ {
"web_url": [ "web_url": [
"https://demo.home-assistant.io", "https://demo.home-assistant.io",
"https://imvbh.github.io/FlipClock" "https://demo.immichkiosk.app"
] ]
} }
``` ```
@@ -140,6 +156,29 @@ For example:
touchkio --web-url=http://192.168.1.42:8123 --mqtt-url=mqtt://192.168.1.42:1883 --mqtt-user=kiosk --mqtt-password=password touchkio --web-url=http://192.168.1.42:8123 --mqtt-url=mqtt://192.168.1.42:1883 --mqtt-user=kiosk --mqtt-password=password
``` ```
## User Interface
TouchKio provides a simple webview window designed specifically for Touch Displays. Electron apps are known to be resource intensive due to their architecture and the inclusion of a full web browser environment. If you just run the kiosk application without other heavy loads, everything should run smoothly.
### Touch Controls
Additional controls can be found along the right edge of the kiosk application. For more details, you may want to have a closer look here:
| Name | Description |
| ------------ | -------------------------------------------------------- |
| `Widget` | [See #16](https://github.com/leukipp/touchkio/issues/16) |
| `Navigation` | [See #45](https://github.com/leukipp/touchkio/issues/45) |
| `Pager` | [See #64](https://github.com/leukipp/touchkio/issues/64) |
### Keyboard Shortcuts
The application also supports basic shortcuts to enhance navigation and usability for users who prefer or require non-touch input methods:
| Name | Description |
| --------------------------- | ---------------------------- |
| `Control+Left` | Navigate to previous page |
| `Control+Right` | Navigate to next page |
| `Control+Num_Subtract` | Decrease the page zoom |
| `Control+Num_Add` | Increase the page zoom |
| `Alt+Left`/`Mouse+Back` | Navigate backward in history |
| `Alt+Right`/`Mouse+Forward` | Navigate forward in history |
## Development ## Development
To create your own local build, you first need to install [Node.js](https://pimylifeup.com/raspberry-pi-nodejs) and [Yarn](https://classic.yarnpkg.com/lang/en/docs/install). To create your own local build, you first need to install [Node.js](https://pimylifeup.com/raspberry-pi-nodejs) and [Yarn](https://classic.yarnpkg.com/lang/en/docs/install).
@@ -158,7 +197,7 @@ To make this permanent, consider adding the export variables into the `~/.bashrc
<details><summary>You probably won't need this.</summary><div></br> <details><summary>You probably won't need this.</summary><div></br>
Incorporating custom extensions and external hardware (like motion sensors, ultrasonic sensors, cameras, relays and switches) via **Raspberry Pi's GPIO/USB** involves several steps. Incorporating custom extensions and external hardware (like motion sensors, ultrasonic sensors, cameras, relays and switches) via **Raspberry Pi's GPIO/USB** involves several steps.
While using external sensors that directly integrate with Home Assistant and by utilizing automation's to interact with **TouchKio via MQTT** is generally easier and **recommended**, here's a rough guide on how to proceed with custom hardware integration: While using external sensors that directly integrate with Home Assistant and by utilizing automation's to interact with **TouchKio via MQTT** is generally easier and **recommended**, here's a rough guide on how to proceed with custom hardware integration:
1. **Install Node.js library**: Use Yarn to add a library that can interact with your hardware (GPIO, USB, etc.): 1. **Install Node.js library**: Use Yarn to add a library that can interact with your hardware (GPIO, USB, etc.):
@@ -185,7 +224,7 @@ While using external sensors that directly integrate with Home Assistant and by
```javascript ```javascript
const init = async (args) => { ... } const init = async (args) => { ... }
``` ```
From there, you will need to further refine your code by tinkering with sensor updates. This can be achieved through either periodic update calls or event based triggers. From there, you will need to further refine your code by tinkering with sensor updates. This can be achieved through either periodic update calls or event based triggers.
</div></details> </div></details>
@@ -193,8 +232,8 @@ While using external sensors that directly integrate with Home Assistant and by
<details><summary>Don't waste your time reading this.</summary><div></br> <details><summary>Don't waste your time reading this.</summary><div></br>
The Raspberry Pi's **build-in on-screen keyboard** named `squeekboard` (it squeaks because some *Rust* got inside), is specifically designed for Wayland environments and features a **D-Bus interface** that allows applications to show or hide the keyboard as needed. The Raspberry Pi's **build-in on-screen keyboard** named `squeekboard` (it squeaks because some _Rust_ got inside), is specifically designed for Wayland environments and features a **D-Bus interface** that allows applications to show or hide the keyboard as needed.
The kiosk application interacts with squeekboard via the `D-Bus` object path `/sm/puri/OSK0`, enabling the keyboard to be hidden or shown based on **MQTT** user input or system events. The kiosk application interacts with squeekboard via the `D-Bus` object path `/sm/puri/OSK0`, enabling the keyboard to be hidden or shown based on **MQTT** user input or system events.
The Raspberry Pi's **build-in screen blanking** function uses the command `swayidle -w timeout 600 'wlopm --off \*' resume 'wlopm --on \*' &` inside `~/.config/labwc/autostart` to blank the screen after **10 minutes**. The Raspberry Pi's **build-in screen blanking** function uses the command `swayidle -w timeout 600 'wlopm --off \*' resume 'wlopm --on \*' &` inside `~/.config/labwc/autostart` to blank the screen after **10 minutes**.
The `wlopm --off \*` command changes the `bl_power` value to **4**, when setting the value to **0** the screen will turn on again. The `wlopm --off \*` command changes the `bl_power` value to **4**, when setting the value to **0** the screen will turn on again.
@@ -205,7 +244,7 @@ I managed to achieve this for the `brightness` file by implementing a simple `fs
However, I found that it **never triggered** for the `bl_power` file. However, I found that it **never triggered** for the `bl_power` file.
Although the file content changes, none of the filesystem listeners where fired. Although the file content changes, none of the filesystem listeners where fired.
This could be due to `swayidle`/`wlopm` performing write actions at a deeper level that are not detectable by file listeners. This could be due to `swayidle`/`wlopm` performing write actions at a deeper level that are not detectable by file listeners.
As a result, I went for a **polling solution**, checking the state of the file **every second** for any changes. As a result, I went for a **polling solution**, checking the state of the `brightness` and `dpms` file **every second** for any changes.
While I understand this is not ideal, it's necessary to ensure proper functionality. While I understand this is not ideal, it's necessary to ensure proper functionality.
The display power status and brightness can be adjusted via the MQTT integration. The display power status and brightness can be adjusted via the MQTT integration.
@@ -219,39 +258,42 @@ Implementing stronger **security measures** would complicate the setup process a
When using the kiosk application without initializing the default arguments, you will need to provide them with every command. When using the kiosk application without initializing the default arguments, you will need to provide them with every command.
This means that the password may be stored as plain text in various files, such as `touchkio.service`, `~/.bash_history`, etc. This means that the password may be stored as plain text in various files, such as `touchkio.service`, `~/.bash_history`, etc.
To resolve the issue where the first **touch** on a **turned-off screen** triggers a **click event** (which could inadvertently activate Home Assistant actions), a workaround needed to be implemented. To address the problem where the first **touch** on a **turned-off screen** may trigger a **click event** (which could inadvertently activate Home Assistant actions), a workaround needed to be implemented.
When the screen **turns off** the **focus** is removed from the kiosk window. When the screen **turns on/off**, the timestamps of these events are recorded.
This way, the first click only turns the screen on and focuses the window, allowing subsequent clicks to work as expected. If a touch event is detected and the **timestamp** of the **last screen-off** is more recent than the **last screen-on** timestamp, the touch event is discarded and the screen is turned on instead.
From that point onward, subsequent touch interactions will function normally as expected.
Additionally, to address the problem that scrolling only works with the **web browser scrollbar** on the right, the application is configured to **simulate a touch device** using `Emulation.setEmitTouchEventsForMouse`. Additionally, to address the problem that scrolling only works with the **web browser scrollbar** on the right, the application is configured to **simulate a touch device** using `Emulation.setEmitTouchEventsForMouse`.
This adjustment provides a user experience similar to that of a proper mobile device. This adjustment provides a user experience similar to that of a proper mobile device.
Electron apps are known to be **resource intensive** due to their architecture and the inclusion of a full web browser environment. If you just run the kiosk application without other heavy loads, everything should run smoothly.
</div></details> </div></details>
## Issues ## Issues
Please have a look into the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation if you ran into any issues. Please review the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation first if you encounter any problems.
**Minimal features** are designed to run on any system without issues:
- A webview kiosk window launched in fullscreen mode and loading the specified `--web-url` website should not cause any problems.
**Extended features** become available when the `--mqtt-*` arguments are provided and the hardware is supported:
- If your hardware is not fully compatible there should be no crashes, but you may miss some sensors.
- On some Debian based systems (e.g. Ubuntu), the display status control may only be available when using X11.
**Known Issues** that are by-design or for which there isn't a solution so far: **Known Issues** that are by-design or for which there isn't a solution so far:
- You can use Raspberry Pi's build-in screen blanking functionality, however, if the screen is turned on through Home Assistant after being automatically turned off, it will remain on indefinitely. - You can use Raspberry Pi's build-in [screen blanking](https://www.raspberrypi.com/documentation/computers/configuration.html#screen-blanking-3) functionality, however, if the screen is turned on through Home Assistant after being automatically turned off, it will remain on indefinitely.
- It's recommended to either use the built-in screen blanking feature or implement a Home Assistant automation (e.g. presence detection) to manage the screen status. - It's recommended to either use the built-in screen blanking feature or implement a Home Assistant [automation](https://www.home-assistant.io/docs/automation/basics) (e.g. presence detection) to manage the screen status.
- When using a Raspberry Pi, the on-screen keyboard doesn't automatically pop-out when entering a text field inside the webview. - An active VNC session can also interfere with the display state, you can find details about this in the features [section](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#features).
- As a current [workaround](https://github.com/leukipp/touchkio/issues/4) you can use the side [widget](https://github.com/leukipp/touchkio/issues/16) to toggle the keyboard visibility. - When using a Raspberry Pi, the on-screen keyboard doesn't [automatically pop-out](https://github.com/leukipp/touchkio/issues/4) when entering a text field inside the webview.
- On the terminal you may see some *ERROR:gbm_wrapper.cc* messages. - As a current workaround you can use the side [widget](https://github.com/leukipp/touchkio/issues/16) to toggle the keyboard visibility.
- This appears to be a [known issue](https://github.com/electron/electron/issues/42322) that currently lacks a fix, but the webview still works. - There is also an [experimental feature](https://github.com/leukipp/touchkio/issues/85) that uses a special Electron flag to address this problem.
- On the terminal you may see some _ERROR:gbm_wrapper.cc_ messages.
- This appears to be a [known issue](https://github.com/electron/electron/issues/42322) that currently lacks a fix, but the webview still works.
For debugging, stop the service and launch `touchkio` directly on the terminal to monitor the log output in real-time.
This output is also written into `~/.config/touchkio/logs/main.log` for review.
If you encounter any problems, please [create an issue](https://github.com/leukipp/touchkio/issues) and include the output of `touchkio --version`, additional information's about your system (such as operating system, hardware, etc.) and any relevant log files.
## Credits ## Credits
[Inspired by](https://www.jeffgeerling.com/blog/2024/home-assistant-and-carplay-pi-touch-display-2) the one and only Raspberry Pi Master, Jeff Geerling ([@geerlingguy](https://github.com/geerlingguy)).
Thanks to Sebastian ([@pdsccode](https://github.com/pdsccode)) for his contributions on issues and [community](https://community.home-assistant.io/t/kiosk-mode-for-raspberry-pi-with-touch-display/821196) discussions. Thanks to Sebastian ([@pdsccode](https://github.com/pdsccode)) for his contributions on issues and [community](https://community.home-assistant.io/t/kiosk-mode-for-raspberry-pi-with-touch-display/821196) discussions.
[Inspired by](https://www.jeffgeerling.com/blog/2024/home-assistant-and-carplay-pi-touch-display-2) the one and only Raspberry Pi Master, Jeff Geerling ([@geerlingguy](https://github.com/geerlingguy)). If you are looking for hardware or a well-designed mounting solution for the Raspberry Pi Display, check out the [blog post](https://www.thestockpot.net/videos/home-assistant-wall-display) from Dillan Stock ([@TheStockPot-AU](https://www.youtube.com/channel/UCmlzNHg8QiWVutUyFOV2UdQ)).
[![What My Smart Home Was Missing](https://img.youtube.com/vi/uTxURzmrVtA/maxresdefault.jpg)](https://www.youtube.com/watch?v=uTxURzmrVtA)
## License ## License
[MIT](https://github.com/leukipp/touchkio/blob/main/LICENSE) [MIT](https://github.com/leukipp/touchkio/blob/main/LICENSE)
+35 -2
View File
@@ -1,4 +1,22 @@
const fs = require("fs"); const fs = require("fs");
const jszip = require("jszip");
const crypto = require("crypto");
const path = require("path");
const date = new Date().toISOString();
const generateBuildFile = (platform, arch, maker) => {
const package = fs.readFileSync(path.join(__dirname, "package.json"), "utf8");
const hash = crypto.createHash("sha256").update(package);
const data = {
id: hash.digest("hex").slice(-6),
platform: platform,
arch: arch,
maker: maker,
date: date,
};
return JSON.stringify(data, null, 2);
};
module.exports = { module.exports = {
packagerConfig: { packagerConfig: {
@@ -33,10 +51,25 @@ module.exports = {
}, },
], ],
hooks: { hooks: {
postPackage: async (config, results) => {
for (const outputPath of results.outputPaths) {
const [name, platform, arch] = path.basename(outputPath).split("-");
const buildFile = path.join(outputPath, "resources", "app", "build.json");
fs.writeFileSync(buildFile, generateBuildFile(platform, arch, "deb"), { encoding: "utf8" });
}
},
postMake: async (config, results) => { postMake: async (config, results) => {
for (const result of results) { for (const result of results) {
const artifacts = []; const artifacts = [];
result.artifacts.forEach((artifact) => { for (const artifact of result.artifacts) {
if (artifact.includes(".zip")) {
const [name, platform, arch] = path.basename(artifact).split("-");
const buildFile = path.join(`${name}-${platform}-${arch}`, "resources", "app", "build.json");
const options = { type: "nodebuffer", compression: "DEFLATE", compressionOptions: { level: 9 } };
const zip = await jszip.loadAsync(fs.readFileSync(artifact));
zip.file(buildFile, generateBuildFile(platform, arch, "zip"));
fs.writeFileSync(artifact, await zip.generateAsync(options));
}
if (artifact.includes("amd64")) { if (artifact.includes("amd64")) {
const renamed = artifact.replace("amd64", "x64"); const renamed = artifact.replace("amd64", "x64");
fs.renameSync(artifact, renamed); fs.renameSync(artifact, renamed);
@@ -44,7 +77,7 @@ module.exports = {
} else { } else {
artifacts.push(artifact); artifacts.push(artifact);
} }
}); }
result.artifacts = artifacts; result.artifacts = artifacts;
} }
}, },
+76 -14
View File
@@ -32,8 +32,8 @@
align-items: center; align-items: center;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
padding: 0 30px; padding: 0 10px;
column-gap: 20px; column-gap: 10px;
list-style-type: none; list-style-type: none;
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
background: var(--background-color); background: var(--background-color);
@@ -46,11 +46,19 @@
li.dynamic { li.dynamic {
flex: 1 1 0%; flex: 1 1 0%;
} }
li.separator {
height: 32px;
border: 1px solid var(--border-color);
}
li.separator:has(+ li.hidden) {
display: none;
}
li.hidden { li.hidden {
display: none; display: none;
} }
li.disabled { li.disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed;
pointer-events: none; pointer-events: none;
} }
button { button {
@@ -62,12 +70,14 @@
input { input {
width: 100%; width: 100%;
min-width: 0; min-width: 0;
margin: 0 10px; margin: 0 5px;
padding: 6px 10px; padding: 6px 10px;
border-radius: 4px; border-radius: 4px;
font-size: 18px; font-size: 18px;
font-family: sans-serif;
outline: none; outline: none;
border: none; border: none;
cursor: text;
box-sizing: border-box; box-sizing: border-box;
color: var(--primary-color); color: var(--primary-color);
background-color: var(--border-color); background-color: var(--border-color);
@@ -91,7 +101,28 @@
</button> </button>
</li> </li>
<li> <li>
<button id="pager"> <button id="refresh">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>refresh-circle</title>
<path
d="M12 2A10 10 0 1 0 22 12A10 10 0 0 0 12 2M18 11H13L14.81 9.19A3.94 3.94 0 0 0 12 8A4 4 0 1 0 15.86 13H17.91A6 6 0 1 1 12 6A5.91 5.91 0 0 1 16.22 7.78L18 6Z"
/>
</svg>
</button>
</li>
<li class="separator"></li>
<li>
<button id="previous">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>page-previous</title>
<path
d="M4,21H19A2,2 0 0,0 21,19V13H17V15L13,12L17,9V11H21V5A2,2 0 0,0 19,3H4A2,2 0 0,0 2,5V19A2,2 0 0,0 4,21M4,15H8V17H4V15M4,11H11V13H4V11M4,7H11V9H4V7M21,11H24V13H21V11Z"
/>
</svg>
</button>
</li>
<li>
<button id="next">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>page-next</title> <title>page-next</title>
<path <path
@@ -100,15 +131,17 @@
</svg> </svg>
</button> </button>
</li> </li>
<li class="separator"></li>
<li class="dynamic"> <li class="dynamic">
<input id="url" type="text" spellcheck="false" /> <input id="url" type="text" spellcheck="false" readonly="true" />
</li> </li>
<li class="separator"></li>
<li> <li>
<button id="backward"> <button id="backward">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>arrow-left-circle</title> <title>history</title>
<path <path
d="M2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12M18,11H10L13.5,7.5L12.08,6.08L6.16,12L12.08,17.92L13.5,16.5L10,13H18V11Z" d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3"
/> />
</svg> </svg>
</button> </button>
@@ -116,9 +149,30 @@
<li> <li>
<button id="forward"> <button id="forward">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>arrow-right-circle</title> <title>update</title>
<path <path
d="M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M6,13H14L10.5,16.5L11.92,17.92L17.84,12L11.92,6.08L10.5,7.5L14,11H6V13Z" d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z"
/>
</svg>
</button>
</li>
<li class="separator"></li>
<li>
<button id="minus">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>magnify-minus</title>
<path
d="M9,2A7,7 0 0,1 16,9C16,10.57 15.5,12 14.61,13.19L15.41,14H16L22,20L20,22L14,16V15.41L13.19,14.61C12,15.5 10.57,16 9,16A7,7 0 0,1 2,9A7,7 0 0,1 9,2M5,8V10H13V8H5Z"
/>
</svg>
</button>
</li>
<li>
<button id="plus">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>magnify-plus</title>
<path
d="M9,2A7,7 0 0,1 16,9C16,10.57 15.5,12 14.61,13.19L15.41,14H16L22,20L20,22L14,16V15.41L13.19,14.61C12,15.5 10.57,16 9,16A7,7 0 0,1 2,9A7,7 0 0,1 9,2M8,5V8H5V10H8V13H10V10H13V8H10V5H8Z"
/> />
</svg> </svg>
</button> </button>
@@ -134,6 +188,9 @@
}); });
}); });
document.querySelectorAll("input").forEach((input) => { document.querySelectorAll("input").forEach((input) => {
input.addEventListener("blur", (e) => {
ipcRenderer.send("input-blur", { id: e.target.id });
});
input.addEventListener("focus", (e) => { input.addEventListener("focus", (e) => {
ipcRenderer.send("input-focus", { id: e.target.id }); ipcRenderer.send("input-focus", { id: e.target.id });
}); });
@@ -152,16 +209,21 @@
ipcRenderer.on("button-hidden", (e, button) => { ipcRenderer.on("button-hidden", (e, button) => {
document.getElementById(button.id).parentNode.classList.toggle("hidden", button.hidden); document.getElementById(button.id).parentNode.classList.toggle("hidden", button.hidden);
}); });
ipcRenderer.on("input-blur", (e, input) => { ipcRenderer.on("input-readonly", (e, input) => {
document.getElementById(input.id).blur(); document.getElementById(input.id).readOnly = input.readonly;
});
ipcRenderer.on("input-select", (e, input) => {
document.getElementById(input.id).select();
}); });
ipcRenderer.on("input-text", (e, input) => { ipcRenderer.on("input-text", (e, input) => {
document.getElementById(input.id).value = input.text; document.getElementById(input.id).value = input.text;
document.getElementById(input.id).placeholder = input.placeholder; document.getElementById(input.id).placeholder = input.placeholder;
}); });
ipcRenderer.on("input-select", (e, input) => {
window.getSelection()?.removeAllRanges();
if (input.select) {
document.getElementById(input.id).select();
} else {
document.getElementById(input.id).blur();
}
});
ipcRenderer.on("data-theme", (e, data) => { ipcRenderer.on("data-theme", (e, data) => {
document.documentElement.setAttribute("data-theme", data.theme); document.documentElement.setAttribute("data-theme", data.theme);
}); });
+80
View File
@@ -0,0 +1,80 @@
<!doctype html>
<html>
<head>
<style>
[data-theme="hidden"] {
--background-color: rgba(0, 0, 0, 0);
}
::-webkit-scrollbar {
display: none;
}
* {
margin: 0;
padding: 0;
cursor: none;
}
body {
display: flex;
align-items: center;
width: 100vw;
height: 100vh;
}
menu {
display: flex;
flex-direction: column;
align-items: center;
width: 100vw;
height: 100vh;
list-style-type: none;
background: var(--background-color);
}
li {
display: flex;
align-items: center;
flex: 0 0 auto;
}
button {
width: 100vw;
height: 50vh;
border: none;
background-color: transparent;
}
#previous {
cursor: w-resize;
}
#next {
cursor: e-resize;
}
</style>
</head>
<body>
<menu>
<li>
<button id="previous"></button>
</li>
<li>
<button id="next"></button>
</li>
</menu>
<script>
const { ipcRenderer } = require("electron");
const registerSendEvents = () => {
document.querySelectorAll("button").forEach((button) => {
button.addEventListener("click", (e) => {
ipcRenderer.send("button-click", { id: e.target.id });
});
});
};
const registerReceiveEvents = () => {
ipcRenderer.on("data-theme", (e, data) => {
document.documentElement.setAttribute("data-theme", data.theme);
});
};
registerSendEvents();
registerReceiveEvents();
</script>
</body>
</html>
+161
View File
@@ -0,0 +1,161 @@
<!doctype html>
<html>
<head>
<style>
[data-theme="dark"] {
--primary-color: rgba(229, 229, 229, 1);
--background-color: rgba(66, 67, 68, 1);
--minimize-color: rgba(52, 199, 89, 1);
--fullscreen-color: rgba(255, 149, 0, 1);
--terminate-color: rgba(255, 59, 48, 1);
}
[data-theme="light"] {
--primary-color: rgba(26, 26, 26, 1);
--background-color: rgba(237, 236, 235, 1);
--minimize-color: rgba(52, 199, 89, 1);
--fullscreen-color: rgba(255, 149, 0, 1);
--terminate-color: rgba(255, 59, 48, 1);
}
::-webkit-scrollbar {
display: none;
}
* {
margin: 0;
padding: 0;
cursor: pointer;
}
body {
display: flex;
align-items: center;
width: 100vw;
height: 100vh;
background: var(--background-color);
}
header {
display: flex;
align-items: center;
flex: 1 1 0%;
height: 100vh;
-webkit-app-region: drag;
}
h1 {
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
font-size: 18px;
font-weight: 500;
font-family: sans-serif;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
user-select: none;
color: var(--primary-color);
}
menu {
display: flex;
flex-direction: row;
align-items: center;
flex: 0 0 auto;
height: 100vh;
min-width: 116px;
padding: 0px 10px;
column-gap: 10px;
list-style-type: none;
background: var(--background-color);
position: relative;
z-index: 1;
}
li {
display: flex;
align-items: center;
flex: 0 0 auto;
}
li.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
button {
width: 32px;
height: 32px;
border: none;
background-color: transparent;
}
svg {
pointer-events: none;
}
#minimize > svg {
fill: var(--minimize-color);
}
#fullscreen > svg {
fill: var(--fullscreen-color);
}
#terminate > svg {
fill: var(--terminate-color);
}
</style>
</head>
<body>
<header>
<h1 id="title"></h1>
</header>
<menu>
<li>
<button id="minimize">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>minus-circle</title>
<path d="M17,13H7V11H17M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</svg>
</button>
</li>
<li>
<button id="fullscreen">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>chevron-up-circle</title>
<path
d="M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M7.4,15.4L12,10.8L16.6,15.4L18,14L12,8L6,14L7.4,15.4Z"
/>
</svg>
</button>
</li>
<li>
<button id="terminate">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>close-circle</title>
<path
d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"
/>
</svg>
</button>
</li>
</menu>
<script>
const { ipcRenderer } = require("electron");
const registerSendEvents = () => {
document.querySelectorAll("button").forEach((button) => {
button.addEventListener("click", (e) => {
ipcRenderer.send("button-click", { id: e.target.id });
});
});
};
const registerReceiveEvents = () => {
ipcRenderer.on("button-disabled", (e, button) => {
document.getElementById(button.id).parentNode.classList.toggle("disabled", button.disabled);
});
ipcRenderer.on("text-content", (e, text) => {
document.getElementById(text.id).textContent = text.content;
});
ipcRenderer.on("data-theme", (e, data) => {
document.documentElement.setAttribute("data-theme", data.theme);
});
};
registerSendEvents();
registerReceiveEvents();
</script>
</body>
</html>
+13 -13
View File
@@ -68,16 +68,6 @@
</head> </head>
<body> <body>
<menu> <menu>
<li>
<button id="keyboard">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>keyboard-close-outline</title>
<path
d="M12 23L16 19H8M4 3C2.9 3 2 3.9 2 5V15C2 16.1 2.9 17 4 17H20C21.1 17 22 16.1 22 15V5C22 3.9 21.1 3 20 3H4M4 5H20V15H4V5M5 6V8H7V6H5M8 6V8H10V6H8M11 6V8H13V6H11M14 6V8H16V6H14M17 6V8H19V6H17M5 9V11H7V9H5M8 9V11H10V9H8M11 9V11H13V9H11M14 9V11H16V9H14M17 9V11H19V9H17M8 12V14H16V12H8Z"
/>
</svg>
</button>
</li>
<li> <li>
<button id="fullscreen"> <button id="fullscreen">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
@@ -89,11 +79,21 @@
</button> </button>
</li> </li>
<li> <li>
<button id="minimize"> <button id="theme">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>image-size-select-small</title> <title>compare</title>
<path <path
d="M23,15H21V17H23V15M23,11H21V13H23V11M23,19H21V21C22,21 23,20 23,19M15,3H13V5H15V3M23,7H21V9H23V7M21,3V5H23C23,4 22,3 21,3M3,21H11V15H1V19A2,2 0 0,0 3,21M3,7H1V9H3V7M15,19H13V21H15V19M19,3H17V5H19V3M19,19H17V21H19V19M3,3C2,3 1,4 1,5H3V3M3,11H1V13H3V11M11,3H9V5H11V3M7,3H5V5H7V3Z" d="M19,3H14V5H19V18L14,12V21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M10,18H5L10,12M10,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H10V23H12V1H10V3Z"
/>
</svg>
</button>
</li>
<li>
<button id="keyboard">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>keyboard-close-outline</title>
<path
d="M12 23L16 19H8M4 3C2.9 3 2 3.9 2 5V15C2 16.1 2.9 17 4 17H20C21.1 17 22 16.1 22 15V5C22 3.9 21.1 3 20 3H4M4 5H20V15H4V5M5 6V8H7V6H5M8 6V8H10V6H8M11 6V8H13V6H11M14 6V8H16V6H14M17 6V8H19V6H17M5 9V11H7V9H5M8 9V11H10V9H8M11 9V11H13V9H11M14 9V11H16V9H14M17 9V11H19V9H17M8 12V14H16V12H8Z"
/> />
</svg> </svg>
</button> </button>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 152 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 90 KiB

+204 -69
View File
@@ -5,9 +5,13 @@ const readline = require("readline/promises");
const integration = require("./js/integration"); const integration = require("./js/integration");
const hardware = require("./js/hardware"); const hardware = require("./js/hardware");
const webview = require("./js/webview"); const webview = require("./js/webview");
const log = require("electron-log");
const { app } = require("electron"); const { app } = require("electron");
const Events = require("events");
global.APP = global.APP || {};
global.ARGS = global.ARGS || {}; global.ARGS = global.ARGS || {};
global.EVENTS = global.EVENTS || new Events();
if (!process.env.DISPLAY) { if (!process.env.DISPLAY) {
console.error(`\n$DISPLAY variable not set to run the GUI application, are you connected via SSH?\n`); console.error(`\n$DISPLAY variable not set to run the GUI application, are you connected via SSH?\n`);
@@ -15,6 +19,7 @@ if (!process.env.DISPLAY) {
console.error(` systemctl --user start touchkio.service`); console.error(` systemctl --user start touchkio.service`);
console.error(`Alternatively export the variables first:`); console.error(`Alternatively export the variables first:`);
console.error(` export DISPLAY=":0" && export WAYLAND_DISPLAY="wayland-0" && touchkio\n`); console.error(` export DISPLAY=":0" && export WAYLAND_DISPLAY="wayland-0" && touchkio\n`);
process.exit(1);
} }
/** /**
@@ -23,43 +28,16 @@ if (!process.env.DISPLAY) {
* initialization tasks. * initialization tasks.
*/ */
app.whenReady().then(async () => { app.whenReady().then(async () => {
const lock = app.requestSingleInstanceLock(); if (!(await initApp()) || !(await initArgs()) || !(await initLog())) {
if (!lock) { return;
console.error("TouchKio is already running");
return app.quit();
} }
let args = parseArgs(process);
let argsProvided = !!Object.keys(args).length;
let argsFilePath = path.join(app.getPath("userData"), "Arguments.json");
let argsFileExists = fs.existsSync(argsFilePath);
// Show version and release info
if ("help" in args || "version" in args) {
console.log(`${app.getName()}-v${app.getVersion()}, https://github.com/leukipp/touchkio`);
return app.quit();
}
// Setup arguments from file path
if ("setup" in args || (!argsProvided && !argsFileExists)) {
await sleep(3000);
do {
args = await promptArgs(process);
} while (!Object.keys(args).length);
writeArgs(argsFilePath, args);
} else if (!argsProvided && argsFileExists) {
args = readArgs(argsFilePath);
}
// Split url arguments
args.web_url = args.web_url || [];
if (!Array.isArray(args.web_url)) {
args.web_url = args.web_url.split(",").map((url) => url.trim());
}
global.ARGS = args;
// Show used arguments // Show used arguments
console.log(`Arguments: ${JSON.stringify(global.ARGS, null, 2)}`); const args = Object.assign({}, ARGS);
if ("mqtt_password" in args) {
args.mqtt_password = "*".repeat((args.mqtt_password || "").length);
}
console.info(`Arguments: ${JSON.stringify(args, null, 2)}`);
// Chained init functions // Chained init functions
const chained = [webview.init, hardware.init, integration.init]; const chained = [webview.init, hardware.init, integration.init];
@@ -70,6 +48,147 @@ app.whenReady().then(async () => {
} }
}); });
/**
* Initializes the global app object.
*
* @returns {bool} Returns true if the initialization was successful.
*/
const initApp = async () => {
const packageJsonPath = path.join(app.getAppPath(), "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
const buildJsonPath = path.join(app.getAppPath(), "build.json");
const buildFileExists = fs.existsSync(buildJsonPath);
// Set required app infos
APP.name = app.getName();
APP.title = packageJson.title;
APP.version = app.getVersion();
APP.path = app.getAppPath();
APP.icon = path.join(app.getAppPath(), "img", "icon.png");
APP.log = path.join(app.getPath("logs"), "main.log");
APP.config = app.getPath("userData");
// Set additional update infos
APP.homepage = `https://github.com/${packageJson.author}/${packageJson.name}`;
APP.releases = {
url: `https://api.github.com/repos/${packageJson.author}/${packageJson.name}/releases`,
latest: null,
};
APP.scripts = {
install: `https://raw.githubusercontent.com/${packageJson.author}/${packageJson.name}/main/install.sh`,
};
// Set additional build infos
APP.build = {};
if (buildFileExists) {
APP.build = JSON.parse(fs.readFileSync(buildJsonPath, "utf8"));
}
// Request single app instance lock
if (!app.requestSingleInstanceLock()) {
console.error(`${APP.title} is already running`);
return app.exit(1);
}
return true;
};
/**
* Initializes the global args object.
*
* @returns {bool} Returns true if the initialization was successful.
*/
const initArgs = async () => {
let args = parseArgs(process);
let argsProvided = !!Object.keys(args).length;
let argsFilePath = path.join(APP.config, "Arguments.json");
let argsFileExists = fs.existsSync(argsFilePath);
// Show version and release info
if ("help" in args || "version" in args) {
let build = "";
if (APP.build.id) {
build = ` (${APP.build.id}), built on ${APP.build.date} (${APP.build.platform}-${APP.build.arch}-${APP.build.maker})`;
}
console.info(`${APP.name}-v${APP.version}${build}\n${APP.homepage}`);
return app.exit(0);
}
// Setup arguments from file path
if ((!argsProvided && !argsFileExists) || "setup" in args) {
await sleep(3000);
do {
args = await promptArgs(process);
} while (!Object.keys(args).length);
writeArgs(argsFilePath, args);
} else if (argsFileExists) {
args = { ...readArgs(argsFilePath), ...args };
}
// Check arguments object
if (!Object.keys(args).length) {
console.error(`No arguments provided`);
return app.exit(1);
}
// Split url arguments
args.web_url = args.web_url || [];
if (!Array.isArray(args.web_url)) {
args.web_url = args.web_url.split(",").map((url) => url.trim());
}
ARGS = args;
return true;
};
/**
* Initializes the global log object.
*
* @returns {bool} Returns true if the initialization was successful.
*/
const initLog = async () => {
try {
if (fs.existsSync(APP.log)) {
fs.unlinkSync(APP.log);
}
} catch (error) {
console.error("Failed to delete log file:", error.message);
}
// Overwrite console log
APP.logs = [];
log.errorHandler.startCatching({
showDialog: false,
onError({ error }) {
console.error(`Uncaught Error: ${error.message} (${error.stack})`);
app.exit(1);
},
});
log.hooks.push((message, transport, type) => {
const text = message.data.join(" ");
if (!text.startsWith("(node:") && type === "console") {
APP.logs.unshift({
time: message.date,
level: message.level,
text: text,
});
if (APP.logs.length > 10) {
APP.logs.splice(10);
}
EVENTS.emit("consoleLog", APP.logs[0]);
}
return message;
});
log.transports.file.resolvePathFn = () => {
return APP.log;
};
Object.assign(console, log.functions);
return true;
};
/** /**
* Parses command-line arguments from the given process object. * Parses command-line arguments from the given process object.
* *
@@ -155,34 +274,41 @@ const promptArgs = async (proc) => {
// Prompt questions and wait for the answers // Prompt questions and wait for the answers
let args = {}; let args = {};
let ignore = []; let ignore = [];
for (const { key, question, fallback } of prompts) { try {
if (key === "mqtt") { for (const { key, question, fallback } of prompts) {
const prompt = `${question} (${fallback}): `; if (key === "mqtt") {
const answer = await read.question(prompt); const prompt = `${question} (${fallback}): `;
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase(); const answer = await read.question(prompt);
if (!["y", "yes"].includes(value)) { const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
ignore = ignore.concat(["mqtt_url", "mqtt_user", "mqtt_password", "mqtt_discovery"]); if (!["y", "yes"].includes(value)) {
} ignore = ignore.concat(["mqtt_url", "mqtt_user", "mqtt_password", "mqtt_discovery"]);
} else if (key === "check") { }
const json = JSON.stringify(args, null, 2); } else if (key === "check") {
const prompt = `${question}\n${json}\n(${fallback}): `; const json = JSON.stringify(args, null, 2);
const answer = await read.question(prompt); const prompt = `${question}\n${json}\n(${fallback}): `;
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase(); const answer = await read.question(prompt);
if (!["y", "yes"].includes(value)) { const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
args = {}; if (!["y", "yes"].includes(value)) {
} args = {};
} else if (!ignore.includes(key)) { }
const prompt = `${question} (${fallback}): `; } else if (!ignore.includes(key)) {
const answer = await read.question(prompt); const prompt = `${question} (${fallback}): `;
const value = answer.trim() || fallback; const answer = await read.question(prompt);
if (key === "web_url") { const value = answer.trim() || fallback;
args[key] = value.split(",").map((v) => v.trim()); if (key === "web_url") {
} else { args[key] = value.split(",").map((v) => v.trim());
args[key] = value; } else {
args[key] = value;
}
} }
} }
} catch (error) {
console.error(`\n${error.message}`);
args = {};
app.exit(1);
} finally {
read.close();
} }
read.close();
return args; return args;
}; };
@@ -194,11 +320,15 @@ const promptArgs = async (proc) => {
* @param {Object} args - The arguments object. * @param {Object} args - The arguments object.
*/ */
const writeArgs = (path, args) => { const writeArgs = (path, args) => {
const argc = Object.assign({}, args); try {
if ("mqtt_password" in argc) { const argc = Object.assign({}, args);
argc.mqtt_password = encrypt(argc.mqtt_password); if ("mqtt_password" in argc) {
argc.mqtt_password = encrypt(argc.mqtt_password);
}
fs.writeFileSync(path, JSON.stringify(argc, null, 2));
} catch (error) {
console.error(`Failed to write ${path}:`, error.message);
} }
fs.writeFileSync(path, JSON.stringify(argc, null, 2));
}; };
/** /**
@@ -208,11 +338,16 @@ const writeArgs = (path, args) => {
* @returns {Object} The arguments object. * @returns {Object} The arguments object.
*/ */
const readArgs = (path) => { const readArgs = (path) => {
const args = JSON.parse(fs.readFileSync(path)); try {
if ("mqtt_password" in args) { const args = JSON.parse(fs.readFileSync(path, "utf8"));
args.mqtt_password = decrypt(args.mqtt_password); if ("mqtt_password" in args) {
args.mqtt_password = decrypt(args.mqtt_password);
}
return args;
} catch (error) {
console.error(`Failed to parse ${path}:`, error.message);
} }
return args; return {};
}; };
/** /**
@@ -223,7 +358,7 @@ const readArgs = (path) => {
*/ */
const encrypt = (value) => { const encrypt = (value) => {
const iv = crypto.randomBytes(16); const iv = crypto.randomBytes(16);
const key = crypto.scryptSync(hardware.getMachineId(), app.getName(), 32); const key = crypto.scryptSync(hardware.getMachineId(), APP.name, 32);
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv); const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
let encrypted = cipher.update(value, "utf8", "hex"); let encrypted = cipher.update(value, "utf8", "hex");
encrypted += cipher.final("hex"); encrypted += cipher.final("hex");
@@ -239,7 +374,7 @@ const encrypt = (value) => {
const decrypt = (value) => { const decrypt = (value) => {
const p = Buffer.from(value, "base64").toString("utf8").split(":"); const p = Buffer.from(value, "base64").toString("utf8").split(":");
const iv = Buffer.from(p.shift(), "hex"); const iv = Buffer.from(p.shift(), "hex");
const key = crypto.scryptSync(hardware.getMachineId(), app.getName(), 32); const key = crypto.scryptSync(hardware.getMachineId(), APP.name, 32);
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
const buffer = Buffer.from(p.join(":"), "hex"); const buffer = Buffer.from(p.join(":"), "hex");
let decrypted = decipher.update(buffer, "binary", "utf8"); let decrypted = decipher.update(buffer, "binary", "utf8");
+66 -52
View File
@@ -1,109 +1,123 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Read arguments
ARG_EARLY=false
ARG_UPDATE=false
for arg in "$@"; do
case "$arg" in
early) ARG_EARLY=true ;;
update) ARG_UPDATE=true ;;
esac
done
# Determine system architecture # Determine system architecture
echo -e "Determining system architecture..." echo -e "Determining system architecture..."
BITS=$(getconf LONG_BIT) BITS=$(getconf LONG_BIT)
case "$(uname -m)" in case "$(uname -m)" in
aarch64) x86_64) ARCH="x64" ;;
ARCH="arm64" aarch64) ARCH="arm64" ;;
;; *) { echo "Architecture $(uname -m) running $BITS-bit operating system is not supported."; exit 1; } ;;
x86_64)
ARCH="x64"
;;
*)
echo "Architecture $(uname -m) running $BITS-bit operating system is not supported."
exit 1
;;
esac esac
if [ "$BITS" -ne 64 ]; then [ "$BITS" -eq 64 ] || { echo "Architecture $ARCH running $BITS-bit operating system is not supported."; exit 1; }
echo "Architecture $ARCH running $BITS-bit operating system is not supported."
exit 1
fi
echo "Architecture $ARCH running $BITS-bit operating system is supported." echo "Architecture $ARCH running $BITS-bit operating system is supported."
# Download the latest .deb package # Download the latest .deb package
echo -e "\nDownloading the latest release..." echo -e "\nDownloading the latest release..."
DEB_URL=$(wget -qO- https://api.github.com/repos/leukipp/touchkio/releases/latest | \
grep -o "\"browser_download_url\": \"[^\"]*_${ARCH}\.deb\"" | \
sed 's/"browser_download_url": "//;s/"//g')
TMP_DIR=$(mktemp -d) TMP_DIR=$(mktemp -d)
DEB_PATH="${TMP_DIR}/$(basename "$DEB_URL")"
chmod 755 "$TMP_DIR" chmod 755 "$TMP_DIR"
if [ -z "$DEB_URL" ]; then JSON=$(wget -qO- "https://api.github.com/repos/leukipp/touchkio/releases" | tr -d '\r\n')
echo "Download url for .deb file not found." if $ARG_EARLY; then
exit 1 DEB_REG='"prerelease":\s*(true|false).*?"browser_download_url":\s*"\K[^\"]*_'$ARCH'\.deb'
else
DEB_REG='"prerelease":\s*false.*?"browser_download_url":\s*"\K[^\"]*_'$ARCH'\.deb'
fi fi
if ! wget --show-progress -q -O "$DEB_PATH" "$DEB_URL"; then DEB_URL=$(echo "$JSON" | grep -oP "$DEB_REG" | head -n 1)
echo "Failed to download the .deb file." DEB_PATH="${TMP_DIR}/$(basename "$DEB_URL")"
exit 1
fi [ -z "$DEB_URL" ] && { echo "Download url for .deb file not found."; exit 1; }
wget --show-progress -q -O "$DEB_PATH" "$DEB_URL" || { echo "Failed to download the .deb file."; exit 1; }
# Install the latest .deb package # Install the latest .deb package
echo -e "\nInstalling the latest release..." echo -e "\nInstalling the latest release..."
if ! command -v apt &> /dev/null; then command -v apt &> /dev/null || { echo "Package manager apt was not found."; exit 1; }
echo "Package manager apt was not found." sudo apt install -y "$DEB_PATH" || { echo "Installation of .deb file failed."; exit 1; }
exit 1
fi
if ! sudo apt install "$DEB_PATH"; then
echo "Installation of .deb file failed."
exit 1
fi
# Create the systemd user service # Create the systemd user service
echo -e "\nCreating systemd user service..." echo -e "\nCreating systemd user service..."
SERVICE_FILE="$HOME/.config/systemd/user/touchkio.service" SERVICE_NAME="touchkio.service"
mkdir -p "$(dirname "$SERVICE_FILE")" || { echo "Failed to create directory for $SERVICE_FILE"; exit 1; } SERVICE_FILE="$HOME/.config/systemd/user/$SERVICE_NAME"
mkdir -p "$(dirname "$SERVICE_FILE")" || { echo "Failed to create directory for $SERVICE_FILE."; exit 1; }
bash -c "cat << EOF > \"$SERVICE_FILE\" SERVICE_CONTENT="[Unit]
[Unit]
Description=TouchKio Description=TouchKio
After=graphical.target After=graphical.target
Wants=network-online.target
[Service] [Service]
ExecStart=/usr/bin/touchkio ExecStart=/usr/bin/touchkio
Restart=on-failure Restart=on-failure
RestartSec=5s RestartSec=5s
[Install] [Install]
WantedBy=default.target WantedBy=default.target"
EOF"
systemctl --user enable "$(basename "$SERVICE_FILE")" || { echo "Failed to enable service $SERVICE_FILE"; exit 1; } if $ARG_UPDATE; then
echo "Service $SERVICE_FILE enabled" if systemctl --user --quiet is-active "${SERVICE_NAME}"; then
systemctl --user restart "${SERVICE_NAME}"
echo "Existing $SERVICE_NAME restarted."
else
echo "Existing $SERVICE_NAME not running, start touchkio manually."
fi
exit 0
fi
SERVICE_CREATE=true
if [ -f "$SERVICE_FILE" ]; then
read -p "Service $SERVICE_FILE exists, overwrite? (y/N) " overwrite
[[ ${overwrite:-n} == [Yy]* ]] || SERVICE_CREATE=false
fi
if $SERVICE_CREATE; then
echo "$SERVICE_CONTENT" > "$SERVICE_FILE" || { echo "Failed to write to $SERVICE_FILE."; exit 1; }
systemctl --user enable "$(basename "$SERVICE_FILE")" || { echo "Failed to enable service $SERVICE_FILE."; exit 1; }
echo "Service $SERVICE_FILE enabled."
else
echo "Service $SERVICE_FILE not created."
fi
# Export display variables # Export display variables
echo -e "\nExporting display variables..." echo -e "\nExporting display variables..."
if [ -z "$DISPLAY" ]; then if [ -z "$DISPLAY" ]; then
export DISPLAY=":0" export DISPLAY=":0"
echo "DISPLAY was not set, defaulting to \"$DISPLAY\"" echo "DISPLAY was not set, defaulting to \"$DISPLAY\"."
else else
echo "DISPLAY is set to \"$DISPLAY\"" echo "DISPLAY is set to \"$DISPLAY\"."
fi fi
if [ -z "$WAYLAND_DISPLAY" ]; then if [ -z "$WAYLAND_DISPLAY" ]; then
export WAYLAND_DISPLAY="wayland-0" export WAYLAND_DISPLAY="wayland-0"
echo "WAYLAND_DISPLAY was not set, defaulting to \"$WAYLAND_DISPLAY\"" echo "WAYLAND_DISPLAY was not set, defaulting to \"$WAYLAND_DISPLAY\"."
else else
echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\"" echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\"."
fi fi
# Start the setup mode # Start the setup mode
echo "" read -p $'\nStart touchkio setup? (Y/n) ' setup
read -p "Start touchkio setup? (Y/n) " answer
if [[ ${answer:-y} == [Yy]* ]]; then if [[ ${setup:-y} == [Yy]* ]]; then
echo "/usr/bin/touchkio --setup" echo "/usr/bin/touchkio --setup"
/usr/bin/touchkio --setup /usr/bin/touchkio --setup
else else
echo "/usr/bin/touchkio" echo "/usr/bin/touchkio"
/usr/bin/touchkio /usr/bin/touchkio
fi fi
exit 0
+444 -156
View File
@@ -6,9 +6,8 @@ const cpr = require("child_process");
global.HARDWARE = global.HARDWARE || { global.HARDWARE = global.HARDWARE || {
initialized: false, initialized: false,
status: null,
session: {},
support: {}, support: {},
session: {},
battery: { battery: {
level: { level: {
path: null, path: null,
@@ -16,19 +15,21 @@ global.HARDWARE = global.HARDWARE || {
}, },
display: { display: {
status: { status: {
path: null,
value: null, value: null,
path: null,
command: null,
}, },
brightness: { brightness: {
path: null,
value: null, value: null,
path: null,
max: null, max: null,
}, },
notifiers: [],
}, },
keyboard: { keyboard: {
visible: null, visible: null,
notifiers: [], },
audio: {
device: null,
}, },
}; };
@@ -38,68 +39,106 @@ global.HARDWARE = global.HARDWARE || {
* @returns {bool} Returns true if the initialization was successful. * @returns {bool} Returns true if the initialization was successful.
*/ */
const init = async () => { const init = async () => {
if (!compatibleDevice()) { if (!compatibleSystem()) {
console.warn("Device not supported"); console.error("Operating system is not supported");
return false; return false;
} }
// Init globals // Init globals
HARDWARE.session.user = sessionUser(); HARDWARE.session.user = sessionUser();
HARDWARE.session.type = sessionType(); HARDWARE.session.type = sessionType();
HARDWARE.session.desktop = sessionDesktop();
HARDWARE.battery.level.path = getBatteryLevelPath(); HARDWARE.battery.level.path = getBatteryLevelPath();
HARDWARE.support.batteryLevel = HARDWARE.battery.level.path !== null;
HARDWARE.display.status.path = getDisplayStatusPath(); HARDWARE.display.status.path = getDisplayStatusPath();
HARDWARE.support.displayStatus = HARDWARE.display.status.path !== null; HARDWARE.display.status.command = getDisplayStatusCommand();
HARDWARE.display.brightness.path = getDisplayBrightnessPath(); HARDWARE.display.brightness.path = getDisplayBrightnessPath();
HARDWARE.support.displayBrightness = HARDWARE.display.brightness.path !== null;
HARDWARE.display.brightness.max = getDisplayBrightnessMax(); HARDWARE.display.brightness.max = getDisplayBrightnessMax();
HARDWARE.support.keyboardVisibility = processRuns("squeekboard"); HARDWARE.audio.device = getAudioDevice();
HARDWARE.support = checkSupport();
HARDWARE.initialized = true; HARDWARE.initialized = true;
HARDWARE.status = "Supported";
// Show supported features // Show supported features
console.log(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`); process.stdout.write("\n");
console.info(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`);
// Show session infos // Show session infos
console.log("\nUser:", HARDWARE.session.user); process.stdout.write("\n");
console.log("Session:", HARDWARE.session.type); console.info("User:", HARDWARE.session.user);
console.info("Session:", HARDWARE.session.type);
console.info("Desktop:", HARDWARE.session.desktop);
// Show device infos // Show device infos
console.log("\nModel:", getModel()); process.stdout.write("\n");
console.log("Vendor:", getVendor()); console.info("Model:", getModel());
console.log("Serial Number:", getSerialNumber()); console.info("Vendor:", getVendor());
console.log("Host Name:", getHostName()); console.info("Serial Number:", getSerialNumber());
console.info("Network Addresses:", getNetworkAddresses());
console.info("Host Name:", getHostName());
// Show system infos // Show system infos
console.log("\nUp Time:", getUpTime()); process.stdout.write("\n");
console.log("Memory Size:", getMemorySize()); console.info("Up Time:", getUpTime());
console.log("Memory Usage:", getMemoryUsage()); console.info("Memory Size:", getMemorySize());
console.log("Processor Usage:", getProcessorUsage()); console.info("Memory Usage:", getMemoryUsage());
console.log("Processor Temperature:", getProcessorTemperature()); console.info("Processor Usage:", getProcessorUsage());
console.info("Processor Temperature:", getProcessorTemperature());
// Show hardware infos // Show hardware infos
console.log(`\nBattery Level [${HARDWARE.battery.level.path}]:`, getBatteryLevel()); process.stdout.write("\n");
console.log(`Display Status [${HARDWARE.display.status.path}]:`, getDisplayStatus()); const unsupported = "unsupported";
console.log(`Display Brightness [${HARDWARE.display.brightness.path}]:`, getDisplayBrightness(), "\n"); console.info(`Battery Level [${HARDWARE.battery.level.path || unsupported}]:`, `${getBatteryLevel() || unsupported}`);
console.info(
`Display Status [${HARDWARE.display.status.path || unsupported}]:`,
`${getDisplayStatus() || unsupported} (${HARDWARE.display.status.command || unsupported})`,
);
console.info(
`Display Brightness [${HARDWARE.display.brightness.path || unsupported}]:`,
`${getDisplayBrightness() || unsupported}`,
);
console.info(
`Audio Volume [${HARDWARE.support.audioVolume ? HARDWARE.audio.device : unsupported}]:`,
`${getAudioVolume() || unsupported}`,
);
console.info(
`Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "squeekboard" : unsupported}]:`,
`${getKeyboardVisibility() || unsupported}`,
);
process.stdout.write("\n");
// Check for keyboard visibility // Monitor audio volume
setKeyboardVisibility("OFF", (reply, error) => { if (HARDWARE.support.audioVolume) {
if (!reply || error) { commandMonitor("pactl", ["subscribe"], (reply, error) => {
return; if (!reply || error) {
}
dbusMonitor("/sm/puri/OSK0", (property, error) => {
if (!property || error) {
return; return;
} }
HARDWARE.keyboard.visibility = property.Visible === "true"; if (reply.includes("'change' on sink")) {
HARDWARE.keyboard.notifiers.forEach((notifier) => { console.info("Update Audio Volume:", getAudioVolume());
notifier(); EVENTS.emit("updateVolume");
}
});
}
// Monitor keyboard visibility
if (HARDWARE.support.keyboardVisibility) {
setKeyboardVisibility("OFF", (reply, error) => {
if (!reply || error) {
return;
}
dbusMonitor("/sm/puri/OSK0", (property, error) => {
if (!property || error) {
return;
}
HARDWARE.keyboard.visibility = property.Visible === "true";
console.info("Update Keyboard Visibility:", getKeyboardVisibility());
EVENTS.emit("updateKeyboard");
}); });
}); });
}); }
// Check for display changes // Check for display changes
interval(update, 1000); setDisplayStatus("ON", () => {
interval(update, 1000);
});
return true; return true;
}; };
@@ -108,7 +147,7 @@ const init = async () => {
* Updates the shared hardware properties. * Updates the shared hardware properties.
*/ */
const update = async () => { const update = async () => {
if (!HARDWARE.initialized) { if (!HARDWARE.initialized || APP.exiting) {
return; return;
} }
@@ -116,11 +155,9 @@ const update = async () => {
if (HARDWARE.support.displayStatus) { if (HARDWARE.support.displayStatus) {
const status = (await fsp.readFile(`${HARDWARE.display.status.path}/dpms`, "utf8")).trim(); const status = (await fsp.readFile(`${HARDWARE.display.status.path}/dpms`, "utf8")).trim();
if (status !== HARDWARE.display.status.value) { if (status !== HARDWARE.display.status.value) {
console.log("Update Display Status:", getDisplayStatus()); console.info("Update Display Status:", getDisplayStatus());
HARDWARE.display.status.value = status; HARDWARE.display.status.value = status;
HARDWARE.display.notifiers.forEach((notifier) => { EVENTS.emit("updateDisplay");
notifier();
});
} }
} }
@@ -128,25 +165,23 @@ const update = async () => {
if (HARDWARE.support.displayBrightness) { if (HARDWARE.support.displayBrightness) {
const brightness = (await fsp.readFile(`${HARDWARE.display.brightness.path}/brightness`, "utf8")).trim(); const brightness = (await fsp.readFile(`${HARDWARE.display.brightness.path}/brightness`, "utf8")).trim();
if (brightness !== HARDWARE.display.brightness.value) { if (brightness !== HARDWARE.display.brightness.value) {
console.log("Update Display Brightness:", getDisplayBrightness()); console.info("Update Display Brightness:", getDisplayBrightness());
HARDWARE.display.brightness.value = brightness; HARDWARE.display.brightness.value = brightness;
HARDWARE.display.notifiers.forEach((notifier) => { EVENTS.emit("updateDisplay");
notifier();
});
} }
} }
}; };
/** /**
* Verifies device compatibility by checking the presence of necessary sys paths. * Verifies system compatibility by checking the presence of necessary sys paths.
* *
* @returns {bool} Returns true if all paths exists. * @returns {bool} Returns true if all paths exists.
*/ */
const compatibleDevice = () => { const compatibleSystem = () => {
if (os.platform() !== "linux") { if (os.platform() !== "linux") {
return false; return false;
} }
const paths = ["/sys/class/drm", "/sys/class/backlight", "/sys/class/thermal"]; const paths = ["/sys/class/drm", "/sys/class/backlight", "/sys/class/power_supply", "/sys/class/thermal"];
return paths.every((path) => fs.existsSync(path)); return paths.every((path) => fs.existsSync(path));
}; };
@@ -179,12 +214,39 @@ const sessionType = () => {
}; };
/** /**
* Checks the session type for the logged in user. * Gets the desktop environment name by checking environment variables.
* *
* @returns {bool} Returns true if the session matches the type. * @returns {string} Returns desktop environment name or 'unknown' if not detected.
*/ */
const session = (type) => { const sessionDesktop = () => {
return HARDWARE.session.type === type; const envs = ["XDG_CURRENT_DESKTOP", "XDG_DESKTOP_SESSION", "DESKTOP_SESSION"];
const names = envs.map((env) => process.env[env]).filter(Boolean);
return (names.join(":") || "unknown").toLowerCase();
};
/**
* Checks supported features based on hardware and software.
*
* @returns {Object} Returns support object with boolean values.
*/
const checkSupport = () => {
const battery = HARDWARE.battery.level;
const status = HARDWARE.display.status;
const brightness = HARDWARE.display.brightness;
const audio = HARDWARE.audio.device;
const keyboard = processRuns("squeekboard");
const service = serviceRuns(APP.name);
const sudo = sudoRights();
const deb = APP.build.maker === "deb";
return {
batteryLevel: !!battery.path,
displayStatus: !!status.path && !!status.command,
displayBrightness: sudo && !!brightness.path && !!brightness.max,
keyboardVisibility: keyboard,
audioVolume: !!audio,
sudoRights: sudo,
appUpdate: service && sudo && deb,
};
}; };
/** /**
@@ -196,7 +258,7 @@ const getModel = () => {
const paths = ["/sys/firmware/devicetree/base/model", "/sys/class/dmi/id/product_name"]; const paths = ["/sys/firmware/devicetree/base/model", "/sys/class/dmi/id/product_name"];
for (const path of paths) { for (const path of paths) {
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
return execSyncCommand("sudo", ["cat", path]) || "Generic"; return execSyncCommand("cat", [path]) || "Generic";
} }
} }
return "Generic"; return "Generic";
@@ -211,7 +273,7 @@ const getVendor = () => {
const paths = ["/sys/class/dmi/id/board_vendor"]; const paths = ["/sys/class/dmi/id/board_vendor"];
for (const path of paths) { for (const path of paths) {
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
return execSyncCommand("sudo", ["cat", path]) || "Generic"; return execSyncCommand("cat", [path]) || "Generic";
} }
} }
const model = getModel(); const model = getModel();
@@ -222,22 +284,22 @@ const getVendor = () => {
}; };
/** /**
* Gets the serial number using `/sys/firmware/devicetree/base/serial-number` or `/sys/class/dmi/id/product_serial`. * Gets the serial number using `/sys/firmware/devicetree/base/serial-number`.
* *
* @returns {string} The serial number of the device or '123456' if not found. * @returns {string} The serial number or machine id parts of the device or '123456' if not found.
*/ */
const getSerialNumber = () => { const getSerialNumber = () => {
const paths = ["/sys/firmware/devicetree/base/serial-number", "/sys/class/dmi/id/product_serial"]; const paths = ["/sys/firmware/devicetree/base/serial-number"];
for (const path of paths) { for (const path of paths) {
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
return execSyncCommand("sudo", ["cat", path]) || "123456"; return execSyncCommand("cat", [path]) || "123456";
} }
} }
return "123456"; return getMachineId().slice(-6);
}; };
/** /**
* Gets the host machine id using `/etc/machine-id`. * Gets the machine id using `/etc/machine-id`.
* *
* @returns {string} The machine id of the system or '123456' if not found. * @returns {string} The machine id of the system or '123456' if not found.
*/ */
@@ -245,12 +307,38 @@ const getMachineId = () => {
const paths = ["/etc/machine-id"]; const paths = ["/etc/machine-id"];
for (const path of paths) { for (const path of paths) {
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
return execSyncCommand("sudo", ["cat", path]) || "123456"; return execSyncCommand("cat", [path]) || "123456";
} }
} }
return "123456"; return "123456";
}; };
/**
* Gets the network interfaces addresses using `os.networkInterfaces()`.
*
* @returns {Object} The network addresses of all interfaces.
*/
const getNetworkAddresses = () => {
const addresses = {};
for (const [key, interfaces] of Object.entries(os.networkInterfaces())) {
for (const interface of interfaces) {
if (interface.internal || !interface.family || !interface.address) {
continue;
}
const name = key.charAt(0).toUpperCase() + key.slice(1);
if (!addresses[name]) {
addresses[name] = {};
}
const family = interface.family;
if (!addresses[name][family]) {
addresses[name][family] = [];
}
addresses[name][family].push(interface.address);
}
}
return addresses;
};
/** /**
* Gets the host name of the current system using `os.hostname()`. * Gets the host name of the current system using `os.hostname()`.
* *
@@ -310,7 +398,7 @@ const getProcessorTemperature = () => {
continue; continue;
} }
const type = fs.readFileSync(typeFile, "utf8").trim(); const type = fs.readFileSync(typeFile, "utf8").trim();
if (["cpu-thermal", "x86_pkg_temp", "k10temp", "acpitz"].includes(type)) { if (["cpu-thermal", "x86_pkg_temp", "k10temp", "acpitz", "cpu"].includes(type)) {
return parseFloat(fs.readFileSync(tempFile, "utf8").trim()) / 1000; return parseFloat(fs.readFileSync(tempFile, "utf8").trim()) / 1000;
} }
} }
@@ -371,7 +459,31 @@ const getDisplayStatusPath = () => {
}; };
/** /**
* Gets the current display power status using `wlopm` or `xset`. * Gets the available display status command checking for `wlopm`, `kscreen-doctor` and `xset`.
*
* @returns {string|null} The display status command or null if nothing was found.
*/
const getDisplayStatusCommand = () => {
const type = HARDWARE.session.type;
const desktop = HARDWARE.session.desktop;
const mapping = {
wayland: [
{ command: "wlopm", desktops: ["labwc", "wayfire", "unknown"] },
{ command: "kscreen-doctor", desktops: ["kde", "plasma", "unknown"] },
],
x11: [{ command: "xset", desktops: ["*"] }],
}[type];
for (const map of mapping || []) {
if (commandExists(map.command) && map.desktops.some((d) => d === "*" || desktop.includes(d))) {
return map.command;
}
}
console.warn("No display command available");
return null;
};
/**
* Gets the current display power status using the available command.
* *
* @returns {string|null} The display status as 'ON'/'OFF' or null if an error occurs. * @returns {string|null} The display status as 'ON'/'OFF' or null if an error occurs.
*/ */
@@ -379,24 +491,34 @@ const getDisplayStatus = () => {
if (!HARDWARE.support.displayStatus) { if (!HARDWARE.support.displayStatus) {
return null; return null;
} }
if (session("wayland") && commandExists("wlopm")) { switch (HARDWARE.display.status.command) {
const status = execSyncCommand("wlopm", []); case "wlopm":
if (status !== null) { const wlopm = execSyncCommand("wlopm", []);
const out = status.split("\n")[0].split(" "); if (wlopm !== null) {
return out.pop().toUpperCase(); const output = wlopm.split("\n")[0].split(" ");
} return output.pop().toUpperCase();
} else if (session("x11") && commandExists("xset")) { }
const status = execSyncCommand("xset", ["-q"]); break;
if (status !== null) { case "kscreen-doctor":
const on = /Monitor is On/.test(status); const kdoc = execSyncCommand("kscreen-doctor", ["--dpms", "show"]);
return on ? "ON" : "OFF"; if (kdoc !== null) {
} const output = kdoc.split("\n")[0].split(" ");
return output.pop().toUpperCase();
}
break;
case "xset":
const xset = execSyncCommand("xset", ["-q"]);
if (xset !== null) {
const output = xset.includes("Monitor is On");
return output ? "ON" : "OFF";
}
break;
} }
return null; return null;
}; };
/** /**
* Sets the display power status using `wlopm` or `xset`. * Sets the display power status using the available command.
* *
* This function takes a desired status ('ON' or 'OFF') and executes * This function takes a desired status ('ON' or 'OFF') and executes
* the appropriate command to set the display status. * the appropriate command to set the display status.
@@ -414,10 +536,16 @@ const setDisplayStatus = (status, callback = null) => {
if (typeof callback === "function") callback(null, "Invalid status"); if (typeof callback === "function") callback(null, "Invalid status");
return; return;
} }
if (session("wayland") && commandExists("wlopm")) { switch (HARDWARE.display.status.command) {
execAsyncCommand("wlopm", [`--${status.toLowerCase()}`, "*"], callback); case "wlopm":
} else if (session("x11") && commandExists("xset")) { execAsyncCommand("wlopm", [`--${status.toLowerCase()}`, "*"], callback);
execAsyncCommand("xset", ["dpms", "force", status.toLowerCase()], callback); break;
case "kscreen-doctor":
execAsyncCommand("kscreen-doctor", ["--dpms", status.toLowerCase()], callback);
break;
case "xset":
execAsyncCommand("xset", ["dpms", "force", status.toLowerCase()], callback);
break;
} }
}; };
@@ -444,7 +572,7 @@ const getDisplayBrightnessPath = () => {
* @returns {number|null} The brightness maximum value or null if an error occurs. * @returns {number|null} The brightness maximum value or null if an error occurs.
*/ */
const getDisplayBrightnessMax = () => { const getDisplayBrightnessMax = () => {
if (!HARDWARE.support.displayBrightness) { if (!HARDWARE.display.brightness.path) {
return null; return null;
} }
const max = fs.readFileSync(`${HARDWARE.display.brightness.path}/max_brightness`, "utf8").trim(); const max = fs.readFileSync(`${HARDWARE.display.brightness.path}/max_brightness`, "utf8").trim();
@@ -490,13 +618,72 @@ const setDisplayBrightness = (brightness, callback = null) => {
if (typeof callback === "function") callback(null, "Invalid brightness"); if (typeof callback === "function") callback(null, "Invalid brightness");
return; return;
} }
const max = HARDWARE.display.brightness.max; const max = HARDWARE.display.brightness.max || 1;
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max)); const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback); const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback);
proc.stdin.write(value.toString()); proc.stdin.write(value.toString());
proc.stdin.end(); proc.stdin.end();
}; };
/**
* Gets the default audio device using `pactl`.
*
* @returns {string|null} The default audio device or null if an error occurs.
*/
const getAudioDevice = () => {
if (!commandExists("pactl")) {
return null;
}
const output = execSyncCommand("pactl", ["get-default-sink"]);
if (!output) {
return null;
}
return !output.includes("auto_null") ? output.trim() : null;
};
/**
* Gets the default audio device volume using `pactl`.
*
* @returns {number|null} The default audio device volume as a percentage or null if an error occurs.
*/
const getAudioVolume = () => {
if (!HARDWARE.support.audioVolume) {
return null;
}
const mute = execSyncCommand("pactl", ["get-sink-mute", "@DEFAULT_SINK@"]);
const volume = execSyncCommand("pactl", ["get-sink-volume", "@DEFAULT_SINK@"]);
if (!mute || !volume) {
return null;
}
const match = volume.match(/\/(\s*(\d+)%)/);
if (match) {
return Math.max(0, Math.min(100, mute.includes("yes") ? 0 : parseInt(match[2])));
}
return null;
};
/**
* Sets the default audio device volume using `pactl`.
*
* This function takes a volume value between 0 to 100 percent and sends it to the device.
*
* @param {number} volume - The desired volume level (0-100).
* @param {Function} callback - A callback function that receives the output or error.
*/
const setAudioVolume = (volume, callback = null) => {
if (!HARDWARE.support.audioVolume) {
if (typeof callback === "function") callback(null, "Not supported");
return;
}
if (typeof volume !== "number" || volume < 0 || volume > 100) {
console.error("Volume must be a number between 0 and 100");
if (typeof callback === "function") callback(null, "Invalid volume");
return;
}
execAsyncCommand("pactl", ["set-sink-mute", "@DEFAULT_SINK@", volume === 0 ? "1" : "0"]);
execAsyncCommand("pactl", ["set-sink-volume", "@DEFAULT_SINK@", `${volume}%`], callback);
};
/** /**
* Gets the keyboard visibility using global properties. * Gets the keyboard visibility using global properties.
* *
@@ -557,6 +744,10 @@ const checkPackageUpgrades = () => {
* @param {Function} callback - A callback function that receives the output or error. * @param {Function} callback - A callback function that receives the output or error.
*/ */
const shutdownSystem = (callback = null) => { const shutdownSystem = (callback = null) => {
if (!HARDWARE.support.sudoRights) {
if (typeof callback === "function") callback(null, "Not supported");
return;
}
execAsyncCommand("sudo", ["shutdown", "-h", "now"], callback); execAsyncCommand("sudo", ["shutdown", "-h", "now"], callback);
}; };
@@ -569,9 +760,40 @@ const shutdownSystem = (callback = null) => {
* @param {Function} callback - A callback function that receives the output or error. * @param {Function} callback - A callback function that receives the output or error.
*/ */
const rebootSystem = (callback = null) => { const rebootSystem = (callback = null) => {
if (!HARDWARE.support.sudoRights) {
if (typeof callback === "function") callback(null, "Not supported");
return;
}
execAsyncCommand("sudo", ["reboot"], callback); execAsyncCommand("sudo", ["reboot"], callback);
}; };
/**
* Checks if sudo commands can run without a password.
*
* @returns {bool} Returns true if password-less sudo rights exists.
*/
const sudoRights = () => {
try {
cpr.execSync(`sudo -n true`, { encoding: "utf8", stdio: "ignore" });
return true;
} catch {}
return false;
};
/**
* Checks if a service is running using `systemctl`.
*
* @param {string} name - The service name to check.
* @returns {bool} Returns true if the service runs.
*/
const serviceRuns = (name) => {
try {
cpr.execSync(`systemctl --user is-active ${name}`, { encoding: "utf8", stdio: "ignore" });
return true;
} catch {}
return false;
};
/** /**
* Checks if a process is running using `pidof`. * Checks if a process is running using `pidof`.
* *
@@ -580,7 +802,8 @@ const rebootSystem = (callback = null) => {
*/ */
const processRuns = (name) => { const processRuns = (name) => {
try { try {
return !!cpr.execSync(`pidof ${name}`, { encoding: "utf8" }); cpr.execSync(`pidof ${name}`, { encoding: "utf8", stdio: "ignore" });
return true;
} catch {} } catch {}
return false; return false;
}; };
@@ -593,7 +816,8 @@ const processRuns = (name) => {
*/ */
const commandExists = (name) => { const commandExists = (name) => {
try { try {
return !!cpr.execSync(`which ${name}`, { encoding: "utf8" }); cpr.execSync(`which ${name}`, { encoding: "utf8", stdio: "ignore" });
return true;
} catch {} } catch {}
return false; return false;
}; };
@@ -605,12 +829,12 @@ const commandExists = (name) => {
* @param {Array<string>} args - The arguments for the command. * @param {Array<string>} args - The arguments for the command.
* @returns {string|null} The output of the command or null if an error occurs. * @returns {string|null} The output of the command or null if an error occurs.
*/ */
const execSyncCommand = (cmd, args = []) => { const execSyncCommand = (cmd, args) => {
try { try {
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" }); const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
return output.trim().replace(/\0/g, ""); return output.trim().replace(/\0/g, "");
} catch (error) { } catch (error) {
console.error("Execute Sync:", error.message); console.error(`Execute Sync: '${[cmd, ...args].join(" ")}' --> ${error.message}`);
} }
return null; return null;
}; };
@@ -623,7 +847,7 @@ const execSyncCommand = (cmd, args = []) => {
* @param {Function} callback - A callback function that receives the output or error. * @param {Function} callback - A callback function that receives the output or error.
* @returns {Object} The spawned process object. * @returns {Object} The spawned process object.
*/ */
const execAsyncCommand = (cmd, args = [], callback = null) => { const execAsyncCommand = (cmd, args, callback = null) => {
let errorOutput = ""; let errorOutput = "";
let successOutput = ""; let successOutput = "";
let proc = cpr.spawn(cmd, args); let proc = cpr.spawn(cmd, args);
@@ -638,19 +862,131 @@ const execAsyncCommand = (cmd, args = [], callback = null) => {
} }
}); });
proc.on("close", (code) => { proc.on("close", (code) => {
const error = errorOutput.trim().replace(/\0/g, "");
const reply = successOutput.trim().replace(/\0/g, "");
if (code !== 0 || error) {
console.error(`Execute Async: '${[cmd, ...args].join(" ")}' --> ${error} (${code})`);
if (typeof callback === "function") callback(null, error);
} else {
if (typeof callback === "function") callback(reply, null);
}
});
return proc;
};
/**
* Executes a script command asynchronously.
*
* @param {string} cmd - The script to execute.
* @param {Array<string>} args - The arguments for the command.
* @param {Function} callback - A callback function that receives the progress or error.
* @returns {Object} The spawned process object.
*/
const execScriptCommand = (cmd, args, callback = null) => {
let progress = 1;
let proc = cpr.spawn(cmd, args);
if (typeof callback === "function") callback(progress, null);
proc.stdout.on("data", (data) => {
if (data) {
const output = data.toString().trim();
const lines = output.replace(/\n+/g, "\n").replace(/^\n+|\n+$|\0/g, "");
for (const line of lines.split("\n").filter(Boolean)) {
console.info(line);
}
}
});
proc.stderr.on("data", (data) => {
if (data) {
const output = data.toString().trim();
const lines = output.replace(/\n+/g, "\n").replace(/^\n+|\n+$|\0/g, "");
for (const line of lines.split("\n").filter(Boolean)) {
const matches = line.match(/(\d{1,3})%/g);
if (matches) {
const match = Math.max(...matches.map((p) => parseInt(p)));
const percent = Math.floor(match / 10) * 10;
if (percent > 10 && percent > progress) {
progress = percent;
if (typeof callback === "function") callback(progress - 10, null);
}
}
}
}
});
proc.on("close", (code) => {
if (code !== 0) {
console.error(`Script exited with error code (${code}).`);
if (typeof callback === "function") callback(null, code);
} else {
console.info("Script exited successfully.");
if (typeof callback === "function") callback(100, null);
}
});
};
/**
* Monitors a command asynchronously.
*
* @param {string} cmd - The command to monitor.
* @param {Array<string>} args - The arguments for the command.
* @param {Function} callback - A callback function that receives the output or error.
* @returns {Object} The spawned process object.
*/
const commandMonitor = (cmd, args, callback) => {
const proc = cpr.spawn(cmd, args);
proc.stdout.on("data", (data) => {
if (data) {
const output = data.toString().trim().replace(/\0/g, "");
if (typeof callback === "function") callback(output, null);
}
});
proc.stderr.on("data", (data) => {
if (data) {
const output = data.toString().trim().replace(/\0/g, "");
if (typeof callback === "function") callback(null, output);
}
});
return proc;
};
/**
* Monitors D-Bus property changes asynchronously using `dbus-monitor`.
*
* @param {string} path - The D-Bus object path.
* @param {Function} callback - A callback function that receives the changed property.
* @returns {Object} The spawned process object.
*/
const dbusMonitor = (path, callback) => {
const cmd = "dbus-monitor";
const args = [`interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='${path}'`];
const proc = cpr.spawn(cmd, args);
proc.stdout.on("data", (data) => {
try { try {
if (typeof callback === "function") { const signal = data.toString();
if (code === 0) { if (signal.includes(`string "Visible"`)) {
callback(successOutput.trim().replace(/\0/g, ""), null); const dicts = [...signal.matchAll(/dict entry\(\s*([^)]*?)\)/g)].map((dict) => dict[1].trim());
if (dicts.length) {
dicts.forEach((dict) => {
const key = dict.match(/string "(.*?)"/);
const value = dict.match(/(?<=variant\s+)(.*)/);
if (key && value) {
const property = { [key[1].trim()]: value[1].trim().split(" ").pop() };
if (typeof callback === "function") callback(property, null);
}
});
} else { } else {
callback(null, errorOutput.trim().replace(/\0/g, "")); const property = { Visible: `${HARDWARE.keyboard.visibility}` };
if (typeof callback === "function") callback(property, null);
} }
} }
} catch (error) { } catch (error) {
console.error("Execute Async:", error.message); console.error("Monitor D-Bus:", error.message);
if (typeof callback === "function") { if (typeof callback === "function") callback(null, error.message);
callback(null, error.message); }
} });
proc.stderr.on("data", (data) => {
if (data) {
console.error("Monitor D-Bus:", data.toString());
if (typeof callback === "function") callback(null, data.toString());
} }
}); });
return proc; return proc;
@@ -670,64 +1006,14 @@ const dbusCall = (path, method, values, callback = null) => {
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`; const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`]; const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
try { try {
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" }); const output = cpr.execSync([cmd, ...args].join(" ").trim(), { encoding: "utf8" });
if (typeof callback === "function") { if (typeof callback === "function") callback(output.trim().replace(/\0/g, ""), null);
callback(output.trim().replace(/\0/g, ""), null);
}
} catch (error) { } catch (error) {
console.error("Call D-Bus:", error.message); console.error("Call D-Bus:", error.message);
if (typeof callback === "function") { if (typeof callback === "function") callback(null, error.message);
callback(null, error.message);
}
} }
}; };
/**
* Monitors D-Bus property changes asynchronously using `dbus-monitor`.
*
* @param {string} path - The D-Bus object path.
* @param {Function} callback - A callback function that receives the changed property.
* @returns {Object} The spawned process object.
*/
const dbusMonitor = (path, callback) => {
const cmd = "dbus-monitor";
const args = [`interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='${path}'`];
const proc = cpr.spawn(cmd, args);
proc.stdout.on("data", (data) => {
try {
const signal = data.toString();
if (signal.includes("member=PropertiesChanged") && signal.includes(`path=${path}`)) {
const dicts = [...signal.matchAll(/dict entry\(\s*([^)]*?)\)/g)].map((dict) => dict[1].trim());
if (dicts.length) {
dicts.forEach((dict) => {
const key = dict.match(/string "(.*?)"/);
const value = dict.match(/(?<=variant\s+)(.*)/);
if (key && value && typeof callback === "function") {
callback({ [key[1].trim()]: value[1].trim().split(" ").pop() }, null);
}
});
} else {
callback({ Visible: `${HARDWARE.keyboard.visibility}` }, null);
}
}
} catch (error) {
console.error("Monitor D-Bus:", error.message);
if (typeof callback === "function") {
callback(null, error.message);
}
}
});
proc.stderr.on("data", (data) => {
if (data) {
console.error("Monitor D-Bus:", data.toString());
if (typeof callback === "function") {
callback(null, data.toString());
}
}
});
return proc;
};
/** /**
* Helper function for asynchronous interval calls. * Helper function for asynchronous interval calls.
* *
@@ -755,24 +1041,26 @@ module.exports = {
getVendor, getVendor,
getSerialNumber, getSerialNumber,
getMachineId, getMachineId,
getNetworkAddresses,
getHostName, getHostName,
getUpTime, getUpTime,
getMemorySize, getMemorySize,
getMemoryUsage, getMemoryUsage,
getProcessorUsage, getProcessorUsage,
getProcessorTemperature, getProcessorTemperature,
getBatteryLevelPath,
getBatteryLevel, getBatteryLevel,
getDisplayStatusPath,
getDisplayStatus, getDisplayStatus,
setDisplayStatus, setDisplayStatus,
getDisplayBrightnessPath,
getDisplayBrightnessMax,
getDisplayBrightness, getDisplayBrightness,
setDisplayBrightness, setDisplayBrightness,
getAudioVolume,
setAudioVolume,
getKeyboardVisibility, getKeyboardVisibility,
setKeyboardVisibility, setKeyboardVisibility,
checkPackageUpgrades, checkPackageUpgrades,
shutdownSystem, shutdownSystem,
rebootSystem, rebootSystem,
execSyncCommand,
execAsyncCommand,
execScriptCommand,
}; };
+476 -151
View File
@@ -4,7 +4,6 @@ const { app } = require("electron");
global.INTEGRATION = global.INTEGRATION || { global.INTEGRATION = global.INTEGRATION || {
initialized: false, initialized: false,
status: null,
}; };
/** /**
@@ -38,41 +37,53 @@ const init = async () => {
// Init globals // Init globals
INTEGRATION.discovery = discovery; INTEGRATION.discovery = discovery;
INTEGRATION.node = `rpi_${deviceId}`; INTEGRATION.node = `rpi_${deviceId}`;
INTEGRATION.root = `${app.getName()}/${INTEGRATION.node}`; INTEGRATION.root = `${APP.name}/${INTEGRATION.node}`;
INTEGRATION.device = { INTEGRATION.device = {
name: `TouchKio ${deviceName}`, name: `${APP.title} ${deviceName}`,
model: model, model: model,
manufacturer: vendor, manufacturer: vendor,
serial_number: serialNumber, serial_number: serialNumber,
identifiers: [INTEGRATION.node], identifiers: [INTEGRATION.node],
sw_version: `${app.getName()}-v${app.getVersion()}`, sw_version: `${APP.name}-v${APP.version}`,
configuration_url: APP.homepage,
}; };
// Connection settings // Init options
const options = user === null || password === null ? null : { username: user, password: password }; const masked = password === null ? null : "*".repeat(password.length);
const masked = password === null ? "null" : "*".repeat(password.length); const options = user === null || password === null ? {} : { username: user, password: password };
console.log("MQTT Connecting:", `${user}:${masked}@${url.toString()}`); options.will = { topic: `${INTEGRATION.root}/kiosk/state`, payload: "Terminated", qos: 1, retain: true };
options.rejectUnauthorized = !("ignore_certificate_errors" in ARGS);
// Client connect
console.info("MQTT Connecting:", `${user}:${masked}@${url.toString()}`);
INTEGRATION.client = mqtt.connect(url.toString(), options); INTEGRATION.client = mqtt.connect(url.toString(), options);
INTEGRATION.client.setMaxListeners(20);
// Client connected // Client connected
INTEGRATION.client INTEGRATION.client
.on("connect", () => { .once("connect", () => {
console.log(`MQTT Connected: ${url.toString()}\n`); console.info(`MQTT Connected: ${url.toString()}`);
process.stdout.write("\n");
// Init client controls // Init client controls
initApp();
initShutdown(); initShutdown();
initReboot(); initReboot();
initRefresh(); initRefresh();
initKiosk(); initKiosk();
initTheme();
initDisplay(); initDisplay();
initVolume();
initKeyboard(); initKeyboard();
initPageNumber(); initPageNumber();
initPageZoom();
initPageUrl(); initPageUrl();
// Init client sensors // Init client sensors
initModel(); initModel();
initSerialNumber(); initSerialNumber();
initHostName(); initHostName();
initNetworkAddress();
initUpTime(); initUpTime();
initMemorySize(); initMemorySize();
initMemoryUsage(); initMemoryUsage();
@@ -80,39 +91,55 @@ const init = async () => {
initProcessorTemperature(); initProcessorTemperature();
initBatteryLevel(); initBatteryLevel();
initPackageUpgrades(); initPackageUpgrades();
initHeartbeat();
initLastActive(); initLastActive();
// Init client diagnostic
initHeartbeat();
initErrors();
initVersion();
// Integration initialized // Integration initialized
INTEGRATION.initialized = true; INTEGRATION.initialized = true;
INTEGRATION.status = "Online";
// Register global events
EVENTS.on("updateDisplay", () => {
updateDisplay();
updateLastActive();
});
EVENTS.on("updateStatus", updateKiosk);
EVENTS.on("updateTheme", updateTheme);
EVENTS.on("updateVolume", updateVolume);
EVENTS.on("updateKeyboard", updateKeyboard);
EVENTS.on("consoleLog", updateErrors);
}) })
.on("error", (error) => { .on("error", (error) => {
console.error("MQTT:", error); console.error("MQTT", error.message);
INTEGRATION.status = "Offline";
}); });
// Update sensor states from notifiers
HARDWARE.display.notifiers.push(() => {
updateDisplay();
});
HARDWARE.keyboard.notifiers.push(() => {
updateKeyboard();
});
// Update time sensors periodically (30s) // Update time sensors periodically (30s)
setInterval(() => { setInterval(() => {
updateHeartbeat(); if (APP.exiting) {
return;
}
updateLastActive(); updateLastActive();
updateHeartbeat();
updateErrors();
}, 30 * 1000); }, 30 * 1000);
// Update system sensors periodically (1min) // Update system sensors periodically (1min)
setInterval(() => { setInterval(() => {
if (APP.exiting) {
return;
}
update(); update();
}, 60 * 1000); }, 60 * 1000);
// Update package sensors periodically (60min) // Update upgrade sensors periodically (1h)
setInterval(() => { setInterval(() => {
if (APP.exiting) {
return;
}
updateApp();
updatePackageUpgrades(); updatePackageUpgrades();
}, 3600 * 1000); }, 3600 * 1000);
@@ -123,14 +150,15 @@ const init = async () => {
* Updates the shared integration properties. * Updates the shared integration properties.
*/ */
const update = async () => { const update = async () => {
if (!INTEGRATION.initialized) { if (!INTEGRATION.initialized || APP.exiting) {
return; return;
} }
// Update client sensors // Update client sensors
updateKiosk();
updatePageNumber(); updatePageNumber();
updatePageZoom();
updatePageUrl(); updatePageUrl();
updateNetworkAddress();
updateUpTime(); updateUpTime();
updateLastActive(); updateLastActive();
updateMemoryUsage(); updateMemoryUsage();
@@ -139,6 +167,22 @@ const update = async () => {
updateBatteryLevel(); updateBatteryLevel();
}; };
/**
* Removes the auto-discovery config via the mqtt connection.
*
* @param {string} type - The entity type name.
* @param {Object} config - The configuration object.
* @returns {Object} Instance of the mqtt client.
*/
const removeConfig = (type, config) => {
if (type === null || config === null) {
return INTEGRATION.client;
}
const path = config.unique_id.replace(`${INTEGRATION.node}_`, "");
const root = `${INTEGRATION.discovery}/${type}/${INTEGRATION.node}/${path}/config`;
return INTEGRATION.client.publish(root, JSON.stringify({}), { qos: 1, retain: true });
};
/** /**
* Publishes the auto-discovery config via the mqtt connection. * Publishes the auto-discovery config via the mqtt connection.
* *
@@ -185,6 +229,63 @@ const publishState = (path, state) => {
return INTEGRATION.client.publish(root, `${state}`, { qos: 1, retain: true }); return INTEGRATION.client.publish(root, `${state}`, { qos: 1, retain: true });
}; };
/**
* Initializes the app update entity and handles the execute logic.
*/
const initApp = () => {
const root = `${INTEGRATION.root}/app`;
const config = {
name: "App",
unique_id: `${INTEGRATION.node}_app`,
command_topic: `${root}/install`,
state_topic: `${root}/version/state`,
payload_install: "app_early" in ARGS ? "update early" : "update",
device: INTEGRATION.device,
};
if (!HARDWARE.support.appUpdate) {
removeConfig("update", config);
return;
}
publishConfig("update", config)
.on("message", (topic, message) => {
if (topic === config.command_topic) {
console.info("Update App...");
hardware.setDisplayStatus("ON", () => {
const args = ["-c", `bash <(wget -qO- ${APP.scripts.install}) ${message.toString()}`];
hardware.execScriptCommand("bash", args, (progress, error) => {
if (progress) {
console.info(`Progress: ${progress}%`);
}
updateApp(progress);
});
});
}
})
.subscribe(config.command_topic);
updateApp();
};
/**
* Updates the app update entity via the mqtt connection.
*/
const updateApp = async (progress = 0) => {
const latest = APP.releases.latest;
if (!HARDWARE.support.appUpdate || !latest || !latest.summary) {
return;
}
const summary = latest.summary.length > 250 ? latest.summary.slice(0, 250) + "..." : latest.summary;
const version = {
title: latest.title,
latest_version: latest.version,
installed_version: APP.version,
release_summary: summary,
release_url: latest.url,
update_percentage: progress || null,
in_progress: progress && progress > 0 && progress < 100,
};
publishState("app/version", JSON.stringify(version));
};
/** /**
* Initializes the shutdown button and handles the execute logic. * Initializes the shutdown button and handles the execute logic.
*/ */
@@ -197,12 +298,17 @@ const initShutdown = () => {
icon: "mdi:power", icon: "mdi:power",
device: INTEGRATION.device, device: INTEGRATION.device,
}; };
if (!HARDWARE.support.sudoRights) {
removeConfig("button", config);
return;
}
publishConfig("button", config) publishConfig("button", config)
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
console.log("Shutdown system..."); console.verbose("Shutdown system...");
hardware.setDisplayStatus("ON"); hardware.setDisplayStatus("ON", () => {
hardware.shutdownSystem(); hardware.shutdownSystem();
});
} }
}) })
.subscribe(config.command_topic); .subscribe(config.command_topic);
@@ -220,12 +326,17 @@ const initReboot = () => {
icon: "mdi:restart", icon: "mdi:restart",
device: INTEGRATION.device, device: INTEGRATION.device,
}; };
if (!HARDWARE.support.sudoRights) {
removeConfig("button", config);
return;
}
publishConfig("button", config) publishConfig("button", config)
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
console.log("Rebooting system..."); console.verbose("Rebooting system...");
hardware.setDisplayStatus("ON"); hardware.setDisplayStatus("ON", () => {
hardware.rebootSystem(); hardware.rebootSystem();
});
} }
}) })
.subscribe(config.command_topic); .subscribe(config.command_topic);
@@ -246,9 +357,10 @@ const initRefresh = () => {
publishConfig("button", config) publishConfig("button", config)
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
console.log("Refreshing webview..."); console.verbose("Refreshing webview...");
hardware.setDisplayStatus("ON"); hardware.setDisplayStatus("ON", () => {
WEBVIEW.views[WEBVIEW.viewActive].webContents.reloadIgnoringCache(); EVENTS.emit("reloadView");
});
} }
}) })
.subscribe(config.command_topic); .subscribe(config.command_topic);
@@ -273,32 +385,10 @@ const initKiosk = () => {
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
const status = message.toString(); const status = message.toString();
console.log("Set Kiosk Status:", status); console.verbose("Set Kiosk Status:", status);
hardware.setDisplayStatus("ON"); hardware.setDisplayStatus("ON", () => {
switch (status) { WEBVIEW.window.setStatus(status);
case "Framed": });
WEBVIEW.window.restore();
WEBVIEW.window.unmaximize();
WEBVIEW.window.setFullScreen(false);
break;
case "Fullscreen":
WEBVIEW.window.restore();
WEBVIEW.window.unmaximize();
WEBVIEW.window.setFullScreen(true);
break;
case "Maximized":
WEBVIEW.window.restore();
WEBVIEW.window.setFullScreen(false);
WEBVIEW.window.maximize();
break;
case "Minimized":
WEBVIEW.window.restore();
WEBVIEW.window.setFullScreen(false);
WEBVIEW.window.minimize();
break;
case "Terminated":
app.quit();
}
} }
}) })
.subscribe(config.command_topic); .subscribe(config.command_topic);
@@ -308,18 +398,50 @@ const initKiosk = () => {
/** /**
* Updates the kiosk status via the mqtt connection. * Updates the kiosk status via the mqtt connection.
*/ */
const updateKiosk = () => { const updateKiosk = async () => {
const kiosk = WEBVIEW.status; const kiosk = WEBVIEW.tracker.window.status;
publishState("kiosk", kiosk); publishState("kiosk", kiosk);
}; };
/**
* Initializes the application theme and handles the execute logic.
*/
const initTheme = () => {
const root = `${INTEGRATION.root}/theme`;
const config = {
name: "Theme",
unique_id: `${INTEGRATION.node}_theme`,
command_topic: `${root}/set`,
state_topic: `${root}/state`,
value_template: "{{ value }}",
options: ["Light", "Dark"],
icon: "mdi:compare",
device: INTEGRATION.device,
};
publishConfig("select", config)
.on("message", (topic, message) => {
if (topic === config.command_topic) {
const theme = message.toString();
console.verbose("Set Application Theme:", theme);
WEBVIEW.theme.set(theme);
}
})
.subscribe(config.command_topic);
updateTheme();
};
/**
* Updates the application theme via the mqtt connection.
*/
const updateTheme = async () => {
const theme = WEBVIEW.theme.get();
publishState("theme", theme.charAt(0).toUpperCase() + theme.slice(1));
};
/** /**
* Initializes the display status, brightness and handles the execute logic. * Initializes the display status, brightness and handles the execute logic.
*/ */
const initDisplay = () => { const initDisplay = () => {
if (!HARDWARE.support.displayStatus) {
return;
}
const root = `${INTEGRATION.root}/display`; const root = `${INTEGRATION.root}/display`;
const config = { const config = {
name: "Display", name: "Display",
@@ -335,18 +457,32 @@ const initDisplay = () => {
brightness_scale: 100, brightness_scale: 100,
}), }),
}; };
if (!HARDWARE.support.displayStatus) {
removeConfig("light", config);
return;
}
publishConfig("light", config) publishConfig("light", config)
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
const status = message.toString(); const status = message.toString();
console.log("Set Display Status:", status); console.verbose("Set Display Status:", status);
hardware.setDisplayStatus(status); hardware.setDisplayStatus(status, (reply, error) => {
hardware.update(); if (!error) {
hardware.update();
} else {
console.warn("Command Failed:", error);
}
});
} else if (topic === config.brightness_command_topic) { } else if (topic === config.brightness_command_topic) {
const brightness = parseInt(message, 10); const brightness = parseInt(message, 10);
console.log("Set Display Brightness:", brightness); console.verbose("Set Display Brightness:", brightness);
hardware.setDisplayBrightness(brightness); hardware.setDisplayBrightness(brightness, (reply, error) => {
hardware.update(); if (!error) {
hardware.update();
} else {
console.warn("Command Failed:", error);
}
});
} }
}) })
.subscribe(config.command_topic) .subscribe(config.command_topic)
@@ -357,20 +493,59 @@ const initDisplay = () => {
/** /**
* Updates the display status, brightness via the mqtt connection. * Updates the display status, brightness via the mqtt connection.
*/ */
const updateDisplay = () => { const updateDisplay = async () => {
const status = hardware.getDisplayStatus(); const status = hardware.getDisplayStatus();
const brightness = hardware.getDisplayBrightness(); const brightness = hardware.getDisplayBrightness();
publishState("display/power", status); publishState("display/power", status);
publishState("display/brightness", brightness); publishState("display/brightness", brightness);
}; };
/**
* Initializes the audio volume and handles the execute logic.
*/
const initVolume = () => {
const root = `${INTEGRATION.root}/volume`;
const config = {
name: "Volume",
unique_id: `${INTEGRATION.node}_volume`,
command_topic: `${root}/set`,
state_topic: `${root}/state`,
value_template: "{{ value | int }}",
mode: "slider",
min: 0,
max: 100,
unit_of_measurement: "%",
icon: "mdi:volume-high",
device: INTEGRATION.device,
};
if (!HARDWARE.support.audioVolume) {
removeConfig("number", config);
return;
}
publishConfig("number", config)
.on("message", (topic, message) => {
if (topic === config.command_topic) {
const volume = parseInt(message, 10);
console.verbose("Set Audio Volume:", volume);
hardware.setAudioVolume(volume);
}
})
.subscribe(config.command_topic);
updateVolume();
};
/**
* Updates the audio volume via the mqtt connection.
*/
const updateVolume = async () => {
const volume = hardware.getAudioVolume();
publishState("volume", volume);
};
/** /**
* Initializes the keyboard visibility and handles the execute logic. * Initializes the keyboard visibility and handles the execute logic.
*/ */
const initKeyboard = () => { const initKeyboard = () => {
if (!HARDWARE.support.keyboardVisibility) {
return;
}
const root = `${INTEGRATION.root}/keyboard`; const root = `${INTEGRATION.root}/keyboard`;
const config = { const config = {
name: "Keyboard", name: "Keyboard",
@@ -380,24 +555,18 @@ const initKeyboard = () => {
icon: "mdi:keyboard-close-outline", icon: "mdi:keyboard-close-outline",
device: INTEGRATION.device, device: INTEGRATION.device,
}; };
if (!HARDWARE.support.keyboardVisibility) {
removeConfig("switch", config);
return;
}
publishConfig("switch", config) publishConfig("switch", config)
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
const status = message.toString(); const status = message.toString();
console.log("Set Keyboard Visibility:", status); console.verbose("Set Keyboard Visibility:", status);
hardware.setKeyboardVisibility(status); hardware.setDisplayStatus("ON", () => {
switch (status) { hardware.setKeyboardVisibility(status);
case "OFF": });
WEBVIEW.window.restore();
WEBVIEW.window.unmaximize();
WEBVIEW.window.setFullScreen(true);
break;
case "ON":
WEBVIEW.window.restore();
WEBVIEW.window.setFullScreen(false);
WEBVIEW.window.maximize();
break;
}
} }
}) })
.subscribe(config.command_topic); .subscribe(config.command_topic);
@@ -407,7 +576,7 @@ const initKeyboard = () => {
/** /**
* Updates the keyboard visibility via the mqtt connection. * Updates the keyboard visibility via the mqtt connection.
*/ */
const updateKeyboard = () => { const updateKeyboard = async () => {
const visibility = hardware.getKeyboardVisibility(); const visibility = hardware.getKeyboardVisibility();
publishState("keyboard", visibility); publishState("keyboard", visibility);
}; };
@@ -416,9 +585,6 @@ const updateKeyboard = () => {
* Initializes the page number and handles the execute logic. * Initializes the page number and handles the execute logic.
*/ */
const initPageNumber = () => { const initPageNumber = () => {
if (WEBVIEW.viewUrls.length < 3) {
return;
}
const root = `${INTEGRATION.root}/page_number`; const root = `${INTEGRATION.root}/page_number`;
const config = { const config = {
name: "Page Number", name: "Page Number",
@@ -433,12 +599,17 @@ const initPageNumber = () => {
icon: "mdi:page-next", icon: "mdi:page-next",
device: INTEGRATION.device, device: INTEGRATION.device,
}; };
if (WEBVIEW.viewUrls.length <= 2) {
removeConfig("number", config);
return;
}
publishConfig("number", config) publishConfig("number", config)
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
const number = parseInt(message, 10); const number = parseInt(message, 10);
console.log("Set Page Number:", number); console.verbose("Set Page Number:", number);
WEBVIEW.viewActive = number; WEBVIEW.viewActive = number || 1;
EVENTS.emit("updateView");
} }
}) })
.subscribe(config.command_topic); .subscribe(config.command_topic);
@@ -448,11 +619,51 @@ const initPageNumber = () => {
/** /**
* Updates the page number via the mqtt connection. * Updates the page number via the mqtt connection.
*/ */
const updatePageNumber = () => { const updatePageNumber = async () => {
const pageNumber = WEBVIEW.viewActive; const pageNumber = WEBVIEW.viewUrls.length <= 2 ? null : WEBVIEW.viewActive || 1;
publishState("page_number", pageNumber); publishState("page_number", pageNumber);
}; };
/**
* Initializes the page zoom and handles the execute logic.
*/
const initPageZoom = () => {
const root = `${INTEGRATION.root}/page_zoom`;
const config = {
name: "Page Zoom",
unique_id: `${INTEGRATION.node}_page_zoom`,
command_topic: `${root}/set`,
state_topic: `${root}/state`,
value_template: "{{ value | int }}",
mode: "box",
min: 25,
max: 400,
step: 5,
unit_of_measurement: "%",
icon: "mdi:magnify-plus",
device: INTEGRATION.device,
};
publishConfig("number", config)
.on("message", (topic, message) => {
if (topic === config.command_topic) {
const zoom = parseInt(message, 10);
console.verbose("Set Page Zoom:", zoom);
WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.setZoomFactor(zoom / 100.0);
EVENTS.emit("updateView");
}
})
.subscribe(config.command_topic);
updatePageZoom();
};
/**
* Updates the page zoom via the mqtt connection.
*/
const updatePageZoom = async () => {
const pageZoom = Math.round(WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.getZoomFactor() * 100.0);
publishState("page_zoom", pageZoom);
};
/** /**
* Initializes the page url and handles the execute logic. * Initializes the page url and handles the execute logic.
*/ */
@@ -472,8 +683,8 @@ const initPageUrl = () => {
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
const url = message.toString(); const url = message.toString();
console.log("Set Page Url:", url); console.verbose("Set Page Url:", url);
WEBVIEW.views[WEBVIEW.viewActive].webContents.loadURL(url); WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.loadURL(url);
} }
}) })
.subscribe(config.command_topic); .subscribe(config.command_topic);
@@ -483,9 +694,9 @@ const initPageUrl = () => {
/** /**
* Updates the page url via the mqtt connection. * Updates the page url via the mqtt connection.
*/ */
const updatePageUrl = () => { const updatePageUrl = async () => {
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive]; const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive || 1];
const currentUrl = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL(); const currentUrl = WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.getURL();
const pageUrl = !currentUrl || currentUrl.startsWith("data:") ? defaultUrl : currentUrl; const pageUrl = !currentUrl || currentUrl.startsWith("data:") ? defaultUrl : currentUrl;
publishState("page_url", pageUrl); publishState("page_url", pageUrl);
}; };
@@ -499,6 +710,7 @@ const initModel = () => {
name: "Model", name: "Model",
unique_id: `${INTEGRATION.node}_model`, unique_id: `${INTEGRATION.node}_model`,
state_topic: `${root}/state`, state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ value }}", value_template: "{{ value }}",
icon: "mdi:raspberry-pi", icon: "mdi:raspberry-pi",
device: INTEGRATION.device, device: INTEGRATION.device,
@@ -510,9 +722,10 @@ const initModel = () => {
/** /**
* Updates the model sensor via the mqtt connection. * Updates the model sensor via the mqtt connection.
*/ */
const updateModel = () => { const updateModel = async () => {
const model = hardware.getModel(); const model = hardware.getModel();
publishState("model", model); publishState("model", model);
publishAttributes("model", HARDWARE.support);
}; };
/** /**
@@ -535,11 +748,41 @@ const initSerialNumber = () => {
/** /**
* Updates the serial number sensor via the mqtt connection. * Updates the serial number sensor via the mqtt connection.
*/ */
const updateSerialNumber = () => { const updateSerialNumber = async () => {
const serialNumber = hardware.getSerialNumber(); const serialNumber = hardware.getSerialNumber();
publishState("serial_number", serialNumber); publishState("serial_number", serialNumber);
}; };
/**
* Initializes the network address sensor.
*/
const initNetworkAddress = () => {
const root = `${INTEGRATION.root}/network_address`;
const config = {
name: "Network Address",
unique_id: `${INTEGRATION.node}_network_address`,
state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ value }}",
icon: "mdi:ip-network",
device: INTEGRATION.device,
};
publishConfig("sensor", config);
updateNetworkAddress();
};
/**
* Updates the network address sensor via the mqtt connection.
*/
const updateNetworkAddress = async () => {
const networkAddresses = hardware.getNetworkAddresses();
const [name] = Object.keys(networkAddresses);
const [family] = name ? Object.keys(networkAddresses[name]) : [];
const networkAddress = networkAddresses[name]?.[family]?.[0] || null;
publishState("network_address", networkAddress);
publishAttributes("network_address", networkAddresses);
};
/** /**
* Initializes the host name sensor. * Initializes the host name sensor.
*/ */
@@ -560,7 +803,7 @@ const initHostName = () => {
/** /**
* Updates the host name sensor via the mqtt connection. * Updates the host name sensor via the mqtt connection.
*/ */
const updateHostName = () => { const updateHostName = async () => {
const hostName = hardware.getHostName(); const hostName = hardware.getHostName();
publishState("host_name", hostName); publishState("host_name", hostName);
}; };
@@ -574,6 +817,7 @@ const initUpTime = () => {
name: "Up Time", name: "Up Time",
unique_id: `${INTEGRATION.node}_up_time`, unique_id: `${INTEGRATION.node}_up_time`,
state_topic: `${root}/state`, state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ (value | float) | round(0) }}", value_template: "{{ (value | float) | round(0) }}",
unit_of_measurement: "min", unit_of_measurement: "min",
icon: "mdi:timeline-clock", icon: "mdi:timeline-clock",
@@ -586,9 +830,13 @@ const initUpTime = () => {
/** /**
* Updates the up time sensor via the mqtt connection. * Updates the up time sensor via the mqtt connection.
*/ */
const updateUpTime = () => { const updateUpTime = async () => {
const upTime = hardware.getUpTime(); const upTime = hardware.getUpTime();
const bootTime = {
boot: new Date(new Date().getTime() - upTime * 60 * 1000),
};
publishState("up_time", upTime); publishState("up_time", upTime);
publishAttributes("up_time", bootTime);
}; };
/** /**
@@ -612,7 +860,7 @@ const initMemorySize = () => {
/** /**
* Updates the memory size sensor via the mqtt connection. * Updates the memory size sensor via the mqtt connection.
*/ */
const updateMemorySize = () => { const updateMemorySize = async () => {
const memorySize = hardware.getMemorySize(); const memorySize = hardware.getMemorySize();
publishState("memory_size", memorySize); publishState("memory_size", memorySize);
}; };
@@ -638,7 +886,7 @@ const initMemoryUsage = () => {
/** /**
* Updates the memory usage sensor via the mqtt connection. * Updates the memory usage sensor via the mqtt connection.
*/ */
const updateMemoryUsage = () => { const updateMemoryUsage = async () => {
const memoryUsage = hardware.getMemoryUsage(); const memoryUsage = hardware.getMemoryUsage();
publishState("memory_usage", memoryUsage); publishState("memory_usage", memoryUsage);
}; };
@@ -664,7 +912,7 @@ const initProcessorUsage = () => {
/** /**
* Updates the processor usage sensor via the mqtt connection. * Updates the processor usage sensor via the mqtt connection.
*/ */
const updateProcessorUsage = () => { const updateProcessorUsage = async () => {
const processorUsage = hardware.getProcessorUsage(); const processorUsage = hardware.getProcessorUsage();
publishState("processor_usage", processorUsage); publishState("processor_usage", processorUsage);
}; };
@@ -690,7 +938,7 @@ const initProcessorTemperature = () => {
/** /**
* Updates the processor temperature sensor via the mqtt connection. * Updates the processor temperature sensor via the mqtt connection.
*/ */
const updateProcessorTemperature = () => { const updateProcessorTemperature = async () => {
const processorTemperature = hardware.getProcessorTemperature(); const processorTemperature = hardware.getProcessorTemperature();
publishState("processor_temperature", processorTemperature); publishState("processor_temperature", processorTemperature);
}; };
@@ -699,9 +947,6 @@ const updateProcessorTemperature = () => {
* Initializes the battery level sensor. * Initializes the battery level sensor.
*/ */
const initBatteryLevel = () => { const initBatteryLevel = () => {
if (!HARDWARE.support.batteryLevel) {
return;
}
const root = `${INTEGRATION.root}/battery_level`; const root = `${INTEGRATION.root}/battery_level`;
const config = { const config = {
name: "Battery Level", name: "Battery Level",
@@ -712,6 +957,10 @@ const initBatteryLevel = () => {
icon: "mdi:battery-medium", icon: "mdi:battery-medium",
device: INTEGRATION.device, device: INTEGRATION.device,
}; };
if (!HARDWARE.support.batteryLevel) {
removeConfig("sensor", config);
return;
}
publishConfig("sensor", config); publishConfig("sensor", config);
updateBatteryLevel(); updateBatteryLevel();
}; };
@@ -719,7 +968,7 @@ const initBatteryLevel = () => {
/** /**
* Updates the battery level sensor via the mqtt connection. * Updates the battery level sensor via the mqtt connection.
*/ */
const updateBatteryLevel = () => { const updateBatteryLevel = async () => {
const batteryLevel = hardware.getBatteryLevel(); const batteryLevel = hardware.getBatteryLevel();
publishState("battery_level", batteryLevel); publishState("battery_level", batteryLevel);
}; };
@@ -745,41 +994,16 @@ const initPackageUpgrades = () => {
/** /**
* Updates the package upgrades sensor via the mqtt connection. * Updates the package upgrades sensor via the mqtt connection.
*/ */
const updatePackageUpgrades = () => { const updatePackageUpgrades = async () => {
const packages = hardware.checkPackageUpgrades(); const packages = hardware.checkPackageUpgrades();
const attributes = { const upgrades = {
total: packages.length, packages: packages.map((pkg) => {
packages: packages.map((pkg) => pkg.replace(/\s*\[.*?\]\s*/g, "").trim()), const [name, version] = pkg.replace(/\s*\[.*?\]\s*/g, "").split(/\s+/, 2);
return { [name]: version };
}),
}; };
publishAttributes("package_upgrades", attributes); publishState("package_upgrades", packages.length);
publishState("package_upgrades", attributes.total); publishAttributes("package_upgrades", upgrades);
};
/**
* Initializes the heartbeat sensor.
*/
const initHeartbeat = () => {
const root = `${INTEGRATION.root}/heartbeat`;
const config = {
name: "Heartbeat",
unique_id: `${INTEGRATION.node}_heartbeat`,
state_topic: `${root}/state`,
value_template: "{{ value }}",
icon: "mdi:heart-flash",
device: INTEGRATION.device,
};
publishConfig("sensor", config);
updateHeartbeat();
};
/**
* Updates the heartbeat sensor via the mqtt connection.
*/
const updateHeartbeat = () => {
const now = new Date();
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000);
const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, "");
publishState("heartbeat", heartbeat);
}; };
/** /**
@@ -791,6 +1015,7 @@ const initLastActive = () => {
name: "Last Active", name: "Last Active",
unique_id: `${INTEGRATION.node}_last_active`, unique_id: `${INTEGRATION.node}_last_active`,
state_topic: `${root}/state`, state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ (value | float) | round(0) }}", value_template: "{{ (value | float) | round(0) }}",
unit_of_measurement: "min", unit_of_measurement: "min",
icon: "mdi:gesture-tap-hold", icon: "mdi:gesture-tap-hold",
@@ -803,11 +1028,111 @@ const initLastActive = () => {
/** /**
* Updates the last active sensor via the mqtt connection. * Updates the last active sensor via the mqtt connection.
*/ */
const updateLastActive = () => { const updateLastActive = async () => {
const now = new Date(); const now = new Date();
const then = WEBVIEW.pointer.time; const then = WEBVIEW.tracker.pointer.time;
const lastActive = Math.abs(now - then) / (1000 * 60); const lastActive = (now - then) / (1000 * 60);
const tracker = {
...WEBVIEW.tracker.pointer.position,
...WEBVIEW.tracker.display,
};
publishState("last_active", lastActive); publishState("last_active", lastActive);
publishAttributes("last_active", tracker);
};
/**
* Initializes the heartbeat sensor.
*/
const initHeartbeat = () => {
const root = `${INTEGRATION.root}/heartbeat`;
const config = {
name: "Heartbeat",
unique_id: `${INTEGRATION.node}_heartbeat`,
state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ value }}",
entity_category: "diagnostic",
icon: "mdi:heart-flash",
device: INTEGRATION.device,
};
publishConfig("sensor", config);
updateHeartbeat();
};
/**
* Updates the heartbeat sensor via the mqtt connection.
*/
const updateHeartbeat = async () => {
const now = new Date();
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000);
const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, "");
const attributes = { date: now };
publishState("heartbeat", heartbeat);
publishAttributes("heartbeat", attributes);
};
/**
* Initializes the error log sensor.
*/
const initErrors = () => {
const root = `${INTEGRATION.root}/errors`;
const config = {
name: "Errors",
unique_id: `${INTEGRATION.node}_errors`,
state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ value | int }}",
entity_category: "diagnostic",
icon: "mdi:alert-circle",
device: INTEGRATION.device,
};
publishConfig("sensor", config);
updateErrors();
};
/**
* Updates the error log sensor via the mqtt connection.
*/
const updateErrors = async () => {
const logs = APP.logs.slice().reverse();
const errors = logs.filter((log) => log.level === "error");
const history = logs.reduce((acc, log) => {
const time = log.time.toISOString().slice(0, 16);
if (!acc[time]) {
acc[time] = [];
}
acc[time].push({ [log.level.toUpperCase()]: log.text });
return acc;
}, {});
publishState("errors", errors.length);
publishAttributes("errors", history);
};
/**
* Initializes the version sensor.
*/
const initVersion = () => {
const root = `${INTEGRATION.root}/version`;
const config = {
name: "Version",
unique_id: `${INTEGRATION.node}_version`,
state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ value }}",
entity_category: "diagnostic",
icon: "mdi:application-braces",
device: INTEGRATION.device,
};
publishConfig("sensor", config);
updateVersion();
};
/**
* Updates the version sensor via the mqtt connection.
*/
const updateVersion = async () => {
publishState("version", APP.version);
publishAttributes("version", APP.build);
}; };
module.exports = { module.exports = {
+704 -204
View File
File diff suppressed because it is too large Load Diff
+12 -8
View File
@@ -1,27 +1,31 @@
{ {
"name": "touchkio", "name": "touchkio",
"title": "TouchKio",
"description": "Home Assistant Touch Kiosk", "description": "Home Assistant Touch Kiosk",
"homepage": "https://github.com/leukipp/touchkio", "homepage": "https://github.com/leukipp/touchkio",
"author": "leukipp", "author": "leukipp",
"version": "1.2.0", "version": "1.4.0",
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "electron . --web-url=https://demo.home-assistant.io --trace-warnings", "start": "electron . --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app --trace-warnings",
"build": "electron-forge make" "build": "electron-forge make"
}, },
"prettier": { "prettier": {
"printWidth": 120 "printWidth": 120
}, },
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^7.8.1", "@electron-forge/cli": "^7.10.0",
"@electron-forge/maker-deb": "^7.8.1", "@electron-forge/maker-deb": "^7.10.0",
"@electron-forge/maker-zip": "^7.8.1", "@electron-forge/maker-zip": "^7.10.0",
"@electron-forge/publisher-github": "^7.8.1", "@electron-forge/publisher-github": "^7.10.0",
"electron": "^37.2.0", "electron": "^38.2.2",
"jszip": "^3.10.1",
"prettier": "^3.6.2" "prettier": "^3.6.2"
}, },
"dependencies": { "dependencies": {
"mqtt": "^5.13.2" "axios": "^1.12.2",
"electron-log": "^5.4.3",
"mqtt": "^5.14.1"
} }
} }
+758 -326
View File
File diff suppressed because it is too large Load Diff