Generating a bill of materials from an ekahau file using python

Last year I had the honor of receiving training from François Vergès - the ekahau ECSE Advanced. Besides stuff around workflow, CAD and reporting there was a topic around using python to read and manipulate ekahau .esx files.

The premise this builds on and that people may or may not know:

ekahau’s .esx files are just .zip files, full of JSON.

Go ahead, just rename one of your projects to .zip, and unzip it.

So I thought to myself that I will use this new knowledge (and my python skills, which are not that great but enough for a few scripts) and make an easy tool: generate a BoM out of an ekahau predictive design. ekahau does not yet have such a function in usable form; it is possible to do this with a report template and some VBA macro of course, but the goal here was to use python.

As naïve as I went into this, I will explain what I did, and why this is not as straight forward as I thought:

What the script would do was fairly easy laid out:

  • unzip the file given as parameter
  • open the JSON file that holds the AP information
  • sum up a list of distinct AP model names
  • print the list with the respective quantities

This works well, using APs with internal antennas. The output looked like this:

Generating BOM from lab.esx
20x Cisco C9120i
3x Cisco C9120e + AIR-ANT2524DW-R
3x Cisco C9130e + C-ANT9103

I thought “well… ekahau just places the antenna with a ‘+’ after the name, so if I split the string at the ‘+’ I can also do the model and number of antennas, right?”.

Well yes, but actually, no.

Let’s look at some APs.

3 different Cisco APs

Here, depending on the AP model, we would have to know if we need three, four or six antennas! Without having a list of AP models and number of ports, this is difficult to count. And to complicate things further - what if we use this antenna on the AP?

Patch antenna with 4 plugs

Depending on the antenna model, I might need on an AP four or just one.

So what I decided was to give up on seperating the antennas out - that means that the output will have AP model name + antenna summed up, for every antenna type, and I would have to do the full sums by hand. Not ideal, but difficult to do automatically.

This was the moment that I stumbled over another problem. Look at this output - a design with internal antenna APs only:

Generating BOM from school.esx
2x Cisco AP2802i 5GHz + 5GHz
1x Cisco AP2802i 2.4GHz + 5GHz
4x Cisco Catalyst 9166 with Dual 5 GHz
2x Cisco Catalyst 9166

There are only two types of APs here - but ekahau puts the mode you planned with - dual 5 or 2.4+5 - into the model name. But not even consistently! This is another headache - as there are not that many, I decided to remove these from the name to give a cleaner BoM back. But as APs like that are coming out more and more (2.4 + 5 + 6 in various modes) and ekahau might name these differently, this is probably subject to needed updates.

NOTE: My script was programmed and tested against files from ekahau AI pro version 11.1. Older or newer versions might break the script.

So, let’s look at the code and what it does:

def extractAPs(fname):
    with zipfile.ZipFile(fname) as esx:
        with'accessPoints.json') as apfile:
            aps = json.load(apfile)
            aplist = {}
            for ap in aps['accessPoints']:
                currentModel = ap['vendor'] + ' ' + ap['model']
                currentModel = normaliseName(str(currentModel))
                if currentModel not in aplist: aplist[currentModel] = 1
                else: aplist[currentModel]+=1
    return aplist


This function opens and unzips the given file, and loads the “accessPoints.json” into a JSON Object. It then iterates over every entry, and stores AP-vendor and AP-model as string into a dictionary - if it is new, with value 1, otherwise it will count up. The function normaliseName is called here, and it looks like this:

def normaliseName(apname):
    unwanted = {
        " with Dual 5 GHz",
        " 2.4GHz + 5GHz",
        " 5GHz + 5GHz"
    for unwantedstring in unwanted:
        apname = apname.replace(unwantedstring, "")
    return apname


It removes the 2.4/5 modes in the name, so the same AP with different mode is not counted as different model. I thought about doing a clever regex here, but this keeps it well readable and easy to expand for future AP modes.

We have a simple output function, that sorts our AP list by count and prints it to the terminal:

def printAPlist(aplist):
    aplist_sorted = sorted(aplist.items(), key = lambda x:x[1], reverse = True)
    for apname, apcount in aplist_sorted:
        print(f"{apcount}x {apname}")


And our main to glue it all together:

if __name__ == "__main__":
    parser = argparse.ArgumentParser(prog = '', description='This script will extract the AP list out of the given ESX file. Note: Antennas will be attached to the APs as in the AP name in the ESX file.')
    parser.add_argument('file', help='ESX file name')
    args = parser.parse_args()

    print("Generating BOM from " +args.file)
    print("-" * 50)

    aplist = extractAPs(args.file)


The script expects the esx file name as argument:

stefan@FERMI % python3 SCP.esx   
Generating BOM from SCP.esx
26x Cisco C9120e + AIR-ANT2524DW-R
19x Cisco C9120i

If you changed the antenna in your ekahau file and the name does not match the antenna now, François made a script to automatically update the model name

You can download my complete script here - it is licensed under “unlicense”, so you can do whatever you want. :-)

If you have any thoughts on how to solve the antenna-issue, please hit me up.