Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfbc391a52 | ||
|
|
b1998ceea9 | ||
|
|
081cee2990 | ||
|
|
fd746cc79a | ||
|
|
4f426df4e6 | ||
|
|
7084fc8188 | ||
|
|
b2685f8d6b | ||
|
|
5743b0ff08 | ||
|
|
a7f4ede899 |
@@ -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
|
||||||
|
|||||||
+15
-14
@@ -1,27 +1,28 @@
|
|||||||
# TouchKio - Supported Hardware
|
# TouchKio - Supported Hardware
|
||||||
This document contains an incomplete list of device and display hardware combinations that are either intended to be tested or have already been tested.
|
This document contains an incomplete list of device and display hardware combinations that are either intended to be tested or have already been tested.
|
||||||
|
|
||||||
The [release](https://github.com/leukipp/touchkio/releases) page has builds exclusively for **arm64**, 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.
|
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.
|
At least the **minimal** features, such as displaying a **kiosk window** (which doesn't necessarily need to be Home Assistant) will work.
|
||||||
|
|
||||||
## Hardware
|
## Hardware
|
||||||
Any SBC clones of the Raspberry Pi that operate on **Raspberry Pi OS (64-bit)** and **Wayland** are likely to function as well.
|
If you are running Linux with a graphical user interface (Wayland or X11), you should be well equipped to use the application. Additionally, any single board computer (SBC) clones of the Raspberry Pi that operate on Raspberry Pi OS **(64-bit)** are likely to function properly as well.
|
||||||
|
|
||||||
### DSI
|
### DSI
|
||||||
| Device | Display | Tested | Status | Notes |
|
| Device | Display | Tested | Status | Notes |
|
||||||
| -------------- | ----------------------------------------------------------------------------------------------- | ------ | ----------------- | ------------------------------------------------------ |
|
| -------------- | ----------------------------------------------------------------------------------------------- | ------ | ----------------- | ---------------------------------------------------------------- |
|
||||||
| Raspberry Pi 3 | [Official 7" Touch Display 1](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | ❌ | Unknown | 99% chance that it is slow and the resolution sucks. |
|
| Raspberry Pi 3 | [Official 7" Touch Display 1](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | ❌ | Unknown | 99% chance that it is slow and the resolution sucks. |
|
||||||
| Raspberry Pi 3 | [Official 7" Touch Display 2](https://www.raspberrypi.com/products/touch-display-2/) | ❌ | Unknown | 99% chance that it is slow. |
|
| Raspberry Pi 3 | [Official 7" Touch Display 2](https://www.raspberrypi.com/products/touch-display-2/) | ❌ | Unknown | 99% chance that it is slow. |
|
||||||
| Raspberry Pi 4 | [Official 7" Touch Display 1](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | ❌ | Unknown | 99% chance that the resolution sucks. |
|
| Raspberry Pi 4 | [Official 7" Touch Display 1](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | ❌ | Unknown | 99% chance that the resolution sucks. |
|
||||||
| Raspberry Pi 4 | [Official 7" Touch Display 2](https://www.raspberrypi.com/products/touch-display-2/) | ❌ | Optimistic | 99% chance that it fully works. |
|
| Raspberry Pi 4 | [Official 7" Touch Display 2](https://www.raspberrypi.com/products/touch-display-2/) | ❌ | Optimistic | 99% chance that it fully works. |
|
||||||
| Raspberry Pi 5 | [Official 7" Touch Display 1](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | ❌ | Unknown | 99% chance that the resolution sucks. |
|
| Raspberry Pi 5 | [Official 7" Touch Display 1](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | ❌ | Unknown | 99% chance that the resolution sucks. |
|
||||||
| Raspberry Pi 5 | [Official 7" Touch Display 2](https://www.raspberrypi.com/products/touch-display-2/) | ✅ | Fully operational | Working display power and brightness control via MQTT. |
|
| Raspberry Pi 5 | [Official 7" Touch Display 2](https://www.raspberrypi.com/products/touch-display-2/) | ✅ | Fully operational | Working display power, brightness and keyboard control via MQTT. |
|
||||||
|
|
||||||
### HDMI
|
### HDMI
|
||||||
| Device | Display | Tested | Status | Notes |
|
| Device | Display | Tested | Status | Notes |
|
||||||
| -------------- | ----------------- | ------ | --------------------- | ----------------------------------------------------- |
|
| -------------- | ---------------------------------------------------------------------- | ------ | --------------------- | ------------------------------------------------------------------ |
|
||||||
| Raspberry Pi 4 | - | ✅ | Partially operational | - |
|
| Raspberry Pi 4 | [LAFVIN 5" Touch Display](https://www.amazon.de/gp/product/B0BWJ8YP7S) | ✅ | Partially operational | Display brightness control is not available via MQTT. |
|
||||||
| Raspberry Pi 5 | Generic Non-Touch | ✅ | Partially operational | Display brightness control is not available via MQTT. |
|
| Raspberry Pi 5 | Generic Non-Touch | ✅ | Partially operational | Display brightness control is not available via MQTT. |
|
||||||
|
| Generic Ubuntu | Generic Non-Touch | ✅ | Partially operational | Display brightness and keyboard control is not available via MQTT. |
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
In case your hardware is not listed above don't worry, give it a try.
|
In case your hardware is not listed above don't worry, give it a try.
|
||||||
|
|||||||
@@ -1,32 +1,37 @@
|
|||||||
# TouchKio
|
# TouchKio
|
||||||
**TouchKio** is a Node.js application that utilizes **Electron** to create a kiosk mode specifically designed for a Home Assistant dashboard (or any other [webpage](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md)).
|
**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** (or similar [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md)) equipped with a 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 with 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] 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, make sure that you have a Raspberry Pi configured and operational with a [compatible touch display](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md).
|
Before you begin, make sure that you have a Linux device configured and operational with a [compatible](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) Touch Display.
|
||||||
This guide assumes that you are using the latest version of **Raspberry Pi OS (64-bit)** together with a **Wayland** based desktop environment.
|
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 is also compatible with any other Debian based 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)
|
||||||
@@ -39,22 +44,36 @@ It's recommended to create a **dedicated user** (local access only) for your kio
|
|||||||
You might also need a keyboard or remote VNC access to input these credentials once.
|
You might also need a keyboard or remote VNC access to input these credentials once.
|
||||||
|
|
||||||
#### 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 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)
|
||||||
```
|
```
|
||||||
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.
|
||||||
|
|
||||||
#### Option 2 - The hard way
|
<details><summary>Alternatives</summary><div>
|
||||||
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,
|
|
||||||
which is specifically suitable for Raspberry Pi **(64-bit)** ARM architecture.
|
|
||||||
- Open a terminal and execute the following command to install the application, e.g:
|
|
||||||
`sudo dpkg -i touchkio_1.x.x_arm64.deb`
|
|
||||||
- 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 need the application to be automatically started on boot, create a **systemd file**.
|
#### Option 2 - The standard way
|
||||||
If you need the Home Assistant MQTT sensors, create the **udev rule** for backlight control.
|
The [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script mentioned above performs the following tasks (and you just have to do it manually):
|
||||||
|
- [Download](https://github.com/leukipp/touchkio/releases/latest) the latest version file that is suitable for your architecture (arm64 or x64).
|
||||||
|
- Debian (**deb**): Open a terminal and execute the following command to install the application, e.g:
|
||||||
|
`sudo dpkg -i touchkio_1.x.x_arm64.deb && touchkio --setup`
|
||||||
|
- Others (**zip**): Extract the archive and run the binary, e.g: `unzip touchkio-linux-x64-1.x.x.zip && cd touchkio-linux-x64 && ./touchkio --setup`
|
||||||
|
- If you just want to load a Home Assistant dashboard without further control you are good to go, e.g: `touchkio --web-url=https://demo.home-assistant.io`
|
||||||
|
- The `--web-url` doesn't necessarily need to be a Home Assistant url, any kind of website can be shown in kiosk mode.
|
||||||
|
- Only when using the MQTT integration via `--mqtt-*`, a running Home Assistant instance is required.
|
||||||
|
- If you need the application to be automatically started on boot, create a systemd file.
|
||||||
|
|
||||||
|
#### Option 3 - The hard way
|
||||||
|
Pre-built release files are available for arm64 and x64 Linux systems.
|
||||||
|
If you are using a different architecture, you can still utilize this repository to build your own application.
|
||||||
|
|
||||||
|
For more information, please refer to the [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development) section. However this could 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.
|
||||||
@@ -85,7 +104,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,13 +112,13 @@ 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](https://github.com/leukipp/touchkio/blob/main/package.json) file.
|
Then use `yarn start` to execute the start script located in [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 Raspberry Pi via SSH, you must export the display variables so that the kiosk application can be loaded in the desktop environment:
|
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
|
```bash
|
||||||
export DISPLAY=":0"
|
export DISPLAY=":0"
|
||||||
export WAYLAND_DISPLAY="wayland-0"
|
export WAYLAND_DISPLAY="wayland-0"
|
||||||
@@ -109,11 +128,8 @@ export WAYLAND_DISPLAY="wayland-0"
|
|||||||
|
|
||||||
<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**. In case you don't use the official touch display you may have to change the rules, that are located under `/etc/udev/rules.d/backlight-permissions.rules`.
|
The Raspberry Pi's **build-in on-screen keyboard** named `squeekboard` (it squeaks because some *Rust* got inside), is specifically designed for Wayland environments and features a **D-Bus interface** that allows applications to show or hide the keyboard as needed.
|
||||||
|
The kiosk application interacts with squeekboard via this `D-Bus` object path `/sm/puri/OSK0`, enabling the keyboard to be hidden or shown based on **MQTT** user input or system events.
|
||||||
This rule is created via the [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script, which also creates a service file.
|
|
||||||
The service file is located at `~/.config/systemd/user/touchkio.service` and starts the `/usr/bin/touchkio` process when the graphical user interface is loaded.
|
|
||||||
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.
|
||||||
@@ -128,7 +144,7 @@ As a result, I went for a **polling solution**, checking the state of the file e
|
|||||||
While I understand this is not ideal, it's necessary to ensure proper functionality.
|
While I understand this is not ideal, it's necessary to ensure proper functionality.
|
||||||
|
|
||||||
The display power status and brightness can be adjusted via the MQTT integration.
|
The display power status and brightness can be adjusted via the MQTT integration.
|
||||||
**Support** for changing the power status for **DSI and HDMI** displays is achieved by checking for connected screens in `/sys/class/drm/*/status`.
|
**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`.
|
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.
|
In cases where no supported backlight device is found, the Home Assistant light entity will only show an on/off switch without brightness control.
|
||||||
|
|
||||||
@@ -138,11 +154,11 @@ Implementing stronger **security measures** would complicate the setup process a
|
|||||||
When using the kiosk application without initializing the default arguments, you will need to provide them with every command.
|
When using the kiosk application without initializing the default arguments, you will need to provide them with every command.
|
||||||
This means that the password may be stored as plain text in various files, such as `touchkio.service`, `~/.bash_history`, etc.
|
This means that the password may be stored as plain text in various files, such as `touchkio.service`, `~/.bash_history`, etc.
|
||||||
|
|
||||||
To resolve the issue where the first **touch** on a **turned-off screen** triggers a **click event** (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** the **focus** is removed 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, the Electron app is configured to **simulate a touch device** using `Emulation.setEmitTouchEventsForMouse`.
|
Additionally, to address the problem that scrolling only works with the **web browser scrollbar** on the right, the application is configured to **simulate a touch device** using `Emulation.setEmitTouchEventsForMouse`.
|
||||||
This adjustment provides a user experience similar to that of a proper mobile device.
|
This adjustment provides a user experience similar to that of a proper mobile device.
|
||||||
|
|
||||||
Electron apps are known to be **resource intensive** due to their architecture and the inclusion of a full web browser environment. If you just run the kiosk application without other heavy loads, everything should run smoothly.
|
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.
|
||||||
@@ -152,13 +168,19 @@ Electron apps are known to be **resource intensive** due to their architecture a
|
|||||||
## Issues
|
## Issues
|
||||||
Please have a look into the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation if you ran into any issues.
|
Please have a look into the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation if you ran into any issues.
|
||||||
|
|
||||||
- Depending on the display hardware you are using, some features of the MQTT integration may not fully work (e.g. display brightness control).
|
**Minimal features** are designed to run on any system without issues:
|
||||||
- This is especially common when using X11 instead of Wayland (default on Raspberry Pi OS).
|
- 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.
|
||||||
|
|
||||||
|
**Known Issues** that are by-design or for which there is so far no solution:
|
||||||
- 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.
|
||||||
- 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 Raspberry Pi's 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
+24
-11
@@ -1,10 +1,27 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Determine system architecture
|
||||||
|
echo -e "Determining system architecture..."
|
||||||
|
|
||||||
|
case "$(uname -m)" in
|
||||||
|
aarch64)
|
||||||
|
ARCH="arm64"
|
||||||
|
;;
|
||||||
|
x86_64)
|
||||||
|
ARCH="x64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "System architecture $(uname -m) not supported."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo "System architecture $ARCH 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")"
|
DEB_PATH="/tmp/$(basename "$DEB_URL")"
|
||||||
|
|
||||||
@@ -21,6 +38,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
|
||||||
@@ -49,15 +71,6 @@ 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"
|
echo "Service $SERVICE_FILE enabled"
|
||||||
|
|
||||||
# Create the udev rule for backlight
|
|
||||||
echo -e "\nCreating udev rule for backlight..."
|
|
||||||
|
|
||||||
UDEV_FILE="/etc/udev/rules.d/backlight-permissions.rules"
|
|
||||||
sudo bash -c "cat << EOF > \"$UDEV_FILE\"
|
|
||||||
SUBSYSTEM==\"backlight\", KERNEL==\"10-0045\", RUN+=\"/bin/chmod 666 /sys/class/backlight/%k/bl_power /sys/class/backlight/%k/brightness\"
|
|
||||||
EOF"
|
|
||||||
echo "Rule $UDEV_FILE created"
|
|
||||||
|
|
||||||
# Export display variables
|
# Export display variables
|
||||||
echo -e "\nExporting display variables..."
|
echo -e "\nExporting display variables..."
|
||||||
|
|
||||||
|
|||||||
+2
-41
@@ -1,54 +1,15 @@
|
|||||||
{
|
{
|
||||||
"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": "1.0.1",
|
"version": "1.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron . --web-url=https://demo.home-assistant.io",
|
"start": "electron . --web-url=https://demo.home-assistant.io",
|
||||||
"build": "electron-forge make"
|
"build": "electron-forge make"
|
||||||
},
|
},
|
||||||
"config": {
|
|
||||||
"forge": {
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"printWidth": 120
|
"printWidth": 120
|
||||||
},
|
},
|
||||||
|
|||||||
+312
-81
@@ -6,8 +6,8 @@ const process = require("child_process");
|
|||||||
global.HARDWARE = global.HARDWARE || {
|
global.HARDWARE = global.HARDWARE || {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
status: "invalid",
|
status: "invalid",
|
||||||
|
support: {},
|
||||||
display: {
|
display: {
|
||||||
name: null,
|
|
||||||
status: {
|
status: {
|
||||||
path: null,
|
path: null,
|
||||||
value: null,
|
value: null,
|
||||||
@@ -19,6 +19,10 @@ global.HARDWARE = global.HARDWARE || {
|
|||||||
},
|
},
|
||||||
notifiers: [],
|
notifiers: [],
|
||||||
},
|
},
|
||||||
|
keyboard: {
|
||||||
|
visible: null,
|
||||||
|
notifiers: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,36 +42,54 @@ const init = async (args) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check model support
|
|
||||||
const model = getModel();
|
|
||||||
if (!model || !model.includes("Raspberry Pi")) {
|
|
||||||
console.warn(`${model} not supported`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init globals
|
// Init globals
|
||||||
HARDWARE.display.name = getDisplayName();
|
HARDWARE.support.session = sessionType();
|
||||||
HARDWARE.display.status.path = getDisplayStatusPath(HARDWARE.display.name);
|
HARDWARE.display.status.path = getDisplayStatusPath();
|
||||||
HARDWARE.display.brightness.path = getDisplayBrightnessPath(HARDWARE.display.name);
|
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.display.brightness.max = getDisplayBrightnessMax();
|
||||||
HARDWARE.initialized = true;
|
HARDWARE.initialized = true;
|
||||||
HARDWARE.status = "valid";
|
HARDWARE.status = "valid";
|
||||||
|
|
||||||
|
// Show session infos
|
||||||
|
console.log("\nUser:", os.userInfo().username);
|
||||||
|
console.log("Session:", HARDWARE.support.session);
|
||||||
|
|
||||||
// Show device infos
|
// Show device infos
|
||||||
console.log("\nModel:", model);
|
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());
|
||||||
|
|
||||||
// Show display infos
|
// Show display infos
|
||||||
console.log("\nDisplay Name:", HARDWARE.display.name);
|
console.log(`\nDisplay Status [${HARDWARE.display.status.path}]:`, getDisplayStatus());
|
||||||
console.log(`Display Status [${HARDWARE.display.status.path}]:`, getDisplayStatus());
|
|
||||||
console.log(`Display Brightness [${HARDWARE.display.brightness.path}]:`, getDisplayBrightness(), "\n");
|
console.log(`Display Brightness [${HARDWARE.display.brightness.path}]:`, getDisplayBrightness(), "\n");
|
||||||
|
|
||||||
|
// Check for keyboard visibility changes
|
||||||
|
HARDWARE.support.keyboardVisibility = processRuns("squeekboard");
|
||||||
|
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);
|
setInterval(update, 500);
|
||||||
|
|
||||||
@@ -83,8 +105,8 @@ const update = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display status has changed
|
// Display status has changed
|
||||||
if (HARDWARE.display.status.path !== null) {
|
if (HARDWARE.support.displayStatus) {
|
||||||
const status = fs.readFileSync(`${HARDWARE.display.status.path}/enabled`, "utf8").trim();
|
const status = fs.readFileSync(`${HARDWARE.display.status.path}/dpms`, "utf8").trim();
|
||||||
if (status !== HARDWARE.display.status.value) {
|
if (status !== HARDWARE.display.status.value) {
|
||||||
console.log("Update Display Status:", getDisplayStatus());
|
console.log("Update Display Status:", getDisplayStatus());
|
||||||
HARDWARE.display.status.value = status;
|
HARDWARE.display.status.value = status;
|
||||||
@@ -95,7 +117,7 @@ const update = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display brightness has changed
|
// Display brightness has changed
|
||||||
if (HARDWARE.display.brightness.path !== null) {
|
if (HARDWARE.support.displayBrightness) {
|
||||||
const brightness = fs.readFileSync(`${HARDWARE.display.brightness.path}/brightness`, "utf8").trim();
|
const brightness = fs.readFileSync(`${HARDWARE.display.brightness.path}/brightness`, "utf8").trim();
|
||||||
if (brightness !== HARDWARE.display.brightness.value) {
|
if (brightness !== HARDWARE.display.brightness.value) {
|
||||||
console.log("Update Display Brightness:", getDisplayBrightness());
|
console.log("Update Display Brightness:", getDisplayBrightness());
|
||||||
@@ -108,45 +130,99 @@ const update = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies device 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 compatibleDevice = () => {
|
const compatibleDevice = () => {
|
||||||
const files = [
|
if (os.platform() !== "linux") {
|
||||||
"/sys/class/drm",
|
return false;
|
||||||
"/sys/class/backlight",
|
}
|
||||||
"/sys/firmware/devicetree/base/model",
|
const paths = ["/sys/class/drm", "/sys/class/backlight", "/sys/class/thermal"];
|
||||||
"/sys/firmware/devicetree/base/serial-number",
|
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`.
|
* Queries the session type for the logged in user.
|
||||||
*
|
*
|
||||||
* @returns {string|null} The model of the Raspberry Pi or null if an error occurs.
|
* @returns {string} 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.support.session == 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", ["/sys/firmware/devicetree/base/serial-number"]);
|
const paths = ["/sys/firmware/devicetree/base/serial-number", "/sys/class/dmi/id/product_serial"];
|
||||||
|
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";
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -195,31 +271,22 @@ 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the primary display name using `wlopm`.
|
|
||||||
*
|
|
||||||
* @returns {string|null} The output of the command or null if an error occurs.
|
|
||||||
*/
|
|
||||||
const getDisplayName = () => {
|
|
||||||
const status = execSyncCommand("wlopm", []);
|
|
||||||
if (status !== null) {
|
|
||||||
const out = status.split("\n")[0].split(" ");
|
|
||||||
return out.shift();
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -227,10 +294,9 @@ const getDisplayName = () => {
|
|||||||
/**
|
/**
|
||||||
* Gets the current display status path using `/sys/class/drm`.
|
* Gets the current display status path using `/sys/class/drm`.
|
||||||
*
|
*
|
||||||
* @param {string} name - Primary display name.
|
|
||||||
* @returns {string|null} The display status path or null if nothing was found.
|
* @returns {string|null} The display status path or null if nothing was found.
|
||||||
*/
|
*/
|
||||||
const getDisplayStatusPath = (name) => {
|
const getDisplayStatusPath = () => {
|
||||||
const drm = "/sys/class/drm";
|
const drm = "/sys/class/drm";
|
||||||
for (const card of fs.readdirSync(drm)) {
|
for (const card of fs.readdirSync(drm)) {
|
||||||
const statusFile = path.join(drm, card, "status");
|
const statusFile = path.join(drm, card, "status");
|
||||||
@@ -238,7 +304,7 @@ const getDisplayStatusPath = (name) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const content = fs.readFileSync(statusFile, "utf8").trim();
|
const content = fs.readFileSync(statusFile, "utf8").trim();
|
||||||
if (card.includes(name) && content === "connected") {
|
if (content === "connected") {
|
||||||
return path.join(drm, card);
|
return path.join(drm, card);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,21 +312,32 @@ const getDisplayStatusPath = (name) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current display power status using `wlopm`.
|
* Gets the current display power status using `wlopm` or `xset`.
|
||||||
*
|
*
|
||||||
* @returns {string|null} The output of the command or null if an error occurs.
|
* @returns {string|null} The display status as 'ON'/'OFF' or null if an error occurs.
|
||||||
*/
|
*/
|
||||||
const getDisplayStatus = () => {
|
const getDisplayStatus = () => {
|
||||||
const status = execSyncCommand("wlopm", []);
|
if (!HARDWARE.support.displayStatus) {
|
||||||
if (status !== null) {
|
return null;
|
||||||
const out = status.split("\n")[0].split(" ");
|
}
|
||||||
return out.pop().toUpperCase();
|
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;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the display power status using `wlopm`.
|
* 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.
|
||||||
@@ -269,32 +346,35 @@ 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 (!HARDWARE.support.displayStatus) {
|
||||||
|
if (typeof callback === "function") callback(null, "Not supported");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (status !== "ON" && status !== "OFF") {
|
if (status !== "ON" && status !== "OFF") {
|
||||||
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.toLowerCase()}`, HARDWARE.display.name];
|
if (session("wayland") && commandExists("wlopm")) {
|
||||||
execAsyncCommand("wlopm", args, callback);
|
execAsyncCommand("wlopm", [`--${status.toLowerCase()}`, "*"], callback);
|
||||||
|
} else if (session("x11") && commandExists("xset")) {
|
||||||
|
execAsyncCommand("xset", ["dpms", "force", status.toLowerCase()], callback);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current display brightness path using `/sys/class/backlight`.
|
* Gets the current display brightness path using `/sys/class/backlight`.
|
||||||
*
|
*
|
||||||
* @param {string} name - Primary display name.
|
|
||||||
* @returns {string|null} The display brightness path or null if nothing was found.
|
* @returns {string|null} The display brightness path or null if nothing was found.
|
||||||
*/
|
*/
|
||||||
const getDisplayBrightnessPath = (name) => {
|
const getDisplayBrightnessPath = () => {
|
||||||
const backlight = "/sys/class/backlight";
|
const backlight = "/sys/class/backlight";
|
||||||
for (const address of fs.readdirSync(backlight)) {
|
for (const address of fs.readdirSync(backlight)) {
|
||||||
const nameFile = path.join(backlight, address, "display_name");
|
const brightnessFile = path.join(backlight, address, "brightness");
|
||||||
if (!fs.existsSync(nameFile)) {
|
if (!fs.existsSync(brightnessFile)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const content = fs.readFileSync(nameFile, "utf8").trim();
|
return path.join(backlight, address);
|
||||||
if (content.includes(name)) {
|
|
||||||
return path.join(backlight, address);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -305,7 +385,7 @@ const getDisplayBrightnessPath = (name) => {
|
|||||||
* @returns {number|null} The brightness maximum value or null if an error occurs.
|
* @returns {number|null} The brightness maximum value or null if an error occurs.
|
||||||
*/
|
*/
|
||||||
const getDisplayBrightnessMax = () => {
|
const getDisplayBrightnessMax = () => {
|
||||||
if (!HARDWARE.display.brightness.path) {
|
if (!HARDWARE.support.displayBrightness) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const max = execSyncCommand("cat", [`${HARDWARE.display.brightness.path}/max_brightness`]);
|
const max = execSyncCommand("cat", [`${HARDWARE.display.brightness.path}/max_brightness`]);
|
||||||
@@ -321,7 +401,7 @@ const getDisplayBrightnessMax = () => {
|
|||||||
* @returns {number|null} The brightness level as a percentage or null if an error occurs.
|
* @returns {number|null} The brightness level as a percentage or null if an error occurs.
|
||||||
*/
|
*/
|
||||||
const getDisplayBrightness = () => {
|
const getDisplayBrightness = () => {
|
||||||
if (!HARDWARE.display.brightness.path) {
|
if (!HARDWARE.support.displayBrightness) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const brightness = execSyncCommand("cat", [`${HARDWARE.display.brightness.path}/brightness`]);
|
const brightness = execSyncCommand("cat", [`${HARDWARE.display.brightness.path}/brightness`]);
|
||||||
@@ -342,7 +422,8 @@ const getDisplayBrightness = () => {
|
|||||||
* @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.display.brightness.path) {
|
if (!HARDWARE.support.displayBrightness) {
|
||||||
|
if (typeof callback === "function") callback(null, "Not supported");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof brightness !== "number" || brightness < 1 || brightness > 100) {
|
if (typeof brightness !== "number" || brightness < 1 || brightness > 100) {
|
||||||
@@ -352,11 +433,57 @@ const setDisplayBrightness = (brightness, callback = null) => {
|
|||||||
}
|
}
|
||||||
const max = HARDWARE.display.brightness.max;
|
const max = HARDWARE.display.brightness.max;
|
||||||
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
|
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
|
||||||
const proc = execAsyncCommand("tee", [`${HARDWARE.display.brightness.path}/brightness`], callback);
|
const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback);
|
||||||
proc.stdin.write(value.toString());
|
proc.stdin.write(value.toString());
|
||||||
proc.stdin.end();
|
proc.stdin.end();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the 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;
|
||||||
|
}
|
||||||
|
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`.
|
||||||
*
|
*
|
||||||
@@ -381,6 +508,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 !!process.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 !!process.execSync(`which ${name}`, { encoding: "utf8" });
|
||||||
|
} catch {}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a command synchronously and returns the output.
|
* Executes a command synchronously and returns the output.
|
||||||
*
|
*
|
||||||
@@ -431,6 +584,81 @@ 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = process.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 = process.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;
|
return proc;
|
||||||
@@ -440,6 +668,7 @@ module.exports = {
|
|||||||
init,
|
init,
|
||||||
update,
|
update,
|
||||||
getModel,
|
getModel,
|
||||||
|
getVendor,
|
||||||
getSerialNumber,
|
getSerialNumber,
|
||||||
getMachineId,
|
getMachineId,
|
||||||
getHostName,
|
getHostName,
|
||||||
@@ -448,7 +677,6 @@ module.exports = {
|
|||||||
getMemoryUsage,
|
getMemoryUsage,
|
||||||
getProcessorUsage,
|
getProcessorUsage,
|
||||||
getProcessorTemperature,
|
getProcessorTemperature,
|
||||||
getDisplayName,
|
|
||||||
getDisplayStatusPath,
|
getDisplayStatusPath,
|
||||||
getDisplayStatus,
|
getDisplayStatus,
|
||||||
setDisplayStatus,
|
setDisplayStatus,
|
||||||
@@ -456,6 +684,9 @@ module.exports = {
|
|||||||
getDisplayBrightnessMax,
|
getDisplayBrightnessMax,
|
||||||
getDisplayBrightness,
|
getDisplayBrightness,
|
||||||
setDisplayBrightness,
|
setDisplayBrightness,
|
||||||
|
getKeyboardVisibility,
|
||||||
|
setKeyboardVisibility,
|
||||||
|
checkPackageUpgrades,
|
||||||
shutdownSystem,
|
shutdownSystem,
|
||||||
rebootSystem,
|
rebootSystem,
|
||||||
};
|
};
|
||||||
|
|||||||
+156
-15
@@ -23,33 +23,29 @@ const init = async (args) => {
|
|||||||
const discovery = args.mqtt_discovery ? args.mqtt_discovery : "homeassistant";
|
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 +60,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,7 +71,9 @@ 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;
|
||||||
@@ -85,18 +84,29 @@ const init = async (args) => {
|
|||||||
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 (30s)
|
// Update time sensors periodically (30s)
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
updateHeartbeat(INTEGRATION.client);
|
updateHeartbeat(INTEGRATION.client);
|
||||||
|
updateLastActive(INTEGRATION.client);
|
||||||
}, 30 * 1000);
|
}, 30 * 1000);
|
||||||
|
|
||||||
// Update sensor states periodically (1min)
|
// 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;
|
||||||
};
|
};
|
||||||
@@ -112,6 +122,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);
|
||||||
@@ -268,6 +279,9 @@ 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 = {
|
const config = {
|
||||||
name: `Display`,
|
name: `Display`,
|
||||||
@@ -278,7 +292,7 @@ const initDisplay = (client) => {
|
|||||||
platform: "light",
|
platform: "light",
|
||||||
device: INTEGRATION.device,
|
device: INTEGRATION.device,
|
||||||
};
|
};
|
||||||
if (HARDWARE.display.brightness.path !== null) {
|
if (HARDWARE.support.displayBrightness) {
|
||||||
config.brightness_scale = 100;
|
config.brightness_scale = 100;
|
||||||
config.brightness_command_topic = `${root}/brightness/set`;
|
config.brightness_command_topic = `${root}/brightness/set`;
|
||||||
config.brightness_state_topic = `${root}/brightness/status`;
|
config.brightness_state_topic = `${root}/brightness/status`;
|
||||||
@@ -318,6 +332,62 @@ const updateDisplay = (client) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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",
|
||||||
|
device: INTEGRATION.device,
|
||||||
|
};
|
||||||
|
client
|
||||||
|
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||||
|
.on("message", (topic, message) => {
|
||||||
|
if (topic === `${root}/set`) {
|
||||||
|
const status = message.toString();
|
||||||
|
console.log("Set Keyboard Visibility:", status);
|
||||||
|
hardware.setKeyboardVisibility(status);
|
||||||
|
switch (status) {
|
||||||
|
case "OFF":
|
||||||
|
WEBVIEW.window.restore();
|
||||||
|
WEBVIEW.window.unmaximize();
|
||||||
|
WEBVIEW.window.setFullScreen(true);
|
||||||
|
break;
|
||||||
|
case "ON":
|
||||||
|
WEBVIEW.window.restore();
|
||||||
|
WEBVIEW.window.setFullScreen(false);
|
||||||
|
WEBVIEW.window.maximize();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.subscribe(`${root}/set`);
|
||||||
|
updateKeyboard(client);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the keyboard visibility via the mqtt connection.
|
||||||
|
*
|
||||||
|
* @param {Object} client - Instance of the mqtt client.
|
||||||
|
*/
|
||||||
|
const updateKeyboard = (client) => {
|
||||||
|
const visibility = hardware.getKeyboardVisibility();
|
||||||
|
const root = `${INTEGRATION.discovery}/switch/${INTEGRATION.node}/keyboard`;
|
||||||
|
if (visibility !== null) {
|
||||||
|
client.publish(`${root}/status`, `${visibility}`, { qos: 1, retain: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the model sensor.
|
* Initializes the model sensor.
|
||||||
*
|
*
|
||||||
@@ -579,6 +649,44 @@ const updateProcessorTemperature = (client) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the package upgrades sensor.
|
||||||
|
*
|
||||||
|
* @param {Object} client - Instance of the mqtt client.
|
||||||
|
*/
|
||||||
|
const initPackageUpgrades = (client) => {
|
||||||
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/package_upgrades`;
|
||||||
|
const config = {
|
||||||
|
name: `Package Upgrades`,
|
||||||
|
unique_id: `${INTEGRATION.node}_package_upgrades`,
|
||||||
|
state_topic: `${root}/status`,
|
||||||
|
json_attributes_topic: `${root}/attributes`,
|
||||||
|
value_template: "{{ value }}",
|
||||||
|
icon: "mdi:package-down",
|
||||||
|
device: INTEGRATION.device,
|
||||||
|
};
|
||||||
|
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||||
|
updatePackageUpgrades(client);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the package upgrades sensor via the mqtt connection.
|
||||||
|
*
|
||||||
|
* @param {Object} client - Instance of the mqtt client.
|
||||||
|
*/
|
||||||
|
const updatePackageUpgrades = (client) => {
|
||||||
|
const packages = hardware.checkPackageUpgrades();
|
||||||
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/package_upgrades`;
|
||||||
|
if (packages !== null) {
|
||||||
|
const attributes = {
|
||||||
|
total: packages.length,
|
||||||
|
packages: packages.map((pkg) => pkg.replace(/\s*\[.*?\]\s*/g, "").trim()),
|
||||||
|
};
|
||||||
|
client.publish(`${root}/status`, `${attributes.total}`, { qos: 1, retain: true });
|
||||||
|
client.publish(`${root}/attributes`, JSON.stringify(attributes), { qos: 1, retain: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the heartbeat sensor.
|
* Initializes the heartbeat sensor.
|
||||||
*
|
*
|
||||||
@@ -611,6 +719,39 @@ const updateHeartbeat = (client) => {
|
|||||||
client.publish(`${root}/status`, `${heartbeat}`, { qos: 1, retain: true });
|
client.publish(`${root}/status`, `${heartbeat}`, { qos: 1, retain: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the last active sensor.
|
||||||
|
*
|
||||||
|
* @param {Object} client - Instance of the mqtt client.
|
||||||
|
*/
|
||||||
|
const initLastActive = (client) => {
|
||||||
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/last_active`;
|
||||||
|
const config = {
|
||||||
|
name: `Last Active`,
|
||||||
|
unique_id: `${INTEGRATION.node}_last_active`,
|
||||||
|
state_topic: `${root}/status`,
|
||||||
|
value_template: "{{ (value | float) | round(0) }}",
|
||||||
|
unit_of_measurement: "min",
|
||||||
|
icon: "mdi:gesture-tap-hold",
|
||||||
|
device: INTEGRATION.device,
|
||||||
|
};
|
||||||
|
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||||
|
updateLastActive(client);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the last active sensor via the mqtt connection.
|
||||||
|
*
|
||||||
|
* @param {Object} client - Instance of the mqtt client.
|
||||||
|
*/
|
||||||
|
const updateLastActive = (client) => {
|
||||||
|
const now = new Date();
|
||||||
|
const then = WEBVIEW.pointer.time;
|
||||||
|
const lastActive = Math.abs(now - then) / (1000 * 60);
|
||||||
|
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/last_active`;
|
||||||
|
client.publish(`${root}/status`, `${lastActive}`, { qos: 1, retain: true });
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
update,
|
update,
|
||||||
|
|||||||
+47
-20
@@ -1,12 +1,16 @@
|
|||||||
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, BaseWindow, WebContentsView } = require("electron");
|
||||||
|
|
||||||
global.WEBVIEW = global.WEBVIEW || {
|
global.WEBVIEW = global.WEBVIEW || {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
status: "offline",
|
status: "offline",
|
||||||
locked: false,
|
locked: false,
|
||||||
|
pointer: {
|
||||||
|
position: {},
|
||||||
|
time: new Date(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +44,7 @@ const init = async (args) => {
|
|||||||
|
|
||||||
// Register events
|
// Register events
|
||||||
windowEvents(WEBVIEW.window, zoom);
|
windowEvents(WEBVIEW.window, zoom);
|
||||||
|
touchEvents(WEBVIEW.window);
|
||||||
viewEvents(WEBVIEW.view);
|
viewEvents(WEBVIEW.view);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -86,24 +91,6 @@ const windowEvents = (window, zoom) => {
|
|||||||
window.on("resize", resize);
|
window.on("resize", resize);
|
||||||
resize();
|
resize();
|
||||||
|
|
||||||
// Handle focus change for webview lock
|
|
||||||
HARDWARE.display.notifiers.push(() => {
|
|
||||||
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
|
|
||||||
if (WEBVIEW.locked) {
|
|
||||||
window.blur();
|
|
||||||
}
|
|
||||||
/* This also works, but it doesn't allow to react for screen wake up.
|
|
||||||
window.setIgnoreMouseEvents(hardware.getDisplayStatus() === "OFF");
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
window.on("focus", () => {
|
|
||||||
if (WEBVIEW.locked) {
|
|
||||||
hardware.setDisplayStatus("ON");
|
|
||||||
window.blur();
|
|
||||||
}
|
|
||||||
WEBVIEW.locked = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle window status updates
|
// Handle window status updates
|
||||||
window.on("minimize", update);
|
window.on("minimize", update);
|
||||||
window.on("restore", update);
|
window.on("restore", update);
|
||||||
@@ -128,6 +115,46 @@ const windowEvents = (window, zoom) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register touch events and handler.
|
||||||
|
*
|
||||||
|
* @param {Object} window - The root window object.
|
||||||
|
*/
|
||||||
|
const touchEvents = (window) => {
|
||||||
|
// Handle focus change for webview lock
|
||||||
|
window.on("focus", () => {
|
||||||
|
if (WEBVIEW.locked) {
|
||||||
|
hardware.setDisplayStatus("ON");
|
||||||
|
window.blur();
|
||||||
|
}
|
||||||
|
WEBVIEW.locked = false;
|
||||||
|
});
|
||||||
|
HARDWARE.display.notifiers.push(() => {
|
||||||
|
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
|
||||||
|
if (WEBVIEW.locked) {
|
||||||
|
window.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle 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 webview events and handler.
|
* Register webview events and handler.
|
||||||
*
|
*
|
||||||
@@ -137,8 +164,8 @@ const viewEvents = (view) => {
|
|||||||
// Enable webview touch emulation
|
// Enable webview touch emulation
|
||||||
view.webContents.debugger.attach("1.1");
|
view.webContents.debugger.attach("1.1");
|
||||||
view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", {
|
view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", {
|
||||||
enabled: true,
|
|
||||||
configuration: "mobile",
|
configuration: "mobile",
|
||||||
|
enabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disable webview hyperlinks
|
// Disable webview hyperlinks
|
||||||
|
|||||||
Reference in New Issue
Block a user