Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ A basic Python client for calling the TP-Link Omada controller API.
pip install tplink-omada-client
```

Note: Version 1.4 and later requires Python 3.11.
Note: Version 1.4 requires Python 3.11.
Note: Version 1.5 and later requires Python 3.13.

## Supported features

Expand Down Expand Up @@ -70,8 +71,8 @@ The Omada platform is transitioning to a new OpenAPI API which this library will
eventually. We will try to avoid breaking changes when this happens, but some will be unavoidable - particularly
authentication.

At the moment, the new API imposes severe daily call limits, even though it is a local device API.
Hopefully this will change, because it is unusable as it stands.
For now, we can use the OpenAPI endpoint with the old authentication style, just like the front-end UI does.
Eventually, we need to switch over to OAuth, but probably once all of the APIs have been migrated.

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "tplink_omada_client"
version = "1.5.0"
version = "1.5.1"
authors = [
{ name="Mark Godwin", email="10632972+MarkGodwin@users.noreply.github.com" },
]
Expand Down
5 changes: 5 additions & 0 deletions src/tplink_omada_client/cli/command_access_points.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Implementation for 'access-points' command"""

from argparse import _SubParsersAction

from tplink_omada_client.definitions import DeviceStatusCategory
from .config import get_target_config, to_omada_connection
from .util import dump_raw_data, get_checkbox_char, get_target_argument

Expand All @@ -14,6 +16,9 @@ async def command_access_points(args) -> int:
site_client = await client.get_site_client(config.site)
for access_point in await site_client.get_access_points():
print(f"{access_point.mac} {access_point.ip_address:>15} {access_point.name:20} ", end="")
if access_point.status_category != DeviceStatusCategory.CONNECTED:
print(f" {access_point.status.name} ({access_point.status_category.name})")
continue
print(f"11ac: {get_checkbox_char(access_point.supports_11ac)} ", end="")
print(f"5g: {get_checkbox_char(access_point.supports_5g)} ", end="")
print(f"5g2: {get_checkbox_char(access_point.supports_5g2)} ", end="")
Expand Down
6 changes: 6 additions & 0 deletions src/tplink_omada_client/cli/command_switches.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Implementation for 'switches' command"""

from argparse import _SubParsersAction

from tplink_omada_client.definitions import DeviceStatusCategory
from .config import get_target_config, to_omada_connection
from .util import dump_raw_data, get_link_status_char, get_power_char, get_target_argument

Expand All @@ -14,6 +16,10 @@ async def command_switches(args) -> int:
site_client = await client.get_site_client(config.site)
for switch in await site_client.get_switches():
print(f"{switch.mac} {switch.ip_address:>15} {switch.name:20} ", end="")
if switch.status_category != DeviceStatusCategory.CONNECTED:
print(f" {switch.status.name} ({switch.status_category.name})")
continue

for port in switch.ports:
if port.is_disabled:
print("x", end="")
Expand Down
65 changes: 39 additions & 26 deletions src/tplink_omada_client/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,32 +46,32 @@ def mac(self) -> str:
@property
def name(self) -> str:
"""The device name."""
return self._data["name"]
return self._data.get("name", self.mac)

@property
def model(self) -> str:
"""The device model, such as EAP225."""
return self._data["model"]
return self._data("model", "Unknown")

@property
def model_display_name(self) -> str:
"""Model description for front-end display."""
return self._data["showModel"]
return self._data.get("showModel", "Unknown Model")

@property
def status(self) -> DeviceStatus:
"""The status of the device."""
return DeviceStatus(self._data["status"])
return DeviceStatus(self._data.get("status", DeviceStatus.UNKNOWN))

@property
def status_category(self) -> DeviceStatusCategory:
"""The high-level status of the device."""
return DeviceStatusCategory(self._data["statusCategory"])
return DeviceStatusCategory(self._data.get("statusCategory", DeviceStatusCategory.UNKNOWN))

@property
def ip_address(self) -> str:
"""IP address of the device."""
return self._data["ip"]
return self._data.get("ip", "")

@property
def display_uptime(self) -> str | None:
Expand Down Expand Up @@ -108,7 +108,10 @@ def uptime(self) -> int:
@property
def firmware_version(self) -> str:
"""Firmware version of the device"""
return self._data["firmwareVersion"]
if self._data["statusCategory"] == DeviceStatusCategory.CONNECTED:
return self._data["firmwareVersion"]
else:
return "Unknown"


class OmadaListDevice(OmadaDevice):
Expand Down Expand Up @@ -137,7 +140,7 @@ class OmadaDetailedDevice(OmadaDevice):
@property
def led_setting(self) -> LedSetting:
"""The onboard LED setting for the device"""
return LedSetting(self._data["ledSetting"])
return LedSetting(self._data.get("ledSetting", LedSetting.UNKNOWN))


class OmadaLink(OmadaApiData):
Expand Down Expand Up @@ -295,17 +298,17 @@ class OmadaSwitchDeviceCaps(OmadaApiData):
@property
def poe_ports(self) -> int:
"""Number of PoE ports supported."""
return self._data["poePortNum"]
return self._data.get("poePortNum", 0)

@property
def supports_poe(self) -> bool:
"""Is PoE supported."""
return self._data["poeSupport"]
return self._data.get("poeSupport", False)

@property
def supports_bt(self) -> bool:
"""Is BT supported."""
return self._data["supportBt"]
return self._data.get("supportBt", False)


class OmadaSwitch(OmadaDetailedDevice):
Expand All @@ -317,12 +320,12 @@ def number_of_ports(self) -> int:
if "portNum" in self._data:
return self._data["portNum"]
# So much for the docs
return self._data["deviceMisc"]["portNum"]
return self._data.get("deviceMisc", {}).get("portNum", 0)

@property
def ports(self) -> list[OmadaSwitchPort]:
"""List of ports attached to the switch."""
return [OmadaSwitchPort(p) for p in self._data["ports"]]
return [OmadaSwitchPort(p) for p in self._data.get("ports", [])]

@property
def uplink(self) -> OmadaUplink | None:
Expand All @@ -344,7 +347,7 @@ def downlink(self) -> list[OmadaDownlink]:
@property
def device_capabilities(self) -> OmadaSwitchDeviceCaps:
"""Capabilities of the switch."""
return OmadaSwitchDeviceCaps(self._data["devCap"])
return OmadaSwitchDeviceCaps(self._data.get("devCap", {}))


class OmadaAccesPointLanPortSettings(OmadaApiData):
Expand All @@ -353,12 +356,12 @@ class OmadaAccesPointLanPortSettings(OmadaApiData):
@property
def port_name(self) -> str:
"""Name of the port - can't be edited"""
return self._data["lanPort"]
return self._data.get("lanPort", "LAN Port")

@property
def supports_vlan(self) -> bool:
"""True if the port supports VLAN tagging"""
return self._data["supportVlan"]
return self._data.get("supportVlan", False)

@property
def local_vlan_enable(self) -> bool:
Expand Down Expand Up @@ -391,37 +394,47 @@ class OmadaAccessPoint(OmadaDetailedDevice):
@property
def wireless_linked(self) -> bool:
"""True, if the AP is connected wirelessley."""
return self._data["wirelessLinked"]
return self._data.get("wirelessLinked", False)

@property
def supports_5g(self) -> bool:
"""True if 5G wifi is supported"""
return self._data["deviceMisc"]["support5g"]
if "deviceMisc" not in self._data:
return False
return self._data["deviceMisc"].get("support5g", False)

@property
def supports_5g2(self) -> bool:
"""True if 5G2 wifi is supported"""
return self._data["deviceMisc"]["support5g2"]
if "deviceMisc" not in self._data:
return False
return self._data["deviceMisc"].get("support5g2", False)

@property
def supports_6g(self) -> bool:
"""True if Wifi 6 is supported"""
return self._data["deviceMisc"]["support6g"]
if "deviceMisc" not in self._data:
return False
return self._data["deviceMisc"].get("support6g", False)

@property
def supports_11ac(self) -> bool:
"""True if PoE is supported"""
return self._data["deviceMisc"]["support11ac"]
if "deviceMisc" not in self._data:
return False
return self._data["deviceMisc"].get("support11ac", False)

@property
def supports_mesh(self) -> bool:
"""True if mesh networking is supported"""
return self._data["deviceMisc"]["supportMesh"]
if "deviceMisc" not in self._data:
return False
return self._data["deviceMisc"].get("supportMesh", False)

@property
def lan_port_settings(self) -> list[OmadaAccesPointLanPortSettings]:
"""Settings for the LAN ports on the access point"""
return [OmadaAccesPointLanPortSettings(p) for p in self._data["lanPortSettings"]]
return [OmadaAccesPointLanPortSettings(p) for p in self._data.get("lanPortSettings", [])]

@property
def wired_uplink(self) -> OmadaUplink | None:
Expand Down Expand Up @@ -882,17 +895,17 @@ def number_of_ports(self) -> int:
@property
def supports_poe(self) -> bool:
"""True if the device supports PoE."""
return self._data["supportPoe"]
return self._data.get("supportPoe", False)

@property
def ip(self) -> str:
"""Gateway's LAN IP address."""
return self._data["ip"]
return self._data.get("ip", "")

@property
def port_status(self) -> list[OmadaGatewayPortStatus]:
"""Status of the gateway's ports."""
return [OmadaGatewayPortStatus(p) for p in self._data["portStats"]]
return [OmadaGatewayPortStatus(p) for p in self._data.get("portStats", [])]

@property
def port_configs(self) -> list[OmadaGatewayPortConfig]:
Expand Down