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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Only a subset of the controller's features are supported:
* Lan port configuration for Access Points
* Gateway port status and WAN port Connect/Disconnect control

Tested with OC200 on Omada Controller Version 5.5.7 - 5.12.x. Other versions may not be fully compatible.
Tested with OC200 on Omada Controller Version 5.5.7 - 6.2.x. Other versions may not be fully compatible.
Version 5.0.x is definitely not compatible.

## CLI
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.5"
version = "1.5.6"
authors = [
{ name="Mark Godwin", email="10632972+MarkGodwin@users.noreply.github.com" },
]
Expand Down
4 changes: 2 additions & 2 deletions src/tplink_omada_client/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ class OmadaWiredClient(OmadaConnectedClient):
@property
def dot1x_vlan(self) -> int:
"""Network name corresponding to the VLAN obtained by 802.1x D-VLAN"""
return self._data["dot1xVlan"]
return self._data.get("dot1xVlan", 0)

@property
def gateway_mac(self) -> str | None:
Expand All @@ -233,7 +233,7 @@ def network_name(self) -> str:
@property
def port(self) -> int:
"""Switch port client is connected to"""
return self._data["port"]
return self._data.get("port", -1)

@property
def switch_mac(self) -> str | None:
Expand Down
30 changes: 27 additions & 3 deletions src/tplink_omada_client/omadaapiconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,16 @@ def format_url(self, end_point: str, site: str | None = None) -> str:

return urljoin(self._url, f"/{self._controller_id}/api/v2/{end_point}")

def format_openapi_url(self, end_point: str, site: str | None = None) -> str:
def format_openapi_url(self, end_point: str, version: str = "v1", site: str | None = None) -> str:
"""Get a REST url for the controller action"""

if site:
end_point = f"/sites/{site}/{end_point}"

return urljoin(self._url, f"/openapi/v1/{self._controller_id}{end_point}")
return urljoin(self._url, f"/openapi/{version}/{self._controller_id}{end_point}")

async def iterate_pages(self, url: str, params: dict[str, Any] | None = None) -> AsyncIterable[dict[str, Any]]:
"""Iterates all the entries of a paged endpoint"""
"""Iterates all the entries of a paged endpoint using GET with query parameters (old API)"""
request_params = {}
if params is not None:
request_params.update(params)
Expand All @@ -169,6 +169,30 @@ async def iterate_pages(self, url: str, params: dict[str, Any] | None = None) ->
for item in data:
yield item

async def iterate_pages_openapi(self, url: str, body: dict[str, Any] | None = None) -> AsyncIterable[dict[str, Any]]:
"""Iterates all the entries of a paged endpoint using POST with JSON body (OpenAPI v2)"""
request_body = {}
if body is not None:
request_body.update(body)
actual_page_size = _PAGE_SIZE

current_page = 1
has_next = True
while has_next:
request_body["pageSize"] = actual_page_size
request_body["page"] = current_page
response = await self.request("post", url, json=request_body)

# Setup next page request
actual_page_size = int(response["currentSize"])
total_rows = int(response["totalRows"])
has_next = total_rows > current_page * actual_page_size
current_page += 1

data: list[dict[str, Any]] = response["data"]
for item in data:
yield item

async def request(self, method: str, url: str, params=None, json=None, data: Payload | None = None) -> Any:
"""Perform a request specific to the controlller, with authentication"""

Expand Down
25 changes: 19 additions & 6 deletions src/tplink_omada_client/omadasiteclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,25 @@ async def update_client(self, mac_or_client: str | OmadaNetworkClient, settings:

async def get_connected_clients(self) -> AsyncIterable[OmadaConnectedClient]:
"""Get the clients connected to the site network."""
async for client in self._api.iterate_pages(self._api.format_url("clients", self._site_id), {"filters.active": "false"}):
is_wireless = client.get("wireless")
if is_wireless:
yield OmadaWirelessClient(client)
elif is_wireless is False:
yield OmadaWiredClient(client)
if (await self._api.get_controller_version()) >= AwesomeVersion("6.2.0.0"):
request_url = self._api.format_openapi_url("clients", version="v2", site=self._site_id)
async for client in self._api.iterate_pages_openapi(
request_url,
body={"filters": {"active": True}, "sorts": {}, "hideHealthUnsupported": True, "scope": 1},
):
is_wireless = client.get("wireless")
if is_wireless:
yield OmadaWirelessClient(client)
elif is_wireless is False:
yield OmadaWiredClient(client)
else:
request_url = self._api.format_url("clients", self._site_id)
async for client in self._api.iterate_pages(request_url, {"filters.active": "false"}):
is_wireless = client.get("wireless")
if is_wireless:
yield OmadaWirelessClient(client)
elif is_wireless is False:
yield OmadaWiredClient(client)

async def get_known_clients(self) -> AsyncIterable[OmadaNetworkClient]:
"""Get the clients connected to the site network."""
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading