Source code for pylocal_akuvox.contacts

# SPDX-FileCopyrightText: 2026 Andrew Grimberg <tykeal@bardicgrove.org>
# SPDX-License-Identifier: Apache-2.0

"""Contact management operations for Akuvox devices.

.. note::

   This module uses the ``/api/contact/*`` HTTP endpoints which manage a
   **separate data store** from the Akuvox device web UI.  Contacts
   created via these endpoints will **not** appear in the web UI, and
   vice-versa.  The web UI uses session-authenticated ``/web/`` endpoints
   that are not supported by this library.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from pylocal_akuvox.exceptions import AkuvoxValidationError
from pylocal_akuvox.models import Contact

if TYPE_CHECKING:
    from pylocal_akuvox._http import AkuvoxHttpClient


def _mutation_body(action: str, item: list[dict[str, Any]]) -> dict[str, Any]:
    """Wrap a contact payload in the device mutation envelope.

    The ``target`` field is required by E18 firmware to route the
    request to the correct CGI handler.
    """
    return {
        "target": "contact",
        "action": action,
        "data": {"item": item},
    }


[docs] async def list_contacts( http: AkuvoxHttpClient, *, page: int | None = None, ) -> list[Contact]: """List contacts from the device, optionally paginated.""" params: dict[str, Any] = {} if page is not None: params["page"] = page data = await http.get("/api/contact/get", params=params or None) items = data.get("item", []) if not isinstance(items, list): return [] return [Contact.from_api_response(item) for item in items if isinstance(item, dict)]
[docs] async def add_contact( http: AkuvoxHttpClient, *, name: str, phone: str | None = None, group: str | None = None, ) -> None: """Add a contact to the device address book.""" if not name: msg = "name is required for add_contact" raise AkuvoxValidationError(msg) payload: dict[str, Any] = {"Name": name} if phone is not None: payload["Phone"] = phone if group is not None: payload["Group"] = group await http.post("/api/contact/set", data=_mutation_body("add", [payload]))
async def _get_contact_by_id( http: AkuvoxHttpClient, contact_id: str, ) -> dict[str, Any]: """Fetch a single contact's raw data by internal ID. Iterates through all pages (device returns 10 per page). """ from pylocal_akuvox.exceptions import AkuvoxDeviceError page = 1 while True: data = await http.get("/api/contact/get", params={"page": page}) items = data.get("item", []) if not isinstance(items, list) or len(items) == 0: break for item in items: if isinstance(item, dict) and item.get("ID") == contact_id: return item page += 1 msg = f"Contact ID {contact_id} not found" raise AkuvoxDeviceError(msg)
[docs] async def modify_contact( http: AkuvoxHttpClient, *, id: str, name: str | None = None, phone: str | None = None, group: str | None = None, ) -> None: """Modify an existing contact on the device. The device requires a full contact record for set operations, so this fetches the current record and merges changes. """ if name is None and phone is None and group is None: msg = "at least one of name, phone, or group is required for modify_contact" raise AkuvoxValidationError(msg) current = await _get_contact_by_id(http, id) if name is not None: current["Name"] = name if phone is not None: current["Phone"] = phone if group is not None: current["Group"] = group await http.post("/api/contact/set", data=_mutation_body("set", [current]))
[docs] async def delete_contact( http: AkuvoxHttpClient, *, id: str | list[str], ) -> None: """Delete one or more contacts from the device.""" if isinstance(id, str): ids = [id] else: ids = id items = [{"ID": cid} for cid in ids] await http.post("/api/contact/set", data=_mutation_body("del", items))