I've recently started using Domoticz to control my various home automation systems. One of the bits of information about the house that I wanted to gather was the presence of mobile phones on the wireless network. Both my partner and I always have our phones with us, so if they are present on the network then it's a good bet that we're at home. This allows constructing domoticz rules based on who's in the house.
There are many examples of scripts on the internet that show how to do this in Domoticz using either ping, arp-scan or a combination of the two. However the success of them seems to be varied at best. The main issue appears to be that mobile devices attempt to conserve power by not being 'active' (whatever that means) on the wireless network. I tried to make my scripts more accurate by adding more and more layers of complexity and complex rules. Eventually I decided the active scanning approach was doomed.
Luckily I run OpenWRT on both of my wireless access points. Logging into the OpenWRT boxes and running
/usr/sbin/iw dev wlan0 station dump
will give a dump of information about the various devices. The next question was how to get this information from the OpenWRT system to the system running Domoticz. I decided to use xinitd on OpenWRT, which allows binding the output of an arbitrary command to a TCP port. It's not at all secure, but I'm happy to take the risk as this isn't going to be facing the public internet. To configure OpenWRT do the following:
- Install xinitd:
opkg update && opkg install xinitd
- Edit
/etc/services
to contain the following line:
wlanap 8216/tcp
- Create
/etc/xinetd.d/wlanap
containing:
service wlanap
{
socket_type = stream
protocol = tcp
wait = no
user = root
group = root
server = /usr/sbin/dump_ap
disable = no
only_from = 192.168.XXX.XXX
}
Replacing XXX.XXX
with the IP of the machine running Domoticz.
- Create
/usr/sbin/dump_ap
containing:
#!/bin/sh
/usr/sbin/iw dev wlan0 station dump
Note: If your router has multiple wireless interfaces (e.g. 2.4GHz and 5GHz) then you should repeat the last line in the script for the other interfaces, such as wlan1.
- Mark it as executable:
chmod a+x /usr/sbin/dump_ap
- Restart xinetd on OpenWRT
- Repeat for each OpenWRT box
It should be noted that the help for the iw tool does give the following warning:
Do NOT screenscrape this tool, we don't consider its output stable
. So every time I update OpenWRT I am going to be at risk of things breaking, but that will just add to the excitement of applying updates!
So the last piece in the puzzle was to write a script to periodically query my wireless access points and toggle virtual switches in Domoticz as appropriate. I created a couple of virtual switches in Domoticz for each of the devices of interest and then created a cronjob to run the following Python script periodically:
#!/usr/bin/env python
# Detects ARP addresses
import argparse
import json
import logging
import os
import re
import socket
import subprocess
import sys
import urllib2
log = logging.getLogger()
DEFAULT_ARP_SCAN = "/usr/bin/arp-scan"
DEFAULT_CONFIG_LOCATION = os.path.join(os.environ['HOME'], ".detect-arp", "config.json")
DOMOTICZ_URL = "http://localhost"
ARP_TO_IDX = {
"12:34:56:78:9A:BC": 31, # Dave's Phone
"DE:AD:BE:EF:00:12": 37, # Bob's Phone
}
WLANAP_HOSTS = ["AccessPoint1", "AccessPoint2"]
WLANAP_PORT = 8216
ARP_LINE_RE = re.compile(r"^Station ([0-9a-f:]{17}) \(on .+\)$")
def getArps():
foundArp = set()
for host in WLANAP_HOSTS:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, WLANAP_PORT))
data = ""
while True:
dataFrag = s.recv(4096)
if len(dataFrag) == 0:
break
data += dataFrag
for line in data.split("\n"):
m = ARP_LINE_RE.search(line)
if m is None:
continue
arpAddr = m.group(1)
log.debug("Found ARP %s", arpAddr)
foundArp.add(arpAddr)
return foundArp
def scan(args):
log.debug("Starting scan")
foundArp = getArps()
# Update the switch state as needed
log.debug("Toggling switches")
for arpAddr, idx in ARP_TO_IDX.iteritems():
# Get the current state
stateUrl = "%s/json.htm?type=devices&rid=%d" % (
DOMOTICZ_URL, idx)
resp = urllib2.urlopen(stateUrl)
jsonStr = resp.read()
stateData = json.loads(jsonStr)
if stateData.get("status", None) != "OK":
log.err("Reply from '%s': %s", stateUrl, jsonStr)
continue
currentState = stateData["result"][0]["Status"]
if arpAddr in foundArp:
switchCmd = "On"
else:
switchCmd = "Off"
if switchCmd == currentState:
log.debug("Not switching %d due to unchanged state (%s)", idx, switchCmd)
continue
log.debug("Switching %s (%d) to %s", arpAddr, idx, switchCmd)
switchUrl = "%s/json.htm?type=command¶m=switchlight&idx=%d&switchcmd=%s" % (
DOMOTICZ_URL, idx, switchCmd)
resp = urllib2.urlopen(switchUrl)
data = resp.read()
switchData = json.loads(data)
if switchData.get("status", None) != "OK":
log.err("Reply from '%s': %s", switchUrl, switchData)
continue
def install(args):
log.debug("Starting install")
def uninstall(args):
log.debug("Starting uninstall")
def getParser():
parser = argparse.ArgumentParser(description="Detects devices on the network based on ARP")
parser.add_argument("--arpscan", dest="arpScanPath", default=DEFAULT_ARP_SCAN,
help="Location of arpscan, default %(default)s")
parser.add_argument("--config", dest="configPath", default=DEFAULT_CONFIG_LOCATION,
help="Location of the config file, default %(default)s")
parser.add_argument("-v", "--verbose", dest="verbose", action="store_true",
help="Enable verbose output")
subParser = parser.add_subparsers(title="action", dest="action")
scanParser = subParser.add_parser("scan", help="scan for devices")
installParser = subParser.add_parser("install", help="install script into crontab")
uninstallParser = subParser.add_parser("uninstall", help="uninstall script from crontab")
return parser
def main():
parser = getParser()
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)
commandFn = globals()[args.action]
commandFn(args)
if __name__ == "__main__":
main()
Note: You'll want to customise the MAC address to switch indexes defined in
ARP_TO_IDX
and make sure that
WLANAP_HOSTS
contains each of the host names for your WiFi access points.