pax_global_header00006660000000000000000000000064151321667330014521gustar00rootroot0000000000000052 comment=ec0c62d25d06e9dc76438ef4132164bfa1dcd2ba pyjvcprojector-2.0.1/000077500000000000000000000000001513216673300146045ustar00rootroot00000000000000pyjvcprojector-2.0.1/.github/000077500000000000000000000000001513216673300161445ustar00rootroot00000000000000pyjvcprojector-2.0.1/.github/workflows/000077500000000000000000000000001513216673300202015ustar00rootroot00000000000000pyjvcprojector-2.0.1/.github/workflows/ci.yml000066400000000000000000000015271513216673300213240ustar00rootroot00000000000000name: CI on: push: branches: [main] pull_request: ~ permissions: contents: read jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: [ "3.10", "3.11", "3.12", "3.13" ] steps: - name: Checkout repository uses: actions/checkout@v4 - name: "Set up Python ${{ matrix.python-version }}" uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install ".[dev]" - name: Lint with ruff run: | ruff check jvcprojector tests - name: Type check with mypy run: | mypy jvcprojector tests - name: Run tests run: | pytest -q pyjvcprojector-2.0.1/.gitignore000066400000000000000000000040221513216673300165720ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # IDE .idea/ .vscode TODO.md pyjvcprojector-2.0.1/LICENSE000066400000000000000000000020551513216673300156130ustar00rootroot00000000000000MIT License Copyright (c) 2021 Steve Easley Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pyjvcprojector-2.0.1/MIGRATION.md000066400000000000000000000043101513216673300164550ustar00rootroot00000000000000## Version 2.0 Breaking Changes Version 2.0 introduces significant API changes that are **not backwards compatible** with 1.x versions. The library has been completely redesigned for better maintainability and extensibility. ### What Changed **Old API (1.x):** ```python jp = JvcProjector("192.168.1.100") await jp.connect() # Helper methods await jp.power_on() state = await jp.get_power() # Direct protocol commands await jp.ref("PMPM") # Read command await jp.op("PMPM01") # Write command # Constants from separate module from jvcprojector import const await jp.remote(const.REMOTE_INFO) ``` **New API (2.0):** ```python jp = JvcProjector("192.168.1.100") await jp.connect() # Unified get/set interface with command classes from jvcprojector import command state = await jp.get(command.Power) await jp.set(command.Power, command.Power.ON) # Commands are self-documenting with constants await jp.remote(command.Remote.INFO) await jp.set(command.PictureMode, command.PictureMode.FILM) # Discover capabilities if jp.supports(command.LensMemory): await jp.set(command.LensMemory, "1") ``` ### Migration Guide | 1.x | 2.0 | |-----|-----| | `await jp.power_on()` | `await jp.set(command.Power, command.Power.ON)` | | `await jp.power_off()` | `await jp.set(command.Power, command.Power.OFF)` | | `await jp.get_power()` | `await jp.get(command.Power)` | | `await jp.get_input()` | `await jp.get(command.Input)` | | `await jp.get_signal()` | `await jp.get(command.SignalStatus)` | | `await jp.get_state()` | `jp.info()` (not async) | | `await jp.ref("PMPM")` | `await jp.get(command.PictureMode)` | | `await jp.op("PMPM01")` | `await jp.set(command.PictureMode, command.PictureMode.FILM)` | | `const.REMOTE_INFO` | `command.Remote.INFO` | | `const.ON` | `command.Power.ON` | ### Why the Change? - **Type Safety**: Command classes provide better IDE autocomplete and type checking - **Self-Documenting**: Commands include their own value constants and descriptions - **Extensibility**: Easy to add new commands and model-specific features - **Discoverability**: Use `capabilities()`, `supports()`, and `describe()` to explore available commands - **Consistency**: Single `get`/`set` interface replaces multiple helper methods pyjvcprojector-2.0.1/Makefile000066400000000000000000000004561513216673300162510ustar00rootroot00000000000000all: format check test format: ruff format jvcprojector tests check: mypy jvcprojector tests ruff check jvcprojector tests --fix pylint jvcprojector tests test: pytest build: clean python3 -m build python -m twine upload -u __token__ dist/* clean: rm -rf dist build pyjvcprojector.egg-infopyjvcprojector-2.0.1/README.md000066400000000000000000000211231513216673300160620ustar00rootroot00000000000000# pyjvcprojector [![Test](https://github.com/SteveEasley/pyjvcprojector/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/SteveEasley/pyjvcprojector/actions/workflows/ci.yml) A Python client library for controlling JVC Projectors over a network connection. ## Features - **Async/await support** - Built with asyncio for non-blocking operations - **Network-based control** - Connects to JVC projectors over TCP - **Command system** - Get/set projector parameters and send remote control commands - **Model detection** - Automatically detects projector model and adjusts capabilities - **Password support** - Optional password authentication - **Command discovery** - Check supported commands and capabilities - **CLI tool** - Command-line interface included ## ⚠️ Version 2.0 Breaking Changes Version 2.0 introduces significant API changes that are **not backwards compatible** with 1.x versions. The library has been completely redesigned for better maintainability and extensibility. See the [Migration Guide](MIGRATION.md) for details. ## Installation ```bash python -m pip install pyjvcprojector ``` ## Requirements - Python 3.10 or higher ## Quickstart ```python import asyncio from jvcprojector import JvcProjector, command async def main(): # Create projector instance jp = JvcProjector("{ip}") await jp.connect() # Get projector info print(f"Model: {jp.model}") # Get current power state power_state = await jp.get(command.Power) print(f"Power state: {power_state}") # Turn projector on if power_state == command.Power.STANDBY: await jp.set(command.Power, command.Power.ON) # Using the remote method to send remote control commands await jp.remote(command.Remote.UP) # Or use the more powerful get/set reference/operation method current_input = await jp.get(command.Input) print(f"Current input: {current_input}") # Disconnect await jp.disconnect() asyncio.run(main()) ``` ## Usage ### Creating a Connection ```python from jvcprojector import JvcProjector # Basic connection jp = JvcProjector("{ip}") # With custom port and timeout jp = JvcProjector("{ip}", port=20554, timeout=5.0) # With password authentication jp = JvcProjector("{ip}", password="{password}") # Connect to projector await jp.connect() ``` ### Getting and Setting Parameters ```python from jvcprojector import command # Get a parameter value (reference command) power_state = await jp.get(command.Power) input_mode = await jp.get(command.Input) picture_mode = await jp.get(command.PictureMode) # Set a parameter value (operation command) await jp.set(command.Power, command.Power.ON) await jp.set(command.Input, command.Input.HDMI1) await jp.set(command.PictureMode, command.PictureMode.CINEMA) ``` ### Sending Remote Commands ```python from jvcprojector import command # Send remote control button presses await jp.remote(command.Remote.MENU) await jp.remote(command.Remote.UP) await jp.remote(command.Remote.OK) await jp.remote(command.Remote.BACK) ``` ### Discovering Capabilities ```python # Check if a command is supported import command if jp.supports(command.InstallationMode): await jp.set(command.LensMemory, "memory-1") # Get description of a command description = jp.describe(command.Power) print(description) # Get all supported commands capabilities = jp.capabilities() for cmd_name, cmd_info in capabilities.items(): print(f"{cmd_name}: {cmd_info}") # Get projector information info = jp.info() print(info) # {'ip': '192.168.1.100', 'model': 'NZ8', 'spec': '...'} ``` ## API Reference ### JvcProjector **Constructor:** ```python JvcProjector(host, port=20554, timeout=2.0, password=None) ``` **Methods:** - `await connect(model=None)` - Initialize connection to the projector - `await disconnect()` - Close connection to the projector - `await get(command)` - Get a projector parameter value (reference command) - `await set(command, value)` - Set a projector parameter value (operation command) - `await remote(value)` - Send a remote control command - `capabilities()` - Get all supported commands for current projector model - `supports(command)` - Check if a command is supported by the projector model - `describe(command)` - Get description of a command - `info()` - Get projector information (IP, model, spec) **Properties:** - `host` - IP address - `port` - TCP port (default: 20554) - `ip` - Resolved IP address (available after connect) - `model` - Projector model name (available after connect) - `spec` - Projector specification (available after connect) ## Command-Line Interface The library includes a CLI tool: ```bash % jvcprojector --help Usage: jvcprojector <-h|--host HOST> [-p|--password PASSWORD] [args...] Commands: list List all available commands describe Describe a command get Get value of a command set Set value of a command listen Listen for events Options: -h, --host HOST Projector hostname or IP address -p, --password PASS Projector password (if required) -m, --model MODEL Model override (e.g. B8B1) -v, --verbose Enable verbose logging ``` ## Development ```bash # Clone the repository git clone https://github.com/SteveEasley/pyjvcprojector.git cd pyjvcprojector # Install development dependencies pip install -e ".[dev]" # Run tests pytest # Run type checking mypy jvcprojector # Run linting ruff check . ``` ### Adding New Commands The library uses a command system defined in `jvcprojector/command/command.py`. This file contains: 1. **Specifications and Models** - Defines which projector models support which command sets 2. **Command Classes** - Individual command implementations (55+ commands including Power, Input, PictureMode, etc.) #### Command Structure Each command class inherits from `Command` and defines: ```python class Power(Command): """Power command.""" code = "PW" # JVC protocol command code reference = True # Supports reading (get) operation = True # Supports writing (set) limp_mode = True # Available in limp mode (unknown models) # Constants for command values OFF = "off" ON = "on" STANDBY = "standby" COOLING = "cooling" WARMING = "warming" # Parameter mapping between JVC codes and human-readable values parameter = MapParameter( size=1, read={"0": STANDBY, "1": ON, "2": COOLING, "3": WARMING}, write={"0": OFF, "1": ON}, ) ``` #### Model-Specific Commands Some commands are only available on certain models. Use conditional parameters: ```python class SomeCommand(Command): code = "XX" reference = True operation = True # Different parameters for different specifications parameter = { CS20241: MapParameter(size=1, readwrite={"0": "value1", "1": "value2"}), (CS20221, CS20191): MapParameter(size=1, readwrite={"0": "value1"}), } ``` #### Parameter Types - **`MapParameter`** - Maps JVC protocol values to human-readable strings - `size` - Expected response size in characters - `read` - Mapping for reference (get) operations - `write` - Mapping for operation (set) operations - `readwrite` - Shorthand when read/write mappings are identical - **`ModelParameter`** - Parses model names - **`MacAddressParameter`** - Formats MAC addresses - **`VersionParameter`** - Handles version strings - **`LaserPowerParameter`** - Converts laser power (hex to percentage) - **`LightTimeParameter`** - Converts light source time (hex to hours) #### Specifications The `SPECIFICATIONS` tuple at the top of `command.py` defines model families and their command support: ```python SPECIFICATIONS = ( CS20241 := Spec( "CS20241", B8A2 := Model("B8A2"), # RS3200, NZ800, etc. B8A1 := Model("B8A1"), # RS4200, NZ900, etc. ), # ... more specs ) ``` Models are matched in order: 1. Exact model name match 2. Prefix match (first 3 characters) 3. Falls back to "limp mode" with minimal command support #### Adding a New Command 1. Define the command class in `command.py` 2. Set the JVC protocol `code` 3. Mark as `reference` and/or `operation` 4. Define the `parameter` (use `MapParameter` for most cases) 5. Add human-readable constants for common values 6. Optionally specify model-specific support using spec keys 7. Run `python tools/update_imports.py` to update imports The command automatically registers itself and becomes available via `command.YourCommandName`. ## License This project is licensed under the terms specified in the `LICENSE` file included in this repository.pyjvcprojector-2.0.1/examples/000077500000000000000000000000001513216673300164225ustar00rootroot00000000000000pyjvcprojector-2.0.1/examples/__init__.py000066400000000000000000000000001513216673300205210ustar00rootroot00000000000000pyjvcprojector-2.0.1/examples/simple.py000066400000000000000000000023721513216673300202710ustar00rootroot00000000000000import asyncio import logging import sys from jvcprojector import JvcProjector, command logging.basicConfig(level=logging.WARNING) async def main(): jp = JvcProjector(sys.argv[1]) await jp.connect() print("Projector model info:") print({ "model": jp.model, "spec": jp.spec, }) if await jp.get(command.Power) == command.Power.STANDBY: print("Turning projector on...") await jp.set(command.Power, command.Power.ON) await asyncio.sleep(1) if await jp.get(command.Power) == command.Power.WARMING: print("Waiting for projector to warmup...") while await jp.get(command.Power) != command.Power.ON: await asyncio.sleep(3) elif await jp.get(command.Power) == command.Power.COOLING: print("Run command after projector has cooled down") sys.exit(0) # Example of sending remote codes print("Showing info on screen") await jp.remote(command.Remote.INFO) await asyncio.sleep(5) print("Hiding info on screen") await jp.remote(command.Remote.BACK) # Example of reference command print("Current projector input:") print(await jp.get(command.Input)) await jp.disconnect() if __name__ == "__main__": asyncio.run(main()) pyjvcprojector-2.0.1/jvcprojector/000077500000000000000000000000001513216673300173165ustar00rootroot00000000000000pyjvcprojector-2.0.1/jvcprojector/__init__.py000066400000000000000000000006631513216673300214340ustar00rootroot00000000000000"""A python library for controlling a JVC Projector over a network connection.""" # ruff: noqa: F401 from .command.base import Command from .error import ( JvcProjectorAuthError, JvcProjectorError, JvcProjectorTimeoutError, ) from .projector import JvcProjector __all__ = [ "JvcProjector", "JvcProjectorError", "JvcProjectorTimeoutError", "JvcProjectorAuthError", "Command", ] __version__ = "2.0.1" pyjvcprojector-2.0.1/jvcprojector/cli.py000066400000000000000000000247771513216673300204600ustar00rootroot00000000000000"""Command-line interface for JVC Projector control.""" from __future__ import annotations import asyncio import logging from os.path import basename import sys from time import time from typing import Any from .command import command from .command.base import CATEGORIES, Command from .error import JvcProjectorError, JvcProjectorTimeoutError from .projector import JvcProjector _LOGGER = logging.getLogger(__name__) class State: """Represents the current state of the projector.""" def __init__(self): """Initialize instance of class.""" self.data: dict[str, str] = {} self._preserve = (command.Power.name, command.Signal.name, command.Input.name) def __getitem__(self, cmd: type[Command]) -> str | None: return self.data.get(cmd.name) def __setitem__(self, cmd: type[Command], value): self.data[cmd.name] = value def update(self, state: "State") -> dict[str, str]: """Update current state from new state.""" changed: dict[str, str] = {} for key, val in state.data.items(): if val is not None: self.data[key] = val changed[key] = val return changed def reset(self) -> None: """Reset current state.""" for key in list(self.data.keys()): if key not in self._preserve: del self.data[key] async def cmd_list(jp: JvcProjector) -> None: """List all available commands.""" commands: dict[str, list[tuple[str, str, str, str]]] = {} all_commands = jp.capabilities() for category in CATEGORIES + ["Other"]: commands[category] = [] for name, cmd in all_commands.items(): if cmd["category"] == category: reference = " ✓" if cmd["reference"] else " ✗" operation = " ✓" if cmd["operation"] else " ✗" commands[category].append( (name, reference, operation, str(cmd["code"])) ) commands[category].sort(key=lambda x: x[0]) print(f"{'Command':<30} {'Read(ref)':<10} {'Write(op)':<10} {'Code':<4}") print("-" * 57) for category in CATEGORIES + ["Other"]: if not commands[category]: continue print(f"[{category}]") for name, reference, operation, code in commands[category]: print(f"{name:<30} {reference:<10} {operation:<10} {code:<4}") print() async def cmd_describe(jp: JvcProjector, name: str) -> None: """Show help for a specific command.""" try: cmd = jp.describe(name) except JvcProjectorError as e: die(f"{e}") code: str = str(cmd["code"]) operation: bool = bool(cmd["operation"]) parameter = cmd["parameter"] print(f"Command: {cmd['name']} ({code})") print(f"Writable: {'yes' if operation else 'no'}") if isinstance(parameter, dict): read: dict[str, str] = parameter["read"] write: dict[str, str] = parameter["write"] readwrite = operation and (write == read) print("Read" + ("/Write" if readwrite else "") + " Value:") for key, value in sorted(read.items()): print(f" {value:<18} ({key})") if operation and not readwrite: print("Write Value:") for key, value in sorted(write.items()): print(f" {value:<18} ({key})") else: print(f"Value: {parameter}") async def cmd_get(jp: JvcProjector, name: str) -> None: """Get command value from the projector.""" try: print(await jp.get(name)) except JvcProjectorError as e: die(f"{e}") async def cmd_set(jp: JvcProjector, name: str, value: str) -> None: """Set a command value on the projector.""" if name not in Command.registry["name"]: die(f"Unknown command {name}") try: await jp.set(name, value) except JvcProjectorError as e: die(f"{e}") else: print("success") async def cmd_listen(jp: JvcProjector) -> None: """Listen for events from the projector.""" state = State() next_full_sync = 0.0 retries = 0 async def update(_cmd: type[Command], _new_state: State) -> str | None: """Helper function to return a reference command value.""" nonlocal next_full_sync if not jp.supports(_cmd): return None value = await jp.get(_cmd) if value != state[_cmd]: _new_state[_cmd] = value next_full_sync = 0.0 return value while True: try: new_state = State() power = await update(command.Power, new_state) if power == command.Power.ON: await update(command.Input, new_state) signal = await update(command.Signal, new_state) if signal == command.Signal.SIGNAL: hdr = await update(command.Hdr, new_state) await update(command.Source, new_state) await update(command.ColorDepth, new_state) await update(command.ColorSpace, new_state) await update(command.InstallationMode, new_state) if next_full_sync <= time(): if hdr and hdr not in (command.Hdr.NONE, command.Hdr.SDR): await update(command.HdrProcessing, new_state) await update(command.PictureMode, new_state) await update(command.ColorProfile, new_state) await update(command.GraphicMode, new_state) await update(command.EShift, new_state) await update(command.Anamorphic, new_state) await update(command.MotionEnhance, new_state) await update(command.LaserPower, new_state) await update(command.LowLatencyMode, new_state) await update(command.LightTime, new_state) next_full_sync = time() + 6 else: if state[command.Signal] != command.Signal.NONE: # Infer signal state new_state[command.Signal] = command.Signal.NONE state.reset() if changed := state.update(new_state): print(changed) retries = 0 await asyncio.sleep(2) except JvcProjectorTimeoutError as e: # Timeouts are expected when the projector loses signal and ignores commands. retries += 1 if retries > 1: file = basename(__file__) line = e.__traceback__.tb_lineno if e.__traceback__ else 0 _LOGGER.warning( "Retrying listener sync due to: %s (%s:%d)", e, file, line ) await asyncio.sleep(1) except JvcProjectorError as e: retries += 1 file = basename(__file__) line = e.__traceback__.tb_lineno if e.__traceback__ else 0 _LOGGER.error("Failed listener sync due to:...%s (%s:%d)", e, file, line) await asyncio.sleep(15) def print_usage() -> None: """Print usage information.""" print( "Usage: jvcprojector <-h|--host HOST> [-p|--password PASSWORD] [args...]" ) print() print("Commands:") print(" list List all available commands") print(" describe Describe a command") print(" get Get value of a command") print(" set Set value of a command") print(" listen Listen for events") print() print("Options:") print(" -h, --host HOST Projector IP address") print(" -p, --password PASS Projector password (if required)") print(" -m, --model MODEL Model override (e.g. B8B1)") print(" -v, --verbose Enable verbose logging") def die(msg: str) -> None: """Print error message and exit.""" print(f"Error: {msg}") sys.exit(1) def parse_args() -> dict[str, Any]: """Parse command-line arguments.""" args = sys.argv[1:] if not args: print_usage() sys.exit(1) result: dict[str, Any] = { "host": "", "password": "", "model": "", "verbose": False, "action": "", "args": [], } i = 0 while i < len(args): arg = args[i] if arg in ("-h", "--host"): if i + 1 >= len(args): die(f"{arg} argument is required") result["host"] = args[i + 1] i += 2 elif arg in ("-p", "--password"): if i + 1 >= len(args): die(f"{arg} argument is required") result["password"] = args[i + 1] i += 2 elif arg in ("-m", "--model"): result["model"] = args[i + 1] i += 2 elif arg in ("-v", "--verbose"): result["verbose"] = True i += 1 elif arg.startswith("-"): die(f"Unknown option '{arg}'") print_usage() sys.exit(1) elif not result["action"]: result["action"] = arg i += 1 else: result["args"].append(arg) i += 1 return result async def main() -> None: """CLI entry point.""" parsed = parse_args() logging.basicConfig(level=logging.DEBUG if parsed["verbose"] else logging.WARNING) host = parsed["host"] password = parsed["password"] model = parsed["model"] action = parsed["action"] args = parsed["args"] jp = JvcProjector(host, password=password) if host: try: await jp.connect(**({"model": model} if model else {})) except JvcProjectorError as e: die(f"{e}") if action in ("list", "describe", "listen"): print(f"Detected model: {jp.model} ({jp.spec})") if action == "list": await cmd_list(jp) elif action == "describe": if not args: die("Usage: jvcprojector describe ") await cmd_describe(jp, args[0]) elif action == "get": if not args: die("Usage: jvcprojector -h get ") await cmd_get(jp, args[0]) elif action == "set": if len(args) < 2: die("Usage: jvcprojector -h set ") await cmd_set(jp, args[0], args[1]) elif action == "listen": await cmd_listen(jp) else: print_usage() sys.exit(1) def cli_entrypoint() -> None: """Synchronous entry point for CLI.""" asyncio.run(main()) pyjvcprojector-2.0.1/jvcprojector/command/000077500000000000000000000000001513216673300207345ustar00rootroot00000000000000pyjvcprojector-2.0.1/jvcprojector/command/__init__.py000066400000000000000000000022011513216673300230400ustar00rootroot00000000000000""" Projector command definitions To update this file with the full list of command classes automatically, run: `python tools/update_imports.py` """ # ruff: noqa: F401 from .command import ( Anamorphic, Aspect, ClearMotionDrive, ColorDepth, Colorimetry, ColorManagement, ColorProfile, ColorSpace, ColorTemperature, ColorTemperatureCorrection, ContentType, DeepBlack, DscMode, DynamicControl, EcoMode, EShift, FilmmakerMode, GraphicMode, Hdmi2D3D, HdmiColorSpace, HdmiInputLevel, Hdr, HdrLevel, HdrProcessing, HideEco, HighlightColor, Input, InstallationMode, IntelligentLensAperture, LaserPower, LightPower, LightTime, LinkRate, LowLatencyAutoMode, LowLatencyMode, MacAddress, Mask, ModelName, MotionEnhance, OffTimer, PictureMode, PictureModeHdr10, PictureModeHlg, PictureModeSdr, PictureModeSdr3d, Power, Remote, Signal, Smoother, Source, TheaterOptimizer, TheaterOptimizerLevel, TheaterOptimizerProcessing, Trigger, Version, ) pyjvcprojector-2.0.1/jvcprojector/command/base.py000066400000000000000000000266621513216673300222340ustar00rootroot00000000000000"""Module for representing a JVC Projector command.""" # pylint: disable=W0223 from __future__ import annotations import logging import re from typing import Any from ..error import JvcProjectorError _LOGGER = logging.getLogger(__name__) _CATEGORY_MATCHER = { "PW|IP|SC|RC|IFLT|MD|LSMA|IFSV": "System", "IF|IS": "Signal", "PM": "Picture", "IN": "Installation", "FU": "Function", } CATEGORIES = list(_CATEGORY_MATCHER.values()) class Command: """Class for representing a JVC Projector command.""" name: str code: str category: str = "Other" reference: bool = False operation: bool = False limp_mode: bool = False parameter: Parameter | dict[Spec | tuple[Spec, ...], Parameter] depends: dict[type[Command], str | tuple[str, ...]] = {} operation_timeout: float | None = None _parameter: Parameter | None = None registry: dict[str, dict[str, type[Command]]] = { "name": {}, "code": {}, } def __init_subclass__(cls, **kwargs): """Initialize subclass.""" super().__init_subclass__(**kwargs) cls.name = cls.__name__ cls.registry["name"][cls.name] = cls cls.registry["code"][cls.code] = cls for match, name in _CATEGORY_MATCHER.items(): pattern = re.compile(f"^({match})") if re.search(pattern, cls.code): cls.category = name break for k, v in cls.depends.items(): if isinstance(v, str): cls.depends[k] = (v,) @classmethod def lookup(cls, name: str) -> type[Command] | None: """Get a command class by name or code.""" assert cls is Command if name in Command.registry["name"]: return Command.registry["name"][name] if name in Command.registry["code"]: return Command.registry["code"][name] return None @classmethod def supports(cls, spec: Spec) -> bool: """Return if the current model supports the command.""" if cls._parameter is None: cls._resolve(spec) assert cls._parameter return cls._parameter.supported() @classmethod def describe(cls) -> dict[str, Any]: """Describe command.""" assert cls._parameter return { "name": cls.name, "code": cls.code, "reference": cls.reference, "operation": cls.operation, "category": cls.category, "parameter": cls._parameter.describe(), } @classmethod def _resolve(cls, spec: Spec) -> None: """Resolve the supported parameter for command.""" # Commands not supported by the current model default to an empty sentinel parameter. parameter = Parameter() if isinstance(cls.parameter, Parameter): # e.g. parameter = MapParameter() if not spec.limp_mode or cls.limp_mode: parameter = cls.parameter elif isinstance(cls.parameter, dict): for key, param in cls.parameter.items(): if isinstance(key, Spec): # e.g. parameter = {CS20241: MapParameter()} if key == spec: parameter = param break elif isinstance(key, tuple): # e.g. parameter = {(CS20241, CS20221): MapParameter()} if spec in key: parameter = param break parameter.resolve(spec) cls._parameter = parameter @classmethod def unload(cls) -> None: """Clear all resolved parameters for each registered command.""" assert cls is Command for cmd in cls.registry["name"].values(): # pylint: disable=protected-access if cmd._parameter: cmd._parameter.unload() cmd._parameter = None def __init__(self, spec: Spec): """Initialize instance of class.""" self.ack: bool = False self.is_ref: bool = True self.is_op: bool = False self._spec: Spec = spec self._op_value: str | None = None self._ref_value: str | None = None if not self.reference and not self.operation: raise RuntimeError( f"Command {self.name} ({self.code}) is neither reference nor operation" ) if self._parameter is None: self._resolve(spec) @property def ref_value(self) -> str | None: """Get response value.""" return self._ref_value @ref_value.setter def ref_value(self, value: str) -> None: """Set response value.""" assert self._parameter if self._parameter.size and len(value) != self._parameter.size: msg = "Command %s (%s) returned unexpected response size %s; expected %s" _LOGGER.warning(msg, self.name, self.code, len(value), self._parameter.size) self._ref_value = self._parameter.ref(self, value) @property def op_value(self) -> str: """Get operation command parameter value.""" return str(self._op_value) @op_value.setter def op_value(self, value: str) -> None: """Set operation command parameter value.""" assert self._parameter self._op_value = self._parameter.op(self, value) self.is_op = True self.is_ref = False class Parameter: """Base class for a command parameter.""" size: int = 0 def ref(self, cmd: Command, value: str) -> str: """Convert a native projector value to a human readable value.""" raise NotImplementedError def op(self, cmd: Command, value: str) -> str: """Convert a human readable value to a native projector value.""" raise NotImplementedError def supported(self) -> bool: """Return if the supported model supports this parameter.""" return type(self) is not Parameter # pylint: disable=unidiomatic-typecheck # pylint: disable=unused-argument def resolve(self, spec: Spec) -> None: """Resolve the matching parameter for the given model.""" return None def describe(self) -> str | dict[str, dict[str, str]]: """Return a descriptive representation of the parameter.""" return "" def unload(self) -> None: """Clear resolved parameter.""" return None class ModelParameter(Parameter): """Parameter for Model command.""" def ref(self, cmd: Command, value: str) -> str: return re.sub(r"ILAFPJ\W+", "", value) def describe(self) -> str: return "Model name (e.g. B2A2)" class MacAddressParameter(Parameter): """Parameter for MacAddress command.""" def ref(self, cmd: Command, value: str) -> str: return re.sub(r"-+", "-", value.replace(" ", "-")) def describe(self) -> str: return "Mac address (e.g. E0DADC0A1562)" class VersionParameter(Parameter): """Parameter for SoftwareVersion command.""" def ref(self, cmd: Command, value: str) -> str: return value def describe(self) -> str: return "Software version" class LaserPowerParameter(Parameter): """Parameter for the LaserPower command.""" def ref(self, cmd: Command, value: str) -> str: return str(round((int(value, 16) - 109) / 110, 2)) def op(self, cmd: Command, value: str) -> str: val = float(value) if val < 0.0 or val > 1.0: raise JvcProjectorError( f"Command {cmd.name} ({cmd.code}) returned an out of range value '{value}'" ) return f"{round(110 * val + 109):04X}" def describe(self) -> str: return "Laser power level % (0.0 - 1.0)" class LightTimeParameter(Parameter): """Parameter for LightSourceTime command.""" def ref(self, cmd: Command, value: str) -> str: return str(int(value, 16)) def describe(self) -> str: return "Light source time in hours" class MapParameter(Parameter): """Parameter for map commands.""" def __init__( self, size: int = 0, read: dict[str, str | tuple[str | Model, ...]] | None = None, write: dict[str, str | tuple[str | Model, ...]] | None = None, readwrite: dict[str, str | tuple[str | Model, ...]] | None = None, ): self._read: dict[str, str | tuple[str | Model, ...]] = {} self._write: dict[str, str | tuple[str | Model, ...]] = {} self._resolved_read: dict[str, str] = {} self._resolved_write: dict[str, str] = {} self.size = size if readwrite and (read or write): raise RuntimeError("Cannot specify both readwrite and read/write") if read: self._read = read elif readwrite: self._read = readwrite if write: self._write = write elif readwrite: self._write = readwrite def resolve(self, spec: Spec): for k, v in self._read.items(): if isinstance(v, str): self._resolved_read[k] = v elif isinstance(v, tuple) and not spec.limp_mode and spec.model in v[1:]: self._resolved_read[k] = str(v[0]) for k, v in self._write.items(): if isinstance(v, str): self._resolved_write[k] = v elif isinstance(v, tuple) and not spec.limp_mode and spec.model in v[1:]: self._resolved_write[k] = str(v[0]) def ref(self, cmd: Command, value: str) -> str: if value not in self._resolved_read: raise JvcProjectorError( f"Command {cmd.name} ({cmd.code}) received unmapped value '{value}' from projector" ) return self._resolved_read[value] def op(self, cmd: Command, value: str) -> str: for k, v in self._resolved_write.items(): if v == value: return k raise JvcProjectorError( f"Command {cmd.name} ({cmd.code}) received unmapped value '{value}' from user" ) def supported(self) -> bool: # A command is supported if it has at least one mapped value for the current model.""" return bool(self._resolved_read or self._resolved_write) def describe(self) -> dict[str, dict[str, str]]: return {"read": self._resolved_read, "write": self._resolved_write} def unload(self) -> None: self._resolved_read = {} self._resolved_write = {} class Model: """Represents a JVC projector model.""" def __init__(self, *args: str) -> None: self.name: str = args[0] if len(args) > 0 else "" self.names: list[str] = list(args) class Spec: """Represents a JVC command specification.""" def __init__(self, name: str, *args: Model) -> None: self.name = name self.models: list[Model] = list(args) self.model: Model = Model() def matches_model(self, name: str) -> bool: """Look up the model by name.""" for model in self.models: if name in model.names: self.model = model return True return False def matches_prefix(self, name: str) -> bool: """Look up the model by prefix.""" for model in self.models: for _name in model.names: if _name[0:3] == name[0:3]: self.model = model return True return False @property def limp_mode(self): """Return if the spec is for limp mode.""" return self is LIMP_MODE LIMP_MODE: Spec = Spec("UNKOWN") pyjvcprojector-2.0.1/jvcprojector/command/command.py000066400000000000000000002306121513216673300227300ustar00rootroot00000000000000"""Command definitions.""" # pylint: disable=line-too-long from .base import ( Command, LaserPowerParameter, LightTimeParameter, MacAddressParameter, MapParameter, Model, ModelParameter, Spec, VersionParameter, ) # SPECIFICATIONS: # * Spec name is the year of the model release followed by an incremental sequence number. # * Model name is really a model series/family since there is no way to know the exact model. # * Model can be a list of models when they could go by different names (like in CS20191) # * At runtime a projector model is matched to a spec in the order given below (e.g., B8A2, B8A1, B5A3, ...) # - First an exact match from top to bottom is done. If no match is found then: # - A partial match of the first 3 characters is done top to bottom. (e.g., B5A9 would match B5A3) # - If no match is found, the projector is considered to be in limp mode, with minimal command support. SPECIFICATIONS: tuple[Spec, ...] = ( CS20241 := Spec( "CS20241", B8A2 := Model("B8A2"), # RS3200, NZ800, N988, V800R B8A1 := Model("B8A1"), # RS4200, NZ900, N1188, V900R ), CS20242 := Spec( "CS20242", D8A2 := Model("D8A2"), # NZ500, RS1200, N799, N788, N700, Z5 D8A1 := Model("D8A1"), # NZ700, RS2200, N899, N888, N800, Z7 ), CS20221 := Spec( "CS20221", B5A3 := Model("B5A3"), # RS2100, NZ7, N88, V70R B5A2 := Model("B5A2"), # RS3100, NZ8, N98, V80R B5A1 := Model("B5A1"), # RS4100, NZ9, N11, V90R B5B1 := Model("B5B1"), # RS1100, NP5, N78, V50 ), CS20191 := Spec( "CS20191", B2A3 := Model("B2A3", "A2B3"), # RS1000, NX5, N5, N6, V5 B2A2 := Model("B2A2", "A2B2"), # RS2000, NX7, N7, N8, V7 B2A1 := Model("B2A1", "A2B1"), # RS3000, NX9, NX11, V9R ), CS20172 := Spec( "CS20172", A0A0 := Model("A0A0"), # Z1, RS4500 ), CS20171 := Spec( "CS20171", XHR1 := Model("XHR1"), # X570R, RS420 XHR3 := Model("XHR3"), # X770R, X970, X970R, RS520, RS620 ), CS20161 := Spec( "CS20161", XHP1 := Model("XHP1"), # X550R, X5000, XC5890R, RS400 XHP2 := Model("XHP2"), # XC6890, XC6890R XHP3 := Model("XHP3"), # X750R, X7000, XC7890R, RS500, X950R, X9000, RS600, PX1 ), CS20141 := Spec( "CS20141", XHK1 := Model("XHK1"), # X500R, XC5880R, RS49 XHK2 := Model("XHK2"), # RS4910 XHK3 := Model("XHK3"), # X700R, X7880R, XC7880R, RS57, X900R, RS67, RS6710 ), CS20131 := Spec( "CS20131", XHG1 := Model("XHG1"), # X35, XC3800, RS46, RS4810 XHH1 := Model("XHH1"), # X55R, XC5800R, RS48 XHH4 := Model("XHH4"), # X75R, XC7800R, RS56, X95R, XC9800R, RS66 ), CS20121 := Spec( "CS20121", XHE := Model("XHE"), # X30, XC388, RS45, RS4800 XHF := Model("XHF"), # X70R, XC788R, RS55, X90R, XC988R, RS65 ), ) class ModelName(Command): """Model command.""" code = "MD" reference = True operation = False limp_mode = True parameter = ModelParameter() class MacAddress(Command): """Mac Address command.""" code = "LSMA" reference = True operation = False limp_mode = True parameter = MacAddressParameter() class Version(Command): """Software Version command.""" code = "IFSV" reference = True operation = False limp_mode = True parameter = VersionParameter() class Power(Command): """Power command.""" code = "PW" reference = True operation = True limp_mode = True OFF = "off" ON = "on" STANDBY = "standby" COOLING = "cooling" WARMING = "warming" ERROR = "error" parameter = MapParameter( size=1, read={ "0": STANDBY, "1": ON, "2": COOLING, "3": WARMING, "4": ERROR, }, write={ "0": OFF, "1": ON, }, ) class Input(Command): """Input command.""" code = "IP" reference = True operation = True operation_timeout = 10.0 limp_mode = True HDMI1 = "hdmi1" HDMI2 = "hdmi2" COMP = "comp" PC = "pc" parameter = MapParameter( size=1, readwrite={ "6": HDMI1, "7": HDMI2, }, ) class Signal(Command): """Signal command.""" code = "SC" reference = True operation = False limp_mode = True NONE = "none" SIGNAL = "signal" parameter = MapParameter( size=1, read={ "0": NONE, "1": SIGNAL, }, ) class Remote(Command): """Remote command.""" code = "RC" reference = False operation = True limp_mode = True OK = "ok" MENU = "menu" BACK = "back" HIDE = "hide" UP = "up" DOWN = "down" LEFT = "left" RIGHT = "right" INFO = "info" INPUT = "input" HDMI1 = "hdmi1" HDMI2 = "hdmi2" SETTING_MEMORY = "setting-memory" LENS_CONTROL = "lens-control" PICTURE_MODE = "picture-mode" COLOR_PROFILE = "color-profile" GAMMA_SETTINGS = "gamma-settings" CMD = "cmd" MPC = "mpc" ADVANCED_MENU = "advanced-menu" MODE_1 = "mode-1" MODE_2 = "mode-2" MODE_3 = "mode-3" MODE_4 = "mode-4" MODE_5 = "mode-5" MODE_6 = "mode-6" MODE_7 = "mode-7" MODE_8 = "mode-8" MODE_9 = "mode-9" MODE_10 = "mode-10" LENS_APERTURE = "lens-aperture" PICTURE_ADJUST = "picture-adjust" ANAMORPHIC = "anamorphic" GAMMA = "gamma" COLOR_TEMP = "color-temp" V3D_FORMAT = "3d-format" NATURAL = "natural" CINEMA = "cinema" parameter = MapParameter( size=4, write={ "732F": OK, "732E": MENU, "7303": BACK, "731D": HIDE, "7301": UP, "7302": DOWN, "7336": LEFT, "7334": RIGHT, "7374": INFO, "7308": INPUT, "7370": HDMI1, "7371": HDMI2, "73D4": SETTING_MEMORY, "7330": LENS_CONTROL, "73F4": PICTURE_MODE, "7388": COLOR_PROFILE, "73F5": GAMMA_SETTINGS, "738A": CMD, "73F0": MPC, "7373": ADVANCED_MENU, "73D8": MODE_1, "73D9": MODE_2, "73DA": MODE_3, "73E5": MODE_4, "73E6": MODE_5, "73E7": MODE_6, "73E8": MODE_7, "73E9": MODE_8, "73EA": MODE_9, "73EB": MODE_10, "7320": LENS_APERTURE, "7372": PICTURE_ADJUST, "73C5": ANAMORPHIC, "7375": GAMMA, "7376": COLOR_TEMP, "73D6": V3D_FORMAT, "736A": NATURAL, "7368": CINEMA, }, ) class PictureMode(Command): """Picture Mode command.""" code = "PMPM" reference = True operation = True ANIMATION = "animation" CINEMA = "cinema" FILM = "film" FILMMAKER_MODE = "filmmaker-mode" FRAME_ADAPT_HDR = "frame-adapt-hdr" FRAME_ADAPT_HDR2 = "frame-adapt-hdr2" FRAME_ADAPT_HDR3 = "frame-adapt-hdr3" HDR = "hdr" HDR1 = "hdr1" HDR2 = "hdr2" HDR10 = "hdr10" HDR10_LL = "hdr10-ll" HDR10_PLUS = "hdr10+" HLG = "hlg" HLG_LL = "hlg-ll" ISF_DAY = "isf-day" ISF_NIGHT = "isf-night" NATURAL = "natural" NATURAL_LL = "natural-ll" PANA_PQ = "pana-pq" PHOTO = "photo" RESERVED = "reserved" SDR_1 = "sdr-1" SDR_2 = "sdr-2" STAGE = "stage" THX = "thx" THX_BRIGHT = "thx-bright" THX_DARK = "thx-dark" USER_1 = "user-1" USER_2 = "user-2" USER_3 = "user-3" USER_4 = "user-4" USER_5 = "user-5" USER_6 = "user-6" VIVID = "vivid" THREE_D = "3d" R4K = "4k" parameter = { CS20241: MapParameter( size=2, readwrite={ "00": FILM, "01": CINEMA, "03": NATURAL, "04": HDR10, "0B": FRAME_ADAPT_HDR, "0C": USER_1, "0D": USER_2, "0E": USER_3, "0F": USER_4, "10": USER_5, "11": USER_6, "14": HLG, "15": HDR10_PLUS, "17": FILMMAKER_MODE, "18": FRAME_ADAPT_HDR2, "19": FRAME_ADAPT_HDR3, "1B": VIVID, "1C": NATURAL_LL, "1D": HDR10_LL, "1E": HLG_LL, }, ), CS20242: MapParameter( size=2, readwrite={ "01": CINEMA, "03": NATURAL, "0B": FRAME_ADAPT_HDR, "0C": SDR_1, "0D": SDR_2, "0E": HDR1, "0F": HDR2, "14": HLG, "15": HDR10_PLUS, "17": FILMMAKER_MODE, "18": FRAME_ADAPT_HDR2, "1B": VIVID, }, ), CS20221: MapParameter( size=2, readwrite={ "00": (FILM, B5A1, B5A2), "01": CINEMA, "03": NATURAL, "04": HDR10, "0B": FRAME_ADAPT_HDR, "0C": USER_1, "0D": USER_2, "0E": USER_3, "0F": USER_4, "10": USER_5, "11": USER_6, "14": HLG, "15": HDR10_PLUS, "16": PANA_PQ, "17": FILMMAKER_MODE, "18": FRAME_ADAPT_HDR2, "19": FRAME_ADAPT_HDR3, }, ), CS20191: MapParameter( size=2, readwrite={ "00": (FILM, B2A1, B2A2), "01": CINEMA, "03": NATURAL, "04": HDR10, "06": (THX, B2A1), "0B": FRAME_ADAPT_HDR, "0C": USER_1, "0D": USER_2, "0E": USER_3, "0F": USER_4, "10": USER_5, "11": USER_6, "14": HLG, "16": PANA_PQ, }, ), CS20172: MapParameter( size=2, readwrite={ "00": FILM, "01": CINEMA, "03": NATURAL, "04": HDR, "06": THX, "0C": USER_1, "0D": USER_2, "0E": USER_3, "0F": USER_4, "10": USER_5, "11": USER_6, }, ), CS20171: MapParameter( size=2, readwrite={ "00": (FILM, XHR3), "01": CINEMA, "02": ANIMATION, "03": NATURAL, "04": HDR, "06": (THX, XHR3), "0C": USER_1, "0D": USER_2, "0E": USER_3, "0F": USER_4, "10": USER_5, }, ), CS20161: MapParameter( size=2, readwrite={ "00": (FILM, XHP3), "01": CINEMA, "02": ANIMATION, "03": NATURAL, "06": (THX, XHP3), "0C": USER_1, "0D": USER_2, "0E": USER_3, "0F": USER_4, "10": USER_5, "11": USER_6, }, ), CS20141: MapParameter( size=2, read={ "00": (FILM, XHK3), "01": CINEMA, "02": ANIMATION, "03": NATURAL, "04": STAGE, "06": (THX, XHK3), "0B": R4K, "0C": USER_1, "0D": USER_2, "0E": USER_3, "0F": USER_4, "12": (PHOTO, XHK3), }, write={ "00": (FILM, XHK3), "01": CINEMA, "02": ANIMATION, "03": NATURAL, "04": STAGE, "06": (THX, XHK3), "0C": USER_1, "0D": USER_2, "0E": USER_3, "0F": USER_4, "12": (PHOTO, XHK3), }, ), CS20131: MapParameter( size=2, readwrite={ "00": FILM, "01": CINEMA, "02": ANIMATION, "03": NATURAL, "04": STAGE, "05": RESERVED, "06": THX, "07": (ISF_DAY, XHH4), "08": (ISF_NIGHT, XHH4), "09": (THX_BRIGHT, XHH4), "0A": (THX_DARK, XHH4), "0B": THREE_D, "0C": USER_1, "0D": USER_2, "0E": USER_3, "0F": USER_4, "10": USER_5, }, ), CS20121: MapParameter( size=2, readwrite={ "00": FILM, "01": CINEMA, "02": ANIMATION, "03": NATURAL, "04": STAGE, "05": RESERVED, "06": THX, "07": (ISF_DAY, XHF), "08": (ISF_NIGHT, XHF), "09": (THX_BRIGHT, XHF), "0A": (THX_DARK, XHF), "0B": THREE_D, "0C": USER_1, "0D": USER_2, "0E": USER_3, "0F": USER_4, "10": USER_5, }, ), } class IntelligentLensAperture(Command): """Intelligent Lens Aperture command.""" code = "PMDI" reference = True operation = True OFF = "off" AUTO_1 = "auto-1" AUTO_2 = "auto-2" parameter = { CS20221: MapParameter( size=1, readwrite={ "0": (OFF, B5B1), "1": (AUTO_1, B5B1), "2": (AUTO_2, B5B1), }, ), (CS20191, CS20171, CS20161, CS20141): MapParameter( size=1, readwrite={ "0": OFF, "1": AUTO_1, "2": AUTO_2, }, ), } class ColorProfile(Command): """Color Profile command.""" code = "PMPR" reference = True operation = True ADOBE = "adobe" ANIME = "anime" ANIME_1 = "anime-1" ANIME_2 = "anime-2" AUTO = "auto" BT_2020 = "bt-2020" BT_2020_NORMAL = "bt-2020-normal" BT_2020_WIDE = "bt-2020-wide" BT_709 = "bt-709" CINEMA = "cinema" CINEMA_1 = "cinema-1" CINEMA_2 = "cinema-2" CUSTOM_1 = "custom-1" CUSTOM_2 = "custom-2" CUSTOM_3 = "custom-3" CUSTOM_4 = "custom-4" CUSTOM_5 = "custom-5" CUSTOM_6 = "custom-6" DCI = "dci" FILM = "film" FILM_1 = "film-1" FILM_2 = "film-2" FILM_3 = "film-3" HDR = "hdr" NATURAL = "natural" OFF = "off" OFF_WIDE = "off-wide" PANA_PQ_BL = "pana-pq-bl" PANA_PQ_HL = "pana-pq-hl" REFERENCE = "reference" STAGE = "stage" STANDARD = "standard" THX = "thx" VIDEO = "video" VIVID = "vivid" THREE_D = "3d" THREE_D_ANIMATION = "3d-animation" THREE_D_ANIME = "3d-anime" THREE_D_CINEMA = "3d-cinema" THREE_D_FILM = "3d-film" THREE_D_PHOTO = "3d-photo" THREE_D_STAGE = "3d-stage" THREE_D_THX = "3d-thx" THREE_D_VIDEO = "3d-video" XV_COLOR = "xv-color" parameter = { CS20241: MapParameter( size=2, readwrite={ "00": OFF, "01": FILM_1, "02": FILM_2, "03": BT_709, "04": CINEMA, "06": ANIME, "08": VIDEO, "0B": BT_2020_WIDE, "0E": CUSTOM_1, "0F": CUSTOM_2, "10": CUSTOM_3, "11": CUSTOM_4, "21": DCI, "23": VIVID, "24": BT_2020_NORMAL, "25": OFF_WIDE, "26": AUTO, }, ), CS20242: MapParameter( size=2, readwrite={ "00": OFF, "03": BT_709, "04": CINEMA, "06": (ANIME, D8A1), "08": VIDEO, "0B": (BT_2020_WIDE, D8A1), "0E": CUSTOM_1, "0F": CUSTOM_2, "10": CUSTOM_3, "11": CUSTOM_4, "21": DCI, "23": VIVID, "24": BT_2020_NORMAL, "25": (OFF_WIDE, D8A1), "26": AUTO, }, ), CS20221: MapParameter( size=2, readwrite={ "00": OFF, "01": (FILM_1, B5A1, B5A2), "02": (FILM_2, B5A1, B5A2), "03": BT_709, "04": CINEMA, "06": (ANIME, B5A1, B5A2), "08": VIDEO, "0B": (BT_2020_WIDE, B5A1, B5A2), "0E": CUSTOM_1, "0F": CUSTOM_2, "10": CUSTOM_3, "11": CUSTOM_4, "12": CUSTOM_5, "21": DCI, "22": CUSTOM_6, "24": BT_2020_NORMAL, "25": (OFF_WIDE, B5A1, B5A2), "26": AUTO, }, ), CS20191: MapParameter( size=2, readwrite={ "00": OFF, "01": (FILM_1, B2A1, B2A2), "02": (FILM_2, B2A1, B2A2), "03": BT_709, "04": CINEMA, "06": (ANIME, B2A1, B2A2), "08": VIDEO, "0A": HDR, "0B": (BT_2020_WIDE, B2A1, B2A2), "0D": (THX, B2A1), "0E": CUSTOM_1, "0F": CUSTOM_2, "10": CUSTOM_3, "11": CUSTOM_4, "12": PANA_PQ_HL, "21": DCI, "22": PANA_PQ_BL, "24": BT_2020_NORMAL, "25": (OFF_WIDE, B2A1, B2A2), "26": AUTO, }, ), CS20172: MapParameter( size=2, readwrite={ "00": OFF, "01": FILM_1, "02": FILM_2, "03": BT_709, "04": CINEMA, "06": ANIME, "0A": HDR, "0B": BT_2020, "0D": THX, "0E": CUSTOM_1, "0F": CUSTOM_2, "10": CUSTOM_3, "11": CUSTOM_4, "12": CUSTOM_5, "21": DCI, "22": CUSTOM_6, }, ), CS20171: MapParameter( size=2, readwrite={ "00": OFF, "01": (FILM_1, XHR3), "02": (FILM_2, XHR3), "03": STANDARD, "04": CINEMA_1, "05": (CINEMA_2, XHR3), "06": ANIME_1, "07": (ANIME_2, XHR3), "08": VIDEO, "09": XV_COLOR, "0B": BT_2020, "0C": THREE_D_CINEMA, "0D": (THX, XHR3), "0E": CUSTOM_1, "0F": CUSTOM_2, "10": CUSTOM_3, "11": CUSTOM_4, "12": CUSTOM_5, "13": (FILM_3, XHR3), "14": THREE_D_VIDEO, "15": THREE_D_ANIMATION, "1E": (THREE_D_FILM, XHR3), "20": (THREE_D_THX, XHR3), "21": (REFERENCE, XHR3), }, ), CS20161: MapParameter( size=2, readwrite={ "00": OFF, "01": (FILM_1, XHP3), "02": (FILM_2, XHP3), "03": STANDARD, "04": CINEMA_1, "05": (CINEMA_2, XHP3), "06": ANIME_1, "07": (ANIME_2, XHP3), "08": VIDEO, "09": (XV_COLOR, XHP3), "0C": THREE_D_CINEMA, "0D": (THX, XHP3), "0E": CUSTOM_1, "0F": CUSTOM_2, "10": CUSTOM_3, "11": CUSTOM_4, "12": CUSTOM_5, "13": (FILM_3, XHP3), "14": THREE_D_VIDEO, "15": THREE_D_ANIMATION, "1E": (THREE_D_FILM, XHP3), "20": (THREE_D_THX, XHP3), "21": (REFERENCE, XHP3), "22": CUSTOM_6, }, ), CS20141: MapParameter( size=2, readwrite={ "00": OFF, "01": (FILM_1, XHK3), "02": (FILM_2, XHK3), "03": STANDARD, "04": CINEMA_1, "05": (CINEMA_2, XHK3), "06": ANIME_1, "07": (ANIME_2, XHK3), "08": VIDEO, "09": (XV_COLOR, XHK3), "0A": (ADOBE, XHK3), "0B": STAGE, "0C": THREE_D_CINEMA, "0D": (THX, XHK3), "0E": CUSTOM_1, "0F": CUSTOM_2, "10": CUSTOM_3, "11": CUSTOM_4, "12": CUSTOM_5, "13": (FILM_3, XHK3), "14": THREE_D_VIDEO, "15": THREE_D_ANIME, "16": (THREE_D_PHOTO, XHK3), "1E": (THREE_D_FILM, XHK3), "1F": (THREE_D_STAGE, XHK3), "20": (THREE_D_THX, XHK3), }, ), CS20131: MapParameter( size=2, readwrite={ "00": (OFF, XHH4), "01": (FILM_1, XHH4), "02": (FILM_2, XHH4), "03": (STANDARD, XHH4), "04": (CINEMA_1, XHH4), "05": (CINEMA_2, XHH4), "06": (ANIME_1, XHH4), "07": (ANIME_2, XHH4), "08": (VIDEO, XHH4), "09": (VIVID, XHH4), "0A": (ADOBE, XHH4), "0B": (STAGE, XHH4), "0C": (THREE_D_CINEMA, XHH4), "0D": (THX, XHH4), "0E": (CUSTOM_1, XHH4), "0F": (CUSTOM_2, XHH4), "10": (CUSTOM_3, XHH4), "11": (CUSTOM_4, XHH4), "12": (CUSTOM_5, XHH4), "13": (FILM_3, XHH4), "14": (THREE_D_VIDEO, XHH4), "15": (THREE_D_ANIME, XHH4), "16": (STANDARD, XHH1), "17": (FILM, XHH1), "18": (CINEMA, XHH1), "19": (ANIME, XHH1), "1A": (NATURAL, XHH1), "1B": (STAGE, XHH1), "1C": (THREE_D, XHH1), "1D": (OFF, XHH1), }, ), CS20121: MapParameter( size=2, readwrite={ "00": (OFF, XHF), "01": (FILM_1, XHF), "02": (FILM_2, XHF), "03": (STANDARD, XHF), "04": (CINEMA_1, XHF), "05": (CINEMA_2, XHF), "06": (ANIME_1, XHF), "07": (ANIME_2, XHF), "08": (VIDEO, XHF), "09": (VIVID, XHF), "0A": (ADOBE, XHF), "0B": (STAGE, XHF), "0C": (THREE_D, XHF), "0D": (THX, XHF), "0E": (CUSTOM_1, XHF), "0F": (CUSTOM_2, XHF), "10": (CUSTOM_3, XHF), "11": (CUSTOM_4, XHF), "12": (CUSTOM_5, XHF), }, ), } class ColorManagement(Command): """Color Management command.""" code = "PMCB" reference = True operation = True OFF = "off" ON = "on" CUSTOM_1 = "custom-1" CUSTOM_2 = "custom-2" CUSTOM_3 = "custom-3" parameter = { (CS20241, CS20242, CS20221, CS20191, CS20172, CS20171, CS20161): MapParameter( size=1, readwrite={ "0": OFF, "1": ON, }, ), CS20141: MapParameter( size=1, readwrite={ "0": OFF, "1": CUSTOM_1, "2": CUSTOM_2, "3": CUSTOM_3, }, ), CS20131: MapParameter( size=1, readwrite={ "0": OFF, "1": (CUSTOM_1, XHH4), "2": (CUSTOM_2, XHH4), "3": (CUSTOM_3, XHH4), }, ), CS20121: MapParameter( size=1, readwrite={ "0": OFF, "1": (CUSTOM_1, XHF), "2": (CUSTOM_2, XHF), "3": (CUSTOM_3, XHF), }, ), } class ColorTemperature(Command): """Color Temperature command.""" code = "PMCL" reference = True operation = True CUSTOM_1 = "custom-1" CUSTOM_2 = "custom-2" CUSTOM_3 = "custom-3" HDR = "hdr" HDR10 = "hdr10" HDR10_PLUS = "hdr10+" HIGH_BRIGHT = "high-bright" HLG = "hlg" VIVID = "vivid" T5500K = "5500k" T6000K = "6000k" T6500K = "6500k" T7000K = "7000k" T7500K = "7500k" T8000K = "8000k" T8500K = "8500k" T9000K = "9000k" T9300K = "9300k" T9500K = "9500k" XENON_1 = "xenon-1" XENON_2 = "xenon-2" XENON_3 = "xenon-3" parameter = { CS20241: MapParameter( size=2, readwrite={ "00": T5500K, "02": T6500K, "04": T7500K, "08": T9300K, "09": HIGH_BRIGHT, "0A": CUSTOM_1, "0B": CUSTOM_2, "0C": HDR10, "0D": XENON_1, "0E": XENON_2, "14": HLG, "15": HDR10_PLUS, }, ), CS20242: MapParameter( size=2, readwrite={ "00": T5500K, "02": T6500K, "04": T7500K, "08": T9300K, "09": HIGH_BRIGHT, "0A": CUSTOM_1, "0B": CUSTOM_2, "0C": HDR10, "0D": (XENON_1, D8A1), "0E": (XENON_2, D8A1), "14": HLG, "15": HDR10_PLUS, }, ), CS20221: MapParameter( size=2, readwrite={ "00": T5500K, "02": T6500K, "04": T7500K, "08": T9300K, "09": HIGH_BRIGHT, "0A": (CUSTOM_1, B5A1, B5A2, B5B1), "0B": CUSTOM_2, "0C": HDR10, "0D": (XENON_1, B5A1, B5A2), "0E": (XENON_2, B5A1, B5A2), "14": HLG, "15": HDR10_PLUS, }, ), CS20191: MapParameter( size=2, readwrite={ "00": T5500K, "02": T6500K, "04": T7500K, "08": T9300K, "09": HIGH_BRIGHT, "0A": (CUSTOM_1, B2A1, B2A2), "0B": CUSTOM_2, "0C": HDR10, "0D": (XENON_1, B2A1, B2A2), "0E": (XENON_2, B2A1, B2A2), "14": HLG, }, ), CS20172: MapParameter( size=2, readwrite={ "00": T5500K, "02": T6500K, "04": T7500K, "08": T9300K, "09": HIGH_BRIGHT, "0A": CUSTOM_1, "0B": CUSTOM_2, "0C": HDR, "0D": XENON_1, "0E": XENON_2, }, ), CS20171: MapParameter( size=1, readwrite={ "0": T5500K, "2": T6500K, "4": T7500K, "8": T9300K, "9": HIGH_BRIGHT, "A": CUSTOM_1, "B": CUSTOM_2, "C": HDR, "D": XENON_1, "E": XENON_2, "F": XENON_3, }, ), CS20161: MapParameter( size=1, readwrite={ "0": T5500K, "2": T6500K, "4": T7500K, "8": T9300K, "9": HIGH_BRIGHT, "A": CUSTOM_1, "B": CUSTOM_2, "C": CUSTOM_3, "D": XENON_1, "E": XENON_2, "F": XENON_3, }, ), (CS20141, CS20131, CS20121): MapParameter( size=1, readwrite={ "0": T5500K, "1": T6000K, "2": T6500K, "3": T7000K, "4": T7500K, "5": T8000K, "6": T8500K, "7": T9000K, "8": T9500K, "9": HIGH_BRIGHT, "A": CUSTOM_1, "B": CUSTOM_2, "C": CUSTOM_3, "D": XENON_1, "E": XENON_2, "F": XENON_3, }, ), } class ColorTemperatureCorrection(Command): """Color Temperature Correction command.""" code = "PMCC" reference = True operation = True HIGH_BRIGHT = "high-bright" T5500K = "5500k" T6000K = "6000k" T6500K = "6500k" T7000K = "7000k" T7500K = "7500k" T8000K = "8000k" T8500K = "8500k" T9000K = "9000k" T9300K = "9300k" T9500K = "9500k" XENON_1 = "xenon-1" XENON_2 = "xenon-2" XENON_3 = "xenon-3" parameter = { CS20241: MapParameter( size=2, readwrite={ "00": T5500K, "02": T6500K, "04": T7500K, "08": T9300K, "09": HIGH_BRIGHT, "0D": XENON_1, "0E": XENON_2, }, ), CS20242: MapParameter( size=2, readwrite={ "00": T5500K, "02": T6500K, "04": T7500K, "08": T9300K, "09": HIGH_BRIGHT, "0D": (XENON_1, D8A1), "0E": (XENON_2, D8A1), }, ), CS20221: MapParameter( size=2, readwrite={ "00": T5500K, "02": T6500K, "04": T7500K, "08": T9300K, "09": HIGH_BRIGHT, "0D": (XENON_1, B5A1, B5A2), "0E": (XENON_2, B5A1, B5A2), }, ), CS20191: MapParameter( size=2, readwrite={ "00": T5500K, "02": T6500K, "04": T7500K, "08": T9300K, "09": HIGH_BRIGHT, "0D": (XENON_1, B2A1, B2A2), "0E": (XENON_2, B2A1, B2A2), }, ), CS20172: MapParameter( size=2, readwrite={ "00": T5500K, "02": T6500K, "04": T7500K, "08": T9300K, "09": HIGH_BRIGHT, "0D": XENON_1, "0E": XENON_2, }, ), (CS20171, CS20161): MapParameter( size=1, readwrite={ "0": T5500K, "2": T6500K, "4": T7500K, "8": T9300K, "9": HIGH_BRIGHT, "D": XENON_1, "E": XENON_2, "F": XENON_3, }, ), CS20141: MapParameter( size=1, readwrite={ "0": T5500K, "1": T6000K, "2": T6500K, "3": T7000K, "4": T7500K, "5": T8000K, "6": T8500K, "7": T9000K, "8": T9500K, "9": HIGH_BRIGHT, "D": XENON_1, "E": XENON_2, "F": XENON_3, }, ), (CS20131, CS20121): MapParameter( size=1, readwrite={ "0": T5500K, "1": T6000K, "2": T6500K, "3": T7000K, "4": T7500K, "5": T8000K, "6": T8500K, "7": T9000K, "8": T9500K, "9": HIGH_BRIGHT, "A": XENON_1, "B": XENON_2, "C": XENON_3, }, ), } class MotionEnhance(Command): """Motion Enhance command.""" code = "PMME" reference = True operation = True OFF = "off" LOW = "low" HIGH = "high" parameter = { (CS20241, CS20221, CS20191, CS20172, CS20171, CS20161): MapParameter( size=1, readwrite={ "0": OFF, "1": LOW, "2": HIGH, }, ), } class LightPower(Command): """Light Power (Lamp/Laser) command.""" code = "PMLP" reference = True operation = True LOW = "low" MID = "mid" HIGH = "high" NORMAL = "normal" parameter = { (CS20241, CS20242): MapParameter( size=1, readwrite={ "0": LOW, "1": HIGH, "2": MID, }, ), CS20221: MapParameter( size=1, readwrite={"0": LOW, "1": HIGH, "2": (MID, B5A3, B5A2, B5A1)}, ), CS20172: MapParameter( size=1, readwrite={ "0": NORMAL, "1": HIGH, "2": MID, }, ), (CS20191, CS20171, CS20161, CS20141, CS20131, CS20121): MapParameter( size=1, readwrite={ "0": NORMAL, "1": HIGH, }, ), } class LaserPower(Command): """Laser Power (LD Current) command.""" code = "PMCV" reference = True operation = True parameter = { (CS20241, CS20242): LaserPowerParameter(), } class EShift(Command): """EShift command.""" code = "PMUS" reference = True operation = True OFF = "off" ON = "on" parameter = { CS20241: MapParameter( size=1, readwrite={ "0": OFF, "1": ON, }, ), CS20221: MapParameter( size=1, readwrite={ "0": (OFF, B5A1, B5A2, B5A3), "1": (ON, B5A1, B5A2, B5A3), }, ), CS20191: MapParameter( size=1, readwrite={ "0": (OFF, B2A1), "1": (ON, B2A1), }, ), (CS20172, CS20171, CS20161, CS20141): MapParameter( size=1, readwrite={ "0": OFF, "1": ON, }, ), } class GraphicMode(Command): """Graphic Mode command.""" code = "PMGM" reference = True operation = True HIGH_RES = "high-res" HIGH_RES_2 = "high-res-2" OFF = "off" STANDARD = "standard" R2K = "2k" R4K = "4k" LOW = "low" HIGH = "high" parameter = { (CS20241, CS20221): MapParameter( size=1, read={ "0": STANDARD, "1": HIGH_RES, "2": HIGH_RES_2, "F": OFF, }, write={ "0": STANDARD, "1": HIGH_RES, "2": HIGH_RES_2, }, ), CS20242: MapParameter( size=1, readwrite={ "0": HIGH, "1": LOW, "2": OFF, }, ), CS20191: MapParameter( size=1, readwrite={ "0": STANDARD, "1": HIGH_RES, }, ), (CS20172, CS20171): MapParameter( size=1, readwrite={ "0": R2K, "1": R4K, }, ), } class Smoother(Command): """Smoother command.""" code = "PMST" reference = True operation = True OFF = "off" ON = "on" parameter = { CS20241: MapParameter( size=4, readwrite={ "0000": ON, "0001": OFF, }, ) } class Hdr(Command): """HDR command.""" code = "IFHR" reference = True operation = False SDR = "sdr" HDR = "hdr" SMPTE_ST_2084 = "smpte-st-2084" HYBRID_LOG = "hybrid-log" HDR10_PLUS = "hdr10+" NONE = "none" parameter = { (CS20241, CS20242): MapParameter( size=1, read={ "0": SDR, "1": HDR, "2": SMPTE_ST_2084, "3": HYBRID_LOG, "4": HDR10_PLUS, "F": NONE, }, ), CS20221: MapParameter( size=1, read={ "0": SDR, "1": HDR, "2": SMPTE_ST_2084, "3": HYBRID_LOG, "4": HDR10_PLUS, }, ), CS20191: MapParameter( size=1, read={ "0": SDR, "1": HDR, "2": SMPTE_ST_2084, "3": HYBRID_LOG, "F": NONE, }, ), CS20172: MapParameter( size=1, read={ "0": SDR, "1": HDR, "2": SMPTE_ST_2084, "F": NONE, }, ), } class HdrLevel(Command): """HDR Level (HDR Quantizer) adjustment command.""" code = "PMHL" reference = True operation = True AUTO = "auto" AUTO_WIDE = "auto-wide" V_M2 = "-2" V_M1 = "-1" V_0 = "0" V_1 = "1" V_2 = "2" parameter = { (CS20241, CS20242, CS20221): MapParameter( size=1, readwrite={ "0": AUTO, "1": V_M2, "2": V_M1, "3": V_0, "4": V_1, "5": V_2, "6": AUTO_WIDE, }, ), CS20191: MapParameter( size=1, readwrite={ "0": AUTO, "1": V_M2, "2": V_M1, "3": V_0, "4": V_1, "5": V_2, }, ), } class HdrProcessing(Command): """HDR Processing command.""" code = "PMHP" reference = True operation = True depends = {Hdr: (Hdr.HDR, Hdr.HDR10_PLUS, Hdr.HYBRID_LOG, Hdr.SMPTE_ST_2084)} HDR10_PLUS = "hdr10+" STATIC = "static" FRAME_BY_FRAME = "frame-by-frame" SCENE_BY_SCENE = "scene-by-scene" parameter = { (CS20241, CS20242): MapParameter( size=1, read={ "0": HDR10_PLUS, "1": STATIC, "2": FRAME_BY_FRAME, "3": SCENE_BY_SCENE, }, write={ "1": STATIC, "2": FRAME_BY_FRAME, "3": SCENE_BY_SCENE, }, ), CS20221: MapParameter( size=1, readwrite={ "0": HDR10_PLUS, "1": STATIC, "2": FRAME_BY_FRAME, "3": SCENE_BY_SCENE, }, ), CS20191: MapParameter( size=1, readwrite={ "1": STATIC, "2": FRAME_BY_FRAME, "3": SCENE_BY_SCENE, }, ), } class ContentType(Command): """Content Type command.""" code = "PMCT" reference = True operation = True AUTO = "auto" SDR = "sdr" HDR10_PLUS = "hdr10+" HDR10 = "hdr10" HLG = "hlg" parameter = { (CS20241, CS20242, CS20221): MapParameter( size=1, readwrite={ "0": AUTO, "1": SDR, "2": HDR10_PLUS, "3": HDR10, "4": HLG, }, ), CS20191: MapParameter( size=1, readwrite={ "0": AUTO, "1": SDR, "3": HDR10, "4": HLG, }, ), } class TheaterOptimizer(Command): """Theater Optimizer command.""" code = "PMNM" reference = True operation = True OFF = "off" ON = "on" parameter = { (CS20241, CS20221, CS20191): MapParameter( size=1, readwrite={ "0": OFF, "1": ON, }, ), } class TheaterOptimizerLevel(Command): """Theater Optimizer Level command.""" code = "PMNL" reference = True operation = True AUTO = "auto" AUTO_WIDE = "auto-wide" HIGH = "high" LOW = "low" MID = "mid" RESERVED = "reserved" V_M2 = "-2" V_M1 = "-1" V_0 = "0" V_1 = "1" V_2 = "2" parameter = { (CS20241, CS20221): MapParameter( size=1, readwrite={ "0": AUTO, "1": V_M2, "2": V_M1, "3": V_0, "4": V_1, "5": V_2, "6": AUTO_WIDE, }, ), CS20191: MapParameter( size=1, readwrite={ "0": RESERVED, "1": LOW, "2": MID, "3": HIGH, }, ), } class TheaterOptimizerProcessing(Command): """Theater Optimizer Processing command.""" code = "PMNP" reference = True operation = True OFF = "off" START = "start" parameter = { (CS20241, CS20221, CS20191): MapParameter( size=1, readwrite={ "0": OFF, "1": START, }, ), } class DeepBlack(Command): """Deep Black command.""" code = "PMBK" reference = True operation = True OFF = "off" ON = "on" parameter = { CS20241: MapParameter( size=1, readwrite={ "0": OFF, "1": ON, }, ), } class HighlightColor(Command): """Highlight Color command.""" code = "PMBE" reference = True operation = True LOW = "low" MID = "mid" HIGH = "high" parameter = { CS20241: MapParameter( size=1, readwrite={ "0": LOW, "1": MID, "2": HIGH, }, ), } class HdmiInputLevel(Command): """HDMI Input Level command.""" code = "ISIL" reference = True operation = True STANDARD = "standard" ENHANCED = "enhanced" SUPER_WHITE = "super-white" AUTO = "auto" parameter = MapParameter( size=1, readwrite={ "0": STANDARD, "1": ENHANCED, "2": SUPER_WHITE, "3": AUTO, }, ) class HdmiColorSpace(Command): """HDMI Color Space switch command.""" code = "ISHS" reference = True operation = True AUTO = "auto" YCBCR_444 = "ycbcr-444" YCBCR_422 = "ycbcr-422" RGB = "rgb" parameter = MapParameter( size=1, readwrite={ "0": AUTO, "1": YCBCR_444, "2": YCBCR_422, "3": RGB, }, ) class Aspect(Command): """Aspect command.""" code = "ISAS" reference = True operation = True A43 = "4:3" A169 = "16:9" ZOOM = "zoom" AUTO = "auto" JUST = "just" FULL = "full" NATIVE = "native" parameter = { (CS20241, CS20242, CS20221, CS20191): MapParameter( size=1, readwrite={ "2": ZOOM, "3": AUTO, "4": NATIVE, }, ), CS20172: MapParameter( size=1, readwrite={ "2": ZOOM, "3": AUTO, "4": JUST, }, ), CS20171: MapParameter( size=1, readwrite={ "0": A43, "1": A169, "2": ZOOM, }, ), (CS20161, CS20141, CS20131, CS20121): MapParameter( size=1, readwrite={ "0": A43, "1": A169, "2": ZOOM, "3": AUTO, "4": JUST, "5": FULL, }, ), } class Hdmi2D3D(Command): """HDMI 2D/3D switch command.""" code = "IS3D" reference = True operation = True TWO_D = "2d" AUTO = "auto" SIDE_BY_SIDE = "side-by-side" TOP_AND_BOTTOM = "top-and-bottom" parameter = MapParameter( size=1, readwrite={ "0": TWO_D, "1": AUTO, "3": SIDE_BY_SIDE, "4": TOP_AND_BOTTOM, }, ) class Mask(Command): """Mask command. TODO: Add numeric top, bottom, left, right values""" code = "ISMA" reference = True operation = True CUSTOM = "custom" CUSTOM_1 = "custom-1" CUSTOM_2 = "custom-2" CUSTOM_3 = "custom-3" OFF = "off" ON = "on" P2_5 = "2.5%" P5 = "5%" parameter = { (CS20241, CS20242, CS20221, CS20191, CS20172): MapParameter( size=1, readwrite={ "1": ON, "2": OFF, }, ), (CS20171, CS20161): MapParameter( size=1, readwrite={ "0": CUSTOM_1, "1": CUSTOM_2, "2": OFF, "3": CUSTOM_3, }, ), (CS20141, CS20131, CS20121): MapParameter( size=1, readwrite={ "0": P2_5, "1": P5, "2": OFF, "3": CUSTOM, }, ), } class PictureModeHdr10(Command): """HDR10 Picture Mode command.""" code = "ISHD" reference = True operation = True HDR10 = "hdr10" USER_4 = "user-4" USER_5 = "user-5" USER_6 = "user-6" FRAME_ADAPT_HDR = "frame-adapt-hdr" FRAME_ADAPT_HDR2 = "frame-adapt-hdr2" FRAME_ADAPT_HDR3 = "frame-adapt-hdr3" HDR10_LL = "hdr10-ll" PANA_PQ = "pana-pq" LAST_SETTING = "last-setting" HDR1 = "hdr1" HDR2 = "hdr2" parameter = { CS20241: MapParameter( size=1, readwrite={ "1": HDR10, "5": USER_4, "6": USER_5, "7": USER_6, "8": FRAME_ADAPT_HDR, "C": FRAME_ADAPT_HDR2, "D": FRAME_ADAPT_HDR3, "E": HDR10_LL, "F": LAST_SETTING, }, ), CS20242: MapParameter( size=1, readwrite={ "4": HDR1, "5": HDR2, "8": FRAME_ADAPT_HDR, "C": FRAME_ADAPT_HDR2, "F": LAST_SETTING, }, ), CS20221: MapParameter( size=1, readwrite={ "1": HDR10, "5": USER_4, "6": USER_5, "7": USER_6, "8": FRAME_ADAPT_HDR, "9": PANA_PQ, "C": FRAME_ADAPT_HDR2, "D": FRAME_ADAPT_HDR3, "F": LAST_SETTING, }, ), CS20191: MapParameter( size=1, readwrite={ "1": HDR10, "5": USER_4, "6": USER_5, "7": USER_6, "8": FRAME_ADAPT_HDR, "9": PANA_PQ, "F": LAST_SETTING, }, ), } class PictureModeHlg(Command): """HLG Picture Mode command.""" code = "ISHL" reference = True operation = True HLG = "hlg" HLG_LL = "hlg-ll" LAST_SETTING = "last-setting" USER_4 = "user-4" USER_5 = "user-5" USER_6 = "user-6" parameter = { CS20241: MapParameter( size=1, readwrite={ "1": HLG, "5": USER_4, "6": USER_5, "7": USER_6, "9": HLG_LL, "F": LAST_SETTING, }, ), (CS20221, CS20191): MapParameter( size=1, readwrite={ "1": HLG, "5": USER_4, "6": USER_5, "7": USER_6, "F": LAST_SETTING, }, ), } class PictureModeSdr(Command): """SDR Picture Mode command.""" code = "ISS2" reference = True operation = True NATURAL = "natural" USER_1 = "user-1" USER_2 = "user-2" USER_3 = "user-3" CINEMA = "cinema" FILM = "film" NATURAL_LL = "natural-ll" VIVID = "vivid" THX = "thx" LAST_SETTING = "last-setting" SDR_1 = "sdr-1" SDR_2 = "sdr-2" parameter = { CS20241: MapParameter( size=1, readwrite={ "1": NATURAL, "2": USER_1, "3": USER_2, "4": USER_3, "8": CINEMA, "9": FILM, "D": NATURAL_LL, "E": VIVID, "F": LAST_SETTING, }, ), CS20242: MapParameter( size=1, readwrite={ "1": NATURAL, "2": SDR_1, "3": SDR_2, "8": CINEMA, "E": VIVID, "F": LAST_SETTING, }, ), CS20221: MapParameter( size=1, readwrite={ "1": NATURAL, "2": USER_1, "3": USER_2, "4": USER_3, "8": CINEMA, "9": (FILM, B5A1, B5A2), "F": LAST_SETTING, }, ), CS20191: MapParameter( size=1, readwrite={ "1": NATURAL, "2": USER_1, "3": USER_2, "4": USER_3, "8": CINEMA, "9": FILM, "E": THX, "F": LAST_SETTING, }, ), } class PictureModeSdr3d(Command): """SDR(3D) Picture Mode command.""" code = "ISS3" reference = True operation = True NATURAL = "natural" USER_1 = "user-1" USER_2 = "user-2" USER_3 = "user-3" CINEMA = "cinema" FILM = "film" NATURAL_LL = "natural-ll" VIVID = "vivid" THX = "thx" LAST_SETTING = "last-setting" parameter = { CS20241: MapParameter( size=1, readwrite={ "1": NATURAL, "2": USER_1, "3": USER_2, "4": USER_3, "8": CINEMA, "9": FILM, "D": NATURAL_LL, "E": VIVID, "F": LAST_SETTING, }, ), CS20221: MapParameter( size=1, readwrite={ "1": NATURAL, "2": USER_1, "3": USER_2, "4": USER_3, "8": CINEMA, "9": (FILM, B5A1, B5A2), "F": LAST_SETTING, }, ), CS20191: MapParameter( size=1, readwrite={ "1": NATURAL, "2": USER_1, "3": USER_2, "4": USER_3, "8": CINEMA, "9": FILM, "E": THX, "F": LAST_SETTING, }, ), } class FilmmakerMode(Command): """Filmmaker Mode command.""" code = "ISFS" reference = True operation = True AUTO = "auto" MANUAL = "manual" parameter = { (CS20241, CS20242, CS20221): MapParameter( size=1, readwrite={ "0": MANUAL, "1": AUTO, }, ), } class LowLatencyMode(Command): """Low Latency Mode command.""" code = "PMLL" reference = True operation = True OFF = "off" ON = "on" parameter = { (CS20241, CS20221, CS20191, CS20172, CS20171): MapParameter( size=1, readwrite={ "0": OFF, "1": ON, }, ), } class LowLatencyAutoMode(Command): """ALLM (Auto Low Latency Mode) command.""" code = "ISAL" reference = True operation = True OFF = "off" ON = "on" parameter = { CS20241: MapParameter( size=1, readwrite={ "0": OFF, "1": ON, }, ), } class DynamicControl(Command): """Dynamic Control command (Dynamic CTRL / CMS Display Color).""" code = "PMDC" reference = True operation = True depends = {LowLatencyMode: LowLatencyMode.OFF} BALANCED = "balanced" HIGH = "high" LOW = "low" MODE_1 = "mode-1" MODE_2 = "mode-2" MODE_3 = "mode-3" OFF = "off" parameter = { (CS20241, CS20242): MapParameter( size=1, readwrite={ "0": OFF, "1": LOW, "2": HIGH, "3": BALANCED, }, ), CS20221: MapParameter( size=1, readwrite={ "0": (OFF, B5A1, B5A2, B5A3), "1": (MODE_1, B5A1, B5A2, B5A3), "2": (MODE_2, B5A1, B5A2, B5A3), "3": (MODE_3, B5A1, B5A2, B5A3), }, ), CS20172: MapParameter( size=1, readwrite={ "0": OFF, "1": MODE_1, "2": MODE_2, }, ), } class ClearMotionDrive(Command): """Clear Motion Drive command.""" code = "PMCM" reference = True operation = True depends = {LowLatencyMode: LowLatencyMode.OFF} OFF = "off" LOW = "low" HIGH = "high" INVERSE_TELECINE = "inverse-telecine" parameter = { ( CS20241, CS20221, CS20191, CS20172, CS20171, CS20161, CS20141, CS20131, CS20121, ): MapParameter( size=1, readwrite={ "0": OFF, "3": LOW, "4": HIGH, "5": INVERSE_TELECINE, }, ), CS20242: MapParameter( size=1, readwrite={ "0": OFF, "6": HIGH, "7": LOW, }, ), } class InstallationMode(Command): """Installation Mode command.""" code = "INML" reference = True operation = True operation_timeout = 30 MEMORY_1 = "memory-1" MEMORY_2 = "memory-2" MEMORY_3 = "memory-3" MEMORY_4 = "memory-4" MEMORY_5 = "memory-5" MEMORY_6 = "memory-6" MEMORY_7 = "memory-7" MEMORY_8 = "memory-8" MEMORY_9 = "memory-9" MEMORY_10 = "memory-10" parameter = { (CS20241, CS20221, CS20191, CS20172): MapParameter( size=1, readwrite={ "0": MEMORY_1, "1": MEMORY_2, "2": MEMORY_3, "3": MEMORY_4, "4": MEMORY_5, "5": MEMORY_6, "6": MEMORY_7, "7": MEMORY_8, "8": MEMORY_9, "9": MEMORY_10, }, ), CS20242: MapParameter( size=1, readwrite={ "0": MEMORY_1, "1": MEMORY_2, "2": MEMORY_3, "3": MEMORY_4, "4": MEMORY_5, }, ), CS20171: MapParameter( size=1, readwrite={ "0": MEMORY_1, "1": MEMORY_2, "2": MEMORY_3, "3": MEMORY_4, "4": MEMORY_5, "5": (MEMORY_6, XHR3), "6": (MEMORY_7, XHR3), "7": (MEMORY_8, XHR3), "8": (MEMORY_9, XHR3), "9": (MEMORY_10, XHR3), }, ), CS20161: MapParameter( size=1, readwrite={ "0": MEMORY_1, "1": MEMORY_2, "2": MEMORY_3, "3": MEMORY_4, "4": MEMORY_5, "5": (MEMORY_6, XHP3), "6": (MEMORY_7, XHP3), "7": (MEMORY_8, XHP3), "8": (MEMORY_9, XHP3), "9": (MEMORY_10, XHP3), }, ), CS20141: MapParameter( size=1, readwrite={ "0": MEMORY_1, "1": MEMORY_2, "2": MEMORY_3, "3": MEMORY_4, "4": MEMORY_5, "5": (MEMORY_6, XHK3), "6": (MEMORY_7, XHK3), "7": (MEMORY_8, XHK3), "8": (MEMORY_9, XHK3), "9": (MEMORY_10, XHK3), }, ), (CS20131, CS20121): MapParameter( size=1, readwrite={ "0": MEMORY_1, "1": MEMORY_2, "2": MEMORY_3, }, ), } class Trigger(Command): """Trigger command.""" code = "FUTR" reference = True operation = True OFF = "off" POWER = "power" ANAMORPHIC = "anamorphic" INSTALLATION_1 = "installation-1" INSTALLATION_2 = "installation-2" INSTALLATION_3 = "installation-3" INSTALLATION_4 = "installation-4" INSTALLATION_5 = "installation-5" INSTALLATION_6 = "installation-6" INSTALLATION_7 = "installation-7" INSTALLATION_8 = "installation-8" INSTALLATION_9 = "installation-9" INSTALLATION_10 = "installation-10" parameter = { (CS20241, CS20221, CS20191, CS20172): MapParameter( size=1, readwrite={ "0": OFF, "1": POWER, "2": ANAMORPHIC, "3": INSTALLATION_1, "4": INSTALLATION_2, "5": INSTALLATION_3, "6": INSTALLATION_4, "7": INSTALLATION_5, "8": INSTALLATION_6, "9": INSTALLATION_7, "A": INSTALLATION_8, "B": INSTALLATION_9, "C": INSTALLATION_10, }, ), (CS20171, CS20161, CS20141, CS20131, CS20121): MapParameter( size=1, readwrite={ "0": OFF, "1": POWER, "2": ANAMORPHIC, }, ), } class OffTimer(Command): """Off Timer switch command.""" code = "FUOT" reference = True operation = True OFF = "off" HOUR_1 = "1-hour" HOUR_2 = "2-hours" HOUR_3 = "3-hours" HOUR_4 = "4-hours" parameter = MapParameter( size=1, readwrite={ "0": OFF, "1": HOUR_1, "2": HOUR_2, "3": HOUR_3, "4": HOUR_4, }, ) class EcoMode(Command): """Eco Mode command.""" code = "FUEM" reference = True operation = True OFF = "off" ON = "on" parameter = MapParameter( size=1, readwrite={ "0": OFF, "1": ON, }, ) class HideEco(Command): """Hide Eco command.""" code = "FUHE" reference = True operation = True OFF = "off" ON = "on" parameter = { (CS20241, CS20242, CS20172): MapParameter( size=1, readwrite={ "0": OFF, "1": ON, }, ), CS20221: MapParameter( size=1, readwrite={ "0": (OFF, B5A1, B5A2, B5A3), "1": (ON, B5A1, B5A2, B5A3), }, ), } class Source(Command): """Signal (Source) command.""" code = "IFIS" reference = True operation = False FWXGA_1366X768 = "fwxga-1366x768" NO_SIGNAL = "no-signal" OUT_OF_RANGE = "out-of-range" QXGA = "qxga" SVGA_800X600 = "svga-800x600" SXGA_1280X1024 = "sxga-1280x1024" UXGA_1600X1200 = "uxga-1600x1200" VGA_640X480 = "vga-640x480" R1080I_3D = "1080i-3d" R1080I_50 = "1080i-50" R1080I_60 = "1080i-60" R1080P_100 = "1080p-100" R1080P_120 = "1080p-120" R1080P_24 = "1080p-24" R1080P_25 = "1080p-25" R1080P_30 = "1080p-30" R1080P_3D = "1080p-3d" R1080P_50 = "1080p-50" R1080P_60 = "1080p-60" R2048X1080_P24 = "2048x1080-p24" R2048X1080_P25 = "2048x1080-p25" R2048X1080_P30 = "2048x1080-p30" R2048X1080_P50 = "2048x1080-p50" R2048X1080_P60 = "2048x1080-p60" R3840X1080_P50 = "3840x1080-p50" R3840X1080_P60 = "3840x1080-p60" R3840X2160_100HZ = "3840x2160-100hz" R3840X2160_P120 = "3840x2160-p120" R4096X2160_100HZ = "4096x2160-100hz" R4096X2160_P120 = "4096x2160-p120" R480I = "480i" R480P = "480p" R4K = "4k" R4K_3840_24 = "4k-3840-24" R4K_3840_25 = "4k-3840-25" R4K_3840_30 = "4k-3840-30" R4K_3840_50 = "4k-3840-50" R4K_3840_60 = "4k-3840-60" R4K_4096_24 = "4k-4096-24" R4K_4096_25 = "4k-4096-25" R4K_4096_30 = "4k-4096-30" R4K_4096_50 = "4k-4096-50" R4K_4096_60 = "4k-4096-60" R576I = "576i" R576P = "576p" R720P_3D = "720p-3d" R720P_50 = "720p-50" R720P_60 = "720p-60" R8K_7680X4320_24 = "8k-7680x4320-24" R8K_7680X4320_25 = "8k-7680x4320-25" R8K_7680X4320_30 = "8k-7680x4320-30" R8K_7680X4320_48 = "8k-7680x4320-48" R8K_7680X4320_50 = "8k-7680x4320-50" R8K_7680X4320_60 = "8k-7680x4320-60" WQHD_120 = "wqhd-120" WQHD_60 = "wqhd-60" WQXGA = "wqxga" WSXGA_PLUS_1680X1050 = "wsxga+1680x1050" WUXGA_1920X1200 = "wuxga-1920x1200" WXGA_1280X768 = "wxga-1280x768" WXGA_1280X800 = "wxga-1280x800" WXGA_PLUS_1440X900 = "wxga+1440x900" WXGA_PLUS_PLUS_1600X900 = "wxga++1600x900" XGA_1024X768 = "xga-1024x768" parameter = { (CS20241, CS20221): MapParameter( size=2, read={ "02": R480P, "03": R576P, "04": R720P_50, "05": R720P_60, "06": R1080I_50, "07": R1080I_60, "08": R1080P_24, "09": R1080P_50, "0A": R1080P_60, "0B": NO_SIGNAL, "0C": R720P_3D, "0D": R1080I_3D, "0E": R1080P_3D, "0F": OUT_OF_RANGE, "10": R4K_4096_60, "11": R4K_4096_50, "12": R4K_4096_30, "13": R4K_4096_25, "14": R4K_4096_24, "15": R4K_3840_60, "16": R4K_3840_50, "17": R4K_3840_30, "18": R4K_3840_25, "19": R4K_3840_24, "1C": R1080P_25, "1D": R1080P_30, "1E": R2048X1080_P24, "1F": R2048X1080_P25, "20": R2048X1080_P30, "21": R2048X1080_P50, "22": R2048X1080_P60, "23": R3840X2160_P120, "24": R4096X2160_P120, "25": VGA_640X480, "26": SVGA_800X600, "27": XGA_1024X768, "28": SXGA_1280X1024, "29": WXGA_1280X768, "2A": WXGA_PLUS_1440X900, "2B": WSXGA_PLUS_1680X1050, "2C": WUXGA_1920X1200, "2D": WXGA_1280X800, "2E": FWXGA_1366X768, "2F": WXGA_PLUS_PLUS_1600X900, "30": UXGA_1600X1200, "31": QXGA, "32": WQXGA, "34": R4096X2160_100HZ, "35": R3840X2160_100HZ, "36": R1080P_100, "37": R1080P_120, "38": R8K_7680X4320_60, "39": R8K_7680X4320_50, "3A": R8K_7680X4320_30, "3B": R8K_7680X4320_25, "3C": R8K_7680X4320_24, "3D": WQHD_60, "3E": WQHD_120, "3F": R8K_7680X4320_48, }, ), CS20242: MapParameter( size=2, read={ "02": R480P, "03": R576P, "04": R720P_50, "05": R720P_60, "08": R1080P_24, "09": R1080P_50, "0A": R1080P_60, "0B": NO_SIGNAL, "0F": OUT_OF_RANGE, "10": R4K_4096_60, "11": R4K_4096_50, "12": R4K_4096_30, "13": R4K_4096_25, "14": R4K_4096_24, "15": R4K_3840_60, "16": R4K_3840_50, "17": R4K_3840_30, "18": R4K_3840_25, "19": R4K_3840_24, "1C": R1080P_25, "1D": R1080P_30, "1E": R2048X1080_P24, "1F": R2048X1080_P25, "20": R2048X1080_P30, "21": R2048X1080_P50, "22": R2048X1080_P60, "25": VGA_640X480, "26": SVGA_800X600, "2C": WUXGA_1920X1200, "30": UXGA_1600X1200, "31": QXGA, "3D": WQHD_60, }, ), CS20191: MapParameter( size=2, read={ "02": R480P, "03": R576P, "04": R720P_50, "05": R720P_60, "06": R1080I_50, "07": R1080I_60, "08": R1080P_24, "09": R1080P_50, "0A": R1080P_60, "0B": NO_SIGNAL, "0C": R720P_3D, "0D": R1080I_3D, "0E": R1080P_3D, "0F": OUT_OF_RANGE, "10": R4K_4096_60, "11": R4K_4096_50, "12": R4K_4096_30, "13": R4K_4096_25, "14": R4K_4096_24, "15": R4K_3840_60, "16": R4K_3840_50, "17": R4K_3840_30, "18": R4K_3840_25, "19": R4K_3840_24, "1C": R1080P_25, "1D": R1080P_30, "1E": R2048X1080_P24, "1F": R2048X1080_P25, "20": R2048X1080_P30, "21": R2048X1080_P50, "22": R2048X1080_P60, "23": R3840X2160_P120, "24": R4096X2160_P120, "25": VGA_640X480, "26": SVGA_800X600, "27": XGA_1024X768, "28": SXGA_1280X1024, "29": WXGA_1280X768, "2A": WXGA_PLUS_1440X900, "2B": WSXGA_PLUS_1680X1050, "2C": WUXGA_1920X1200, "2D": WXGA_1280X800, "2E": FWXGA_1366X768, "2F": WXGA_PLUS_PLUS_1600X900, "30": UXGA_1600X1200, "31": QXGA, "32": WQXGA, }, ), CS20172: MapParameter( size=2, read={ "02": R480P, "03": R576P, "04": R720P_50, "05": R720P_60, "06": R1080I_50, "07": R1080I_60, "08": R1080P_24, "09": R1080P_50, "0A": R1080P_60, "0B": NO_SIGNAL, "0C": R720P_3D, "0D": R1080I_3D, "0E": R1080P_3D, "0F": OUT_OF_RANGE, "10": R4K_4096_60, "11": R4K_4096_50, "12": R4K_4096_30, "13": R4K_4096_25, "14": R4K_4096_24, "15": R4K_3840_60, "16": R4K_3840_50, "17": R4K_3840_30, "18": R4K_3840_25, "19": R4K_3840_24, "1C": R1080P_25, "1D": R1080P_30, "1E": R2048X1080_P24, "1F": R2048X1080_P25, "20": R2048X1080_P30, "21": R2048X1080_P50, "22": R2048X1080_P60, "23": R3840X2160_P120, "24": R4096X2160_P120, }, ), CS20171: MapParameter( size=2, read={ "02": R480P, "03": R576P, "04": R720P_50, "05": R720P_60, "06": R1080I_50, "07": R1080I_60, "08": R1080P_24, "09": R1080P_50, "0A": R1080P_60, "0B": NO_SIGNAL, "0C": R720P_3D, "0D": R1080I_3D, "0E": R1080P_3D, "10": R4K_4096_60, "11": R4K_4096_50, "12": R4K_4096_30, "13": R4K_4096_25, "14": R4K_4096_24, "15": R4K_3840_60, "16": R4K_3840_50, "17": R4K_3840_30, "18": R4K_3840_25, "19": R4K_3840_24, "1A": R3840X1080_P50, "1B": R3840X1080_P60, "1C": R1080P_25, "1D": R1080P_30, }, ), CS20161: MapParameter( size=2, read={ "02": R480P, "03": R576P, "04": R720P_50, "05": R720P_60, "06": R1080I_50, "07": R1080I_60, "08": R1080P_24, "09": R1080P_50, "0A": R1080P_60, "0B": NO_SIGNAL, "0C": R720P_3D, "0D": R1080I_3D, "0E": R1080P_3D, "10": R4K_4096_60, "11": R4K_4096_50, "12": R4K_4096_30, "13": R4K_4096_25, "14": R4K_4096_24, "15": R4K_3840_60, "16": R4K_3840_50, "17": R4K_3840_30, "18": R4K_3840_25, "19": R4K_3840_24, }, ), CS20141: MapParameter( size=1, read={ "0": R480I, "1": R576I, "2": R480P, "3": R576P, "4": R720P_50, "5": R720P_60, "6": R1080I_50, "7": R1080I_60, "8": R1080P_24, "9": R1080P_50, "A": R1080P_60, "B": NO_SIGNAL, "C": R720P_3D, "D": R1080I_3D, "E": R1080P_3D, "F": R4K, }, ), (CS20131, CS20121): MapParameter( size=1, read={ "0": R480I, "1": R576I, "2": R480P, "3": R576P, "4": R720P_50, "5": R720P_60, "6": R1080I_50, "7": R1080I_60, "8": R1080P_24, "9": R1080P_50, "A": R1080P_60, "B": NO_SIGNAL, "C": R720P_3D, "D": R1080I_3D, "E": R1080P_3D, }, ), } class ColorDepth(Command): """Color Depth (Deep Color) command.""" code = "IFDC" reference = True operation = False BIT_8 = "8-bit" BIT_10 = "10-bit" BIT_12 = "12-bit" parameter = MapParameter( size=1, read={ "0": BIT_8, "1": BIT_10, "2": BIT_12, }, ) class ColorSpace(Command): """Color Space Display command.""" code = "IFXV" reference = True operation = False RGB = "rgb" YCBCR_422 = "ycbcr-422" YCBCR_444 = "ycbcr-444" YCBCR_420 = "ycbcr-420" YUV = "yuv" XV_COLOR = "xv-color" parameter = { (CS20241, CS20242, CS20221): MapParameter( size=1, read={ "0": RGB, "1": YCBCR_422, "2": YCBCR_444, "3": YCBCR_420, }, ), (CS20191, CS20172): MapParameter( size=1, read={ "0": RGB, "1": YUV, }, ), (CS20171, CS20161, CS20141): MapParameter( size=1, read={ "0": RGB, "1": YUV, "2": XV_COLOR, }, ), } class LightTime(Command): """Light Time (Lamp/Laser) command.""" code = "IFLT" reference = True operation = False parameter = LightTimeParameter() class Colorimetry(Command): """Colorimetry command.""" code = "IFCM" reference = True operation = False NO_DATA = "no-data" BT_601 = "bt-601" BT_709 = "bt-709" XV_YCC_601 = "xvycc-601" XV_YCC_709 = "xvycc-709" S_YCC_601 = "sycc-601" ADOBE_YCC_601 = "adobe-ycc-601" ADOBE_RGB = "adobe-rgb" BT_2020_CONSTANT_LUMINANCE = "bt-2020-constant-luminance" BT_2020_NON_CONSTANT_LUMINANCE = "bt-2020-non-constant-luminance" S_RGB = "srgb" OTHER = "other" DCI_P3_D65 = "dci-p3-d65" DCI_P3_THEATER = "dci-p3-theater" parameter = { (CS20241, CS20242, CS20221): MapParameter( size=1, read={ "0": NO_DATA, "1": BT_601, "2": BT_709, "3": XV_YCC_601, "4": XV_YCC_709, "5": S_YCC_601, "6": ADOBE_YCC_601, "7": ADOBE_RGB, "8": BT_2020_CONSTANT_LUMINANCE, "9": BT_2020_NON_CONSTANT_LUMINANCE, "A": S_RGB, "B": DCI_P3_D65, "C": DCI_P3_THEATER, }, ), CS20191: MapParameter( size=1, read={ "0": NO_DATA, "1": BT_601, "2": BT_709, "3": XV_YCC_601, "4": XV_YCC_709, "5": S_YCC_601, "6": ADOBE_YCC_601, "7": ADOBE_RGB, "8": BT_2020_CONSTANT_LUMINANCE, "9": BT_2020_NON_CONSTANT_LUMINANCE, "A": S_RGB, }, ), CS20172: MapParameter( size=1, read={ "0": NO_DATA, "1": BT_601, "2": BT_709, "3": XV_YCC_601, "4": XV_YCC_709, "5": S_YCC_601, "6": ADOBE_YCC_601, "7": ADOBE_RGB, "8": BT_2020_CONSTANT_LUMINANCE, "9": BT_2020_NON_CONSTANT_LUMINANCE, "A": OTHER, }, ), } class DscMode(Command): """Dsc Mode command.""" code = "IFDS" reference = True operation = False OFF = "off" ON = "on" parameter = { (CS20241, CS20221): MapParameter( size=1, read={ "0": OFF, "1": ON, }, ) } class LinkRate(Command): """Link Rate command.""" code = "IFLR" reference = True operation = False DISABLE = "disable" R_3_GBPS_3_LANES = "3-gbps-3-lanes" R_6_GBPS_3_LANES = "6-gbps-3-lanes" R_6_GBPS_4_LANES = "6-gbps-4-lanes" R_8_GBPS_4_LANES = "8-gbps-4-lanes" R_10_GBPS_4_LANES = "10-gbps-4-lanes" parameter = { (CS20241, CS20242, CS20221): MapParameter( size=1, read={ "0": DISABLE, "1": R_3_GBPS_3_LANES, "3": R_6_GBPS_3_LANES, "4": R_6_GBPS_4_LANES, "5": R_8_GBPS_4_LANES, "6": R_10_GBPS_4_LANES, }, ), } class Anamorphic(Command): """Anamorphic command.""" code = "INVS" reference = True operation = True OFF = "off" A = "a" B = "b" C = "c" D = "d" parameter = { (CS20241, CS20242, CS20221, CS20191): MapParameter( size=1, readwrite={ "0": OFF, "1": A, "2": B, "3": C, "4": D, }, ), (CS20172, CS20171, CS20161, CS20141, CS20131, CS20121): MapParameter( size=1, readwrite={ "0": OFF, "1": A, "2": B, }, ), } pyjvcprojector-2.0.1/jvcprojector/connection.py000066400000000000000000000037411513216673300220340ustar00rootroot00000000000000"""Module for representing a JVC Projector network connection.""" from __future__ import annotations import asyncio class Connection: """Class for representing a JVC Projector network connection.""" def __init__(self, ip: str, port: int, timeout: float): """Initialize instance of class.""" self._ip = ip self._port = port self._timeout = timeout self._reader: asyncio.StreamReader | None = None self._writer: asyncio.StreamWriter | None = None @property def ip(self) -> str: """Return ip address.""" return self._ip @property def port(self) -> int: """Return port.""" return self._port def is_connected(self) -> bool: """Return if connected to the projector.""" return self._reader is not None and self._writer is not None async def connect(self) -> None: """Connect to the projector.""" assert self._reader is None and self._writer is None conn = asyncio.open_connection(self._ip, self._port) self._reader, self._writer = await asyncio.wait_for(conn, timeout=self._timeout) async def read(self, n: int) -> bytes: """Read n bytes from the projector.""" assert self._reader return await asyncio.wait_for(self._reader.read(n), timeout=self._timeout) async def readline(self, timeout: float | None = None) -> bytes: """Read all bytes up to a newline from the projector.""" assert self._reader return await asyncio.wait_for( self._reader.readline(), timeout=timeout or self._timeout ) async def write(self, data: bytes) -> None: """Write data to the projector.""" assert self._writer self._writer.write(data) await self._writer.drain() async def disconnect(self) -> None: """Disconnect from the projector.""" if self._writer: self._writer.close() self._writer = None self._reader = None pyjvcprojector-2.0.1/jvcprojector/device.py000066400000000000000000000162441513216673300211360ustar00rootroot00000000000000"""Module for representing a JVC Projector device.""" from __future__ import annotations import asyncio from hashlib import sha256 import logging import struct from time import time from typing import TYPE_CHECKING, Final from .connection import Connection from .error import ( JvcProjectorAuthError, JvcProjectorError, JvcProjectorReadWriteTimeoutError, JvcProjectorTimeoutError, ) if TYPE_CHECKING: from .command.base import Command PJOK: Final = b"PJ_OK" PJNG: Final = b"PJ_NG" PJREQ: Final = b"PJREQ" PJACK: Final = b"PJACK" PJNAK: Final = b"PJNAK" UNIT_ID: Final = b"\x89\x01" HEAD_OP: Final = b"!" + UNIT_ID HEAD_REF: Final = b"?" + UNIT_ID HEAD_RES: Final = b"@" + UNIT_ID HEAD_ACK: Final = b"\x06" + UNIT_ID HEAD_LEN: Final = 1 + len(UNIT_ID) END: Final = b"\n" AUTH_SALT: Final = "JVCKWPJ" KEEPALIVE_TTL: Final = 0.5 _LOGGER = logging.getLogger(__name__) class Device: """Class for representing a JVC Projector device.""" def __init__( self, ip: str, port: int, timeout: float, password: str | None ) -> None: """Initialize instance of class.""" self._conn = Connection(ip, port, timeout) self._auth = self._auth_hash = b"" if password: self._auth = struct.pack(f"{max(16, len(password))}s", password.encode()) self._auth_hash = ( sha256(f"{password}{AUTH_SALT}".encode()).hexdigest().encode() ) self._lock = asyncio.Lock() self._keepalive: asyncio.Task | None = None self._last_connect: float = 0.0 self._next_send: float = 0.0 async def send(self, cmd: Command) -> None: """Send command to the device.""" async with self._lock: if self._keepalive: self._keepalive.cancel() self._keepalive = None # Throttle commands to avoid known issues ts = time() if self._next_send and ts < self._next_send: await asyncio.sleep(self._next_send - ts) try: await self._send(cmd) finally: self._keepalive = asyncio.create_task(self.disconnect(KEEPALIVE_TTL)) # Throttle next command. Give ops more time to take effect. self._next_send = time() + 0.1 if cmd.is_ref else 1.0 async def _connect(self) -> None: """Connect to device.""" assert not self._conn.is_connected() # Throttle new connections to avoid known issues elapsed = time() - self._last_connect if elapsed < 0.75: await asyncio.sleep(0.75 - elapsed) retries = 0 while retries < 12: try: _LOGGER.debug("Connecting to %s", self._conn.ip) await self._conn.connect() except (ConnectionRefusedError, asyncio.TimeoutError): retries += 1 if retries == 5: _LOGGER.warning("Retrying refused connection") else: _LOGGER.debug("Retrying refused connection") await asyncio.sleep(0.25 * (retries + 1)) continue except ConnectionError as e: raise JvcProjectorError from e try: data = await self._conn.read(len(PJOK)) except asyncio.TimeoutError as err: raise JvcProjectorTimeoutError("Handshake init timeout") from err _LOGGER.debug("Handshake received %s", data) if data == PJNG: _LOGGER.warning("Handshake retrying on busy") retries += 1 await asyncio.sleep(0.2 * retries) continue if data != PJOK: raise JvcProjectorError("Handshake init invalid") break else: raise JvcProjectorTimeoutError( f"Failed to connect to {self._conn.ip}; retries exceeded" ) _LOGGER.debug("Handshake sending '%s'", PJREQ.decode()) await self._conn.write(PJREQ + (b"_" + self._auth if self._auth else b"")) try: data = await self._conn.read(len(PJACK)) _LOGGER.debug("Handshake received %s", data) if data == PJNAK: _LOGGER.debug("Standard auth failed, trying SHA256 auth") await self._conn.write(PJREQ + b"_" + self._auth_hash) data = await self._conn.read(len(PJACK)) if data == PJACK: self._auth = self._auth_hash if data == PJNAK: raise JvcProjectorAuthError("Authentication failed") if data != PJACK: raise JvcProjectorError("Handshake ack invalid") except asyncio.TimeoutError as err: raise JvcProjectorTimeoutError("Handshake ack timeout") from err self._last_connect = time() async def _send(self, cmd: Command) -> None: """Send command to the device.""" if not self._conn.is_connected(): await self._connect() data = HEAD_REF if cmd.is_ref else HEAD_OP code = cmd.code.encode() data += code if cmd.is_op and cmd.op_value: data += cmd.op_value.encode() data += END _LOGGER.debug( "Sending %s %s (%s) %s", "ref" if cmd.is_ref else "op", cmd.name, cmd.code, data, ) await self._conn.write(data) try: data = await self._conn.readline( cmd.operation_timeout if cmd.is_op else None ) except asyncio.TimeoutError as e: raise JvcProjectorReadWriteTimeoutError( f"Read timeout for command {cmd.name} ({cmd.code})" ) from e if not data.startswith(HEAD_ACK + code[0:2]): raise JvcProjectorError( f"Invalid ack '{data!r}' for command {cmd.name} ({cmd.code})" ) _LOGGER.debug("Received ack %s", data) if cmd.is_ref: try: data = await self._conn.readline() except asyncio.TimeoutError as e: raise JvcProjectorReadWriteTimeoutError( f"Read timeout for command {cmd.name} ({cmd.code})" ) from e _LOGGER.debug("Received ref %s (%s)", data[HEAD_LEN + 2 : -1], data) if not data.startswith(HEAD_RES + code[0:2]): raise JvcProjectorError( f"Invalid header '{data!r}' for command {cmd.name} ({cmd.code})" ) try: cmd.ref_value = data[HEAD_LEN + 2 : -1].decode() except UnicodeDecodeError as e: cmd.ref_value = data.hex() raise JvcProjectorError( f"Invalid response '{data!r} for command {cmd.name} ({cmd.code})'" ) from e cmd.ack = True async def disconnect(self, delay: float = 0.0) -> None: """Disconnect from the device.""" if delay: await asyncio.sleep(delay) if self._keepalive: self._keepalive.cancel() self._keepalive = None await self._conn.disconnect() _LOGGER.debug("Disconnected") pyjvcprojector-2.0.1/jvcprojector/error.py000066400000000000000000000005551513216673300210260ustar00rootroot00000000000000"""Error classes.""" class JvcProjectorError(Exception): """Projector error.""" class JvcProjectorTimeoutError(JvcProjectorError): """Projector timeout error.""" class JvcProjectorReadWriteTimeoutError(JvcProjectorTimeoutError): """Projector read timeout error.""" class JvcProjectorAuthError(JvcProjectorError): """Projector auth error.""" pyjvcprojector-2.0.1/jvcprojector/projector.py000066400000000000000000000136361513216673300217100ustar00rootroot00000000000000"""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 pyjvcprojector-2.0.1/jvcprojector/py.typed000066400000000000000000000000001513216673300210030ustar00rootroot00000000000000pyjvcprojector-2.0.1/pyproject.toml000066400000000000000000000040101513216673300175130ustar00rootroot00000000000000[project] name = "pyjvcprojector" description = "A python client library for controlling a JVC Projector over a network connection." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.10" license = { file = "MIT" } authors = [ { name = "Steve Easley", email = "tardis74@yahoo.com" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Topic :: Home Automation", "Topic :: Software Development :: Libraries", "Programming Language :: Python :: 3.10", ] urls = { "Homepage" = "https://github.com/SteveEasley/pyjvcprojector" } dynamic = ["version"] dependencies = [] [project.optional-dependencies] dev = [ "build==1.3.0", "coverage==7.13.0", "mypy==1.19.0", "pytest-asyncio==1.3.0", "pytest==9.0.2", "ruff==0.14.9", "twine==6.2.0", ] [build-system] requires = ["setuptools>=75.1.0", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools] packages = ["jvcprojector", "jvcprojector.command"] [tool.setuptools.package-data] jvcprojector = ["py.typed"] [tool.setuptools.dynamic] version = { attr = "jvcprojector.__version__" } [tool.pytest.ini_options] log_level = "DEBUG" testpaths = "tests" norecursedirs = ".git" filterwarnings = [ "ignore:.*loop argument is deprecated:DeprecationWarning" ] [tool.ruff] required-version = ">=0.14.9" exclude = ["examples"] [tool.ruff.lint] select = ["E", "F", "I"] ignore = ["F405", "E501"] [tool.ruff.lint.isort] combine-as-imports = true force-sort-within-sections = true [tool.mypy] disable_error_code = "annotation-unchecked" [tool.pylint] disable = [ "duplicate-code", "too-few-public-methods", "too-many-arguments", "too-many-branches", "too-many-instance-attributes", "too-many-lines", "too-many-nested-blocks", "too-many-positional-arguments", "too-many-public-methods", "too-many-statements", ] [project.scripts] jvcprojector = "jvcprojector.cli:cli_entrypoint" pyjvcprojector-2.0.1/tests/000077500000000000000000000000001513216673300157465ustar00rootroot00000000000000pyjvcprojector-2.0.1/tests/__init__.py000066400000000000000000000004051513216673300200560ustar00rootroot00000000000000"""pytest tests.""" from jvcprojector.device import END IP = "127.0.0.1" PORT = 12345 TIMEOUT = 3.0 MAC = "abcd1234" MODEL = "B2A2" PASSWORD = "pass1234" def cc(hdr: bytes, cmd: str) -> bytes: """Create a command.""" return hdr + cmd.encode() + END pyjvcprojector-2.0.1/tests/conftest.py000066400000000000000000000043261513216673300201520ustar00rootroot00000000000000"""pytest fixtures.""" from __future__ import annotations from unittest.mock import patch import pytest from jvcprojector import command from jvcprojector.command.base import Command from jvcprojector.device import HEAD_ACK, PJACK, PJOK from . import IP, MAC, MODEL, PORT, cc @pytest.fixture(name="conn") def fixture_mock_connection(request): """Return a mocked connection.""" with patch("jvcprojector.device.Connection", autospec=True) as mock: connected = False fixture = {"raise_on_connect": 0} if hasattr(request, "param"): fixture.update(request.param) def connect(): nonlocal connected if fixture["raise_on_connect"] > 0: fixture["raise_on_connect"] -= 1 raise ConnectionRefusedError connected = True def disconnect(): nonlocal connected connected = False conn = mock.return_value conn.ip = IP conn.port = PORT conn.is_connected.side_effect = lambda: connected conn.connect.side_effect = connect conn.disconnect.side_effect = disconnect conn.read.side_effect = [PJOK, PJACK] conn.readline.side_effect = [cc(HEAD_ACK, command.Power.code)] conn.write.side_effect = lambda p: None yield conn @pytest.fixture(name="dev") def fixture_mock_device(request): """Return a mocked device.""" with patch("jvcprojector.projector.Device", autospec=True) as mock: fixture: dict[type[Command], str] = { command.MacAddress: MAC, command.ModelName: MODEL, command.Power: "1", command.Input: "6", command.Signal: "1", } if hasattr(request, "param"): fixture.update(request.param) async def send(cmd: Command): if type(cmd) in fixture: if cmd.is_ref: cmd.ref_value = fixture[type(cmd)] cmd.ack = True dev = mock.return_value dev.send.side_effect = send yield dev @pytest.fixture(autouse=True) def reset_commands(): """Ensure Command subclasses' cached state is reset before each test.""" Command.unload() yield pyjvcprojector-2.0.1/tests/test_device.py000066400000000000000000000113351513216673300206210ustar00rootroot00000000000000"""Tests for device module.""" from hashlib import sha256 from unittest.mock import AsyncMock, call import pytest from jvcprojector import command from jvcprojector.command.command import CS20191 from jvcprojector.device import ( AUTH_SALT, HEAD_ACK, HEAD_OP, HEAD_REF, HEAD_RES, PJACK, PJNAK, PJNG, PJOK, PJREQ, Device, ) from jvcprojector.error import JvcProjectorError from . import IP, PORT, TIMEOUT, cc @pytest.mark.asyncio async def test_send_ref(conn: AsyncMock): """Test send reference command succeeds.""" conn.readline.side_effect = [ cc(HEAD_ACK, command.Power.code), cc(HEAD_RES, command.Power.code + "1"), ] dev = Device(IP, PORT, TIMEOUT, None) cmd = command.Power(CS20191) await dev.send(cmd) await dev.disconnect() assert cmd.ack assert cmd.ref_value == command.Power.ON conn.connect.assert_called_once() conn.write.assert_has_calls([call(PJREQ), call(cc(HEAD_REF, command.Power.code))]) @pytest.mark.asyncio async def test_send_op(conn: AsyncMock): """Test send operation command succeeds.""" dev = Device(IP, PORT, TIMEOUT, None) cmd = command.Power(CS20191) cmd.op_value = command.Power.ON await dev.send(cmd) await dev.disconnect() assert cmd.ack assert cmd.ref_value is None conn.connect.assert_called_once() conn.write.assert_has_calls( [call(PJREQ), call(cc(HEAD_OP, f"{command.Power.code}1"))] ) @pytest.mark.asyncio async def test_send_with_password(conn: AsyncMock): """Test send with 10 character password succeeds.""" dev = Device(IP, PORT, TIMEOUT, "passwd7890") cmd = command.Power(CS20191) cmd.op_value = command.Power.ON await dev.send(cmd) await dev.disconnect() conn.write.assert_has_calls( [ call(PJREQ + b"_passwd7890\x00\x00\x00\x00\x00\x00"), call(cc(HEAD_OP, f"{command.Power.code}1")), ] ) @pytest.mark.asyncio async def test_send_with_password_sha256(conn: AsyncMock): """Test send with a projector requiring sha256 hashing.""" conn.read.side_effect = [PJOK, PJNAK, PJACK] dev = Device(IP, PORT, TIMEOUT, "passwd78901") cmd = command.Power(CS20191) cmd.op_value = command.Power.ON await dev.send(cmd) await dev.disconnect() auth = sha256(f"passwd78901{AUTH_SALT}".encode()).hexdigest().encode() conn.write.assert_has_calls( [call(PJREQ + b"_" + auth), call(cc(HEAD_OP, f"{command.Power.code}1"))] ) @pytest.mark.asyncio @pytest.mark.parametrize("conn", [{"raise_on_connect": 1}], indirect=True) async def test_connection_refused_retry(conn: AsyncMock): """Test connection refused results in retry.""" dev = Device(IP, PORT, TIMEOUT, None) cmd = command.Power(CS20191) cmd.op_value = command.Power.ON await dev.send(cmd) await dev.disconnect() assert cmd.ack assert conn.connect.call_count == 2 conn.write.assert_has_calls( [call(PJREQ), call(cc(HEAD_OP, f"{command.Power.code}1"))] ) @pytest.mark.asyncio async def test_connection_busy_retry(conn: AsyncMock): """Test handshake busy results in retry.""" conn.read.side_effect = [PJNG, PJOK, PJACK] dev = Device(IP, PORT, TIMEOUT, None) cmd = command.Power(CS20191) cmd.op_value = command.Power.ON await dev.send(cmd) await dev.disconnect() assert conn.connect.call_count == 2 conn.write.assert_has_calls( [call(PJREQ), call(cc(HEAD_OP, f"{command.Power.code}1"))] ) @pytest.mark.asyncio async def test_connection_bad_handshake_error(conn: AsyncMock): """Test bad handshake results in error.""" conn.read.side_effect = [b"BAD"] dev = Device(IP, PORT, TIMEOUT, None) cmd = command.Power(CS20191) cmd.op_value = command.Power.ON with pytest.raises(JvcProjectorError): await dev.send(cmd) conn.connect.assert_called_once() assert not cmd.ack @pytest.mark.asyncio async def test_send_op_bad_ack_error(conn: AsyncMock): """Test send operation with bad ack results in error.""" conn.readline.side_effect = [cc(HEAD_ACK, "ZZ")] dev = Device(IP, PORT, TIMEOUT, None) cmd = command.Power(CS20191) cmd.op_value = command.Power.ON with pytest.raises(JvcProjectorError): await dev.send(cmd) conn.connect.assert_called_once() assert not cmd.ack @pytest.mark.asyncio async def test_send_ref_bad_ack_error(conn: AsyncMock): """Test send reference with bad ack results in error.""" conn.readline.side_effect = [cc(HEAD_ACK, command.Power.code), cc(HEAD_RES, "ZZ1")] dev = Device(IP, PORT, TIMEOUT, None) cmd = command.Power(CS20191) with pytest.raises(JvcProjectorError): await dev.send(cmd) conn.connect.assert_called_once() assert not cmd.ack pyjvcprojector-2.0.1/tests/test_projector.py000066400000000000000000000102521513216673300213660ustar00rootroot00000000000000"""Tests for projector module.""" from unittest.mock import AsyncMock import pytest from jvcprojector import command from jvcprojector.error import JvcProjectorError from jvcprojector.projector import JvcProjector from . import IP, PORT # pylint: disable=unused-argument @pytest.mark.asyncio async def test_init(dev: AsyncMock): """Test init succeeds.""" p = JvcProjector(IP, port=PORT) assert p.host == IP assert p.port == PORT with pytest.raises(JvcProjectorError): assert p.model with pytest.raises(JvcProjectorError): assert p.spec @pytest.mark.asyncio async def test_connect(dev: AsyncMock): """Test connect succeeds.""" p = JvcProjector(IP, port=PORT) await p.connect() assert p.host == IP await p.disconnect() assert dev.disconnect.call_count == 1 @pytest.mark.asyncio @pytest.mark.parametrize("dev", [{command.ModelName: "ABCD"}], indirect=True) async def test_connect_unknown_model(dev: AsyncMock): """Test connect with an unknown model succeeds.""" p = JvcProjector(IP, port=PORT) await p.connect() assert p.host == IP assert p.model == "ABCD" assert p.spec == "UNKOWN" await p.disconnect() @pytest.mark.asyncio @pytest.mark.parametrize("dev", [{command.ModelName: "B2A9"}], indirect=True) async def test_connect_partial_model_match(dev: AsyncMock): """Test connect with a partial model match succeeds.""" p = JvcProjector(IP, port=PORT) await p.connect() assert p.host == IP assert p.model == "B2A9" assert p.spec == "CS20191-B2A3" await p.disconnect() @pytest.mark.asyncio async def test_get(dev: AsyncMock): """Test get method.""" p = JvcProjector(IP, port=PORT) await p.connect() # succeeds assert await p.get(command.Power) == command.Power.ON assert await p.get("Power") == command.Power.ON assert await p.get("PW") == command.Power.ON # fails with pytest.raises(JvcProjectorError): await p.get("BAD") with pytest.raises(JvcProjectorError): await p.get(command.EShift) @pytest.mark.asyncio async def test_set(dev: AsyncMock): """Test set method.""" p = JvcProjector(IP, port=PORT) await p.connect() # succeeds await p.set(command.Power, command.Power.ON) await p.set("Power", command.Power.ON) await p.set("PW", command.Power.ON) # fails with pytest.raises(JvcProjectorError): await p.set(command.Power, "bad") with pytest.raises(JvcProjectorError): await p.set("BAD", "") with pytest.raises(JvcProjectorError): await p.set(command.EShift, command.EShift.ON) @pytest.mark.asyncio async def test_supports(dev: AsyncMock): """Test support method.""" p = JvcProjector(IP, port=PORT) await p.connect() # succeeds assert p.supports(command.Power) assert p.supports(command.ColorProfile) # fails assert not p.supports(command.LaserPower) with pytest.raises(JvcProjectorError): p.supports("BAD") @pytest.mark.asyncio async def test_describe(dev: AsyncMock): """Test describe method.""" p = JvcProjector(IP, port=PORT) await p.connect() # succeeds info = p.describe(command.Power) assert info["name"] == "Power" assert info["code"] == "PW" assert info["reference"] is True assert info["operation"] is True assert info["category"] == "System" assert info["parameter"]["read"]["0"] == "standby" assert info["parameter"]["read"]["1"] == "on" assert info["parameter"]["write"]["0"] == "off" assert info["parameter"]["write"]["1"] == "on" # fails with pytest.raises(JvcProjectorError): p.describe(command.LaserPower) with pytest.raises(JvcProjectorError): p.describe("BAD") @pytest.mark.asyncio @pytest.mark.parametrize("dev", [{command.ModelName: "B2A3"}], indirect=True) async def test_capabilities(dev: AsyncMock): """Test describe method.""" p = JvcProjector(IP, port=PORT) await p.connect() caps = p.capabilities() assert "Power" in caps assert "ColorProfile" in caps assert "03" in caps["ColorProfile"]["parameter"]["read"] assert "01" not in caps["ColorProfile"]["parameter"]["read"] assert "LaserPower" not in caps pyjvcprojector-2.0.1/tools/000077500000000000000000000000001513216673300157445ustar00rootroot00000000000000pyjvcprojector-2.0.1/tools/update_imports.py000066400000000000000000000021471513216673300213610ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path import re import subprocess import jvcprojector from jvcprojector.command.base import Command import jvcprojector.command.command as command_module def get_class_names() -> list[str]: """Return a sorted list of all Command subclass names defined in the command module.""" names: set[str] = set() for cls in Command.registry["name"].values(): if cls.__module__ == command_module.__name__: names.add(cls.__name__) return sorted(names) def main() -> None: """Rewrite the import line in the jvcprojector/__init__.py with the sorted Command names.""" path = Path(command_module.__file__).resolve().with_name("__init__.py") text = path.read_text(encoding="utf-8") replace = "from .command import " + ", ".join(get_class_names()) pattern = r"^from \.command import[^)]+\)" text = re.sub(pattern, replace, text, flags=re.MULTILINE) path.write_text(text, encoding="utf-8") subprocess.run(["ruff", "check", "--fix", str(path)], check=False) print("Done") if __name__ == "__main__": main()