#!/bin/bash HASSCTL_VERSION="1.2.1" debug() { (( $DEBUG )) && echo "$(basename $0): DEBUG:" "$@" } info() { echo "$(basename $0):" "$@" } warn() { echo "$(basename $0):" "$@" >&2 } die() { local rc=1 local message= [[ $# -gt 1 ]] && rc=$1 && shift [[ ! -z "$@" ]] && warn "$@" exit $rc } usage() { echo "usage: $(basename $0) {help|version|start|stop|restart|kill|log|error|debug|zwave|config|update-hassctl|update-hass|backup|service}" 2>&1 exit 1 } version() { echo "$HASSCTL_VERSION" exit 0 } # Show usage if no arguments given if [[ $# -lt 1 ]]; then usage fi # Report unimplemented command no_command() { warn "command not yet implemented: $1" usage } # Absolute paths to executables chmod=/bin/chmod cp=/bin/cp rm=/bin/rm curl=/usr/bin/curl systemctl=/bin/systemctl journalctl=/bin/journalctl tail=/usr/bin/tail sed=/bin/sed # Get the current username me="$(whoami)" # Portable version of realpath realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" } # HASSCTL_CONF can be overridden as an env var HASSCTL_CONF="${HASSCTL_CONF:-/etc/hassctl.conf}" HASSCTL_CONF="$(realpath "$HASSCTL_CONF")" # Read configuration file read_config() { # Set the repository branch (can be overridden in config) HASSCTL_BRANCH="${HASSCTL_BRANCH:-master}" # Try to download config if it doesn't exist [[ -f "$HASSCTL_CONF" ]] || download_config # Load config if it exists if [[ -f "$HASSCTL_CONF" ]]; then source "$HASSCTL_CONF" else warn "$HASSCTL_CONF: config file missing, using defaults" fi # Get this scripts full path HASSCTL_EXEC="$(realpath $0)" # Confirm virtualenv path exists or is empty VIRTUAL_ENV="${VIRTUAL_ENV-/srv/homeassistant}" [[ -z "$VIRTUAL_ENV" ]] && warn "continuing without VIRTUAL_ENV" [[ -z "$VIRTUAL_ENV" ]] || [[ -d "$VIRTUAL_ENV" ]] \ || die "$VIRTUAL_ENV: no such directory -- please update VIRTUAL_ENV in $HASSCTL_CONF" # Confirm Home Assistant config path exists HASS_CONFIG="${HASS_CONFIG:-/home/homeassistant/.homeassistant}" [[ -z "$HASS_CONFIG" ]] || [[ -d "$HASS_CONFIG" ]] \ || die "$HASS_CONFIG: no such directory -- please update HASS_CONFIG in $HASSCTL_CONF" # Verify user exists HASS_USER="${HASS_USER:-homeassistant}" id -u "$HASS_USER" >/dev/null || die "$HASS_USER: no such user -- please update HASS_USER in $HASSCTL_CONF" HASSCTL_BRANCH="${HASSCTL_BRANCH:-master}" HASS_SERVICE="${HASS_SERVICE:-home*assistant*}" OZW_LOG="${OZW_LOG:-$HASS_CONFIG/OZW_Log.txt}" if [[ ! -z "$VIRTUAL_ENV" ]]; then PIP_EXEC="${PIP_EXEC:-$VIRTUAL_ENV/bin/pip3}" HASS_EXEC="${HASS_EXEC:-$VIRTUAL_ENV/bin/hass}" else PIP_EXEC="${PIP_EXEC:-$(which pip3)}" HASS_EXEC="${HASS_EXEC:-$(which hass)}" fi [[ -f "$PIP_EXEC" ]] || die "$PIP_EXEC: no such file -- please update PIP_EXEC in $HASSCTL_CONF" [[ -x "$PIP_EXEC" ]] || die "$PIP_EXEC: file not executable -- please update PIP_EXEC in $HASSCTL_CONF" [[ -f "$HASS_EXEC" ]] || die "$HASS_EXEC: no such file -- please update HASS_EXEC in $HASSCTL_CONF" [[ -x "$HASS_EXEC" ]] || die "$HASS_EXEC: file not executable -- please update HASS_EXEC in $HASSCTL_CONF" return 0 } download_config() { [[ ! -f "$HASSCTL_CONF" ]] || return BRANCH="${1:-$HASSCTL_BRANCH}" BRANCH="${BRANCH:-master}" CONFIG_URL="https://raw.githubusercontent.com/dale3h/hassctl/$BRANCH/hassctl.conf" debug "downloading default config from $CONFIG_URL" HTTP_CODE="$(sudo $curl --write-out %{http_code} --silent --show-error --output "$HASSCTL_CONF" "$CONFIG_URL")" if [[ $? -ne 0 ]] || [[ "$HTTP_CODE" != "200" ]]; then # Clean up failed config file [[ -f "$HASSCTL_CONF" ]] && sudo $rm "$HASSCTL_CONF" debug "could not install default config" else debug "installed default config to $HASSCTL_CONF" fi } # Execute commands for a specific user maybe_exec_as() { local RUN_AS="$1" shift if [[ "$RUN_AS" != "$me" ]] && id -u "$RUN_AS" >/dev/null; then debug "sudo -u \"$RUN_AS\" -H \"$@\"" sudo -u "$RUN_AS" -H "$@" else debug "\"$@\"" "$@" fi } # Update hassctl script update_hassctl() { read_config BRANCH="${1:-$HASSCTL_BRANCH}" BRANCH="${BRANCH:-master}" BRANCH_URL="https://raw.githubusercontent.com/dale3h/hassctl/$BRANCH" HASSCTL_URL="$BRANCH_URL/hassctl" CONFIG_URL="$BRANCH_URL/hassctl.conf" debug "installing from $HASSCTL_URL" CP_OPTS="--remove-destination" sudo $curl -s -S -o "$HASSCTL_EXEC.update" "$HASSCTL_URL" \ && sudo $chmod +x "$HASSCTL_EXEC.update" \ && sudo $cp $CP_OPTS "$HASSCTL_EXEC.update" "$HASSCTL_EXEC" \ && sudo $rm "$HASSCTL_EXEC.update" if [[ $? -ne 0 ]]; then # Clean up if something went wrong sudo $rm "$HASSCTL_EXEC.update" die "utility update failed" fi info "utility has been updated to the latest $BRANCH version" exit 0 } # Update homeassistant package update_hass() { read_config local VERSION="$1" if [[ ! -z $VERSION ]]; then VERSION="==$VERSION" fi maybe_exec_as "$HASS_USER" "$PIP_EXEC" install --upgrade homeassistant$VERSION exit $? } # Run hass check_config script check_config() { read_config maybe_exec_as "$HASS_USER" "$HASS_EXEC" --script check_config -c "$HASS_CONFIG" "$@" } # Show Home Assistant raw log hass_raw_log() { read_config debug "clear && sudo $journalctl \"$@\" --no-pager -o cat -fu \"$HASS_SERVICE\"" clear && sudo $journalctl "$@" --no-pager -o cat -fu "$HASS_SERVICE" } # Show Home Assistant error log hass_error_log() { read_config debug "clear && sudo $journalctl \"$@\" --no-pager -o cat -fu \"$HASS_SERVICE\" | grep -iP '^ .*$|^(?!.*is_failed=)(?!.*service=remove_failed_node)(?!.*service=replace_failed_node).*(error|errno|warning|exception|failure|failed|warn|except|fail|traceback).*'" clear && sudo $journalctl "$@" --no-pager -o cat -fu "$HASS_SERVICE" | grep -iP '^ .*$|^(?!.*is_failed=)(?!.*service=remove_failed_node)(?!.*service=replace_failed_node).*(error|errno|warning|exception|failure|failed|warn|except|fail|traceback).*' } # Show Home Assistant debug log hass_debug_log() { read_config debug "clear && sudo $journalctl \"$@\" --no-pager -o cat -fu \"$HASS_SERVICE\" | grep -iP '[0-9]{2}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} DEBUG'" clear && sudo $journalctl "$@" --no-pager -o cat -fu "$HASS_SERVICE" | grep -iP '[0-9]{2}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} DEBUG' } # Kill Home Assistant service hass_kill() { read_config debug "sudo $systemctl kill --signal=SIGKILL \"$HASS_SERVICE\"" sudo $systemctl kill --signal=SIGKILL "$HASS_SERVICE" } # Kill Home Assistant using alternative method hass_kill_alt() { debug "ps aux | grep python3 | grep hass | grep -vE grep | awk '{print $2}' | xargs kill -9" ps aux | grep python3 | grep hass | grep -vE grep | awk '{print $2}' | xargs kill -9 } # Pass commands to Home Assistant service systemctl_cmd() { read_config debug "sudo $systemctl \"$@\" \"$HASS_SERVICE\"" sudo $systemctl "$@" "$HASS_SERVICE" } # Show Z-Wave log zwave_log() { read_config [[ -f "$OZW_LOG" ]] || die "$OZW_LOG: no such file -- please update OZW_LOG in $HASSCTL_CONF" [[ -r "$OZW_LOG" ]] || die "$OZW_LOG: file not readable -- please update OZW_LOG in $HASSCTL_CONF" debug "clear && $tail \"$@\" -f \"$OZW_LOG\"" clear && $tail "$@" -f "$OZW_LOG" | $sed \ -e 's/\(.*Always,.*\)/\o033[35m\1\o033[39m/' \ -e 's/\(.*Detail,.*\)/\o033[36m\1\o033[39m/' \ -e 's/\(.*Info,.*\)/\o033[32m\1\o033[39m/' \ -e 's/\(.*Warning,.*\)/\o033[33m\1\o033[39m/' \ -e 's/\(.*Error,.*\)/\o033[31m\1\o033[39m/' } # Process arguments case "$1" in help) usage ;; log) hass_raw_log "${@:2}" ;; error) hass_error_log "${@:2}" ;; debug) hass_debug_log "${@:2}" ;; zwave) zwave_log "${@:2}" ;; config) check_config "${@:2}" ;; update-hassctl) update_hassctl "${@:2}" ;; update-hass) update_hass "${@:2}" ;; backup) no_command "$@" ;; service) no_command "$@" ;; kill) hass_kill ;; kill-alt) hass_kill_alt ;; list-units|list-sockets|list-timers|start|stop|\ reload|restart|reload-or-restart|is-active|is-enabled|\ is-failed|status|show|cat|enable|disable) systemctl_cmd "$@" ;; version) version ;; *) usage ;; esac