Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 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.4"
version = "1.5.5"
authors = [
{ name="Mark Godwin", email="10632972+MarkGodwin@users.noreply.github.com" },
]
Expand Down
9 changes: 9 additions & 0 deletions src/tplink_omada_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
PortProfileOverrides,
SwitchPortSettings,
)
from .definitions import (
OmadaControllerUpdateInfo,
OmadaHardwareUpgradeStatus,
OmadaHardwareUpdateInfo,
)

from . import definitions
from . import exceptions
from . import clients
Expand All @@ -19,6 +25,9 @@
"OmadaClient",
"OmadaSite",
"OmadaSiteClient",
"OmadaControllerUpdateInfo",
"OmadaHardwareUpgradeStatus",
"OmadaHardwareUpdateInfo",
"AccessPointPortSettings",
"GatewayPortSettings",
"OmadaClientSettings",
Expand Down
2 changes: 2 additions & 0 deletions src/tplink_omada_client/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
command_clients,
command_default,
command_devices,
command_firmware,
command_gateway,
command_known_clients,
command_poe,
Expand Down Expand Up @@ -53,6 +54,7 @@ def main(argv: Sequence[str] | None = None) -> int:
command_clients.arg_parser(subparsers)
command_default.arg_parser(subparsers)
command_devices.arg_parser(subparsers)
command_firmware.arg_parser(subparsers)
command_gateway.arg_parser(subparsers)
command_known_clients.arg_parser(subparsers)
command_poe.arg_parser(subparsers)
Expand Down
52 changes: 52 additions & 0 deletions src/tplink_omada_client/cli/command_firmware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Implementation for 'firmware' command"""

from argparse import ArgumentParser
from .config import get_target_config, to_omada_connection
from .util import dump_raw_data, get_target_argument


async def command_firmware(args) -> int:
"""Executes 'firmware' command"""
controller = get_target_argument(args)
config = get_target_config(controller)

async with to_omada_connection(config) as client:
controller_updates = await client.check_firmware_updates()
if controller_updates.hardware:
hardware_update = controller_updates.hardware
status = "\u2757 UPDATE" if hardware_update.upgrade else "\u2713 UP-TO-DATE"
print(f"{'Controller':<30} {hardware_update.current_version:<36} {hardware_update.latest_version:<36} {status}")
if hardware_update.upgrade and hardware_update.release_notes and args["release_notes"]:
print(f" Release Notes: {hardware_update.release_notes}")
else:
print("No controller firmware updates available.")

dump_raw_data(args, controller_updates)

site_client = await client.get_site_client(config.site)
devices = await site_client.get_devices()
if not devices:
print("No devices found in the specified site.")
return 0

for device in devices:
firmware = await site_client.get_firmware_details(device)
status = "\u2757 UPDATE" if firmware.current_version != firmware.latest_version else "\u2713 UP-TO-DATE"
print(f"{device.name:<30} {firmware.current_version:<36} {firmware.latest_version:<36} {status}")
if firmware.current_version != firmware.latest_version and firmware.release_notes and args["release_notes"]:
print(f" Release Notes: {firmware.release_notes}")
Comment on lines +30 to +34
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

firmware.release_notes can raise KeyError because OmadaFirmwareUpdate.release_notes indexes self._data["fwReleaseLog"] directly. Since this command accesses firmware.release_notes, it can crash when the API omits that field. Consider using firmware.raw_data.get("fwReleaseLog") here, or updating OmadaFirmwareUpdate.release_notes to return str | None via .get() and then checking for None.

Copilot uses AI. Check for mistakes.

dump_raw_data(args, firmware)

return 0


def arg_parser(subparsers) -> None:
"""Configures arguments parser for 'firmware' command"""
firmware_parser: ArgumentParser = subparsers.add_parser(
"firmware", help="Shows firmware information and update availability for the specified device"
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Help text says this command shows firmware info for the “specified device”, but the implementation prints controller firmware plus firmware status for all devices in the configured site. Update the help string to reflect what the command actually does to avoid confusing CLI users.

Suggested change
"firmware", help="Shows firmware information and update availability for the specified device"
"firmware", help="Shows controller and device firmware information and update availability for the configured site"

Copilot uses AI. Check for mistakes.
)
firmware_parser.set_defaults(func=command_firmware)

firmware_parser.add_argument("-d", "--dump", help="Output raw firmware information", action="store_true")
firmware_parser.add_argument("-rn", "--release-notes", help="Show release notes for firmware updates", action="store_true")
62 changes: 62 additions & 0 deletions src/tplink_omada_client/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,65 @@ class LedSetting(IntEnum):
@classmethod
def _missing_(cls, _):
return LedSetting.UNKNOWN


class OmadaHardwareUpdateInfo(OmadaApiData):
"""Information about available hardware firmware updates."""

@property
def upgrade(self) -> bool:
"""Whether a firmware upgrade is available."""
return self._data["upgrade"]

@property
def latest_version(self) -> str:
"""The latest available firmware version."""
return self._data.get("latestVersion", self.current_version)

@property
def current_version(self) -> str:
"""The currently installed firmware version."""
return self._data["currentVersion"]

@property
def release_notes(self) -> str | None:
"""Release notes for the latest firmware version."""
return self._data.get("fwReleaseLog", None)


class OmadaControllerUpdateInfo(OmadaApiData):
"""Information about available controller and device firmware updates."""

@property
def hardware(self) -> OmadaHardwareUpdateInfo | None:
"""Information about available hardware controller firmware updates."""
return OmadaHardwareUpdateInfo(self._data["hardware"]) if "hardware" in self._data else None


class OmadaHardwareUpgradeStatus(OmadaApiData):
"""Information about the status of a hardware controller firmware upgrade."""

@property
def upgrade_status(self) -> int:
"""The current status of the firmware upgrade process."""
return self._data["upgradeStatus"]

@property
def upgrade_msg(self) -> str:
"""Any message associated with the current firmware upgrade status."""
return self._data.get("upgradeMsg", "")

@property
def upgrade_time(self) -> int:
"""How often to refresh to watch the download progress?"""
return self._data.get("upgradeTime", 0)

@property
def download_progress(self) -> int:
"""The current progress of the firmware download, as a percentage."""
return self._data.get("downloadProgress", 0)

@property
def reboot_time(self) -> int:
"""How long after the donwload completes before we expect the controller to come back online, in seconds."""
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in docstring: “donwload” should be “download”.

Suggested change
"""How long after the donwload completes before we expect the controller to come back online, in seconds."""
"""How long after the download completes before we expect the controller to come back online, in seconds."""

Copilot uses AI. Check for mistakes.
return self._data.get("rebootTime", 300)
4 changes: 2 additions & 2 deletions src/tplink_omada_client/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def name(self) -> str:
@property
def model(self) -> str:
"""The device model, such as EAP225."""
return self._data("model", "Unknown")
return self._data.get("model", "Unknown")

@property
def model_display_name(self) -> str:
Expand Down Expand Up @@ -723,7 +723,7 @@ def current_version(self) -> str:
@property
def latest_version(self) -> str:
"""Latest firmware version available."""
return self._data["lastFwVer"]
return self._data.get("lastFwVer", self.current_version)

@property
def release_notes(self) -> str:
Expand Down
23 changes: 23 additions & 0 deletions src/tplink_omada_client/omadaclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from awesomeversion import AwesomeVersion
from multidict import CIMultiDict

from .definitions import OmadaControllerUpdateInfo, OmadaHardwareUpgradeStatus
from .omadasiteclient import OmadaSiteClient
from .omadaapiconnection import OmadaApiConnection

Expand Down Expand Up @@ -141,3 +142,25 @@ async def set_certificate(self, file: str, cert_password: str):
}
url = self._api.format_url("controller/setting")
await self._api.request("patch", url, json=payload)

async def check_firmware_updates(self) -> OmadaControllerUpdateInfo:
"""Check if firmware updates are available for the Omada hardware controller

Software Controller updates are probably not available via this API
"""
result = await self._api.request("get", self._api.format_url("controller/notification/updateInfo"))

return OmadaControllerUpdateInfo(result)
Comment on lines +146 to +153
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says check_firmware_updates “returns information … only if there is an update”, but the implementation always returns an OmadaControllerUpdateInfo instance. Either update the method contract (e.g., return OmadaControllerUpdateInfo | None and return None when there are no updates), or adjust the description/docstring to match the actual behavior.

Copilot uses AI. Check for mistakes.

async def upgrade_controller_firmware(self, target_version: str) -> bool:
"""Upgrade the Omada hardware controller firmware to the specified version."""
url = self._api.format_url("cmd/upgradeFirmware")
payload = {"targetVersion": target_version}
await self._api.request("post", url, json=payload)
return True
Comment on lines +155 to +160
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upgrade_controller_firmware returns True unconditionally, even though the request can fail/raise. This makes the return value misleading and redundant. Consider returning None (let exceptions signal failure) or returning/deriving a meaningful value from the API response (e.g., accepted/started status).

Suggested change
async def upgrade_controller_firmware(self, target_version: str) -> bool:
"""Upgrade the Omada hardware controller firmware to the specified version."""
url = self._api.format_url("cmd/upgradeFirmware")
payload = {"targetVersion": target_version}
await self._api.request("post", url, json=payload)
return True
async def upgrade_controller_firmware(self, target_version: str) -> None:
"""Upgrade the Omada hardware controller firmware to the specified version."""
url = self._api.format_url("cmd/upgradeFirmware")
payload = {"targetVersion": target_version}
await self._api.request("post", url, json=payload)

Copilot uses AI. Check for mistakes.

async def get_controller_upgrade_status(self) -> OmadaHardwareUpgradeStatus:
"""Get the status of an ongoing hardware controller firmware upgrade."""
url = self._api.format_url("maintenance/hardware/upgradeStatus")
result = await self._api.request("get", url)
return OmadaHardwareUpgradeStatus(result)
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