"""Module for interacting with a JVC Projector.""" from __future__ import annotations import logging from typing import TYPE_CHECKING, Any, Final from . import command from .command.base import LIMP_MODE, Command from .command.command import SPECIFICATIONS from .device import Device from .error import JvcProjectorError if TYPE_CHECKING: from .command.command import Spec _LOGGER = logging.getLogger(__name__) DEFAULT_PORT: Final = 20554 DEFAULT_TIMEOUT: Final = 2.0 class JvcProjector: """Class for interacting with a JVC Projector.""" def __init__( self, host: str, *, port: int = DEFAULT_PORT, timeout: float = DEFAULT_TIMEOUT, password: str | None = None, ) -> None: """Initialize instance of class.""" self._host = host self._port = port self._timeout = timeout self._password = password self._device: Device | None = None self._spec: Spec = LIMP_MODE self._model: str | None = None @property def host(self) -> str: """Returns IP address.""" return self._host @property def port(self) -> int: """Returns ip port.""" return self._port @property def model(self) -> str: """Returns model name.""" if self._model is None: raise JvcProjectorError("Model not initialized") return self._model @property def spec(self) -> str: """Returns specification.""" if not self._device: raise JvcProjectorError("Not connected") spec = self._spec.name if not self._spec.limp_mode and self._spec.model.name != self._model: spec += f"-{self._spec.model.name}" return spec async def connect(self, *, model: str | None = None) -> None: """Initialize communication with the projector.""" if self._device: return self._device = Device(self._host, self._port, self._timeout, self._password) self._model = model if model else await self.get(command.ModelName) for spec in SPECIFICATIONS: if spec.matches_model(self._model): self._spec = spec break if self._spec.limp_mode: for spec in SPECIFICATIONS: if spec.matches_prefix(self._model): msg = "Unknown model %s detected; defaulting %s (%s)" _LOGGER.warning(msg, self._model, spec.model.name, spec.name) self._spec = spec break if self._spec.limp_mode: _LOGGER.warning( "Unknown model %s detected; entering limp mode", self._model ) async def disconnect(self) -> None: """Disconnect from the projector.""" if self._device: await self._device.disconnect() self._device = None self._model = None self._spec = LIMP_MODE Command.unload() _LOGGER.debug("Disconnected from projector") async def get(self, name: str | type[Command]) -> str: """Get a projector parameter value (reference command).""" return str(await self._send(name)) async def set(self, name: str | type[Command], value: Any = None) -> None: """Set a projector parameter value (operation command).""" await self._send(name, value) async def remote(self, value: Any = None) -> None: """Send a projector remote command.""" await self.set(command.Remote, value) async def _send(self, name: str | type[Command], value: Any = None) -> str | None: """Send a command to the projector.""" if not self._device: raise JvcProjectorError("Not connected") cls = Command.lookup(name) if isinstance(name, str) else name if cls is None: raise JvcProjectorError(f"Command {name} not implemented") cmd = cls(self._spec) if not cmd.supports(self._spec): raise JvcProjectorError( f"Command {cmd.name} ({cmd.code}) not supported by this model" ) if value is None: if not cmd.reference: raise JvcProjectorError( f"Invalid attempt to read from non-reference command {cmd.name} ({cmd.code})" ) else: if not cmd.operation: raise JvcProjectorError( f"Invalid attempt to write to non-operation command {cmd.name} ({cmd.code})" ) cmd.op_value = str(value) await self._device.send(cmd) return cmd.ref_value def supports(self, name: str | type[Command]) -> bool: """Check if a command is supported by the projector.""" if not self._device: raise JvcProjectorError("Not connected") cls = Command.lookup(name) if isinstance(name, str) else name if cls is None: raise JvcProjectorError(f"Command {name} not implemented") return cls.supports(self._spec) def describe(self, name: str | type[Command]) -> dict[str, Any]: """Return a command description.""" if not self._device: raise JvcProjectorError("Not connected") cls = Command.lookup(name) if isinstance(name, str) else name if cls is None: raise JvcProjectorError(f"Command {name} not implemented") if cls.supports(self._spec): return cls.describe() raise JvcProjectorError( f"Command {cls.name} ({cls.code}) not supported by this model" ) def capabilities(self) -> dict[str, Any]: """Return the supported command list.""" if not self._device: raise JvcProjectorError("Not connected") commands: dict[str, Any] = {} for cls in Command.registry["name"].values(): if cls.supports(self._spec): commands[cls.name] = cls.describe() return commands