Reverse-engineered JSON-RPC API for the Bell Giga Hub (Sagemcom Fast 5689E): SHA512 auth, TR-181 data model, hidden WiFi tuning, and NAT port-forwarding, with a working Python client.
Find a file
Alan 4b38d2bb69 Bell Giga Hub (Sagemcom Fast 5689E) reverse-engineered API reference + Python client
Reverse-engineered JSON-RPC API documentation and a working Python client for the
Bell Giga Hub residential gateway (firmware 4KU_21.3.1.170_prod):

- Two-phase SHA512 challenge-response authentication
- TR-181 data model, XPath addressing, and error-code reference
- WiFi radio tuning that the web UI hides (beamforming, Green AP, band steering, channel)
- NAT port-forwarding: the non-obvious addChild-then-setValue-by-position recipe
- bellapi.py CLI + list/set port-forward scripts (password from $BELL_PW; real config gitignored)
2026-07-21 16:11:10 -04:00
.gitignore Bell Giga Hub (Sagemcom Fast 5689E) reverse-engineered API reference + Python client 2026-07-21 16:11:10 -04:00
apply_optimisations.sh Bell Giga Hub (Sagemcom Fast 5689E) reverse-engineered API reference + Python client 2026-07-21 16:11:10 -04:00
bellapi.py Bell Giga Hub (Sagemcom Fast 5689E) reverse-engineered API reference + Python client 2026-07-21 16:11:10 -04:00
forwards.example.json Bell Giga Hub (Sagemcom Fast 5689E) reverse-engineered API reference + Python client 2026-07-21 16:11:10 -04:00
LICENSE Bell Giga Hub (Sagemcom Fast 5689E) reverse-engineered API reference + Python client 2026-07-21 16:11:10 -04:00
list_port_forwards.py Bell Giga Hub (Sagemcom Fast 5689E) reverse-engineered API reference + Python client 2026-07-21 16:11:10 -04:00
README.md Bell Giga Hub (Sagemcom Fast 5689E) reverse-engineered API reference + Python client 2026-07-21 16:11:10 -04:00
set_port_forwards.py Bell Giga Hub (Sagemcom Fast 5689E) reverse-engineered API reference + Python client 2026-07-21 16:11:10 -04:00

Bell Giga Hub (Sagemcom Fast 5689E) — Reverse-Engineered API Reference

Reverse-engineered JSON-RPC API documentation for the Bell Giga Hub residential gateway, based on the Sagemcom Fast 5689E platform running firmware 4KU_21.3.1.170_prod (GUI version 7.3.28).

This router is deployed by Bell Canada (and Virgin Plus, which runs on Bell infrastructure) as the standard residential gateway for fibre internet service. If you have one of these units and want to script port-forwarding, tune WiFi settings that the web UI hides, or just understand the data model, this is a complete field guide plus a working Python client.

Unofficial. Independent reverse-engineering for interoperability and personal administration of your own device. Not affiliated with, authorised by, or endorsed by Bell Canada or Sagemcom. API writes are volatile (RAM, not NVRAM) and a reboot reverts everything — use at your own risk on hardware you own. Built on the excellent sagemcom_api Python library.

Router Details

Field Value
Model Sagemcom Fast 5689E
Product Class F5689E / 5690
Hardware Version 5690-000001-000
Firmware (Internal) 4KU_21.3.1.170_prod
Firmware (External) SGC84000DC
GUI Version 7.3.28
Web UI http://192.168.2.1
Default Username admin
MAC OUI 0C:AC:8A (Sagemcom Broadband SAS)
WiFi Radios 3 (2.4 GHz, 5 GHz, 6 GHz — tri-band WiFi 6E)

The default admin password is printed on the router's label, and the unit reverts to that label password after any factory reset. Keep your password out of scripts — this client reads it from the BELL_PW environment variable.

Authentication

The Sagemcom API uses a two-phase SHA512 challenge-response authentication protocol.

Protocol Flow

Client                                    Router
  │                                         │
  ├─── Phase 1: logIn (no credentials) ───► │
  │                                         │
  ◄────── nonce + session-id ──────────────┤
  │                                         │
  │  Compute:                               │
  │    hash_pass = SHA512(password)          │
  │    ha1 = SHA512(user:nonce:hash_pass)    │
  │    cnonce = random uint32               │
  │    auth_key = SHA512(ha1:reqId:cnonce    │
  │               :JSON:/cgi/json-req)       │
  │                                         │
  ├─── Phase 2: logIn (with auth_key) ────► │
  │                                         │
  ◄────── authenticated session ───────────┤

Password Hashing

The GUI JavaScript (gui-core.js) reveals the hashing scheme:

// GUI_ACTIVATE_SHA512ENCODE_OPT = 1 → use SHA512
// GUI_PASSWORD_SALT = "" → no salt on this firmware
hashEncoderPass = SHA512(password)        // salt is empty
ha1 = SHA512(username + ":" + nonce + ":" + hashEncoderPass)
auth_key = SHA512(ha1 + ":" + reqId + ":" + cnonce + ":JSON:/cgi/json-req")

Request Format

All API calls are HTTP POST to /cgi/json-req with form-encoded body (not raw JSON):

Content-Type: application/x-www-form-urlencoded

req={"request":{"id":1,"session-id":"SESSION","priority":true,"actions":[...],"cnonce":CNONCE,"auth-key":"AUTH_KEY"}}

The body is req=<url-encoded JSON>. The response is application/json.

Login Example (Phase 1)

{
  "request": {
    "id": 1,
    "session-id": "0",
    "priority": true,
    "actions": [{
      "id": 0,
      "method": "logIn",
      "parameters": {
        "user": "admin",
        "persistent": "true",
        "session-options": {
          "nss": [
            {"name": "gtw", "uri": "http://sagemcom.com/gateway-data"}
          ],
          "language": "ident",
          "current-language": "en",
          "flags": {
            "no-hierarchies": true,
            "return-session-options": true
          }
        }
      }
    }],
    "cnonce": 0,
    "auth-key": ""
  }
}

Data Model Namespaces

The router supports three data model namespaces (configured via session-options.nss):

Namespace URI Description
gtw http://sagemcom.com/gateway-data Internal gateway data model
tr181 http://sagemcom.com/tr181-data TR-181 standard data model
tr181bell http://sagemcom.com/tr181bell-data Bell-specific TR-181 extensions

API Methods

getValue

Read a value from the data model.

{
  "id": 0,
  "method": "getValue",
  "xpath": "Device/WiFi/Radios",
  "options": {}
}

setValue

Write a value to the data model.

CRITICAL: Values must be sent as native JSON types, not strings.

{
  "id": 0,
  "method": "setValue",
  "xpath": "Device/WiFi/Radios/Radio[3]/GreenAPEnabled",
  "parameters": {"value": false},
  "options": {}
}
Value Type Correct Wrong
Boolean {"value": false} {"value": "false"} or {"value": "False"}
Integer {"value": 44} {"value": "44"}
Boolean (true) {"value": true} {"value": "true"} or {"value": "True"}

The sagemcom_api Python library (v1.4.3) has a bug where set_value_by_xpath() calls str(value) on all values, converting booleans to "True"/"False" strings. This causes XMO_PARAMETER_ERR on radio properties. Bypass the library's method and call the internal __api_request_async() directly (see bellapi.py for a working implementation).

addChild

Adds a new element to a list node. Used to create band steering interface slots.

{
  "id": 0,
  "method": "addChild",
  "xpath": "Device/WiFi/BandSteering/Interfaces",
  "parameters": {"InterfaceReference": "Device/WiFi/SSIDs/SSID[wl0]"}
}

Returns the new element's UID. Must be followed by setValue calls to configure the child's properties (e.g. InterfacesSelection).

Container-dependent behaviour (learned on Device/NAT/PortMappings):

  • Passing parameters: {} (empty) HANGS the firmware — the call never returns. Always pass a non-empty dict; on PortMappings its contents are then discarded but the call completes.
  • On PortMappings the params are ignored entirelyaddChild yields a blank row with an auto-assigned alias cpe-<uid>, and every field must be set afterward via setValue. (You cannot create a fully-formed rule in one addChild.) The band-steering usage above may honour InterfaceReference, but do not assume seeding works on other list nodes — verify per container.

reboot

Reboots the router. Available via the sagemcom_api library as client.reboot(). The HTTP response will time out (the router goes down before responding), so catch TimeoutError / asyncio.TimeoutError and treat it as success.

try:
    await client.reboot()
except (TimeoutError, asyncio.TimeoutError):
    pass  # Expected — router is rebooting

All Methods

Method Description Status
logIn Two-phase authenticate Working
logOut End session Working
getValue Read data model node Working
setValue Write leaf property Working (native types only)
addChild Add list element Working (band steering interfaces; NAT port mappings — blank row, params ignored)
reboot Reboot router Working (times out, expected)
getNotificationEvents Subscribe to value changes Not tested
setValues Batch write Tested, not supported
modify Modify object Tested, not supported
apply Apply changes Tested, not supported
save Save to NVRAM Tested, not supported
commit Commit changes Tested, not supported
applyAndSave Apply + save Tested, not supported
saveConfiguration Save config Tested, not supported
addListItem Add to list Tested, not supported (use addChild)

XPath Reference

Path Format

List elements use bracket notation with UID (1-indexed):

Device/WiFi/Radios/Radio[1]    ← 2.4 GHz (uid=1)
Device/WiFi/Radios/Radio[2]    ← 5 GHz   (uid=2)
Device/WiFi/Radios/Radio[3]    ← 6 GHz   (uid=3)

Index = position, not uid. Radio[N] / PortMapping[N] selects the Nth element in list order, which equals the uid only when uids are contiguous. WiFi radios are 1/2/3, so Radio[2] happens to be uid 2. But Device/NAT/PortMappings has sparse uids (gaps left by deleted/failed rows), so there [N] is the position and addressing by a gap uid returns XMO_UNKNOWN_PATH_ERR. See the NAT / Port Forwarding section.

Leaf properties use slash notation:

Device/WiFi/Radios/Radio[2]/Channel
Device/WiFi/Radios/Radio[2]/TransmitBeamForming
Device/WiFi/BandSteering/Enable

Path Formats That Do NOT Work

Format Error
Radio[@uid='2'] XMO_INVALID_PATH_ERR
Radio[RADIO5G] XMO_UNKNOWN_PATH_ERR
Radio/2 XMO_INVALID_PATH_ERR
Radios/2 XMO_UNKNOWN_PATH_ERR
Radios[2] XMO_UNKNOWN_PATH_ERR

The Radio[Alias='RADIO5G'] format works for collection reads via the sagemcom_api library (returns the whole radio object), but leaf property access (Radio[Alias='RADIO5G']/Channel) returns XMO_UNKNOWN_PATH_ERR. Use UID notation (Radio[2]/Channel) for leaf access.

Confirmed Readable Paths

Path Returns
Device/WiFi Full WiFi tree (radios, SSIDs, APs, band steering)
Device/WiFi/Radios List of 3 radio objects
Device/WiFi/Radios/Radio[N] Single radio by UID (1-3)
Device/WiFi/Radios/Radio[N]/Property Single leaf property
Device/WiFi/SSIDs List of 7 SSID objects
Device/WiFi/AccessPoints List of APs with security + client lists
Device/WiFi/BandSteering Band steering config tree
Device/WiFi/BandSteering/Enable Leaf boolean
Device/WiFi/BandSteering/Interfaces Interface list (empty until configured)
Device/WiFi/BandSteering/Interfaces/Interface[N] Single interface slot
Device/WiFi/BandSteering/Interfaces/Interface[N]/InterfacesSelection SSID reference

Confirmed Writable Paths

Path Type Notes
Device/WiFi/BandSteering/Enable bool Persists across reboots
Device/WiFi/Radios/Radio[2]/TransmitBeamForming bool Volatile — reverts on reboot
Device/WiFi/Radios/Radio[3]/GreenAPEnabled bool Volatile status unconfirmed
Device/WiFi/Radios/Radio[2]/AutoChannelEnable bool Persists (confirmed)
Device/WiFi/Radios/Radio[2]/Channel int Persists (confirmed)
Device/WiFi/BandSteering/Interfaces/Interface[N]/InterfacesSelection string SSID ref, volatile

Error Codes

Code Hex Name Meaning
16777216 0x01000000 XMO_REQUEST_NO_ERR Success
16777224 0x01000008 XMO_LOGIN_RETRY_ERR Rate limited, wait and retry
16777236 0x01000014 XMO_REQUEST_ACTION_ERR Action failed
16777241 0x01000019 XMO_INVALID_PATH_ERR Path syntax invalid
16777242 0x0100001A XMO_UNKNOWN_PATH_ERR Path does not exist
16777243 0x0100001B XMO_UNKNOWN_PATH_ERR Path/index does not exist (observed for NAT xpath by wrong position)
16777244 0x0100001C XMO_PARAMETER_ERR Wrong parameter format/type
16777261 0x0100002D XMO_REGEX_ERR Value failed a field's regex constraint (e.g. / in Description)

Rate Limiting

The router enforces login rate limiting. After approximately 5-6 failed login attempts in quick succession, it returns XMO_LOGIN_RETRY_ERR for all subsequent attempts. The lockout lasts 2-3 minutes. A router reboot clears the rate limit immediately. Make one attempt at a time with a known-good password.

WiFi Data Model

Radios (Device/WiFi/Radios)

Returns a list of 3 radio objects. Key properties:

Property (CamelCase) Property (snake_case) Type Description
Enable enable bool Radio on/off
Alias alias string RADIO2G4, RADIO5G, RADIO6G
OperatingFrequencyBand operating_frequency_band string 2_4GHZ, 5GHZ, 6GHZ
Channel channel int Current channel number
AutoChannelEnable auto_channel_enable bool Auto-channel selection
AutoChannelRefreshPeriod auto_channel_refresh_period int Seconds between auto-scans (900)
OperatingChannelBandwidth operating_channel_bandwidth string AUTO, 20, 40, 80, 160
CurrentOperatingChannelBandwidth current_operating_channel_bandwidth string e.g. 160MHz
TransmitBeamForming transmit_beam_forming bool Beamforming on/off
GreenAPEnabled green_ap_enabled bool Green AP power saving
GreenAPDelay green_ap_delay int Seconds before power reduction (20)
TransmitPower transmit_power int 0-100 (percentage)
TransmitPowerMax transmit_power_max float Max dBm (31.5)
PossibleChannels possible_channels string Comma-separated channel list
BlackListedChannels black_listed_channels string Channels excluded from auto-select
ChannelHoppingEnable channel_hopping_enable bool DFS channel hopping
DLMUMIMOEnabled dlmumimo_enabled bool Downlink MU-MIMO
DownlinkOFDMAEnable downlink_ofdma_enable bool Downlink OFDMA
UplinkOFDMAEnable uplink_ofdma_enable bool Uplink OFDMA
OperatingStandards operating_standards string e.g. a,n,ac,ax
BeaconPeriod beacon_period int Beacon interval in TU (100)
DTIMPeriod dtim_period int DTIM multiplier (1)
IfcName ifc_name string Linux interface: wl0, wl1, wl2
ATFEnable atf_enable bool Airtime fairness
BurstModeEnable burst_mode_enable bool Frame bursting
CSAEnable csa_enable bool Channel Switch Announcement
CSACount csa_count int CSA frame count before switch
CSADeauth csa_deauth string Deauth mode on CSA
ChannelsInUse channels_in_use string Channels the radio considers usable
IncreasedPowerEnable increased_power_enable bool Extended tx power mode
RegulatoryDomain regulatory_domain string e.g. CAI (Canada)
RegulatoryRegionSubRegion regulatory_region_sub_region string e.g. CA/0
PacketAggregationEnable packet_aggregation_enable bool A-MPDU/A-MSDU aggregation
AMPDU AMPDU int A-MPDU setting (-1 = auto)
AMSDU AMSDU int A-MSDU setting
VoWEnable vo_w_enable bool Voice over WiFi
InitiateACS initiate_acs string Auto-channel scan status (READY)
RadarDetections radar_detections int DFS radar detection count

Radio Defaults (as deployed by Bell)

Setting 2.4 GHz (Radio 1) 5 GHz (Radio 2) 6 GHz (Radio 3)
Standards b,g,n,ax a,n,ac,ax ax
Channel 11 (auto) 60 (auto, DFS) 213 (auto)
Bandwidth AUTO (40 MHz) AUTO (160 MHz) AUTO (160 MHz)
Tx Power 100% (31.5 dBm) 100% (31.5 dBm) 100% (31.5 dBm)
Beamforming ON OFF ON
Green AP OFF OFF ON
MU-MIMO DL OFF ON ON
OFDMA Up/Down ON ON ON
Interface wl0 wl1 wl2

Note: Bell ships the 5 GHz radio with beamforming disabled and the 6 GHz radio with Green AP enabled. Both are suboptimal for coverage (see Coverage Optimisation).

5 GHz Channel Blacklist (Bell firmware)

Bell's firmware blacklists the majority of 5 GHz channels from auto-selection:

Blacklisted: 36,40,48,52,56,64,104,108,112,116,136,140,144,149,153,161,165
Available:   44,60,100,132,157

This means auto-channel only selects from 5 channels. Channel 44 (UNII-1, non-DFS) is the only non-DFS option available after blacklisting. Channels 60, 100, 132 are DFS. Channel 157 is UNII-3. Manual channel selection can override the blacklist.

Band Steering (Device/WiFi/BandSteering)

{
  "band_steering": {
    "enable": false,
    "status": "DISABLED",
    "bsd_parameters": {
      "bsd_scheme": 2,
      "message_level_debug": false,
      "bounce_detection": {
        "window_time": 180,
        "counts": 2,
        "dwell_time": 1800
      },
      "phy_rate": 0,
      "airtime_slowest_link": 0,
      "bss_transition": true,
      "legacy_band_steering": true
    },
    "devices": [],
    "interfaces": []
  }
}

Set via: Device/WiFi/BandSteering/Enable = true

Bell ships this disabled by default. Enabling it allows the router to guide clients between bands based on signal quality (802.11v BSS Transition).

SSIDs (Device/WiFi/SSIDs)

The gateway exposes 7 SSID objects — a private + guest pair on each of the three bands, plus a hidden IPTV SSID on 5 GHz for Fibe TV set-top boxes:

UID Alias Band Default state Notes
1 WL_PRIV 2.4 GHz UP Private network
2 WL_GUEST 2.4 GHz DOWN Guest (off by default)
3 WL_IPTV_5G 5 GHz UP (hidden) Fibe TV set-top boxes
4 WL_DATA_5G 5 GHz UP Private network
5 WL_GUEST_5G 5 GHz DOWN Guest
6 WL_PRIV_6G 6 GHz UP Private network
7 WL_GUEST_6G 6 GHz DOWN Guest

Each SSID object carries its SSID (name), BSSID (MAC), OperatingFrequencyBand, and Status. Bell auto-generates the default guest SSID name in the form BELLxxxx-V. The three private SSIDs usually share one name, presenting a single roaming network across all bands.

Access Points (Device/WiFi/AccessPoints)

Contains security configuration, WPS settings, and associated device lists per SSID. Security modes: WPA2-Personal (2.4/5 GHz), WPA3-Personal/SAE (6 GHz).

Coverage Optimisation

Two of Bell's shipped radio defaults are suboptimal for coverage, and both are only adjustable via the API (the web UI hides them). bellapi.py --optimise applies this set:

# Setting Path Bell default Better Why
1 Band Steering BandSteering/Enable false true Guides clients to the best band as they move (802.11v BSS Transition)
2 5 GHz Beamforming Radio[2]/TransmitBeamForming false true Focuses the 5 GHz signal toward associated devices
3 6 GHz Green AP Radio[3]/GreenAPEnabled true false Keeps the 6 GHz radio at full power instead of dropping it to save energy
4 5 GHz Channel Radio[2]/Channel auto (often DFS) 44 (non-DFS) Avoids radar-driven DFS channel interruptions
4b 5 GHz Auto-Channel Radio[2]/AutoChannelEnable true false Locks the manual channel choice

Edit the set in cmd_optimise() for your own environment — the right 5 GHz channel depends on local congestion, and Bell's firmware limits auto-selection to just five channels (see the 5 GHz blacklist above).

A common symptom this fixes: a phone or laptop latches onto the 6 GHz band — which has the worst wall/floor penetration of the three — and refuses to fall back to 5 or 2.4 GHz as you walk away from the router, holding the 6 GHz link until it drops out entirely. With band steering disabled (Bell's default) the client gets no hint to step down a band. Enabling band steering, and keeping 6 GHz at full power, is the usual remedy. Manually selecting a non-DFS 5 GHz channel additionally avoids the multi-second outages that a DFS radar event triggers.

NAT / Port Forwarding

Port forwarding lives in the NAT branch of the data model:

Device/NAT/PortMappings                       ← container (list node)
Device/NAT/PortMappings/PortMapping[N]        ← one forward rule, addressed by 1-based POSITION
Device/NAT/PortMappings/PortMapping[N]/Field  ← leaf property

A Bell factory reset wipes every rule in this container, so a scripted, repeatable way to recreate your forwards is worth having. This was reverse-engineered to do exactly that.

Write recipe (this is NOT the intuitive one)

A working rule takes two distinct steps — you cannot create a fully-formed rule in one call:

  1. addChild creates a BLANK row. Its parameters are ignored: the row is born with default values and an auto-assigned alias cpe-<uid>, no matter what you pass. (And empty {} params hang the firmware — pass a non-empty dict whose contents are discarded.)
  2. Populate the row with setValue, one leaf at a time, addressed by position (below). Set Enable last so a port never opens while the row is half-configured.
# raw() = call client._SagemcomClient__api_request_async([action], priority=True) directly,
# so values keep their native JSON type (the library's set_value_by_xpath str()-ifies them).

# 1) create a blank row — params ignored, but MUST be non-empty or the call hangs
await raw("addChild", "Device/NAT/PortMappings", {"Enable": False})

# 2) find the new row's 1-based POSITION in the list, then populate it
base = f"Device/NAT/PortMappings/PortMapping[{pos}]"
await raw("setValue", f"{base}/Alias",          {"value": "MyService"})      # str, writable
await raw("setValue", f"{base}/Protocol",       {"value": "TCP"})
await raw("setValue", f"{base}/ExternalPort",   {"value": 8080})             # native int
await raw("setValue", f"{base}/InternalPort",   {"value": 8080})
await raw("setValue", f"{base}/InternalClient", {"value": "192.168.2.10"})   # your LAN client
await raw("setValue", f"{base}/Enable",         {"value": True})             # native bool, LAST

Gotchas (each cost a debugging cycle)

Gotcha Detail
Empty addChild HANGS parameters: {} never returns (true hang, not slowness — a 60 s timeout still hung with no row created). Pass any non-empty dict and the call completes.
addChild params are ignored The new row is always a blank cpe-<uid>; every field is set afterward with setValue.
Index is POSITION, not uid PortMapping[N] selects the Nth row in list order, not the row whose uid == N. PortMapping uids are sparse (gaps from deleted/failed rows), so uid ≠ position; addressing a gap uid gives XMO_UNKNOWN_PATH_ERR (16777243). WiFi radios hid this because their uids are contiguous.
Native types only ExternalPort must be int 8080, Enable must be bool true. The library's set_value_by_xpath does str(value); the firmware rejects "8080" / "True". Call __api_request_async directly.
Description rejects punctuation A / in Description returns XMO_REGEX_ERR (16777261). Keep it to [A-Za-z0-9 ].
Alias IS writable It can be overwritten from cpe-<uid> to a chosen name — not an immutable key.
No delete sagemcom_api exposes no deleteChild/removeChild. A row cannot be removed via the library. A disabled row (Enable=false, ExternalPort=0, no client) is inert — forwards nothing — so leave unwanted rows disabled.

PortMapping field grammar

Field Type Notes
Enable bool Master on/off. Set LAST.
Alias string Pattern ^([-_A-Za-z0-9])*$, max 64. Writable. Auto = cpe-<uid>.
Protocol enum ALL, TCP, UDP, BOTH, ICMP, GRE, ESP, AH. Default TCP.
ExternalPort uint16 WAN-side port (start of range).
ExternalPortEndRange uint16 End of external range; omit / 0 for a single port.
InternalPort uint16 LAN-side port.
InternalClient string Target LAN IP, e.g. 192.168.2.10.
Target enum DROP, ACCEPT, REJECT. Default ACCEPT.
Service enum NONE, FTP, HTTP, HTTPS, DMZ, … (predefined service templates).
Description string Cosmetic. [A-Za-z0-9 ] only (punctuation → XMO_REGEX_ERR).
ExternalInterface ref Default Device/IP/Interfaces/Interface[IP_DATA] (WAN).
InternalInterface ref Default Device/IP/Interfaces/Interface[IP_BR_LAN] (LAN bridge).

Defining your forwards

set_port_forwards.py reads the target LAN client and the rule set from a forwards.json beside it (gitignored — keep your own infrastructure out of version control). A committed forwards.example.json shows the shape:

{
  "client": "192.168.2.10",
  "rules": [
    {"alias": "MyService", "protocol": "TCP", "external": 8080, "internal": 8080, "external_end": null, "description": "example forward"},
    {"alias": "MyRange",   "protocol": "TCP", "external": 9000, "internal": 9000, "external_end": 9002, "description": "example range"}
  ]
}

Copy it to forwards.json, fill in your real rules, and run the script. Rows you no longer want can be left disabled (Enable=false) — the API exposes no delete.

Recovery scripts

The full addChildsetValue flow is scripted, idempotent, and repair-capable. It reads your rules from forwards.json (above) and the admin password from $BELL_PW:

# Recreate/repair every forward in forwards.json. Safe to re-run: it auto-provisions
# blank rows, populates them by position, sets Enable last, canaries the first rule,
# and skips any rule already enabled + correct.
BELL_PW='<admin pw>' .venv/bin/python set_port_forwards.py

# Read-only status of the current NAT table:
BELL_PW='<admin pw>' .venv/bin/python list_port_forwards.py

Neither the admin password nor any real forward is stored in this repo — the password comes from the environment (it is the router-label default), and real rules live in the gitignored forwards.json.

⚠️ Persistence UNTESTED for NAT

As with WiFi, the API may write to RAM, not NVRAM (there is no save/commit/apply). Rules written this way read back ENABLED, but a write → reboot → recheck cycle has not been confirmed for NAT. If your forwards vanish after a reboot or power blip, re-run set_port_forwards.py — it restores the whole set in ~15 seconds.

Web UI Limitations

The Bell Giga Hub web UI (http://192.168.2.1) does not expose:

  • Beamforming (TransmitBeamForming)
  • Green AP (GreenAPEnabled)
  • Manual per-radio channel selection (partially available under Advanced Settings)
  • Detailed radio configuration

These settings exist in the firmware data model and are readable/writable via the JSON-RPC API, but Bell has hidden them from the web interface. The web UI JavaScript bundles (common-bundle.js, main-bundle.js) contain zero references to beamforming or Green AP.

The web UI does expose: SSID/password, guest network toggle, WPS, MAC filtering, and basic channel/bandwidth options (under Manage my Wi-Fi → Advanced Settings).

Web UI SPA Routes

The Bell Giga Hub uses a single-page application (SPA) built on jQuery + Dust.js templates. Routes are hash/query-param based:

Route Page
?c=dashboard Main dashboard
managewifi WiFi settings (SSID, password, guest toggle)
managewifi/advancedsettings Advanced WiFi (channel, bandwidth, security)
advancedtools Advanced tools menu
advancedtools/wifi WiFi advanced tools
advancedtools/wifi/macfiltering MAC filtering
advancedtools/wifi/wps WPS configuration
advancedtools/dhcp DHCP settings
advancedtools/dns DNS settings
advancedtools/dmz DMZ configuration
advancedtools/ddns Dynamic DNS
advancedtools/diagnostics Network diagnostics
advancedtools/logs System logs
advancedtools/monitoring Network monitoring

Web UI Technical Notes

  • SPA framework: jQuery 1.8.3 + Dust.js 3.0.1 templating
  • Auth: same SHA512 challenge-response as the JSON-RPC API
  • JS bundles: common-bundle.js (shared), main-bundle.js (desktop), system-csp-production.js
  • Template system: .gtpl files loaded dynamically via tpl/src/
  • The SPA aggressively navigates to about:blank in headless/automated browsers, making it difficult to automate. The #headerActions element exists but is hidden during the splash screen. Headless automation requires careful timing or JS injection.

Persistence

API writes are volatile. The Sagemcom API writes to RAM, not NVRAM. Settings revert to Bell's defaults after a router reboot. There is no save, commit, or applyAndSave method exposed in the API.

Setting Persists? Evidence
Band Steering Enable Yes Confirmed true after reboot
BSS Transition Yes Always-on in firmware
5 GHz Channel (44) Yes Confirmed 44 (manual) after reboot
5 GHz AutoChannelEnable Yes Confirmed false after reboot
5 GHz Beamforming No Confirmed reverts to false
6 GHz Green AP Unconfirmed Was false after reboot, may persist
Band Steering Interfaces Unconfirmed Added via addChild, volatile status unknown
NAT PortMappings Untested Rules read back ENABLED after write; write→reboot→recheck not yet run

Workaround

Run apply_optimisations.sh after any router reboot:

./apply_optimisations.sh

This re-applies all optimisations via the API. It creates a .venv on first run.

Band Steering Notes

Band steering has two layers:

  1. 802.11v BSS Transition (BSSTransition: true) — standards-based. The router sends management frames suggesting clients move to a better BSSID/band. Supported by modern phones and laptops. Always active on this firmware.

  2. Broadcom BSD (proprietary) — more aggressive steering at association time. Requires interfaces to be configured via addChild to Device/WiFi/BandSteering/Interfaces, then setting InterfacesSelection on each to the corresponding SSID reference. The Status field may remain "DISABLED" even when properly configured.

For most use cases, 802.11v BSS Transition alone is sufficient.

Python Usage

Dependencies

python3 -m venv .venv
source .venv/bin/activate
pip install sagemcom_api

Credentials

The client reads the admin password from the BELL_PW environment variable — nothing is hard-coded. Export it (or prefix each command) with your router-label password:

export BELL_PW='your-router-admin-password'

Quick Start

python3 bellapi.py --status     # Show current settings
python3 bellapi.py --optimise   # Apply all optimisations
python3 bellapi.py --set "Device/WiFi/Radios/Radio[2]/Channel" 44
python3 bellapi.py --get "Device/WiFi/BandSteering"
python3 bellapi.py --radios     # Full radio property dump
python3 bellapi.py --clients    # Connected device list

See bellapi.py for the full implementation.

Additional API Methods Discovered

Method Path Parameters Result
addChild Device/WiFi/BandSteering/Interfaces {"InterfaceReference": "..."} Adds interface slot
setValue Interface[N]/InterfacesSelection {"value": "Device/WiFi/SSIDs/SSID[ALIAS]"} Links to SSID

The addChild method works for adding list items, while setValue with native JSON types works for scalar properties. The setValues, modify, apply, save, and commit methods do not exist on this firmware.

Hardware Notes

  • RAM: ~1 GB total
  • Regulatory domain: CA (Canada), DFS-FCC
  • 6 GHz regulatory: 5925-7125 MHz @ 12 dBm, NO-OUTDOOR

Contributing

Firmware versions drift, and Bell/Sagemcom may change field names, defaults, or the blacklist between releases. If your unit behaves differently — a new firmware string, a method that now works, a field with a different constraint — issues and pull requests are welcome. Please include your firmware version (Firmware (Internal) from the router details page) so findings can be pinned to a release.

License

GPL-3.0. See LICENSE; full text at https://www.gnu.org/licenses/gpl-3.0.html.