Detecting home presence via FritzBox router with HomeyScript

Summary
This explains a method for detecting home presence using a FritzBox router and HomeyScript. On the FritzBox this only needs renaming mobiles with a ‘mobile-’ prefix followed by the user’s name (e.g., ‘mobile-Adam’). It creates Homey variables FritzMobiles, FritzAtHome, and FritzPresence to track connected devices. The script itself only requires setting some constants like fritzUsername and fritzPassword. The script processes the router’s data to determine who is home and updates the variables accordingly.


I wanted a simple way to detect whether someone was at home, and preferably also to know the names of those who were, by checking which phones were logged into my FritzBox router’s Wi-Fi network.

I achieved this with a single Homey script and minimal maintenance on my FritzBox. All I had to do was make a small change to the names of the logged-in phones by searching for them on the FritzBox (via Home Network > Network) and giving them a name starting with ‘mobile-’ followed by the person’s name, for example: ‘mobile-Adam’, ‘mobile-Eva’, ‘mobile-Cain’, etc. This naming allows me to know if and who is home at any given time.

The script uses the TR-064 protocol to read data from the FritzBox. I only use TR-064 statements that work on a normal local HTTP connection with the FritzBox, to avoid the need for complex network adjustments to establish an HTTPS connection between the Homey and the FritzBox (if that’s even possible). To enable this TR-064 connection, you need to check the “Permit access for apps” option on your FritzBox under “Access Settings in the Home Network” via Home Network > Network > Network Settings.

The script uses the Homey variables “FritzMobiles,” “FritzAtHome,” and “FritzPresence.” These variables are automatically created by the script if they don’t already exist.
In the script itself, the constants “HomeyTimeZone,” “fritzUsername,” and “fritzPassword” (lines 15 and following) must be entered correctly for it to work properly. You can use a specific user name on the FritzBox or the default password of the FritzBox itself (in that case, leave ‘fritzUsername’ blank).

On line 23 of the script, you can optionally adjust the phonePatterns constant. This ensures that phones whose names you haven’t changed (yet) with the ‘mobile-’ prefix are still found. A mobile phone will register with a specific make or model name. Adjust this array as needed.

Once the script has run, the following will be displayed in the new variables on your Homey:

  • FritzMobiles: a string containing the mobile phones found on your FritzBox (index|name|IP|Mac||…). This string is generated at least once a day and used to quickly check whether these mobile phones are active on the Wi-Fi network every x minutes. If, during script execution, the IP addresses of the mobile phones are discovered to have changed, this string will be rebuilt. This will be rare.
  • FritzAtHome: a string with the date/time followed by the names of the phones/people present, for example, “2025-12-02 14:57 | Eva | Cain”. If no one is present, this string will contain “Nobody at home”.
  • FritzPresence: a true/false variable that indicates whether the FritzBox has detected presence.

You can use the Homey variables FritzPresence and FritzAtHome to make decisions within Homey flows about lighting, heating, surveillance, etc. The string in FritzMobiles is only useful for this script itself, to avoid having to go through the entire list of Wi-Fi data from the FritzBox every x minutes. This makes the script efficient enough to run every x minutes.

I’d love to hear if this script is useful for others. Of course, I’d also like to know if it works well and if it could be made smarter/better.

// HomeyScript: Query Fritz!Box hosts and detect mobiles
// ============================================================================
// To safely detect mobile devices on the Fritz network rename them 
// on your FritzBos as: mobile-Username
// 1. Create a user on your Fritz!Box with full rights or use the default
//    user (name empty) with the default password. 
// 2. Set Network > Settings > Permit access for apps (TR-064 protocol)
// 3. Homey variabeles used:
//   FritzMobiles (string)  :  List of mobile devices with index,name,IP,MAC
//   FritzAtHome (string)   :  Active mobile devices or 'Nobody at home'
//   FritzPresence (boolean):  true/false if any mobile device is active 
// ============================================================================

// Constants for this script 
const HomeyTimeZone = "Europe/Amsterdam";
const fritzLocalURL = "http://fritz.box";
const fritzPort     = "49000";
const fritzSuffix   = "/upnp/control/hosts";
const fritzUsername = "";
const fritzPassword = "";

// Set names that indicate a phone or mobile device
const phonePatterns = ["android", "iphone", "galaxy", "pixel"];

// Create fritzFullURL and authString
const fritzFullURL = fritzLocalURL + ":" + fritzPort + fritzSuffix;
const authString = "Basic " + Buffer.from(fritzUsername + ":" + fritzPassword).toString("base64");

// Get FritzMobiles + FritzAtHome variables
let mobileDevices = await ReadLogicVar('FritzMobiles');
let fritzAtHome = await ReadLogicVar('FritzAtHome');
if (fritzAtHome === null) {
  // Variable does not exist, create it
  fritzAtHome = '0000-00-00 00:00 | Nobody at home';
  await createVariable('FritzAtHome', 'string', fritzAtHome);
}
let dateNow = getDateNow(HomeyTimeZone);
if ( fritzAtHome.startsWith(dateNow) == false ) {
  // New day, blank mobileDevices to force recheck of all devices
  mobileDevices = '';
}
if (mobileDevices === null) {
  // Variable does not exist, create it
  await createVariable('FritzMobiles', 'string', '');
  mobileDevices = '';
}
if (mobileDevices.trim() == '') {
  mobileDevices = await rebuildFritzMobiles();
  log('Rebuilt FritzMobiles variable...');
}
log('FritzMobiles:', mobileDevices);

// Main loop: check each mobile device for activity
let activeNames = "";
let aDevices = mobileDevices.split("||");
for ( let i=0; i < aDevices.length-1; i++ ) {
  const [devIndex,devName,devIP,devMac] = aDevices[i].split("|");
  log('Checking device:', devIndex, 'Name:', devName, 'IP:', devIP, 'MAC:', devMac);
    let bodyEntry = `<?xml version="1.0"?>
    <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
                s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
      <s:Body>
        <u:GetGenericHostEntry xmlns:u="urn:dslforum-org:service:Hosts:1">
          <NewIndex>${devIndex}</NewIndex>
        </u:GetGenericHostEntry>
      </s:Body>
    </s:Envelope>`;

    let entryResp = await soapCall("GetGenericHostEntry", bodyEntry);
    let active = (entryResp.match(/<NewActive>(.*?)<\/NewActive>/) || [])[1] || "";
    let ip = (entryResp.match(/<NewIPAddress>(.*?)<\/NewIPAddress>/) || [])[1] || "";
    let mac = (entryResp.match(/<NewMACAddress>(.*?)<\/NewMACAddress>/) || [])[1] || "";

    // Check if device listing is up to date
    if ( ip !== devIP || mac !== devMac ) {
      log('Fritz host listing changed, rebuilding FritzMobiles variable');
      mobileDevices = await rebuildFritzMobiles();
      return;
    }

    log('Device', devName.trim(), ' ', 'is active:', active === "1" ? 'YES' : 'NO');
    if ( active === "1" ) {
      activeNames += devName.replace(/^mobile-/, '').replace(/^./, c => c.toUpperCase()) + ' | ';
    }
}

// Update FritzAtHome and FritzPresence variables
let timeNow = getDateTimeNow(HomeyTimeZone);
if ( activeNames.trim() > '' ) {
  let AtHomeString = timeNow + " | " + activeNames.slice(0, -3); // Remove trailing ' | '
  await WriteLogicVar('FritzAtHome', AtHomeString);
  await WriteLogicVar('FritzPresence', true);
} else {
  await WriteLogicVar('FritzAtHome', timeNow + " | " + 'Nobody at home');
  await WriteLogicVar('FritzPresence', false);
}

// Helper: perform SOAP call using fritzFullURL and authString
async function soapCall(action, body) {
  const response = await fetch(fritzFullURL, {
    method: "POST",
    headers: {
      "SOAPACTION": `urn:dslforum-org:service:Hosts:1#${action}`,
      "Content-Type": "text/xml; charset=\"utf-8\"",
      "Authorization": authString
    },
    body
  });
  return await response.text();
}

// Helper: ReadLogicVar
// Gets the value of a Homey Logic variable by name
// Returns null if variable not found, otherwise its value
async function ReadLogicVar(name) {
  let Logic = await Homey.logic.getVariables();
  let idArr = Object.entries(Logic);
  let filtered = idArr.filter(([key, value]) => value.name === name);
  if (filtered.length === 0) { return null;}
  let [[, { id: varID }]] = filtered;
  let varObj = await Homey.logic.getVariable({ id: varID });
  return varObj.value;
}

// Helper: WriteLogicVar
// Sets the value of a Homey Logic variable by name
// If variable does not exist, it is created with the correct type
async function WriteLogicVar(name,value) {
  let Logic = await Homey.logic.getVariables();
  let idArr = Object.entries(Logic);
  let filtered = idArr.filter(([key,value]) => value.name==name);
  if (filtered.length === 0) {
    // Variable not found - create it now
    await createVariable(name, typeof(value), value);
    return true;
  } else {
    let [[,{id:varID}]] = filtered;
    var varArr = await Homey.logic.getVariable({id:varID});
    await Homey.logic.updateVariable({id:varID,variable:{value:value}})
    return true;
  }
}

// Helper: createVariable
// Creates a new Homey Logic variable with the given name, type, and value
async function createVariable(name, type, value) {
  if (!['string', 'number', 'boolean'].includes(type)) {
    throw new Error('Type must be "string", "number", or "boolean".');
  }
  if (type === 'string' && typeof value !== 'string') throw new Error('Value must be a string.');
  if (type === 'number' && typeof value !== 'number') throw new Error('Value must be a number.');
  if (type === 'boolean' && typeof value !== 'boolean') throw new Error('Value must be a boolean.');
  // Check for existing variable by name
  const vars = await Homey.logic.getVariables(); // object keyed by id
  const existing = Object.values(vars).find(v => v.name === name);
  if (existing) {
    throw new Error(`Variable "${name}" already exists (id: ${existing.id}).`);
  }
  const created = await Homey.logic.createVariable({
    variable: { name, type, value }
  });
  console.log(`Created variable "${name}" [type=${type}] with value:`, value);
  return created;
}

// Helper: rebuildFritzMobiles
// Rebuilds the FritzMobiles variable by querying the Fritz!Box
async function rebuildFritzMobiles() {
  // (Re)build FritzMobiles variable
  const bodyCount = `<?xml version="1.0"?>
    <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
                s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
      <s:Body>
        <u:GetHostNumberOfEntries xmlns:u="urn:dslforum-org:service:Hosts:1"/>
      </s:Body>
    </s:Envelope>`;
  const countResp = await soapCall("GetHostNumberOfEntries", bodyCount);
  const match = countResp.match(/<NewHostNumberOfEntries>(\d+)<\/NewHostNumberOfEntries>/);
  const numHosts = match ? parseInt(match[1], 10) : 0;
  let fritzMobiles = "";

  for (let i = 0; i < numHosts; i++) {
    let bodyEntry = `<?xml version="1.0"?>
    <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
                s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
      <s:Body>
        <u:GetGenericHostEntry xmlns:u="urn:dslforum-org:service:Hosts:1">
          <NewIndex>${i}</NewIndex>
        </u:GetGenericHostEntry>
      </s:Body>
    </s:Envelope>`;
    let entryResp = await soapCall("GetGenericHostEntry", bodyEntry);
    let lowerName = ((entryResp.match(/<NewHostName>(.*?)<\/NewHostName>/) || [])[1] || "").toLowerCase();
    if ( lowerName.startsWith("mobile-") || phonePatterns.some(pattern => lowerName.includes(pattern)) ) {
      let ip = (entryResp.match(/<NewIPAddress>(.*?)<\/NewIPAddress>/) || [])[1] || "";
      let mac = (entryResp.match(/<NewMACAddress>(.*?)<\/NewMACAddress>/) || [])[1] || "";
      fritzMobiles += i.toString() + "|" + lowerName + "|" + ip + "|" + mac + "||";
    }
  }
  await WriteLogicVar("FritzMobiles", fritzMobiles); 
  return fritzMobiles; 
}

// Helper: getDateTimeNow
// Returns current date and time in 'YYYY-MM-DD HH:MM' format for given timezone
function getDateTimeNow(myTimezone) {
  let now = new Date();
  let parts = new Intl.DateTimeFormat('en-GB', {
    timeZone: myTimezone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    hour12: false
  }).formatToParts(now);
  // Assemble strict YYYY-MM-DD HH:MM
  return `${parts.find(p => p.type === 'year').value}-` +
         `${parts.find(p => p.type === 'month').value}-` +
         `${parts.find(p => p.type === 'day').value} ` +
         `${parts.find(p => p.type === 'hour').value}:` +
         `${parts.find(p => p.type === 'minute').value}`;
}

// Helper: getDateNow
// Returns current date in 'YYYY-MM-DD' format for given timezone
function getDateNow(myTimezone) {
  let now = new Date();
  let parts = new Intl.DateTimeFormat('en-GB', {
    timeZone: myTimezone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    hour12: false
  }).formatToParts(now);
  // Assemble strict YYYY-MM-DD
  return `${parts.find(p => p.type === 'year').value}-` +
         `${parts.find(p => p.type === 'month').value}-` +
         `${parts.find(p => p.type === 'day').value}`;
}

Use in a flow
Create a presence flow that starts with a when timer every x minutes and a then Homeyscript containing this Homey script code.