Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e195a56739 | ||
|
|
d86a5385a8 | ||
|
|
3ce1265c8a | ||
|
|
c2bf551834 | ||
|
|
785a97721a | ||
|
|
05c9d98b61 | ||
|
|
9c26d9d8bb | ||
|
|
ad7d3fd3ff | ||
|
|
f666ed57ab | ||
|
|
c0b1a6e9ce | ||
|
|
19e51e349c | ||
|
|
5d012bd637 | ||
|
|
01c7411ddc | ||
|
|
3e4d96536f | ||
|
|
4bcb2c4e89 | ||
|
|
b2b87888e4 | ||
|
|
164ef2838c | ||
|
|
ae2633800d | ||
|
|
552f0e19a4 | ||
|
|
890c84d69d | ||
|
|
ac69dee387 | ||
|
|
ac62dc17ce | ||
|
|
f2f80e7d52 | ||
|
|
10e7ff078a | ||
|
|
e677fff160 | ||
|
|
63db26611e | ||
|
|
5ec6577b5a | ||
|
|
0359742e70 | ||
|
|
b70f4074e5 | ||
|
|
a4c300407b | ||
|
|
758f64ced4 | ||
|
|
7ab803d4ca | ||
|
|
092697850d | ||
|
|
422e92dd89 | ||
|
|
a15c0fc91d | ||
|
|
358fa3ca9c | ||
|
|
10722fa217 | ||
|
|
7267fafb97 | ||
|
|
b5e95936dc | ||
|
|
047b38a41b | ||
|
|
a93fbb2ad3 | ||
|
|
0b927a3ef6 | ||
|
|
d35cf27de9 | ||
|
|
19348b4b60 | ||
|
|
ce1eed391d | ||
|
|
fe6fe92656 | ||
|
|
3aea938cfb | ||
|
|
c39428fa42 | ||
|
|
7caa5bfc8a | ||
|
|
5f69961a9f | ||
|
|
f44d998016 | ||
|
|
3ea65a4af8 | ||
|
|
2ad49c3167 | ||
|
|
ef2093a25e | ||
|
|
2737eabd8a | ||
|
|
b8f809c50e | ||
|
|
c3e625a8f6 | ||
|
|
7c15707e3b | ||
|
|
dbd3756793 | ||
|
|
85fe5a910a | ||
|
|
d78088e6af | ||
|
|
bf77d9b2a0 | ||
|
|
f4ca7bc412 | ||
|
|
d0ea377ead | ||
|
|
9a829f72e3 | ||
|
|
482a56c9b9 | ||
|
|
32409983b2 | ||
|
|
fe0667919e | ||
|
|
0037a892cd | ||
|
|
ad22dd3215 | ||
|
|
619179c3c8 | ||
|
|
7e5e669b09 | ||
|
|
437c4ab1c5 | ||
|
|
ea01876ea7 | ||
|
|
9de170ce6c | ||
|
|
6b3e89d522 | ||
|
|
cbfd0538e7 | ||
|
|
d3af6d397e | ||
|
|
dd81da204e | ||
|
|
00aef6402e | ||
|
|
a36c624601 | ||
|
|
adbc214f4d | ||
|
|
e64f092f6c | ||
|
|
cab2052477 | ||
|
|
0d66d104fb | ||
|
|
1afe335379 | ||
|
|
568be07043 | ||
|
|
9d70b09358 | ||
|
|
6570d5c00e | ||
|
|
af7d0c58ce | ||
|
|
aed79ab519 | ||
|
|
e2397bad65 | ||
|
|
e7622bef17 | ||
|
|
2553f52983 | ||
|
|
30b5ec2d8b | ||
|
|
94c7e34706 | ||
|
|
09dda65333 | ||
|
|
99f465a044 | ||
|
|
02d1309345 | ||
|
|
3142606d12 | ||
|
|
51189e8595 | ||
|
|
71a9d4c1de | ||
|
|
f022db9478 | ||
|
|
fcc0e5813e | ||
|
|
6d23efd713 | ||
|
|
a37226fd39 | ||
|
|
9b8067a598 | ||
|
|
257dab8d7f | ||
|
|
ff4b9d1262 | ||
|
|
757ba9ff92 | ||
|
|
ed3bad7292 | ||
|
|
f00a49e4e4 | ||
|
|
bbe20fef8f | ||
|
|
cfbc391a52 | ||
|
|
b1998ceea9 | ||
|
|
081cee2990 | ||
|
|
fd746cc79a | ||
|
|
4f426df4e6 | ||
|
|
7084fc8188 | ||
|
|
b2685f8d6b | ||
|
|
5743b0ff08 | ||
|
|
a7f4ede899 | ||
|
|
3201786d41 | ||
|
|
0656f8f897 | ||
|
|
dbcfb74923 | ||
|
|
640156ba87 | ||
|
|
8d2f3da93e | ||
|
|
286e8c1758 | ||
|
|
c8f254d90a | ||
|
|
5e3d204019 | ||
|
|
926bcdfab8 | ||
|
|
99d3eea20d | ||
|
|
6d7e1e0bdf | ||
|
|
bbc435ef1e | ||
|
|
a9b7820261 | ||
|
|
d7dd3616bf | ||
|
|
7ef5f3705a | ||
|
|
21dbe9fd5f | ||
|
|
eb45dd001d |
@@ -0,0 +1 @@
|
|||||||
|
github: leukipp
|
||||||
@@ -8,9 +8,6 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
arch: [arm64]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -30,4 +27,4 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
yarn electron-forge publish --arch arm64
|
yarn electron-forge publish --arch arm64,x64
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
docs/
|
docs/
|
||||||
out/
|
out/
|
||||||
|
build.json
|
||||||
|
|||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
# Supported Hardware
|
||||||
|
This document contains an incomplete list of device and display hardware combinations that have already been tested.
|
||||||
|
|
||||||
|
The [release](https://github.com/leukipp/touchkio/releases) page has builds exclusively for **arm64** and **x64**, but custom builds for other architectures can be made (see [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development)), allowing the application to operate on any hardware.
|
||||||
|
At least the **minimal** features, such as displaying a **kiosk window** (which doesn't necessarily need to be Home Assistant) will work.
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
If you are running Linux with a graphical user interface (Wayland or X11), you should be well equipped to use the application. Additionally, any single board computer (SBC) clones of the Raspberry Pi that operate on Raspberry Pi OS **(64-bit)** are likely to function as well.
|
||||||
|
|
||||||
|
| | Status | Notes |
|
||||||
|
| --- | --------------------- | ------------------------------------------------------------------------- |
|
||||||
|
| 🟩 | Fully operational | Working display power, brightness and keyboard control via MQTT. |
|
||||||
|
| 🟨 | Partially operational | Display brightness control is not available via MQTT. |
|
||||||
|
| 🟧 | Partially operational | Display brightness and keyboard control is not available via MQTT. |
|
||||||
|
| 🟥 | Partially operational | Display power, brightness and keyboard control is not available via MQTT. |
|
||||||
|
| ⬜ | Somehow operational | Issues can occur and the overall performance is very slow. |
|
||||||
|
| ⬛ | Not operational | The house is on fire. |
|
||||||
|
|
||||||
|
### DSI
|
||||||
|
| Device | System | Display | Status |
|
||||||
|
| ---------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------ |
|
||||||
|
| Raspberry Pi 3 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | ⬜ |
|
||||||
|
| Raspberry Pi 3 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | ⬜ |
|
||||||
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | 🟩 |
|
||||||
|
| Raspberry Pi 4 (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 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | 🟩 |
|
||||||
|
| 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
|
||||||
|
| Device | System | Display | Status |
|
||||||
|
| ------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------ |
|
||||||
|
| 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 7EP-CAPLCD 7" (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 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 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 | [GeeekPi Capacitive Touch 10.1" (1280x800)](https://www.amazon.nl/dp/B0DHV6DZC1) | 🟨 |
|
||||||
|
| 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 | [Waveshare Capacitive Touch 14" (2160x1440)](https://www.waveshare.com/14inch-2160x1440-lcd.htm) | 🟨 |
|
||||||
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Akntzcs Portable Touch HD 16" (1920x1200)](https://www.amazon.com/dp/B0CTGW6MQ6) | 🟨 |
|
||||||
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Prechen Portable Touch FHD 18.5" (1920x1080)](https://www.amazon.de/dp/B0CT2KLDBQ) | 🟨 |
|
||||||
|
| 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
|
||||||
|
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.
|
||||||
|
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 and are able to fix it yourself, feel free to create a [pull request](https://github.com/leukipp/touchkio/pulls).
|
||||||
|
- If everything works as expected and your hardware is not yet listed, you are welcome to [report](https://github.com/leukipp/touchkio/issues/12) it or create a [pull request](https://github.com/leukipp/touchkio/pulls).
|
||||||
@@ -1 +1,299 @@
|
|||||||
# touchkio
|
# TouchKio
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
[](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.
|
||||||
|
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.
|
||||||
|
|
||||||
|
[](https://github.com/leukipp/touchkio/blob/main/img/display.png)
|
||||||
|
|
||||||
|
This implementation addresses common issues encountered when using the built-in browser running in fullscreen mode on a Linux device with Touch Display.
|
||||||
|
Moreover, the device running the **kiosk application** also offers several **Home Assistant MQTT** sensors, enhancing it's functionality for automation purposes.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- [x] Fast and easy setup.
|
||||||
|
- [x] Remember login credentials.
|
||||||
|
- [x] Touch optimized web browsing.
|
||||||
|
- [x] Dynamic window status bar layout.
|
||||||
|
- [x] Side panel widget for kiosk control.
|
||||||
|
- [x] Navigation bar to switch between pages.
|
||||||
|
- [x] Adjustable browser zoom and theme support.
|
||||||
|
- [x] Extended touch screen wake-up functionality.
|
||||||
|
- [x] Remote controllable via MQTT.
|
||||||
|
- [x] Toggle the on-screen keyboard.
|
||||||
|
- [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] Audio volume control for connected devices.
|
||||||
|
- [x] Execute system reboot and shutdown commands.
|
||||||
|
- [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.
|
||||||
|
Additionally, a **MQTT endpoint** can be defined, allowing the application to provide controls and sensors for the Linux device and the connected Touch Display.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Before you begin, make sure that you have a Linux device configured and operational with a [compatible](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) Touch Display.
|
||||||
|
This guide assumes that you are using a Raspberry Pi with the latest version of Raspberry Pi OS **(64-bit)**, along with a desktop environment (preferred using **labwc**).
|
||||||
|
However, the **.deb** setup procedure is also compatible with any other Debian based 64-bit system.
|
||||||
|
|
||||||
|
### Optional
|
||||||
|
To utilize the sensor features of your device through Home Assistant, it's essential to have a **MQTT broker running** and the **MQTT integration installed** on your Home Assistant instance.
|
||||||
|
This setup allows seamless communication between your kiosk device and Home Assistant, enabling **real-time data exchange**.
|
||||||
|
|
||||||
|
[](https://github.com/leukipp/touchkio/blob/main/img/mqtt.png)
|
||||||
|
|
||||||
|
For a comprehensive guide on setting up MQTT with Home Assistant, please refer to the official documentation available here: https://www.home-assistant.io/integrations/mqtt.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
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 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.
|
||||||
|
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).
|
||||||
|
|
||||||
|
#### Option 1 - The easy way
|
||||||
|
Run this command to download and install the latest **.deb** (arm64 or x64) release.
|
||||||
|
It will also create a systemd user file for auto-startup and will guide you through the setup process:
|
||||||
|
```bash
|
||||||
|
bash <(wget -qO- https://raw.githubusercontent.com/leukipp/touchkio/main/install.sh)
|
||||||
|
```
|
||||||
|
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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
```bash
|
||||||
|
systemctl --user start|stop|status|restart touchkio.service
|
||||||
|
```
|
||||||
|
|
||||||
|
<details><summary>Alternatives</summary><div>
|
||||||
|
|
||||||
|
#### Option 2 - The standard way
|
||||||
|
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):
|
||||||
|
- [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: `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`
|
||||||
|
- 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.
|
||||||
|
- Only when using the MQTT integration via `--mqtt-*`, a running Home Assistant instance is required.
|
||||||
|
- If you need the application to be automatically started on boot, create a systemd file.
|
||||||
|
|
||||||
|
#### Option 3 - The hard way
|
||||||
|
Pre-built release files are available for arm64 and x64 Linux systems.
|
||||||
|
If you are using a different architecture, you can still utilize this repository to build your own application.
|
||||||
|
|
||||||
|
For more information please refer to the [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development) section, however this will do the job:
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
</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
|
||||||
|
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.
|
||||||
|
|
||||||
|
### WEB
|
||||||
|
The available arguments to control the kiosk application via terminal are as follows:
|
||||||
|
| 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-theme` (Optional) | `dark` | Theme settings of the web browser (`light` or `dark`) |
|
||||||
|
| `--web-zoom` (Optional) | `1.25` | Zoom settings of the web browser (`1.0` is `100%`) |
|
||||||
|
| `--web-widget` (Optional) | `true` | Enables the sidebar widget (`true` or `false`) |
|
||||||
|
|
||||||
|
These arguments allow you to customize the appearance of the web browser view.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```bash
|
||||||
|
touchkio --web-url=http://192.168.1.42:8123 --web-theme=light --web-zoom=1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
> <a id="foot1"></a><a href="#ref1">[1]</a>: This doesn't necessarily have to be a Home Assistant Url.
|
||||||
|
> You can configure multiple pages by separating them with a comma:
|
||||||
|
> `touchkio --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app`.
|
||||||
|
|
||||||
|
In the `~/.config/touchkio/Arguments.json` file:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"web_url": [
|
||||||
|
"https://demo.home-assistant.io",
|
||||||
|
"https://demo.immichkiosk.app"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MQTT
|
||||||
|
To broadcast your local sensor data to Home Assistant, you can use the following arguments, which require a running MQTT broker:
|
||||||
|
| Name | Default | Description |
|
||||||
|
| ----------------------------- | --------------- | ---------------------------------------------------------------------------------------- |
|
||||||
|
| `--mqtt-url` (Required) | - | Url of the MQTT broker instance (MQTT(S)://IP:PORT) |
|
||||||
|
| `--mqtt-user` (Required) | - | Username which is available in Home Assistant (e.g. create a user named `kiosk`) |
|
||||||
|
| `--mqtt-password` (Required) | - | The password of the user (e.g. use `password`, because it's secure and easy to remember) |
|
||||||
|
| `--mqtt-discovery` (Optional) | `homeassistant` | The discovery prefix for MQTT (`homeassistant` works with default setups) |
|
||||||
|
|
||||||
|
When you start the application with the MQTT arguments, the Home Assistant auto-discovery feature will automatically add controls and sensors that can be used for further integration.
|
||||||
|
You can find them under **Settings** -> **Devices and Services** -> **Devices** by searching for **TouchKio**.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
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).
|
||||||
|
|
||||||
|
Clone this repository and run `yarn install` to install the dependencies.
|
||||||
|
Then use `yarn start` to execute the start script located in the [package.json](https://github.com/leukipp/touchkio/blob/main/package.json) file.
|
||||||
|
There you may want to adjust the `--web-url` and other arguments for development runs.
|
||||||
|
|
||||||
|
If you access your device over SSH, make sure to export the display variables so the kiosk application can launch in the desktop environment.
|
||||||
|
```bash
|
||||||
|
export DISPLAY=":0"
|
||||||
|
export WAYLAND_DISPLAY="wayland-0"
|
||||||
|
```
|
||||||
|
To make this permanent, consider adding the export variables into the `~/.bashrc` file.
|
||||||
|
|
||||||
|
### Extensions
|
||||||
|
|
||||||
|
<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.
|
||||||
|
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.):
|
||||||
|
```bash
|
||||||
|
yarn add [package-name]
|
||||||
|
```
|
||||||
|
This will update the `package.json` file with the required dependencies.
|
||||||
|
|
||||||
|
2. **Import the library**: Open the `hardware.js` file and import the library using:
|
||||||
|
```javascript
|
||||||
|
const package = require("[package-name]");
|
||||||
|
```
|
||||||
|
Then implement your custom methods and logic to handle the hardware. Don't forget to export the methods at the end of the file:
|
||||||
|
```javascript
|
||||||
|
module.exports = { ... };
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Expose sensors via MQTT**: If you want to publish sensor data through MQTT, implement some init and update methods in the `integration.js` file:
|
||||||
|
```javascript
|
||||||
|
const initCustomSensor = (client) => { ... }
|
||||||
|
const updateCustomSensor = (client) => { ... }
|
||||||
|
```
|
||||||
|
To get started have a look at the existing methods. Don't forget to call the custom sensor initialization method inside the global init method, where the MQTT connection is established:
|
||||||
|
```javascript
|
||||||
|
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.
|
||||||
|
|
||||||
|
</div></details>
|
||||||
|
|
||||||
|
### The nitty gritty
|
||||||
|
|
||||||
|
<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 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 `wlopm --off \*` command changes the `bl_power` value to **4**, when setting the value to **0** the screen will turn on again.
|
||||||
|
However, `swayidle` still seems to consider the screen to be off and as a result it will not turn off again unless there is some interaction in the meantime.
|
||||||
|
|
||||||
|
When using the MQTT integration, the kiosk application must be able to **detect changes** made on the **device** itself.
|
||||||
|
I managed to achieve this for the `brightness` file by implementing a simple `fs.watch(..)` file listener.
|
||||||
|
However, I found that it **never triggered** for the `bl_power` file.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
|
||||||
|
The display power status and brightness can be adjusted via the MQTT integration.
|
||||||
|
**Support** for changing the power status on **DSI and HDMI** displays is achieved by checking for connected screens in `/sys/class/drm/*/status`.
|
||||||
|
Support for changing the brightness of connected display is implemented by using `/sys/class/backlight/*/brightness`.
|
||||||
|
In cases where no supported backlight device is found, the Home Assistant light entity will only show an on/off switch without brightness control.
|
||||||
|
|
||||||
|
Keep in mind that default arguments are stored as plain text in `~/.config/touchkio/Arguments.json`.
|
||||||
|
This file also includes the **MQTT user password**, which is somewhat obfuscated/encrypted, but in a way that it could be easily reverse engineered.
|
||||||
|
Implementing stronger **security measures** would complicate the setup process and could discourage some users from configuring the application properly.
|
||||||
|
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.
|
||||||
|
|
||||||
|
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 on/off**, the timestamps of these events are recorded.
|
||||||
|
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`.
|
||||||
|
This adjustment provides a user experience similar to that of a proper mobile device.
|
||||||
|
|
||||||
|
</div></details>
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
Please review the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation first if you encounter any problems.
|
||||||
|
|
||||||
|
**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](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](https://www.home-assistant.io/docs/automation/basics) (e.g. presence detection) to manage the screen status.
|
||||||
|
- 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).
|
||||||
|
- 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.
|
||||||
|
- As a current workaround you can use the side [widget](https://github.com/leukipp/touchkio/issues/16) to toggle the keyboard visibility.
|
||||||
|
- 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
|
||||||
|
[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.
|
||||||
|
|
||||||
|
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)).
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=uTxURzmrVtA)
|
||||||
|
|
||||||
|
## License
|
||||||
|
[MIT](https://github.com/leukipp/touchkio/blob/main/LICENSE)
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
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 = {
|
||||||
|
packagerConfig: {
|
||||||
|
ignore: [".github", ".gitignore", "install.sh", "forge.config.js"],
|
||||||
|
},
|
||||||
|
makers: [
|
||||||
|
{
|
||||||
|
name: "@electron-forge/maker-deb",
|
||||||
|
config: {
|
||||||
|
options: {
|
||||||
|
productName: "TouchKio",
|
||||||
|
productDescription: "Kiosk mode application for a Home Assistant dashboard",
|
||||||
|
categories: ["Network"],
|
||||||
|
icon: "img/icon.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "@electron-forge/maker-zip",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
publishers: [
|
||||||
|
{
|
||||||
|
name: "@electron-forge/publisher-github",
|
||||||
|
config: {
|
||||||
|
repository: {
|
||||||
|
owner: "leukipp",
|
||||||
|
name: "touchkio",
|
||||||
|
},
|
||||||
|
draft: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
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) => {
|
||||||
|
for (const result of results) {
|
||||||
|
const artifacts = [];
|
||||||
|
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")) {
|
||||||
|
const renamed = artifact.replace("amd64", "x64");
|
||||||
|
fs.renameSync(artifact, renamed);
|
||||||
|
artifacts.push(renamed);
|
||||||
|
} else {
|
||||||
|
artifacts.push(artifact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.artifacts = artifacts;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--primary-color: rgba(225, 225, 225, 1);
|
||||||
|
--border-color: rgba(225, 225, 225, 0.15);
|
||||||
|
--background-color: rgba(28, 28, 28, 1);
|
||||||
|
}
|
||||||
|
[data-theme="light"] {
|
||||||
|
--primary-color: rgba(33, 33, 33, 1);
|
||||||
|
--border-color: rgba(33, 33, 33, 0.15);
|
||||||
|
--background-color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 0 10px;
|
||||||
|
column-gap: 10px;
|
||||||
|
list-style-type: none;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
background: var(--background-color);
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
li.dynamic {
|
||||||
|
flex: 1 1 0%;
|
||||||
|
}
|
||||||
|
li.separator {
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
li.separator:has(+ li.hidden) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
li.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
li.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0 5px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
cursor: text;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--primary-color);
|
||||||
|
background-color: var(--border-color);
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
pointer-events: none;
|
||||||
|
fill: var(--primary-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<menu>
|
||||||
|
<li>
|
||||||
|
<button id="home">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>home-circle</title>
|
||||||
|
<path
|
||||||
|
d="M19.07,4.93C17.22,3 14.66,1.96 12,2C9.34,1.96 6.79,3 4.94,4.93C3,6.78 1.96,9.34 2,12C1.96,14.66 3,17.21 4.93,19.06C6.78,21 9.34,22.04 12,22C14.66,22.04 17.21,21 19.06,19.07C21,17.22 22.04,14.66 22,12C22.04,9.34 21,6.78 19.07,4.93M17,12V18H13.5V13H10.5V18H7V12H5L12,5L19.5,12H17Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<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">
|
||||||
|
<title>page-next</title>
|
||||||
|
<path
|
||||||
|
d="M20,3H5A2,2 0 0,0 3,5V11H7V9L11,12L7,15V13H3V19A2,2 0 0,0 5,21H20A2,2 0 0,0 22,19V5A2,2 0 0,0 20,3M17,17H13V15H17V17M20,13H13V11H20V13M20,9H13V7H20V9M3,13H0V11H3V13Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="separator"></li>
|
||||||
|
<li class="dynamic">
|
||||||
|
<input id="url" type="text" spellcheck="false" readonly="true" />
|
||||||
|
</li>
|
||||||
|
<li class="separator"></li>
|
||||||
|
<li>
|
||||||
|
<button id="backward">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>history</title>
|
||||||
|
<path
|
||||||
|
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>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="forward">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>update</title>
|
||||||
|
<path
|
||||||
|
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>
|
||||||
|
</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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.querySelectorAll("input").forEach((input) => {
|
||||||
|
input.addEventListener("blur", (e) => {
|
||||||
|
ipcRenderer.send("input-blur", { id: e.target.id });
|
||||||
|
});
|
||||||
|
input.addEventListener("focus", (e) => {
|
||||||
|
ipcRenderer.send("input-focus", { id: e.target.id });
|
||||||
|
});
|
||||||
|
input.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
ipcRenderer.send("input-enter", { id: e.target.id, text: e.target.value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerReceiveEvents = () => {
|
||||||
|
ipcRenderer.on("button-disabled", (e, button) => {
|
||||||
|
document.getElementById(button.id).parentNode.classList.toggle("disabled", button.disabled);
|
||||||
|
});
|
||||||
|
ipcRenderer.on("button-hidden", (e, button) => {
|
||||||
|
document.getElementById(button.id).parentNode.classList.toggle("hidden", button.hidden);
|
||||||
|
});
|
||||||
|
ipcRenderer.on("input-readonly", (e, input) => {
|
||||||
|
document.getElementById(input.id).readOnly = input.readonly;
|
||||||
|
});
|
||||||
|
ipcRenderer.on("input-text", (e, input) => {
|
||||||
|
document.getElementById(input.id).value = input.text;
|
||||||
|
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) => {
|
||||||
|
document.documentElement.setAttribute("data-theme", data.theme);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerSendEvents();
|
||||||
|
registerReceiveEvents();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--primary-color: rgba(225, 225, 225, 1);
|
||||||
|
--border-color: rgba(225, 225, 225, 0.15);
|
||||||
|
--background-color: rgba(28, 28, 28, 1);
|
||||||
|
}
|
||||||
|
[data-theme="light"] {
|
||||||
|
--primary-color: rgba(33, 33, 33, 1);
|
||||||
|
--border-color: rgba(33, 33, 33, 0.15);
|
||||||
|
--background-color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
[data-theme="hidden"] {
|
||||||
|
--primary-color: rgba(0, 0, 0, 0);
|
||||||
|
--border-color: rgba(0, 0, 0, 0);
|
||||||
|
--background-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
padding: 10px;
|
||||||
|
row-gap: 10px;
|
||||||
|
list-style-type: none;
|
||||||
|
border-top-left-radius: 15px;
|
||||||
|
border-bottom-left-radius: 15px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
border-left: 1px solid var(--border-color);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: var(--background-color);
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
li.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
pointer-events: none;
|
||||||
|
fill: var(--primary-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<menu>
|
||||||
|
<li>
|
||||||
|
<button id="fullscreen">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>overscan</title>
|
||||||
|
<path
|
||||||
|
d="M12 5.5L10 8H14L12 5.5M18 10V14L20.5 12L18 10M6 10L3.5 12L6 14V10M14 16H10L12 18.5L14 16M21 3H3C1.9 3 1 3.9 1 5V19C1 20.1 1.9 21 3 21H21C22.1 21 23 20.1 23 19V5C23 3.9 22.1 3 21 3M21 19H3V5H21V19Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="theme">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>compare</title>
|
||||||
|
<path
|
||||||
|
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>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="navigation">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>web</title>
|
||||||
|
<path
|
||||||
|
d="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
|
||||||
|
/>
|
||||||
|
</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-hidden", (e, button) => {
|
||||||
|
document.getElementById(button.id).parentNode.classList.toggle("hidden", button.hidden);
|
||||||
|
});
|
||||||
|
ipcRenderer.on("data-theme", (e, data) => {
|
||||||
|
document.documentElement.setAttribute("data-theme", data.theme);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerSendEvents();
|
||||||
|
registerReceiveEvents();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 152 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 90 KiB |
@@ -1,10 +1,26 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const crypto = require("crypto");
|
||||||
const readline = require("readline/promises");
|
const readline = require("readline/promises");
|
||||||
const integration = require("./src/integration");
|
const integration = require("./js/integration");
|
||||||
const hardware = require("./src/hardware");
|
const hardware = require("./js/hardware");
|
||||||
const webview = require("./src/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.EVENTS = global.EVENTS || new Events();
|
||||||
|
|
||||||
|
if (!process.env.DISPLAY) {
|
||||||
|
console.error(`\n$DISPLAY variable not set to run the GUI application, are you connected via SSH?\n`);
|
||||||
|
console.error(`If you have installed the service use:`);
|
||||||
|
console.error(` systemctl --user start touchkio.service`);
|
||||||
|
console.error(`Alternatively export the variables first:`);
|
||||||
|
console.error(` export DISPLAY=":0" && export WAYLAND_DISPLAY="wayland-0" && touchkio\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This promise resolves when the app has finished initializing,
|
* This promise resolves when the app has finished initializing,
|
||||||
@@ -12,33 +28,167 @@ const { app } = require("electron");
|
|||||||
* initialization tasks.
|
* initialization tasks.
|
||||||
*/
|
*/
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
let args = parseArgs(process);
|
if (!(await initApp()) || !(await initArgs()) || !(await initLog())) {
|
||||||
let argsProvided = !!Object.keys(args).length;
|
return;
|
||||||
|
|
||||||
let argsFilePath = path.join(app.getPath("userData"), "Arguments.json");
|
|
||||||
let argsFileExists = fs.existsSync(argsFilePath);
|
|
||||||
|
|
||||||
// Setup arguments from file path
|
|
||||||
if ("setup" in args || (!argsProvided && !argsFileExists)) {
|
|
||||||
await sleep(3000);
|
|
||||||
do {
|
|
||||||
args = await promptArgs(process);
|
|
||||||
} while (!Object.keys(args).length);
|
|
||||||
fs.writeFileSync(argsFilePath, JSON.stringify(args, null, 2));
|
|
||||||
} else if (!argsProvided && argsFileExists) {
|
|
||||||
args = JSON.parse(fs.readFileSync(argsFilePath));
|
|
||||||
}
|
}
|
||||||
console.log(JSON.stringify(args, null, 2));
|
|
||||||
|
// Show used arguments
|
||||||
|
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];
|
||||||
for (const init of chained) {
|
for (const init of chained) {
|
||||||
if (!(await init(args))) {
|
if (!(await init())) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
*
|
*
|
||||||
@@ -46,14 +196,13 @@ app.whenReady().then(async () => {
|
|||||||
* @returns {Object} An object mapping argument names to their corresponding values.
|
* @returns {Object} An object mapping argument names to their corresponding values.
|
||||||
*/
|
*/
|
||||||
const parseArgs = (proc) => {
|
const parseArgs = (proc) => {
|
||||||
let args = {};
|
const args = proc.argv.slice(1).filter((arg) => arg !== ".");
|
||||||
proc.argv.slice(1).forEach((arg) => {
|
return Object.fromEntries(
|
||||||
if (arg !== ".") {
|
args.flatMap((arg) => {
|
||||||
const [key, value] = arg.split("=");
|
const match = arg.match(/^--?([^=]+)(?:=(.*))?$/);
|
||||||
args[key.replace("--", "").replace("-", "_")] = value;
|
return match ? [[match[1].replace(/-/g, "_"), match[2] ?? null]] : [];
|
||||||
}
|
}),
|
||||||
});
|
);
|
||||||
return args;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,8 +217,8 @@ const promptArgs = async (proc) => {
|
|||||||
output: proc.stdout,
|
output: proc.stdout,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Array of arguments
|
// Array of prompts
|
||||||
const arguments = [
|
const prompts = [
|
||||||
{
|
{
|
||||||
key: "web_url",
|
key: "web_url",
|
||||||
question: "\nEnter WEB url",
|
question: "\nEnter WEB url",
|
||||||
@@ -85,6 +234,16 @@ const promptArgs = async (proc) => {
|
|||||||
question: "Enter WEB zoom level",
|
question: "Enter WEB zoom level",
|
||||||
fallback: "1.25",
|
fallback: "1.25",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "web_widget",
|
||||||
|
question: "Enter WEB widget enabled",
|
||||||
|
fallback: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "mqtt",
|
||||||
|
question: "\nConnect to MQTT Broker?",
|
||||||
|
fallback: "y/N",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "mqtt_url",
|
key: "mqtt_url",
|
||||||
question: "\nEnter MQTT url",
|
question: "\nEnter MQTT url",
|
||||||
@@ -108,35 +267,127 @@ const promptArgs = async (proc) => {
|
|||||||
{
|
{
|
||||||
key: "check",
|
key: "check",
|
||||||
question: "\nEverything looks good?",
|
question: "\nEverything looks good?",
|
||||||
fallback: "yes",
|
fallback: "Y/n",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Prompt each argument and wait for the answer
|
// Prompt questions and wait for the answers
|
||||||
let args = {};
|
let args = {};
|
||||||
for (const { key, question, fallback } of arguments) {
|
let ignore = [];
|
||||||
if (key === "check") {
|
try {
|
||||||
const json = JSON.stringify(args, null, 2);
|
for (const { key, question, fallback } of prompts) {
|
||||||
const prompt = `${question}\n${json}\n(${fallback}): `;
|
if (key === "mqtt") {
|
||||||
const answer = (await read.question(prompt)).trim() || fallback;
|
const prompt = `${question} (${fallback}): `;
|
||||||
if (!["y", "yes"].includes(answer)) {
|
const answer = await read.question(prompt);
|
||||||
args = {};
|
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
|
||||||
|
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);
|
||||||
|
const prompt = `${question}\n${json}\n(${fallback}): `;
|
||||||
|
const answer = await read.question(prompt);
|
||||||
|
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
|
||||||
|
if (!["y", "yes"].includes(value)) {
|
||||||
|
args = {};
|
||||||
|
}
|
||||||
|
} else if (!ignore.includes(key)) {
|
||||||
|
const prompt = `${question} (${fallback}): `;
|
||||||
|
const answer = await read.question(prompt);
|
||||||
|
const value = answer.trim() || fallback;
|
||||||
|
if (key === "web_url") {
|
||||||
|
args[key] = value.split(",").map((v) => v.trim());
|
||||||
|
} else {
|
||||||
|
args[key] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const prompt = `${question} (${fallback}): `;
|
|
||||||
const answer = (await read.question(prompt)).trim() || fallback;
|
|
||||||
args[key] = answer;
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`\n${error.message}`);
|
||||||
|
args = {};
|
||||||
|
app.exit(1);
|
||||||
|
} finally {
|
||||||
|
read.close();
|
||||||
}
|
}
|
||||||
read.close();
|
|
||||||
|
|
||||||
return args;
|
return args;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes argument values to the filesystem.
|
||||||
|
*
|
||||||
|
* @param {string} path - Path of the .json file.
|
||||||
|
* @param {Object} args - The arguments object.
|
||||||
|
*/
|
||||||
|
const writeArgs = (path, args) => {
|
||||||
|
try {
|
||||||
|
const argc = Object.assign({}, args);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads argument values from the filesystem.
|
||||||
|
*
|
||||||
|
* @param {string} path - Path of the .json file.
|
||||||
|
* @returns {Object} The arguments object.
|
||||||
|
*/
|
||||||
|
const readArgs = (path) => {
|
||||||
|
try {
|
||||||
|
const args = JSON.parse(fs.readFileSync(path, "utf8"));
|
||||||
|
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 {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for string encryption.
|
||||||
|
*
|
||||||
|
* @param {string} value - Plain text value.
|
||||||
|
* @returns {string} Encrypted value.
|
||||||
|
*/
|
||||||
|
const encrypt = (value) => {
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
const key = crypto.scryptSync(hardware.getMachineId(), APP.name, 32);
|
||||||
|
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
||||||
|
let encrypted = cipher.update(value, "utf8", "hex");
|
||||||
|
encrypted += cipher.final("hex");
|
||||||
|
return Buffer.from(iv.toString("hex") + ":" + encrypted).toString("base64");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for string decryption.
|
||||||
|
*
|
||||||
|
* @param {string} value - Encrypted value.
|
||||||
|
* @returns {string} Plain text value.
|
||||||
|
*/
|
||||||
|
const decrypt = (value) => {
|
||||||
|
const p = Buffer.from(value, "base64").toString("utf8").split(":");
|
||||||
|
const iv = Buffer.from(p.shift(), "hex");
|
||||||
|
const key = crypto.scryptSync(hardware.getMachineId(), APP.name, 32);
|
||||||
|
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
|
||||||
|
const buffer = Buffer.from(p.join(":"), "hex");
|
||||||
|
let decrypted = decipher.update(buffer, "binary", "utf8");
|
||||||
|
decrypted += decipher.final("utf8");
|
||||||
|
return decrypted;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function for asynchronous sleep.
|
* Helper function for asynchronous sleep.
|
||||||
*
|
*
|
||||||
* @param {number} ms - Sleep time in milliseconds.
|
* @param {number} ms - Sleep time in milliseconds.
|
||||||
* @returns {Promise} A promise resolving after the timeout.
|
* @returns {Promise} A promise resolving after the timeout.
|
||||||
*/
|
*/
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms) => {
|
||||||
|
return new Promise((r) => setTimeout(r, ms));
|
||||||
|
};
|
||||||
|
|||||||
Executable
+123
@@ -0,0 +1,123 @@
|
|||||||
|
#!/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
|
||||||
|
echo -e "Determining system architecture..."
|
||||||
|
|
||||||
|
BITS=$(getconf LONG_BIT)
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64) ARCH="x64" ;;
|
||||||
|
aarch64) ARCH="arm64" ;;
|
||||||
|
*) { echo "Architecture $(uname -m) running $BITS-bit operating system is not supported."; exit 1; } ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ "$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 supported."
|
||||||
|
|
||||||
|
# Download the latest .deb package
|
||||||
|
echo -e "\nDownloading the latest release..."
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
chmod 755 "$TMP_DIR"
|
||||||
|
|
||||||
|
JSON=$(wget -qO- "https://api.github.com/repos/leukipp/touchkio/releases" | tr -d '\r\n')
|
||||||
|
if $ARG_EARLY; then
|
||||||
|
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
|
||||||
|
|
||||||
|
DEB_URL=$(echo "$JSON" | grep -oP "$DEB_REG" | head -n 1)
|
||||||
|
DEB_PATH="${TMP_DIR}/$(basename "$DEB_URL")"
|
||||||
|
|
||||||
|
[ -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
|
||||||
|
echo -e "\nInstalling the latest release..."
|
||||||
|
|
||||||
|
command -v apt &> /dev/null || { echo "Package manager apt was not found."; exit 1; }
|
||||||
|
sudo apt install -y "$DEB_PATH" || { echo "Installation of .deb file failed."; exit 1; }
|
||||||
|
|
||||||
|
# Create the systemd user service
|
||||||
|
echo -e "\nCreating systemd user service..."
|
||||||
|
|
||||||
|
SERVICE_NAME="touchkio.service"
|
||||||
|
SERVICE_FILE="$HOME/.config/systemd/user/$SERVICE_NAME"
|
||||||
|
mkdir -p "$(dirname "$SERVICE_FILE")" || { echo "Failed to create directory for $SERVICE_FILE."; exit 1; }
|
||||||
|
|
||||||
|
SERVICE_CONTENT="[Unit]
|
||||||
|
Description=TouchKio
|
||||||
|
After=graphical.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/touchkio
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target"
|
||||||
|
|
||||||
|
if $ARG_UPDATE; then
|
||||||
|
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
|
||||||
|
echo -e "\nExporting display variables..."
|
||||||
|
|
||||||
|
if [ -z "$DISPLAY" ]; then
|
||||||
|
export DISPLAY=":0"
|
||||||
|
echo "DISPLAY was not set, defaulting to \"$DISPLAY\"."
|
||||||
|
else
|
||||||
|
echo "DISPLAY is set to \"$DISPLAY\"."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$WAYLAND_DISPLAY" ]; then
|
||||||
|
export WAYLAND_DISPLAY="wayland-0"
|
||||||
|
echo "WAYLAND_DISPLAY was not set, defaulting to \"$WAYLAND_DISPLAY\"."
|
||||||
|
else
|
||||||
|
echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\"."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the setup mode
|
||||||
|
read -p $'\nStart touchkio setup? (Y/n) ' setup
|
||||||
|
|
||||||
|
if [[ ${setup:-y} == [Yy]* ]]; then
|
||||||
|
echo "/usr/bin/touchkio --setup"
|
||||||
|
/usr/bin/touchkio --setup
|
||||||
|
else
|
||||||
|
echo "/usr/bin/touchkio"
|
||||||
|
/usr/bin/touchkio
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
+1066
File diff suppressed because it is too large
Load Diff
+1141
File diff suppressed because it is too large
Load Diff
+1209
File diff suppressed because it is too large
Load Diff
+17
-48
@@ -1,62 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "touchkio",
|
"name": "touchkio",
|
||||||
|
"title": "TouchKio",
|
||||||
"description": "Home Assistant Touch Kiosk",
|
"description": "Home Assistant Touch Kiosk",
|
||||||
|
"homepage": "https://github.com/leukipp/touchkio",
|
||||||
"author": "leukipp",
|
"author": "leukipp",
|
||||||
"version": "0.0.2",
|
"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",
|
"start": "electron . --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app --trace-warnings",
|
||||||
"build": "electron-forge make"
|
"build": "electron-forge make"
|
||||||
},
|
},
|
||||||
"config": {
|
"prettier": {
|
||||||
"forge": {
|
"printWidth": 120
|
||||||
"packagerConfig": {
|
|
||||||
"ignore": [
|
|
||||||
".gitignore",
|
|
||||||
".github/",
|
|
||||||
"docs/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"makers": [
|
|
||||||
{
|
|
||||||
"name": "@electron-forge/maker-zip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "@electron-forge/maker-deb",
|
|
||||||
"config": {
|
|
||||||
"options": {
|
|
||||||
"productName": "TouchKio",
|
|
||||||
"icon": "img/icon.png",
|
|
||||||
"categories": [
|
|
||||||
"Network"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"publishers": [
|
|
||||||
{
|
|
||||||
"name": "@electron-forge/publisher-github",
|
|
||||||
"config": {
|
|
||||||
"repository": {
|
|
||||||
"owner": "leukipp",
|
|
||||||
"name": "touchkio"
|
|
||||||
},
|
|
||||||
"draft": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.6.0",
|
"@electron-forge/cli": "^7.10.0",
|
||||||
"@electron-forge/maker-deb": "^7.6.0",
|
"@electron-forge/maker-deb": "^7.10.0",
|
||||||
"@electron-forge/maker-zip": "^7.6.0",
|
"@electron-forge/maker-zip": "^7.10.0",
|
||||||
"@electron-forge/publisher-github": "^7.6.0",
|
"@electron-forge/publisher-github": "^7.10.0",
|
||||||
"electron": "^33.2.1"
|
"electron": "^38.2.2",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"prettier": "^3.6.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mqtt": "^5.10.3"
|
"axios": "^1.12.2",
|
||||||
|
"electron-log": "^5.4.3",
|
||||||
|
"mqtt": "^5.14.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-379
@@ -1,379 +0,0 @@
|
|||||||
const os = require("os");
|
|
||||||
const fs = require("fs");
|
|
||||||
const process = require("child_process");
|
|
||||||
|
|
||||||
global.HARDWARE = global.HARDWARE || {
|
|
||||||
initialized: false,
|
|
||||||
status: "invalid",
|
|
||||||
display: {
|
|
||||||
status: null,
|
|
||||||
brightness: null,
|
|
||||||
notifiers: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the hardware with the provided arguments.
|
|
||||||
*
|
|
||||||
* @param {Object} args - The command-line arguments to customize the initialization process.
|
|
||||||
* @returns {boolean} Returns true if the initialization was successful.
|
|
||||||
*/
|
|
||||||
const init = async (args) => {
|
|
||||||
if (!args.mqtt_url) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check supported hardware
|
|
||||||
if (!hardwareExists()) {
|
|
||||||
console.warn("Hardware not supported");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check supported model
|
|
||||||
const model = getModel();
|
|
||||||
if (!model || !model.includes("Raspberry Pi 5")) {
|
|
||||||
console.warn(`Device ${model} not supported`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\nModel:", getModel());
|
|
||||||
console.log("Serial Number:", getSerialNumber());
|
|
||||||
console.log("Host Name:", getHostName());
|
|
||||||
console.log("Up Time:", getUpTime());
|
|
||||||
console.log("Memory Size:", getMemorySize());
|
|
||||||
console.log("Memory Usage:", getMemoryUsage());
|
|
||||||
console.log("Processor Usage:", getProcessorUsage());
|
|
||||||
console.log("Processor Temperature:", getProcessorTemperature());
|
|
||||||
console.log("Display Status:", getDisplayStatus());
|
|
||||||
console.log("Display Brightness:", getDisplayBrightness(), "\n");
|
|
||||||
|
|
||||||
// Init globals
|
|
||||||
HARDWARE.initialized = true;
|
|
||||||
HARDWARE.status = "valid";
|
|
||||||
|
|
||||||
// Check for display changes
|
|
||||||
setInterval(update, 500);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the shared hardware properties.
|
|
||||||
*/
|
|
||||||
const update = () => {
|
|
||||||
if (!HARDWARE.initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const path = "/sys/class/backlight/10-0045";
|
|
||||||
|
|
||||||
// Display status has changed
|
|
||||||
const status = fs.readFileSync(`${path}/bl_power`, "utf8").trim();
|
|
||||||
if (status !== HARDWARE.display.status) {
|
|
||||||
console.log("Update Display Status:", getDisplayStatus());
|
|
||||||
HARDWARE.display.status = status;
|
|
||||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
|
||||||
notifier();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display brightness has changed
|
|
||||||
const brightness = fs.readFileSync(`${path}/brightness`, "utf8").trim();
|
|
||||||
if (brightness !== HARDWARE.display.brightness) {
|
|
||||||
console.log("Update Display Brightness:", getDisplayBrightness());
|
|
||||||
HARDWARE.display.brightness = brightness;
|
|
||||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
|
||||||
notifier();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies hardware compatibility by checking the presence of necessary sys files.
|
|
||||||
*
|
|
||||||
* @returns {bool} Returns true if all files exists.
|
|
||||||
*/
|
|
||||||
const hardwareExists = () => {
|
|
||||||
const files = [
|
|
||||||
"/sys/firmware/devicetree/base/model",
|
|
||||||
"/sys/firmware/devicetree/base/serial-number",
|
|
||||||
"/sys/class/backlight/10-0045/brightness",
|
|
||||||
"/sys/class/backlight/10-0045/bl_power",
|
|
||||||
];
|
|
||||||
return files.every((file) => fs.existsSync(file));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the Raspberry Pi model name using `/sys/firmware/devicetree/base/model`.
|
|
||||||
*
|
|
||||||
* @returns {string|null} The model of the Raspberry Pi or null if an error occurs.
|
|
||||||
*/
|
|
||||||
const getModel = () => {
|
|
||||||
return execSyncCommand("cat", ["/sys/firmware/devicetree/base/model"]);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the Raspberry Pi serial number using `/sys/firmware/devicetree/base/serial-number`.
|
|
||||||
*
|
|
||||||
* @returns {string|null} The serial number of the Raspberry Pi or null if an error occurs.
|
|
||||||
*/
|
|
||||||
const getSerialNumber = () => {
|
|
||||||
return execSyncCommand("cat", [
|
|
||||||
"/sys/firmware/devicetree/base/serial-number",
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the host name of the current system using `os.hostname()`.
|
|
||||||
*
|
|
||||||
* @returns {string} The host name of the system.
|
|
||||||
*/
|
|
||||||
const getHostName = () => {
|
|
||||||
return os.hostname();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the up time of the system in minutes using `os.uptime()`.
|
|
||||||
*
|
|
||||||
* @returns {number} The up time of the system in minutes.
|
|
||||||
*/
|
|
||||||
const getUpTime = () => {
|
|
||||||
return os.uptime() / 60;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the total available memory in gibibytes using `os.totalmem()`.
|
|
||||||
*
|
|
||||||
* @returns {number} The total available memory in GiB.
|
|
||||||
*/
|
|
||||||
const getMemorySize = () => {
|
|
||||||
return os.totalmem() / 1024 ** 3;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current memory usage as a percentage using `os.totalmem()` and `os.freemem()`.
|
|
||||||
*
|
|
||||||
* @returns {number} The percentage of used memory.
|
|
||||||
*/
|
|
||||||
const getMemoryUsage = () => {
|
|
||||||
return ((os.totalmem() - os.freemem()) / os.totalmem()) * 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the CPU load average over the last 5 minutes as a percentage using `os.loadavg()` and `os.cpus()`.
|
|
||||||
*
|
|
||||||
* @returns {number} The CPU load average percentage over the last 5 minutes.
|
|
||||||
*/
|
|
||||||
const getProcessorUsage = () => {
|
|
||||||
return (os.loadavg()[1] / os.cpus().length) * 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current CPU temperature using `vcgencmd measure_temp`.
|
|
||||||
*
|
|
||||||
* @returns {number|null} The CPU temperature in degrees Celsius or null if an error occurs.
|
|
||||||
*/
|
|
||||||
const getProcessorTemperature = () => {
|
|
||||||
const output = execSyncCommand("vcgencmd", ["measure_temp"]);
|
|
||||||
if (output !== null) {
|
|
||||||
const temp = output.match(/temp=(\d+\.\d+)/);
|
|
||||||
if (temp && temp[1]) {
|
|
||||||
return parseFloat(temp[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current display power status using `wlopm`.
|
|
||||||
*
|
|
||||||
* @returns {string|null} The output of the command or null if an error occurs.
|
|
||||||
*/
|
|
||||||
const getDisplayStatus = () => {
|
|
||||||
const status = execSyncCommand("wlopm", []);
|
|
||||||
if (status !== null) {
|
|
||||||
return status.slice(status.lastIndexOf(" ") + 1).toUpperCase();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
|
|
||||||
/* This also works, but it's better to use `wlopm` for getter and setter methods.
|
|
||||||
const status = execSyncCommand("cat", [
|
|
||||||
"/sys/class/backlight/10-0045/bl_power",
|
|
||||||
]);
|
|
||||||
if (status !== null) {
|
|
||||||
return status === "0" ? "ON" : "OFF";
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the display power status using `wlopm`.
|
|
||||||
*
|
|
||||||
* This function takes a desired status ('ON' or 'OFF') and executes
|
|
||||||
* the appropriate command to set the display status.
|
|
||||||
*
|
|
||||||
* @param {string} status - The desired status ('ON' or 'OFF').
|
|
||||||
* @param {function} callback - A callback function that receives the output or error.
|
|
||||||
*/
|
|
||||||
const setDisplayStatus = (status, callback = null) => {
|
|
||||||
if (status !== "ON" && status !== "OFF") {
|
|
||||||
console.error("Status must be 'ON' or 'OFF'");
|
|
||||||
if (typeof callback === "function") callback(null, "Invalid status");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const args = status === "ON" ? ["--on", "*"] : ["--off", "*"];
|
|
||||||
execAsyncCommand("wlopm", args, callback);
|
|
||||||
|
|
||||||
/* This also works, but interferes with the built-in screen blanking function.
|
|
||||||
const value = status === "ON" ? "0" : "4";
|
|
||||||
const proc = execAsyncCommand(
|
|
||||||
"tee",
|
|
||||||
["/sys/class/backlight/10-0045/bl_power"],
|
|
||||||
callback
|
|
||||||
);
|
|
||||||
proc.stdin.write(value.toString());
|
|
||||||
proc.stdin.end();
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current display brightness level using `/sys/class/backlight/10-0045/brightness`.
|
|
||||||
*
|
|
||||||
* This function retrieves the brightness level, which is in the range of 0-31,
|
|
||||||
* and maps it to a percentage in the range of 0-100 percent.
|
|
||||||
*
|
|
||||||
* @returns {number|null} The brightness level as a percentage or null if an error occurs.
|
|
||||||
*/
|
|
||||||
const getDisplayBrightness = () => {
|
|
||||||
const brightness = execSyncCommand("cat", [
|
|
||||||
"/sys/class/backlight/10-0045/brightness",
|
|
||||||
]);
|
|
||||||
if (brightness !== null) {
|
|
||||||
return Math.round((parseInt(brightness, 10) / 31) * 100);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the display brightness level using `/sys/class/backlight/10-0045/brightness`.
|
|
||||||
*
|
|
||||||
* This function takes a brightness value in the range of 1-100 percent,
|
|
||||||
* maps it to the range of 0-31, and writes it to the system.
|
|
||||||
*
|
|
||||||
* @param {number} brightness - The desired brightness level (1-100).
|
|
||||||
* @param {function} callback - A callback function that receives the output or error.
|
|
||||||
*/
|
|
||||||
const setDisplayBrightness = (brightness, callback = null) => {
|
|
||||||
if (typeof brightness !== "number" || brightness < 1 || brightness > 100) {
|
|
||||||
console.error("Brightness must be a number between 1 and 100");
|
|
||||||
if (typeof callback === "function") callback(null, "Invalid brightness");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const value = Math.max(1, Math.min(Math.round((brightness / 100) * 31), 31));
|
|
||||||
const proc = execAsyncCommand(
|
|
||||||
"tee",
|
|
||||||
["/sys/class/backlight/10-0045/brightness"],
|
|
||||||
callback
|
|
||||||
);
|
|
||||||
proc.stdin.write(value.toString());
|
|
||||||
proc.stdin.end();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shuts down the system using `sudo shutdown -h now`.
|
|
||||||
*
|
|
||||||
* This function executes the command asynchronously.
|
|
||||||
* The output of the command will be provided through the callback function.
|
|
||||||
*
|
|
||||||
* @param {function} callback - A callback function that receives the output or error.
|
|
||||||
*/
|
|
||||||
const shutdownSystem = (callback = null) => {
|
|
||||||
execAsyncCommand("sudo", ["shutdown", "-h", "now"], callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reboots the system using `sudo reboot`.
|
|
||||||
*
|
|
||||||
* This function executes the command asynchronously.
|
|
||||||
* The output of the command will be provided through the callback function.
|
|
||||||
*
|
|
||||||
* @param {function} callback - A callback function that receives the output or error.
|
|
||||||
*/
|
|
||||||
const rebootSystem = (callback = null) => {
|
|
||||||
execAsyncCommand("sudo", ["reboot"], callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes a command synchronously and returns the output.
|
|
||||||
*
|
|
||||||
* @param {string} cmd - The command to execute.
|
|
||||||
* @param {Array<string>} args - The arguments for the command.
|
|
||||||
* @returns {string|null} The output of the command or null if an error occurs.
|
|
||||||
*/
|
|
||||||
const execSyncCommand = (cmd, args = []) => {
|
|
||||||
try {
|
|
||||||
const output = process.execSync([cmd, ...args].join(" "), {
|
|
||||||
encoding: "utf8",
|
|
||||||
});
|
|
||||||
return output.trim().replace(/\0/g, "");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Execute Sync:", error.message);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes a command asynchronously.
|
|
||||||
*
|
|
||||||
* @param {string} cmd - The command to execute.
|
|
||||||
* @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 execAsyncCommand = (cmd, args = [], callback = null) => {
|
|
||||||
let errorOutput = "";
|
|
||||||
let successOutput = "";
|
|
||||||
let proc = process.spawn(cmd, args);
|
|
||||||
proc.stderr.on("data", (data) => {
|
|
||||||
if (data) {
|
|
||||||
errorOutput += data.toString();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
proc.stdout.on("data", (data) => {
|
|
||||||
if (data) {
|
|
||||||
successOutput += data.toString();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
proc.on("close", (code) => {
|
|
||||||
try {
|
|
||||||
if (typeof callback === "function") {
|
|
||||||
if (code === 0) {
|
|
||||||
callback(successOutput.trim().replace(/\0/g, ""), null);
|
|
||||||
} else {
|
|
||||||
callback(null, errorOutput.trim().replace(/\0/g, ""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Execute Async:", error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return proc;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
init,
|
|
||||||
update,
|
|
||||||
getModel,
|
|
||||||
getSerialNumber,
|
|
||||||
getHostName,
|
|
||||||
getUpTime,
|
|
||||||
getMemorySize,
|
|
||||||
getMemoryUsage,
|
|
||||||
getProcessorUsage,
|
|
||||||
getProcessorTemperature,
|
|
||||||
getDisplayStatus,
|
|
||||||
setDisplayStatus,
|
|
||||||
getDisplayBrightness,
|
|
||||||
setDisplayBrightness,
|
|
||||||
shutdownSystem,
|
|
||||||
rebootSystem,
|
|
||||||
};
|
|
||||||
@@ -1,677 +0,0 @@
|
|||||||
const mqtt = require("mqtt");
|
|
||||||
const hardware = require("./hardware");
|
|
||||||
const { app } = require("electron");
|
|
||||||
|
|
||||||
global.INTEGRATION = global.INTEGRATION || {
|
|
||||||
initialized: false,
|
|
||||||
status: "offline",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the integration with the provided arguments.
|
|
||||||
*
|
|
||||||
* @param {Object} args - The command-line arguments to customize the initialization process.
|
|
||||||
* @returns {boolean} Returns true if the initialization was successful.
|
|
||||||
*/
|
|
||||||
const init = async (args) => {
|
|
||||||
if (!args.mqtt_url) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const url = new URL(args.mqtt_url);
|
|
||||||
const user = args.mqtt_user ? args.mqtt_user : null;
|
|
||||||
const password = args.mqtt_password ? args.mqtt_password : null;
|
|
||||||
const discovery = args.mqtt_discovery ? args.mqtt_discovery : "homeassistant";
|
|
||||||
|
|
||||||
const model = hardware.getModel();
|
|
||||||
const hostName = hardware.getHostName();
|
|
||||||
const serialNumber = hardware.getSerialNumber();
|
|
||||||
|
|
||||||
const deviceId = serialNumber.slice(-6).toUpperCase();
|
|
||||||
const deviceName = hostName.charAt(0).toUpperCase() + hostName.slice(1);
|
|
||||||
|
|
||||||
// Init globals
|
|
||||||
INTEGRATION.node = `rpi_${deviceId}`;
|
|
||||||
INTEGRATION.discovery = discovery;
|
|
||||||
INTEGRATION.device = {
|
|
||||||
name: `Raspberry Pi ${deviceName}`,
|
|
||||||
model: model,
|
|
||||||
serial_number: serialNumber,
|
|
||||||
manufacturer: "Raspberry Pi Ltd",
|
|
||||||
identifiers: [INTEGRATION.node],
|
|
||||||
sw_version: `${app.getName()}-v${app.getVersion()}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Connection settings
|
|
||||||
const masked = password === null ? "null" : "*".repeat(password.length);
|
|
||||||
const options =
|
|
||||||
user === null || password == null
|
|
||||||
? null
|
|
||||||
: {
|
|
||||||
username: user,
|
|
||||||
password: password,
|
|
||||||
};
|
|
||||||
console.log("MQTT Connecting:", `${user}:${masked}@${url.toString()}`);
|
|
||||||
INTEGRATION.client = mqtt.connect(url.toString(), options);
|
|
||||||
|
|
||||||
// Client connected
|
|
||||||
INTEGRATION.client
|
|
||||||
.on("connect", () => {
|
|
||||||
console.log(`MQTT Connected: ${url.toString()}\n`);
|
|
||||||
|
|
||||||
// Init client controls
|
|
||||||
initShutdown(INTEGRATION.client);
|
|
||||||
initReboot(INTEGRATION.client);
|
|
||||||
initRefresh(INTEGRATION.client);
|
|
||||||
initKiosk(INTEGRATION.client);
|
|
||||||
initDisplay(INTEGRATION.client);
|
|
||||||
|
|
||||||
// Init client sensors
|
|
||||||
initModel(INTEGRATION.client);
|
|
||||||
initSerialNumber(INTEGRATION.client);
|
|
||||||
initHostName(INTEGRATION.client);
|
|
||||||
initUpTime(INTEGRATION.client);
|
|
||||||
initMemorySize(INTEGRATION.client);
|
|
||||||
initMemoryUsage(INTEGRATION.client);
|
|
||||||
initProcessorUsage(INTEGRATION.client);
|
|
||||||
initProcessorTemperature(INTEGRATION.client);
|
|
||||||
initHeartbeat(INTEGRATION.client);
|
|
||||||
|
|
||||||
// Integration initialized
|
|
||||||
INTEGRATION.initialized = true;
|
|
||||||
INTEGRATION.status = "online";
|
|
||||||
})
|
|
||||||
.on("error", (error) => {
|
|
||||||
console.error("MQTT:", error);
|
|
||||||
INTEGRATION.status = "offline";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update sensor states on display change
|
|
||||||
HARDWARE.display.notifiers.push(() => {
|
|
||||||
updateDisplay(INTEGRATION.client);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update heartbeat time periodically
|
|
||||||
setInterval(() => {
|
|
||||||
updateHeartbeat(INTEGRATION.client);
|
|
||||||
}, 30 * 1000);
|
|
||||||
|
|
||||||
// Update sensor states periodically
|
|
||||||
setInterval(update, 60 * 1000);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the shared integration properties.
|
|
||||||
*/
|
|
||||||
const update = () => {
|
|
||||||
if (!INTEGRATION.initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update client sensors
|
|
||||||
updateKiosk(INTEGRATION.client);
|
|
||||||
updateUpTime(INTEGRATION.client);
|
|
||||||
updateMemoryUsage(INTEGRATION.client);
|
|
||||||
updateProcessorUsage(INTEGRATION.client);
|
|
||||||
updateProcessorTemperature(INTEGRATION.client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the shutdown button and handles the execute logic.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initShutdown = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/button/${INTEGRATION.node}/shutdown`;
|
|
||||||
client
|
|
||||||
.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: "Shutdown",
|
|
||||||
unique_id: `${INTEGRATION.node}_shutdown`,
|
|
||||||
command_topic: `${root}/execute`,
|
|
||||||
icon: "mdi:power",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
)
|
|
||||||
.on("message", (topic, message) => {
|
|
||||||
if (topic === `${root}/execute`) {
|
|
||||||
console.log("Shutdown system...");
|
|
||||||
hardware.setDisplayStatus("ON");
|
|
||||||
hardware.shutdownSystem();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.subscribe(`${root}/execute`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the reboot button and handles the execute logic.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initReboot = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/button/${INTEGRATION.node}/reboot`;
|
|
||||||
client
|
|
||||||
.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: "Reboot",
|
|
||||||
unique_id: `${INTEGRATION.node}_reboot`,
|
|
||||||
command_topic: `${root}/execute`,
|
|
||||||
icon: "mdi:restart",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
)
|
|
||||||
.on("message", (topic, message) => {
|
|
||||||
if (topic === `${root}/execute`) {
|
|
||||||
console.log("Rebooting system...");
|
|
||||||
hardware.setDisplayStatus("ON");
|
|
||||||
hardware.rebootSystem();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.subscribe(`${root}/execute`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the refresh button and handles the execute logic.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initRefresh = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/button/${INTEGRATION.node}/refresh`;
|
|
||||||
client
|
|
||||||
.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: "Refresh",
|
|
||||||
unique_id: `${INTEGRATION.node}_refresh`,
|
|
||||||
command_topic: `${root}/execute`,
|
|
||||||
icon: "mdi:web-refresh",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
)
|
|
||||||
.on("message", (topic, message) => {
|
|
||||||
if (topic === `${root}/execute`) {
|
|
||||||
console.log("Refreshing webview...");
|
|
||||||
hardware.setDisplayStatus("ON");
|
|
||||||
WEBVIEW.view.webContents.reloadIgnoringCache();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.subscribe(`${root}/execute`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the kiosk select status and handles the execute logic.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initKiosk = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/select/${INTEGRATION.node}/kiosk`;
|
|
||||||
client
|
|
||||||
.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: `Kiosk`,
|
|
||||||
unique_id: `${INTEGRATION.node}_kiosk`,
|
|
||||||
command_topic: `${root}/set`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ value }}",
|
|
||||||
options: [
|
|
||||||
"Framed",
|
|
||||||
"Fullscreen",
|
|
||||||
"Maximized",
|
|
||||||
"Minimized",
|
|
||||||
"Terminated",
|
|
||||||
],
|
|
||||||
icon: "mdi:overscan",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
)
|
|
||||||
.on("message", (topic, message) => {
|
|
||||||
if (topic === `${root}/set`) {
|
|
||||||
const status = message.toString();
|
|
||||||
console.log("Set Kiosk Status:", status);
|
|
||||||
hardware.setDisplayStatus("ON");
|
|
||||||
switch (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(`${root}/set`);
|
|
||||||
updateKiosk(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the kiosk status via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateKiosk = (client) => {
|
|
||||||
const kiosk = WEBVIEW.status;
|
|
||||||
const root = `${INTEGRATION.discovery}/select/${INTEGRATION.node}/kiosk`;
|
|
||||||
client.publish(`${root}/status`, `${kiosk}`, {
|
|
||||||
qos: 1,
|
|
||||||
retain: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the display status, brightness and handles the execute logic.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initDisplay = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`;
|
|
||||||
client
|
|
||||||
.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: `Display`,
|
|
||||||
unique_id: `${INTEGRATION.node}_display`,
|
|
||||||
command_topic: `${root}/set`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
brightness_command_topic: `${root}/brightness/set`,
|
|
||||||
brightness_state_topic: `${root}/brightness/status`,
|
|
||||||
brightness_scale: 100,
|
|
||||||
icon: "mdi:monitor-shimmer",
|
|
||||||
platform: "light",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
)
|
|
||||||
.on("message", (topic, message) => {
|
|
||||||
if (topic === `${root}/set`) {
|
|
||||||
const status = message.toString();
|
|
||||||
console.log("Set Display Status:", status);
|
|
||||||
hardware.setDisplayStatus(status);
|
|
||||||
} else if (topic === `${root}/brightness/set`) {
|
|
||||||
const brightness = parseInt(message, 10);
|
|
||||||
console.log("Set Display Brightness:", brightness);
|
|
||||||
hardware.setDisplayBrightness(brightness);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.subscribe(`${root}/set`)
|
|
||||||
.subscribe(`${root}/brightness/set`);
|
|
||||||
updateDisplay(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the display status, brightness via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateDisplay = (client) => {
|
|
||||||
const status = hardware.getDisplayStatus();
|
|
||||||
const brightness = hardware.getDisplayBrightness();
|
|
||||||
const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`;
|
|
||||||
client.publish(`${root}/status`, `${status}`, {
|
|
||||||
qos: 1,
|
|
||||||
retain: true,
|
|
||||||
});
|
|
||||||
client.publish(`${root}/brightness/status`, `${brightness}`, {
|
|
||||||
qos: 1,
|
|
||||||
retain: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the model sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initModel = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/model`;
|
|
||||||
client.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: `Model`,
|
|
||||||
unique_id: `${INTEGRATION.node}_model`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ value }}",
|
|
||||||
icon: "mdi:raspberry-pi",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateModel(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the model sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateModel = (client) => {
|
|
||||||
const model = hardware.getModel();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/model`;
|
|
||||||
client.publish(`${root}/status`, `${model}`, {
|
|
||||||
qos: 1,
|
|
||||||
retain: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the serial number sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initSerialNumber = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/serial_number`;
|
|
||||||
client.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: `Serial Number`,
|
|
||||||
unique_id: `${INTEGRATION.node}_serial_number`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ value }}",
|
|
||||||
icon: "mdi:hexadecimal",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateSerialNumber(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the serial number sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateSerialNumber = (client) => {
|
|
||||||
const serialNumber = hardware.getSerialNumber();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/serial_number`;
|
|
||||||
client.publish(`${root}/status`, `${serialNumber}`, {
|
|
||||||
qos: 1,
|
|
||||||
retain: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the host name sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initHostName = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/host_name`;
|
|
||||||
client.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: `Host Name`,
|
|
||||||
unique_id: `${INTEGRATION.node}_host_name`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ value }}",
|
|
||||||
icon: "mdi:console-network",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateHostName(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the host name sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateHostName = (client) => {
|
|
||||||
const hostName = hardware.getHostName();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/host_name`;
|
|
||||||
client.publish(`${root}/status`, `${hostName}`, {
|
|
||||||
qos: 1,
|
|
||||||
retain: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the up time sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initUpTime = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/up_time`;
|
|
||||||
client.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: `Up Time`,
|
|
||||||
unique_id: `${INTEGRATION.node}_up_time`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ (value | float) | round(0) }}",
|
|
||||||
unit_of_measurement: "min",
|
|
||||||
icon: "mdi:timeline-clock",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateUpTime(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the up time sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateUpTime = (client) => {
|
|
||||||
const upTime = hardware.getUpTime();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/up_time`;
|
|
||||||
client.publish(`${root}/status`, `${upTime}`, {
|
|
||||||
qos: 1,
|
|
||||||
retain: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the memory size sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initMemorySize = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_size`;
|
|
||||||
client.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: `Memory Size`,
|
|
||||||
unique_id: `${INTEGRATION.node}_memory_size`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ (value | float) | round(2) }}",
|
|
||||||
unit_of_measurement: "GiB",
|
|
||||||
icon: "mdi:memory",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateMemorySize(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the memory size sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateMemorySize = (client) => {
|
|
||||||
const memorySize = hardware.getMemorySize();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_size`;
|
|
||||||
client.publish(`${root}/status`, `${memorySize}`, {
|
|
||||||
qos: 1,
|
|
||||||
retain: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the memory usage sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initMemoryUsage = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_usage`;
|
|
||||||
client.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: `Memory Usage`,
|
|
||||||
unique_id: `${INTEGRATION.node}_memory_usage`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ (value | float) | round(0) }}",
|
|
||||||
unit_of_measurement: "%",
|
|
||||||
icon: "mdi:memory-arrow-down",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateMemoryUsage(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the memory usage sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateMemoryUsage = (client) => {
|
|
||||||
const memoryUsage = hardware.getMemoryUsage();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_usage`;
|
|
||||||
client.publish(`${root}/status`, `${memoryUsage}`, {
|
|
||||||
qos: 1,
|
|
||||||
retain: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the processor usage sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initProcessorUsage = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_usage`;
|
|
||||||
client.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: `Processor Usage`,
|
|
||||||
unique_id: `${INTEGRATION.node}_processor_usage`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ (value | float) | round(0) }}",
|
|
||||||
unit_of_measurement: "%",
|
|
||||||
icon: "mdi:cpu-64-bit",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateProcessorUsage(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the processor usage sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateProcessorUsage = (client) => {
|
|
||||||
const processorUsage = hardware.getProcessorUsage();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_usage`;
|
|
||||||
client.publish(`${root}/status`, `${processorUsage}`, {
|
|
||||||
qos: 1,
|
|
||||||
retain: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the processor temperature sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initProcessorTemperature = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_temperature`;
|
|
||||||
client.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: `Processor Temperature`,
|
|
||||||
unique_id: `${INTEGRATION.node}_processor_temperature`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ (value | float) | round(0) }}",
|
|
||||||
unit_of_measurement: "°C",
|
|
||||||
icon: "mdi:radiator",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateProcessorTemperature(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the processor temperature sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateProcessorTemperature = (client) => {
|
|
||||||
const processorTemperature = hardware.getProcessorTemperature();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_temperature`;
|
|
||||||
client.publish(`${root}/status`, `${processorTemperature}`, {
|
|
||||||
qos: 1,
|
|
||||||
retain: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the heartbeat sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initHeartbeat = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/heartbeat`;
|
|
||||||
client.publish(
|
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: `Heartbeat`,
|
|
||||||
unique_id: `${INTEGRATION.node}_heartbeat`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ value }}",
|
|
||||||
icon: "mdi:heart-flash",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateHeartbeat(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the heartbeat sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateHeartbeat = (client) => {
|
|
||||||
const now = new Date();
|
|
||||||
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000);
|
|
||||||
const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, "");
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/heartbeat`;
|
|
||||||
client.publish(`${root}/status`, `${heartbeat}`, {
|
|
||||||
qos: 1,
|
|
||||||
retain: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
init,
|
|
||||||
update,
|
|
||||||
};
|
|
||||||
-159
@@ -1,159 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const hardware = require("./hardware");
|
|
||||||
const integration = require("./integration");
|
|
||||||
const { app, nativeTheme, BaseWindow, WebContentsView } = require("electron");
|
|
||||||
|
|
||||||
global.WEBVIEW = global.WEBVIEW || {
|
|
||||||
initialized: false,
|
|
||||||
status: "offline",
|
|
||||||
locked: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the webview with the provided arguments.
|
|
||||||
*
|
|
||||||
* @param {Object} args - The command-line arguments to customize the initialization process.
|
|
||||||
* @returns {boolean} Returns true if the initialization was successful.
|
|
||||||
*/
|
|
||||||
const init = async (args) => {
|
|
||||||
if (!args.web_url) {
|
|
||||||
console.error("Please provide the '--web-url' parameter");
|
|
||||||
return app.quit();
|
|
||||||
}
|
|
||||||
const url = new URL(args.web_url);
|
|
||||||
const theme = args.web_theme ? args.web_theme : "dark";
|
|
||||||
const zoom = args.web_zoom ? parseFloat(args.web_zoom) : 1.25;
|
|
||||||
|
|
||||||
// Set app theme and name
|
|
||||||
nativeTheme.themeSource = theme;
|
|
||||||
app.setName("TouchKio");
|
|
||||||
|
|
||||||
// Init global root window
|
|
||||||
WEBVIEW.window = new BaseWindow({
|
|
||||||
icon: path.join(__dirname, "..", "img", "icon.png"),
|
|
||||||
});
|
|
||||||
WEBVIEW.window.setMenuBarVisibility(false);
|
|
||||||
WEBVIEW.window.setFullScreen(true);
|
|
||||||
|
|
||||||
// Init global main webview
|
|
||||||
WEBVIEW.view = new WebContentsView();
|
|
||||||
WEBVIEW.window.contentView.addChildView(WEBVIEW.view);
|
|
||||||
WEBVIEW.view.webContents.loadURL(url.toString());
|
|
||||||
|
|
||||||
// Register events
|
|
||||||
windowEvents(WEBVIEW.window, zoom);
|
|
||||||
viewEvents(WEBVIEW.view);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the shared webview properties.
|
|
||||||
*/
|
|
||||||
const update = () => {
|
|
||||||
if (!WEBVIEW.initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update window status
|
|
||||||
if (WEBVIEW.window.isFullScreen()) {
|
|
||||||
WEBVIEW.status = "Fullscreen";
|
|
||||||
} else if (WEBVIEW.window.isMinimized()) {
|
|
||||||
WEBVIEW.status = "Minimized";
|
|
||||||
} else if (WEBVIEW.window.isMaximized()) {
|
|
||||||
WEBVIEW.status = "Maximized";
|
|
||||||
} else {
|
|
||||||
WEBVIEW.status = "Framed";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update integration sensor
|
|
||||||
console.log("Update Kiosk Status:", WEBVIEW.status);
|
|
||||||
integration.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register window events and handler.
|
|
||||||
*
|
|
||||||
* @param {Object} window - The root window object.
|
|
||||||
* @param {number} zoom - Zoom level used for the webview.
|
|
||||||
*/
|
|
||||||
const windowEvents = (window, zoom) => {
|
|
||||||
// Handle window resize for webview bounds
|
|
||||||
const resize = () => {
|
|
||||||
const { width, height } = window.getBounds();
|
|
||||||
WEBVIEW.view.setBounds({ x: 0, y: 0, width: width, height: height });
|
|
||||||
WEBVIEW.view.webContents.setZoomFactor(zoom);
|
|
||||||
};
|
|
||||||
window.on("ready-to-show", resize);
|
|
||||||
window.on("resize", resize);
|
|
||||||
resize();
|
|
||||||
|
|
||||||
// Handle focus change for webview lock
|
|
||||||
HARDWARE.display.notifiers.push(() => {
|
|
||||||
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
|
|
||||||
if (WEBVIEW.locked) {
|
|
||||||
window.blur();
|
|
||||||
}
|
|
||||||
/* This also works, but it doesn't allow to react for screen wake up.
|
|
||||||
window.setIgnoreMouseEvents(getDisplayStatus() === "OFF");
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
window.on("focus", () => {
|
|
||||||
if (WEBVIEW.locked) {
|
|
||||||
hardware.setDisplayStatus("ON");
|
|
||||||
window.blur();
|
|
||||||
}
|
|
||||||
WEBVIEW.locked = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update window status
|
|
||||||
window.on("minimize", update);
|
|
||||||
window.on("restore", update);
|
|
||||||
window.on("maximize", update);
|
|
||||||
window.on("unmaximize", update);
|
|
||||||
window.on("enter-full-screen", update);
|
|
||||||
window.on("leave-full-screen", update);
|
|
||||||
process.on("SIGINT", app.quit);
|
|
||||||
app.on("before-quit", () => {
|
|
||||||
WEBVIEW.status = "Terminated";
|
|
||||||
integration.update();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register webview events and handler.
|
|
||||||
*
|
|
||||||
* @param {Object} view - The main webview object.
|
|
||||||
*/
|
|
||||||
const viewEvents = (view) => {
|
|
||||||
// Enable webview touch emulation
|
|
||||||
view.webContents.debugger.attach("1.1");
|
|
||||||
view.webContents.debugger.sendCommand(
|
|
||||||
"Emulation.setEmitTouchEventsForMouse",
|
|
||||||
{
|
|
||||||
enabled: true,
|
|
||||||
configuration: "mobile",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Disable webview hyperlinks
|
|
||||||
view.webContents.setWindowOpenHandler(() => {
|
|
||||||
return { action: "deny" };
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove webview scrollbar
|
|
||||||
view.webContents.on("dom-ready", () => {
|
|
||||||
view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Webview fully loaded
|
|
||||||
view.webContents.on("did-finish-load", () => {
|
|
||||||
WEBVIEW.initialized = true;
|
|
||||||
update();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
init,
|
|
||||||
update,
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user