Differences
This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
| homeautomation [2024/06/19 14:46] – MQTT Integration misterb | homeautomation [2026/02/20 15:59] (current) – Advanced MQTT example lbrpdx | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| ====== Home automation ====== | ====== Home automation ====== | ||
| - | Welcome to the awesome world of home automation! | + | Since Batocera is [[wake_on_lan|Wake on LAN (WoL)]] enabled by default |
| ===== Integrate Batocera with MQTT ===== | ===== Integrate Batocera with MQTT ===== | ||
| Line 10: | Line 10: | ||
| ==== Requirements ==== | ==== Requirements ==== | ||
| - | * A separate MQTT broker (server) to publish events to. Batocera only contains tools to read & write to an MQTT broker, but not the broker itself. | + | * A separate MQTT broker (server) to publish events to. Batocera only contains tools to read & write to an MQTT broker, but not the broker itself. Solutions like Home Assistant have fairly easy packages MQTT brokers available. |
| ==== Use a Custom User Service to Forward Events ==== | ==== Use a Custom User Service to Forward Events ==== | ||
| + | |||
| The following user service listens for the [[wiki: | The following user service listens for the [[wiki: | ||
| Line 34: | Line 35: | ||
| * '' | * '' | ||
| - | <code bash mqtt-events> | + | <code bash mqtt_events> |
| MQTT_HOST=your-mqtt-server.lan | MQTT_HOST=your-mqtt-server.lan | ||
| MQTT_PORT=1883 | MQTT_PORT=1883 | ||
| Line 160: | Line 161: | ||
| fi | fi | ||
| </ | </ | ||
| + | |||
| + | ==== More Advanced Script ==== | ||
| + | |||
| + | The following script is an extension of the previous script, that provides more data. Once enabled and integrated with a Home Assistant MQTT broker, you can transparently get cards like this: | ||
| + | {{ :: | ||
| + | |||
| + | To install this one: | ||
| + | - copy this script into ''/ | ||
| + | - edit configuration variables at the top of the file with the credentials your MQTT broker | ||
| + | - make it executable with '' | ||
| + | - run ''/ | ||
| + | - then, either go to EmulationStation and enable the service, or do it from the command line with '' | ||
| + | |||
| + | <code bash mqtt_events> | ||
| + | #!/bin/bash | ||
| + | # Batocera MQTT bridge (boxart + marquee + CPU-only telemetry pinned to HWMON:temp) | ||
| + | # Initially from kit on Batocera Discord | ||
| + | # - CPU temp: | ||
| + | # * Median-of-samples + sticky + last-good + hard range guards. | ||
| + | # * Cached temp*_input path to avoid flicks and rescan churn. | ||
| + | # - Home Assistant MQTT discovery: | ||
| + | # * Image: Box Art + Marquee (no YAML). | ||
| + | # * Sensors: cpu_temp (+source/ | ||
| + | # - Debug topic: batocera/< | ||
| + | |||
| + | ###################################### | ||
| + | # Broker / topics | ||
| + | ###################################### | ||
| + | MQTT_HOST="" | ||
| + | MQTT_PORT="" | ||
| + | MQTT_USER="" | ||
| + | MQTT_PASSWORD="" | ||
| + | |||
| + | DISCOVERY_PREFIX=" | ||
| + | HOSTNAME=" | ||
| + | MQTT_BASE_TOPIC=" | ||
| + | |||
| + | # Telemetry | ||
| + | TELEMETRY_ENABLE=1 | ||
| + | TELEMETRY_INTERVAL=5 | ||
| + | |||
| + | # Media behavior | ||
| + | RETAIN_MEDIA_BYTES=1 | ||
| + | PUBLISH_IMAGE_B64=1 | ||
| + | |||
| + | # Paths / cache | ||
| + | PIDFILE="/ | ||
| + | TMP_DIR="/ | ||
| + | mkdir -p " | ||
| + | CORETEMP_PATH_CACHE="/ | ||
| + | CPU_TEMP_STATE_FILE="/ | ||
| + | |||
| + | # jq detection (optional) | ||
| + | HAS_JQ=0 | ||
| + | command -v jq >/ | ||
| + | |||
| + | ###################################### | ||
| + | # ES service | ||
| + | ###################################### | ||
| + | SERVICE_NAME=$(basename " | ||
| + | ES_EVENTS=(game-start game-end game-selected system-selected theme-changed settings-changed controls-changed config-changed quit reboot shutdown sleep wake achievements screensaver-start screensaver-stop) | ||
| + | |||
| + | ###################################### | ||
| + | # Helpers | ||
| + | ###################################### | ||
| + | _ts() { date -u +" | ||
| + | _local_ts() { date +" | ||
| + | |||
| + | _json_array_from_args() { | ||
| + | if [ " | ||
| + | local arr=" | ||
| + | for arg in " | ||
| + | arr+=$(jq -Rn --arg a " | ||
| + | arr+="," | ||
| + | done | ||
| + | echo " | ||
| + | else | ||
| + | local arr=" | ||
| + | for arg in " | ||
| + | echo " | ||
| + | fi | ||
| + | } | ||
| + | |||
| + | # Retained publishers | ||
| + | _pub() { mosquitto_pub -h " | ||
| + | _pub_str() { _pub " | ||
| + | |||
| + | # Non-retained | ||
| + | _pub_event() { mosquitto_pub -h " | ||
| + | |||
| + | # File publisher (bytes) | ||
| + | _pub_file() { | ||
| + | [ -f " | ||
| + | local args=(-h " | ||
| + | [ " | ||
| + | mosquitto_pub " | ||
| + | } | ||
| + | |||
| + | _dbg() { _pub_event " | ||
| + | |||
| + | ###################################### | ||
| + | # Startup cleanup: purge any old GPU retained topics/ | ||
| + | # Unused, kept as an example for clearing out entities | ||
| + | ###################################### | ||
| + | _clear_legacy_gpu_topics() { | ||
| + | local ID=" | ||
| + | # HA discovery (GPU) | ||
| + | for t in \ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | mosquitto_pub -h " | ||
| + | done | ||
| + | # State topics (GPU) | ||
| + | for t in gpu_temp gpu_temp_source gpu_temp_status; | ||
| + | mosquitto_pub -h " | ||
| + | done | ||
| + | } | ||
| + | |||
| + | ###################################### | ||
| + | # Converters: dwebp | magick | convert | ffmpeg | ||
| + | ###################################### | ||
| + | _convert_webp_to_png() { | ||
| + | local in=" | ||
| + | out=" | ||
| + | if command -v dwebp >/ | ||
| + | echo " | ||
| + | return 0 | ||
| + | }; fi | ||
| + | if command -v magick >/ | ||
| + | echo " | ||
| + | return 0 | ||
| + | }; fi | ||
| + | if command -v convert >/ | ||
| + | echo " | ||
| + | return 0 | ||
| + | }; fi | ||
| + | if command -v ffmpeg >/ | ||
| + | echo " | ||
| + | return 0 | ||
| + | }; fi | ||
| + | return 1 | ||
| + | } | ||
| + | _maybe_convert_to_png() { | ||
| + | local in=" | ||
| + | case " | ||
| + | webp) | ||
| + | local out | ||
| + | out=" | ||
| + | _dbg " | ||
| + | echo " | ||
| + | return 0 | ||
| + | } | ||
| + | _dbg " | ||
| + | echo " | ||
| + | ;; | ||
| + | *) echo " | ||
| + | esac | ||
| + | } | ||
| + | |||
| + | ###################################### | ||
| + | # Media discovery (box-art + marquee) | ||
| + | ###################################### | ||
| + | _find_game_image() { | ||
| + | local sys=" | ||
| + | base=" | ||
| + | stem=" | ||
| + | name=" | ||
| + | # 1) roms/< | ||
| + | for ext in webp png jpg jpeg JPG JPEG PNG; do | ||
| + | for candidate in \ | ||
| + | "/ | ||
| + | "/ | ||
| + | "/ | ||
| + | "/ | ||
| + | done | ||
| + | # 2) downloaded_images/< | ||
| + | for root in "/ | ||
| + | [ -d " | ||
| + | for ext in webp png jpg jpeg JPG JPEG PNG; do | ||
| + | for candidate in \ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | done | ||
| + | done | ||
| + | # 3) < | ||
| + | local gl="/ | ||
| + | if [ -f " | ||
| + | p=" | ||
| + | if [ -n " | ||
| + | line=" | ||
| + | [ -n " | ||
| + | case " | ||
| + | [ -f " | ||
| + | fi | ||
| + | fi | ||
| + | return 1 | ||
| + | } | ||
| + | _find_marquee_image() { | ||
| + | local sys=" | ||
| + | base=" | ||
| + | stem=" | ||
| + | name=" | ||
| + | # 1) roms/< | ||
| + | for ext in webp png jpg jpeg JPG JPEG PNG; do | ||
| + | for candidate in \ | ||
| + | "/ | ||
| + | "/ | ||
| + | "/ | ||
| + | "/ | ||
| + | "/ | ||
| + | "/ | ||
| + | done | ||
| + | # 2) downloaded_images/< | ||
| + | for root in "/ | ||
| + | [ -d " | ||
| + | for ext in webp png jpg jpeg JPG JPEG PNG; do | ||
| + | for candidate in \ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | done | ||
| + | done | ||
| + | # 3) < | ||
| + | local gl="/ | ||
| + | if [ -f " | ||
| + | p=" | ||
| + | if [ -n " | ||
| + | for tag in marquee thumbnail; do | ||
| + | line=" | ||
| + | [ -n " | ||
| + | case " | ||
| + | [ -f " | ||
| + | done | ||
| + | fi | ||
| + | fi | ||
| + | return 1 | ||
| + | } | ||
| + | |||
| + | _publish_file_variants() { | ||
| + | local kind=" | ||
| + | [ -f " | ||
| + | _dbg " | ||
| + | return 1 | ||
| + | } | ||
| + | ext=" | ||
| + | if [ " | ||
| + | local conv | ||
| + | conv=" | ||
| + | fp=" | ||
| + | fi | ||
| + | _pub_event " | ||
| + | _pub_file " | ||
| + | if [ " | ||
| + | base64 " | ||
| + | -t " | ||
| + | fi | ||
| + | } | ||
| + | |||
| + | _publish_media_for_selection() { | ||
| + | local sys=" | ||
| + | fp=" | ||
| + | fp=" | ||
| + | } | ||
| + | |||
| + | ###################################### | ||
| + | # Core publish | ||
| + | ###################################### | ||
| + | publish() { | ||
| + | local subtopic=" | ||
| + | shift | ||
| + | local data ts | ||
| + | data=$(_json_array_from_args " | ||
| + | ts=$(_ts) | ||
| + | _pub " | ||
| + | case " | ||
| + | emulationstation/ | ||
| + | local rom_path rom_name | ||
| + | if [ " | ||
| + | rom_path=$(jq -r '.[0] // empty' <<<" | ||
| + | rom_name=$(jq -r '.[1] // .[0]' <<<" | ||
| + | else | ||
| + | rom_path=" | ||
| + | rom_name=" | ||
| + | fi | ||
| + | [ -n " | ||
| + | _pub_str " | ||
| + | ;; | ||
| + | emulationstation/ | ||
| + | _pub_str " | ||
| + | _pub_str " | ||
| + | ;; | ||
| + | emulationstation/ | ||
| + | local sys | ||
| + | if [ " | ||
| + | [ -n " | ||
| + | ;; | ||
| + | emulationstation/ | ||
| + | local sys romp rom_name ts_local | ||
| + | ts_local=" | ||
| + | if [ " | ||
| + | sys=$(jq -r '.[0] // empty' <<<" | ||
| + | romp=$(jq -r '.[1] // empty' <<<" | ||
| + | rom_name=$(jq -r '.[2] // .[1] // .[0]' <<<" | ||
| + | else | ||
| + | sys=" | ||
| + | romp=" | ||
| + | rom_name=" | ||
| + | fi | ||
| + | [ -n " | ||
| + | [ -n " | ||
| + | if [ " | ||
| + | _pub_event " | ||
| + | else | ||
| + | _pub_event " | ||
| + | fi | ||
| + | _pub_event " | ||
| + | _pub_event " | ||
| + | _publish_media_for_selection " | ||
| + | ;; | ||
| + | game/ | ||
| + | local sys | ||
| + | if [ " | ||
| + | [ -n " | ||
| + | _pub_str " | ||
| + | ;; | ||
| + | esac | ||
| + | } | ||
| + | |||
| + | ###################################### | ||
| + | # HA MQTT Discovery (images + CPU telemetry only) | ||
| + | ###################################### | ||
| + | ha_discover() { | ||
| + | local ID=" | ||
| + | |||
| + | # CPU/system sensors | ||
| + | declare -A SENS=([cpu_temp]=" | ||
| + | for key in cpu_temp cpu_load ram_used disk_used uptime; do | ||
| + | _pub " | ||
| + | "{ | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | }" | ||
| + | done | ||
| + | |||
| + | # Diagnostics for CPU temp | ||
| + | for diag in cpu_temp_source cpu_temp_status; | ||
| + | _pub " | ||
| + | "{ | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | }" | ||
| + | done | ||
| + | |||
| + | # Now Playing / System / Playing | ||
| + | _pub " | ||
| + | "{ | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | }" | ||
| + | _pub " | ||
| + | "{ | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | }" | ||
| + | _pub " | ||
| + | "{ | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | }" | ||
| + | |||
| + | # Buttons | ||
| + | for cmd in reboot shutdown; do | ||
| + | _pub " | ||
| + | "{ | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | }" | ||
| + | done | ||
| + | |||
| + | # Images | ||
| + | _pub " | ||
| + | "{ | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | }" | ||
| + | _pub " | ||
| + | "{ | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | \" | ||
| + | }" | ||
| + | } | ||
| + | |||
| + | ###################################### | ||
| + | # CPU temperature | ||
| + | ###################################### | ||
| + | CPU_TEMP_SAMPLE_COUNT=3 | ||
| + | CPU_TEMP_SAMPLE_DELAY_MS=150 | ||
| + | CPU_TEMP_STICKY_SECONDS=600 | ||
| + | CPU_TEMP_MIN_VALID=-40 | ||
| + | CPU_TEMP_MAX_VALID=125 | ||
| + | |||
| + | _norm_celsius() { | ||
| + | local v=" | ||
| + | [ -z " | ||
| + | v=" | ||
| + | case " | ||
| + | if [ " | ||
| + | } | ||
| + | _is_valid_temp() { | ||
| + | local c=" | ||
| + | [ -z " | ||
| + | [ " | ||
| + | [ " | ||
| + | return 0 | ||
| + | } | ||
| + | _sleep_ms() { usleep "$(($1 * 1000))" | ||
| + | |||
| + | _resolve_coretemp_path() { | ||
| + | # Use cached path if still valid | ||
| + | if [ -f " | ||
| + | local p | ||
| + | p=" | ||
| + | [ -n " | ||
| + | fi | ||
| + | # Scan for ' | ||
| + | for h in / | ||
| + | [ -f " | ||
| + | local name | ||
| + | name=" | ||
| + | case " | ||
| + | local first="" | ||
| + | for n in " | ||
| + | [ -f " | ||
| + | [ -z " | ||
| + | local lblfile=" | ||
| + | [ -f " | ||
| + | if echo " | ||
| + | echo " | ||
| + | return 0 | ||
| + | fi | ||
| + | done | ||
| + | if [ -n " | ||
| + | echo " | ||
| + | return 0 | ||
| + | fi | ||
| + | ;; | ||
| + | esac | ||
| + | done | ||
| + | return 1 | ||
| + | } | ||
| + | |||
| + | _read_coretemp_once() { | ||
| + | local p | ||
| + | p=" | ||
| + | local raw | ||
| + | raw=" | ||
| + | local c | ||
| + | c=" | ||
| + | echo " | ||
| + | return 0 | ||
| + | } | ||
| + | |||
| + | _sample_coretemp_median() { | ||
| + | local n=" | ||
| + | local vals=() i=0 v | ||
| + | while [ $i -lt " | ||
| + | v=" | ||
| + | _sleep_ms " | ||
| + | i=$((i + 1)) | ||
| + | continue | ||
| + | } | ||
| + | vals+=(" | ||
| + | i=$((i + 1)) | ||
| + | [ $i -lt " | ||
| + | done | ||
| + | [ " | ||
| + | IFS=$' | ||
| + | unset IFS | ||
| + | local mid=$(((${# | ||
| + | echo " | ||
| + | return 0 | ||
| + | } | ||
| + | |||
| + | _publish_cpu_temp() { | ||
| + | local now c last_c last_src last_ts sticky_src sticky_until | ||
| + | now=" | ||
| + | [ -f " | ||
| + | |||
| + | c=" | ||
| + | |||
| + | if _is_valid_temp " | ||
| + | _pub_str " | ||
| + | _pub_str " | ||
| + | _pub_str " | ||
| + | local until=$((now + CPU_TEMP_STICKY_SECONDS)) | ||
| + | echo " | ||
| + | else | ||
| + | if _is_valid_temp " | ||
| + | _pub_str " | ||
| + | _pub_str " | ||
| + | _pub_str " | ||
| + | else | ||
| + | _dbg " | ||
| + | fi | ||
| + | fi | ||
| + | } | ||
| + | |||
| + | ###################################### | ||
| + | # Command listener | ||
| + | ###################################### | ||
| + | listen_commands() { | ||
| + | mosquitto_sub -h " | ||
| + | -t " | ||
| + | case " | ||
| + | reboot) reboot ;; | ||
| + | shutdown) shutdown -h now ;; | ||
| + | esac | ||
| + | done & | ||
| + | CMD_PID=$! | ||
| + | } | ||
| + | |||
| + | ###################################### | ||
| + | # Telemetry loop | ||
| + | ###################################### | ||
| + | telemetry_loop() { | ||
| + | [ " | ||
| + | while true; do | ||
| + | _publish_cpu_temp | ||
| + | |||
| + | # CPU load (1min) | ||
| + | [ -r / | ||
| + | |||
| + | # RAM used (MB) | ||
| + | if [ -r / | ||
| + | local mt ma | ||
| + | mt=" | ||
| + | ma=" | ||
| + | [ -n " | ||
| + | fi | ||
| + | |||
| + | # Disk used (% of /userdata) | ||
| + | if df -P /userdata >/ | ||
| + | local du | ||
| + | du=" | ||
| + | [ -n " | ||
| + | fi | ||
| + | |||
| + | # Uptime (s) | ||
| + | [ -r / | ||
| + | |||
| + | sleep " | ||
| + | done & | ||
| + | TEL_PID=$! | ||
| + | } | ||
| + | |||
| + | ###################################### | ||
| + | # Install / Uninstall ES hooks | ||
| + | ###################################### | ||
| + | install() { | ||
| + | mkdir -p / | ||
| + | for event in " | ||
| + | local script_dir="/ | ||
| + | mkdir -p " | ||
| + | local script=" | ||
| + | if [ ! -f " | ||
| + | cat >" | ||
| + | #!/bin/bash | ||
| + | event_name=" | ||
| + | / | ||
| + | EOF | ||
| + | sed -i " | ||
| + | chmod +x " | ||
| + | fi | ||
| + | done | ||
| + | |||
| + | mkdir -p / | ||
| + | local game_script="/ | ||
| + | if [ ! -f " | ||
| + | cat >" | ||
| + | #!/bin/bash | ||
| + | # Args: gameStart|gameStop system emulator core rompath | ||
| + | / | ||
| + | EOF | ||
| + | sed -i " | ||
| + | chmod +x " | ||
| + | fi | ||
| + | } | ||
| + | uninstall() { | ||
| + | for event in " | ||
| + | local script_dir="/ | ||
| + | local script=" | ||
| + | [ -f " | ||
| + | [ -d " | ||
| + | done | ||
| + | rm -f "/ | ||
| + | } | ||
| + | |||
| + | ###################################### | ||
| + | # Process Control | ||
| + | ###################################### | ||
| + | getMPID() { | ||
| + | X=$(cat " | ||
| + | test -z " | ||
| + | if test -e "/ | ||
| + | echo " | ||
| + | return 0 | ||
| + | fi | ||
| + | echo "" | ||
| + | return 1 | ||
| + | } | ||
| + | |||
| + | ###################################### | ||
| + | # Start / Stop | ||
| + | ###################################### | ||
| + | start() { | ||
| + | P=$(getMPID) | ||
| + | if test -n " | ||
| + | kill " | ||
| + | fi | ||
| + | echo $$ >" | ||
| + | install | ||
| + | _pub_str " | ||
| + | publish " | ||
| + | ha_discover | ||
| + | listen_commands | ||
| + | telemetry_loop | ||
| + | } | ||
| + | stop() { | ||
| + | P=$(getMPID) | ||
| + | if test -n " | ||
| + | kill " | ||
| + | fi | ||
| + | /bin/rm " | ||
| + | publish " | ||
| + | [ -n " | ||
| + | [ -n " | ||
| + | _pub_str " | ||
| + | } | ||
| + | |||
| + | ###################################### | ||
| + | # Entrypoint | ||
| + | ###################################### | ||
| + | if [ $# -eq 0 ]; then | ||
| + | echo " | ||
| + | exit 1 | ||
| + | fi | ||
| + | case " | ||
| + | publish) | ||
| + | shift | ||
| + | publish " | ||
| + | ;; | ||
| + | start) start ;; | ||
| + | stop) stop ;; | ||
| + | install) install ;; | ||
| + | uninstall) uninstall ;; | ||
| + | *) | ||
| + | echo " | ||
| + | exit 1 | ||
| + | ;; | ||
| + | esac | ||
| + | </ | ||
| + | |||
| ===== Control Batocera with a Logitech Harmony remote via Home Assistant (HA) ===== | ===== Control Batocera with a Logitech Harmony remote via Home Assistant (HA) ===== | ||
| Line 339: | Line 1041: | ||
| That's it! Oh boy, how cool is that? You are now able to power on/off your Batocera system with just one click from your Logitech Harmony remote control. Feel free to integrate the Roku 4 device into your Logitech Harmony activities so you can power on/off all of your devices (e.g. TV, AV receiver, amplifier, HTPC, ...) with just one click with a single activity. | That's it! Oh boy, how cool is that? You are now able to power on/off your Batocera system with just one click from your Logitech Harmony remote control. Feel free to integrate the Roku 4 device into your Logitech Harmony activities so you can power on/off all of your devices (e.g. TV, AV receiver, amplifier, HTPC, ...) with just one click with a single activity. | ||
| + | |||
| + | === Advanced: Boot automatically into OSes on multiboot systems === | ||
| + | |||
| + | In case you got Batocera set up as a [[dual_boot_ubuntu_batocera.linux|dual- or multiboot]] system, you may be interested on how to set up PXE-boot (network boot) on your Batocera PC so you can boot automatically into one of the according installed OSes on your PC, based on Home Assistant automations. | ||
| + | To accomplish this, please refer to [[https:// | ||
- homeautomation.1718808381.txt.gz
- Last modified: 24 months ago
- by misterb