Saturday, May 16, 2015

Home "automation" in Linux with netcat/socat

I realize that this post i lacking in instructions. I might flesh this out at a later date but no guaranties. It's not really that difficult once you figure out my madness ;)

I'm using tasker and udp sender to send udp packets with commands to a listening socat.

So if I send, for example (trailing blank line is intentional):
hue_on
kodi_on

my Hue lights and HTPC will turn on. Tasker is then set up to send these commands when my phone connects to my home network. Simple and effective.

On to the script.


Please note:
The tdtool-lines you see in the case file is a tool for Tellstick which can be used to toggle power switches on and off remotely.

The ssh command requires you to have set up ssh keys to automate ssh logins.

Kodi stuff requires that your Kodi installation is set up to accept calls to its JSON-RPC API.



With that out of the way ...
The script is actually made up of at least three files.

One containing variables and functions (sourced in main script as *.fv):
### Variables
htpc_name=user@computername
kodi_json=http://computername:8080/jsonrpc
kodi_mac=00:00:00:00:00:00
hue_bridge=0.0.0.0
hue_apikey=1234567890
hue_lights=1,2,3,4,5
rc_ip=0.0.0.0


### Functions
doKodi() {
  curl -s --data-binary "{ \"jsonrpc\": \"2.0\", \"method\": $1,\"params\":{$2} , \"id\": \"mybash\" }" \
    -H 'content-type: application/json;' $kodi_json
}

doHue() {
  curl -s -X PUT -H "Content-Type: application/json" -d \
    "{\"ct\":600,\"bri\":254,\"on\":$2,\"transitiontime\":100}" http://$hue_bridge/api/$hue_apikey/lights/$1/state
}

doLX() {
  telnet $rc_ip 2> /dev/null
}

doHTPC() {
  ssh -n -f $htpc_name "sh -c 'nohup $1 &> /dev/null &'"
}

One, or more, containing the case statement (sourced in main script as *.do):
# Changes to this file does NOT require reload of main script.
# Note that line endings must be LF or bash gets confused and cranky.
case "$1" in

  ### DAC
  dac_on)
    echo DAC: on
    tdtool -n 1
    ;;
  dac_off)
    echo DAC: off
    tdtool -f 1
    ;;

  ### FAN
  fan_on)
    echo FAN: on
    tdtool -n 2
    ;;
  fan_off)
    echo FAN: off
    tdtool -f 2
    ;;
  
  ### Reciever
  rc_on)
    echo Reciever: on
    { sleep 0.1; echo "PO"; sleep 6; echo "01FN"; sleep 2; } | doLX
    ;;
  rc_off)
    echo Reciever: off
    { sleep 0.1; echo "25FN"; sleep 2; } | doLX
    ;;
  
  ### KODI
  dlna_on)
    echo Kodi: DLNA on
    doKodi '"Input.Home"' ''
    sleep 1
    doKodi '"Profiles.LoadProfile"' '"profile":"optical"'
    ;;  
  dlna_off)
    echo Kodi: DLNA off
    doKodi '"Input.Home"' ''
    sleep 1
    doKodi '"Profiles.LoadProfile"' '"profile":"Master user"'
    ;;  
  kodi_on)
    echo Kodi: system on
    wol $kodi_mac
    ;;
  kodi_off)
    echo Kodi: system off
    doKodi '"Input.Home"' ''
    sleep 1
    doKodi '"System.Shutdown"' ''
    ;;
  
  ### HUE
  hue_on)
    echo HUE: all on
    for x in $hue_lights; do
      doHue $x true &
      sleep .3
    done
    ;;
  hue_off)
    echo HUE: all off
    for x in $hue_lights; do
      doHue $x false &
      sleep .3
    done
    ;;

  ### HTPC
  spotify_on)
    echo Spotify: Start
    doHTPC "start_spotify.sh"
    ;;
  spotify_off)
    echo Spotify: Stop
    doHTPC "pkill Spotify"
    ;;  
    
esac

And finally the main script tying it all together:
#!/bin/bash

### Figure out who we are
me=$(basename $0)

### Figure out where we are
dir="${BASH_SOURCE%/*}"
if [[ ! -d "$dir" ]]; then dir="$PWD"; fi

### Figure out where to talk
logfile=$dir/$me.log
#touch $logfile
echo > $logfile

### Figure out where to listen
socat_port=2222


### Figure out how to do
# Sourcing functions and variables from .fv files for readability
# Changes to .fv-files requires reload of main script.
. $dir/*.fv


### Go, do.
doFluff() {
  # tell bash to separate on comma
  IFS=','
    
  ### Figure out what to do ...
  # Sourcing cases from .do files for readability.
  # If a incomming call is present in more than one file, 
  # both commands will be executed.
  # Changes to .do-files does NOT require reload of main script.
  for f in $dir/*.do; do 
    . $f
  done

  unset IFS # restore IFS to default
}


### Right round, baby, right round.
loop() {
  socat udp-listen:$socat_port,reuseaddr,fork - | while read do; do
    local d=$(date +"%F %T")
    
    echo -n $d >> $logfile
    echo " :: do: $do" >> $logfile
    
    doFluff $do >> $logfile
    
    echo >> $logfile
  done
}

killtree() {
  local pid=$1 child

  for child in $(pgrep -P $pid); do
    killtree $child
  done
  [ $pid -ne $$ ] && kill -kill $pid
}

### If we are already alive, kill us ...
if [ $(pgrep -c $me) -gt 1 ]; then
  echo "  Killing previous instance(s) of $me"
  #pkill -of $me
  
  pids=($(pgrep "$me"))

  for i in "${pids[@]}"
  do
    #(( $$ != i)) && kill -- -"$i"
    #(( $$ != i)) && kill -- -$(ps -o pgid=$i | grep -o [0-9]*)
    killtree $i &> /dev/null
  done
fi

### then rise again ...
loop &

### and fade away.
echo "  Loop engaged, script backgrounded."
exit 0