""" Module for basic object for nodes. Node object is an interface class and should be derived by other objects like window openers and roller shutters. """ from typing import TYPE_CHECKING, Any, Awaitable, Callable, List from .api import SetNodeName, WinkSend from .const import OperatingState, RunStatus, StatusReply, WinkTime from .exception import PyVLXException if TYPE_CHECKING: from pyvlx import PyVLX CallbackType = Callable[["Node"], Awaitable[None]] class Node: """Class for node abstraction.""" def __init__(self, pyvlx: "PyVLX", node_id: int, name: str, serial_number: str | None): """Initialize Node object.""" self.pyvlx = pyvlx self.node_id = node_id self.name = name self.serial_number = serial_number self.last_frame_state: OperatingState | None = None self.last_frame_status_reply: StatusReply | None = None self.last_frame_run_status: RunStatus | None = None self.device_updated_cbs: List[CallbackType] = [] self.pyvlx.connection.register_connection_opened_cb(self.after_update) self.pyvlx.connection.register_connection_closed_cb(self.after_update) self._disposed = False def dispose(self) -> None: """Unregister callbacks and clear local subscriptions.""" if self._disposed: return self.pyvlx.connection.unregister_connection_opened_cb(self.after_update) self.pyvlx.connection.unregister_connection_closed_cb(self.after_update) self.device_updated_cbs.clear() self._disposed = True def register_device_updated_cb(self, device_updated_cb: CallbackType) -> None: """Register device updated callback.""" self.device_updated_cbs.append(device_updated_cb) def unregister_device_updated_cb(self, device_updated_cb: CallbackType) -> None: """Unregister device updated callback.""" self.device_updated_cbs.remove(device_updated_cb) async def after_update(self) -> None: """Execute callbacks after internal state has been changed.""" for device_updated_cb in self.device_updated_cbs: await device_updated_cb(self) def represents_same_node(self, other: Any) -> bool: """Return True if both objects represent the same physical node. Matching requires the same concrete node class. Identity is then resolved by serial number when both nodes have one. If exactly one node has a serial number, they are considered different. If both serial numbers are missing, node_id is used as fallback. This intentionally differs from __eq__, which compares full object state. Identity matching is used to decide whether two node instances can be treated as the same device (registered in the gateway) across reloads, even when runtime state (positions, frame history, callbacks) differs. """ if not isinstance(other, Node): return False if type(self) is not type(other): return False if self.serial_number and other.serial_number: return self.serial_number == other.serial_number if self.serial_number or other.serial_number: return False return self.node_id == other.node_id async def rename(self, name: str) -> None: """Change name of node.""" set_node_name = SetNodeName(pyvlx=self.pyvlx, node_id=self.node_id, name=name) await set_node_name.do_api_call() if not set_node_name.success: raise PyVLXException("Unable to rename node") self.name = name async def wink(self, wink_time: WinkTime = WinkTime.BY_MANUFACTURER, wait_for_completion: bool = True) -> None: """Identify node by making it wink.""" wink_send = WinkSend( pyvlx=self.pyvlx, node_id=self.node_id, wink_time=wink_time, wait_for_completion=wait_for_completion, ) await wink_send.send() @property def is_available(self) -> bool: """Return True if node is available.""" return self.pyvlx.get_connected() def __str__(self) -> str: """Return object as readable string.""" return ( f'<{type(self).__name__} name="{self.name}" ' f'node_id="{self.node_id}" ' f'serial_number="{self.serial_number}" ' f'last_frame_state="{self.last_frame_state}" ' f'last_frame_status_reply="{self.last_frame_status_reply}"/>' ) def __eq__(self, other: Any) -> bool: """Equal operator.""" return ( type(self).__name__ == type(other).__name__ and self.__dict__ == other.__dict__ )