Source code for pylocal_akuvox.models.contacts

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

"""Contact / address-book data model."""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from pylocal_akuvox._capability_types import SchemaShape
from pylocal_akuvox.exceptions import AkuvoxParseError

if TYPE_CHECKING:
    from pylocal_akuvox._capability_profile import DeviceCapabilities


[docs] @dataclass(frozen=True, kw_only=True) class Contact: """Contact entry in a door-phone or apartment-book address book. Door-phone records populate ``name``, optional ``id``, ``phone``, and ``group``. Apartment-book records may omit a device-assigned ``ID`` and additionally expose ``apt_name``, ``apt_num``, ``building``, and ``landline``. The library does not synthesize a unique identifier for apartment-book records. Apartment-book-only fields are read-fidelity fields and are intentionally omitted from :meth:`to_api_payload`. """ name: str id: str | None = None phone: str | None = None group: str | None = None apt_name: str | None = None # APTName apt_num: str | None = None # APTNum building: str | None = None # Building landline: str | None = None # Landline
[docs] @classmethod def from_api_response( cls, data: dict[str, Any], *, capabilities: DeviceCapabilities | None = None, ) -> Contact: """Create Contact from API response data. ``capabilities`` is an optional :class:`DeviceCapabilities` record. When supplied, the parser consults ``capabilities.schema_shapes.get("contact", SchemaShape.DOOR_PHONE)`` to choose between two parse paths: * :attr:`SchemaShape.DOOR_PHONE` (default): today's parser, byte-identical to the pre-refactor behaviour — requires ``Name`` (raises :class:`AkuvoxParseError` on missing ``Name``); optionally consumes ``ID``, ``Phone``, ``Group``. * :attr:`SchemaShape.APARTMENT_BOOK`: additive parser used by X915S current firmware. ``Name`` is required as on door-phone; ``ID`` is **not** required (apartment-book payloads from the device may omit it). Apartment-book-only fields are surfaced as ``apt_name`` (``APTName``), ``apt_num`` (``APTNum``), ``building`` (``Building``), and ``landline`` (``Landline``). Empty apartment-book strings are preserved as device-returned information. Omitting the ``capabilities`` kwarg (or supplying a record with no ``"contact"`` schema-shape entry) falls back to the door-phone path — preserving FR-016 for legacy callers. """ shape = SchemaShape.DOOR_PHONE if capabilities is not None: shape = capabilities.schema_shapes.get("contact", SchemaShape.DOOR_PHONE) try: name = data["Name"] except KeyError as exc: msg = f"Missing required field {exc} in contact data" raise AkuvoxParseError(msg) from exc if shape is SchemaShape.APARTMENT_BOOK: # Apartment-book payloads (X915S) may omit ``ID`` entirely. # ``Phone`` is read if present so a door-phone-style # payload still parses under the apartment-book branch. return cls( name=name, id=data.get("ID"), phone=data.get("Phone") or None, group=data.get("Group") or None, apt_name=data.get("APTName"), apt_num=data.get("APTNum"), building=data.get("Building"), landline=data.get("Landline"), ) # Door-phone path — byte-identical to the pre-refactor parser. return cls( name=name, id=data.get("ID"), phone=data.get("Phone") or None, group=data.get("Group") or None, )
[docs] def to_api_payload(self) -> dict[str, str]: """Convert to PascalCase dict for add/set API calls.""" payload: dict[str, str] = {"Name": self.name} if self.id is not None: payload["ID"] = self.id if self.phone is not None: payload["Phone"] = self.phone if self.group is not None: payload["Group"] = self.group return payload