Reading a switch

Every read below is available on SyncSwitch and AsyncSwitch alike, and each takes an optional backend=. They return frozen dataclasses from netgear_switch.models, identical whichever protocol produced them.

The operations

Method

Returns

Notes

get_ports

list[PortStatus]

Port number, ifName, admin state, link state, speed in Mbit/s, and the operator-set description (ifAlias) where the backend can read one. speed_config carries the port’s configured speed/duplex, which is a different question from the negotiated speed_mbps — see Configured speed is not negotiated speed. The CLI and the GoAhead web UI both report it; SNMP and NSDP leave it None, because ifSpeed and NSDP’s port record are both the negotiated rate.

get_stats

list[PortStats]

RX/TX bytes, packets and errors. Any counter a backend cannot read is None, never zero.

get_vlans

list[VLANInfo]

VLAN id, name, and three port sets: member_ports, tagged_ports, untagged_ports.

get_pvids

list[tuple[int, int]]

(port, vlan) pairs.

get_lldp

list[LLDPNeighbor]

Local port plus the neighbour’s system name, chassis id, port id and port description.

get_macs

list[MacEntry]

The forwarding table: MAC, port, VLAN.

get_poe

list[PoEStatus]

Admin state, detection state (PoEDetect) and delivered milliwatts.

get_sensors

list[Sensor]

Fan, PSU and temperature readings with their units.

get_mgmt_ip

MgmtIpConfig

Address, netmask, gateway, DHCP-or-static mode, and the switch’s base MAC.

get_hostname

str

The switch’s configured host name.

get_users

list[SwitchUser]

Local login accounts. access_mode is the switch’s own wording for the account, kept verbatim because it differs by face, not just by firmware: asked over the CLI the same admin account reads Privilege-15 on one image and Read/Write on another, while both switches’ web UIs call it Super User. The normalised privileged flag beside it agrees across all three.

get_services

list[ServiceStatus]

Whether http, https, telnet and ssh are enabled, and on which port where the firmware reports one.

get_syslog

SyslogConfig

Whether remote logging is on, the local source port, and the configured collectors.

Not every model serves every one of these on every backend. Support matrix has the complete grid, and netgear_switch.capabilities.support answers the question in code.

Absent data is None, never a substitute

A field a backend genuinely cannot read stays None:

  • PortStatus.description is None where the backend has no ifAlias equivalent — not "". NSDP is not such a backend: tag 0xB000 carries the operator’s label and was cross-checked byte-for-byte against three real GS110EMX units’ own “Port Description” column, so it fills this field like SNMP’s ifAlias does. PortStatus.name is the opposite way round there: NSDP reports a port NUMBER and no interface identifier, so name is None over NSDP while SNMP gives 1/0/1 and the web UI g1.

  • PoEStatus.power_mw is None on a switch with no vendor power column (the GS728TPP serves everything from standard MIBs and has no such column) — not 0.

  • MgmtIpConfig.base_mac is None where the model’s web UI has no page carrying it.

An operation that cannot be answered at all raises; it does not return an empty list. An empty list means the switch really reported nothing.

Two special reads

identify asks the switch what it actually is, over SNMP, ignoring the model the facade was constructed with. That is the point: use it to confirm or discover a model when you only have a host and a community.

detected = switch.identify()
if detected.matched and detected.key != switch.model.key:
    raise SystemExit(f"{switch.host} is really a {detected.key}")
detected = await switch.identify()
if detected.matched and detected.key != switch.model.key:
    raise SystemExit(f"{switch.host} is really a {detected.key}")

Matching prefers sysObjectID — an unambiguous product identifier that can distinguish SKUs whose sysDescr text is identical — and falls back to sysDescr. key is None when neither matched; it is never a guess.

nsdp_device returns the complete raw NSDP device record for a Plus switch: firmware, serial, DHCP mode, VLAN engine, port mirroring, IGMP snooping and the unconverted per-port fields. It is NSDP-only by nature and bypasses backend dispatch.

Snapshots

snapshot runs every read over one backend and returns a SwitchData:

snap = switch.snapshot()
print(snap.model, snap.host, len(snap.ports), len(snap.vlans))
snap = await switch.snapshot()
print(snap.model, snap.host, len(snap.ports), len(snap.vlans))

Fields the backend cannot serve degrade to () or None. They are not re-read over another protocol — so a snapshot describes what one protocol really reports, rather than a blend of several. To compare protocols, take two snapshots:

from netgear_switch import Backend

over_snmp = switch.snapshot(backend=Backend.SNMP)
over_web = switch.snapshot(backend=Backend.HTTP)
assert over_snmp.vlans == over_web.vlans
from netgear_switch import Backend

over_snmp = await switch.snapshot(backend=Backend.SNMP)
over_web = await switch.snapshot(backend=Backend.HTTP)
assert over_snmp.vlans == over_web.vlans

The cross-backend equivalence tests do exactly this; see tests/test_cross_backend_equivalence.py.

Note

snapshot is the one place where an unsupported operation is swallowed rather than raised, because its job is “collect what this protocol can tell me”. Every individual get_* call still raises. If you need to know why a field is empty, call the operation directly, or ask netgear_switch.capabilities.support.

Reading a fleet

Here the two APIs stop being interchangeable: the asynchronous facade reads every switch at once, the synchronous one reads them in turn.

from netgear_switch import SyncSwitch, get_model

def sweep(hosts: list[str]) -> None:
    for host in hosts:
        switch = SyncSwitch(
            get_model("gsm7252ps"), host=host, snmp_community="public"
        )
        snap = switch.snapshot()
        print(snap.host, len(snap.ports))
import asyncio

from netgear_switch import AsyncSwitch, get_model

async def sweep(hosts: list[str]) -> None:
    switches = [
        AsyncSwitch(get_model("gsm7252ps"), host=h, snmp_community="public")
        for h in hosts
    ]
    try:
        for snap in await asyncio.gather(*(s.snapshot() for s in switches)):
            print(snap.host, len(snap.ports))
    finally:
        await asyncio.gather(*(s.aclose() for s in switches))

AsyncSwitch requires the [async] extra for SNMP (pysnmp) and [http] for web-UI backends.

Handling refusals

from netgear_switch import Backend, UnsupportedCapabilityError, support

# Ask first — no network traffic:
if support(switch.model, Backend.SNMP, "get_sensors").supported:
    print(switch.get_sensors(backend=Backend.SNMP))

# Or act and handle the refusal, which names the backend that refused:
try:
    switch.get_poe()
except UnsupportedCapabilityError as exc:
    print(exc)
from netgear_switch import Backend, UnsupportedCapabilityError, support

# support() is a plain function — the question needs no switch and no await.
if support(switch.model, Backend.SNMP, "get_sensors").supported:
    print(await switch.get_sensors(backend=Backend.SNMP))

# Or act and handle the refusal, which names the backend that refused:
try:
    await switch.get_poe()
except UnsupportedCapabilityError as exc:
    print(exc)

Capturing what a switch says

ngsw capture writes a complete JSON record of a switch’s readable state. This project’s fixtures and mock seeds are produced with it:

ngsw --switch core capture core-2026-07-31.json

See How the fake is built for how a capture becomes a mock.