diff --git a/README.md b/README.md index 4f1aa31..8b9682d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 973cfd9..26e23df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" }, ] diff --git a/src/tplink_omada_client/cli/command_access_points.py b/src/tplink_omada_client/cli/command_access_points.py index c63bc7c..5e95066 100644 --- a/src/tplink_omada_client/cli/command_access_points.py +++ b/src/tplink_omada_client/cli/command_access_points.py @@ -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 @@ -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="") diff --git a/src/tplink_omada_client/cli/command_switches.py b/src/tplink_omada_client/cli/command_switches.py index e3057d5..a37db34 100644 --- a/src/tplink_omada_client/cli/command_switches.py +++ b/src/tplink_omada_client/cli/command_switches.py @@ -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 @@ -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="") diff --git a/src/tplink_omada_client/devices.py b/src/tplink_omada_client/devices.py index 690bfb6..b4c069e 100644 --- a/src/tplink_omada_client/devices.py +++ b/src/tplink_omada_client/devices.py @@ -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: @@ -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): @@ -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): @@ -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): @@ -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: @@ -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): @@ -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: @@ -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: @@ -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]: