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) |
||
|---|---|---|
| .gitignore | ||
| apply_optimisations.sh | ||
| bellapi.py | ||
| forwards.example.json | ||
| LICENSE | ||
| list_port_forwards.py | ||
| README.md | ||
| set_port_forwards.py | ||
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_apiPython 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 entirely —
addChildyields a blank row with an auto-assigned aliascpe-<uid>, and every field must be set afterward viasetValue. (You cannot create a fully-formed rule in oneaddChild.) The band-steering usage above may honourInterfaceReference, 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, soRadio[2]happens to be uid 2. ButDevice/NAT/PortMappingshas sparse uids (gaps left by deleted/failed rows), so there[N]is the position and addressing by a gap uid returnsXMO_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:
addChildcreates a BLANK row. Itsparametersare ignored: the row is born with default values and an auto-assigned aliascpe-<uid>, no matter what you pass. (And empty{}params hang the firmware — pass a non-empty dict whose contents are discarded.)- Populate the row with
setValue, one leaf at a time, addressed by position (below). SetEnablelast 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 addChild → setValue 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:
.gtplfiles loaded dynamically viatpl/src/ - The SPA aggressively navigates to
about:blankin headless/automated browsers, making it difficult to automate. The#headerActionselement 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:
-
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. -
Broadcom BSD (proprietary) — more aggressive steering at association time. Requires interfaces to be configured via
addChildtoDevice/WiFi/BandSteering/Interfaces, then settingInterfacesSelectionon each to the corresponding SSID reference. TheStatusfield 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.