Afwezigheidsdetectie: Eureka!

Na wat finetunen werkt alles zoals in eerste instantie bedoeld: er wordt een reeks IP-adressen gescand en indien online wordt het MAC-adres vergeleken met bekende apparaten. Is er een bekend apparaat online, dan zal de thermostaat (geroote Toon) zijn programma vervolgen. Zijn er geen apparaten online, dan wordt de thermostaat ingesteld op programma ‘Weg’ en gaan de opgegeven lampen (Philips Hue) automatisch uit! Naast lampen kan door middel van slimme stekkers natuurlijk ook sluipverbruik (standby apparaten) voorkomen worden.

Toekomstwensen? Mogelijkheid tot opnieuw inschakelen van bijvoorbeeld nachtlampjes op de kinderkamers. Zijn we weg, dan blijft alles uit. Komen we weer thuis, dan zou er bijvoorbeeld ingeschakeld kunnen worden als de zon onder is. De voorbereidingen voor zoninformatie is al in de laatste versie van mijn script verwerkt.

Het script

Je dient variabelen ‘toonIP’, ‘hueIP’, ‘locationLat’, ‘locationLon’, ‘ipRange’, ‘ipStart’, ‘ipEnd’, ‘hueAPIKey’ en ‘path’ in te stellen.

#!/bin/bash
# Script door Dennis Bor

# IP van Toon
toonIP="192.168.0.X"

# IP van Hue
hueIP="192.168.0.X"

# jouw locatie
locationLat="XX.XXXXXX"
locationLon="X.XXXXXX"

# te scannen IP range (opgegeven IP .1 - .255
ipRange="192.168.0."
ipStart=130
ipEnd=140

# API-key van Hue
hueAPIKey="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

# pad naar het script
path="/home/dennis/home_automation/"

# pad naar arp
arpPath="/usr/sbin/"

# toon informatie
date

# zonsopkomst en ondergang ophalen aan de hand van de opgegeven longitude en latitude
sunrise=`curl -s "https://api.sunrise-sunset.org/json?lat=$locationLat&lng=$locationLon"  | jq -r '.results.sunrise'`
sunset=`curl -s "https://api.sunrise-sunset.org/json?lat=$locationLat&lng=$locationLon"  | jq -r '.results.sunset'`

# tijdverschil ten opzichte van UTC ophalen
timeDifferenceUTC=`date +%z`

# substring vanaf index 2 (+0100 wordt bijvoorbeeld 100; 1 uur)
timeOffset=${timeDifferenceUTC:2}

# offset delen door 100 voor het aantal hele uren
timeOffset="$(($timeOffset / 100))"

# timestamps genereren voor zonsopkomst, zonsondergang en huidige tijd, inclusief UTC offset
# voor gegevens van api.sunrise-sunset.org
sunriseTimestamp=`date -d "$sunrise + $timeOffset hours" +%s`
sunsetTimestamp=`date -d "$sunset + $timeOffset hours" +%s`
currentTimestamp=`date +%s`

# huidige tijd tussen zonsopkomst en ondergang
if [ $sunriseTimestamp -lt $currentTimestamp ] && [ $sunsetTimestamp -gt $currentTimestamp ]; then
	
	# ja
	
	# toon informatie
	echo "De zon is momenteel op."
	
	# stel de boolean in op true
	sunUp=true

# huidige tijd voor zonsopkomst of na zonsondergang	
else

	# ja
	
	# toon informatie
	echo "De zon is momenteel onder."
	
	# stel de boolean in op true
	sunUp=false
fi

# Array met mac adressen
# Als geen van deze apparaten gevonden wordt in het lokale
# netwerk, dan zal er geschakeld worden.
deviceArray=()

# itereer door bestand 'devices' waarin
# per regel een mac-adres van een apparaat opgegeven
# kan worden
while read line; do

	# geen lege regel?
	if [[ $line != "" ]]; then

		# voeg het apparaat toe aan de array
		deviceArray+=("$line")
	fi

done < "$path"devices

# Boolean waarin opgeslagen wordt of er tenminste een
# van de gespecificeerde apparaten online is. Standaard false
deviceFound=false

# Toon informatie
echo "Zoeken naar apparaten, dit duurt een paar minuutjes..."

# itereer door het opgegeven ip bereik
for iterator in $(seq $ipStart $ipEnd)
do

	# apparaat gevonden (boolean true)?
	if [[ $deviceFound == true ]]; then
	
		# ja, escape deze for-loop
		break
	fi

	# toon informatie
	printf "Scannen van $ipRange$iterator"

	# itereer van 1 t/m 50 (maximaal aantal pogingen). Het grote aantal iteraties bleek
	# nodig omdat de enige open WiFi poort op de iphone tijdens standy (62078) maar
	# op bepaalde momenten reageert. 99% van de keren is dit binnen 25 pogingen, maar
	# enkele uitschieters tussen de 30 en 40 kwamen voor. 50 is dus echt met een 
	# ingebouwd veilig marge
	for subIterator in {1..50}}
	do

		# apparaat nog (steeds) niet gevonden?
		if [[ $deviceFound == false ]]; then

			# toon informatie (puntje, soort progress indicator)
			printf "."

			# scan poort 62078 met een delay van 250ms op het huidige ip adres
			nmapResult=`nmap --scan-delay 250ms -p 62078 "$ipRange$iterator"`

			# komt de tekst '1 host up' voor in de uitvoer van nmap? dat
			# betekent dat het apparaat gevonden is
			if [[ $nmapResult == *"1 host up"* ]]; then

				# itereer door alle apparaten in de array
				for macIterator in "${deviceArray[@]}"
				do
					# komt het huidige mac adres voor in de uitvoer van nmap?
					if [[ $nmapResult == *"$macIterator"* ]]; then

						# ja
						
						# toon informatie (eerste echo zorgt voor een linebreak na de progress
						# indicator puntjes)
						echo ""
						printf "Apparaat met adres $macIterator gevonden"
						
						# stel de boolean in op true (apparaat gevonden!)
						deviceFound=true
						
						# verlaat de for-loop
						break
					fi
				done
			fi
		fi
	done
	
	# apparaat nog (steeds) niet gevonden
	if [[ $deviceFound == false ]]; then

		# nee
		
		# toon progress indicator
		printf "."
	
		# als laatste poging pingen we, waarop Android apparaten wel altijd reageren.
		# lukt de ping ook niet, dan kunnen we er wel van uit gaan dat het apparaat echot
		# niet binnen het bereik van het WiFi-netwerk is.
		pingResult=`ping -c 4 "$ipRange$iterator"`

		# komt de tekst '100% packet loss' niet voor in de uitvoer van ping?
		if [[ $pingResult != *"100% packet loss"* ]]; then

			# nee! dat betekent dat het apparaat tenmiste eenmaal gereageerd
			# heeft op een ping
			
			# arp informatie ophalen van het apparaat, zodat we het mac adres	
			# kunnen vergelijken met onze bekende apparaten
			arpResult=`"$arpPath"arp -a "$ipRange$iterator"`

			# itereer door alle mac adressen in de array
			for macIterator in "${deviceArray[@]}"
            do
				# komt het huidige mac adres voor in de uitvoer van arp?	
				if [[ ${arpResult^^} == *"$macIterator"* ]]; then

					# yep! bekend apparaat gevonden!
			
					# toon informatie (eerste echo is een linebreak na de progress
					# indicator
					echo ""
					printf "Apparaat met adres $macIterator gevonden"
					
					# stel de boolean in op true (apparaat gevonden)
					deviceFound=true
					
					# verlaat de for loop
					break
				fi
			done
		fi
	fi	
	
	# toon informatie (linebreak)
	echo ""

done

# Geen apparaten online?
if [[ $deviceFound == false ]]; then

	# nee

	# toon informatie
	echo "Geen apparaten online, actie ondernemen..."

	# schakel programma Toon in op Weg
	# gebruik curl, parse de uitvoer als JSON en sla de waarde van 'result' op
	# de waarde wordt bij de tweede call overschreven; als de laatste faalt,
	# dan moet de eerste ook gefaald zijn en andersom
	result=`curl -s "http://$toonIP/happ_thermstat?action=changeSchemeState&state=0"`
	result=`curl -s "http://$toonIP/happ_thermstat?action=changeSchemeState&state=2&temperatureState=3" | jq -r '.result'`

	# waarde van result 'ok'?
	# dat betekent dat Toon het gekozen programma geacvtiveerd heeft
	if [[ $result == "ok" ]]; then

		# toon informatie
		echo "Toon ingesteld op 'Weg'"

	# waarde van result ongelijk aan 'ok'? dit kan alles zijn, van een ander resultaat tot
	# verbindingsproblemen, etc.
	else

		# toon informatie
		echo "Kan toon niet instellen op 'Weg'"
	fi

	# toon informatie
	echo "Hue instellen..."

	# haal alle lampen op
	result=`curl -s "http://$hueIP/api/$hueAPIKey/lights"`

	# array keys uit de json array van Hue halen. helaas gebruikt Hue geen
	# array met indices, maar met keys. van lampen die verwijderd worden
	# vervalt het id.
	arrayKeys=`echo "$result" | jq -r 'keys | .[]'`
	
	# nieuwe array om alle keys in op te slaan
	arrayIDs=()

	# itereer door alle array keys
	for key in ${arrayKeys//\\n/ }
	do
		# voeg de key toe aan de array
		arrayIDs+=("$key")

	done

	# inhoud van bestand 'lights' opslaan in de variabele. Bij alle lampen van Hue
	# wordt gecontroleerd of ze in deze lijst voorkomen, voordat ze uitgeschakeld worden.
	# hierdoor blijft het mogelijk om bepaalde verlichting te laten branden (bijvoorbeeld
	# tuinverlichting)
	lightsToSwitch=`cat "$path"lights`

	# itereer door alle IDs
	for lightIterator in "${arrayIDs[@]}"
	do

		# unieke id van de lamp ophalen. in het betand 'lights' kun je een lijst aanleggen
		# van unieke ids van lampen die uitgeschakeld moeten worden
		currentUniqueId=`echo "$result" | jq --arg value $lightIterator -r '.[$value].uniqueid'`
		
		# komt het huidige unieke id voor in de lijst met ids?
		if [[ $lightsToSwitch == *"$currentUniqueId"* ]]; then

			# ja
			
			# lamp naam ophalen zodat deze getoond kan worden
			currentName=`echo "$result" | jq --arg value $lightIterator -r  '.[$value].name'`
			
			# toon informatie
			echo "Uitschakelen: $currentName ($currentUniqueId)"
			
			# schakel de lamp uit
			toonResult=`curl -s -X PUT -H "Content-Type: application/json" -d '{"on":false}' "http://$hueIP/api/$hueAPIKey/lights/$lightIterator/state"`

		fi
	done

# Tenminste een apparaat online?
else

	# ja

	 # toon informatie
     echo "Een of meer apparaten online, thermostaat herstellen..."

	# hervat het programma van toon
	# gebruik curl, parse de uitvoer als JSON en sla de waarde van 'result' op
	result=`curl -s "http://$toonIP/happ_thermstat?action=changeSchemeState&state=1" | jq -r '.result'`

	# waarde van result 'ok'?
        # dat betekent dat Toon het programma hersteld heeft
        if [[ $result == "ok" ]]; then

                # toon informatie
                echo "Toon hervat programma"

        # waarde van result ongelijk aan 'ok'? dit kan alles zijn, van een ander resultaat tot
        # verbindingsproblemen, etc.
        else

                # toon informatie
                echo "Kan het programma op Toon niet hervatten"
        fi

	# toon informatie
	echo "Klaar..."

fi

Benodigde bestanden

Bestandsnaam ‘devices’

# lijst met MAC adressen van bekende apparaten. 
# zet elk apparaat op een aparte regel 
XX:XX:XX:XX:XX:XX
XX:XX:XX:XX:XX:XX

Bestandsnaam ‘lights’

# lijst met uit te schakelen Hue lampen bij afwezigheid
# zet elke lamp (of slimme stekker) op een aparte regel
# gebruik de 'uniqueid' parameter
XX:XX:XX:XX:XX:XX-XX:XX
XX:XX:XX:XX:XX:XX-XX:XX