1257 lines
38 KiB
Bash
Executable File
1257 lines
38 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#:########################################################################################:#
|
|
# HA Kiosk MQTT Control Script #
|
|
#:########################################################################################:#
|
|
#
|
|
# TITLE:
|
|
# HA Kiosk MQTT Control
|
|
#
|
|
# VERSION:
|
|
# V3.0 - 2026-09-26
|
|
#
|
|
# PURPOSE:
|
|
# Provides local MQTT control and state reporting for a Linux touchscreen kiosk.
|
|
#
|
|
# CONTROLS:
|
|
# - Four persistent display modes using one MQTT select.
|
|
# - Saved current brightness and physical backlight brightness.
|
|
# - Temporary presence brightness from touch or camera motion.
|
|
# - Shared touch and presence timeout using a ten-step brightness ramp.
|
|
# - Speaker volume and mute using PipeWire wpctl.
|
|
# - Actual screen, screensaver, brightness, and presence state reporting.
|
|
#
|
|
# DISPLAY MODES:
|
|
# Screen Lock:
|
|
# - Disables touchscreen input.
|
|
# - Stops XScreenSaver.
|
|
# - Keeps the physical display off.
|
|
# - Ignores touch and camera-presence activity.
|
|
#
|
|
# Screen Off:
|
|
# - Keeps the touchscreen enabled.
|
|
# - Keeps the physical display off while presence is inactive.
|
|
# - Touch always wakes the display.
|
|
# - Camera motion wakes the display when Motion Wake is enabled.
|
|
# - Returns to Screen Off after the presence ramp reaches Current Brightness.
|
|
#
|
|
# Screensaver:
|
|
# - Starts XScreenSaver immediately at Current Brightness.
|
|
# - Touch stops the screensaver and activates Presence Brightness.
|
|
# - Camera motion does the same when Motion Wake is enabled.
|
|
# - Returns to XScreenSaver at Current Brightness after the presence ramp.
|
|
#
|
|
# Screen On:
|
|
# - Keeps the display on at Current Brightness while presence is inactive.
|
|
# - Touch activates Presence Brightness.
|
|
# - Camera motion does the same when Motion Wake is enabled.
|
|
# - Returns to Current Brightness after the presence ramp.
|
|
#
|
|
# PRESENCE OPERATION:
|
|
# - Touch and camera motion share the same Presence Timeout.
|
|
# - Touch always counts as activity except in Screen Lock mode.
|
|
# - Camera motion counts as activity only when Motion Wake is enabled.
|
|
# - Activity turns the display on and applies Presence Brightness immediately.
|
|
# - A touch starts or restarts the ten-step brightness ramp immediately unless
|
|
# qualifying camera motion is currently holding presence active.
|
|
# - Qualifying camera motion holds presence active until motion ends.
|
|
# - The latest saved Current Brightness is used as the ramp target.
|
|
# - The selected display mode determines what happens after the final ramp step.
|
|
# - A Presence End MQTT command can run a one-off ramp without changing the
|
|
# saved Presence Timeout.
|
|
#
|
|
# MQTT AUTHENTICATION:
|
|
# Configure Mosquitto client defaults securely on this host. This script uses
|
|
# the installed mosquitto_pub and mosquitto_sub client configuration.
|
|
#
|
|
# VERSION HISTORY:
|
|
# V3.0 - 2026-09-26
|
|
# - Parameterized kiosk identity, MQTT roots, display settings and service names.
|
|
# - Added periodic retained-state refresh and an MQTT subscriber heartbeat watchdog.
|
|
# - Kept the browser in an independent restartable systemd service.
|
|
# - Added a Presence End MQTT command for one-off presence ramps.
|
|
#
|
|
# V2.0 - 2026-07-28
|
|
# - Replaced separate Screen and Screensaver command switches with one
|
|
# persistent four-option Display Mode control.
|
|
# - Added Screen Lock, Screen Off, Screensaver, and Screen On modes.
|
|
# - Added direct touchscreen activity detection using XInput RawTouchBegin.
|
|
# - Made touch and camera presence use the same Presence Timeout and ramp.
|
|
# - Added automatic return to each selected mode after presence finishes.
|
|
# - Added touchscreen disable and display enforcement for Screen Lock.
|
|
# - Retained actual Screen and Screensaver state topics as diagnostics.
|
|
#
|
|
# V1.7 - 2026-07-27
|
|
# - Re-enabled DPMS after XScreenSaver activation.
|
|
# - Corrected Screen state reporting while the screensaver is running.
|
|
#
|
|
# V1.6 - 2026-07-27
|
|
# - Changed Screensaver ON to explicitly turn the display on through DPMS.
|
|
# - Ensured the Screen state reports ON whenever the screensaver is enabled.
|
|
#
|
|
# V1.5 - 2026-07-27
|
|
# - Changed Motion Wake ON to immediately wake the display when motion is
|
|
# currently detected or presence behaviour is already active.
|
|
# - Removed the requirement to wait for a new motion event after enabling
|
|
# Motion Wake.
|
|
#
|
|
# V1.4 - 2026-07-27
|
|
# - Added persistent MQTT control for motion-triggered display wake-up.
|
|
# - Kept camera motion detection active while Motion Wake is disabled.
|
|
# - Added retained Motion Wake state reporting.
|
|
#
|
|
# V1.3 - 2026-07-27
|
|
# - Replaced the fixed presence-timeout delay with a ten-step brightness ramp.
|
|
# - Spread the ten ramp steps evenly across the configured timeout.
|
|
# - Made the ramp follow the latest saved Current Brightness value.
|
|
# - Added immediate ramp cancellation when new movement is detected.
|
|
#
|
|
# V1.2 - 2026-07-27
|
|
# - Added persistent Current Brightness and Presence Brightness values.
|
|
# - Added local presence handling from the retained Motion MQTT state.
|
|
# - Added a configurable local presence timeout.
|
|
# - Added reporting of the actual physical backlight level.
|
|
#
|
|
# V1.1 - 2026-07-27
|
|
# - Added MQTT control and reporting for XScreenSaver.
|
|
# - Forced XScreenSaver OFF whenever the control service starts.
|
|
# - Changed Mosquitto commands to use protected client configuration files.
|
|
#
|
|
# V1.0 - 2026-07-26
|
|
# - Initial screen, brightness, volume, and mute controls.
|
|
#
|
|
|
|
set -u
|
|
|
|
# Optional per-device shell configuration (never commit the real file).
|
|
KIOSK_CONFIG_FILE="${KIOSK_CONFIG_FILE:-$HOME/.config/ha-kiosk/config}"
|
|
if [[ -r "$KIOSK_CONFIG_FILE" ]]; then
|
|
# shellcheck source=/dev/null
|
|
source "$KIOSK_CONFIG_FILE"
|
|
fi
|
|
|
|
: "${KIOSK_ID:=kiosk}"
|
|
: "${COMMAND_ROOT:=kiosk/commands}"
|
|
: "${STATUS_ROOT:=kiosk/status}"
|
|
: "${BROWSER_SERVICE:=ha-kiosk-browser.service}"
|
|
: "${MQTT_REPUBLISH_INTERVAL_SECONDS:=30}"
|
|
: "${MQTT_HEARTBEAT_TIMEOUT_SECONDS:=90}"
|
|
: "${MOTION_SERVICE:=${KIOSK_ID}-motion.service}"
|
|
: "${MOTION_EVENT_SCRIPT:=$HOME/${KIOSK_ID}-motion-event.sh}"
|
|
: "${STATE_DIR:=${XDG_STATE_HOME:-$HOME/.local/state}/ha-kiosk-${KIOSK_ID}}"
|
|
export DISPLAY="${KIOSK_DISPLAY:-:0}"
|
|
export XAUTHORITY="${KIOSK_XAUTHORITY:-$HOME/.Xauthority}"
|
|
|
|
#:########################################################################################:#
|
|
# Device Settings #
|
|
#:########################################################################################:#
|
|
|
|
TOUCH_DEVICE_NAME="${TOUCH_DEVICE_NAME:-Touchscreen Device Name}"
|
|
TOUCH_DISPLAY_OUTPUT="${TOUCH_DISPLAY_OUTPUT:-eDP-1}"
|
|
SCREEN_LOCK_BRIGHTNESS="${SCREEN_LOCK_BRIGHTNESS:-0}"
|
|
|
|
#:########################################################################################:#
|
|
# MQTT Topics #
|
|
#:########################################################################################:#
|
|
|
|
DISPLAY_MODE_COMMAND_TOPIC="${COMMAND_ROOT}/${KIOSK_ID}/display_mode/set"
|
|
DISPLAY_MODE_STATE_TOPIC="${STATUS_ROOT}/${KIOSK_ID}/display_mode/state"
|
|
|
|
# These state topics now report actual physical conditions only.
|
|
SCREEN_STATE_TOPIC="${STATUS_ROOT}/${KIOSK_ID}/screen/state"
|
|
SCREENSAVER_STATE_TOPIC="${STATUS_ROOT}/${KIOSK_ID}/screensaver/state"
|
|
|
|
CURRENT_BRIGHTNESS_COMMAND_TOPIC="${COMMAND_ROOT}/${KIOSK_ID}/brightness/set"
|
|
CURRENT_BRIGHTNESS_STATE_TOPIC="${STATUS_ROOT}/${KIOSK_ID}/brightness/state"
|
|
ACTUAL_BRIGHTNESS_STATE_TOPIC="${STATUS_ROOT}/${KIOSK_ID}/brightness/actual/state"
|
|
|
|
PRESENCE_BRIGHTNESS_COMMAND_TOPIC="${COMMAND_ROOT}/${KIOSK_ID}/brightness/presence/set"
|
|
PRESENCE_BRIGHTNESS_STATE_TOPIC="${STATUS_ROOT}/${KIOSK_ID}/brightness/presence/state"
|
|
PRESENCE_ACTIVE_STATE_TOPIC="${STATUS_ROOT}/${KIOSK_ID}/presence/state"
|
|
|
|
PRESENCE_TIMEOUT_COMMAND_TOPIC="${COMMAND_ROOT}/${KIOSK_ID}/presence/timeout/set"
|
|
PRESENCE_TIMEOUT_STATE_TOPIC="${STATUS_ROOT}/${KIOSK_ID}/presence/timeout/state"
|
|
PRESENCE_END_COMMAND_TOPIC="${COMMAND_ROOT}/${KIOSK_ID}/presence/end"
|
|
|
|
MOTION_STATE_TOPIC="${STATUS_ROOT}/${KIOSK_ID}/motion/state"
|
|
|
|
MOTION_WAKE_COMMAND_TOPIC="${COMMAND_ROOT}/${KIOSK_ID}/motion_wake/set"
|
|
MOTION_WAKE_STATE_TOPIC="${STATUS_ROOT}/${KIOSK_ID}/motion_wake/state"
|
|
|
|
MOTION_SERVICE="${MOTION_SERVICE}"
|
|
MOTION_EVENT_SCRIPT="${MOTION_EVENT_SCRIPT}"
|
|
|
|
VOLUME_COMMAND_TOPIC="${COMMAND_ROOT}/${KIOSK_ID}/audio/volume/set"
|
|
VOLUME_STATE_TOPIC="${STATUS_ROOT}/${KIOSK_ID}/audio/volume/state"
|
|
|
|
MUTE_COMMAND_TOPIC="${COMMAND_ROOT}/${KIOSK_ID}/audio/mute/set"
|
|
MUTE_STATE_TOPIC="${STATUS_ROOT}/${KIOSK_ID}/audio/mute/state"
|
|
|
|
CHROME_RESTART_COMMAND_TOPIC="${COMMAND_ROOT}/${KIOSK_ID}/chrome/restart"
|
|
MQTT_HEARTBEAT_TOPIC="${STATUS_ROOT}/${KIOSK_ID}/control/heartbeat"
|
|
MQTT_REPUBLISH_INTERVAL_SECONDS="${MQTT_REPUBLISH_INTERVAL_SECONDS:-30}"
|
|
MQTT_HEARTBEAT_TIMEOUT_SECONDS="${MQTT_HEARTBEAT_TIMEOUT_SECONDS:-90}"
|
|
|
|
# Previous command topics are cleared during initialisation and are no longer
|
|
# subscribed to by this script.
|
|
LEGACY_SCREEN_COMMAND_TOPIC="${COMMAND_ROOT}/${KIOSK_ID}/screen/set"
|
|
LEGACY_SCREENSAVER_COMMAND_TOPIC="${COMMAND_ROOT}/${KIOSK_ID}/screensaver/set"
|
|
|
|
#:########################################################################################:#
|
|
# Local State #
|
|
#:########################################################################################:#
|
|
|
|
STATE_DIR="${STATE_DIR}"
|
|
DISPLAY_MODE_FILE="$STATE_DIR/display_mode"
|
|
CURRENT_BRIGHTNESS_FILE="$STATE_DIR/current_brightness"
|
|
PRESENCE_BRIGHTNESS_FILE="$STATE_DIR/presence_brightness"
|
|
PRESENCE_ACTIVE_FILE="$STATE_DIR/presence_active"
|
|
PRESENCE_TIMEOUT_FILE="$STATE_DIR/presence_timeout_seconds"
|
|
MOTION_WAKE_FILE="$STATE_DIR/motion_wake"
|
|
MQTT_EVENT_FIFO="$STATE_DIR/mqtt-events.fifo"
|
|
TOUCH_EVENT_FIFO="$STATE_DIR/touch-events.fifo"
|
|
|
|
mkdir -p "$STATE_DIR"
|
|
chmod 700 "$STATE_DIR"
|
|
|
|
#:########################################################################################:#
|
|
# MQTT Publishing #
|
|
#:########################################################################################:#
|
|
|
|
mqtt_publish() {
|
|
/usr/bin/mosquitto_pub "$@"
|
|
}
|
|
|
|
publish_retained() {
|
|
local topic="$1"
|
|
local payload="$2"
|
|
|
|
mqtt_publish \
|
|
-r \
|
|
-t "$topic" \
|
|
-m "$payload"
|
|
}
|
|
|
|
#:########################################################################################:#
|
|
# Value Helpers #
|
|
#:########################################################################################:#
|
|
|
|
clamp_integer() {
|
|
local value="$1"
|
|
local minimum="$2"
|
|
local maximum="$3"
|
|
|
|
if (( value < minimum )); then
|
|
value="$minimum"
|
|
elif (( value > maximum )); then
|
|
value="$maximum"
|
|
fi
|
|
|
|
printf '%s\n' "$value"
|
|
}
|
|
|
|
read_integer_file() {
|
|
local file="$1"
|
|
local fallback="$2"
|
|
local minimum="$3"
|
|
local maximum="$4"
|
|
local value=""
|
|
|
|
if [[ -r "$file" ]]; then
|
|
read -r value < "$file" || true
|
|
fi
|
|
|
|
if [[ ! "$value" =~ ^[0-9]+$ ]]; then
|
|
value="$fallback"
|
|
fi
|
|
|
|
clamp_integer "$value" "$minimum" "$maximum"
|
|
}
|
|
|
|
write_state_file() {
|
|
local file="$1"
|
|
local value="$2"
|
|
local temporary_file="${file}.tmp"
|
|
|
|
printf '%s\n' "$value" > "$temporary_file"
|
|
chmod 600 "$temporary_file"
|
|
mv -f "$temporary_file" "$file"
|
|
}
|
|
|
|
get_current_brightness() {
|
|
read_integer_file "$CURRENT_BRIGHTNESS_FILE" 100 1 100
|
|
}
|
|
|
|
get_presence_brightness() {
|
|
read_integer_file "$PRESENCE_BRIGHTNESS_FILE" 80 1 100
|
|
}
|
|
|
|
get_presence_timeout() {
|
|
read_integer_file "$PRESENCE_TIMEOUT_FILE" 300 5 86400
|
|
}
|
|
|
|
get_motion_wake() {
|
|
local value="ON"
|
|
|
|
if [[ -r "$MOTION_WAKE_FILE" ]]; then
|
|
read -r value < "$MOTION_WAKE_FILE" || true
|
|
fi
|
|
|
|
case "${value^^}" in
|
|
OFF)
|
|
printf 'OFF\n'
|
|
;;
|
|
|
|
*)
|
|
printf 'ON\n'
|
|
;;
|
|
esac
|
|
}
|
|
|
|
get_presence_active() {
|
|
local value="OFF"
|
|
|
|
if [[ -r "$PRESENCE_ACTIVE_FILE" ]]; then
|
|
read -r value < "$PRESENCE_ACTIVE_FILE" || true
|
|
fi
|
|
|
|
case "${value^^}" in
|
|
ON)
|
|
printf 'ON\n'
|
|
;;
|
|
|
|
*)
|
|
printf 'OFF\n'
|
|
;;
|
|
esac
|
|
}
|
|
|
|
get_display_mode() {
|
|
local value="Screen On"
|
|
|
|
if [[ -r "$DISPLAY_MODE_FILE" ]]; then
|
|
read -r value < "$DISPLAY_MODE_FILE" || true
|
|
fi
|
|
|
|
case "${value^^}" in
|
|
"SCREEN LOCK")
|
|
printf 'Screen Lock\n'
|
|
;;
|
|
|
|
"SCREEN OFF")
|
|
printf 'Screen Off\n'
|
|
;;
|
|
|
|
"SCREENSAVER")
|
|
printf 'Screensaver\n'
|
|
;;
|
|
|
|
*)
|
|
printf 'Screen On\n'
|
|
;;
|
|
esac
|
|
}
|
|
|
|
|
|
#:########################################################################################:#
|
|
# Camera Service Functions #
|
|
#:########################################################################################:#
|
|
|
|
camera_should_run() {
|
|
if [[ "$(get_motion_wake)" == "ON" ]] &&
|
|
[[ "$(get_display_mode)" != "Screen Lock" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
sync_camera_service() {
|
|
if camera_should_run; then
|
|
if ! /usr/bin/systemctl --user \
|
|
is-active --quiet "$MOTION_SERVICE"; then
|
|
|
|
/usr/bin/systemctl --user \
|
|
start "$MOTION_SERVICE" \
|
|
>/dev/null 2>&1 || true
|
|
fi
|
|
|
|
else
|
|
if /usr/bin/systemctl --user \
|
|
is-active --quiet "$MOTION_SERVICE"; then
|
|
|
|
/usr/bin/systemctl --user \
|
|
stop "$MOTION_SERVICE" \
|
|
>/dev/null 2>&1 || true
|
|
fi
|
|
|
|
# Ensure the retained Motion state returns to OFF even when Motion is
|
|
# deliberately stopped before it publishes an event-end message.
|
|
if [[ -x "$MOTION_EVENT_SCRIPT" ]]; then
|
|
"$MOTION_EVENT_SCRIPT" end >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
set_presence_active() {
|
|
local value="${1^^}"
|
|
|
|
case "$value" in
|
|
ON|OFF)
|
|
write_state_file "$PRESENCE_ACTIVE_FILE" "$value"
|
|
publish_retained "$PRESENCE_ACTIVE_STATE_TOPIC" "$value"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
set_display_mode_state() {
|
|
local mode="$1"
|
|
|
|
write_state_file "$DISPLAY_MODE_FILE" "$mode"
|
|
publish_retained "$DISPLAY_MODE_STATE_TOPIC" "$mode"
|
|
}
|
|
|
|
#:########################################################################################:#
|
|
# Display and Audio Helpers #
|
|
#:########################################################################################:#
|
|
|
|
set_physical_brightness() {
|
|
local brightness="$1"
|
|
|
|
/usr/bin/brightnessctl \
|
|
--device=intel_backlight \
|
|
set "${brightness}%" \
|
|
>/dev/null
|
|
}
|
|
|
|
get_physical_brightness() {
|
|
/usr/bin/brightnessctl \
|
|
--device=intel_backlight \
|
|
--machine-readable |
|
|
/usr/bin/cut -d',' -f4 |
|
|
/usr/bin/tr -d '%'
|
|
}
|
|
|
|
get_screen_state() {
|
|
/usr/bin/xset q |
|
|
/usr/bin/awk '/Monitor is/ { print toupper($3) }'
|
|
}
|
|
|
|
touchscreen_is_enabled() {
|
|
/usr/bin/xinput \
|
|
list-props "$TOUCH_DEVICE_NAME" |
|
|
/usr/bin/awk '/Device Enabled/ { print $NF; exit }'
|
|
}
|
|
|
|
touchscreen_enable() {
|
|
/usr/bin/xinput \
|
|
enable "$TOUCH_DEVICE_NAME" \
|
|
>/dev/null 2>&1 || true
|
|
|
|
/usr/bin/xinput \
|
|
map-to-output "$TOUCH_DEVICE_NAME" "$TOUCH_DISPLAY_OUTPUT" \
|
|
>/dev/null 2>&1 || true
|
|
}
|
|
|
|
touchscreen_disable() {
|
|
/usr/bin/xinput \
|
|
disable "$TOUCH_DEVICE_NAME" \
|
|
>/dev/null 2>&1 || true
|
|
}
|
|
|
|
#:########################################################################################:#
|
|
# Chrome Kiosk Functions #
|
|
#:########################################################################################:#
|
|
|
|
restart_chrome_kiosk() {
|
|
/usr/bin/systemctl --user restart "$BROWSER_SERVICE"
|
|
}
|
|
|
|
#:########################################################################################:#
|
|
# Screensaver Functions #
|
|
#:########################################################################################:#
|
|
|
|
screensaver_is_running() {
|
|
/usr/bin/pgrep -x xscreensaver >/dev/null 2>&1
|
|
}
|
|
|
|
screensaver_start() {
|
|
# The screensaver requires the physical display to be on.
|
|
/usr/bin/xset +dpms
|
|
/usr/bin/xset dpms force on
|
|
|
|
if ! screensaver_is_running; then
|
|
/usr/bin/xscreensaver \
|
|
-no-splash \
|
|
>/dev/null 2>&1 &
|
|
|
|
for _ in {1..20}; do
|
|
if screensaver_is_running; then
|
|
break
|
|
fi
|
|
|
|
/usr/bin/sleep 0.1
|
|
done
|
|
fi
|
|
|
|
/usr/bin/xscreensaver-command \
|
|
-activate \
|
|
>/dev/null 2>&1 || true
|
|
|
|
# XScreenSaver disables DPMS while activating. Re-enable it afterwards so
|
|
# physical screen state reporting and manual DPMS control continue to work.
|
|
/usr/bin/xset +dpms
|
|
/usr/bin/xset dpms force on
|
|
}
|
|
|
|
screensaver_stop() {
|
|
/usr/bin/xscreensaver-command \
|
|
-exit \
|
|
>/dev/null 2>&1 || true
|
|
|
|
/usr/bin/pkill \
|
|
-x glmatrix \
|
|
>/dev/null 2>&1 || true
|
|
}
|
|
|
|
#:########################################################################################:#
|
|
# Display Mode Functions #
|
|
#:########################################################################################:#
|
|
|
|
apply_display_mode_base_state() {
|
|
local mode=""
|
|
local current_brightness=""
|
|
|
|
mode="$(get_display_mode)"
|
|
current_brightness="$(get_current_brightness)"
|
|
|
|
case "$mode" in
|
|
"Screen Lock")
|
|
touchscreen_disable
|
|
screensaver_stop
|
|
/usr/bin/xset +dpms
|
|
/usr/bin/xset dpms force off
|
|
set_physical_brightness "$SCREEN_LOCK_BRIGHTNESS"
|
|
;;
|
|
|
|
"Screen Off")
|
|
touchscreen_enable
|
|
screensaver_stop
|
|
set_physical_brightness "$SCREEN_LOCK_BRIGHTNESS"
|
|
/usr/bin/xset +dpms
|
|
/usr/bin/xset dpms force off
|
|
;;
|
|
|
|
"Screensaver")
|
|
touchscreen_enable
|
|
set_physical_brightness "$current_brightness"
|
|
screensaver_start
|
|
;;
|
|
|
|
"Screen On")
|
|
touchscreen_enable
|
|
screensaver_stop
|
|
set_physical_brightness "$current_brightness"
|
|
/usr/bin/xset +dpms
|
|
/usr/bin/xset dpms force on
|
|
;;
|
|
esac
|
|
}
|
|
|
|
enforce_display_mode() {
|
|
local mode=""
|
|
local screen_state=""
|
|
local touch_enabled=""
|
|
|
|
mode="$(get_display_mode)"
|
|
screen_state="$(get_screen_state)"
|
|
touch_enabled="$(touchscreen_is_enabled)"
|
|
|
|
case "$mode" in
|
|
"Screen Lock")
|
|
if [[ "$touch_enabled" != "0" ]]; then
|
|
touchscreen_disable
|
|
fi
|
|
|
|
if screensaver_is_running; then
|
|
screensaver_stop
|
|
fi
|
|
|
|
if [[ "$screen_state" != "OFF" ]] ||
|
|
[[ "$(get_physical_brightness)" -gt "$SCREEN_LOCK_BRIGHTNESS" ]]; then
|
|
printf '%s Screen Lock corrected unexpected DPMS state: %s\n' \
|
|
"$(/usr/bin/date --iso-8601=seconds)" \
|
|
"${screen_state:-UNKNOWN}" \
|
|
>&2
|
|
|
|
/usr/bin/xset +dpms
|
|
/usr/bin/xset dpms force off
|
|
set_physical_brightness "$SCREEN_LOCK_BRIGHTNESS"
|
|
fi
|
|
;;
|
|
|
|
"Screen Off")
|
|
if [[ "$touch_enabled" != "1" ]]; then
|
|
touchscreen_enable
|
|
fi
|
|
|
|
if [[ "$(get_presence_active)" != "ON" ]]; then
|
|
if screensaver_is_running; then
|
|
screensaver_stop
|
|
fi
|
|
|
|
#if [[ "$screen_state" != "OFF" ]]; then
|
|
/usr/bin/xset +dpms
|
|
/usr/bin/xset dpms force off
|
|
#fi
|
|
fi
|
|
;;
|
|
|
|
"Screensaver")
|
|
if [[ "$touch_enabled" != "1" ]]; then
|
|
touchscreen_enable
|
|
fi
|
|
|
|
if [[ "$(get_presence_active)" != "ON" ]]; then
|
|
if [[ "$screen_state" != "ON" ]] ||
|
|
! screensaver_is_running; then
|
|
screensaver_start
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
"Screen On")
|
|
if [[ "$touch_enabled" != "1" ]]; then
|
|
touchscreen_enable
|
|
fi
|
|
|
|
if [[ "$(get_presence_active)" != "ON" ]]; then
|
|
if screensaver_is_running; then
|
|
screensaver_stop
|
|
fi
|
|
|
|
if [[ "$screen_state" != "ON" ]]; then
|
|
/usr/bin/xset +dpms
|
|
/usr/bin/xset dpms force on
|
|
fi
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
set_display_mode() {
|
|
local mode="$1"
|
|
|
|
cancel_presence_ramp
|
|
set_presence_active "OFF"
|
|
set_display_mode_state "$mode"
|
|
apply_display_mode_base_state
|
|
sync_camera_service
|
|
}
|
|
|
|
#:########################################################################################:#
|
|
# Presence Functions #
|
|
#:########################################################################################:#
|
|
|
|
presence_ramp_pid=""
|
|
|
|
cancel_presence_ramp() {
|
|
if [[ -n "$presence_ramp_pid" ]] &&
|
|
/usr/bin/kill -0 "$presence_ramp_pid" >/dev/null 2>&1; then
|
|
/usr/bin/kill "$presence_ramp_pid" >/dev/null 2>&1 || true
|
|
wait "$presence_ramp_pid" 2>/dev/null || true
|
|
fi
|
|
|
|
presence_ramp_pid=""
|
|
}
|
|
|
|
activate_presence() {
|
|
local mode=""
|
|
local presence_brightness=""
|
|
|
|
mode="$(get_display_mode)"
|
|
|
|
if [[ "$mode" == "Screen Lock" ]]; then
|
|
return
|
|
fi
|
|
|
|
cancel_presence_ramp
|
|
|
|
presence_brightness="$(get_presence_brightness)"
|
|
|
|
touchscreen_enable
|
|
screensaver_stop
|
|
/usr/bin/xset +dpms
|
|
/usr/bin/xset dpms force on
|
|
set_physical_brightness "$presence_brightness"
|
|
set_presence_active "ON"
|
|
}
|
|
|
|
run_presence_ramp() {
|
|
local timeout_seconds="${1:-}"
|
|
local interval_seconds=""
|
|
local remaining_steps=""
|
|
local actual_brightness=""
|
|
local target_brightness=""
|
|
local brightness_difference=""
|
|
local brightness_change=""
|
|
local next_brightness=""
|
|
|
|
if [[ -z "$timeout_seconds" ]]; then
|
|
timeout_seconds="$(get_presence_timeout)"
|
|
fi
|
|
|
|
interval_seconds="$(
|
|
/usr/bin/awk \
|
|
-v timeout="$timeout_seconds" \
|
|
'BEGIN { printf "%.3f\n", timeout / 10 }'
|
|
)"
|
|
|
|
for ((remaining_steps = 10; remaining_steps >= 1; remaining_steps--)); do
|
|
/usr/bin/sleep "$interval_seconds"
|
|
|
|
# Stop without changing the selected display mode when cancelled.
|
|
if [[ "$(get_presence_active)" != "ON" ]]; then
|
|
return
|
|
fi
|
|
|
|
actual_brightness="$(get_physical_brightness)"
|
|
target_brightness="$(get_current_brightness)"
|
|
|
|
if [[ ! "$actual_brightness" =~ ^[0-9]+$ ]] ||
|
|
[[ ! "$target_brightness" =~ ^[0-9]+$ ]]; then
|
|
continue
|
|
fi
|
|
|
|
brightness_difference=$((target_brightness - actual_brightness))
|
|
|
|
# The final step always applies the exact latest Current Brightness.
|
|
if (( remaining_steps == 1 )); then
|
|
next_brightness="$target_brightness"
|
|
|
|
elif (( brightness_difference >= 0 )); then
|
|
brightness_change=$((
|
|
(brightness_difference + remaining_steps / 2) /
|
|
remaining_steps
|
|
))
|
|
|
|
next_brightness=$((actual_brightness + brightness_change))
|
|
|
|
else
|
|
brightness_change=$((
|
|
((-brightness_difference) + remaining_steps / 2) /
|
|
remaining_steps
|
|
))
|
|
|
|
next_brightness=$((actual_brightness - brightness_change))
|
|
fi
|
|
|
|
next_brightness="$(
|
|
clamp_integer "$next_brightness" 1 100
|
|
)"
|
|
|
|
set_physical_brightness "$next_brightness"
|
|
done
|
|
|
|
# Re-read the target so the final level includes any Current Brightness
|
|
# changes received while the ramp was running.
|
|
target_brightness="$(get_current_brightness)"
|
|
set_physical_brightness "$target_brightness"
|
|
set_presence_active "OFF"
|
|
apply_display_mode_base_state
|
|
}
|
|
|
|
schedule_presence_ramp() {
|
|
local timeout_seconds="${1:-}"
|
|
|
|
cancel_presence_ramp
|
|
|
|
if [[ "$(get_presence_active)" != "ON" ]]; then
|
|
return
|
|
fi
|
|
|
|
run_presence_ramp "$timeout_seconds" &
|
|
presence_ramp_pid="$!"
|
|
}
|
|
|
|
handle_touch_activity() {
|
|
if [[ "$(get_display_mode)" == "Screen Lock" ]]; then
|
|
return
|
|
fi
|
|
|
|
activate_presence
|
|
|
|
# Qualifying camera motion holds presence active. Otherwise, a touch starts
|
|
# or restarts the shared Presence Timeout immediately.
|
|
if [[ "$current_motion_state" == "ON" ]] &&
|
|
[[ "$(get_motion_wake)" == "ON" ]]; then
|
|
return
|
|
fi
|
|
|
|
schedule_presence_ramp
|
|
}
|
|
|
|
#:########################################################################################:#
|
|
# Initialisation #
|
|
#:########################################################################################:#
|
|
|
|
current_display_mode="$(get_display_mode)"
|
|
current_brightness="$(get_current_brightness)"
|
|
presence_brightness="$(get_presence_brightness)"
|
|
presence_timeout="$(get_presence_timeout)"
|
|
motion_wake="$(get_motion_wake)"
|
|
|
|
write_state_file "$DISPLAY_MODE_FILE" "$current_display_mode"
|
|
write_state_file "$CURRENT_BRIGHTNESS_FILE" "$current_brightness"
|
|
write_state_file "$PRESENCE_BRIGHTNESS_FILE" "$presence_brightness"
|
|
write_state_file "$PRESENCE_TIMEOUT_FILE" "$presence_timeout"
|
|
write_state_file "$PRESENCE_ACTIVE_FILE" "OFF"
|
|
write_state_file "$MOTION_WAKE_FILE" "$motion_wake"
|
|
|
|
# Enable manual DPMS control without configuring an automatic display-off timeout.
|
|
/usr/bin/xset +dpms
|
|
|
|
# A service restart ends temporary presence and restores the selected base mode.
|
|
screensaver_stop
|
|
set_presence_active "OFF"
|
|
apply_display_mode_base_state
|
|
sync_camera_service
|
|
|
|
# Remove retained commands from the two controls replaced by Display Mode.
|
|
for legacy_topic in \
|
|
"$LEGACY_SCREEN_COMMAND_TOPIC" \
|
|
"$LEGACY_SCREENSAVER_COMMAND_TOPIC"; do
|
|
mqtt_publish \
|
|
-r \
|
|
-n \
|
|
-t "$legacy_topic" \
|
|
>/dev/null 2>&1 || true
|
|
done
|
|
|
|
publish_retained "$DISPLAY_MODE_STATE_TOPIC" "$current_display_mode"
|
|
publish_retained "$CURRENT_BRIGHTNESS_STATE_TOPIC" "$current_brightness"
|
|
publish_retained "$PRESENCE_BRIGHTNESS_STATE_TOPIC" "$presence_brightness"
|
|
publish_retained "$PRESENCE_TIMEOUT_STATE_TOPIC" "$presence_timeout"
|
|
publish_retained "$PRESENCE_ACTIVE_STATE_TOPIC" "OFF"
|
|
publish_retained "$MOTION_WAKE_STATE_TOPIC" "$motion_wake"
|
|
|
|
#:########################################################################################:#
|
|
# State Reporting #
|
|
#:########################################################################################:#
|
|
|
|
(
|
|
previous_display_mode=""
|
|
previous_screen_state=""
|
|
previous_actual_brightness=""
|
|
previous_current_brightness=""
|
|
previous_presence_brightness=""
|
|
previous_presence_active=""
|
|
previous_presence_timeout=""
|
|
previous_motion_wake=""
|
|
previous_volume=""
|
|
previous_mute_state=""
|
|
previous_screensaver_state=""
|
|
last_full_state_publish=0
|
|
|
|
while true; do
|
|
|
|
current_display_mode="$(get_display_mode)"
|
|
current_screen_state="$(get_screen_state)"
|
|
actual_brightness="$(get_physical_brightness)"
|
|
saved_current_brightness="$(get_current_brightness)"
|
|
saved_presence_brightness="$(get_presence_brightness)"
|
|
current_presence_active="$(get_presence_active)"
|
|
saved_presence_timeout="$(get_presence_timeout)"
|
|
current_motion_wake="$(get_motion_wake)"
|
|
|
|
audio_state="$(
|
|
/usr/bin/wpctl get-volume @DEFAULT_AUDIO_SINK@
|
|
)"
|
|
|
|
current_volume="$(
|
|
/usr/bin/awk \
|
|
'{ printf "%.0f\n", $2 * 100 }' \
|
|
<<< "$audio_state"
|
|
)"
|
|
|
|
if /usr/bin/grep -q '\[MUTED\]' <<< "$audio_state"; then
|
|
current_mute_state="ON"
|
|
else
|
|
current_mute_state="OFF"
|
|
fi
|
|
|
|
if screensaver_is_running; then
|
|
current_screensaver_state="ON"
|
|
else
|
|
current_screensaver_state="OFF"
|
|
fi
|
|
|
|
state_report_epoch="$(/usr/bin/date +%s)"
|
|
if (( state_report_epoch - last_full_state_publish >= MQTT_REPUBLISH_INTERVAL_SECONDS )); then
|
|
# Reassert retained state after a broker restart, even when unchanged.
|
|
mqtt_publish -t "$MQTT_HEARTBEAT_TOPIC" -m "$state_report_epoch"
|
|
publish_retained "$DISPLAY_MODE_STATE_TOPIC" "$current_display_mode"
|
|
if [[ "$current_screen_state" == "ON" || "$current_screen_state" == "OFF" ]]; then
|
|
publish_retained "$SCREEN_STATE_TOPIC" "$current_screen_state"
|
|
fi
|
|
if [[ "$actual_brightness" =~ ^[0-9]+$ ]]; then
|
|
publish_retained "$ACTUAL_BRIGHTNESS_STATE_TOPIC" "$actual_brightness"
|
|
fi
|
|
publish_retained "$CURRENT_BRIGHTNESS_STATE_TOPIC" "$saved_current_brightness"
|
|
publish_retained "$PRESENCE_BRIGHTNESS_STATE_TOPIC" "$saved_presence_brightness"
|
|
publish_retained "$PRESENCE_ACTIVE_STATE_TOPIC" "$current_presence_active"
|
|
publish_retained "$PRESENCE_TIMEOUT_STATE_TOPIC" "$saved_presence_timeout"
|
|
publish_retained "$MOTION_WAKE_STATE_TOPIC" "$current_motion_wake"
|
|
if [[ "$current_volume" =~ ^[0-9]+$ ]]; then
|
|
publish_retained "$VOLUME_STATE_TOPIC" "$current_volume"
|
|
fi
|
|
publish_retained "$MUTE_STATE_TOPIC" "$current_mute_state"
|
|
publish_retained "$SCREENSAVER_STATE_TOPIC" "$current_screensaver_state"
|
|
last_full_state_publish="$state_report_epoch"
|
|
fi
|
|
|
|
if [[ "$current_display_mode" != "$previous_display_mode" ]]; then
|
|
publish_retained "$DISPLAY_MODE_STATE_TOPIC" "$current_display_mode"
|
|
previous_display_mode="$current_display_mode"
|
|
fi
|
|
|
|
if [[ "$current_screen_state" == "ON" ||
|
|
"$current_screen_state" == "OFF" ]]; then
|
|
if [[ "$current_screen_state" != "$previous_screen_state" ]]; then
|
|
publish_retained "$SCREEN_STATE_TOPIC" "$current_screen_state"
|
|
previous_screen_state="$current_screen_state"
|
|
fi
|
|
fi
|
|
|
|
if [[ "$actual_brightness" =~ ^[0-9]+$ ]] &&
|
|
[[ "$actual_brightness" != "$previous_actual_brightness" ]]; then
|
|
publish_retained "$ACTUAL_BRIGHTNESS_STATE_TOPIC" "$actual_brightness"
|
|
previous_actual_brightness="$actual_brightness"
|
|
fi
|
|
|
|
if [[ "$saved_current_brightness" != "$previous_current_brightness" ]]; then
|
|
publish_retained "$CURRENT_BRIGHTNESS_STATE_TOPIC" "$saved_current_brightness"
|
|
previous_current_brightness="$saved_current_brightness"
|
|
fi
|
|
|
|
if [[ "$saved_presence_brightness" != "$previous_presence_brightness" ]]; then
|
|
publish_retained "$PRESENCE_BRIGHTNESS_STATE_TOPIC" "$saved_presence_brightness"
|
|
previous_presence_brightness="$saved_presence_brightness"
|
|
fi
|
|
|
|
if [[ "$current_presence_active" != "$previous_presence_active" ]]; then
|
|
publish_retained "$PRESENCE_ACTIVE_STATE_TOPIC" "$current_presence_active"
|
|
previous_presence_active="$current_presence_active"
|
|
fi
|
|
|
|
if [[ "$saved_presence_timeout" != "$previous_presence_timeout" ]]; then
|
|
publish_retained "$PRESENCE_TIMEOUT_STATE_TOPIC" "$saved_presence_timeout"
|
|
previous_presence_timeout="$saved_presence_timeout"
|
|
fi
|
|
|
|
if [[ "$current_motion_wake" != "$previous_motion_wake" ]]; then
|
|
publish_retained "$MOTION_WAKE_STATE_TOPIC" "$current_motion_wake"
|
|
previous_motion_wake="$current_motion_wake"
|
|
fi
|
|
|
|
if [[ "$current_volume" =~ ^[0-9]+$ ]] &&
|
|
[[ "$current_volume" != "$previous_volume" ]]; then
|
|
publish_retained "$VOLUME_STATE_TOPIC" "$current_volume"
|
|
previous_volume="$current_volume"
|
|
fi
|
|
|
|
if [[ "$current_mute_state" != "$previous_mute_state" ]]; then
|
|
publish_retained "$MUTE_STATE_TOPIC" "$current_mute_state"
|
|
previous_mute_state="$current_mute_state"
|
|
fi
|
|
|
|
if [[ "$current_screensaver_state" != "$previous_screensaver_state" ]]; then
|
|
publish_retained "$SCREENSAVER_STATE_TOPIC" "$current_screensaver_state"
|
|
previous_screensaver_state="$current_screensaver_state"
|
|
fi
|
|
|
|
/usr/bin/sleep 2
|
|
done
|
|
) &
|
|
|
|
state_reporter_pid="$!"
|
|
|
|
#:########################################################################################:#
|
|
# Event Listeners #
|
|
#:########################################################################################:#
|
|
|
|
rm -f "$MQTT_EVENT_FIFO" "$TOUCH_EVENT_FIFO"
|
|
mkfifo "$MQTT_EVENT_FIFO" "$TOUCH_EVENT_FIFO"
|
|
chmod 600 "$MQTT_EVENT_FIFO" "$TOUCH_EVENT_FIFO"
|
|
|
|
# Open both FIFOs read/write in the parent so listener startup does not block and
|
|
# a listener failure cannot silently end the main event loop.
|
|
exec 3<>"$MQTT_EVENT_FIFO"
|
|
exec 4<>"$TOUCH_EVENT_FIFO"
|
|
|
|
/usr/bin/stdbuf -oL /usr/bin/mosquitto_sub \
|
|
-t "$DISPLAY_MODE_COMMAND_TOPIC" \
|
|
-t "$CURRENT_BRIGHTNESS_COMMAND_TOPIC" \
|
|
-t "$PRESENCE_BRIGHTNESS_COMMAND_TOPIC" \
|
|
-t "$PRESENCE_TIMEOUT_COMMAND_TOPIC" \
|
|
-t "$PRESENCE_END_COMMAND_TOPIC" \
|
|
-t "$MOTION_STATE_TOPIC" \
|
|
-t "$MOTION_WAKE_COMMAND_TOPIC" \
|
|
-t "$VOLUME_COMMAND_TOPIC" \
|
|
-t "$MUTE_COMMAND_TOPIC" \
|
|
-t "$CHROME_RESTART_COMMAND_TOPIC" \
|
|
-t "$MQTT_HEARTBEAT_TOPIC" \
|
|
-F "%t|%p" \
|
|
> "$MQTT_EVENT_FIFO" &
|
|
|
|
mqtt_listener_pid="$!"
|
|
|
|
(
|
|
/usr/bin/stdbuf \
|
|
-oL \
|
|
/usr/bin/xinput test-xi2 --root |
|
|
/usr/bin/awk '
|
|
/EVENT type 22 \(RawTouchBegin\)/ {
|
|
print "TOUCH"
|
|
fflush()
|
|
}
|
|
'
|
|
) > "$TOUCH_EVENT_FIFO" &
|
|
|
|
touch_listener_pid="$!"
|
|
|
|
cleanup() {
|
|
cancel_presence_ramp
|
|
|
|
for process_id in \
|
|
"$state_reporter_pid" \
|
|
"$mqtt_listener_pid" \
|
|
"$touch_listener_pid"; do
|
|
if /usr/bin/kill -0 "$process_id" >/dev/null 2>&1; then
|
|
/usr/bin/kill "$process_id" >/dev/null 2>&1 || true
|
|
fi
|
|
done
|
|
|
|
exec 3>&- 3<&-
|
|
exec 4>&- 4<&-
|
|
|
|
rm -f "$MQTT_EVENT_FIFO" "$TOUCH_EVENT_FIFO"
|
|
}
|
|
|
|
trap cleanup EXIT INT TERM
|
|
|
|
#:########################################################################################:#
|
|
# MQTT Command Handler #
|
|
#:########################################################################################:#
|
|
|
|
current_motion_state="OFF"
|
|
|
|
handle_mqtt_message() {
|
|
local topic="$1"
|
|
local payload="$2"
|
|
|
|
case "$topic" in
|
|
|
|
"$MQTT_HEARTBEAT_TOPIC")
|
|
if [[ "$payload" =~ ^[0-9]+$ ]]; then
|
|
last_mqtt_heartbeat_epoch="$payload"
|
|
fi
|
|
;;
|
|
|
|
"$DISPLAY_MODE_COMMAND_TOPIC")
|
|
case "${payload^^}" in
|
|
"SCREEN LOCK")
|
|
set_display_mode "Screen Lock"
|
|
;;
|
|
|
|
"SCREEN OFF")
|
|
set_display_mode "Screen Off"
|
|
;;
|
|
|
|
"SCREENSAVER")
|
|
set_display_mode "Screensaver"
|
|
;;
|
|
|
|
"SCREEN ON")
|
|
set_display_mode "Screen On"
|
|
;;
|
|
esac
|
|
;;
|
|
|
|
"$CURRENT_BRIGHTNESS_COMMAND_TOPIC")
|
|
if [[ "$payload" =~ ^[0-9]+$ ]]; then
|
|
payload="$(clamp_integer "$payload" 1 100)"
|
|
write_state_file "$CURRENT_BRIGHTNESS_FILE" "$payload"
|
|
publish_retained "$CURRENT_BRIGHTNESS_STATE_TOPIC" "$payload"
|
|
|
|
if [[ "$(get_presence_active)" != "ON" ]]; then
|
|
case "$(get_display_mode)" in
|
|
"Screensaver"|"Screen On")
|
|
set_physical_brightness "$payload"
|
|
;;
|
|
esac
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
"$PRESENCE_BRIGHTNESS_COMMAND_TOPIC")
|
|
if [[ "$payload" =~ ^[0-9]+$ ]]; then
|
|
payload="$(clamp_integer "$payload" 1 100)"
|
|
write_state_file "$PRESENCE_BRIGHTNESS_FILE" "$payload"
|
|
publish_retained "$PRESENCE_BRIGHTNESS_STATE_TOPIC" "$payload"
|
|
|
|
if [[ "$(get_presence_active)" == "ON" ]]; then
|
|
set_physical_brightness "$payload"
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
"$PRESENCE_TIMEOUT_COMMAND_TOPIC")
|
|
if [[ "$payload" =~ ^[0-9]+$ ]]; then
|
|
payload="$(clamp_integer "$payload" 5 86400)"
|
|
write_state_file "$PRESENCE_TIMEOUT_FILE" "$payload"
|
|
publish_retained "$PRESENCE_TIMEOUT_STATE_TOPIC" "$payload"
|
|
|
|
if [[ "$(get_presence_active)" == "ON" ]]; then
|
|
if [[ "$current_motion_state" != "ON" ]] ||
|
|
[[ "$(get_motion_wake)" != "ON" ]]; then
|
|
schedule_presence_ramp
|
|
fi
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
"$PRESENCE_END_COMMAND_TOPIC")
|
|
if [[ "$payload" =~ ^[0-9]+$ ]] &&
|
|
(( payload >= 1 && payload <= 3600 )); then
|
|
schedule_presence_ramp "$payload"
|
|
fi
|
|
;;
|
|
|
|
"$MOTION_STATE_TOPIC")
|
|
case "${payload^^}" in
|
|
ON)
|
|
current_motion_state="ON"
|
|
|
|
if [[ "$(get_motion_wake)" == "ON" ]] &&
|
|
[[ "$(get_display_mode)" != "Screen Lock" ]]; then
|
|
activate_presence
|
|
fi
|
|
;;
|
|
|
|
OFF)
|
|
current_motion_state="OFF"
|
|
|
|
if [[ "$(get_motion_wake)" == "ON" ]] &&
|
|
[[ "$(get_presence_active)" == "ON" ]]; then
|
|
schedule_presence_ramp
|
|
fi
|
|
;;
|
|
esac
|
|
;;
|
|
|
|
"$MOTION_WAKE_COMMAND_TOPIC")
|
|
case "${payload^^}" in
|
|
ON)
|
|
write_state_file "$MOTION_WAKE_FILE" "ON"
|
|
publish_retained "$MOTION_WAKE_STATE_TOPIC" "ON"
|
|
sync_camera_service
|
|
|
|
if [[ "$current_motion_state" == "ON" ]] &&
|
|
[[ "$(get_display_mode)" != "Screen Lock" ]]; then
|
|
activate_presence
|
|
fi
|
|
;;
|
|
|
|
OFF)
|
|
write_state_file "$MOTION_WAKE_FILE" "OFF"
|
|
publish_retained "$MOTION_WAKE_STATE_TOPIC" "OFF"
|
|
sync_camera_service
|
|
|
|
# Disabling Motion Wake stops current motion from indefinitely holding
|
|
# presence active. Touch-triggered presence still uses the same timeout.
|
|
if [[ "$current_motion_state" == "ON" ]] &&
|
|
[[ "$(get_presence_active)" == "ON" ]]; then
|
|
schedule_presence_ramp
|
|
fi
|
|
;;
|
|
esac
|
|
;;
|
|
|
|
"$VOLUME_COMMAND_TOPIC")
|
|
if [[ "$payload" =~ ^[0-9]+$ ]]; then
|
|
payload="$(clamp_integer "$payload" 0 100)"
|
|
|
|
/usr/bin/wpctl \
|
|
set-volume \
|
|
@DEFAULT_AUDIO_SINK@ \
|
|
"${payload}%"
|
|
fi
|
|
;;
|
|
|
|
"$MUTE_COMMAND_TOPIC")
|
|
case "${payload^^}" in
|
|
ON)
|
|
/usr/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ 1
|
|
;;
|
|
|
|
OFF)
|
|
/usr/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ 0
|
|
;;
|
|
esac
|
|
;;
|
|
|
|
"$CHROME_RESTART_COMMAND_TOPIC")
|
|
case "${payload^^}" in
|
|
PRESS|RESTART)
|
|
restart_chrome_kiosk
|
|
;;
|
|
esac
|
|
;;
|
|
|
|
esac
|
|
}
|
|
|
|
#:########################################################################################:#
|
|
# Main Event Loop #
|
|
#:########################################################################################:#
|
|
|
|
last_mqtt_heartbeat_epoch="$(/usr/bin/date +%s)"
|
|
|
|
while true; do
|
|
|
|
if ! /usr/bin/kill -0 "$mqtt_listener_pid" >/dev/null 2>&1; then
|
|
exit 1
|
|
fi
|
|
|
|
if ! /usr/bin/kill -0 "$touch_listener_pid" >/dev/null 2>&1; then
|
|
exit 1
|
|
fi
|
|
|
|
current_epoch="$(/usr/bin/date +%s)"
|
|
if (( current_epoch - last_mqtt_heartbeat_epoch > MQTT_HEARTBEAT_TIMEOUT_SECONDS )); then
|
|
exit 1
|
|
fi
|
|
|
|
if IFS='|' read -r -t 0.20 -u 3 topic payload; then
|
|
handle_mqtt_message "$topic" "$payload"
|
|
|
|
# Drain any additional MQTT messages already waiting in the FIFO.
|
|
while IFS='|' read -r -t 0.01 -u 3 topic payload; do
|
|
handle_mqtt_message "$topic" "$payload"
|
|
done
|
|
fi
|
|
|
|
while IFS= read -r -t 0.01 -u 4 touch_event; do
|
|
if [[ "$touch_event" == "TOUCH" ]]; then
|
|
handle_touch_activity
|
|
fi
|
|
done
|
|
|
|
# Check the requested display mode after every event-loop cycle.
|
|
# The MQTT read timeout limits this to approximately five checks per second.
|
|
enforce_display_mode
|
|
|
|
done
|