Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
+2
-1
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
docs/
|
||||
out/
|
||||
out/
|
||||
build.json
|
||||
|
||||
+20
-11
@@ -1,20 +1,20 @@
|
||||
# Supported Hardware
|
||||
This document contains an incomplete list of device and display hardware combinations that are either intended to be tested or have already been tested.
|
||||
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 properly as well.
|
||||
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 |
|
||||
| --- | --------------------- | ------------------------------------------------------------------------- |
|
||||
| ⬜ | Unknown | Who knows. |
|
||||
| 🟩 | 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. |
|
||||
| ⬛ | Crashed | The house is on fire. |
|
||||
| ⬜ | Somehow operational | Issues can occur and the overall performance is very slow. |
|
||||
| ⬛ | Not operational | The house is on fire. |
|
||||
|
||||
### DSI
|
||||
| Device | System | Display | Status |
|
||||
@@ -27,13 +27,22 @@ If you are running Linux with a graphical user interface (Wayland or X11), you s
|
||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | 🟩 |
|
||||
|
||||
### HDMI
|
||||
| Device | System | Display | Status |
|
||||
| ---------------------- | -------------------------------------- | -------------------------------------------------------------------------------- | ------ |
|
||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [LAFVIN 5" Touch Display (800x480)](https://www.amazon.de/gp/product/B0BWJ8YP7S) | 🟨 |
|
||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [GeeekPi 10.1" Capacitive Touch (1280x800)](https://www.amazon.nl/dp/B0DHV6DZC1) | 🟨 |
|
||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | Generic Non-Touch | 🟨 |
|
||||
| Generic PC (x64) | Ubuntu (64-bit), X11 | Generic Non-Touch | 🟧 |
|
||||
| Generic PC (x64) | Ubuntu (64-bit), Wayland | Generic Non-Touch | 🟥 |
|
||||
| Device | System | Display | Status |
|
||||
| ------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------ |
|
||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [LAFVIN 5" Touch Display (800x480)](https://www.amazon.de/gp/product/B0BWJ8YP7S) | 🟨 |
|
||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 8" CAPLCD (768x1024)](https://www.waveshare.com/wiki/8inch_768x1024_LCD) | 🟨 |
|
||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 7" 7EP-CAPLCD (1280x800)](https://www.waveshare.com/7ep-caplcd.htm) | 🟨 |
|
||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Hannspree HT225 21.5" (1920x1080)](https://www.hannspree.eu/product/HT-225-HPB) | 🟨 |
|
||||
| Raspberry Pi 400 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [UPerfect Vertical Touch 15.6" (1920x1080)](https://uperfect.com/products/uperfect-y-vertical-monitor-15-6) | 🟨 |
|
||||
| Raspberry Pi CM 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Lenovo ThinkCentre 23.8" (1920x1080)](https://psref.lenovo.com/Detail/ThinkCentre_Tiny_In_One_24_Gen_5?M=12NBGAT1EU) | 🟨 |
|
||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [GeeekPi 10.1" Capacitive Touch (1280x800)](https://www.amazon.nl/dp/B0DHV6DZC1) | 🟨 |
|
||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Prechen Portable Touch 18.5" FHD (1920x1080)](https://www.amazon.de/dp/B0CT2KLDBQ) | 🟨 |
|
||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Akntzcs Portable Touch 16" HD (1920x1200)](https://www.amazon.com/dp/B0CTGW6MQ6) | 🟨 |
|
||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [ELECROW Portable Monitor 10.1" (1280x800)](https://www.amazon.co.uk/dp/B0BHHQLKPY) | 🟨 |
|
||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | Generic Non-Touch | 🟨 |
|
||||
| Generic PC (x64) | Debian KDE (64-bit), Wayland, 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 | 🟥 |
|
||||
|
||||
## Contributions
|
||||
In case your hardware is not listed above don't worry, give it a try.
|
||||
|
||||
@@ -11,17 +11,20 @@ Moreover, the device running the **kiosk application** also offers several **Hom
|
||||
## Features
|
||||
- [x] Fast and easy setup.
|
||||
- [x] Remember login credentials.
|
||||
- [x] Touch optimized web browsing.
|
||||
- [x] Side panel widget for kiosk control.
|
||||
- [x] Touch friendly web browser interface.
|
||||
- [x] Configurable zoom and theme support.
|
||||
- [x] Single-tap screen wake-up functionality.
|
||||
- [x] Navigation bar to switch between pages.
|
||||
- [x] Configurable browser zoom and theme support.
|
||||
- [x] Extended touch screen wake-up functionality.
|
||||
- [x] Remote controllable via MQTT.
|
||||
- [x] Manage kiosk window status.
|
||||
- [x] Manage touch keyboard status.
|
||||
- [x] Toggle the on-screen keyboard.
|
||||
- [x] Touch display power and brightness.
|
||||
- [x] Show available system package upgrades.
|
||||
- [x] Execute system reboot and shutdown commands.
|
||||
- [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] Monitor temperature, processor and memory usage.
|
||||
- [x] Execute system reboot and shutdown commands.
|
||||
|
||||
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.
|
||||
@@ -41,19 +44,26 @@ For a comprehensive guide on setting up MQTT with Home Assistant, please refer t
|
||||
|
||||
## 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 user** (local access only) for your kiosk device.
|
||||
It's recommended to create a dedicated Home Assistant user (local access only) for your kiosk device.
|
||||
|
||||
You might also need a physical keyboard or remote VNC access to input these credentials once.
|
||||
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 file for auto-startup and will guide you through the setup process:
|
||||
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
|
||||
@@ -61,7 +71,7 @@ When connected via SSH, it's necessary to export the display variables first, as
|
||||
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 dpkg -i touchkio_1.x.x_arm64.deb && touchkio --setup`
|
||||
`sudo apt install ./touchkio_1.x.x_arm64.deb && touchkio --setup`
|
||||
- Others (**zip**): Extract the archive and run the binary, e.g: `unzip touchkio-linux-x64-1.x.x.zip && cd touchkio-linux-x64 && ./touchkio --setup`
|
||||
- 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.
|
||||
@@ -72,25 +82,34 @@ The [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) scrip
|
||||
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:
|
||||
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 (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`) |
|
||||
| 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.
|
||||
|
||||
@@ -99,6 +118,20 @@ For example:
|
||||
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 |
|
||||
@@ -116,19 +149,42 @@ For example:
|
||||
touchkio --web-url=http://192.168.1.42:8123 --mqtt-url=mqtt://192.168.1.42:1883 --mqtt-user=kiosk --mqtt-password=password
|
||||
```
|
||||
|
||||
## 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 can adjust the `--web-url` and other arguments for development runs.
|
||||
There you may want to adjust the `--web-url` and other arguments for development runs.
|
||||
|
||||
If you connect to your device via SSH, you may have to export display variables so that the kiosk application can be loaded inside the desktop environment:
|
||||
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 to the `~/.bashrc` file.
|
||||
To make this permanent, consider adding the export variables into the `~/.bashrc` file.
|
||||
|
||||
### Extensions
|
||||
|
||||
@@ -170,7 +226,7 @@ While using external sensors that directly integrate with Home Assistant and by
|
||||
<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 this `D-Bus` object path `/sm/puri/OSK0`, enabling the keyboard to be hidden or shown based on **MQTT** user input or system events.
|
||||
The kiosk application interacts with squeekboard via the `D-Bus` object path `/sm/puri/OSK0`, enabling the keyboard to be hidden or shown based on **MQTT** user input or system events.
|
||||
|
||||
The Raspberry Pi's **build-in screen blanking** function uses the command `swayidle -w timeout 600 'wlopm --off \*' resume 'wlopm --on \*' &` inside `~/.config/labwc/autostart` to blank the screen after **10 minutes**.
|
||||
The `wlopm --off \*` command changes the `bl_power` value to **4**, when setting the value to **0** the screen will turn on again.
|
||||
@@ -181,7 +237,7 @@ I managed to achieve this for the `brightness` file by implementing a simple `fs
|
||||
However, I found that it **never triggered** for the `bl_power` file.
|
||||
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 file every **500 milliseconds** for any changes.
|
||||
As a result, I went for a **polling solution**, checking the state of the 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.
|
||||
@@ -195,40 +251,49 @@ Implementing stronger **security measures** would complicate the setup process a
|
||||
When using the kiosk application without initializing the default arguments, you will need to provide them with every command.
|
||||
This means that the password may be stored as plain text in various files, such as `touchkio.service`, `~/.bash_history`, etc.
|
||||
|
||||
To resolve the issue where the first **touch** on a **turned-off screen** triggers a **click event** (which could inadvertently activate Home Assistant actions), a workaround needed to be implemented.
|
||||
When the screen **turns off** the **focus** is removed from the kiosk window.
|
||||
This way, the first click only turns the screen on and focuses the window, allowing subsequent clicks to work as expected.
|
||||
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.
|
||||
|
||||
Electron apps are known to be **resource intensive** due to their architecture and the inclusion of a full web browser environment. If you just run the kiosk application without other heavy loads, everything should run smoothly.
|
||||
|
||||
</div></details>
|
||||
|
||||
## Issues
|
||||
Please have a look into the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation if you ran into any issues.
|
||||
Please review the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation first if you encounter any problems.
|
||||
|
||||
**Minimal features** are designed to run on any system without issues:
|
||||
- A webview kiosk window launched in fullscreen mode and loading the specified `--web-url` website should not cause any problems.
|
||||
|
||||
**Extended features** become available when the `--mqtt-*` arguments are provided and the hardware is supported:
|
||||
- Sensor features that may work out-of-the-box include those that do not require any interaction with the display or keyboard.
|
||||
- If your hardware is not fully compatible there should be no crashes, but you may miss some sensors.
|
||||
- On some Debian based systems (e.g. Ubuntu), the display status control may only be available when using X11.
|
||||
- On some Debian based systems (e.g. Ubuntu GNOME), the display status control may only be available when using X11.
|
||||
- The following commands are currently implemented to modify the display status.
|
||||
- `wlopm --[on]/[off] *` (Raspberry Pi OS, Wayland)
|
||||
- `kscreen-doctor --dpms [on]/[off]` (Debian KDE, Wayland)
|
||||
- `xset dpms force [on]/[off]` (Generic, X11)
|
||||
|
||||
**Known Issues** that are by-design or for which there isn't a solution so far:
|
||||
- You can use Raspberry Pi's build-in screen blanking functionality, however, if the screen is turned on through Home Assistant after being automatically turned off, it will remain on indefinitely.
|
||||
- You can use Raspberry Pi's build-in [screen blanking](https://www.raspberrypi.com/documentation/computers/configuration.html#screen-blanking-3) functionality, however, if the screen is turned on through Home Assistant after being automatically turned off, it will remain on indefinitely.
|
||||
- It's recommended to either use the built-in screen blanking feature or implement a Home Assistant automation (e.g. presence detection) to manage the screen status.
|
||||
- When using a Raspberry Pi, the on-screen keyboard doesn't automatically pop-out when entering a text field.
|
||||
- This [issue](https://github.com/leukipp/touchkio/issues/4) also occurs in the default browser but as a current workaround you can use the side [widget](https://github.com/leukipp/touchkio/issues/16) to toggle the keyboard.
|
||||
- Hyperlinks that redirect the browser away from the main window are intentionally disabled.
|
||||
- This decision was made to maintain a clean design, as navigation buttons to move back and forth between different sites were not included.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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 specs, etc.) and any relevant log files.
|
||||
|
||||
## Credits
|
||||
Inspired by the one and only Raspberry Pi Master Lord, [Jeff Geerling](https://www.jeffgeerling.com/blog/2024/home-assistant-and-carplay-pi-touch-display-2).
|
||||
Thanks to Sebastian ([@pdsccode](https://github.com/pdsccode)) for his contributions on issues and [community](https://community.home-assistant.io/t/kiosk-mode-for-raspberry-pi-with-touch-display/821196) discussions.
|
||||
|
||||
[Inspired by](https://www.jeffgeerling.com/blog/2024/home-assistant-and-carplay-pi-touch-display-2) the one and only Raspberry Pi Master, Jeff Geerling ([@geerlingguy](https://github.com/geerlingguy)).
|
||||
|
||||
## License
|
||||
[MIT](https://github.com/leukipp/touchkio/blob/main/LICENSE)
|
||||
|
||||
+35
-2
@@ -1,4 +1,22 @@
|
||||
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: {
|
||||
@@ -33,10 +51,25 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
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 = [];
|
||||
result.artifacts.forEach((artifact) => {
|
||||
for (const artifact of result.artifacts) {
|
||||
if (artifact.includes(".zip")) {
|
||||
const [name, platform, arch] = path.basename(artifact).split("-");
|
||||
const buildFile = path.join(`${name}-${platform}-${arch}`, "resources", "app", "build.json");
|
||||
const options = { type: "nodebuffer", compression: "DEFLATE", compressionOptions: { level: 9 } };
|
||||
const zip = await jszip.loadAsync(fs.readFileSync(artifact));
|
||||
zip.file(buildFile, generateBuildFile(platform, arch, "zip"));
|
||||
fs.writeFileSync(artifact, await zip.generateAsync(options));
|
||||
}
|
||||
if (artifact.includes("amd64")) {
|
||||
const renamed = artifact.replace("amd64", "x64");
|
||||
fs.renameSync(artifact, renamed);
|
||||
@@ -44,7 +77,7 @@ module.exports = {
|
||||
} else {
|
||||
artifacts.push(artifact);
|
||||
}
|
||||
});
|
||||
}
|
||||
result.artifacts = artifacts;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
<!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 30px;
|
||||
column-gap: 20px;
|
||||
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;
|
||||
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" />
|
||||
</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("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-blur", (e, input) => {
|
||||
document.getElementById(input.id).blur();
|
||||
});
|
||||
ipcRenderer.on("input-select", (e, input) => {
|
||||
document.getElementById(input.id).select();
|
||||
});
|
||||
ipcRenderer.on("input-text", (e, input) => {
|
||||
document.getElementById(input.id).value = input.text;
|
||||
document.getElementById(input.id).placeholder = input.placeholder;
|
||||
});
|
||||
ipcRenderer.on("data-theme", (e, data) => {
|
||||
document.documentElement.setAttribute("data-theme", data.theme);
|
||||
});
|
||||
};
|
||||
|
||||
registerSendEvents();
|
||||
registerReceiveEvents();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,82 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
[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: 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>
|
||||
@@ -46,6 +46,14 @@
|
||||
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;
|
||||
@@ -61,11 +69,11 @@
|
||||
<body>
|
||||
<menu>
|
||||
<li>
|
||||
<button id="refresh">
|
||||
<button id="keyboard">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<title>web-refresh</title>
|
||||
<title>keyboard-close-outline</title>
|
||||
<path
|
||||
d="M13.82 14H9.66C9.56 13.34 9.5 12.68 9.5 12S9.56 10.65 9.66 10H14.34C14.43 10.65 14.5 11.32 14.5 12C14.5 12.5 14.46 13 14.4 13.46C15 12.96 15.72 12.57 16.5 12.32C16.5 12.21 16.5 12.11 16.5 12C16.5 11.32 16.44 10.66 16.36 10H19.74C19.9 10.64 20 11.31 20 12C20 12.06 20 12.12 20 12.18C20.7 12.35 21.35 12.63 21.95 13C22 12.68 22 12.34 22 12C22 6.5 17.5 2 12 2C6.47 2 2 6.5 2 12C2 17.5 6.5 22 12 22C12.34 22 12.68 22 13 21.95C12.59 21.29 12.29 20.55 12.13 19.75C12.09 19.82 12.05 19.89 12 19.96C11.17 18.76 10.5 17.43 10.09 16H12.5C12.81 15.25 13.26 14.58 13.82 14M18.92 8H15.97C15.65 6.75 15.19 5.55 14.59 4.44C16.43 5.07 17.96 6.34 18.92 8M12 4.03C12.83 5.23 13.5 6.57 13.91 8H10.09C10.5 6.57 11.17 5.23 12 4.03M4.26 14C4.1 13.36 4 12.69 4 12S4.1 10.64 4.26 10H7.64C7.56 10.66 7.5 11.32 7.5 12S7.56 13.34 7.64 14H4.26M5.08 16H8C8.35 17.25 8.8 18.45 9.4 19.56C7.57 18.93 6.03 17.65 5.08 16M8 8H5.08C6.03 6.34 7.57 5.06 9.4 4.44C8.8 5.55 8.35 6.75 8 8M20.83 15.67L22 14.5V18.5H18L19.77 16.73C19.32 16.28 18.69 16 18 16C16.62 16 15.5 17.12 15.5 18.5S16.62 21 18 21C18.82 21 19.54 20.61 20 20H21.71C21.12 21.47 19.68 22.5 18 22.5C15.79 22.5 14 20.71 14 18.5S15.79 14.5 18 14.5C19.11 14.5 20.11 14.95 20.83 15.67Z"
|
||||
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>
|
||||
@@ -91,11 +99,11 @@
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button id="keyboard">
|
||||
<button id="navigation">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<title>keyboard-close-outline</title>
|
||||
<title>web</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"
|
||||
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>
|
||||
@@ -104,22 +112,25 @@
|
||||
<script>
|
||||
const { ipcRenderer } = require("electron");
|
||||
|
||||
// Register button click send message
|
||||
document.querySelectorAll("button").forEach((button) => {
|
||||
button.addEventListener("click", (e) => {
|
||||
ipcRenderer.send("button-click", { id: e.target.id });
|
||||
const registerSendEvents = () => {
|
||||
document.querySelectorAll("button").forEach((button) => {
|
||||
button.addEventListener("click", (e) => {
|
||||
ipcRenderer.send("button-click", { id: e.target.id });
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Register button hide receive message
|
||||
ipcRenderer.on("button-hide", (e, button) => {
|
||||
document.getElementById(button.id).parentNode.hidden = true;
|
||||
});
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
// Register data theme receive message
|
||||
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: 154 KiB After Width: | Height: | Size: 154 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 78 KiB |
@@ -2,10 +2,24 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const readline = require("readline/promises");
|
||||
const integration = require("./src/integration");
|
||||
const hardware = require("./src/hardware");
|
||||
const webview = require("./src/webview");
|
||||
const integration = require("./js/integration");
|
||||
const hardware = require("./js/hardware");
|
||||
const webview = require("./js/webview");
|
||||
const log = require("electron-log");
|
||||
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`);
|
||||
}
|
||||
|
||||
/**
|
||||
* This promise resolves when the app has finished initializing,
|
||||
@@ -13,17 +27,93 @@ const { app } = require("electron");
|
||||
* initialization tasks.
|
||||
*/
|
||||
app.whenReady().then(async () => {
|
||||
const lock = app.requestSingleInstanceLock();
|
||||
if (!lock) {
|
||||
console.error("TouchKio is already running");
|
||||
if (!(await initApp()) || !(await initArgs()) || !(await initLog())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show used arguments
|
||||
const args = Object.assign({}, ARGS);
|
||||
if ("mqtt_password" in args) {
|
||||
args.mqtt_password = "*".repeat((args.mqtt_password || "").length);
|
||||
}
|
||||
console.log(`Arguments: ${JSON.stringify(args, null, 2)}`);
|
||||
|
||||
// Chained init functions
|
||||
const chained = [webview.init, hardware.init, integration.init];
|
||||
for (const init of chained) {
|
||||
if (!(await init())) {
|
||||
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.config = app.getPath("userData");
|
||||
APP.log = path.join(app.getPath("logs"), "main.log");
|
||||
|
||||
// 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.quit();
|
||||
}
|
||||
|
||||
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.getPath("userData"), "Arguments.json");
|
||||
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.log(`${APP.name}-v${APP.version}${build}\n${APP.homepage}`);
|
||||
return app.quit();
|
||||
}
|
||||
|
||||
// Setup arguments from file path
|
||||
if ("setup" in args || (!argsProvided && !argsFileExists)) {
|
||||
await sleep(3000);
|
||||
@@ -35,17 +125,44 @@ app.whenReady().then(async () => {
|
||||
args = readArgs(argsFilePath);
|
||||
}
|
||||
|
||||
// Show used arguments
|
||||
console.log(`Arguments: ${JSON.stringify(args, null, 2)}`);
|
||||
|
||||
// Chained init functions
|
||||
const chained = [webview.init, hardware.init, integration.init];
|
||||
for (const init of chained) {
|
||||
if (!(await init(args))) {
|
||||
break;
|
||||
}
|
||||
// Check arguments object
|
||||
if (!Object.keys(args).length) {
|
||||
console.error(`No arguments provided`);
|
||||
return app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// 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 settings
|
||||
log.transports.file.resolvePathFn = () => {
|
||||
return APP.log;
|
||||
};
|
||||
Object.assign(console, log.functions);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses command-line arguments from the given process object.
|
||||
@@ -54,14 +171,13 @@ app.whenReady().then(async () => {
|
||||
* @returns {Object} An object mapping argument names to their corresponding values.
|
||||
*/
|
||||
const parseArgs = (proc) => {
|
||||
let args = {};
|
||||
proc.argv.slice(1).forEach((arg) => {
|
||||
if (arg !== ".") {
|
||||
const [key, value] = arg.split("=");
|
||||
args[key.replace("--", "").replace("-", "_")] = value;
|
||||
}
|
||||
});
|
||||
return args;
|
||||
const args = proc.argv.slice(1).filter((arg) => arg !== ".");
|
||||
return Object.fromEntries(
|
||||
args.flatMap((arg) => {
|
||||
const match = arg.match(/^--?([^=]+)(?:=(.*))?$/);
|
||||
return match ? [[match[1].replace(/-/g, "_"), match[2] ?? null]] : [];
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -133,30 +249,41 @@ const promptArgs = async (proc) => {
|
||||
// Prompt questions and wait for the answers
|
||||
let args = {};
|
||||
let ignore = [];
|
||||
for (const { key, question, fallback } of prompts) {
|
||||
if (key === "mqtt") {
|
||||
const prompt = `${question} (${fallback}): `;
|
||||
const answer = await read.question(prompt);
|
||||
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"]);
|
||||
try {
|
||||
for (const { key, question, fallback } of prompts) {
|
||||
if (key === "mqtt") {
|
||||
const prompt = `${question} (${fallback}): `;
|
||||
const answer = await read.question(prompt);
|
||||
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 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;
|
||||
args[key] = value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`\n${error.message}`);
|
||||
args = {};
|
||||
app.quit();
|
||||
} finally {
|
||||
read.close();
|
||||
}
|
||||
read.close();
|
||||
|
||||
return args;
|
||||
};
|
||||
@@ -168,11 +295,15 @@ const promptArgs = async (proc) => {
|
||||
* @param {Object} args - The arguments object.
|
||||
*/
|
||||
const writeArgs = (path, args) => {
|
||||
const argc = Object.assign({}, args);
|
||||
if ("mqtt_password" in argc) {
|
||||
argc.mqtt_password = encrypt(argc.mqtt_password);
|
||||
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);
|
||||
}
|
||||
fs.writeFileSync(path, JSON.stringify(argc, null, 2));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -182,11 +313,16 @@ const writeArgs = (path, args) => {
|
||||
* @returns {Object} The arguments object.
|
||||
*/
|
||||
const readArgs = (path) => {
|
||||
const args = JSON.parse(fs.readFileSync(path));
|
||||
if ("mqtt_password" in args) {
|
||||
args.mqtt_password = decrypt(args.mqtt_password);
|
||||
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 args;
|
||||
return {};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -197,7 +333,7 @@ const readArgs = (path) => {
|
||||
*/
|
||||
const encrypt = (value) => {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const key = crypto.scryptSync(hardware.getMachineId(), app.getName(), 32);
|
||||
const key = crypto.scryptSync(hardware.getMachineId(), APP.name, 32);
|
||||
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
||||
let encrypted = cipher.update(value, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
@@ -213,7 +349,7 @@ const encrypt = (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.getName(), 32);
|
||||
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");
|
||||
@@ -227,4 +363,6 @@ const decrypt = (value) => {
|
||||
* @param {number} ms - Sleep time in milliseconds.
|
||||
* @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));
|
||||
};
|
||||
|
||||
+59
-42
@@ -1,5 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Read arguments
|
||||
ARG_UPDATE=false
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "update" ]; then
|
||||
ARG_UPDATE=true
|
||||
fi
|
||||
done
|
||||
|
||||
# Determine system architecture
|
||||
echo -e "Determining system architecture..."
|
||||
|
||||
@@ -12,94 +20,103 @@ case "$(uname -m)" in
|
||||
ARCH="x64"
|
||||
;;
|
||||
*)
|
||||
echo "Architecture $(uname -m) running $BITS-bit operating system is not supported."
|
||||
exit 1
|
||||
{ echo "Architecture $(uname -m) running $BITS-bit operating system is not supported."; exit 1; }
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$BITS" -ne 64 ]; then
|
||||
echo "Architecture $ARCH running $BITS-bit operating system is not supported."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ "$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)
|
||||
DEB_URL=$(wget -qO- https://api.github.com/repos/leukipp/touchkio/releases/latest | \
|
||||
grep -o "\"browser_download_url\": \"[^\"]*_${ARCH}\.deb\"" | \
|
||||
sed 's/"browser_download_url": "//;s/"//g')
|
||||
|
||||
TMP_DIR=$(mktemp -d)
|
||||
DEB_PATH="${TMP_DIR}/$(basename "$DEB_URL")"
|
||||
chmod 755 "$TMP_DIR"
|
||||
|
||||
if [ -z "$DEB_URL" ]; then
|
||||
echo "Download url for .deb file not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! wget --show-progress -q -O "$DEB_PATH" "$DEB_URL"; then
|
||||
echo "Failed to download the .deb file."
|
||||
exit 1
|
||||
fi
|
||||
[ -z "$DEB_URL" ] && { echo "Download url for .deb file not found."; exit 1; }
|
||||
wget --show-progress -q -O "$DEB_PATH" "$DEB_URL" || { echo "Failed to download the .deb file."; exit 1; }
|
||||
|
||||
# Install the latest .deb package
|
||||
echo -e "\nInstalling the latest release..."
|
||||
|
||||
if ! command -v dpkg &> /dev/null; then
|
||||
echo "Package manager dpkg was not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! sudo dpkg -i "$DEB_PATH"; then
|
||||
echo "Installation of .deb file failed."
|
||||
exit 1
|
||||
fi
|
||||
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_FILE="$HOME/.config/systemd/user/touchkio.service"
|
||||
mkdir -p "$(dirname "$SERVICE_FILE")" || { echo "Failed to create directory for $SERVICE_FILE"; exit 1; }
|
||||
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; }
|
||||
|
||||
bash -c "cat << EOF > \"$SERVICE_FILE\"
|
||||
[Unit]
|
||||
SERVICE_CONTENT="[Unit]
|
||||
Description=TouchKio
|
||||
After=graphical.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/touchkio
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF"
|
||||
WantedBy=default.target"
|
||||
|
||||
systemctl --user enable "$(basename "$SERVICE_FILE")" || { echo "Failed to enable service $SERVICE_FILE"; exit 1; }
|
||||
echo "Service $SERVICE_FILE enabled"
|
||||
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\""
|
||||
echo "DISPLAY was not set, defaulting to \"$DISPLAY\"."
|
||||
else
|
||||
echo "DISPLAY is set to \"$DISPLAY\""
|
||||
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\""
|
||||
echo "WAYLAND_DISPLAY was not set, defaulting to \"$WAYLAND_DISPLAY\"."
|
||||
else
|
||||
echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\""
|
||||
echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\"."
|
||||
fi
|
||||
|
||||
# Start the setup mode
|
||||
echo ""
|
||||
read -p "Start touchkio setup? (Y/n) " answer
|
||||
if [[ ${answer:-y} == [Yy]* ]]; then
|
||||
read -p "Start 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
|
||||
|
||||
+361
-117
@@ -1,80 +1,103 @@
|
||||
const os = require("os");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const process = require("child_process");
|
||||
const fsp = require("fs/promises");
|
||||
const cpr = require("child_process");
|
||||
|
||||
global.HARDWARE = global.HARDWARE || {
|
||||
initialized: false,
|
||||
status: "invalid",
|
||||
session: {},
|
||||
support: {},
|
||||
session: {},
|
||||
battery: {
|
||||
level: {
|
||||
path: null,
|
||||
},
|
||||
},
|
||||
display: {
|
||||
status: {
|
||||
path: null,
|
||||
value: null,
|
||||
path: null,
|
||||
command: null,
|
||||
},
|
||||
brightness: {
|
||||
path: null,
|
||||
value: null,
|
||||
path: null,
|
||||
max: null,
|
||||
},
|
||||
notifiers: [],
|
||||
},
|
||||
keyboard: {
|
||||
visible: 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.
|
||||
* @returns {bool} Returns true if the initialization was successful.
|
||||
*/
|
||||
const init = async (args) => {
|
||||
if (!compatibleDevice()) {
|
||||
console.warn("Device not supported");
|
||||
const init = async () => {
|
||||
if (!compatibleSystem()) {
|
||||
console.error("Operating system is not supported");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Init globals
|
||||
HARDWARE.session.user = sessionUser();
|
||||
HARDWARE.session.type = sessionType();
|
||||
HARDWARE.session.desktop = sessionDesktop();
|
||||
HARDWARE.battery.level.path = getBatteryLevelPath();
|
||||
HARDWARE.display.status.path = getDisplayStatusPath();
|
||||
HARDWARE.support.displayStatus = HARDWARE.display.status.path !== null;
|
||||
HARDWARE.display.status.command = getDisplayStatusCommand();
|
||||
HARDWARE.display.brightness.path = getDisplayBrightnessPath();
|
||||
HARDWARE.support.displayBrightness = HARDWARE.display.brightness.path !== null;
|
||||
HARDWARE.display.brightness.max = getDisplayBrightnessMax();
|
||||
HARDWARE.support.keyboardVisibility = processRuns("squeekboard");
|
||||
HARDWARE.support = checkSupport();
|
||||
HARDWARE.initialized = true;
|
||||
HARDWARE.status = "valid";
|
||||
|
||||
// Show supported features
|
||||
process.stdout.write("\n");
|
||||
console.log(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`);
|
||||
|
||||
// Show session infos
|
||||
console.log("\nUser:", HARDWARE.session.user);
|
||||
process.stdout.write("\n");
|
||||
console.log("User:", HARDWARE.session.user);
|
||||
console.log("Session:", HARDWARE.session.type);
|
||||
console.log("Desktop:", HARDWARE.session.desktop);
|
||||
|
||||
// Show device infos
|
||||
console.log("\nModel:", getModel());
|
||||
process.stdout.write("\n");
|
||||
console.log("Model:", getModel());
|
||||
console.log("Vendor:", getVendor());
|
||||
console.log("Serial Number:", getSerialNumber());
|
||||
console.log("Network Addresses:", getNetworkAddresses());
|
||||
console.log("Host Name:", getHostName());
|
||||
|
||||
// Show system infos
|
||||
console.log("\nUp Time:", getUpTime());
|
||||
process.stdout.write("\n");
|
||||
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());
|
||||
|
||||
// Show display infos
|
||||
console.log(`\nDisplay Status [${HARDWARE.display.status.path}]:`, getDisplayStatus());
|
||||
console.log(`Display Brightness [${HARDWARE.display.brightness.path}]:`, getDisplayBrightness(), "\n");
|
||||
// Show hardware infos
|
||||
process.stdout.write("\n");
|
||||
const unsupported = "unsupported";
|
||||
console.log(`Battery Level [${HARDWARE.battery.level.path || unsupported}]:`, `${getBatteryLevel() || unsupported}`);
|
||||
console.log(
|
||||
`Display Status [${HARDWARE.display.status.path || unsupported}]:`,
|
||||
`${getDisplayStatus() || unsupported} (${HARDWARE.display.status.command || unsupported})`,
|
||||
);
|
||||
console.log(
|
||||
`Display Brightness [${HARDWARE.display.brightness.path || unsupported}]:`,
|
||||
`${getDisplayBrightness() || unsupported}`,
|
||||
);
|
||||
console.log(
|
||||
`Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "squeekboard" : unsupported}]:`,
|
||||
`${getKeyboardVisibility() || unsupported}`,
|
||||
);
|
||||
|
||||
// Check for keyboard visibility
|
||||
process.stdout.write("\n");
|
||||
setKeyboardVisibility("OFF", (reply, error) => {
|
||||
if (!reply || error) {
|
||||
return;
|
||||
@@ -84,14 +107,14 @@ const init = async (args) => {
|
||||
return;
|
||||
}
|
||||
HARDWARE.keyboard.visibility = property.Visible === "true";
|
||||
HARDWARE.keyboard.notifiers.forEach((notifier) => {
|
||||
notifier();
|
||||
});
|
||||
EVENTS.emit("updateKeyboard");
|
||||
});
|
||||
});
|
||||
|
||||
// Check for display changes
|
||||
setInterval(update, 500);
|
||||
setDisplayStatus("ON", () => {
|
||||
interval(update, 1000);
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -99,46 +122,42 @@ const init = async (args) => {
|
||||
/**
|
||||
* Updates the shared hardware properties.
|
||||
*/
|
||||
const update = () => {
|
||||
const update = async () => {
|
||||
if (!HARDWARE.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Display status has changed
|
||||
if (HARDWARE.support.displayStatus) {
|
||||
const status = fs.readFileSync(`${HARDWARE.display.status.path}/dpms`, "utf8").trim();
|
||||
const status = (await fsp.readFile(`${HARDWARE.display.status.path}/dpms`, "utf8")).trim();
|
||||
if (status !== HARDWARE.display.status.value) {
|
||||
console.log("Update Display Status:", getDisplayStatus());
|
||||
HARDWARE.display.status.value = status;
|
||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
||||
notifier();
|
||||
});
|
||||
EVENTS.emit("updateDisplay");
|
||||
}
|
||||
}
|
||||
|
||||
// Display brightness has changed
|
||||
if (HARDWARE.support.displayBrightness) {
|
||||
const brightness = fs.readFileSync(`${HARDWARE.display.brightness.path}/brightness`, "utf8").trim();
|
||||
const brightness = (await fsp.readFile(`${HARDWARE.display.brightness.path}/brightness`, "utf8")).trim();
|
||||
if (brightness !== HARDWARE.display.brightness.value) {
|
||||
console.log("Update Display Brightness:", getDisplayBrightness());
|
||||
HARDWARE.display.brightness.value = brightness;
|
||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
||||
notifier();
|
||||
});
|
||||
EVENTS.emit("updateDisplay");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies device compatibility by checking the presence of necessary sys paths.
|
||||
* Verifies system compatibility by checking the presence of necessary sys paths.
|
||||
*
|
||||
* @returns {bool} Returns true if all paths exists.
|
||||
*/
|
||||
const compatibleDevice = () => {
|
||||
const compatibleSystem = () => {
|
||||
if (os.platform() !== "linux") {
|
||||
return false;
|
||||
}
|
||||
const paths = ["/sys/class/drm", "/sys/class/backlight", "/sys/class/thermal"];
|
||||
const paths = ["/sys/class/drm", "/sys/class/backlight", "/sys/class/power_supply", "/sys/class/thermal"];
|
||||
return paths.every((path) => fs.existsSync(path));
|
||||
};
|
||||
|
||||
@@ -171,24 +190,49 @@ const sessionType = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the session type for the logged in user.
|
||||
* Gets the desktop environment name by checking environment variables.
|
||||
*
|
||||
* @returns {bool} Returns true if the session matches the type.
|
||||
* @returns {string} Returns desktop environment name or 'unknown' if not detected.
|
||||
*/
|
||||
const session = (type) => {
|
||||
return HARDWARE.session.type == type;
|
||||
const sessionDesktop = () => {
|
||||
const envs = ["XDG_CURRENT_DESKTOP", "XDG_DESKTOP_SESSION", "DESKTOP_SESSION"];
|
||||
const names = envs.map((env) => process.env[env]).filter(Boolean);
|
||||
return (names.join(":") || "unknown").toLowerCase();
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks supported features based on hardware and software.
|
||||
*
|
||||
* @returns {Object} Returns support object with boolean values.
|
||||
*/
|
||||
const checkSupport = () => {
|
||||
const battery = HARDWARE.battery.level;
|
||||
const status = HARDWARE.display.status;
|
||||
const brightness = HARDWARE.display.brightness;
|
||||
const keyboard = processRuns("squeekboard");
|
||||
const service = serviceRuns(APP.name);
|
||||
const sudo = sudoRights();
|
||||
const deb = APP.build.maker === "deb";
|
||||
return {
|
||||
batteryLevel: !!battery.path,
|
||||
displayStatus: !!status.path && !!status.command,
|
||||
displayBrightness: sudo && !!brightness.path && !!brightness.max,
|
||||
keyboardVisibility: keyboard,
|
||||
sudoRights: sudo,
|
||||
appUpdate: sudo && service && deb,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the model name using `/sys/firmware/devicetree/base/model` or `/sys/class/dmi/id/product_name`.
|
||||
*
|
||||
* @returns {string|null} The model name of the device or 'Generic' if not found.
|
||||
* @returns {string} The model name of the device or 'Generic' if not found.
|
||||
*/
|
||||
const getModel = () => {
|
||||
const paths = ["/sys/firmware/devicetree/base/model", "/sys/class/dmi/id/product_name"];
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
return execSyncCommand("sudo", ["cat", path]);
|
||||
return execSyncCommand("cat", [path]) || "Generic";
|
||||
}
|
||||
}
|
||||
return "Generic";
|
||||
@@ -197,13 +241,13 @@ const getModel = () => {
|
||||
/**
|
||||
* Gets the vendor name using `/sys/class/dmi/id/board_vendor`.
|
||||
*
|
||||
* @returns {string|null} The vendor name of the device or 'Generic' if not found.
|
||||
* @returns {string} The vendor name of the device or 'Generic' if not found.
|
||||
*/
|
||||
const getVendor = () => {
|
||||
const paths = ["/sys/class/dmi/id/board_vendor"];
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
return execSyncCommand("sudo", ["cat", path]);
|
||||
return execSyncCommand("cat", [path]) || "Generic";
|
||||
}
|
||||
}
|
||||
const model = getModel();
|
||||
@@ -214,27 +258,59 @@ const getVendor = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the serial number using `/sys/firmware/devicetree/base/serial-number` or `/sys/class/dmi/id/product_serial`.
|
||||
* Gets the serial number using `/sys/firmware/devicetree/base/serial-number`.
|
||||
*
|
||||
* @returns {string|null} The serial number of the device or '123456' if not found.
|
||||
* @returns {string} The serial number or machine id parts of the device or '123456' if not found.
|
||||
*/
|
||||
const getSerialNumber = () => {
|
||||
const paths = ["/sys/firmware/devicetree/base/serial-number", "/sys/class/dmi/id/product_serial"];
|
||||
const paths = ["/sys/firmware/devicetree/base/serial-number"];
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
return execSyncCommand("sudo", ["cat", path]);
|
||||
return execSyncCommand("cat", [path]) || "123456";
|
||||
}
|
||||
}
|
||||
return getMachineId().slice(-6);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the machine id using `/etc/machine-id`.
|
||||
*
|
||||
* @returns {string} The machine id of the system or '123456' if not found.
|
||||
*/
|
||||
const getMachineId = () => {
|
||||
const paths = ["/etc/machine-id"];
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
return execSyncCommand("cat", [path]) || "123456";
|
||||
}
|
||||
}
|
||||
return "123456";
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the host machine id using `/etc/machine-id`.
|
||||
* Gets the network interfaces addresses using `os.networkInterfaces()`.
|
||||
*
|
||||
* @returns {string} The machine id of the system or '123456' if not found.
|
||||
* @returns {Object} The network addresses of all interfaces.
|
||||
*/
|
||||
const getMachineId = () => {
|
||||
return execSyncCommand("cat", ["/etc/machine-id"]) || "123456";
|
||||
const getNetworkAddresses = () => {
|
||||
const addresses = {};
|
||||
for (const [key, interfaces] of Object.entries(os.networkInterfaces())) {
|
||||
for (const interface of interfaces) {
|
||||
if (interface.internal || !interface.family || !interface.address) {
|
||||
continue;
|
||||
}
|
||||
const name = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
if (!addresses[name]) {
|
||||
addresses[name] = {};
|
||||
}
|
||||
const family = interface.family;
|
||||
if (!addresses[name][family]) {
|
||||
addresses[name][family] = [];
|
||||
}
|
||||
addresses[name][family].push(interface.address);
|
||||
}
|
||||
}
|
||||
return addresses;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -303,6 +379,39 @@ const getProcessorTemperature = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current battery power level path using `/sys/class/power_supply`.
|
||||
*
|
||||
* @returns {string|null} The battery power level path or null if nothing was found.
|
||||
*/
|
||||
const getBatteryLevelPath = () => {
|
||||
const power = "/sys/class/power_supply";
|
||||
for (const supply of fs.readdirSync(power)) {
|
||||
const capacityFile = path.join(power, supply, "capacity");
|
||||
if (!fs.existsSync(capacityFile)) {
|
||||
continue;
|
||||
}
|
||||
return path.join(power, supply);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current battery power level using `/sys/class/power_supply/.../capacity`.
|
||||
*
|
||||
* @returns {number|null} The battery power level in percentage or null if nothing was found.
|
||||
*/
|
||||
const getBatteryLevel = () => {
|
||||
if (!HARDWARE.support.batteryLevel) {
|
||||
return null;
|
||||
}
|
||||
const capacity = fs.readFileSync(`${HARDWARE.battery.level.path}/capacity`, "utf8").trim();
|
||||
if (capacity) {
|
||||
return parseFloat(capacity);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current display status path using `/sys/class/drm`.
|
||||
*
|
||||
@@ -324,7 +433,31 @@ const getDisplayStatusPath = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current display power status using `wlopm` or `xset`.
|
||||
* Gets the available display status command checking for `wlopm`, `kscreen-doctor` and `xset`.
|
||||
*
|
||||
* @returns {string|null} The display status command or null if nothing was found.
|
||||
*/
|
||||
const getDisplayStatusCommand = () => {
|
||||
const type = HARDWARE.session.type;
|
||||
const desktop = HARDWARE.session.desktop;
|
||||
const mapping = {
|
||||
wayland: [
|
||||
{ command: "wlopm", desktops: ["labwc", "wayfire", "unknown"] },
|
||||
{ command: "kscreen-doctor", desktops: ["kde", "plasma", "unknown"] },
|
||||
],
|
||||
x11: [{ command: "xset", desktops: ["*"] }],
|
||||
}[type];
|
||||
for (const map of mapping || []) {
|
||||
if (commandExists(map.command) && map.desktops.some((d) => d === "*" || desktop.includes(d))) {
|
||||
return map.command;
|
||||
}
|
||||
}
|
||||
console.warn("No display command available");
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current display power status using the available command.
|
||||
*
|
||||
* @returns {string|null} The display status as 'ON'/'OFF' or null if an error occurs.
|
||||
*/
|
||||
@@ -332,30 +465,40 @@ const getDisplayStatus = () => {
|
||||
if (!HARDWARE.support.displayStatus) {
|
||||
return null;
|
||||
}
|
||||
if (session("wayland") && commandExists("wlopm")) {
|
||||
const status = execSyncCommand("wlopm", []);
|
||||
if (status !== null) {
|
||||
const out = status.split("\n")[0].split(" ");
|
||||
return out.pop().toUpperCase();
|
||||
}
|
||||
} else if (session("x11") && commandExists("xset")) {
|
||||
const status = execSyncCommand("xset", ["-q"]);
|
||||
if (status !== null) {
|
||||
const on = /Monitor is On/.test(status);
|
||||
return on ? "ON" : "OFF";
|
||||
}
|
||||
switch (HARDWARE.display.status.command) {
|
||||
case "wlopm":
|
||||
const wlopm = execSyncCommand("wlopm", []);
|
||||
if (wlopm !== null) {
|
||||
const output = wlopm.split("\n")[0].split(" ");
|
||||
return output.pop().toUpperCase();
|
||||
}
|
||||
break;
|
||||
case "kscreen-doctor":
|
||||
const kdoc = execSyncCommand("kscreen-doctor", ["--dpms", "show"]);
|
||||
if (kdoc !== null) {
|
||||
const output = kdoc.split("\n")[0].split(" ");
|
||||
return output.pop().toUpperCase();
|
||||
}
|
||||
break;
|
||||
case "xset":
|
||||
const xset = execSyncCommand("xset", ["-q"]);
|
||||
if (xset !== null) {
|
||||
const output = /Monitor is On/.test(xset);
|
||||
return output ? "ON" : "OFF";
|
||||
}
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the display power status using `wlopm` or `xset`.
|
||||
* Sets the display power status using the available command.
|
||||
*
|
||||
* This function takes a desired status ('ON' or 'OFF') and executes
|
||||
* 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.
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const setDisplayStatus = (status, callback = null) => {
|
||||
if (!HARDWARE.support.displayStatus) {
|
||||
@@ -367,10 +510,16 @@ const setDisplayStatus = (status, callback = null) => {
|
||||
if (typeof callback === "function") callback(null, "Invalid status");
|
||||
return;
|
||||
}
|
||||
if (session("wayland") && commandExists("wlopm")) {
|
||||
execAsyncCommand("wlopm", [`--${status.toLowerCase()}`, "*"], callback);
|
||||
} else if (session("x11") && commandExists("xset")) {
|
||||
execAsyncCommand("xset", ["dpms", "force", status.toLowerCase()], callback);
|
||||
switch (HARDWARE.display.status.command) {
|
||||
case "wlopm":
|
||||
execAsyncCommand("wlopm", [`--${status.toLowerCase()}`, "*"], callback);
|
||||
break;
|
||||
case "kscreen-doctor":
|
||||
execAsyncCommand("kscreen-doctor", ["--dpms", status.toLowerCase()], callback);
|
||||
break;
|
||||
case "xset":
|
||||
execAsyncCommand("xset", ["dpms", "force", status.toLowerCase()], callback);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -397,11 +546,11 @@ const getDisplayBrightnessPath = () => {
|
||||
* @returns {number|null} The brightness maximum value or null if an error occurs.
|
||||
*/
|
||||
const getDisplayBrightnessMax = () => {
|
||||
if (!HARDWARE.support.displayBrightness) {
|
||||
if (!HARDWARE.display.brightness.path) {
|
||||
return null;
|
||||
}
|
||||
const max = execSyncCommand("cat", [`${HARDWARE.display.brightness.path}/max_brightness`]);
|
||||
if (max !== null) {
|
||||
const max = fs.readFileSync(`${HARDWARE.display.brightness.path}/max_brightness`, "utf8").trim();
|
||||
if (max) {
|
||||
return parseInt(max, 10);
|
||||
}
|
||||
return null;
|
||||
@@ -416,10 +565,10 @@ const getDisplayBrightness = () => {
|
||||
if (!HARDWARE.support.displayBrightness) {
|
||||
return null;
|
||||
}
|
||||
const brightness = execSyncCommand("cat", [`${HARDWARE.display.brightness.path}/brightness`]);
|
||||
if (brightness !== null) {
|
||||
const max = HARDWARE.display.brightness.max;
|
||||
return Math.round((parseInt(brightness, 10) / max) * 100);
|
||||
const brightness = fs.readFileSync(`${HARDWARE.display.brightness.path}/brightness`, "utf8").trim();
|
||||
if (brightness) {
|
||||
const max = HARDWARE.display.brightness.max || 1;
|
||||
return Math.max(1, Math.min(Math.round((parseInt(brightness, 10) / max) * 100), 100));
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -431,7 +580,7 @@ const getDisplayBrightness = () => {
|
||||
* maps it to the proper range 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.
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const setDisplayBrightness = (brightness, callback = null) => {
|
||||
if (!HARDWARE.support.displayBrightness) {
|
||||
@@ -443,7 +592,7 @@ const setDisplayBrightness = (brightness, callback = null) => {
|
||||
if (typeof callback === "function") callback(null, "Invalid brightness");
|
||||
return;
|
||||
}
|
||||
const max = HARDWARE.display.brightness.max;
|
||||
const max = HARDWARE.display.brightness.max || 1;
|
||||
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
|
||||
const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback);
|
||||
proc.stdin.write(value.toString());
|
||||
@@ -468,8 +617,8 @@ const getKeyboardVisibility = () => {
|
||||
* This function takes a desired visibility ('ON' or 'OFF') and executes
|
||||
* the appropriate command to show or hide the keyboard.
|
||||
*
|
||||
* @param {boolean} visibility - The desired visibility ('ON' or 'OFF').
|
||||
* @param {function} callback - A callback function that receives the output or error.
|
||||
* @param {bool} visibility - The desired visibility ('ON' or 'OFF').
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const setKeyboardVisibility = (visibility, callback = null) => {
|
||||
if (!HARDWARE.support.keyboardVisibility) {
|
||||
@@ -507,9 +656,13 @@ const checkPackageUpgrades = () => {
|
||||
* 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.
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const shutdownSystem = (callback = null) => {
|
||||
if (!HARDWARE.support.sudoRights) {
|
||||
if (typeof callback === "function") callback(null, "Not supported");
|
||||
return;
|
||||
}
|
||||
execAsyncCommand("sudo", ["shutdown", "-h", "now"], callback);
|
||||
};
|
||||
|
||||
@@ -519,21 +672,51 @@ const shutdownSystem = (callback = null) => {
|
||||
* 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.
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const rebootSystem = (callback = null) => {
|
||||
if (!HARDWARE.support.sudoRights) {
|
||||
if (typeof callback === "function") callback(null, "Not supported");
|
||||
return;
|
||||
}
|
||||
execAsyncCommand("sudo", ["reboot"], callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if sudo commands can run without a password.
|
||||
*
|
||||
* @returns {bool} Returns true if password-less sudo rights exists.
|
||||
*/
|
||||
const sudoRights = () => {
|
||||
try {
|
||||
const output = cpr.execSync(`sudo -n true`, { encoding: "utf8" });
|
||||
return output !== null && !output.includes("password is required");
|
||||
} catch {}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a service is running using `systemctl`.
|
||||
*
|
||||
* @param {string} name - The service name to check.
|
||||
* @returns {bool} Returns true if the service runs.
|
||||
*/
|
||||
const serviceRuns = (name) => {
|
||||
try {
|
||||
return !!cpr.execSync(`systemctl --user is-active ${name}`, { encoding: "utf8" });
|
||||
} catch {}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a process is running using `pidof`.
|
||||
*
|
||||
* @param {string} name - The process name to check.
|
||||
* @returns {boolean} Returns true if the process runs.
|
||||
* @returns {bool} Returns true if the process runs.
|
||||
*/
|
||||
const processRuns = (name) => {
|
||||
try {
|
||||
return !!process.execSync(`pidof ${name}`, { encoding: "utf8" });
|
||||
return !!cpr.execSync(`pidof ${name}`, { encoding: "utf8" });
|
||||
} catch {}
|
||||
return false;
|
||||
};
|
||||
@@ -542,11 +725,11 @@ const processRuns = (name) => {
|
||||
* Checks if a command is available using `which`.
|
||||
*
|
||||
* @param {string} name - The command name to check.
|
||||
* @returns {boolean} Returns true if the command is available.
|
||||
* @returns {bool} Returns true if the command is available.
|
||||
*/
|
||||
const commandExists = (name) => {
|
||||
try {
|
||||
return !!process.execSync(`which ${name}`, { encoding: "utf8" });
|
||||
return !!cpr.execSync(`which ${name}`, { encoding: "utf8" });
|
||||
} catch {}
|
||||
return false;
|
||||
};
|
||||
@@ -560,7 +743,7 @@ const commandExists = (name) => {
|
||||
*/
|
||||
const execSyncCommand = (cmd, args = []) => {
|
||||
try {
|
||||
const output = process.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
||||
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
||||
return output.trim().replace(/\0/g, "");
|
||||
} catch (error) {
|
||||
console.error("Execute Sync:", error.message);
|
||||
@@ -573,13 +756,13 @@ const execSyncCommand = (cmd, args = []) => {
|
||||
*
|
||||
* @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.
|
||||
* @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);
|
||||
let proc = cpr.spawn(cmd, args);
|
||||
proc.stderr.on("data", (data) => {
|
||||
if (data) {
|
||||
errorOutput += data.toString();
|
||||
@@ -601,21 +784,68 @@ const execAsyncCommand = (cmd, args = [], callback = null) => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Execute Async:", error.message);
|
||||
if (typeof callback === "function") {
|
||||
callback(null, error.message);
|
||||
}
|
||||
if (typeof callback === "function") callback(null, error.message);
|
||||
}
|
||||
});
|
||||
return proc;
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a script command asynchronously.
|
||||
*
|
||||
* @param {string} cmd - The script to execute.
|
||||
* @param {Array<string>} args - The arguments for the command.
|
||||
* @param {Function} callback - A callback function that receives the progress or error.
|
||||
* @returns {Object} The spawned process object.
|
||||
*/
|
||||
const execScriptCommand = (cmd, args = [], callback = null) => {
|
||||
let progress = 1;
|
||||
let proc = cpr.spawn(cmd, args);
|
||||
if (typeof callback === "function") callback(progress, null);
|
||||
proc.stdout.on("data", (data) => {
|
||||
if (data) {
|
||||
const output = data.toString().trim();
|
||||
const lines = output.replace(/\n+/g, "\n").replace(/^\n+|\n+$|\0/g, "");
|
||||
for (const line of lines.split("\n").filter(Boolean)) {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
});
|
||||
proc.stderr.on("data", (data) => {
|
||||
if (data) {
|
||||
const output = data.toString().trim();
|
||||
const lines = output.replace(/\n+/g, "\n").replace(/^\n+|\n+$|\0/g, "");
|
||||
for (const line of lines.split("\n").filter(Boolean)) {
|
||||
const matches = line.match(/(\d{1,3})%/g);
|
||||
if (matches) {
|
||||
const match = Math.max(...matches.map((p) => parseInt(p)));
|
||||
const percent = Math.floor(match / 10) * 10;
|
||||
if (percent > 10 && percent > progress) {
|
||||
progress = percent;
|
||||
if (typeof callback === "function") callback(progress - 10, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
proc.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
console.log(`Script exited with error code ${code}.`);
|
||||
if (typeof callback === "function") callback(null, code);
|
||||
} else {
|
||||
console.log("Script exited successfully.");
|
||||
if (typeof callback === "function") callback(100, null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a D-Bus method call synchronously using `dbus-send`.
|
||||
*
|
||||
* @param {string} path - The D-Bus object path.
|
||||
* @param {string} method - The D-Bus method name.
|
||||
* @param {Array<string>} values - The argument values for the D-Bus method.
|
||||
* @param {function} callback - A callback function that receives the output or error.
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const dbusCall = (path, method, values, callback = null) => {
|
||||
const cmd = "dbus-send";
|
||||
@@ -623,15 +853,11 @@ const dbusCall = (path, method, values, callback = null) => {
|
||||
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
|
||||
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
|
||||
try {
|
||||
const output = process.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
||||
if (typeof callback === "function") {
|
||||
callback(output.trim().replace(/\0/g, ""), null);
|
||||
}
|
||||
const output = cpr.execSync([cmd, ...args].join(" ").trim(), { encoding: "utf8" });
|
||||
if (typeof callback === "function") callback(output.trim().replace(/\0/g, ""), null);
|
||||
} catch (error) {
|
||||
console.error("Call D-Bus:", error.message);
|
||||
if (typeof callback === "function") {
|
||||
callback(null, error.message);
|
||||
}
|
||||
if (typeof callback === "function") callback(null, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -639,13 +865,13 @@ const dbusCall = (path, method, values, callback = null) => {
|
||||
* Monitors D-Bus property changes asynchronously using `dbus-monitor`.
|
||||
*
|
||||
* @param {string} path - The D-Bus object path.
|
||||
* @param {function} callback - A callback function that receives the changed property.
|
||||
* @returns {object} The spawned process object.
|
||||
* @param {Function} callback - A callback function that receives the changed property.
|
||||
* @returns {Object} The spawned process object.
|
||||
*/
|
||||
const dbusMonitor = (path, callback) => {
|
||||
const cmd = "dbus-monitor";
|
||||
const args = [`interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='${path}'`];
|
||||
const proc = process.spawn(cmd, args);
|
||||
const proc = cpr.spawn(cmd, args);
|
||||
proc.stdout.on("data", (data) => {
|
||||
try {
|
||||
const signal = data.toString();
|
||||
@@ -665,22 +891,38 @@ const dbusMonitor = (path, callback) => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Monitor D-Bus:", error.message);
|
||||
if (typeof callback === "function") {
|
||||
callback(null, error.message);
|
||||
}
|
||||
if (typeof callback === "function") callback(null, error.message);
|
||||
}
|
||||
});
|
||||
proc.stderr.on("data", (data) => {
|
||||
if (data) {
|
||||
console.error("Monitor D-Bus:", data.toString());
|
||||
if (typeof callback === "function") {
|
||||
callback(null, data.toString());
|
||||
}
|
||||
if (typeof callback === "function") callback(null, data.toString());
|
||||
}
|
||||
});
|
||||
return proc;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function for asynchronous interval calls.
|
||||
*
|
||||
* @param {Function} callback - An async callback function.
|
||||
* @param {number} ms - Sleep time in milliseconds.
|
||||
*/
|
||||
const interval = (callback, ms) => {
|
||||
const run = () => {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
console.error("Interval Callback:", error.message);
|
||||
}
|
||||
run();
|
||||
}, ms);
|
||||
};
|
||||
run();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
update,
|
||||
@@ -688,17 +930,16 @@ module.exports = {
|
||||
getVendor,
|
||||
getSerialNumber,
|
||||
getMachineId,
|
||||
getNetworkAddresses,
|
||||
getHostName,
|
||||
getUpTime,
|
||||
getMemorySize,
|
||||
getMemoryUsage,
|
||||
getProcessorUsage,
|
||||
getProcessorTemperature,
|
||||
getDisplayStatusPath,
|
||||
getBatteryLevel,
|
||||
getDisplayStatus,
|
||||
setDisplayStatus,
|
||||
getDisplayBrightnessPath,
|
||||
getDisplayBrightnessMax,
|
||||
getDisplayBrightness,
|
||||
setDisplayBrightness,
|
||||
getKeyboardVisibility,
|
||||
@@ -706,4 +947,7 @@ module.exports = {
|
||||
checkPackageUpgrades,
|
||||
shutdownSystem,
|
||||
rebootSystem,
|
||||
execSyncCommand,
|
||||
execAsyncCommand,
|
||||
execScriptCommand,
|
||||
};
|
||||
@@ -0,0 +1,966 @@
|
||||
const mqtt = require("mqtt");
|
||||
const hardware = require("./hardware");
|
||||
const { app } = require("electron");
|
||||
|
||||
global.INTEGRATION = global.INTEGRATION || {
|
||||
initialized: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the integration with the provided arguments.
|
||||
*
|
||||
* @returns {bool} Returns true if the initialization was successful.
|
||||
*/
|
||||
const init = async () => {
|
||||
if (!ARGS.mqtt_url) {
|
||||
return false;
|
||||
}
|
||||
if (!/^mqtts?:\/\//.test(ARGS.mqtt_url)) {
|
||||
console.error("Please provide the '--mqtt-url' parameter with mqtt(s)");
|
||||
return app.quit();
|
||||
}
|
||||
|
||||
// Parse arguments
|
||||
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 vendor = hardware.getVendor();
|
||||
const hostName = hardware.getHostName();
|
||||
const serialNumber = hardware.getSerialNumber();
|
||||
const serialNumberSuffix = serialNumber.slice(-6);
|
||||
const deviceName = hostName.charAt(0).toUpperCase() + hostName.slice(1);
|
||||
const deviceId = serialNumberSuffix.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
||||
|
||||
// Init globals
|
||||
INTEGRATION.discovery = discovery;
|
||||
INTEGRATION.node = `rpi_${deviceId}`;
|
||||
INTEGRATION.root = `${APP.name}/${INTEGRATION.node}`;
|
||||
INTEGRATION.device = {
|
||||
name: `${APP.title} ${deviceName}`,
|
||||
model: model,
|
||||
manufacturer: vendor,
|
||||
serial_number: serialNumber,
|
||||
identifiers: [INTEGRATION.node],
|
||||
sw_version: `${APP.name}-v${APP.version}`,
|
||||
configuration_url: APP.homepage,
|
||||
};
|
||||
|
||||
// Connection settings
|
||||
const options = user === null || password === null ? null : { username: user, password: password };
|
||||
const masked = password === null ? "null" : "*".repeat(password.length);
|
||||
console.log("MQTT Connecting:", `${user}:${masked}@${url.toString()}`);
|
||||
INTEGRATION.client = mqtt.connect(url.toString(), options);
|
||||
|
||||
// Client connected
|
||||
INTEGRATION.client
|
||||
.once("connect", () => {
|
||||
console.log(`MQTT Connected: ${url.toString()}`);
|
||||
process.stdout.write("\n");
|
||||
|
||||
// Init client controls
|
||||
initApp();
|
||||
initShutdown();
|
||||
initReboot();
|
||||
initRefresh();
|
||||
initKiosk();
|
||||
initDisplay();
|
||||
initKeyboard();
|
||||
initPageNumber();
|
||||
initPageZoom();
|
||||
initPageUrl();
|
||||
|
||||
// Init client sensors
|
||||
initModel();
|
||||
initSerialNumber();
|
||||
initHostName();
|
||||
initNetworkAddress();
|
||||
initUpTime();
|
||||
initMemorySize();
|
||||
initMemoryUsage();
|
||||
initProcessorUsage();
|
||||
initProcessorTemperature();
|
||||
initBatteryLevel();
|
||||
initPackageUpgrades();
|
||||
initHeartbeat();
|
||||
initLastActive();
|
||||
|
||||
// Integration initialized
|
||||
INTEGRATION.initialized = true;
|
||||
})
|
||||
.on("error", (error) => {
|
||||
console.error("MQTT", error.message);
|
||||
});
|
||||
|
||||
// Update sensor states from events
|
||||
EVENTS.on("updateDisplay", updateDisplay);
|
||||
EVENTS.on("updateKeyboard", updateKeyboard);
|
||||
|
||||
// Update time sensors periodically (30s)
|
||||
setInterval(() => {
|
||||
updateHeartbeat();
|
||||
updateLastActive();
|
||||
}, 30 * 1000);
|
||||
|
||||
// Update system sensors periodically (1min)
|
||||
setInterval(() => {
|
||||
update();
|
||||
}, 60 * 1000);
|
||||
|
||||
// Update upgrade sensors periodically (1h)
|
||||
setInterval(() => {
|
||||
updateApp();
|
||||
updatePackageUpgrades();
|
||||
}, 3600 * 1000);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the shared integration properties.
|
||||
*/
|
||||
const update = async () => {
|
||||
if (!INTEGRATION.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update client sensors
|
||||
updateKiosk();
|
||||
updatePageNumber();
|
||||
updatePageZoom();
|
||||
updatePageUrl();
|
||||
updateNetworkAddress();
|
||||
updateUpTime();
|
||||
updateLastActive();
|
||||
updateMemoryUsage();
|
||||
updateProcessorUsage();
|
||||
updateProcessorTemperature();
|
||||
updateBatteryLevel();
|
||||
};
|
||||
|
||||
/**
|
||||
* Publishes the auto-discovery config via the mqtt connection.
|
||||
*
|
||||
* @param {string} type - The entity type name.
|
||||
* @param {Object} config - The configuration object.
|
||||
* @returns {Object} Instance of the mqtt client.
|
||||
*/
|
||||
const publishConfig = (type, config) => {
|
||||
if (type === null || config === null) {
|
||||
return INTEGRATION.client;
|
||||
}
|
||||
const path = config.unique_id.replace(`${INTEGRATION.node}_`, "");
|
||||
const root = `${INTEGRATION.discovery}/${type}/${INTEGRATION.node}/${path}/config`;
|
||||
return INTEGRATION.client.publish(root, JSON.stringify(config), { qos: 1, retain: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Publishes the sensor attributes via the mqtt connection.
|
||||
*
|
||||
* @param {string} path - The entity path name.
|
||||
* @param {Object} attributes - The attributes object.
|
||||
* @returns {Object} Instance of the mqtt client.
|
||||
*/
|
||||
const publishAttributes = (path, attributes) => {
|
||||
if (path === null || attributes === null) {
|
||||
return INTEGRATION.client;
|
||||
}
|
||||
const root = `${INTEGRATION.root}/${path}/attributes`;
|
||||
return INTEGRATION.client.publish(root, JSON.stringify(attributes), { qos: 1, retain: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Publishes the sensor state via the mqtt connection.
|
||||
*
|
||||
* @param {string} path - The entity path name.
|
||||
* @param {string|number} state - The state value.
|
||||
* @returns {Object} Instance of the mqtt client.
|
||||
*/
|
||||
const publishState = (path, state) => {
|
||||
if (path === null || state === null) {
|
||||
return INTEGRATION.client;
|
||||
}
|
||||
const root = `${INTEGRATION.root}/${path}/state`;
|
||||
return INTEGRATION.client.publish(root, `${state}`, { qos: 1, retain: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the app update entity and handles the execute logic.
|
||||
*/
|
||||
const initApp = () => {
|
||||
if (!HARDWARE.support.appUpdate) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.root}/app`;
|
||||
const config = {
|
||||
name: "App",
|
||||
unique_id: `${INTEGRATION.node}_app`,
|
||||
command_topic: `${root}/install`,
|
||||
state_topic: `${root}/version/state`,
|
||||
payload_install: "update",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("update", config)
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === config.command_topic) {
|
||||
console.log("Update App...");
|
||||
hardware.setDisplayStatus("ON", () => {
|
||||
const args = ["-c", `bash <(wget -qO- ${APP.scripts.install}) update`];
|
||||
hardware.execScriptCommand("bash", args, (progress, error) => {
|
||||
if (progress) {
|
||||
console.log(`Progress: ${progress}%`);
|
||||
}
|
||||
updateApp(progress);
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
updateApp();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the app update entity via the mqtt connection.
|
||||
*/
|
||||
const updateApp = async (progress = 0) => {
|
||||
const latest = APP.releases.latest;
|
||||
if (!latest) {
|
||||
return;
|
||||
}
|
||||
const version = {
|
||||
title: latest.title,
|
||||
latest_version: latest.version,
|
||||
installed_version: APP.version,
|
||||
release_summary: latest.summary.slice(0, 250) + "...",
|
||||
release_url: latest.url,
|
||||
update_percentage: progress || null,
|
||||
in_progress: progress && progress > 0 && progress < 100,
|
||||
};
|
||||
publishState("app/version", JSON.stringify(version));
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the shutdown button and handles the execute logic.
|
||||
*/
|
||||
const initShutdown = () => {
|
||||
if (!HARDWARE.support.sudoRights) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.root}/shutdown`;
|
||||
const config = {
|
||||
name: "Shutdown",
|
||||
unique_id: `${INTEGRATION.node}_shutdown`,
|
||||
command_topic: `${root}/execute`,
|
||||
icon: "mdi:power",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("button", config)
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === config.command_topic) {
|
||||
console.log("Shutdown system...");
|
||||
hardware.setDisplayStatus("ON", () => {
|
||||
hardware.shutdownSystem();
|
||||
});
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the reboot button and handles the execute logic.
|
||||
*/
|
||||
const initReboot = () => {
|
||||
if (!HARDWARE.support.sudoRights) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.root}/reboot`;
|
||||
const config = {
|
||||
name: "Reboot",
|
||||
unique_id: `${INTEGRATION.node}_reboot`,
|
||||
command_topic: `${root}/execute`,
|
||||
icon: "mdi:restart",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("button", config)
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === config.command_topic) {
|
||||
console.log("Rebooting system...");
|
||||
hardware.setDisplayStatus("ON", () => {
|
||||
hardware.rebootSystem();
|
||||
});
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the refresh button and handles the execute logic.
|
||||
*/
|
||||
const initRefresh = () => {
|
||||
const root = `${INTEGRATION.root}/refresh`;
|
||||
const config = {
|
||||
name: "Refresh",
|
||||
unique_id: `${INTEGRATION.node}_refresh`,
|
||||
command_topic: `${root}/execute`,
|
||||
icon: "mdi:web-refresh",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("button", config)
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === config.command_topic) {
|
||||
console.log("Refreshing webview...");
|
||||
hardware.setDisplayStatus("ON", () => {
|
||||
EVENTS.emit("reloadView");
|
||||
});
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the kiosk select status and handles the execute logic.
|
||||
*/
|
||||
const initKiosk = () => {
|
||||
const root = `${INTEGRATION.root}/kiosk`;
|
||||
const config = {
|
||||
name: "Kiosk",
|
||||
unique_id: `${INTEGRATION.node}_kiosk`,
|
||||
command_topic: `${root}/set`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ value }}",
|
||||
options: ["Framed", "Fullscreen", "Maximized", "Minimized", "Terminated"],
|
||||
icon: "mdi:overscan",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("select", config)
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === config.command_topic) {
|
||||
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(config.command_topic);
|
||||
updateKiosk();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the kiosk status via the mqtt connection.
|
||||
*/
|
||||
const updateKiosk = async () => {
|
||||
const kiosk = WEBVIEW.tracker.status;
|
||||
publishState("kiosk", kiosk);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the display status, brightness and handles the execute logic.
|
||||
*/
|
||||
const initDisplay = () => {
|
||||
if (!HARDWARE.support.displayStatus) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.root}/display`;
|
||||
const config = {
|
||||
name: "Display",
|
||||
unique_id: `${INTEGRATION.node}_display`,
|
||||
command_topic: `${root}/power/set`,
|
||||
state_topic: `${root}/power/state`,
|
||||
icon: "mdi:monitor-shimmer",
|
||||
platform: "light",
|
||||
device: INTEGRATION.device,
|
||||
...(HARDWARE.support.displayBrightness && {
|
||||
brightness_command_topic: `${root}/brightness/set`,
|
||||
brightness_state_topic: `${root}/brightness/state`,
|
||||
brightness_scale: 100,
|
||||
}),
|
||||
};
|
||||
publishConfig("light", config)
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === config.command_topic) {
|
||||
const status = message.toString();
|
||||
console.log("Set Display Status:", status);
|
||||
hardware.setDisplayStatus(status, (reply, error) => {
|
||||
if (!error) {
|
||||
hardware.update();
|
||||
} else {
|
||||
console.warn("Failed:", error);
|
||||
}
|
||||
});
|
||||
} else if (topic === config.brightness_command_topic) {
|
||||
const brightness = parseInt(message, 10);
|
||||
console.log("Set Display Brightness:", brightness);
|
||||
hardware.setDisplayBrightness(brightness, (reply, error) => {
|
||||
if (!error) {
|
||||
hardware.update();
|
||||
} else {
|
||||
console.warn("Failed:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic)
|
||||
.subscribe(config.brightness_command_topic);
|
||||
updateDisplay();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the display status, brightness via the mqtt connection.
|
||||
*/
|
||||
const updateDisplay = async () => {
|
||||
const status = hardware.getDisplayStatus();
|
||||
const brightness = hardware.getDisplayBrightness();
|
||||
publishState("display/power", status);
|
||||
publishState("display/brightness", brightness);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the keyboard visibility and handles the execute logic.
|
||||
*/
|
||||
const initKeyboard = () => {
|
||||
if (!HARDWARE.support.keyboardVisibility) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.root}/keyboard`;
|
||||
const config = {
|
||||
name: "Keyboard",
|
||||
unique_id: `${INTEGRATION.node}_keyboard`,
|
||||
command_topic: `${root}/set`,
|
||||
state_topic: `${root}/state`,
|
||||
icon: "mdi:keyboard-close-outline",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("switch", config)
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === config.command_topic) {
|
||||
const status = message.toString();
|
||||
console.log("Set Keyboard Visibility:", status);
|
||||
hardware.setDisplayStatus("ON", () => {
|
||||
switch (status) {
|
||||
case "OFF":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
break;
|
||||
case "ON":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.maximize();
|
||||
break;
|
||||
}
|
||||
hardware.setKeyboardVisibility(status);
|
||||
});
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
updateKeyboard();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the keyboard visibility via the mqtt connection.
|
||||
*/
|
||||
const updateKeyboard = async () => {
|
||||
const visibility = hardware.getKeyboardVisibility();
|
||||
publishState("keyboard", visibility);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the page number and handles the execute logic.
|
||||
*/
|
||||
const initPageNumber = () => {
|
||||
if (WEBVIEW.viewUrls.length <= 2) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.root}/page_number`;
|
||||
const config = {
|
||||
name: "Page Number",
|
||||
unique_id: `${INTEGRATION.node}_page_number`,
|
||||
command_topic: `${root}/set`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ value | int }}",
|
||||
mode: "box",
|
||||
min: 1,
|
||||
max: WEBVIEW.viewUrls.length - 1,
|
||||
unit_of_measurement: "Page",
|
||||
icon: "mdi:page-next",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("number", config)
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === config.command_topic) {
|
||||
const number = parseInt(message, 10);
|
||||
console.log("Set Page Number:", number);
|
||||
WEBVIEW.viewActive = number || 1;
|
||||
EVENTS.emit("updateView");
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
updatePageNumber();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the page number via the mqtt connection.
|
||||
*/
|
||||
const updatePageNumber = async () => {
|
||||
const pageNumber = WEBVIEW.viewActive || 1;
|
||||
publishState("page_number", pageNumber);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the page zoom and handles the execute logic.
|
||||
*/
|
||||
const initPageZoom = () => {
|
||||
const root = `${INTEGRATION.root}/page_zoom`;
|
||||
const config = {
|
||||
name: "Page Zoom",
|
||||
unique_id: `${INTEGRATION.node}_page_zoom`,
|
||||
command_topic: `${root}/set`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ value | int }}",
|
||||
mode: "slider",
|
||||
min: 25,
|
||||
max: 400,
|
||||
unit_of_measurement: "%",
|
||||
icon: "mdi:magnify-plus",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("number", config)
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === config.command_topic) {
|
||||
const zoom = parseInt(message, 10);
|
||||
console.log("Set Page Zoom:", zoom);
|
||||
WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.setZoomFactor(zoom / 100.0);
|
||||
EVENTS.emit("updateView");
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
updatePageZoom();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the page zoom via the mqtt connection.
|
||||
*/
|
||||
const updatePageZoom = async () => {
|
||||
const pageZoom = Math.round(WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.getZoomFactor() * 100.0);
|
||||
publishState("page_zoom", pageZoom);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the page url and handles the execute logic.
|
||||
*/
|
||||
const initPageUrl = () => {
|
||||
const root = `${INTEGRATION.root}/page_url`;
|
||||
const config = {
|
||||
name: "Page Url",
|
||||
unique_id: `${INTEGRATION.node}_page_url`,
|
||||
command_topic: `${root}/set`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ value }}",
|
||||
pattern: "https?://.*",
|
||||
icon: "mdi:web",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("text", config)
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === config.command_topic) {
|
||||
const url = message.toString();
|
||||
console.log("Set Page Url:", url);
|
||||
WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.loadURL(url);
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
updatePageUrl();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the page url via the mqtt connection.
|
||||
*/
|
||||
const updatePageUrl = async () => {
|
||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive || 1];
|
||||
const currentUrl = WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.getURL();
|
||||
const pageUrl = !currentUrl || currentUrl.startsWith("data:") ? defaultUrl : currentUrl;
|
||||
publishState("page_url", pageUrl);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the model sensor.
|
||||
*/
|
||||
const initModel = () => {
|
||||
const root = `${INTEGRATION.root}/model`;
|
||||
const config = {
|
||||
name: "Model",
|
||||
unique_id: `${INTEGRATION.node}_model`,
|
||||
state_topic: `${root}/state`,
|
||||
json_attributes_topic: `${root}/attributes`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:raspberry-pi",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("sensor", config);
|
||||
updateModel();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the model sensor via the mqtt connection.
|
||||
*/
|
||||
const updateModel = async () => {
|
||||
const model = hardware.getModel();
|
||||
publishState("model", model);
|
||||
publishAttributes("model", HARDWARE.support);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the serial number sensor.
|
||||
*/
|
||||
const initSerialNumber = () => {
|
||||
const root = `${INTEGRATION.root}/serial_number`;
|
||||
const config = {
|
||||
name: "Serial Number",
|
||||
unique_id: `${INTEGRATION.node}_serial_number`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:hexadecimal",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("sensor", config);
|
||||
updateSerialNumber();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the serial number sensor via the mqtt connection.
|
||||
*/
|
||||
const updateSerialNumber = async () => {
|
||||
const serialNumber = hardware.getSerialNumber();
|
||||
publishState("serial_number", serialNumber);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the network address sensor.
|
||||
*/
|
||||
const initNetworkAddress = () => {
|
||||
const root = `${INTEGRATION.root}/network_address`;
|
||||
const config = {
|
||||
name: "Network Address",
|
||||
unique_id: `${INTEGRATION.node}_network_address`,
|
||||
state_topic: `${root}/state`,
|
||||
json_attributes_topic: `${root}/attributes`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:ip-network",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("sensor", config);
|
||||
updateNetworkAddress();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the network address sensor via the mqtt connection.
|
||||
*/
|
||||
const updateNetworkAddress = async () => {
|
||||
const networkAddresses = hardware.getNetworkAddresses();
|
||||
const [name] = Object.keys(networkAddresses);
|
||||
const [family] = name ? Object.keys(networkAddresses[name]) : [];
|
||||
const networkAddress = networkAddresses[name]?.[family]?.[0] || null;
|
||||
publishState("network_address", networkAddress);
|
||||
publishAttributes("network_address", networkAddresses);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the host name sensor.
|
||||
*/
|
||||
const initHostName = () => {
|
||||
const root = `${INTEGRATION.root}/host_name`;
|
||||
const config = {
|
||||
name: "Host Name",
|
||||
unique_id: `${INTEGRATION.node}_host_name`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:console-network",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("sensor", config);
|
||||
updateHostName();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the host name sensor via the mqtt connection.
|
||||
*/
|
||||
const updateHostName = async () => {
|
||||
const hostName = hardware.getHostName();
|
||||
publishState("host_name", hostName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the up time sensor.
|
||||
*/
|
||||
const initUpTime = () => {
|
||||
const root = `${INTEGRATION.root}/up_time`;
|
||||
const config = {
|
||||
name: "Up Time",
|
||||
unique_id: `${INTEGRATION.node}_up_time`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ (value | float) | round(0) }}",
|
||||
unit_of_measurement: "min",
|
||||
icon: "mdi:timeline-clock",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("sensor", config);
|
||||
updateUpTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the up time sensor via the mqtt connection.
|
||||
*/
|
||||
const updateUpTime = async () => {
|
||||
const upTime = hardware.getUpTime();
|
||||
publishState("up_time", upTime);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the memory size sensor.
|
||||
*/
|
||||
const initMemorySize = () => {
|
||||
const root = `${INTEGRATION.root}/memory_size`;
|
||||
const config = {
|
||||
name: "Memory Size",
|
||||
unique_id: `${INTEGRATION.node}_memory_size`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ (value | float) | round(2) }}",
|
||||
unit_of_measurement: "GiB",
|
||||
icon: "mdi:memory",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("sensor", config);
|
||||
updateMemorySize();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the memory size sensor via the mqtt connection.
|
||||
*/
|
||||
const updateMemorySize = async () => {
|
||||
const memorySize = hardware.getMemorySize();
|
||||
publishState("memory_size", memorySize);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the memory usage sensor.
|
||||
*/
|
||||
const initMemoryUsage = () => {
|
||||
const root = `${INTEGRATION.root}/memory_usage`;
|
||||
const config = {
|
||||
name: "Memory Usage",
|
||||
unique_id: `${INTEGRATION.node}_memory_usage`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ (value | float) | round(0) }}",
|
||||
unit_of_measurement: "%",
|
||||
icon: "mdi:memory-arrow-down",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("sensor", config);
|
||||
updateMemoryUsage();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the memory usage sensor via the mqtt connection.
|
||||
*/
|
||||
const updateMemoryUsage = async () => {
|
||||
const memoryUsage = hardware.getMemoryUsage();
|
||||
publishState("memory_usage", memoryUsage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the processor usage sensor.
|
||||
*/
|
||||
const initProcessorUsage = () => {
|
||||
const root = `${INTEGRATION.root}/processor_usage`;
|
||||
const config = {
|
||||
name: "Processor Usage",
|
||||
unique_id: `${INTEGRATION.node}_processor_usage`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ (value | float) | round(0) }}",
|
||||
unit_of_measurement: "%",
|
||||
icon: "mdi:cpu-64-bit",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("sensor", config);
|
||||
updateProcessorUsage();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the processor usage sensor via the mqtt connection.
|
||||
*/
|
||||
const updateProcessorUsage = async () => {
|
||||
const processorUsage = hardware.getProcessorUsage();
|
||||
publishState("processor_usage", processorUsage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the processor temperature sensor.
|
||||
*/
|
||||
const initProcessorTemperature = () => {
|
||||
const root = `${INTEGRATION.root}/processor_temperature`;
|
||||
const config = {
|
||||
name: "Processor Temperature",
|
||||
unique_id: `${INTEGRATION.node}_processor_temperature`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ (value | float) | round(0) }}",
|
||||
unit_of_measurement: "°C",
|
||||
icon: "mdi:radiator",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("sensor", config);
|
||||
updateProcessorTemperature();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the processor temperature sensor via the mqtt connection.
|
||||
*/
|
||||
const updateProcessorTemperature = async () => {
|
||||
const processorTemperature = hardware.getProcessorTemperature();
|
||||
publishState("processor_temperature", processorTemperature);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the battery level sensor.
|
||||
*/
|
||||
const initBatteryLevel = () => {
|
||||
if (!HARDWARE.support.batteryLevel) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.root}/battery_level`;
|
||||
const config = {
|
||||
name: "Battery Level",
|
||||
unique_id: `${INTEGRATION.node}_battery_level`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ (value | float) | round(0) }}",
|
||||
unit_of_measurement: "%",
|
||||
icon: "mdi:battery-medium",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("sensor", config);
|
||||
updateBatteryLevel();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the battery level sensor via the mqtt connection.
|
||||
*/
|
||||
const updateBatteryLevel = async () => {
|
||||
const batteryLevel = hardware.getBatteryLevel();
|
||||
publishState("battery_level", batteryLevel);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the package upgrades sensor.
|
||||
*/
|
||||
const initPackageUpgrades = () => {
|
||||
const root = `${INTEGRATION.root}/package_upgrades`;
|
||||
const config = {
|
||||
name: "Package Upgrades",
|
||||
unique_id: `${INTEGRATION.node}_package_upgrades`,
|
||||
state_topic: `${root}/state`,
|
||||
json_attributes_topic: `${root}/attributes`,
|
||||
value_template: "{{ value | int }}",
|
||||
icon: "mdi:package-down",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("sensor", config);
|
||||
updatePackageUpgrades();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the package upgrades sensor via the mqtt connection.
|
||||
*/
|
||||
const updatePackageUpgrades = async () => {
|
||||
const packages = hardware.checkPackageUpgrades();
|
||||
const attributes = {
|
||||
total: packages.length,
|
||||
packages: packages.map((pkg) => pkg.replace(/\s*\[.*?\]\s*/g, "").trim()),
|
||||
};
|
||||
publishState("package_upgrades", attributes.total);
|
||||
publishAttributes("package_upgrades", attributes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the heartbeat sensor.
|
||||
*/
|
||||
const initHeartbeat = () => {
|
||||
const root = `${INTEGRATION.root}/heartbeat`;
|
||||
const config = {
|
||||
name: "Heartbeat",
|
||||
unique_id: `${INTEGRATION.node}_heartbeat`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:heart-flash",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("sensor", config);
|
||||
updateHeartbeat();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the heartbeat sensor via the mqtt connection.
|
||||
*/
|
||||
const updateHeartbeat = async () => {
|
||||
const now = new Date();
|
||||
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000);
|
||||
const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, "");
|
||||
publishState("heartbeat", heartbeat);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the last active sensor.
|
||||
*/
|
||||
const initLastActive = () => {
|
||||
const root = `${INTEGRATION.root}/last_active`;
|
||||
const config = {
|
||||
name: "Last Active",
|
||||
unique_id: `${INTEGRATION.node}_last_active`,
|
||||
state_topic: `${root}/state`,
|
||||
value_template: "{{ (value | float) | round(0) }}",
|
||||
unit_of_measurement: "min",
|
||||
icon: "mdi:gesture-tap-hold",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
publishConfig("sensor", config);
|
||||
updateLastActive();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the last active sensor via the mqtt connection.
|
||||
*/
|
||||
const updateLastActive = async () => {
|
||||
const now = new Date();
|
||||
const then = WEBVIEW.tracker.pointer.time;
|
||||
const lastActive = (now - then) / (1000 * 60);
|
||||
publishState("last_active", lastActive);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
update,
|
||||
};
|
||||
+954
@@ -0,0 +1,954 @@
|
||||
const path = require("path");
|
||||
const axios = require("axios");
|
||||
const hardware = require("./hardware");
|
||||
const integration = require("./integration");
|
||||
const { app, nativeTheme, globalShortcut, ipcMain, session, BaseWindow, WebContentsView } = require("electron");
|
||||
|
||||
global.WEBVIEW = global.WEBVIEW || {
|
||||
initialized: false,
|
||||
tracker: {
|
||||
pointer: {
|
||||
position: {},
|
||||
time: new Date(),
|
||||
},
|
||||
display: {},
|
||||
status: null,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the webview with the provided arguments.
|
||||
*
|
||||
* @returns {bool} Returns true if the initialization was successful.
|
||||
*/
|
||||
const init = async () => {
|
||||
if (ARGS.web_url.length === 0) {
|
||||
console.error("Please provide the '--web-url' parameter");
|
||||
return app.quit();
|
||||
}
|
||||
if (ARGS.web_url.some((url) => !/^https?:\/\//.test(url))) {
|
||||
console.error("Please provide the '--web-url' parameter with http(s)");
|
||||
return app.quit();
|
||||
}
|
||||
session.defaultSession.clearCache();
|
||||
|
||||
// Parse arguments
|
||||
const fullscreen = ARGS.app_debug !== "true";
|
||||
const widget = ARGS.web_widget ? ARGS.web_widget === "true" : true;
|
||||
const zoom = !isNaN(parseFloat(ARGS.web_zoom)) ? parseFloat(ARGS.web_zoom) : 1.25;
|
||||
const theme = ["light", "dark"].includes(ARGS.web_theme) ? ARGS.web_theme : "dark";
|
||||
const urls = [loaderHtml(40, 1.0, theme)].concat(ARGS.web_url);
|
||||
|
||||
// Init global properties
|
||||
WEBVIEW.viewActive = 0;
|
||||
WEBVIEW.viewUrls = urls;
|
||||
WEBVIEW.viewZoom = zoom;
|
||||
WEBVIEW.viewTheme = theme;
|
||||
WEBVIEW.pagerEnabled = widget;
|
||||
WEBVIEW.widgetTheme = theme;
|
||||
WEBVIEW.widgetEnabled = widget;
|
||||
WEBVIEW.navigationTheme = theme;
|
||||
WEBVIEW.navigationEnabled = widget;
|
||||
nativeTheme.themeSource = WEBVIEW.viewTheme;
|
||||
|
||||
// Init global root window
|
||||
WEBVIEW.window = new BaseWindow({
|
||||
title: APP.title,
|
||||
icon: path.join(APP.path, "img", "icon.png"),
|
||||
});
|
||||
WEBVIEW.window.setMenuBarVisibility(false);
|
||||
WEBVIEW.window.setFullScreen(fullscreen);
|
||||
if (!fullscreen) {
|
||||
WEBVIEW.window.maximize();
|
||||
}
|
||||
|
||||
// Init global webview
|
||||
WEBVIEW.views = [];
|
||||
urls.forEach((url, i) => {
|
||||
const view = new WebContentsView({
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
view.setVisible(i === 0);
|
||||
view.setBackgroundColor("#FFFFFFFF");
|
||||
WEBVIEW.window.contentView.addChildView(view);
|
||||
WEBVIEW.views.push(view);
|
||||
onlineStatus(url).then(() => {
|
||||
view.webContents.loadURL(url);
|
||||
});
|
||||
});
|
||||
|
||||
// Init global pager
|
||||
WEBVIEW.pager = new WebContentsView({
|
||||
transparent: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
});
|
||||
WEBVIEW.pager.setBackgroundColor("#00000000");
|
||||
WEBVIEW.window.contentView.addChildView(WEBVIEW.pager);
|
||||
WEBVIEW.pager.webContents.loadFile(path.join(APP.path, "html", "pager.html"));
|
||||
|
||||
// Init global widget
|
||||
WEBVIEW.widget = new WebContentsView({
|
||||
transparent: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
});
|
||||
WEBVIEW.widget.setBackgroundColor("#00000000");
|
||||
WEBVIEW.window.contentView.addChildView(WEBVIEW.widget);
|
||||
WEBVIEW.widget.webContents.loadFile(path.join(APP.path, "html", "widget.html"));
|
||||
|
||||
// Init global navigation
|
||||
WEBVIEW.navigation = new WebContentsView({
|
||||
transparent: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
});
|
||||
WEBVIEW.navigation.setBackgroundColor("#00000000");
|
||||
WEBVIEW.window.contentView.addChildView(WEBVIEW.navigation);
|
||||
WEBVIEW.navigation.webContents.loadFile(path.join(APP.path, "html", "navigation.html"));
|
||||
|
||||
// Register global events
|
||||
EVENTS.on("reloadView", reloadView);
|
||||
EVENTS.on("updateView", updateView);
|
||||
EVENTS.on("updateDisplay", () => {
|
||||
const status = hardware.getDisplayStatus();
|
||||
if (status) {
|
||||
WEBVIEW.tracker.display[status.toLowerCase()] = new Date();
|
||||
}
|
||||
});
|
||||
|
||||
// Register local events
|
||||
await windowEvents();
|
||||
await widgetEvents();
|
||||
await navigationEvents();
|
||||
await viewEvents();
|
||||
await appEvents();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the shared webview properties.
|
||||
*/
|
||||
const update = async () => {
|
||||
if (!WEBVIEW.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update window status
|
||||
if (WEBVIEW.window.isFullScreen()) {
|
||||
WEBVIEW.tracker.status = "Fullscreen";
|
||||
} else if (WEBVIEW.window.isMinimized()) {
|
||||
WEBVIEW.tracker.status = "Minimized";
|
||||
} else if (WEBVIEW.window.isMaximized()) {
|
||||
WEBVIEW.tracker.status = "Maximized";
|
||||
} else {
|
||||
WEBVIEW.tracker.status = "Framed";
|
||||
}
|
||||
|
||||
// Update widget status
|
||||
updateWidget();
|
||||
|
||||
// Update navigation status
|
||||
updateNavigation();
|
||||
|
||||
// Update integration sensor
|
||||
console.log("Update Kiosk Status:", WEBVIEW.tracker.status);
|
||||
integration.update();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the active view.
|
||||
*/
|
||||
const updateView = () => {
|
||||
if (!WEBVIEW.viewActive) {
|
||||
return;
|
||||
}
|
||||
const url = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL();
|
||||
const host = url.startsWith("data:") ? "whoopsie" : new URL(url).host;
|
||||
const title = `${APP.title} - ${host} (${WEBVIEW.viewActive})`;
|
||||
|
||||
// Update window title
|
||||
console.log(`Update View: ${title}`);
|
||||
WEBVIEW.window.setTitle(title);
|
||||
|
||||
// Hide all other webviews and show only the active one
|
||||
WEBVIEW.views.forEach((view, i) => {
|
||||
view.setVisible(i === WEBVIEW.viewActive);
|
||||
});
|
||||
update();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the widget control.
|
||||
*/
|
||||
const updateWidget = () => {
|
||||
// Hide keyboard button
|
||||
WEBVIEW.widget.webContents.send("button-hidden", {
|
||||
id: "keyboard",
|
||||
hidden: !HARDWARE.support.keyboardVisibility,
|
||||
});
|
||||
|
||||
// Hide navigation button
|
||||
WEBVIEW.widget.webContents.send("button-hidden", {
|
||||
id: "navigation",
|
||||
hidden: !WEBVIEW.navigationEnabled,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the navigation control.
|
||||
*/
|
||||
const updateNavigation = () => {
|
||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||
const currentUrl = view.webContents.getURL();
|
||||
|
||||
// Hide pager buttons
|
||||
WEBVIEW.navigation.webContents.send("button-hidden", {
|
||||
id: "previous",
|
||||
hidden: WEBVIEW.viewUrls.length <= 2,
|
||||
});
|
||||
WEBVIEW.navigation.webContents.send("button-hidden", {
|
||||
id: "next",
|
||||
hidden: WEBVIEW.viewUrls.length <= 2,
|
||||
});
|
||||
|
||||
// Disable pager buttons
|
||||
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||
id: "previous",
|
||||
disabled: WEBVIEW.viewActive <= 1,
|
||||
});
|
||||
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||
id: "next",
|
||||
disabled: WEBVIEW.viewActive >= WEBVIEW.viewUrls.length - 1,
|
||||
});
|
||||
|
||||
// Update url text
|
||||
WEBVIEW.navigation.webContents.send("input-text", {
|
||||
id: "url",
|
||||
text: currentUrl.startsWith("data:") ? "" : currentUrl,
|
||||
placeholder: defaultUrl.startsWith("data:") ? "" : defaultUrl,
|
||||
});
|
||||
|
||||
// Disable zoom buttons
|
||||
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||
id: "minus",
|
||||
disabled: view.webContents.getZoomFactor().toFixed(2) <= 0.25,
|
||||
});
|
||||
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||
id: "plus",
|
||||
disabled: view.webContents.getZoomFactor().toFixed(2) >= 4.0,
|
||||
});
|
||||
|
||||
// Disable history buttons
|
||||
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||
id: "backward",
|
||||
disabled: !view.webContents.navigationHistory.canGoBack(),
|
||||
});
|
||||
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||
id: "forward",
|
||||
disabled: !view.webContents.navigationHistory.canGoForward(),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows or hides the webview navigation bar.
|
||||
*/
|
||||
const toggleNavigation = () => {
|
||||
const window = WEBVIEW.window.getBounds();
|
||||
const navigation = WEBVIEW.navigation.getBounds();
|
||||
const height = navigation.height > 0 ? 0 : 60;
|
||||
|
||||
// Show or hide navigation based on height
|
||||
WEBVIEW.navigation.setBounds({
|
||||
x: 0,
|
||||
y: window.height - height,
|
||||
width: window.width,
|
||||
height: height,
|
||||
});
|
||||
if (height > 0) {
|
||||
WEBVIEW.navigation.webContents.focus();
|
||||
} else {
|
||||
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
|
||||
}
|
||||
|
||||
// Resize webview
|
||||
resizeView();
|
||||
};
|
||||
|
||||
/**
|
||||
* Decreases page zoom on the active webview.
|
||||
*/
|
||||
const zoomMinus = () => {
|
||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||
view.webContents.setZoomFactor(Math.max(0.25, view.webContents.getZoomFactor() - 0.1));
|
||||
update();
|
||||
};
|
||||
|
||||
/**
|
||||
* Increases page zoom on the active webview.
|
||||
*/
|
||||
const zoomPlus = () => {
|
||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||
view.webContents.setZoomFactor(Math.min(4.0, view.webContents.getZoomFactor() + 0.1));
|
||||
update();
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates backward in the history of the active webview.
|
||||
*/
|
||||
const historyBackward = () => {
|
||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||
if (view.webContents.navigationHistory.canGoBack()) {
|
||||
view.webContents.navigationHistory.goBack();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates forward in the history of the active webview.
|
||||
*/
|
||||
const historyForward = () => {
|
||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||
if (view.webContents.navigationHistory.canGoForward()) {
|
||||
view.webContents.navigationHistory.goForward();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Activates the previous webview page.
|
||||
*/
|
||||
const previousView = () => {
|
||||
if (WEBVIEW.viewActive > 1) WEBVIEW.viewActive--;
|
||||
updateView();
|
||||
};
|
||||
|
||||
/**
|
||||
* Activates the next webview page.
|
||||
*/
|
||||
const nextView = () => {
|
||||
if (WEBVIEW.viewActive < WEBVIEW.views.length - 1) WEBVIEW.viewActive++;
|
||||
updateView();
|
||||
};
|
||||
|
||||
/**
|
||||
* Reloads the default url and settings on the active webview.
|
||||
*/
|
||||
const homeView = () => {
|
||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||
const currentUrl = view.webContents.getURL();
|
||||
|
||||
// Reload the default url or refresh the page
|
||||
if (currentUrl != defaultUrl) {
|
||||
view.webContents.loadURL(defaultUrl);
|
||||
} else {
|
||||
view.webContents.reloadIgnoringCache();
|
||||
}
|
||||
|
||||
// Reset page zoom and history
|
||||
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
||||
setTimeout(() => {
|
||||
view.webContents.navigationHistory.clear();
|
||||
update();
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reloads the current url on the active webview.
|
||||
*/
|
||||
const reloadView = () => {
|
||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||
const currentUrl = view.webContents.getURL();
|
||||
|
||||
// Reload the default url or refresh the page
|
||||
if (currentUrl.startsWith("data:")) {
|
||||
view.webContents.loadURL(defaultUrl);
|
||||
} else {
|
||||
view.webContents.reloadIgnoringCache();
|
||||
}
|
||||
update();
|
||||
};
|
||||
|
||||
/**
|
||||
* Resizes and positions all webviews.
|
||||
*/
|
||||
const resizeView = () => {
|
||||
const window = WEBVIEW.window.getBounds();
|
||||
const navigation = WEBVIEW.navigation.getBounds();
|
||||
const pager = { width: 20, height: window.height };
|
||||
const widget = { width: 60, height: 200 };
|
||||
|
||||
// Update view size
|
||||
WEBVIEW.views.forEach((view) => {
|
||||
view.setBounds({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: window.width,
|
||||
height: window.height - navigation.height,
|
||||
});
|
||||
});
|
||||
|
||||
// Update pager size
|
||||
if (WEBVIEW.pagerEnabled) {
|
||||
WEBVIEW.pager.setBounds({
|
||||
x: window.width - pager.width,
|
||||
y: 0,
|
||||
width: pager.width,
|
||||
height: pager.height,
|
||||
});
|
||||
WEBVIEW.pager.webContents.send("data-theme", { theme: "hidden" });
|
||||
}
|
||||
|
||||
// Update widget size
|
||||
if (WEBVIEW.widgetEnabled) {
|
||||
WEBVIEW.widget.setBounds({
|
||||
x: window.width - 20,
|
||||
y: parseInt(window.height / 2 - widget.height / 2, 10),
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
});
|
||||
WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" });
|
||||
}
|
||||
|
||||
// Update navigation size
|
||||
if (WEBVIEW.navigationEnabled) {
|
||||
WEBVIEW.navigation.setBounds({
|
||||
x: 0,
|
||||
y: window.height - navigation.height,
|
||||
width: window.width,
|
||||
height: navigation.height,
|
||||
});
|
||||
WEBVIEW.navigation.webContents.send("data-theme", { theme: WEBVIEW.navigationTheme });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Register window events and handler.
|
||||
*/
|
||||
const windowEvents = async () => {
|
||||
// Handle window resize events
|
||||
WEBVIEW.window.on("ready-to-show", resizeView);
|
||||
WEBVIEW.window.on("resize", resizeView);
|
||||
resizeView();
|
||||
|
||||
// Handle window status updates
|
||||
WEBVIEW.window.on("minimize", update);
|
||||
WEBVIEW.window.on("restore", update);
|
||||
WEBVIEW.window.on("maximize", update);
|
||||
WEBVIEW.window.on("unmaximize", update);
|
||||
WEBVIEW.window.on("enter-full-screen", update);
|
||||
WEBVIEW.window.on("leave-full-screen", update);
|
||||
|
||||
// Handle global shortcut events
|
||||
globalShortcut.register("Control+Left", () => {
|
||||
previousView();
|
||||
});
|
||||
globalShortcut.register("Control+Right", () => {
|
||||
nextView();
|
||||
});
|
||||
globalShortcut.register("Control+numsub", () => {
|
||||
zoomMinus();
|
||||
});
|
||||
globalShortcut.register("Control+numadd", () => {
|
||||
zoomPlus();
|
||||
});
|
||||
globalShortcut.register("Alt+Left", () => {
|
||||
historyBackward();
|
||||
});
|
||||
globalShortcut.register("Alt+Right", () => {
|
||||
historyForward();
|
||||
});
|
||||
|
||||
// Check for window touch events (1s)
|
||||
setInterval(() => {
|
||||
const now = new Date();
|
||||
const then = WEBVIEW.tracker.pointer.time;
|
||||
const delta = (now - then) / 1000;
|
||||
|
||||
// Auto-hide navigation
|
||||
if (delta > 60) {
|
||||
const navigation = WEBVIEW.navigation.getBounds();
|
||||
if (navigation.height > 0) {
|
||||
toggleNavigation();
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register widget events and handler.
|
||||
*/
|
||||
const widgetEvents = async () => {
|
||||
if (!WEBVIEW.widgetEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle widget focus events
|
||||
WEBVIEW.widget.webContents.on("focus", () => {
|
||||
const window = WEBVIEW.window.getBounds();
|
||||
const widget = WEBVIEW.widget.getBounds();
|
||||
|
||||
// Show widget
|
||||
WEBVIEW.widget.setBounds({
|
||||
x: window.width - 60,
|
||||
y: widget.y,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
});
|
||||
WEBVIEW.widget.webContents.send("data-theme", { theme: WEBVIEW.widgetTheme });
|
||||
});
|
||||
|
||||
// Handle widget blur events
|
||||
WEBVIEW.widget.webContents.on("blur", () => {
|
||||
const window = WEBVIEW.window.getBounds();
|
||||
const widget = WEBVIEW.widget.getBounds();
|
||||
|
||||
// Hide widget
|
||||
WEBVIEW.widget.setBounds({
|
||||
x: window.width - 20,
|
||||
y: widget.y,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
});
|
||||
WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" });
|
||||
});
|
||||
|
||||
// Handle widget button click events
|
||||
ipcMain.on("button-click", (e, button) => {
|
||||
switch (button.id) {
|
||||
case "keyboard":
|
||||
const toggle = hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON";
|
||||
switch (toggle) {
|
||||
case "OFF":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
break;
|
||||
case "ON":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.maximize();
|
||||
break;
|
||||
}
|
||||
hardware.setKeyboardVisibility(toggle, () => {
|
||||
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
|
||||
});
|
||||
break;
|
||||
case "fullscreen":
|
||||
if (WEBVIEW.window.isFullScreen()) {
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
} else {
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
}
|
||||
hardware.setKeyboardVisibility("OFF", () => {
|
||||
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
|
||||
});
|
||||
break;
|
||||
case "minimize":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.minimize();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "navigation":
|
||||
toggleNavigation();
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register navigation events and handler.
|
||||
*/
|
||||
const navigationEvents = async () => {
|
||||
if (!WEBVIEW.navigationEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle navigation blur events
|
||||
let ignoreBlur = false;
|
||||
WEBVIEW.navigation.webContents.on("blur", () => {
|
||||
const visibility = hardware.getKeyboardVisibility();
|
||||
if (visibility === "ON" && !ignoreBlur) {
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
WEBVIEW.navigation.webContents.send("input-blur", { id: "url" });
|
||||
}
|
||||
});
|
||||
|
||||
// Handle input focus events
|
||||
ipcMain.on("input-focus", (e, input) => {
|
||||
const visibility = hardware.getKeyboardVisibility();
|
||||
switch (input.id) {
|
||||
case "url":
|
||||
if (visibility === "OFF") {
|
||||
ignoreBlur = true;
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.maximize();
|
||||
hardware.setKeyboardVisibility("ON", () => {
|
||||
setTimeout(() => {
|
||||
WEBVIEW.navigation.webContents.focus();
|
||||
WEBVIEW.navigation.webContents.send("input-select", { id: "url" });
|
||||
ignoreBlur = false;
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle input enter events
|
||||
ipcMain.on("input-enter", (e, input) => {
|
||||
switch (input.id) {
|
||||
case "url":
|
||||
let url = input.text.trim();
|
||||
if (url && !/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(url)) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
if (!url) {
|
||||
url = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||
}
|
||||
WEBVIEW.views[WEBVIEW.viewActive].webContents.loadURL(url);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle navigation button click events
|
||||
ipcMain.on("button-click", (e, button) => {
|
||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||
switch (button.id) {
|
||||
case "home":
|
||||
homeView();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "refresh":
|
||||
reloadView();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "previous":
|
||||
previousView();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "next":
|
||||
nextView();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "minus":
|
||||
zoomMinus();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "plus":
|
||||
zoomPlus();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "backward":
|
||||
historyBackward();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "forward":
|
||||
historyForward();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register view events and handler.
|
||||
*/
|
||||
const viewEvents = async () => {
|
||||
const ready = [];
|
||||
WEBVIEW.views.forEach((view, i) => {
|
||||
// Enable webview touch emulation
|
||||
view.webContents.debugger.attach("1.1");
|
||||
view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", {
|
||||
configuration: "mobile",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
// Redirect webview hyperlinks
|
||||
view.webContents.setWindowOpenHandler(({ url }) => {
|
||||
view.webContents.loadURL(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
// Update webview layout
|
||||
view.webContents.on("dom-ready", () => {
|
||||
view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
|
||||
if (ready.length < WEBVIEW.views.length) {
|
||||
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
||||
ready.push(i);
|
||||
}
|
||||
});
|
||||
|
||||
// Webview fully loaded
|
||||
view.webContents.on("did-finish-load", () => {
|
||||
if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) {
|
||||
nextView();
|
||||
}
|
||||
if (ARGS.app_debug === "true") {
|
||||
setTimeout(() => {
|
||||
view.webContents.openDevTools();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// Webview not loaded
|
||||
view.webContents.on("did-fail-load", (e, code, text, url, mainframe) => {
|
||||
if (mainframe) {
|
||||
if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) {
|
||||
nextView();
|
||||
}
|
||||
view.webContents.loadURL(errorHtml(code, text, url));
|
||||
}
|
||||
});
|
||||
|
||||
// Webview url changed
|
||||
view.webContents.on("did-navigate-in-page", (e, url, mainframe) => {
|
||||
if (mainframe) {
|
||||
updateView();
|
||||
}
|
||||
});
|
||||
view.webContents.on("did-navigate", () => {
|
||||
updateView();
|
||||
});
|
||||
|
||||
// Handle webview mouse events
|
||||
view.webContents.on("before-mouse-event", (e, mouse) => {
|
||||
const now = new Date();
|
||||
const then = WEBVIEW.tracker.pointer.time;
|
||||
const delta = (now - then) / 1000;
|
||||
switch (mouse.type) {
|
||||
case "mouseMove":
|
||||
const posNew = { x: mouse.globalX, y: mouse.globalY };
|
||||
if (posNew.x < 0 || posNew.y < 0) {
|
||||
break;
|
||||
}
|
||||
// Update last active on pointer position change
|
||||
const posOld = WEBVIEW.tracker.pointer.position;
|
||||
if (posOld.x !== posNew.x || posOld.y !== posNew.y) {
|
||||
WEBVIEW.tracker.pointer.time = now;
|
||||
WEBVIEW.tracker.pointer.position = posNew;
|
||||
if (delta > 30) {
|
||||
console.log("Update Last Active");
|
||||
integration.update();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "mouseDown":
|
||||
switch (mouse.button) {
|
||||
case "left":
|
||||
// Ignore touch event if display was off
|
||||
if (WEBVIEW.tracker.display.off > WEBVIEW.tracker.display.on) {
|
||||
console.log("Display Touch Event: Ignored");
|
||||
e.preventDefault();
|
||||
}
|
||||
// Turn display on if it was off
|
||||
if (hardware.getDisplayStatus() === "OFF") {
|
||||
hardware.setDisplayStatus("ON");
|
||||
}
|
||||
break;
|
||||
case "back":
|
||||
historyBackward();
|
||||
break;
|
||||
case "forward":
|
||||
historyForward();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register app events and handler.
|
||||
*/
|
||||
const appEvents = async () => {
|
||||
// Handle multiple instances
|
||||
app.on("second-instance", () => {
|
||||
if (WEBVIEW.window.isMinimized()) {
|
||||
WEBVIEW.window.restore();
|
||||
}
|
||||
WEBVIEW.window.focus();
|
||||
});
|
||||
|
||||
// Handle signal and exit events
|
||||
app.on("before-quit", () => {
|
||||
WEBVIEW.tracker.status = "Terminated";
|
||||
console.warn(`${APP.title} Terminated`);
|
||||
integration.update();
|
||||
});
|
||||
process.on("SIGINT", app.quit);
|
||||
|
||||
// Webview initialized
|
||||
WEBVIEW.initialized = true;
|
||||
|
||||
// Check for latest release infos (2h)
|
||||
setInterval(() => {
|
||||
latestRelease();
|
||||
}, 7200 * 1000);
|
||||
await latestRelease();
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the latest app release infos from github.
|
||||
*/
|
||||
const latestRelease = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${APP.releases.url}/latest`, { timeout: 10000 });
|
||||
const data = response ? response.data : null;
|
||||
if (!data || data.draft || data.prerelease) {
|
||||
return;
|
||||
}
|
||||
APP.releases.latest = {
|
||||
title: APP.title,
|
||||
version: (data.tag_name || data.name || "").replace(/^v/i, ""),
|
||||
summary: data.body || "",
|
||||
url: data.html_url || "",
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("Github Error:", error.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks for network connectivity by requesting a known url.
|
||||
*
|
||||
* @param {string} url - Url to request.
|
||||
* @param {number} interval - Interval between requests in milliseconds.
|
||||
* @param {number} timeout - Maximum time to repeat requests in milliseconds.
|
||||
* @returns {Promise<boolean>} Resolves true if online, false on timeout.
|
||||
*/
|
||||
const onlineStatus = (url, interval = 1000, timeout = 60000) => {
|
||||
return new Promise((resolve) => {
|
||||
const start = Date.now();
|
||||
const check = async () => {
|
||||
const elapsed = Date.now() - start;
|
||||
try {
|
||||
if (!url.startsWith("data:")) {
|
||||
await axios.get(url, { timeout: 10000 });
|
||||
}
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
if (elapsed >= interval) {
|
||||
console.warn(`Checking Connection: ${url}`, error.message);
|
||||
}
|
||||
if (elapsed >= timeout) {
|
||||
resolve(false);
|
||||
} else {
|
||||
setTimeout(check, interval);
|
||||
}
|
||||
}
|
||||
};
|
||||
check();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a html template for a spinning loader.
|
||||
*
|
||||
* @param {number} size - The size of the circle.
|
||||
* @param {number} speed - The rotation speed of the circle.
|
||||
* @param {string} theme - The theme used for spinner colors.
|
||||
* @returns {string} A data string with the generated html.
|
||||
*/
|
||||
const loaderHtml = (size, speed, theme) => {
|
||||
const color = {
|
||||
dark: { border: "#2A2A2A", spinner: "#03A9F4", background: "#111111" },
|
||||
light: { border: "#DCDCDC", spinner: "#03A9F4", background: "#FAFAFA" },
|
||||
}[theme];
|
||||
const html = `
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: ${color.background};
|
||||
}
|
||||
.spinner {
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid ${color.border};
|
||||
border-top-color: ${color.spinner};
|
||||
animation: spin ${speed}s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="spinner"></div>
|
||||
</body>
|
||||
</html>`;
|
||||
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a html template for an error page.
|
||||
*
|
||||
* @param {number} code - The error code of the response.
|
||||
* @param {string} text - The error text of the response.
|
||||
* @param {string} url - The url of the requested page.
|
||||
* @returns {string} A data string with the generated html.
|
||||
*/
|
||||
const errorHtml = (code, text, url) => {
|
||||
const html = `
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
.icon {
|
||||
margin: 0;
|
||||
font-size: 5rem;
|
||||
color: orange;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p class="icon">⚠</p>
|
||||
<h1 class="title">Whoopsie!</h1>
|
||||
<p><strong>Loading:</strong> ${url}</p>
|
||||
<p><strong>Error:</strong> ${text} (${code})</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
update,
|
||||
};
|
||||
+13
-9
@@ -1,27 +1,31 @@
|
||||
{
|
||||
"name": "touchkio",
|
||||
"title": "TouchKio",
|
||||
"description": "Home Assistant Touch Kiosk",
|
||||
"homepage": "https://github.com/leukipp/touchkio",
|
||||
"author": "leukipp",
|
||||
"version": "1.1.1",
|
||||
"version": "1.3.1",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "electron . --web-url=https://demo.home-assistant.io",
|
||||
"start": "electron . --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app --app-debug=false --trace-warnings",
|
||||
"build": "electron-forge make"
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 120
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.6.0",
|
||||
"@electron-forge/maker-deb": "^7.6.0",
|
||||
"@electron-forge/maker-zip": "^7.6.0",
|
||||
"@electron-forge/publisher-github": "^7.6.0",
|
||||
"electron": "^33.3.1",
|
||||
"prettier": "^3.4.2"
|
||||
"@electron-forge/cli": "^7.8.3",
|
||||
"@electron-forge/maker-deb": "^7.8.3",
|
||||
"@electron-forge/maker-zip": "^7.8.3",
|
||||
"@electron-forge/publisher-github": "^7.8.3",
|
||||
"electron": "^38.0.0",
|
||||
"jszip": "^3.10.1",
|
||||
"prettier": "^3.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"mqtt": "^5.10.3"
|
||||
"axios": "^1.11.0",
|
||||
"electron-log": "^5.4.3",
|
||||
"mqtt": "^5.14.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,762 +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;
|
||||
}
|
||||
if (!/^mqtts?:\/\//.test(args.mqtt_url)) {
|
||||
console.error("Please provide the '--mqtt-url' parameter with mqtt(s)");
|
||||
return app.quit();
|
||||
}
|
||||
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 vendor = hardware.getVendor();
|
||||
const hostName = hardware.getHostName();
|
||||
const serialNumber = hardware.getSerialNumber();
|
||||
const serialNumberSuffix = serialNumber.slice(-6);
|
||||
|
||||
const deviceId = serialNumberSuffix.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
||||
const deviceName = hostName.charAt(0).toUpperCase() + hostName.slice(1);
|
||||
|
||||
// Init globals
|
||||
INTEGRATION.node = `rpi_${deviceId}`;
|
||||
INTEGRATION.discovery = discovery;
|
||||
INTEGRATION.device = {
|
||||
name: `TouchKio ${deviceName}`,
|
||||
model: model,
|
||||
manufacturer: vendor,
|
||||
serial_number: serialNumber,
|
||||
identifiers: [INTEGRATION.node],
|
||||
sw_version: `${app.getName()}-v${app.getVersion()}`,
|
||||
};
|
||||
|
||||
// Connection settings
|
||||
const options = user === null || password == null ? null : { username: user, password: password };
|
||||
const masked = password === null ? "null" : "*".repeat(password.length);
|
||||
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);
|
||||
initKeyboard(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);
|
||||
initPackageUpgrades(INTEGRATION.client);
|
||||
initHeartbeat(INTEGRATION.client);
|
||||
initLastActive(INTEGRATION.client);
|
||||
|
||||
// Integration initialized
|
||||
INTEGRATION.initialized = true;
|
||||
INTEGRATION.status = "online";
|
||||
})
|
||||
.on("error", (error) => {
|
||||
console.error("MQTT:", error);
|
||||
INTEGRATION.status = "offline";
|
||||
});
|
||||
|
||||
// Update sensor states from notifiers
|
||||
HARDWARE.display.notifiers.push(() => {
|
||||
updateDisplay(INTEGRATION.client);
|
||||
});
|
||||
HARDWARE.keyboard.notifiers.push(() => {
|
||||
updateKeyboard(INTEGRATION.client);
|
||||
});
|
||||
|
||||
// Update time sensors periodically (30s)
|
||||
setInterval(() => {
|
||||
updateHeartbeat(INTEGRATION.client);
|
||||
updateLastActive(INTEGRATION.client);
|
||||
}, 30 * 1000);
|
||||
|
||||
// Update system sensors periodically (1min)
|
||||
setInterval(() => {
|
||||
update();
|
||||
}, 60 * 1000);
|
||||
|
||||
// Update package sensors periodically (60min)
|
||||
setInterval(() => {
|
||||
updatePackageUpgrades(INTEGRATION.client);
|
||||
}, 3600 * 1000);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the shared integration properties.
|
||||
*/
|
||||
const update = () => {
|
||||
if (!INTEGRATION.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update client sensors
|
||||
updateKiosk(INTEGRATION.client);
|
||||
updateUpTime(INTEGRATION.client);
|
||||
updateLastActive(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`;
|
||||
const config = {
|
||||
name: "Shutdown",
|
||||
unique_id: `${INTEGRATION.node}_shutdown`,
|
||||
command_topic: `${root}/execute`,
|
||||
icon: "mdi:power",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { 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`;
|
||||
const config = {
|
||||
name: "Reboot",
|
||||
unique_id: `${INTEGRATION.node}_reboot`,
|
||||
command_topic: `${root}/execute`,
|
||||
icon: "mdi:restart",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { 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`;
|
||||
const config = {
|
||||
name: "Refresh",
|
||||
unique_id: `${INTEGRATION.node}_refresh`,
|
||||
command_topic: `${root}/execute`,
|
||||
icon: "mdi:web-refresh",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { 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`;
|
||||
const config = {
|
||||
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,
|
||||
};
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { 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`;
|
||||
if (kiosk !== null) {
|
||||
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) => {
|
||||
if (!HARDWARE.support.displayStatus) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`;
|
||||
const config = {
|
||||
name: `Display`,
|
||||
unique_id: `${INTEGRATION.node}_display`,
|
||||
command_topic: `${root}/set`,
|
||||
state_topic: `${root}/status`,
|
||||
icon: "mdi:monitor-shimmer",
|
||||
platform: "light",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
if (HARDWARE.support.displayBrightness) {
|
||||
config.brightness_scale = 100;
|
||||
config.brightness_command_topic = `${root}/brightness/set`;
|
||||
config.brightness_state_topic = `${root}/brightness/status`;
|
||||
}
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { 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`;
|
||||
if (status !== null) {
|
||||
client.publish(`${root}/status`, `${status}`, { qos: 1, retain: true });
|
||||
}
|
||||
if (brightness !== null) {
|
||||
client.publish(`${root}/brightness/status`, `${brightness}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the keyboard visibility and handles the execute logic.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initKeyboard = (client) => {
|
||||
if (!HARDWARE.support.keyboardVisibility) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.discovery}/switch/${INTEGRATION.node}/keyboard`;
|
||||
const config = {
|
||||
name: `Keyboard`,
|
||||
unique_id: `${INTEGRATION.node}_keyboard`,
|
||||
command_topic: `${root}/set`,
|
||||
state_topic: `${root}/status`,
|
||||
icon: "mdi:keyboard-close-outline",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === `${root}/set`) {
|
||||
const status = message.toString();
|
||||
console.log("Set Keyboard Visibility:", status);
|
||||
hardware.setKeyboardVisibility(status);
|
||||
switch (status) {
|
||||
case "OFF":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
break;
|
||||
case "ON":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.maximize();
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.subscribe(`${root}/set`);
|
||||
updateKeyboard(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the keyboard visibility via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateKeyboard = (client) => {
|
||||
const visibility = hardware.getKeyboardVisibility();
|
||||
const root = `${INTEGRATION.discovery}/switch/${INTEGRATION.node}/keyboard`;
|
||||
if (visibility !== null) {
|
||||
client.publish(`${root}/status`, `${visibility}`, { 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`;
|
||||
const config = {
|
||||
name: `Model`,
|
||||
unique_id: `${INTEGRATION.node}_model`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:raspberry-pi",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { 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`;
|
||||
if (model !== null) {
|
||||
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`;
|
||||
const config = {
|
||||
name: `Serial Number`,
|
||||
unique_id: `${INTEGRATION.node}_serial_number`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:hexadecimal",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { 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`;
|
||||
if (serialNumber !== null) {
|
||||
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`;
|
||||
const config = {
|
||||
name: `Host Name`,
|
||||
unique_id: `${INTEGRATION.node}_host_name`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:console-network",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { 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`;
|
||||
if (hostName !== null) {
|
||||
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`;
|
||||
const config = {
|
||||
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,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { 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`;
|
||||
if (upTime !== null) {
|
||||
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`;
|
||||
const config = {
|
||||
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,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { 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`;
|
||||
if (memorySize !== null) {
|
||||
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`;
|
||||
const config = {
|
||||
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,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { 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`;
|
||||
if (memoryUsage !== null) {
|
||||
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`;
|
||||
const config = {
|
||||
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,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { 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`;
|
||||
if (processorUsage !== null) {
|
||||
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`;
|
||||
const config = {
|
||||
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,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { 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`;
|
||||
if (processorTemperature !== null) {
|
||||
client.publish(`${root}/status`, `${processorTemperature}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the package upgrades sensor.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initPackageUpgrades = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/package_upgrades`;
|
||||
const config = {
|
||||
name: `Package Upgrades`,
|
||||
unique_id: `${INTEGRATION.node}_package_upgrades`,
|
||||
state_topic: `${root}/status`,
|
||||
json_attributes_topic: `${root}/attributes`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:package-down",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||
updatePackageUpgrades(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the package upgrades sensor via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updatePackageUpgrades = (client) => {
|
||||
const packages = hardware.checkPackageUpgrades();
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/package_upgrades`;
|
||||
if (packages !== null) {
|
||||
const attributes = {
|
||||
total: packages.length,
|
||||
packages: packages.map((pkg) => pkg.replace(/\s*\[.*?\]\s*/g, "").trim()),
|
||||
};
|
||||
client.publish(`${root}/status`, `${attributes.total}`, { qos: 1, retain: true });
|
||||
client.publish(`${root}/attributes`, JSON.stringify(attributes), { 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`;
|
||||
const config = {
|
||||
name: `Heartbeat`,
|
||||
unique_id: `${INTEGRATION.node}_heartbeat`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:heart-flash",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { 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 });
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the last active sensor.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initLastActive = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/last_active`;
|
||||
const config = {
|
||||
name: `Last Active`,
|
||||
unique_id: `${INTEGRATION.node}_last_active`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ (value | float) | round(0) }}",
|
||||
unit_of_measurement: "min",
|
||||
icon: "mdi:gesture-tap-hold",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||
updateLastActive(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the last active sensor via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateLastActive = (client) => {
|
||||
const now = new Date();
|
||||
const then = WEBVIEW.pointer.time;
|
||||
const lastActive = Math.abs(now - then) / (1000 * 60);
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/last_active`;
|
||||
client.publish(`${root}/status`, `${lastActive}`, { qos: 1, retain: true });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
update,
|
||||
};
|
||||
-320
@@ -1,320 +0,0 @@
|
||||
const path = require("path");
|
||||
const hardware = require("./hardware");
|
||||
const integration = require("./integration");
|
||||
const { app, screen, nativeTheme, ipcMain, BaseWindow, WebContentsView } = require("electron");
|
||||
|
||||
global.WEBVIEW = global.WEBVIEW || {
|
||||
initialized: false,
|
||||
status: "offline",
|
||||
locked: false,
|
||||
pointer: {
|
||||
position: {},
|
||||
time: new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
if (!/^https?:\/\//.test(args.web_url)) {
|
||||
console.error("Please provide the '--web-url' parameter with http(s)");
|
||||
return app.quit();
|
||||
}
|
||||
const url = new URL(args.web_url);
|
||||
const theme = ["light", "dark"].includes(args.web_theme) ? args.web_theme : "dark";
|
||||
const zoom = !isNaN(parseFloat(args.web_zoom)) ? parseFloat(args.web_zoom) : 1.25;
|
||||
const widget = args.web_widget ? args.web_widget === "true" : true;
|
||||
|
||||
// Init global layout
|
||||
WEBVIEW.viewZoom = zoom;
|
||||
WEBVIEW.viewTheme = theme;
|
||||
WEBVIEW.widgetTheme = theme;
|
||||
WEBVIEW.widgetEnabled = widget;
|
||||
nativeTheme.themeSource = WEBVIEW.viewTheme;
|
||||
|
||||
// Init global root window
|
||||
WEBVIEW.window = new BaseWindow({
|
||||
title: `TouchKio - ${url.host}`,
|
||||
icon: path.join(__dirname, "..", "img", "icon.png"),
|
||||
});
|
||||
WEBVIEW.window.setMenuBarVisibility(false);
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
|
||||
// Init global webview
|
||||
WEBVIEW.view = new WebContentsView({
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
WEBVIEW.window.contentView.addChildView(WEBVIEW.view);
|
||||
WEBVIEW.view.webContents.loadURL(url.toString());
|
||||
WEBVIEW.view.setBackgroundColor("#FFFFFFFF");
|
||||
|
||||
// Init global widget
|
||||
WEBVIEW.widget = new WebContentsView({
|
||||
transparent: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
});
|
||||
WEBVIEW.window.contentView.addChildView(WEBVIEW.widget);
|
||||
WEBVIEW.widget.webContents.loadFile(path.join(__dirname, "widget.html"));
|
||||
WEBVIEW.widget.setBackgroundColor("#00000000");
|
||||
|
||||
// Register events
|
||||
windowEvents();
|
||||
widgetEvents();
|
||||
domEvents();
|
||||
appEvents();
|
||||
|
||||
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 widget status
|
||||
if (!HARDWARE.support.keyboardVisibility) {
|
||||
WEBVIEW.widget.webContents.send("button-hide", { id: "keyboard" });
|
||||
}
|
||||
|
||||
// Update integration sensor
|
||||
console.log("Update Kiosk Status:", WEBVIEW.status);
|
||||
integration.update();
|
||||
};
|
||||
|
||||
/**
|
||||
* Register window events and handler.
|
||||
*/
|
||||
const windowEvents = () => {
|
||||
// Handle window resize events
|
||||
const resize = () => {
|
||||
const window = WEBVIEW.window.getBounds();
|
||||
const widget = { width: 60, height: 200 };
|
||||
|
||||
// Update view size
|
||||
WEBVIEW.view.setBounds({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: window.width,
|
||||
height: window.height,
|
||||
});
|
||||
WEBVIEW.view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
||||
WEBVIEW.view.webContents.focus();
|
||||
|
||||
// Update widget size
|
||||
if (WEBVIEW.widgetEnabled) {
|
||||
WEBVIEW.widget.setBounds({
|
||||
x: window.width - 15,
|
||||
y: parseInt(window.height / 2 - widget.height / 2),
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
});
|
||||
WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" });
|
||||
}
|
||||
};
|
||||
WEBVIEW.window.on("ready-to-show", resize);
|
||||
WEBVIEW.window.on("resize", resize);
|
||||
resize();
|
||||
|
||||
// Handle window focus events
|
||||
WEBVIEW.window.on("focus", () => {
|
||||
if (WEBVIEW.locked) {
|
||||
hardware.setDisplayStatus("ON");
|
||||
WEBVIEW.window.blur();
|
||||
}
|
||||
WEBVIEW.locked = false;
|
||||
});
|
||||
HARDWARE.display.notifiers.push(() => {
|
||||
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
|
||||
if (WEBVIEW.locked) {
|
||||
WEBVIEW.window.blur();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle window status updates
|
||||
WEBVIEW.window.on("minimize", update);
|
||||
WEBVIEW.window.on("restore", update);
|
||||
WEBVIEW.window.on("maximize", update);
|
||||
WEBVIEW.window.on("unmaximize", update);
|
||||
WEBVIEW.window.on("enter-full-screen", update);
|
||||
WEBVIEW.window.on("leave-full-screen", update);
|
||||
|
||||
// Handle window touch events for activity tracking
|
||||
setInterval(() => {
|
||||
const posOld = WEBVIEW.pointer.position;
|
||||
const posNew = screen.getCursorScreenPoint();
|
||||
if (posOld.x !== posNew.x || posOld.y !== posNew.y) {
|
||||
const now = new Date();
|
||||
const then = WEBVIEW.pointer.time;
|
||||
|
||||
// Update integration sensor
|
||||
WEBVIEW.pointer.time = now;
|
||||
if (Math.abs(now - then) / 1000 > 30) {
|
||||
console.log("Update Last Active");
|
||||
integration.update();
|
||||
}
|
||||
}
|
||||
WEBVIEW.pointer.position = posNew;
|
||||
}, 1 * 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register widget events and handler.
|
||||
*/
|
||||
const widgetEvents = () => {
|
||||
if (!WEBVIEW.widgetEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle widget focus events
|
||||
WEBVIEW.widget.webContents.on("focus", () => {
|
||||
const window = WEBVIEW.window.getBounds();
|
||||
const widget = WEBVIEW.widget.getBounds();
|
||||
|
||||
// Show widget
|
||||
WEBVIEW.widget.setBounds({
|
||||
x: window.width - 60,
|
||||
y: widget.y,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
});
|
||||
WEBVIEW.widget.webContents.send("data-theme", { theme: WEBVIEW.widgetTheme });
|
||||
});
|
||||
|
||||
// Handle widget blur events
|
||||
WEBVIEW.widget.webContents.on("blur", () => {
|
||||
const window = WEBVIEW.window.getBounds();
|
||||
const widget = WEBVIEW.widget.getBounds();
|
||||
|
||||
// Hide widget
|
||||
WEBVIEW.widget.setBounds({
|
||||
x: window.width - 15,
|
||||
y: widget.y,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
});
|
||||
WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" });
|
||||
});
|
||||
|
||||
// Handle widget button click events
|
||||
ipcMain.on("button-click", (e, button) => {
|
||||
switch (button.id) {
|
||||
case "refresh":
|
||||
WEBVIEW.view.webContents.reloadIgnoringCache();
|
||||
break;
|
||||
case "fullscreen":
|
||||
if (WEBVIEW.window.isFullScreen()) {
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
} else {
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
}
|
||||
break;
|
||||
case "minimize":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.minimize();
|
||||
break;
|
||||
case "keyboard":
|
||||
const toggle = hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON";
|
||||
hardware.setKeyboardVisibility(toggle);
|
||||
switch (toggle) {
|
||||
case "OFF":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
break;
|
||||
case "ON":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.maximize();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register dom events and handler.
|
||||
*/
|
||||
const domEvents = () => {
|
||||
// Enable webview touch emulation
|
||||
WEBVIEW.view.webContents.debugger.attach("1.1");
|
||||
WEBVIEW.view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", {
|
||||
configuration: "mobile",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
// Disable webview hyperlinks
|
||||
WEBVIEW.view.webContents.setWindowOpenHandler(() => {
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
// Remove webview scrollbar
|
||||
WEBVIEW.view.webContents.on("dom-ready", () => {
|
||||
WEBVIEW.view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
|
||||
});
|
||||
|
||||
// Webview fully loaded
|
||||
WEBVIEW.view.webContents.on("did-finish-load", () => {
|
||||
// view.webContents.openDevTools();
|
||||
WEBVIEW.initialized = true;
|
||||
update();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register app events and handler.
|
||||
*/
|
||||
const appEvents = () => {
|
||||
// Handle signal and exit events
|
||||
process.on("SIGINT", app.quit);
|
||||
app.on("before-quit", () => {
|
||||
WEBVIEW.status = "Terminated";
|
||||
integration.update();
|
||||
});
|
||||
|
||||
// Handle multiple instances
|
||||
app.on("second-instance", () => {
|
||||
if (WEBVIEW.window.isMinimized()) {
|
||||
WEBVIEW.window.restore();
|
||||
}
|
||||
WEBVIEW.window.focus();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
update,
|
||||
};
|
||||
Reference in New Issue
Block a user