Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aed79ab519 | ||
|
|
e2397bad65 | ||
|
|
e7622bef17 | ||
|
|
2553f52983 | ||
|
|
30b5ec2d8b | ||
|
|
94c7e34706 | ||
|
|
09dda65333 | ||
|
|
99f465a044 | ||
|
|
02d1309345 | ||
|
|
3142606d12 | ||
|
|
51189e8595 | ||
|
|
71a9d4c1de | ||
|
|
f022db9478 | ||
|
|
fcc0e5813e | ||
|
|
6d23efd713 | ||
|
|
a37226fd39 | ||
|
|
9b8067a598 | ||
|
|
257dab8d7f | ||
|
|
ff4b9d1262 | ||
|
|
757ba9ff92 | ||
|
|
ed3bad7292 | ||
|
|
f00a49e4e4 | ||
|
|
bbe20fef8f | ||
|
|
cfbc391a52 | ||
|
|
b1998ceea9 | ||
|
|
081cee2990 | ||
|
|
fd746cc79a | ||
|
|
4f426df4e6 | ||
|
|
7084fc8188 | ||
|
|
b2685f8d6b | ||
|
|
5743b0ff08 | ||
|
|
a7f4ede899 | ||
|
|
3201786d41 | ||
|
|
0656f8f897 | ||
|
|
dbcfb74923 | ||
|
|
640156ba87 | ||
|
|
8d2f3da93e | ||
|
|
286e8c1758 | ||
|
|
c8f254d90a | ||
|
|
5e3d204019 | ||
|
|
926bcdfab8 |
@@ -8,9 +8,6 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
arch: [arm64]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -30,4 +27,4 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
yarn electron-forge publish --arch arm64
|
yarn electron-forge publish --arch arm64,x64
|
||||||
|
|||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
# Supported Hardware
|
||||||
|
This document contains an incomplete list of device and display hardware combinations that have already been tested.
|
||||||
|
|
||||||
|
The [release](https://github.com/leukipp/touchkio/releases) page has builds exclusively for **arm64** and **x64**, but custom builds for other architectures can be made (see [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development)), allowing the application to operate on any hardware.
|
||||||
|
At least the **minimal** features, such as displaying a **kiosk window** (which doesn't necessarily need to be Home Assistant) will work.
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
If you are running Linux with a graphical user interface (Wayland or X11), you should be well equipped to use the application. Additionally, any single board computer (SBC) clones of the Raspberry Pi that operate on Raspberry Pi OS **(64-bit)** are likely to function as well.
|
||||||
|
|
||||||
|
| | Status | Notes |
|
||||||
|
| --- | --------------------- | ------------------------------------------------------------------------- |
|
||||||
|
| 🟩 | Fully operational | Working display power, brightness and keyboard control via MQTT. |
|
||||||
|
| 🟨 | Partially operational | Display brightness control is not available via MQTT. |
|
||||||
|
| 🟧 | Partially operational | Display brightness and keyboard control is not available via MQTT. |
|
||||||
|
| 🟥 | Partially operational | Display power, brightness and keyboard control is not available via MQTT. |
|
||||||
|
| ⬜ | Somehow operational | Issues can occur and the overall performance is very slow. |
|
||||||
|
| ⬛ | Not operational | The house is on fire. |
|
||||||
|
|
||||||
|
### DSI
|
||||||
|
| Device | System | Display | Status |
|
||||||
|
| ---------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------ |
|
||||||
|
| Raspberry Pi 3 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | ⬜ |
|
||||||
|
| Raspberry Pi 3 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | ⬜ |
|
||||||
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | 🟩 |
|
||||||
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | 🟩 |
|
||||||
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | 🟩 |
|
||||||
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | 🟩 |
|
||||||
|
|
||||||
|
### HDMI
|
||||||
|
| Device | System | Display | Status |
|
||||||
|
| ------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------ |
|
||||||
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [LAFVIN 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 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 | [Portable Touch 18.5" FHD (1920x1080)](https://www.amazon.de/dp/B0CT2KLDBQ) | 🟨 |
|
||||||
|
| 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 | 🟥 |
|
||||||
|
|
||||||
|
## Contributions
|
||||||
|
In case your hardware is not listed above don't worry, give it a try.
|
||||||
|
Running `touchkio --web-url=https://demo.home-assistant.io` will most likely just work.
|
||||||
|
The only problems that may arise are when controlling the display or keyboard via the Home Assistant integration.
|
||||||
|
|
||||||
|
- If you encounter any problems, please create a new [issue](https://github.com/leukipp/touchkio/issues).
|
||||||
|
- If you encounter any problems and are able to fix it yourself, feel free to create a [pull request](https://github.com/leukipp/touchkio/pulls).
|
||||||
|
- If everything works as expected and your hardware is not yet listed, you are welcome to [report](https://github.com/leukipp/touchkio/issues/12) it or create a [pull request](https://github.com/leukipp/touchkio/pulls).
|
||||||
@@ -1,32 +1,38 @@
|
|||||||
# TouchKio
|
# TouchKio
|
||||||
**TouchKio** is a Node.js application that utilizes **Electron** to create a kiosk mode specifically designed for a Home Assistant dashboard.
|
**TouchKio** is a Node.js application that utilizes Electron to create a kiosk mode window specifically designed for a Home Assistant dashboard.
|
||||||
This tool is packaged as a **.deb** file, making it easy to launch the kiosk mode on a **Raspberry Pi** equipped with the official 7" touch display.
|
This tool is packaged as a **.deb** file, making it easy to launch the kiosk application on any Debian based Linux [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) (e.g. **Raspberry Pi**) equipped with a **DSI or HDMI** Touch Display.
|
||||||
|
Additional releases for other Linux systems are available as **.zip** file.
|
||||||
|
|
||||||
[](https://github.com/leukipp/touchkio/blob/main/img/display.png)
|
[](https://github.com/leukipp/touchkio/blob/main/img/display.png)
|
||||||
|
|
||||||
This implementation addresses common issues encountered when using the built-in browser running in fullscreen mode on the Raspberry Pi touch display.
|
This implementation addresses common issues encountered when using the built-in browser running in fullscreen mode on a Linux device with Touch Display.
|
||||||
Moreover, the device running the **kiosk application** also offers several **Home Assistant MQTT** sensors, enhancing it's functionality for automation purposes.
|
Moreover, the device running the **kiosk application** also offers several **Home Assistant MQTT** sensors, enhancing it's functionality for automation purposes.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- [x] Fast and easy setup.
|
- [x] Fast and easy setup.
|
||||||
|
- [x] Remember login credentials.
|
||||||
|
- [x] Side panel widget for kiosk control.
|
||||||
- [x] Touch friendly web browser interface.
|
- [x] Touch friendly web browser interface.
|
||||||
- [x] Configurable zoom and theme support.
|
- [x] Configurable zoom and theme support.
|
||||||
- [x] Single-tap screen wake-up functionality.
|
- [x] Single-tap screen wake-up functionality.
|
||||||
- [x] Remote controllable via MQTT.
|
- [x] Remote controllable via MQTT.
|
||||||
- [x] Manage kiosk window status.
|
- [x] Manage kiosk window status.
|
||||||
|
- [x] Manage touch keyboard status.
|
||||||
- [x] Touch display power and brightness.
|
- [x] Touch display power and brightness.
|
||||||
|
- [x] Show available system package upgrades.
|
||||||
- [x] Execute system reboot and shutdown commands.
|
- [x] Execute system reboot and shutdown commands.
|
||||||
- [x] Monitor temperature, processor and memory usage.
|
- [x] Monitor temperature, processor and memory usage.
|
||||||
|
|
||||||
The kiosk application can be executed with command line arguments to load a **Home Assistant dashboard in fullscreen** mode.
|
The kiosk application can be executed with command line arguments to load a **Home Assistant dashboard in fullscreen** mode.
|
||||||
Additionally, a **MQTT endpoint** can be defined, allowing the application to provide controls and sensors for the Raspberry Pi and the connected touch display.
|
Additionally, a **MQTT endpoint** can be defined, allowing the application to provide controls and sensors for the Linux device and the connected Touch Display.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
Before you begin, ensure that you have a Raspberry Pi configured and running with a compatible touch display.
|
Before you begin, make sure that you have a Linux device configured and operational with a [compatible](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) Touch Display.
|
||||||
This guide assumes you are using **Raspberry Pi OS Lite (64-bit)**, which is recommended for kiosk applications due to its **lightweight** nature and minimal overhead.
|
This guide assumes that you are using a Raspberry Pi with the latest version of Raspberry Pi OS **(64-bit)**, along with a desktop environment (preferred using **labwc**).
|
||||||
|
However, the **.deb** setup procedure is also compatible with any other Debian based 64-bit system.
|
||||||
|
|
||||||
### Optional
|
### Optional
|
||||||
To utilize the sensor features of your display through Home Assistant, it's essential to have a **MQTT broker running** and the **MQTT integration installed** on your Home Assistant instance.
|
To utilize the sensor features of your device through Home Assistant, it's essential to have a **MQTT broker running** and the **MQTT integration installed** on your Home Assistant instance.
|
||||||
This setup allows seamless communication between your kiosk device and Home Assistant, enabling **real-time data exchange**.
|
This setup allows seamless communication between your kiosk device and Home Assistant, enabling **real-time data exchange**.
|
||||||
|
|
||||||
[](https://github.com/leukipp/touchkio/blob/main/img/mqtt.png)
|
[](https://github.com/leukipp/touchkio/blob/main/img/mqtt.png)
|
||||||
@@ -36,25 +42,50 @@ For a comprehensive guide on setting up MQTT with Home Assistant, please refer t
|
|||||||
## Installation
|
## Installation
|
||||||
On the first run of the application, you may encounter a **setup procedure** and the Home Assistant **login screen**.
|
On the first run of the application, you may encounter a **setup procedure** and the Home Assistant **login screen**.
|
||||||
It's recommended to create a **dedicated user** (local access only) for your kiosk device.
|
It's recommended to create a **dedicated user** (local access only) for your kiosk device.
|
||||||
You might also need a keyboard or remote VNC access to input these credentials once.
|
|
||||||
|
You might also need a physical keyboard or remote VNC access to input these credentials once.
|
||||||
|
If your hardware is [supported](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) you may be able to activate the on-screen keyboard using the side [widget](https://github.com/leukipp/touchkio/issues/16).
|
||||||
|
|
||||||
#### Option 1 - The easy way
|
#### Option 1 - The easy way
|
||||||
Run this command to download and install the release **.deb** file. It will also adjust some permissions, create a systemd file for auto-startup and will guide you through the setup process:
|
Run this command to download and install the latest **.deb** (arm64 or x64) release.
|
||||||
|
It will also create a systemd user file for auto-startup and will guide you through the setup process:
|
||||||
```bash
|
```bash
|
||||||
bash <(wget -qO- https://raw.githubusercontent.com/leukipp/touchkio/main/install.sh)
|
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.
|
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 script can also be re-run to **update** an existing installation to the latest version.
|
||||||
|
|
||||||
#### Option 2 - The hard way
|
The systemd **user service** is enabled by default and the kiosk application should start automatically the next time you boot.
|
||||||
|
If you need manual control use:
|
||||||
|
```bash
|
||||||
|
systemctl --user start|stop|status|restart touchkio.service
|
||||||
|
```
|
||||||
|
|
||||||
|
<details><summary>Alternatives</summary><div>
|
||||||
|
|
||||||
|
#### Option 2 - The standard way
|
||||||
|
When connected via SSH, it's necessary to export the display variables first, as outlined in the [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development) section.
|
||||||
The [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script mentioned above performs the following tasks (and you just have to do it manually):
|
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 **.deb** file,
|
- [Download](https://github.com/leukipp/touchkio/releases/latest) the latest version file that is suitable for your architecture (arm64 or x64).
|
||||||
which is specifically suitable for Raspberry Pi **(64-bit)** ARM architecture.
|
- Debian (**deb**): Open a terminal and execute the following command to install the application, e.g:
|
||||||
- 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 dpkg -i touchkio_1.x.x_arm64.deb`
|
- Others (**zip**): Extract the archive and run the binary, e.g: `unzip touchkio-linux-x64-1.x.x.zip && cd touchkio-linux-x64 && ./touchkio --setup`
|
||||||
- If you just want to load a Home Assistant dashboard without further control you are good to go, e.g: `touchkio --web-url=https://demo.home-assistant.io`
|
- If you just want to load a Home Assistant dashboard without further control you are good to go, e.g: `touchkio --web-url=https://demo.home-assistant.io`
|
||||||
|
- The `--web-url` doesn't necessarily need to be a Home Assistant url, any kind of website can be shown in kiosk mode.
|
||||||
|
- Only when using the MQTT integration via `--mqtt-*`, a running Home Assistant instance is required.
|
||||||
|
- If you need the application to be automatically started on boot, create a systemd file.
|
||||||
|
|
||||||
If you need the application to be automatically started on boot, create a **systemd file**.
|
#### Option 3 - The hard way
|
||||||
If you need the Home Assistant MQTT sensors, create the **udev rule** for backlight control.
|
Pre-built release files are available for arm64 and x64 Linux systems.
|
||||||
|
If you are using a different architecture, you can still utilize this repository to build your own application.
|
||||||
|
|
||||||
|
For more information please refer to the [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development) section, however this will do the job:
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
</div></details>
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
Running `touchkio --setup` will prompt you to enter arguments that will be used when the application starts without any specified arguments.
|
Running `touchkio --setup` will prompt you to enter arguments that will be used when the application starts without any specified arguments.
|
||||||
@@ -62,17 +93,18 @@ These default arguments are stored in `~/.config/touchkio/Arguments.json`, where
|
|||||||
|
|
||||||
### WEB
|
### WEB
|
||||||
The available arguments to control the kiosk application via terminal are as follows:
|
The available arguments to control the kiosk application via terminal are as follows:
|
||||||
| Name | Default | Description |
|
| Name | Default | Description |
|
||||||
| ------------------------ | ------- | ------------------------------------------------------ |
|
| ------------------------- | ------- | ------------------------------------------------------ |
|
||||||
| `--web-url` (Required) | - | Url of the Home Assistant instance (HTTP(S)://IP:PORT) |
|
| `--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-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-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 window.
|
These arguments allow you to customize the appearance of the web browser view.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
```bash
|
```bash
|
||||||
touchkio --web-url=http://192.168.1.42:8123 --web-theme=dark --web-zoom=1.25
|
touchkio --web-url=http://192.168.1.42:8123 --web-theme=light --web-zoom=1.0
|
||||||
```
|
```
|
||||||
|
|
||||||
### MQTT
|
### MQTT
|
||||||
@@ -85,7 +117,7 @@ To broadcast your local sensor data to Home Assistant, you can use the following
|
|||||||
| `--mqtt-discovery` (Optional) | `homeassistant` | The discovery prefix for MQTT (`homeassistant` works with default setups) |
|
| `--mqtt-discovery` (Optional) | `homeassistant` | The discovery prefix for MQTT (`homeassistant` works with default setups) |
|
||||||
|
|
||||||
When you start the application with the MQTT arguments, the Home Assistant auto-discovery feature will automatically add controls and sensors that can be used for further integration.
|
When you start the application with the MQTT arguments, the Home Assistant auto-discovery feature will automatically add controls and sensors that can be used for further integration.
|
||||||
You can find them under **Settings** -> **Devices and Services** -> **Devices** by searching for "Raspberry Pi".
|
You can find them under **Settings** -> **Devices and Services** -> **Devices** by searching for **TouchKio**.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
```bash
|
```bash
|
||||||
@@ -93,19 +125,60 @@ touchkio --web-url=http://192.168.1.42:8123 --mqtt-url=mqtt://192.168.1.42:1883
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
If you want to create your own local build install [nodejs](https://pimylifeup.com/raspberry-pi-nodejs) and [yarn](https://classic.yarnpkg.com/lang/en/docs/install) on your Raspberry Pi.
|
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.
|
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` file.
|
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 can 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:
|
||||||
|
```bash
|
||||||
|
export DISPLAY=":0"
|
||||||
|
export WAYLAND_DISPLAY="wayland-0"
|
||||||
|
```
|
||||||
|
To make this permanent, consider adding the export variables into the `~/.bashrc` file.
|
||||||
|
|
||||||
|
### Extensions
|
||||||
|
|
||||||
|
<details><summary>You probably won't need this.</summary><div></br>
|
||||||
|
|
||||||
|
Incorporating custom extensions and external hardware (like motion sensors, ultrasonic sensors, cameras, relays and switches) via **Raspberry Pi's GPIO/USB** involves several steps.
|
||||||
|
While using external sensors that directly integrate with Home Assistant and by utilizing automation's to interact with **TouchKio via MQTT** is generally easier and **recommended**, here's a rough guide on how to proceed with custom hardware integration:
|
||||||
|
|
||||||
|
1. **Install Node.js library**: Use Yarn to add a library that can interact with your hardware (GPIO, USB, etc.):
|
||||||
|
```bash
|
||||||
|
yarn add [package-name]
|
||||||
|
```
|
||||||
|
This will update the `package.json` file with the required dependencies.
|
||||||
|
|
||||||
|
2. **Import the library**: Open the `hardware.js` file and import the library using:
|
||||||
|
```javascript
|
||||||
|
const package = require("[package-name]");
|
||||||
|
```
|
||||||
|
Then implement your custom methods and logic to handle the hardware. Don't forget to export the methods at the end of the file:
|
||||||
|
```javascript
|
||||||
|
module.exports = { ... };
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Expose sensors via MQTT**: If you want to publish sensor data through MQTT, implement some init and update methods in the `integration.js` file:
|
||||||
|
```javascript
|
||||||
|
const initCustomSensor = (client) => { ... }
|
||||||
|
const updateCustomSensor = (client) => { ... }
|
||||||
|
```
|
||||||
|
To get started have a look at the existing methods. Don't forget to call the custom sensor initialization method inside the global init method, where the MQTT connection is established:
|
||||||
|
```javascript
|
||||||
|
const init = async (args) => { ... }
|
||||||
|
```
|
||||||
|
From there, you will need to further refine your code by tinkering with sensor updates. This can be achieved through either periodic update calls or event based triggers.
|
||||||
|
|
||||||
|
</div></details>
|
||||||
|
|
||||||
### The nitty gritty
|
### The nitty gritty
|
||||||
|
|
||||||
<details><summary>Don't waste your time reading this.</summary><div></br>
|
<details><summary>Don't waste your time reading this.</summary><div></br>
|
||||||
|
|
||||||
To enable **write access** to the `/sys/class/backlight/10-0045/bl_power` and `/sys/class/backlight/10-0045/brightness` files, you need to set up a **udev rule**.
|
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.
|
||||||
This is done within the `install.sh` script, which also creates a `.service` file.
|
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.
|
||||||
While creating a service file is optional, it's highly recommended if you want your Raspberry Pi to automatically boot into kiosk mode.
|
|
||||||
|
|
||||||
The Raspberry Pi's **build-in screen blanking** function uses the command `swayidle -w timeout 600 'wlopm --off \*' resume 'wlopm --on \*' &` inside `~/.config/labwc/autostart` to blank the screen after **10 minutes**.
|
The Raspberry Pi's **build-in screen blanking** function uses the command `swayidle -w timeout 600 'wlopm --off \*' resume 'wlopm --on \*' &` inside `~/.config/labwc/autostart` to blank the screen after **10 minutes**.
|
||||||
The `wlopm --off \*` command changes the `bl_power` value to **4**, when setting the value to **0** the screen will turn on again.
|
The `wlopm --off \*` command changes the `bl_power` value to **4**, when setting the value to **0** the screen will turn on again.
|
||||||
@@ -116,20 +189,25 @@ I managed to achieve this for the `brightness` file by implementing a simple `fs
|
|||||||
However, I found that it **never triggered** for the `bl_power` file.
|
However, I found that it **never triggered** for the `bl_power` file.
|
||||||
Although the file content changes, none of the filesystem listeners where fired.
|
Although the file content changes, none of the filesystem listeners where fired.
|
||||||
This could be due to `swayidle`/`wlopm` performing write actions at a deeper level that are not detectable by file listeners.
|
This could be due to `swayidle`/`wlopm` performing write actions at a deeper level that are not detectable by file listeners.
|
||||||
As a result, I went for a **polling solution**, checking the state of both files 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.
|
While I understand this is not ideal, it's necessary to ensure proper functionality.
|
||||||
|
|
||||||
|
The display power status and brightness can be adjusted via the MQTT integration.
|
||||||
|
**Support** for changing the power status on **DSI and HDMI** displays is achieved by checking for connected screens in `/sys/class/drm/*/status`.
|
||||||
|
Support for changing the brightness of connected display is implemented by using `/sys/class/backlight/*/brightness`.
|
||||||
|
In cases where no supported backlight device is found, the Home Assistant light entity will only show an on/off switch without brightness control.
|
||||||
|
|
||||||
Keep in mind that default arguments are stored as plain text in `~/.config/touchkio/Arguments.json`.
|
Keep in mind that default arguments are stored as plain text in `~/.config/touchkio/Arguments.json`.
|
||||||
This file also includes the **MQTT user password**, which is somewhat obfuscated/encrypted, but in a way that it could be easily reverse engineered.
|
This file also includes the **MQTT user password**, which is somewhat obfuscated/encrypted, but in a way that it could be easily reverse engineered.
|
||||||
Implementing stronger **security measures** would complicate the setup process and could discourage some users from configuring the application properly.
|
Implementing stronger **security measures** would complicate the setup process and could discourage some users from configuring the application properly.
|
||||||
When using the kiosk application without initializing the default arguments, you will need to provide them with every command.
|
When using the kiosk application without initializing the default arguments, you will need to provide them with every command.
|
||||||
This means that the password may be stored as plain text in various files, such as `touchkio.service`, `~/.bash_history`, etc.
|
This means that the password may be stored as plain text in various files, such as `touchkio.service`, `~/.bash_history`, etc.
|
||||||
|
|
||||||
To resolve the issue where the first **touch** on a **turned-off screen** triggers a **click event** (potentially activating Home Assistant actions), I implemented a workaround.
|
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**, I remove **focus** from the kiosk window.
|
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.
|
This way, the first click only turns the screen on and focuses the window, allowing subsequent clicks to work as expected.
|
||||||
|
|
||||||
Additionally, to address the problem that scrolling on the Raspberry Pi only works with the **web browser scrollbar** on the right, I configured the Electron app to **simulate a touch device** using `Emulation.setEmitTouchEventsForMouse`.
|
Additionally, to address the problem that scrolling only works with the **web browser scrollbar** on the right, the application is configured to **simulate a touch device** using `Emulation.setEmitTouchEventsForMouse`.
|
||||||
This adjustment provides a user experience similar to that of a proper mobile device.
|
This adjustment provides a user experience similar to that of a proper mobile device.
|
||||||
|
|
||||||
Electron apps are known to be **resource intensive** due to their architecture and the inclusion of a full web browser environment. If you just run the kiosk application without other heavy loads, everything should run smoothly.
|
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.
|
||||||
@@ -137,13 +215,24 @@ Electron apps are known to be **resource intensive** due to their architecture a
|
|||||||
</div></details>
|
</div></details>
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
- Currently, only the Raspberry Pi 5 in combination with the new touch display 2 supports the MQTT integration.
|
Please have a look into the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation if you ran into any issues.
|
||||||
- Other variations, such as Raspberry Pi 4 or Raspberry Pi 5 with touch display 1, could likely be added, but this will require additional testing and contributions via pull requests.
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**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 functionality, however, if the screen is turned on through Home Assistant after being automatically turned off, it will remain on indefinitely.
|
||||||
- It's recommended to either use the built-in screen blanking feature or implement a Home Assistant automation (e.g., presence detection) to manage the screen status.
|
- It's recommended to either use the built-in screen blanking feature or implement a Home Assistant automation (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.
|
- 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.
|
- This decision was made to maintain a clean design, as navigation buttons to move back and forth between different sites were not included.
|
||||||
- On the Raspberry Pi terminal you may see some *ERROR:gbm_wrapper.cc* messages.
|
- 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.
|
- This appears to be a [known issue](https://github.com/electron/electron/issues/42322) that currently lacks a fix, but the webview still works.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
packagerConfig: {
|
||||||
|
ignore: [".github", ".gitignore", "install.sh", "forge.config.js"],
|
||||||
|
},
|
||||||
|
makers: [
|
||||||
|
{
|
||||||
|
name: "@electron-forge/maker-deb",
|
||||||
|
config: {
|
||||||
|
options: {
|
||||||
|
productName: "TouchKio",
|
||||||
|
productDescription: "Kiosk mode application for a Home Assistant dashboard",
|
||||||
|
categories: ["Network"],
|
||||||
|
icon: "img/icon.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "@electron-forge/maker-zip",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
publishers: [
|
||||||
|
{
|
||||||
|
name: "@electron-forge/publisher-github",
|
||||||
|
config: {
|
||||||
|
repository: {
|
||||||
|
owner: "leukipp",
|
||||||
|
name: "touchkio",
|
||||||
|
},
|
||||||
|
draft: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hooks: {
|
||||||
|
postMake: async (config, results) => {
|
||||||
|
for (const result of results) {
|
||||||
|
const artifacts = [];
|
||||||
|
result.artifacts.forEach((artifact) => {
|
||||||
|
if (artifact.includes("amd64")) {
|
||||||
|
const renamed = artifact.replace("amd64", "x64");
|
||||||
|
fs.renameSync(artifact, renamed);
|
||||||
|
artifacts.push(renamed);
|
||||||
|
} else {
|
||||||
|
artifacts.push(artifact);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
result.artifacts = artifacts;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 154 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 59 KiB |
@@ -7,12 +7,27 @@ const hardware = require("./src/hardware");
|
|||||||
const webview = require("./src/webview");
|
const webview = require("./src/webview");
|
||||||
const { app } = require("electron");
|
const { app } = require("electron");
|
||||||
|
|
||||||
|
global.ARGS = global.ARGS || {};
|
||||||
|
|
||||||
|
if (!process.env.DISPLAY) {
|
||||||
|
console.error(`$DISPLAY variable not set to run the GUI application, are you connected via SSH?`);
|
||||||
|
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,
|
* This promise resolves when the app has finished initializing,
|
||||||
* allowing to safely create browser windows and perform other
|
* allowing to safely create browser windows and perform other
|
||||||
* initialization tasks.
|
* initialization tasks.
|
||||||
*/
|
*/
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
|
const lock = app.requestSingleInstanceLock();
|
||||||
|
if (!lock) {
|
||||||
|
console.error("TouchKio is already running");
|
||||||
|
return app.quit();
|
||||||
|
}
|
||||||
let args = parseArgs(process);
|
let args = parseArgs(process);
|
||||||
let argsProvided = !!Object.keys(args).length;
|
let argsProvided = !!Object.keys(args).length;
|
||||||
|
|
||||||
@@ -29,15 +44,15 @@ app.whenReady().then(async () => {
|
|||||||
} else if (!argsProvided && argsFileExists) {
|
} else if (!argsProvided && argsFileExists) {
|
||||||
args = readArgs(argsFilePath);
|
args = readArgs(argsFilePath);
|
||||||
}
|
}
|
||||||
|
global.ARGS = args;
|
||||||
|
|
||||||
// Show used arguments
|
// Show used arguments
|
||||||
const json = JSON.stringify(args, null, 2);
|
console.log(`Arguments: ${JSON.stringify(global.ARGS, null, 2)}`);
|
||||||
console.log(`Arguments: ${json}`);
|
|
||||||
|
|
||||||
// Chained init functions
|
// Chained init functions
|
||||||
const chained = [webview.init, hardware.init, integration.init];
|
const chained = [webview.init, hardware.init, integration.init];
|
||||||
for (const init of chained) {
|
for (const init of chained) {
|
||||||
if (!(await init(args))) {
|
if (!(await init())) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,6 +104,11 @@ const promptArgs = async (proc) => {
|
|||||||
question: "Enter WEB zoom level",
|
question: "Enter WEB zoom level",
|
||||||
fallback: "1.25",
|
fallback: "1.25",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "web_widget",
|
||||||
|
question: "Enter WEB widget enabled",
|
||||||
|
fallback: "true",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "mqtt",
|
key: "mqtt",
|
||||||
question: "\nConnect to MQTT Broker?",
|
question: "\nConnect to MQTT Broker?",
|
||||||
@@ -130,12 +150,7 @@ const promptArgs = async (proc) => {
|
|||||||
const answer = await read.question(prompt);
|
const answer = await read.question(prompt);
|
||||||
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
|
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
|
||||||
if (!["y", "yes"].includes(value)) {
|
if (!["y", "yes"].includes(value)) {
|
||||||
ignore = ignore.concat([
|
ignore = ignore.concat(["mqtt_url", "mqtt_user", "mqtt_password", "mqtt_discovery"]);
|
||||||
"mqtt_url",
|
|
||||||
"mqtt_user",
|
|
||||||
"mqtt_password",
|
|
||||||
"mqtt_discovery",
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
} else if (key === "check") {
|
} else if (key === "check") {
|
||||||
const json = JSON.stringify(args, null, 2);
|
const json = JSON.stringify(args, null, 2);
|
||||||
@@ -223,4 +238,6 @@ const decrypt = (value) => {
|
|||||||
* @param {number} ms - Sleep time in milliseconds.
|
* @param {number} ms - Sleep time in milliseconds.
|
||||||
* @returns {Promise} A promise resolving after the timeout.
|
* @returns {Promise} A promise resolving after the timeout.
|
||||||
*/
|
*/
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms) => {
|
||||||
|
return new Promise((r) => setTimeout(r, ms));
|
||||||
|
};
|
||||||
|
|||||||
+52
-8
@@ -1,12 +1,38 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Determine system architecture
|
||||||
|
echo -e "Determining system architecture..."
|
||||||
|
|
||||||
|
BITS=$(getconf LONG_BIT)
|
||||||
|
case "$(uname -m)" in
|
||||||
|
aarch64)
|
||||||
|
ARCH="arm64"
|
||||||
|
;;
|
||||||
|
x86_64)
|
||||||
|
ARCH="x64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
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
|
||||||
|
|
||||||
|
echo "Architecture $ARCH running $BITS-bit operating system is supported."
|
||||||
|
|
||||||
# Download the latest .deb package
|
# Download the latest .deb package
|
||||||
echo -e "Downloading the latest release..."
|
echo -e "\nDownloading the latest release..."
|
||||||
|
|
||||||
DEB_URL=$(wget -qO- https://api.github.com/repos/leukipp/touchkio/releases/latest | \
|
DEB_URL=$(wget -qO- https://api.github.com/repos/leukipp/touchkio/releases/latest | \
|
||||||
grep -o '"browser_download_url": "[^"]*\.deb"' | \
|
grep -o "\"browser_download_url\": \"[^\"]*_${ARCH}\.deb\"" | \
|
||||||
sed 's/"browser_download_url": "//;s/"//g')
|
sed 's/"browser_download_url": "//;s/"//g')
|
||||||
DEB_PATH="/tmp/$(basename "$DEB_URL")"
|
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
DEB_PATH="${TMP_DIR}/$(basename "$DEB_URL")"
|
||||||
|
|
||||||
if [ -z "$DEB_URL" ]; then
|
if [ -z "$DEB_URL" ]; then
|
||||||
echo "Download url for .deb file not found."
|
echo "Download url for .deb file not found."
|
||||||
@@ -21,6 +47,11 @@ fi
|
|||||||
# Install the latest .deb package
|
# Install the latest .deb package
|
||||||
echo -e "\nInstalling the latest release..."
|
echo -e "\nInstalling the latest release..."
|
||||||
|
|
||||||
|
if ! command -v dpkg &> /dev/null; then
|
||||||
|
echo "Package manager dpkg was not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if ! sudo dpkg -i "$DEB_PATH"; then
|
if ! sudo dpkg -i "$DEB_PATH"; then
|
||||||
echo "Installation of .deb file failed."
|
echo "Installation of .deb file failed."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -47,13 +78,23 @@ WantedBy=default.target
|
|||||||
EOF"
|
EOF"
|
||||||
|
|
||||||
systemctl --user enable "$(basename "$SERVICE_FILE")" || { echo "Failed to enable service $SERVICE_FILE"; exit 1; }
|
systemctl --user enable "$(basename "$SERVICE_FILE")" || { echo "Failed to enable service $SERVICE_FILE"; exit 1; }
|
||||||
|
echo "Service $SERVICE_FILE enabled"
|
||||||
|
|
||||||
# Create the udev rule for backlight
|
# Export display variables
|
||||||
echo -e "\nCreating udev rule for backlight..."
|
echo -e "\nExporting display variables..."
|
||||||
|
|
||||||
sudo bash -c 'cat << EOF > /etc/udev/rules.d/backlight-permissions.rules
|
if [ -z "$DISPLAY" ]; then
|
||||||
SUBSYSTEM=="backlight", KERNEL=="10-0045", RUN+="/bin/chmod 666 /sys/class/backlight/%k/bl_power /sys/class/backlight/%k/brightness"
|
export DISPLAY=":0"
|
||||||
EOF'
|
echo "DISPLAY was not set, defaulting to \"$DISPLAY\""
|
||||||
|
else
|
||||||
|
echo "DISPLAY is set to \"$DISPLAY\""
|
||||||
|
fi
|
||||||
|
if [ -z "$WAYLAND_DISPLAY" ]; then
|
||||||
|
export WAYLAND_DISPLAY="wayland-0"
|
||||||
|
echo "WAYLAND_DISPLAY was not set, defaulting to \"$WAYLAND_DISPLAY\""
|
||||||
|
else
|
||||||
|
echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\""
|
||||||
|
fi
|
||||||
|
|
||||||
# Start the setup mode
|
# Start the setup mode
|
||||||
echo ""
|
echo ""
|
||||||
@@ -61,4 +102,7 @@ read -p "Start touchkio setup? (Y/n) " answer
|
|||||||
if [[ ${answer:-y} == [Yy]* ]]; then
|
if [[ ${answer:-y} == [Yy]* ]]; then
|
||||||
echo "/usr/bin/touchkio --setup"
|
echo "/usr/bin/touchkio --setup"
|
||||||
/usr/bin/touchkio --setup
|
/usr/bin/touchkio --setup
|
||||||
|
else
|
||||||
|
echo "/usr/bin/touchkio"
|
||||||
|
/usr/bin/touchkio
|
||||||
fi
|
fi
|
||||||
|
|||||||
+7
-42
@@ -1,62 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "touchkio",
|
"name": "touchkio",
|
||||||
"description": "Home Assistant Touch Kiosk",
|
"description": "Home Assistant Touch Kiosk",
|
||||||
|
"homepage": "https://github.com/leukipp/touchkio",
|
||||||
"author": "leukipp",
|
"author": "leukipp",
|
||||||
"version": "0.0.3",
|
"version": "1.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron . --web-url=https://demo.home-assistant.io",
|
"start": "electron . --web-url=https://demo.home-assistant.io",
|
||||||
"build": "electron-forge make"
|
"build": "electron-forge make"
|
||||||
},
|
},
|
||||||
"config": {
|
"prettier": {
|
||||||
"forge": {
|
"printWidth": 120
|
||||||
"packagerConfig": {
|
|
||||||
"ignore": [
|
|
||||||
".gitignore",
|
|
||||||
".github/",
|
|
||||||
"docs/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"makers": [
|
|
||||||
{
|
|
||||||
"name": "@electron-forge/maker-zip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "@electron-forge/maker-deb",
|
|
||||||
"config": {
|
|
||||||
"options": {
|
|
||||||
"productName": "TouchKio",
|
|
||||||
"icon": "img/icon.png",
|
|
||||||
"categories": [
|
|
||||||
"Network"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"publishers": [
|
|
||||||
{
|
|
||||||
"name": "@electron-forge/publisher-github",
|
|
||||||
"config": {
|
|
||||||
"repository": {
|
|
||||||
"owner": "leukipp",
|
|
||||||
"name": "touchkio"
|
|
||||||
},
|
|
||||||
"draft": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.6.0",
|
"@electron-forge/cli": "^7.6.0",
|
||||||
"@electron-forge/maker-deb": "^7.6.0",
|
"@electron-forge/maker-deb": "^7.6.0",
|
||||||
"@electron-forge/maker-zip": "^7.6.0",
|
"@electron-forge/maker-zip": "^7.6.0",
|
||||||
"@electron-forge/publisher-github": "^7.6.0",
|
"@electron-forge/publisher-github": "^7.6.0",
|
||||||
"electron": "^33.2.1"
|
"electron": "^33.3.1",
|
||||||
|
"prettier": "^3.4.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mqtt": "^5.10.3"
|
"mqtt": "^5.10.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+462
-122
@@ -1,13 +1,28 @@
|
|||||||
const os = require("os");
|
const os = require("os");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const process = require("child_process");
|
const path = require("path");
|
||||||
|
const fsp = require("fs/promises");
|
||||||
|
const cpr = require("child_process");
|
||||||
|
|
||||||
global.HARDWARE = global.HARDWARE || {
|
global.HARDWARE = global.HARDWARE || {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
status: "invalid",
|
status: null,
|
||||||
|
session: {},
|
||||||
|
support: {},
|
||||||
display: {
|
display: {
|
||||||
status: null,
|
status: {
|
||||||
brightness: null,
|
path: null,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
brightness: {
|
||||||
|
path: null,
|
||||||
|
value: null,
|
||||||
|
max: null,
|
||||||
|
},
|
||||||
|
notifiers: [],
|
||||||
|
},
|
||||||
|
keyboard: {
|
||||||
|
visible: null,
|
||||||
notifiers: [],
|
notifiers: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -15,44 +30,68 @@ global.HARDWARE = global.HARDWARE || {
|
|||||||
/**
|
/**
|
||||||
* Initializes the hardware with the provided arguments.
|
* 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 {boolean} Returns true if the initialization was successful.
|
||||||
*/
|
*/
|
||||||
const init = async (args) => {
|
const init = async () => {
|
||||||
if (!args.mqtt_url) {
|
if (!compatibleDevice()) {
|
||||||
|
console.warn("Device not supported");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check supported hardware
|
// Init globals
|
||||||
if (!hardwareExists()) {
|
HARDWARE.session.user = sessionUser();
|
||||||
console.warn("Hardware not supported");
|
HARDWARE.session.type = sessionType();
|
||||||
return false;
|
HARDWARE.display.status.path = getDisplayStatusPath();
|
||||||
}
|
HARDWARE.support.displayStatus = HARDWARE.display.status.path !== null;
|
||||||
|
HARDWARE.display.brightness.path = getDisplayBrightnessPath();
|
||||||
|
HARDWARE.support.displayBrightness = HARDWARE.display.brightness.path !== null;
|
||||||
|
HARDWARE.display.brightness.max = getDisplayBrightnessMax();
|
||||||
|
HARDWARE.support.keyboardVisibility = processRuns("squeekboard");
|
||||||
|
HARDWARE.initialized = true;
|
||||||
|
HARDWARE.status = "Supported";
|
||||||
|
|
||||||
// Check supported model
|
// Show supported features
|
||||||
const model = getModel();
|
console.log(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`);
|
||||||
if (!model || !model.includes("Raspberry Pi 5")) {
|
|
||||||
console.warn(`Device ${model} not supported`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Show session infos
|
||||||
|
console.log("\nUser:", HARDWARE.session.user);
|
||||||
|
console.log("Session:", HARDWARE.session.type);
|
||||||
|
|
||||||
|
// Show device infos
|
||||||
console.log("\nModel:", getModel());
|
console.log("\nModel:", getModel());
|
||||||
|
console.log("Vendor:", getVendor());
|
||||||
console.log("Serial Number:", getSerialNumber());
|
console.log("Serial Number:", getSerialNumber());
|
||||||
console.log("Host Name:", getHostName());
|
console.log("Host Name:", getHostName());
|
||||||
console.log("Up Time:", getUpTime());
|
|
||||||
|
// Show system infos
|
||||||
|
console.log("\nUp Time:", getUpTime());
|
||||||
console.log("Memory Size:", getMemorySize());
|
console.log("Memory Size:", getMemorySize());
|
||||||
console.log("Memory Usage:", getMemoryUsage());
|
console.log("Memory Usage:", getMemoryUsage());
|
||||||
console.log("Processor Usage:", getProcessorUsage());
|
console.log("Processor Usage:", getProcessorUsage());
|
||||||
console.log("Processor Temperature:", getProcessorTemperature());
|
console.log("Processor Temperature:", getProcessorTemperature());
|
||||||
console.log("Display Status:", getDisplayStatus());
|
|
||||||
console.log("Display Brightness:", getDisplayBrightness(), "\n");
|
|
||||||
|
|
||||||
// Init globals
|
// Show display infos
|
||||||
HARDWARE.initialized = true;
|
console.log(`\nDisplay Status [${HARDWARE.display.status.path}]:`, getDisplayStatus());
|
||||||
HARDWARE.status = "valid";
|
console.log(`Display Brightness [${HARDWARE.display.brightness.path}]:`, getDisplayBrightness(), "\n");
|
||||||
|
|
||||||
|
// Check for keyboard visibility
|
||||||
|
setKeyboardVisibility("OFF", (reply, error) => {
|
||||||
|
if (!reply || error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dbusMonitor("/sm/puri/OSK0", (property, error) => {
|
||||||
|
if (!property || error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HARDWARE.keyboard.visibility = property.Visible === "true";
|
||||||
|
HARDWARE.keyboard.notifiers.forEach((notifier) => {
|
||||||
|
notifier();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Check for display changes
|
// Check for display changes
|
||||||
setInterval(update, 500);
|
interval(update, 1000);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -60,75 +99,142 @@ const init = async (args) => {
|
|||||||
/**
|
/**
|
||||||
* Updates the shared hardware properties.
|
* Updates the shared hardware properties.
|
||||||
*/
|
*/
|
||||||
const update = () => {
|
const update = async () => {
|
||||||
if (!HARDWARE.initialized) {
|
if (!HARDWARE.initialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const path = "/sys/class/backlight/10-0045";
|
|
||||||
|
|
||||||
// Display status has changed
|
// Display status has changed
|
||||||
const status = fs.readFileSync(`${path}/bl_power`, "utf8").trim();
|
if (HARDWARE.support.displayStatus) {
|
||||||
if (status !== HARDWARE.display.status) {
|
const status = (await fsp.readFile(`${HARDWARE.display.status.path}/dpms`, "utf8")).trim();
|
||||||
console.log("Update Display Status:", getDisplayStatus());
|
if (status !== HARDWARE.display.status.value) {
|
||||||
HARDWARE.display.status = status;
|
console.log("Update Display Status:", getDisplayStatus());
|
||||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
HARDWARE.display.status.value = status;
|
||||||
notifier();
|
HARDWARE.display.notifiers.forEach((notifier) => {
|
||||||
});
|
notifier();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display brightness has changed
|
// Display brightness has changed
|
||||||
const brightness = fs.readFileSync(`${path}/brightness`, "utf8").trim();
|
if (HARDWARE.support.displayBrightness) {
|
||||||
if (brightness !== HARDWARE.display.brightness) {
|
const brightness = (await fsp.readFile(`${HARDWARE.display.brightness.path}/brightness`, "utf8")).trim();
|
||||||
console.log("Update Display Brightness:", getDisplayBrightness());
|
if (brightness !== HARDWARE.display.brightness.value) {
|
||||||
HARDWARE.display.brightness = brightness;
|
console.log("Update Display Brightness:", getDisplayBrightness());
|
||||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
HARDWARE.display.brightness.value = brightness;
|
||||||
notifier();
|
HARDWARE.display.notifiers.forEach((notifier) => {
|
||||||
});
|
notifier();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies hardware compatibility by checking the presence of necessary sys files.
|
* Verifies device compatibility by checking the presence of necessary sys paths.
|
||||||
*
|
*
|
||||||
* @returns {bool} Returns true if all files exists.
|
* @returns {bool} Returns true if all paths exists.
|
||||||
*/
|
*/
|
||||||
const hardwareExists = () => {
|
const compatibleDevice = () => {
|
||||||
const files = [
|
if (os.platform() !== "linux") {
|
||||||
"/sys/firmware/devicetree/base/model",
|
return false;
|
||||||
"/sys/firmware/devicetree/base/serial-number",
|
}
|
||||||
"/sys/class/backlight/10-0045/brightness",
|
const paths = ["/sys/class/drm", "/sys/class/backlight", "/sys/class/thermal"];
|
||||||
"/sys/class/backlight/10-0045/bl_power",
|
return paths.every((path) => fs.existsSync(path));
|
||||||
];
|
|
||||||
return files.every((file) => fs.existsSync(file));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the Raspberry Pi model name using `/sys/firmware/devicetree/base/model`.
|
* Gets the session user name using `os.userInfo()`.
|
||||||
*
|
*
|
||||||
* @returns {string|null} The model of the Raspberry Pi or null if an error occurs.
|
* @returns {string|null} Returns session user name or null if an error occurs.
|
||||||
|
*/
|
||||||
|
const sessionUser = () => {
|
||||||
|
try {
|
||||||
|
return os.userInfo().username;
|
||||||
|
} catch {}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the session type for the user using `loginctl`.
|
||||||
|
*
|
||||||
|
* @returns {string|null} Returns session type 'x11'/'wayland' or null if an error occurs.
|
||||||
|
*/
|
||||||
|
const sessionType = () => {
|
||||||
|
if (!commandExists("loginctl")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return execSyncCommand("loginctl", [
|
||||||
|
"show-session",
|
||||||
|
"$(loginctl show-user $(whoami) -p Display --value)",
|
||||||
|
"-p Type --value",
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the session type for the logged in user.
|
||||||
|
*
|
||||||
|
* @returns {bool} Returns true if the session matches the type.
|
||||||
|
*/
|
||||||
|
const session = (type) => {
|
||||||
|
return HARDWARE.session.type == type;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
*/
|
*/
|
||||||
const getModel = () => {
|
const getModel = () => {
|
||||||
return execSyncCommand("cat", ["/sys/firmware/devicetree/base/model"]);
|
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 "Generic";
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the Raspberry Pi serial number using `/sys/firmware/devicetree/base/serial-number`.
|
* Gets the vendor name using `/sys/class/dmi/id/board_vendor`.
|
||||||
*
|
*
|
||||||
* @returns {string|null} The serial number of the Raspberry Pi or null if an error occurs.
|
* @returns {string|null} 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const model = getModel();
|
||||||
|
if (model.includes("Raspberry Pi")) {
|
||||||
|
return "Raspberry Pi Ltd";
|
||||||
|
}
|
||||||
|
return "Generic";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the serial number using `/sys/firmware/devicetree/base/serial-number` or `/sys/class/dmi/id/product_serial`.
|
||||||
|
*
|
||||||
|
* @returns {string|null} The serial number of the device or '123456' if not found.
|
||||||
*/
|
*/
|
||||||
const getSerialNumber = () => {
|
const getSerialNumber = () => {
|
||||||
return execSyncCommand("cat", [
|
const paths = ["/sys/firmware/devicetree/base/serial-number", "/sys/class/dmi/id/product_serial"];
|
||||||
"/sys/firmware/devicetree/base/serial-number",
|
for (const path of paths) {
|
||||||
]);
|
if (fs.existsSync(path)) {
|
||||||
|
return execSyncCommand("sudo", ["cat", path]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "123456";
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the host machine id using `/etc/machine-id`.
|
* Gets the host machine id using `/etc/machine-id`.
|
||||||
*
|
*
|
||||||
* @returns {string|null} The machine id of the system.
|
* @returns {string} The machine id of the system or '123456' if not found.
|
||||||
*/
|
*/
|
||||||
const getMachineId = () => {
|
const getMachineId = () => {
|
||||||
return execSyncCommand("cat", ["/etc/machine-id"]);
|
return execSyncCommand("cat", ["/etc/machine-id"]) || "123456";
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -177,45 +283,73 @@ const getProcessorUsage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current CPU temperature using `vcgencmd measure_temp`.
|
* Gets the current CPU temperature using `/sys/class/thermal`.
|
||||||
*
|
*
|
||||||
* @returns {number|null} The CPU temperature in degrees Celsius or null if an error occurs.
|
* @returns {number|null} The CPU temperature in degrees celsius or null if nothing was found.
|
||||||
*/
|
*/
|
||||||
const getProcessorTemperature = () => {
|
const getProcessorTemperature = () => {
|
||||||
const output = execSyncCommand("vcgencmd", ["measure_temp"]);
|
const thermal = "/sys/class/thermal";
|
||||||
if (output !== null) {
|
for (const zone of fs.readdirSync(thermal)) {
|
||||||
const temp = output.match(/temp=(\d+\.\d+)/);
|
const typeFile = path.join(thermal, zone, "type");
|
||||||
if (temp && temp[1]) {
|
const tempFile = path.join(thermal, zone, "temp");
|
||||||
return parseFloat(temp[1]);
|
if (!fs.existsSync(typeFile) || !fs.existsSync(tempFile)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const type = fs.readFileSync(typeFile, "utf8").trim();
|
||||||
|
if (["cpu-thermal", "x86_pkg_temp", "k10temp", "acpitz"].includes(type)) {
|
||||||
|
return parseFloat(fs.readFileSync(tempFile, "utf8").trim()) / 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current display power status using `wlopm`.
|
* Gets the current display status path using `/sys/class/drm`.
|
||||||
*
|
*
|
||||||
* @returns {string|null} The output of the command or null if an error occurs.
|
* @returns {string|null} The display status path or null if nothing was found.
|
||||||
*/
|
*/
|
||||||
const getDisplayStatus = () => {
|
const getDisplayStatusPath = () => {
|
||||||
const status = execSyncCommand("wlopm", []);
|
const drm = "/sys/class/drm";
|
||||||
if (status !== null) {
|
for (const card of fs.readdirSync(drm)) {
|
||||||
return status.slice(status.lastIndexOf(" ") + 1).toUpperCase();
|
const statusFile = path.join(drm, card, "status");
|
||||||
|
if (!fs.existsSync(statusFile)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const content = fs.readFileSync(statusFile, "utf8").trim();
|
||||||
|
if (content === "connected") {
|
||||||
|
return path.join(drm, card);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
/* This also works, but it's better to use `wlopm` for getter and setter methods.
|
|
||||||
const status = execSyncCommand("cat", [
|
|
||||||
"/sys/class/backlight/10-0045/bl_power",
|
|
||||||
]);
|
|
||||||
if (status !== null) {
|
|
||||||
return status === "0" ? "ON" : "OFF";
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the display power status using `wlopm`.
|
* Gets the current display power status using `wlopm` or `xset`.
|
||||||
|
*
|
||||||
|
* @returns {string|null} The display status as 'ON'/'OFF' or null if an error occurs.
|
||||||
|
*/
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the display power status using `wlopm` or `xset`.
|
||||||
*
|
*
|
||||||
* This function takes a desired status ('ON' or 'OFF') and executes
|
* This function takes a desired status ('ON' or 'OFF') and executes
|
||||||
* the appropriate command to set the display status.
|
* the appropriate command to set the display status.
|
||||||
@@ -224,69 +358,149 @@ const getDisplayStatus = () => {
|
|||||||
* @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) => {
|
const setDisplayStatus = (status, callback = null) => {
|
||||||
if (status !== "ON" && status !== "OFF") {
|
if (!HARDWARE.support.displayStatus) {
|
||||||
|
if (typeof callback === "function") callback(null, "Not supported");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!["ON", "OFF"].includes(status)) {
|
||||||
console.error("Status must be 'ON' or 'OFF'");
|
console.error("Status must be 'ON' or 'OFF'");
|
||||||
if (typeof callback === "function") callback(null, "Invalid status");
|
if (typeof callback === "function") callback(null, "Invalid status");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const args = status === "ON" ? ["--on", "*"] : ["--off", "*"];
|
if (session("wayland") && commandExists("wlopm")) {
|
||||||
execAsyncCommand("wlopm", args, callback);
|
execAsyncCommand("wlopm", [`--${status.toLowerCase()}`, "*"], callback);
|
||||||
|
} else if (session("x11") && commandExists("xset")) {
|
||||||
/* This also works, but interferes with the built-in screen blanking function.
|
execAsyncCommand("xset", ["dpms", "force", status.toLowerCase()], callback);
|
||||||
const value = status === "ON" ? "0" : "4";
|
}
|
||||||
const proc = execAsyncCommand(
|
|
||||||
"tee",
|
|
||||||
["/sys/class/backlight/10-0045/bl_power"],
|
|
||||||
callback
|
|
||||||
);
|
|
||||||
proc.stdin.write(value.toString());
|
|
||||||
proc.stdin.end();
|
|
||||||
*/
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current display brightness level using `/sys/class/backlight/10-0045/brightness`.
|
* Gets the current display brightness path using `/sys/class/backlight`.
|
||||||
*
|
*
|
||||||
* This function retrieves the brightness level, which is in the range of 0-31,
|
* @returns {string|null} The display brightness path or null if nothing was found.
|
||||||
* and maps it to a percentage in the range of 0-100 percent.
|
|
||||||
*
|
|
||||||
* @returns {number|null} The brightness level as a percentage or null if an error occurs.
|
|
||||||
*/
|
*/
|
||||||
const getDisplayBrightness = () => {
|
const getDisplayBrightnessPath = () => {
|
||||||
const brightness = execSyncCommand("cat", [
|
const backlight = "/sys/class/backlight";
|
||||||
"/sys/class/backlight/10-0045/brightness",
|
for (const address of fs.readdirSync(backlight)) {
|
||||||
]);
|
const brightnessFile = path.join(backlight, address, "brightness");
|
||||||
if (brightness !== null) {
|
if (!fs.existsSync(brightnessFile)) {
|
||||||
return Math.round((parseInt(brightness, 10) / 31) * 100);
|
continue;
|
||||||
|
}
|
||||||
|
return path.join(backlight, address);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the display brightness level using `/sys/class/backlight/10-0045/brightness`.
|
* Gets the maximum display brightness value using `/sys/class/backlight/.../max_brightness`.
|
||||||
*
|
*
|
||||||
* This function takes a brightness value in the range of 1-100 percent,
|
* @returns {number|null} The brightness maximum value or null if an error occurs.
|
||||||
* maps it to the range of 0-31, and writes it to the system.
|
*/
|
||||||
|
const getDisplayBrightnessMax = () => {
|
||||||
|
if (!HARDWARE.support.displayBrightness) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const max = execSyncCommand("cat", [`${HARDWARE.display.brightness.path}/max_brightness`]);
|
||||||
|
if (max !== null) {
|
||||||
|
return parseInt(max, 10);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current display brightness level using `/sys/class/backlight/.../brightness`.
|
||||||
|
*
|
||||||
|
* @returns {number|null} The brightness level as a percentage or null if an error occurs.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the display brightness level using `/sys/class/backlight/.../brightness`.
|
||||||
|
*
|
||||||
|
* This function takes a brightness value between 1 to 100 percent,
|
||||||
|
* maps it to the proper range and writes it to the system.
|
||||||
*
|
*
|
||||||
* @param {number} brightness - The desired brightness level (1-100).
|
* @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) => {
|
const setDisplayBrightness = (brightness, callback = null) => {
|
||||||
|
if (!HARDWARE.support.displayBrightness) {
|
||||||
|
if (typeof callback === "function") callback(null, "Not supported");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (typeof brightness !== "number" || brightness < 1 || brightness > 100) {
|
if (typeof brightness !== "number" || brightness < 1 || brightness > 100) {
|
||||||
console.error("Brightness must be a number between 1 and 100");
|
console.error("Brightness must be a number between 1 and 100");
|
||||||
if (typeof callback === "function") callback(null, "Invalid brightness");
|
if (typeof callback === "function") callback(null, "Invalid brightness");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const value = Math.max(1, Math.min(Math.round((brightness / 100) * 31), 31));
|
const max = HARDWARE.display.brightness.max;
|
||||||
const proc = execAsyncCommand(
|
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
|
||||||
"tee",
|
const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback);
|
||||||
["/sys/class/backlight/10-0045/brightness"],
|
|
||||||
callback
|
|
||||||
);
|
|
||||||
proc.stdin.write(value.toString());
|
proc.stdin.write(value.toString());
|
||||||
proc.stdin.end();
|
proc.stdin.end();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the keyboard visibility using global properties.
|
||||||
|
*
|
||||||
|
* @returns {string|null} The keyboard visibility as 'ON'/'OFF' or null if an error occurs.
|
||||||
|
*/
|
||||||
|
const getKeyboardVisibility = () => {
|
||||||
|
if (!HARDWARE.support.keyboardVisibility) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return HARDWARE.keyboard.visibility ? "ON" : "OFF";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the keyboard visibility using `dbus-send`.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
const setKeyboardVisibility = (visibility, callback = null) => {
|
||||||
|
if (!HARDWARE.support.keyboardVisibility) {
|
||||||
|
if (typeof callback === "function") callback(null, "Not supported");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!["ON", "OFF"].includes(visibility)) {
|
||||||
|
console.error("Visibility must be 'ON' or 'OFF'");
|
||||||
|
if (typeof callback === "function") callback(null, "Invalid visibility");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const visible = visibility === "ON";
|
||||||
|
HARDWARE.keyboard.visibility = visible;
|
||||||
|
dbusCall("/sm/puri/OSK0", "SetVisible", [`boolean:${visible}`], callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if system upgrades are available using `apt`.
|
||||||
|
*
|
||||||
|
* @returns {Array<string>} A list of package names that are available for upgrade.
|
||||||
|
*/
|
||||||
|
const checkPackageUpgrades = () => {
|
||||||
|
if (!commandExists("apt")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const output = execSyncCommand("apt", ["list", "--upgradable", "2>/dev/null"]);
|
||||||
|
const packages = (output || "").trim().split("\n");
|
||||||
|
packages.shift();
|
||||||
|
return packages;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuts down the system using `sudo shutdown -h now`.
|
* Shuts down the system using `sudo shutdown -h now`.
|
||||||
*
|
*
|
||||||
@@ -311,6 +525,32 @@ const rebootSystem = (callback = null) => {
|
|||||||
execAsyncCommand("sudo", ["reboot"], callback);
|
execAsyncCommand("sudo", ["reboot"], callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a process is running using `pidof`.
|
||||||
|
*
|
||||||
|
* @param {string} name - The process name to check.
|
||||||
|
* @returns {boolean} Returns true if the process runs.
|
||||||
|
*/
|
||||||
|
const processRuns = (name) => {
|
||||||
|
try {
|
||||||
|
return !!cpr.execSync(`pidof ${name}`, { encoding: "utf8" });
|
||||||
|
} catch {}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
const commandExists = (name) => {
|
||||||
|
try {
|
||||||
|
return !!cpr.execSync(`which ${name}`, { encoding: "utf8" });
|
||||||
|
} catch {}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a command synchronously and returns the output.
|
* Executes a command synchronously and returns the output.
|
||||||
*
|
*
|
||||||
@@ -320,9 +560,7 @@ const rebootSystem = (callback = null) => {
|
|||||||
*/
|
*/
|
||||||
const execSyncCommand = (cmd, args = []) => {
|
const execSyncCommand = (cmd, args = []) => {
|
||||||
try {
|
try {
|
||||||
const output = process.execSync([cmd, ...args].join(" "), {
|
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
||||||
encoding: "utf8",
|
|
||||||
});
|
|
||||||
return output.trim().replace(/\0/g, "");
|
return output.trim().replace(/\0/g, "");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Execute Sync:", error.message);
|
console.error("Execute Sync:", error.message);
|
||||||
@@ -341,7 +579,7 @@ const execSyncCommand = (cmd, args = []) => {
|
|||||||
const execAsyncCommand = (cmd, args = [], callback = null) => {
|
const execAsyncCommand = (cmd, args = [], callback = null) => {
|
||||||
let errorOutput = "";
|
let errorOutput = "";
|
||||||
let successOutput = "";
|
let successOutput = "";
|
||||||
let proc = process.spawn(cmd, args);
|
let proc = cpr.spawn(cmd, args);
|
||||||
proc.stderr.on("data", (data) => {
|
proc.stderr.on("data", (data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
errorOutput += data.toString();
|
errorOutput += data.toString();
|
||||||
@@ -363,15 +601,111 @@ const execAsyncCommand = (cmd, args = [], callback = null) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Execute Async:", error.message);
|
console.error("Execute Async:", error.message);
|
||||||
|
if (typeof callback === "function") {
|
||||||
|
callback(null, error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return proc;
|
return proc;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
const dbusCall = (path, method, values, callback = null) => {
|
||||||
|
const cmd = "dbus-send";
|
||||||
|
const iface = path.slice(1).replace(/\//g, ".");
|
||||||
|
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
|
||||||
|
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
|
||||||
|
try {
|
||||||
|
const output = cpr.execSync([cmd, ...args].join(" "), { 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitors D-Bus property changes asynchronously using `dbus-monitor`.
|
||||||
|
*
|
||||||
|
* @param {string} path - The D-Bus object path.
|
||||||
|
* @param {function} callback - A callback function that receives the changed property.
|
||||||
|
* @returns {object} The spawned process object.
|
||||||
|
*/
|
||||||
|
const dbusMonitor = (path, callback) => {
|
||||||
|
const cmd = "dbus-monitor";
|
||||||
|
const args = [`interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='${path}'`];
|
||||||
|
const proc = cpr.spawn(cmd, args);
|
||||||
|
proc.stdout.on("data", (data) => {
|
||||||
|
try {
|
||||||
|
const signal = data.toString();
|
||||||
|
if (signal.includes("member=PropertiesChanged") && signal.includes(`path=${path}`)) {
|
||||||
|
const dicts = [...signal.matchAll(/dict entry\(\s*([^)]*?)\)/g)].map((dict) => dict[1].trim());
|
||||||
|
if (dicts.length) {
|
||||||
|
dicts.forEach((dict) => {
|
||||||
|
const key = dict.match(/string "(.*?)"/);
|
||||||
|
const value = dict.match(/(?<=variant\s+)(.*)/);
|
||||||
|
if (key && value && typeof callback === "function") {
|
||||||
|
callback({ [key[1].trim()]: value[1].trim().split(" ").pop() }, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback({ Visible: `${HARDWARE.keyboard.visibility}` }, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Monitor D-Bus:", error.message);
|
||||||
|
if (typeof callback === "function") {
|
||||||
|
callback(null, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
proc.stderr.on("data", (data) => {
|
||||||
|
if (data) {
|
||||||
|
console.error("Monitor D-Bus:", data.toString());
|
||||||
|
if (typeof callback === "function") {
|
||||||
|
callback(null, data.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return proc;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for asynchronous interval calls.
|
||||||
|
*
|
||||||
|
* @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 = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
update,
|
update,
|
||||||
getModel,
|
getModel,
|
||||||
|
getVendor,
|
||||||
getSerialNumber,
|
getSerialNumber,
|
||||||
getMachineId,
|
getMachineId,
|
||||||
getHostName,
|
getHostName,
|
||||||
@@ -380,10 +714,16 @@ module.exports = {
|
|||||||
getMemoryUsage,
|
getMemoryUsage,
|
||||||
getProcessorUsage,
|
getProcessorUsage,
|
||||||
getProcessorTemperature,
|
getProcessorTemperature,
|
||||||
|
getDisplayStatusPath,
|
||||||
getDisplayStatus,
|
getDisplayStatus,
|
||||||
setDisplayStatus,
|
setDisplayStatus,
|
||||||
|
getDisplayBrightnessPath,
|
||||||
|
getDisplayBrightnessMax,
|
||||||
getDisplayBrightness,
|
getDisplayBrightness,
|
||||||
setDisplayBrightness,
|
setDisplayBrightness,
|
||||||
|
getKeyboardVisibility,
|
||||||
|
setKeyboardVisibility,
|
||||||
|
checkPackageUpgrades,
|
||||||
shutdownSystem,
|
shutdownSystem,
|
||||||
rebootSystem,
|
rebootSystem,
|
||||||
};
|
};
|
||||||
|
|||||||
+341
-255
@@ -4,52 +4,51 @@ const { app } = require("electron");
|
|||||||
|
|
||||||
global.INTEGRATION = global.INTEGRATION || {
|
global.INTEGRATION = global.INTEGRATION || {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
status: "offline",
|
status: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the integration with the provided arguments.
|
* 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.
|
* @returns {boolean} Returns true if the initialization was successful.
|
||||||
*/
|
*/
|
||||||
const init = async (args) => {
|
const init = async () => {
|
||||||
if (!args.mqtt_url) {
|
if (!ARGS.mqtt_url) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const url = new URL(args.mqtt_url);
|
if (!/^mqtts?:\/\//.test(ARGS.mqtt_url)) {
|
||||||
const user = args.mqtt_user ? args.mqtt_user : null;
|
console.error("Please provide the '--mqtt-url' parameter with mqtt(s)");
|
||||||
const password = args.mqtt_password ? args.mqtt_password : null;
|
return app.quit();
|
||||||
const discovery = args.mqtt_discovery ? args.mqtt_discovery : "homeassistant";
|
}
|
||||||
|
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 model = hardware.getModel();
|
||||||
|
const vendor = hardware.getVendor();
|
||||||
const hostName = hardware.getHostName();
|
const hostName = hardware.getHostName();
|
||||||
const serialNumber = hardware.getSerialNumber();
|
const serialNumber = hardware.getSerialNumber();
|
||||||
|
const serialNumberSuffix = serialNumber.slice(-6);
|
||||||
|
|
||||||
const deviceId = serialNumber.slice(-6).toUpperCase();
|
const deviceId = serialNumberSuffix.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
||||||
const deviceName = hostName.charAt(0).toUpperCase() + hostName.slice(1);
|
const deviceName = hostName.charAt(0).toUpperCase() + hostName.slice(1);
|
||||||
|
|
||||||
// Init globals
|
// Init globals
|
||||||
INTEGRATION.node = `rpi_${deviceId}`;
|
INTEGRATION.node = `rpi_${deviceId}`;
|
||||||
INTEGRATION.discovery = discovery;
|
INTEGRATION.discovery = discovery;
|
||||||
INTEGRATION.device = {
|
INTEGRATION.device = {
|
||||||
name: `Raspberry Pi ${deviceName}`,
|
name: `TouchKio ${deviceName}`,
|
||||||
model: model,
|
model: model,
|
||||||
|
manufacturer: vendor,
|
||||||
serial_number: serialNumber,
|
serial_number: serialNumber,
|
||||||
manufacturer: "Raspberry Pi Ltd",
|
|
||||||
identifiers: [INTEGRATION.node],
|
identifiers: [INTEGRATION.node],
|
||||||
sw_version: `${app.getName()}-v${app.getVersion()}`,
|
sw_version: `${app.getName()}-v${app.getVersion()}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connection settings
|
// Connection settings
|
||||||
|
const options = user === null || password == null ? null : { username: user, password: password };
|
||||||
const masked = password === null ? "null" : "*".repeat(password.length);
|
const masked = password === null ? "null" : "*".repeat(password.length);
|
||||||
const options =
|
|
||||||
user === null || password == null
|
|
||||||
? null
|
|
||||||
: {
|
|
||||||
username: user,
|
|
||||||
password: password,
|
|
||||||
};
|
|
||||||
console.log("MQTT Connecting:", `${user}:${masked}@${url.toString()}`);
|
console.log("MQTT Connecting:", `${user}:${masked}@${url.toString()}`);
|
||||||
INTEGRATION.client = mqtt.connect(url.toString(), options);
|
INTEGRATION.client = mqtt.connect(url.toString(), options);
|
||||||
|
|
||||||
@@ -64,6 +63,7 @@ const init = async (args) => {
|
|||||||
initRefresh(INTEGRATION.client);
|
initRefresh(INTEGRATION.client);
|
||||||
initKiosk(INTEGRATION.client);
|
initKiosk(INTEGRATION.client);
|
||||||
initDisplay(INTEGRATION.client);
|
initDisplay(INTEGRATION.client);
|
||||||
|
initKeyboard(INTEGRATION.client);
|
||||||
|
|
||||||
// Init client sensors
|
// Init client sensors
|
||||||
initModel(INTEGRATION.client);
|
initModel(INTEGRATION.client);
|
||||||
@@ -74,29 +74,42 @@ const init = async (args) => {
|
|||||||
initMemoryUsage(INTEGRATION.client);
|
initMemoryUsage(INTEGRATION.client);
|
||||||
initProcessorUsage(INTEGRATION.client);
|
initProcessorUsage(INTEGRATION.client);
|
||||||
initProcessorTemperature(INTEGRATION.client);
|
initProcessorTemperature(INTEGRATION.client);
|
||||||
|
initPackageUpgrades(INTEGRATION.client);
|
||||||
initHeartbeat(INTEGRATION.client);
|
initHeartbeat(INTEGRATION.client);
|
||||||
|
initLastActive(INTEGRATION.client);
|
||||||
|
|
||||||
// Integration initialized
|
// Integration initialized
|
||||||
INTEGRATION.initialized = true;
|
INTEGRATION.initialized = true;
|
||||||
INTEGRATION.status = "online";
|
INTEGRATION.status = "Online";
|
||||||
})
|
})
|
||||||
.on("error", (error) => {
|
.on("error", (error) => {
|
||||||
console.error("MQTT:", error);
|
console.error("MQTT:", error);
|
||||||
INTEGRATION.status = "offline";
|
INTEGRATION.status = "Offline";
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update sensor states on display change
|
// Update sensor states from notifiers
|
||||||
HARDWARE.display.notifiers.push(() => {
|
HARDWARE.display.notifiers.push(() => {
|
||||||
updateDisplay(INTEGRATION.client);
|
updateDisplay(INTEGRATION.client);
|
||||||
});
|
});
|
||||||
|
HARDWARE.keyboard.notifiers.push(() => {
|
||||||
|
updateKeyboard(INTEGRATION.client);
|
||||||
|
});
|
||||||
|
|
||||||
// Update heartbeat time periodically
|
// Update time sensors periodically (30s)
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
updateHeartbeat(INTEGRATION.client);
|
updateHeartbeat(INTEGRATION.client);
|
||||||
|
updateLastActive(INTEGRATION.client);
|
||||||
}, 30 * 1000);
|
}, 30 * 1000);
|
||||||
|
|
||||||
// Update sensor states periodically
|
// Update system sensors periodically (1min)
|
||||||
setInterval(update, 60 * 1000);
|
setInterval(() => {
|
||||||
|
update();
|
||||||
|
}, 60 * 1000);
|
||||||
|
|
||||||
|
// Update package sensors periodically (60min)
|
||||||
|
setInterval(() => {
|
||||||
|
updatePackageUpgrades(INTEGRATION.client);
|
||||||
|
}, 3600 * 1000);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -104,7 +117,7 @@ const init = async (args) => {
|
|||||||
/**
|
/**
|
||||||
* Updates the shared integration properties.
|
* Updates the shared integration properties.
|
||||||
*/
|
*/
|
||||||
const update = () => {
|
const update = async () => {
|
||||||
if (!INTEGRATION.initialized) {
|
if (!INTEGRATION.initialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -112,6 +125,7 @@ const update = () => {
|
|||||||
// Update client sensors
|
// Update client sensors
|
||||||
updateKiosk(INTEGRATION.client);
|
updateKiosk(INTEGRATION.client);
|
||||||
updateUpTime(INTEGRATION.client);
|
updateUpTime(INTEGRATION.client);
|
||||||
|
updateLastActive(INTEGRATION.client);
|
||||||
updateMemoryUsage(INTEGRATION.client);
|
updateMemoryUsage(INTEGRATION.client);
|
||||||
updateProcessorUsage(INTEGRATION.client);
|
updateProcessorUsage(INTEGRATION.client);
|
||||||
updateProcessorTemperature(INTEGRATION.client);
|
updateProcessorTemperature(INTEGRATION.client);
|
||||||
@@ -124,18 +138,15 @@ const update = () => {
|
|||||||
*/
|
*/
|
||||||
const initShutdown = (client) => {
|
const initShutdown = (client) => {
|
||||||
const root = `${INTEGRATION.discovery}/button/${INTEGRATION.node}/shutdown`;
|
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
|
client
|
||||||
.publish(
|
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: "Shutdown",
|
|
||||||
unique_id: `${INTEGRATION.node}_shutdown`,
|
|
||||||
command_topic: `${root}/execute`,
|
|
||||||
icon: "mdi:power",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
)
|
|
||||||
.on("message", (topic, message) => {
|
.on("message", (topic, message) => {
|
||||||
if (topic === `${root}/execute`) {
|
if (topic === `${root}/execute`) {
|
||||||
console.log("Shutdown system...");
|
console.log("Shutdown system...");
|
||||||
@@ -153,18 +164,15 @@ const initShutdown = (client) => {
|
|||||||
*/
|
*/
|
||||||
const initReboot = (client) => {
|
const initReboot = (client) => {
|
||||||
const root = `${INTEGRATION.discovery}/button/${INTEGRATION.node}/reboot`;
|
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
|
client
|
||||||
.publish(
|
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: "Reboot",
|
|
||||||
unique_id: `${INTEGRATION.node}_reboot`,
|
|
||||||
command_topic: `${root}/execute`,
|
|
||||||
icon: "mdi:restart",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
)
|
|
||||||
.on("message", (topic, message) => {
|
.on("message", (topic, message) => {
|
||||||
if (topic === `${root}/execute`) {
|
if (topic === `${root}/execute`) {
|
||||||
console.log("Rebooting system...");
|
console.log("Rebooting system...");
|
||||||
@@ -182,18 +190,15 @@ const initReboot = (client) => {
|
|||||||
*/
|
*/
|
||||||
const initRefresh = (client) => {
|
const initRefresh = (client) => {
|
||||||
const root = `${INTEGRATION.discovery}/button/${INTEGRATION.node}/refresh`;
|
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
|
client
|
||||||
.publish(
|
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: "Refresh",
|
|
||||||
unique_id: `${INTEGRATION.node}_refresh`,
|
|
||||||
command_topic: `${root}/execute`,
|
|
||||||
icon: "mdi:web-refresh",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
)
|
|
||||||
.on("message", (topic, message) => {
|
.on("message", (topic, message) => {
|
||||||
if (topic === `${root}/execute`) {
|
if (topic === `${root}/execute`) {
|
||||||
console.log("Refreshing webview...");
|
console.log("Refreshing webview...");
|
||||||
@@ -211,27 +216,18 @@ const initRefresh = (client) => {
|
|||||||
*/
|
*/
|
||||||
const initKiosk = (client) => {
|
const initKiosk = (client) => {
|
||||||
const root = `${INTEGRATION.discovery}/select/${INTEGRATION.node}/kiosk`;
|
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
|
client
|
||||||
.publish(
|
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: `Kiosk`,
|
|
||||||
unique_id: `${INTEGRATION.node}_kiosk`,
|
|
||||||
command_topic: `${root}/set`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ value }}",
|
|
||||||
options: [
|
|
||||||
"Framed",
|
|
||||||
"Fullscreen",
|
|
||||||
"Maximized",
|
|
||||||
"Minimized",
|
|
||||||
"Terminated",
|
|
||||||
],
|
|
||||||
icon: "mdi:overscan",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
)
|
|
||||||
.on("message", (topic, message) => {
|
.on("message", (topic, message) => {
|
||||||
if (topic === `${root}/set`) {
|
if (topic === `${root}/set`) {
|
||||||
const status = message.toString();
|
const status = message.toString();
|
||||||
@@ -275,10 +271,9 @@ const initKiosk = (client) => {
|
|||||||
const updateKiosk = (client) => {
|
const updateKiosk = (client) => {
|
||||||
const kiosk = WEBVIEW.status;
|
const kiosk = WEBVIEW.status;
|
||||||
const root = `${INTEGRATION.discovery}/select/${INTEGRATION.node}/kiosk`;
|
const root = `${INTEGRATION.discovery}/select/${INTEGRATION.node}/kiosk`;
|
||||||
client.publish(`${root}/status`, `${kiosk}`, {
|
if (kiosk !== null) {
|
||||||
qos: 1,
|
client.publish(`${root}/status`, `${kiosk}`, { qos: 1, retain: true });
|
||||||
retain: true,
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -287,33 +282,37 @@ const updateKiosk = (client) => {
|
|||||||
* @param {Object} client - Instance of the mqtt client.
|
* @param {Object} client - Instance of the mqtt client.
|
||||||
*/
|
*/
|
||||||
const initDisplay = (client) => {
|
const initDisplay = (client) => {
|
||||||
|
if (!HARDWARE.support.displayStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`;
|
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
|
client
|
||||||
.publish(
|
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||||
`${root}/config`,
|
|
||||||
JSON.stringify({
|
|
||||||
name: `Display`,
|
|
||||||
unique_id: `${INTEGRATION.node}_display`,
|
|
||||||
command_topic: `${root}/set`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
brightness_command_topic: `${root}/brightness/set`,
|
|
||||||
brightness_state_topic: `${root}/brightness/status`,
|
|
||||||
brightness_scale: 100,
|
|
||||||
icon: "mdi:monitor-shimmer",
|
|
||||||
platform: "light",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
)
|
|
||||||
.on("message", (topic, message) => {
|
.on("message", (topic, message) => {
|
||||||
if (topic === `${root}/set`) {
|
if (topic === `${root}/set`) {
|
||||||
const status = message.toString();
|
const status = message.toString();
|
||||||
console.log("Set Display Status:", status);
|
console.log("Set Display Status:", status);
|
||||||
hardware.setDisplayStatus(status);
|
hardware.setDisplayStatus(status);
|
||||||
|
hardware.update();
|
||||||
} else if (topic === `${root}/brightness/set`) {
|
} else if (topic === `${root}/brightness/set`) {
|
||||||
const brightness = parseInt(message, 10);
|
const brightness = parseInt(message, 10);
|
||||||
console.log("Set Display Brightness:", brightness);
|
console.log("Set Display Brightness:", brightness);
|
||||||
hardware.setDisplayBrightness(brightness);
|
hardware.setDisplayBrightness(brightness);
|
||||||
|
hardware.update();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.subscribe(`${root}/set`)
|
.subscribe(`${root}/set`)
|
||||||
@@ -330,14 +329,68 @@ const updateDisplay = (client) => {
|
|||||||
const status = hardware.getDisplayStatus();
|
const status = hardware.getDisplayStatus();
|
||||||
const brightness = hardware.getDisplayBrightness();
|
const brightness = hardware.getDisplayBrightness();
|
||||||
const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`;
|
const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`;
|
||||||
client.publish(`${root}/status`, `${status}`, {
|
if (status !== null) {
|
||||||
qos: 1,
|
client.publish(`${root}/status`, `${status}`, { qos: 1, retain: true });
|
||||||
retain: true,
|
}
|
||||||
});
|
if (brightness !== null) {
|
||||||
client.publish(`${root}/brightness/status`, `${brightness}`, {
|
client.publish(`${root}/brightness/status`, `${brightness}`, { qos: 1, retain: true });
|
||||||
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 });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -347,18 +400,15 @@ const updateDisplay = (client) => {
|
|||||||
*/
|
*/
|
||||||
const initModel = (client) => {
|
const initModel = (client) => {
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/model`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/model`;
|
||||||
client.publish(
|
const config = {
|
||||||
`${root}/config`,
|
name: `Model`,
|
||||||
JSON.stringify({
|
unique_id: `${INTEGRATION.node}_model`,
|
||||||
name: `Model`,
|
state_topic: `${root}/status`,
|
||||||
unique_id: `${INTEGRATION.node}_model`,
|
value_template: "{{ value }}",
|
||||||
state_topic: `${root}/status`,
|
icon: "mdi:raspberry-pi",
|
||||||
value_template: "{{ value }}",
|
device: INTEGRATION.device,
|
||||||
icon: "mdi:raspberry-pi",
|
};
|
||||||
device: INTEGRATION.device,
|
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateModel(client);
|
updateModel(client);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -370,10 +420,9 @@ const initModel = (client) => {
|
|||||||
const updateModel = (client) => {
|
const updateModel = (client) => {
|
||||||
const model = hardware.getModel();
|
const model = hardware.getModel();
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/model`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/model`;
|
||||||
client.publish(`${root}/status`, `${model}`, {
|
if (model !== null) {
|
||||||
qos: 1,
|
client.publish(`${root}/status`, `${model}`, { qos: 1, retain: true });
|
||||||
retain: true,
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -383,18 +432,15 @@ const updateModel = (client) => {
|
|||||||
*/
|
*/
|
||||||
const initSerialNumber = (client) => {
|
const initSerialNumber = (client) => {
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/serial_number`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/serial_number`;
|
||||||
client.publish(
|
const config = {
|
||||||
`${root}/config`,
|
name: `Serial Number`,
|
||||||
JSON.stringify({
|
unique_id: `${INTEGRATION.node}_serial_number`,
|
||||||
name: `Serial Number`,
|
state_topic: `${root}/status`,
|
||||||
unique_id: `${INTEGRATION.node}_serial_number`,
|
value_template: "{{ value }}",
|
||||||
state_topic: `${root}/status`,
|
icon: "mdi:hexadecimal",
|
||||||
value_template: "{{ value }}",
|
device: INTEGRATION.device,
|
||||||
icon: "mdi:hexadecimal",
|
};
|
||||||
device: INTEGRATION.device,
|
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateSerialNumber(client);
|
updateSerialNumber(client);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -406,10 +452,9 @@ const initSerialNumber = (client) => {
|
|||||||
const updateSerialNumber = (client) => {
|
const updateSerialNumber = (client) => {
|
||||||
const serialNumber = hardware.getSerialNumber();
|
const serialNumber = hardware.getSerialNumber();
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/serial_number`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/serial_number`;
|
||||||
client.publish(`${root}/status`, `${serialNumber}`, {
|
if (serialNumber !== null) {
|
||||||
qos: 1,
|
client.publish(`${root}/status`, `${serialNumber}`, { qos: 1, retain: true });
|
||||||
retain: true,
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -419,18 +464,15 @@ const updateSerialNumber = (client) => {
|
|||||||
*/
|
*/
|
||||||
const initHostName = (client) => {
|
const initHostName = (client) => {
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/host_name`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/host_name`;
|
||||||
client.publish(
|
const config = {
|
||||||
`${root}/config`,
|
name: `Host Name`,
|
||||||
JSON.stringify({
|
unique_id: `${INTEGRATION.node}_host_name`,
|
||||||
name: `Host Name`,
|
state_topic: `${root}/status`,
|
||||||
unique_id: `${INTEGRATION.node}_host_name`,
|
value_template: "{{ value }}",
|
||||||
state_topic: `${root}/status`,
|
icon: "mdi:console-network",
|
||||||
value_template: "{{ value }}",
|
device: INTEGRATION.device,
|
||||||
icon: "mdi:console-network",
|
};
|
||||||
device: INTEGRATION.device,
|
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateHostName(client);
|
updateHostName(client);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -442,10 +484,9 @@ const initHostName = (client) => {
|
|||||||
const updateHostName = (client) => {
|
const updateHostName = (client) => {
|
||||||
const hostName = hardware.getHostName();
|
const hostName = hardware.getHostName();
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/host_name`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/host_name`;
|
||||||
client.publish(`${root}/status`, `${hostName}`, {
|
if (hostName !== null) {
|
||||||
qos: 1,
|
client.publish(`${root}/status`, `${hostName}`, { qos: 1, retain: true });
|
||||||
retain: true,
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -455,19 +496,16 @@ const updateHostName = (client) => {
|
|||||||
*/
|
*/
|
||||||
const initUpTime = (client) => {
|
const initUpTime = (client) => {
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/up_time`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/up_time`;
|
||||||
client.publish(
|
const config = {
|
||||||
`${root}/config`,
|
name: `Up Time`,
|
||||||
JSON.stringify({
|
unique_id: `${INTEGRATION.node}_up_time`,
|
||||||
name: `Up Time`,
|
state_topic: `${root}/status`,
|
||||||
unique_id: `${INTEGRATION.node}_up_time`,
|
value_template: "{{ (value | float) | round(0) }}",
|
||||||
state_topic: `${root}/status`,
|
unit_of_measurement: "min",
|
||||||
value_template: "{{ (value | float) | round(0) }}",
|
icon: "mdi:timeline-clock",
|
||||||
unit_of_measurement: "min",
|
device: INTEGRATION.device,
|
||||||
icon: "mdi:timeline-clock",
|
};
|
||||||
device: INTEGRATION.device,
|
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateUpTime(client);
|
updateUpTime(client);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -479,10 +517,9 @@ const initUpTime = (client) => {
|
|||||||
const updateUpTime = (client) => {
|
const updateUpTime = (client) => {
|
||||||
const upTime = hardware.getUpTime();
|
const upTime = hardware.getUpTime();
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/up_time`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/up_time`;
|
||||||
client.publish(`${root}/status`, `${upTime}`, {
|
if (upTime !== null) {
|
||||||
qos: 1,
|
client.publish(`${root}/status`, `${upTime}`, { qos: 1, retain: true });
|
||||||
retain: true,
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -492,19 +529,16 @@ const updateUpTime = (client) => {
|
|||||||
*/
|
*/
|
||||||
const initMemorySize = (client) => {
|
const initMemorySize = (client) => {
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_size`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_size`;
|
||||||
client.publish(
|
const config = {
|
||||||
`${root}/config`,
|
name: `Memory Size`,
|
||||||
JSON.stringify({
|
unique_id: `${INTEGRATION.node}_memory_size`,
|
||||||
name: `Memory Size`,
|
state_topic: `${root}/status`,
|
||||||
unique_id: `${INTEGRATION.node}_memory_size`,
|
value_template: "{{ (value | float) | round(2) }}",
|
||||||
state_topic: `${root}/status`,
|
unit_of_measurement: "GiB",
|
||||||
value_template: "{{ (value | float) | round(2) }}",
|
icon: "mdi:memory",
|
||||||
unit_of_measurement: "GiB",
|
device: INTEGRATION.device,
|
||||||
icon: "mdi:memory",
|
};
|
||||||
device: INTEGRATION.device,
|
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateMemorySize(client);
|
updateMemorySize(client);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -516,10 +550,9 @@ const initMemorySize = (client) => {
|
|||||||
const updateMemorySize = (client) => {
|
const updateMemorySize = (client) => {
|
||||||
const memorySize = hardware.getMemorySize();
|
const memorySize = hardware.getMemorySize();
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_size`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_size`;
|
||||||
client.publish(`${root}/status`, `${memorySize}`, {
|
if (memorySize !== null) {
|
||||||
qos: 1,
|
client.publish(`${root}/status`, `${memorySize}`, { qos: 1, retain: true });
|
||||||
retain: true,
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -529,19 +562,16 @@ const updateMemorySize = (client) => {
|
|||||||
*/
|
*/
|
||||||
const initMemoryUsage = (client) => {
|
const initMemoryUsage = (client) => {
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_usage`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_usage`;
|
||||||
client.publish(
|
const config = {
|
||||||
`${root}/config`,
|
name: `Memory Usage`,
|
||||||
JSON.stringify({
|
unique_id: `${INTEGRATION.node}_memory_usage`,
|
||||||
name: `Memory Usage`,
|
state_topic: `${root}/status`,
|
||||||
unique_id: `${INTEGRATION.node}_memory_usage`,
|
value_template: "{{ (value | float) | round(0) }}",
|
||||||
state_topic: `${root}/status`,
|
unit_of_measurement: "%",
|
||||||
value_template: "{{ (value | float) | round(0) }}",
|
icon: "mdi:memory-arrow-down",
|
||||||
unit_of_measurement: "%",
|
device: INTEGRATION.device,
|
||||||
icon: "mdi:memory-arrow-down",
|
};
|
||||||
device: INTEGRATION.device,
|
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateMemoryUsage(client);
|
updateMemoryUsage(client);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -553,10 +583,9 @@ const initMemoryUsage = (client) => {
|
|||||||
const updateMemoryUsage = (client) => {
|
const updateMemoryUsage = (client) => {
|
||||||
const memoryUsage = hardware.getMemoryUsage();
|
const memoryUsage = hardware.getMemoryUsage();
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_usage`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_usage`;
|
||||||
client.publish(`${root}/status`, `${memoryUsage}`, {
|
if (memoryUsage !== null) {
|
||||||
qos: 1,
|
client.publish(`${root}/status`, `${memoryUsage}`, { qos: 1, retain: true });
|
||||||
retain: true,
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -566,19 +595,16 @@ const updateMemoryUsage = (client) => {
|
|||||||
*/
|
*/
|
||||||
const initProcessorUsage = (client) => {
|
const initProcessorUsage = (client) => {
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_usage`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_usage`;
|
||||||
client.publish(
|
const config = {
|
||||||
`${root}/config`,
|
name: `Processor Usage`,
|
||||||
JSON.stringify({
|
unique_id: `${INTEGRATION.node}_processor_usage`,
|
||||||
name: `Processor Usage`,
|
state_topic: `${root}/status`,
|
||||||
unique_id: `${INTEGRATION.node}_processor_usage`,
|
value_template: "{{ (value | float) | round(0) }}",
|
||||||
state_topic: `${root}/status`,
|
unit_of_measurement: "%",
|
||||||
value_template: "{{ (value | float) | round(0) }}",
|
icon: "mdi:cpu-64-bit",
|
||||||
unit_of_measurement: "%",
|
device: INTEGRATION.device,
|
||||||
icon: "mdi:cpu-64-bit",
|
};
|
||||||
device: INTEGRATION.device,
|
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateProcessorUsage(client);
|
updateProcessorUsage(client);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -590,10 +616,9 @@ const initProcessorUsage = (client) => {
|
|||||||
const updateProcessorUsage = (client) => {
|
const updateProcessorUsage = (client) => {
|
||||||
const processorUsage = hardware.getProcessorUsage();
|
const processorUsage = hardware.getProcessorUsage();
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_usage`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_usage`;
|
||||||
client.publish(`${root}/status`, `${processorUsage}`, {
|
if (processorUsage !== null) {
|
||||||
qos: 1,
|
client.publish(`${root}/status`, `${processorUsage}`, { qos: 1, retain: true });
|
||||||
retain: true,
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -603,19 +628,16 @@ const updateProcessorUsage = (client) => {
|
|||||||
*/
|
*/
|
||||||
const initProcessorTemperature = (client) => {
|
const initProcessorTemperature = (client) => {
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_temperature`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_temperature`;
|
||||||
client.publish(
|
const config = {
|
||||||
`${root}/config`,
|
name: `Processor Temperature`,
|
||||||
JSON.stringify({
|
unique_id: `${INTEGRATION.node}_processor_temperature`,
|
||||||
name: `Processor Temperature`,
|
state_topic: `${root}/status`,
|
||||||
unique_id: `${INTEGRATION.node}_processor_temperature`,
|
value_template: "{{ (value | float) | round(0) }}",
|
||||||
state_topic: `${root}/status`,
|
unit_of_measurement: "°C",
|
||||||
value_template: "{{ (value | float) | round(0) }}",
|
icon: "mdi:radiator",
|
||||||
unit_of_measurement: "°C",
|
device: INTEGRATION.device,
|
||||||
icon: "mdi:radiator",
|
};
|
||||||
device: INTEGRATION.device,
|
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateProcessorTemperature(client);
|
updateProcessorTemperature(client);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -627,10 +649,47 @@ const initProcessorTemperature = (client) => {
|
|||||||
const updateProcessorTemperature = (client) => {
|
const updateProcessorTemperature = (client) => {
|
||||||
const processorTemperature = hardware.getProcessorTemperature();
|
const processorTemperature = hardware.getProcessorTemperature();
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_temperature`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_temperature`;
|
||||||
client.publish(`${root}/status`, `${processorTemperature}`, {
|
if (processorTemperature !== null) {
|
||||||
qos: 1,
|
client.publish(`${root}/status`, `${processorTemperature}`, { qos: 1, retain: true });
|
||||||
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 });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -640,18 +699,15 @@ const updateProcessorTemperature = (client) => {
|
|||||||
*/
|
*/
|
||||||
const initHeartbeat = (client) => {
|
const initHeartbeat = (client) => {
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/heartbeat`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/heartbeat`;
|
||||||
client.publish(
|
const config = {
|
||||||
`${root}/config`,
|
name: `Heartbeat`,
|
||||||
JSON.stringify({
|
unique_id: `${INTEGRATION.node}_heartbeat`,
|
||||||
name: `Heartbeat`,
|
state_topic: `${root}/status`,
|
||||||
unique_id: `${INTEGRATION.node}_heartbeat`,
|
value_template: "{{ value }}",
|
||||||
state_topic: `${root}/status`,
|
icon: "mdi:heart-flash",
|
||||||
value_template: "{{ value }}",
|
device: INTEGRATION.device,
|
||||||
icon: "mdi:heart-flash",
|
};
|
||||||
device: INTEGRATION.device,
|
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||||
}),
|
|
||||||
{ qos: 1, retain: true }
|
|
||||||
);
|
|
||||||
updateHeartbeat(client);
|
updateHeartbeat(client);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -665,10 +721,40 @@ const updateHeartbeat = (client) => {
|
|||||||
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000);
|
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000);
|
||||||
const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, "");
|
const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, "");
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/heartbeat`;
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/heartbeat`;
|
||||||
client.publish(`${root}/status`, `${heartbeat}`, {
|
client.publish(`${root}/status`, `${heartbeat}`, { qos: 1, retain: true });
|
||||||
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 = {
|
module.exports = {
|
||||||
|
|||||||
+242
-76
@@ -1,48 +1,84 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const hardware = require("./hardware");
|
const hardware = require("./hardware");
|
||||||
const integration = require("./integration");
|
const integration = require("./integration");
|
||||||
const { app, nativeTheme, BaseWindow, WebContentsView } = require("electron");
|
const { app, screen, nativeTheme, ipcMain, BaseWindow, WebContentsView } = require("electron");
|
||||||
|
|
||||||
global.WEBVIEW = global.WEBVIEW || {
|
global.WEBVIEW = global.WEBVIEW || {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
status: "offline",
|
status: null,
|
||||||
locked: false,
|
locked: false,
|
||||||
|
pointer: {
|
||||||
|
position: {},
|
||||||
|
time: new Date(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the webview with the provided arguments.
|
* 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.
|
* @returns {boolean} Returns true if the initialization was successful.
|
||||||
*/
|
*/
|
||||||
const init = async (args) => {
|
const init = async () => {
|
||||||
if (!args.web_url) {
|
if (!ARGS.web_url) {
|
||||||
console.error("Please provide the '--web-url' parameter");
|
console.error("Please provide the '--web-url' parameter");
|
||||||
return app.quit();
|
return app.quit();
|
||||||
}
|
}
|
||||||
const url = new URL(args.web_url);
|
if (!/^https?:\/\//.test(ARGS.web_url)) {
|
||||||
const theme = args.web_theme ? args.web_theme : "dark";
|
console.error("Please provide the '--web-url' parameter with http(s)");
|
||||||
const zoom = args.web_zoom ? parseFloat(args.web_zoom) : 1.25;
|
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;
|
||||||
|
const fullscreen = ARGS.app_debug !== "true";
|
||||||
|
|
||||||
// Set app theme and name
|
// Init global layout
|
||||||
nativeTheme.themeSource = theme;
|
WEBVIEW.viewZoom = zoom;
|
||||||
app.setName("TouchKio");
|
WEBVIEW.viewTheme = theme;
|
||||||
|
WEBVIEW.widgetTheme = theme;
|
||||||
|
WEBVIEW.widgetEnabled = widget;
|
||||||
|
nativeTheme.themeSource = WEBVIEW.viewTheme;
|
||||||
|
|
||||||
// Init global root window
|
// Init global root window
|
||||||
WEBVIEW.window = new BaseWindow({
|
WEBVIEW.window = new BaseWindow({
|
||||||
|
title: `TouchKio - ${url.host}`,
|
||||||
icon: path.join(__dirname, "..", "img", "icon.png"),
|
icon: path.join(__dirname, "..", "img", "icon.png"),
|
||||||
});
|
});
|
||||||
WEBVIEW.window.setMenuBarVisibility(false);
|
WEBVIEW.window.setMenuBarVisibility(false);
|
||||||
WEBVIEW.window.setFullScreen(true);
|
WEBVIEW.window.setFullScreen(fullscreen);
|
||||||
|
if (!fullscreen) {
|
||||||
|
WEBVIEW.window.maximize();
|
||||||
|
}
|
||||||
|
|
||||||
// Init global main webview
|
// Init global webview
|
||||||
WEBVIEW.view = new WebContentsView();
|
WEBVIEW.view = new WebContentsView({
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
WEBVIEW.window.contentView.addChildView(WEBVIEW.view);
|
WEBVIEW.window.contentView.addChildView(WEBVIEW.view);
|
||||||
WEBVIEW.view.webContents.loadURL(url.toString());
|
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
|
// Register events
|
||||||
windowEvents(WEBVIEW.window, zoom);
|
windowEvents();
|
||||||
viewEvents(WEBVIEW.view);
|
widgetEvents();
|
||||||
|
domEvents();
|
||||||
|
appEvents();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -50,7 +86,7 @@ const init = async (args) => {
|
|||||||
/**
|
/**
|
||||||
* Updates the shared webview properties.
|
* Updates the shared webview properties.
|
||||||
*/
|
*/
|
||||||
const update = () => {
|
const update = async () => {
|
||||||
if (!WEBVIEW.initialized) {
|
if (!WEBVIEW.initialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -66,6 +102,11 @@ const update = () => {
|
|||||||
WEBVIEW.status = "Framed";
|
WEBVIEW.status = "Framed";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update widget status
|
||||||
|
if (!HARDWARE.support.keyboardVisibility) {
|
||||||
|
WEBVIEW.widget.webContents.send("button-hide", { id: "keyboard" });
|
||||||
|
}
|
||||||
|
|
||||||
// Update integration sensor
|
// Update integration sensor
|
||||||
console.log("Update Kiosk Status:", WEBVIEW.status);
|
console.log("Update Kiosk Status:", WEBVIEW.status);
|
||||||
integration.update();
|
integration.update();
|
||||||
@@ -73,83 +114,208 @@ const update = () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Register window events and handler.
|
* Register window events and handler.
|
||||||
*
|
|
||||||
* @param {Object} window - The root window object.
|
|
||||||
* @param {number} zoom - Zoom level used for the webview.
|
|
||||||
*/
|
*/
|
||||||
const windowEvents = (window, zoom) => {
|
const windowEvents = () => {
|
||||||
// Handle window resize for webview bounds
|
// Handle window resize events
|
||||||
const resize = () => {
|
const resize = () => {
|
||||||
const { width, height } = window.getBounds();
|
const window = WEBVIEW.window.getBounds();
|
||||||
WEBVIEW.view.setBounds({ x: 0, y: 0, width: width, height: height });
|
const widget = { width: 60, height: 200 };
|
||||||
WEBVIEW.view.webContents.setZoomFactor(zoom);
|
|
||||||
|
// 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" });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
window.on("ready-to-show", resize);
|
WEBVIEW.window.on("ready-to-show", resize);
|
||||||
window.on("resize", resize);
|
WEBVIEW.window.on("resize", resize);
|
||||||
resize();
|
resize();
|
||||||
|
|
||||||
// Handle focus change for webview lock
|
// Handle window focus events
|
||||||
HARDWARE.display.notifiers.push(() => {
|
WEBVIEW.window.on("focus", () => {
|
||||||
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
|
|
||||||
if (WEBVIEW.locked) {
|
|
||||||
window.blur();
|
|
||||||
}
|
|
||||||
/* This also works, but it doesn't allow to react for screen wake up.
|
|
||||||
window.setIgnoreMouseEvents(getDisplayStatus() === "OFF");
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
window.on("focus", () => {
|
|
||||||
if (WEBVIEW.locked) {
|
if (WEBVIEW.locked) {
|
||||||
hardware.setDisplayStatus("ON");
|
hardware.setDisplayStatus("ON");
|
||||||
window.blur();
|
WEBVIEW.window.blur();
|
||||||
}
|
}
|
||||||
WEBVIEW.locked = false;
|
WEBVIEW.locked = false;
|
||||||
});
|
});
|
||||||
|
HARDWARE.display.notifiers.push(() => {
|
||||||
|
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
|
||||||
|
if (WEBVIEW.locked) {
|
||||||
|
WEBVIEW.window.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Update window status
|
// Handle window status updates
|
||||||
window.on("minimize", update);
|
WEBVIEW.window.on("minimize", update);
|
||||||
window.on("restore", update);
|
WEBVIEW.window.on("restore", update);
|
||||||
window.on("maximize", update);
|
WEBVIEW.window.on("maximize", update);
|
||||||
window.on("unmaximize", update);
|
WEBVIEW.window.on("unmaximize", update);
|
||||||
window.on("enter-full-screen", update);
|
WEBVIEW.window.on("enter-full-screen", update);
|
||||||
window.on("leave-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", () => {
|
||||||
|
if (ARGS.app_debug === "true") {
|
||||||
|
WEBVIEW.view.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
WEBVIEW.initialized = true;
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register app events and handler.
|
||||||
|
*/
|
||||||
|
const appEvents = () => {
|
||||||
|
// Handle signal and exit events
|
||||||
process.on("SIGINT", app.quit);
|
process.on("SIGINT", app.quit);
|
||||||
app.on("before-quit", () => {
|
app.on("before-quit", () => {
|
||||||
WEBVIEW.status = "Terminated";
|
WEBVIEW.status = "Terminated";
|
||||||
integration.update();
|
integration.update();
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
// Handle multiple instances
|
||||||
* Register webview events and handler.
|
app.on("second-instance", () => {
|
||||||
*
|
if (WEBVIEW.window.isMinimized()) {
|
||||||
* @param {Object} view - The main webview object.
|
WEBVIEW.window.restore();
|
||||||
*/
|
|
||||||
const viewEvents = (view) => {
|
|
||||||
// Enable webview touch emulation
|
|
||||||
view.webContents.debugger.attach("1.1");
|
|
||||||
view.webContents.debugger.sendCommand(
|
|
||||||
"Emulation.setEmitTouchEventsForMouse",
|
|
||||||
{
|
|
||||||
enabled: true,
|
|
||||||
configuration: "mobile",
|
|
||||||
}
|
}
|
||||||
);
|
WEBVIEW.window.focus();
|
||||||
|
|
||||||
// Disable webview hyperlinks
|
|
||||||
view.webContents.setWindowOpenHandler(() => {
|
|
||||||
return { action: "deny" };
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove webview scrollbar
|
|
||||||
view.webContents.on("dom-ready", () => {
|
|
||||||
view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Webview fully loaded
|
|
||||||
view.webContents.on("did-finish-load", () => {
|
|
||||||
WEBVIEW.initialized = true;
|
|
||||||
update();
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--primary-color: rgba(225, 225, 225, 1);
|
||||||
|
--border-color: rgba(225, 225, 225, 0.15);
|
||||||
|
--background-color: rgba(28, 28, 28, 1);
|
||||||
|
}
|
||||||
|
[data-theme="light"] {
|
||||||
|
--primary-color: rgba(33, 33, 33, 1);
|
||||||
|
--border-color: rgba(33, 33, 33, 0.15);
|
||||||
|
--background-color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
[data-theme="hidden"] {
|
||||||
|
--primary-color: rgba(0, 0, 0, 0);
|
||||||
|
--border-color: rgba(0, 0, 0, 0);
|
||||||
|
--background-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
padding: 10px;
|
||||||
|
row-gap: 10px;
|
||||||
|
list-style-type: none;
|
||||||
|
border-top-left-radius: 15px;
|
||||||
|
border-bottom-left-radius: 15px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
border-left: 1px solid var(--border-color);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: var(--background-color);
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
pointer-events: none;
|
||||||
|
fill: var(--primary-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<menu>
|
||||||
|
<li>
|
||||||
|
<button id="refresh">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>web-refresh</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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="fullscreen">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>overscan</title>
|
||||||
|
<path
|
||||||
|
d="M12 5.5L10 8H14L12 5.5M18 10V14L20.5 12L18 10M6 10L3.5 12L6 14V10M14 16H10L12 18.5L14 16M21 3H3C1.9 3 1 3.9 1 5V19C1 20.1 1.9 21 3 21H21C22.1 21 23 20.1 23 19V5C23 3.9 22.1 3 21 3M21 19H3V5H21V19Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="minimize">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>image-size-select-small</title>
|
||||||
|
<path
|
||||||
|
d="M23,15H21V17H23V15M23,11H21V13H23V11M23,19H21V21C22,21 23,20 23,19M15,3H13V5H15V3M23,7H21V9H23V7M21,3V5H23C23,4 22,3 21,3M3,21H11V15H1V19A2,2 0 0,0 3,21M3,7H1V9H3V7M15,19H13V21H15V19M19,3H17V5H19V3M19,19H17V21H19V19M3,3C2,3 1,4 1,5H3V3M3,11H1V13H3V11M11,3H9V5H11V3M7,3H5V5H7V3Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="keyboard">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>keyboard-close-outline</title>
|
||||||
|
<path
|
||||||
|
d="M12 23L16 19H8M4 3C2.9 3 2 3.9 2 5V15C2 16.1 2.9 17 4 17H20C21.1 17 22 16.1 22 15V5C22 3.9 21.1 3 20 3H4M4 5H20V15H4V5M5 6V8H7V6H5M8 6V8H10V6H8M11 6V8H13V6H11M14 6V8H16V6H14M17 6V8H19V6H17M5 9V11H7V9H5M8 9V11H10V9H8M11 9V11H13V9H11M14 9V11H16V9H14M17 9V11H19V9H17M8 12V14H16V12H8Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
<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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register button hide receive message
|
||||||
|
ipcRenderer.on("button-hide", (e, button) => {
|
||||||
|
document.getElementById(button.id).parentNode.hidden = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register data theme receive message
|
||||||
|
ipcRenderer.on("data-theme", (e, data) => {
|
||||||
|
document.documentElement.setAttribute("data-theme", data.theme);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1129,10 +1129,10 @@ electron-installer-debian@^3.2.0:
|
|||||||
word-wrap "^1.2.3"
|
word-wrap "^1.2.3"
|
||||||
yargs "^16.0.2"
|
yargs "^16.0.2"
|
||||||
|
|
||||||
electron@^33.2.1:
|
electron@^33.3.1:
|
||||||
version "33.2.1"
|
version "33.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/electron/-/electron-33.2.1.tgz#d0d7bba7a7abf4f14881d0a6e03c498b301a2d5f"
|
resolved "https://registry.yarnpkg.com/electron/-/electron-33.3.1.tgz#26d63b3a110c4a9db639c33633bf806f91b4f649"
|
||||||
integrity sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==
|
integrity sha512-Z7l2bVgpdKxHQMI4i0CirBX2n+iCYKOx5mbzNM3BpOyFELwlobEXKmzCmEnwP+3EcNeIhUQyIEBFQxN06QgdIw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@electron/get" "^2.0.0"
|
"@electron/get" "^2.0.0"
|
||||||
"@types/node" "^20.9.0"
|
"@types/node" "^20.9.0"
|
||||||
@@ -2396,6 +2396,11 @@ postject@^1.0.0-alpha.6:
|
|||||||
dependencies:
|
dependencies:
|
||||||
commander "^9.4.0"
|
commander "^9.4.0"
|
||||||
|
|
||||||
|
prettier@^3.4.2:
|
||||||
|
version "3.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f"
|
||||||
|
integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==
|
||||||
|
|
||||||
proc-log@^2.0.1:
|
proc-log@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-2.0.1.tgz#8f3f69a1f608de27878f91f5c688b225391cb685"
|
resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-2.0.1.tgz#8f3f69a1f608de27878f91f5c688b225391cb685"
|
||||||
|
|||||||
Reference in New Issue
Block a user