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 dumpwill 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 }
ReplacingXXX.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_IDXand make sure that
WLANAP_HOSTScontains each of the host names for your WiFi access points.