diff --git a/Makefile b/Makefile deleted file mode 100644 index a95331a..0000000 --- a/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -.PHONY: test dev_install build upload - -dev_install: - python3 -m venv .venv - . .venv/bin/activate && \ - pip3 install --upgrade pyjvc -test: - LOG_LEVEL=debug python -m unittest discover -s tests diff --git a/custom_components/jvc_projectors/__init__.py b/custom_components/jvc_projectors/__init__.py index df7341a..242b52d 100644 --- a/custom_components/jvc_projectors/__init__.py +++ b/custom_components/jvc_projectors/__init__.py @@ -1,92 +1 @@ """The JVC Projector integration.""" - -from __future__ import annotations -import logging -from jvc_projector.jvc_projector import JVCProjectorCoordinator, JVCInput -from homeassistant import config_entries -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_TIMEOUT, - Platform, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant - -from .const import DOMAIN, PLATFORM_SCHEMA - -_LOGGER = logging.getLogger("JVC_projectors") - - -async def async_setup_entry(hass, entry): - """Set up JVC Projector from a config entry.""" - host = entry.data.get(CONF_HOST) - password = entry.data.get(CONF_PASSWORD) - - timeout = entry.data.get(CONF_TIMEOUT, 3) - port = 20554 - options = JVCInput(host, password, port, timeout) - # Create a coordinator or directly set up your entities with the provided information - coordinator = JVCProjectorCoordinator(options, _LOGGER) - - # Store the coordinator in hass.data for use by your platform (e.g., remote) - hass.data[DOMAIN] = coordinator - # Forward the setup to the platform, e.g., remote - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, Platform.REMOTE) - ) - entry.async_on_unload(entry.add_update_listener(async_reload_entry)) - _LOGGER.debug("Set up coordinator") - - return True - - -async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Reload JVC Projector configuration entry.""" - await async_unload_entry(hass, entry) - await async_setup_entry(hass, entry) - - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the component from configuration.yaml.""" - if DOMAIN not in config: - return True - for conf in config[DOMAIN]: - # Check if an entry for this configuration already exists - if any( - entry.data.get(PLATFORM_SCHEMA) == conf[PLATFORM_SCHEMA] - for entry in hass.config_entries.async_entries(DOMAIN) - ): - continue - - # If the entry does not exist, create a new config entry - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf - ) - ) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Handle unloading of JVC Projector integration.""" - # Unload your integration's platforms (e.g., 'remote', 'sensor', etc.) - _LOGGER.debug("Unloading JVC Projector integration") - try: - coordinator: JVCProjectorCoordinator = hass.data[DOMAIN] - await coordinator.close_connection() - except Exception as e: - _LOGGER.error("Error closing JVC Projector connection - %s", e) - unload_ok = await hass.config_entries.async_forward_entry_unload( - entry, Platform.REMOTE - ) - - # If you have other resources to unload (listeners, services, etc.), do it here - _LOGGER.debug("Unloaded JVC Projector integration") - # Return True if unload was successful - try: - hass.data.pop(DOMAIN) - except KeyError: - pass - return unload_ok diff --git a/custom_components/jvc_projectors/config_flow.py b/custom_components/jvc_projectors/config_flow.py deleted file mode 100644 index b588cb3..0000000 --- a/custom_components/jvc_projectors/config_flow.py +++ /dev/null @@ -1,118 +0,0 @@ -import voluptuous as vol -import logging -from homeassistant import config_entries -from homeassistant.core import callback -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD -) -from jvc_projector.jvc_projector import JVCProjectorCoordinator, JVCInput - -from .const import DOMAIN # Import the domain constant - -_LOGGER = logging.getLogger(__name__) - - -class JVCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for JVC Projector.""" - - VERSION = 1 - - async def async_step_user(self, user_input=None): - """Handle the initial step.""" - errors = {} - _LOGGER.debug("user input is %s", user_input) - if user_input is not None: - host = user_input.get(CONF_HOST) - password = user_input.get(CONF_PASSWORD) - timeout = 5 - - valid = await self.validate_setup(host, password, timeout) - - if valid: - await self.async_set_unique_id(user_input[CONF_HOST]) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) - - errors["base"] = "cannot_connect" - - data_schema = vol.Schema( - { - vol.Required(CONF_NAME): str, - vol.Required(CONF_HOST): str, - vol.Optional(CONF_PASSWORD): str - } - ) - - return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors - ) - - async def validate_setup(self, host: str, password: str, timeout: int) -> bool: - """return True if the projector connects""" - try: - options = JVCInput(host, password, 20554, timeout) - coordinator = JVCProjectorCoordinator(options, _LOGGER) - _LOGGER.debug("Validating JVC Projector setup") - res = await coordinator.open_connection() - if res: - _LOGGER.debug("JVC Projector setup connection worked") - await coordinator.close_connection() - _LOGGER.debug("JVC Projector setup connection sucessfully closed") - return True - except Exception as e: - _LOGGER.error( - "Error validating JVC Projector setup. Please check host and password: %s", - e, - ) - return False - return False - - async def async_step_import(self, import_config): - """Handle the import of a configuration from YAML.""" - _LOGGER.debug("importing JVC Projector") - _LOGGER.debug(import_config) - unique_id = import_config.get(CONF_HOST) - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() - _LOGGER.debug("unique id: %s", unique_id) - - return self.async_create_entry( - title=import_config.get(CONF_NAME, "JVC Projector"), data=import_config - ) - - @staticmethod - @callback - def async_get_options_flow(config_entry): - return JVCOptionsFlow(config_entry) - - -class JVCOptionsFlow(config_entries.OptionsFlow): - """Handle JVC options.""" - - def __init__(self, config_entry): - """Initialize JVC options flow.""" - self.config_entry = config_entry - _LOGGER.debug("JVCOptionsFlow init") - - async def async_step_init(self, user_input=None): - """Manage the options.""" - _LOGGER.debug("JVCOptionsFlow init step") - if user_input is not None: - return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) - - current_config = {**self.config_entry.data, **self.config_entry.options} - options_schema = vol.Schema( - { - vol.Optional(CONF_NAME, default=current_config.get(CONF_NAME)): str, - vol.Optional(CONF_HOST, default=current_config.get(CONF_HOST)): str, - vol.Optional( - CONF_PASSWORD, default=current_config.get(CONF_PASSWORD) - ): str - } - ) - - return self.async_show_form(step_id="init", data_schema=options_schema) diff --git a/custom_components/jvc_projectors/const.py b/custom_components/jvc_projectors/const.py index 6d1d0a9..e44fd6d 100644 --- a/custom_components/jvc_projectors/const.py +++ b/custom_components/jvc_projectors/const.py @@ -1,24 +1,5 @@ """Constants for the JVC Projector integration.""" -import voluptuous as vol -from homeassistant.components.remote import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_TIMEOUT, - CONF_SCAN_INTERVAL, -) -from homeassistant.helpers import config_validation as cv -DOMAIN = "jvc_projectors" -# Services -INFO_COMMAND = "info" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Required(CONF_SCAN_INTERVAL): cv.time_period, - vol.Optional(CONF_TIMEOUT): cv.positive_int, - } -) \ No newline at end of file +DOMAIN = "jvc_projectors" +NAME = "JVC Projector Custom" +MANUFACTURER = "JVC" diff --git a/custom_components/jvc_projectors/manifest.json b/custom_components/jvc_projectors/manifest.json index 7976695..3a3dcab 100644 --- a/custom_components/jvc_projectors/manifest.json +++ b/custom_components/jvc_projectors/manifest.json @@ -1,21 +1,18 @@ { "domain": "jvc_projectors", "name": "JVC Projector", - "config_flow": true, - "documentation": "https://github.com/iloveicedgreentea/jvc_homeassistant", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/jvc_projector", "requirements": [ - "pyjvc==4.4.0" + "pyjvc==5.0.0" ], "ssdp": [], "zeroconf": [], "homekit": {}, "dependencies": [], - "loggers": [ - "custom_components.jvc_projectors" - ], "codeowners": [ "@iloveicedgreentea" ], "iot_class": "local_polling", - "version": "4.2.9" + "version": "5.0.0" } \ No newline at end of file diff --git a/custom_components/jvc_projectors/remote.py b/custom_components/jvc_projectors/remote.py index 6f66642..470a3b2 100644 --- a/custom_components/jvc_projectors/remote.py +++ b/custom_components/jvc_projectors/remote.py @@ -2,264 +2,99 @@ from collections.abc import Iterable import logging -import asyncio -from dataclasses import asdict -from typing import Callable -import datetime -import itertools +import threading +from jvc_projector.jvc_projector import JVCProjector +import voluptuous as vol -from jvc_projector.jvc_projector import JVCInput, JVCProjectorCoordinator, Header -from homeassistant.helpers.event import async_track_time_interval - -from .const import DOMAIN - -from homeassistant.components.remote import RemoteEntity +from homeassistant.components.remote import PLATFORM_SCHEMA, RemoteEntity from homeassistant.const import ( + CONF_HOST, CONF_NAME, + CONF_PASSWORD, + CONF_TIMEOUT, ) from homeassistant.core import HomeAssistant - +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -logging.basicConfig( - level=logging.DEBUG, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(funcName)s - Line: %(lineno)d", -) _LOGGER = logging.getLogger(__name__) +# Validation of the user's configuration +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + } +) + + +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType = None, +) -> None: + """Set up platform.""" + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + password = config.get(CONF_PASSWORD) + jvc_client = JVCProjector( + host=host, + password=password, + logger=_LOGGER, + connect_timeout=int(config.get(CONF_TIMEOUT, 3)), + ) + # create a long lived connection + s = jvc_client.open_connection() + if not s: + _LOGGER.error("Failed to connect to the projector") + return + add_entities( + [ + JVCRemote(name, host, jvc_client), + ] + ) + _LOGGER.debug("JVC platform loaded") + class JVCRemote(RemoteEntity): """Implements the interface for JVC Remote in HA.""" def __init__( self, - hass: HomeAssistant, - entry, name: str, - options: JVCInput, - jvc_client: JVCProjectorCoordinator = None, + host: str, + jvc_client: JVCProjector = None, ) -> None: """JVC Init.""" - super().__init__() self._name = name - self._host = options.host - self.entry = entry - # tie the entity to the config flow - self._attr_unique_id = entry.entry_id - + self._host = host self.jvc_client = jvc_client - self.jvc_client.logger = _LOGGER + self.lock = threading.Lock() + # attributes self._state = False + self._model_family = self.jvc_client.model_family + self._attributes = { + "power_state": self._state, + "model": self._model_family, + } - # async queue - self.tasks = [] - # use one queue for all commands - self.command_queue = asyncio.PriorityQueue() - self.attribute_queue = asyncio.Queue() - - self.stop_processing_commands = asyncio.Event() - - self.hass = hass - self._update_interval = None - - # counter for unique IDs - self._counter = itertools.count() - - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - # add the queue handler to the event loop - # get updates in a set interval - self._update_interval = async_track_time_interval( - self.hass, self.async_update_state, datetime.timedelta(seconds=5) - ) - # open connection - _LOGGER.debug("adding conection to loop") - conn = self.hass.loop.create_task(self.open_conn()) - self.tasks.append(conn) - - # handle commands - _LOGGER.debug("adding queue handler to loop") - queue_handler = self.hass.loop.create_task(self.handle_queue()) - self.tasks.append(queue_handler) - - # handle updates - _LOGGER.debug("adding update handler to loop") - update_handler = self.hass.loop.create_task(self.update_worker()) - self.tasks.append(update_handler) - - async def async_will_remove_from_hass(self) -> None: - """close the connection and cancel all tasks when the entity is removed""" - # close connection - # stop scheduled updates - if self._update_interval: - self._update_interval() - self._update_interval = None - - await self.jvc_client.close_connection() - # cancel all tasks - for task in self.tasks: - if not task.done(): - task.cancel() - - async def open_conn(self): - """Open the connection to the projector.""" - _LOGGER.debug("About to open connection with jvc_client: %s", self.jvc_client) - try: - _LOGGER.debug("Opening connection to %s", self.host) - res = await asyncio.wait_for(self.jvc_client.open_connection(), timeout=3) - if res: - _LOGGER.debug("Connection to %s opened", self.host) - return True - except asyncio.TimeoutError: - _LOGGER.warning("Timeout while trying to connect to %s", self._host) - except asyncio.CancelledError: - return - # intentionally broad - except TypeError as err: - # this is benign, just means the PJ is not connected yet - _LOGGER.debug("open_connection: %s", err) - return - except Exception as err: - _LOGGER.error("some error happened with open_connection: %s", err) - await asyncio.sleep(5) - - async def generate_unique_id(self) -> int: - """this is used to sort the queue because it contains non-comparable items""" - return next(self._counter) - - async def handle_queue(self): - """ - Handle items in command queue. - This is run in an event loop - """ - while True: - if not self.jvc_client.connection_open: - _LOGGER.debug("Connection is closed not processing commands") - await asyncio.sleep(5) - continue - try: - # send all commands in queue - _LOGGER.debug("processing commands") - # can be a command or a tuple[function, attribute] - # first item is the priority - try: - priority, item = await asyncio.wait_for( - self.command_queue.get(), timeout=5 - ) - except asyncio.TimeoutError: - _LOGGER.debug("Timeout in command queue") - continue - _LOGGER.debug("got queue item %s with priority %s", item, priority) - # if its a 3 its an attribute tuple - if len(item) == 3: - # discard the unique ID - _, getter, attribute = item - _LOGGER.debug( - "trying attribute %s with getter %s", attribute, getter - ) - try: - value = await asyncio.wait_for(getter(), timeout=3) - except asyncio.TimeoutError: - _LOGGER.debug("Timeout with item %s", item) - try: - self.command_queue.task_done() - except ValueError: - pass - continue - _LOGGER.debug("got value %s for attribute %s", value, attribute) - setattr(self.jvc_client.attributes, attribute, value) - self.async_write_ha_state() - elif len(item) == 2: - # run the item and set type to operation - # HA sends commands like ["power, on"] which is one item - _, command = item - _LOGGER.debug("executing command %s", command) - try: - await asyncio.wait_for( - self.jvc_client.exec_command( - command, Header.operation.value - ), - timeout=5, - ) - except asyncio.TimeoutError: - _LOGGER.debug("Timeout with command %s", command) - try: - self.command_queue.task_done() - except ValueError: - pass - continue - try: - self.command_queue.task_done() - except ValueError: - pass - await asyncio.sleep(0.1) - # if we are stopping and the queue is not empty, clear it - # this is so it doesnt continuously print the stopped processing commands message - if ( - self.stop_processing_commands.is_set() - and not self.command_queue.empty() - ): - await self.clear_queue() - _LOGGER.debug("Stopped processing commands") - # break to the outer loop so it can restart itself if needed - break - # save cpu - await asyncio.sleep(0.1) - except asyncio.CancelledError: - _LOGGER.debug("handle_queue cancelled") - return - except TypeError as err: - _LOGGER.debug("TypeError in handle_queue, moving on: %s -- %s", err, item) - # in this case likely the queue priority is the same, lets just skip it - self.command_queue.task_done() - continue - except Exception as err: # pylint: disable=broad-except - _LOGGER.error("Unhandled exception in handle_queue: %s", err) - await asyncio.sleep(5) - - async def clear_queue(self): - """Clear the queue""" - try: - # clear the queue - while not self.command_queue.empty(): - self.command_queue.get_nowait() - self.command_queue.task_done() - - while not self.attribute_queue.empty(): - self.attribute_queue.get_nowait() - self.attribute_queue.task_done() - # reset the counter - self._counter = itertools.count() - except ValueError: - pass - - async def update_worker(self): - """Gets a function and attribute from a queue and adds it to the command interface""" - while True: - # this is just an async interface so the other processor doesnt become complicated + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + _LOGGER.info("JVCRemote entity added to hass: %s", self._name) - # getter will be a Callable - try: - unique_id, getter, attribute = await self.attribute_queue.get() - # add to the command queue with a single interface - await self.command_queue.put((1, (unique_id, getter, attribute))) - try: - self.attribute_queue.task_done() - except ValueError: - pass - except asyncio.TimeoutError: - _LOGGER.debug("Timeout in update_worker") - except asyncio.CancelledError: - _LOGGER.debug("update_worker cancelled") - return - await asyncio.sleep(0.1) + async def async_will_remove_from_hass(self): + """Call when entity will be removed from hass.""" + _LOGGER.info("JVCRemote entity will be removed from hass: %s", self._name) + self.jvc_client.close_connection() @property def should_poll(self): """Poll.""" - return False + return True @property def name(self): @@ -275,213 +110,144 @@ def host(self): def extra_state_attributes(self): """Return extra state attributes.""" # Separate views for models to be cleaner - if self._state: - all_attr = asdict(self.jvc_client.attributes) - # remove lamp stuff if its a laser - if "NZ" in self.jvc_client.model_family: - all_attr.pop("lamp_power") - all_attr.pop("lamp_time") - - return all_attr - return { - "power_state": self._state, - "model": self.jvc_client.model_family, - "connection_state": self.jvc_client.attributes.connection_active, - } + return self._attributes @property def is_on(self): """Return the last known state of the projector.""" + return self._state - async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument + def turn_on(self, **kwargs): """Send the power on command.""" - self._state = True - - try: - await self.jvc_client.power_on() - self.stop_processing_commands.clear() - # save state - except Exception as err: # pylint: disable=broad-except - _LOGGER.error("Error turning on projector: %s", err) - self._state = False - finally: - self.async_write_ha_state() + with self.lock: + try: + self.jvc_client.power_on() + self._state = True + self._attributes["power_state"] = self._state + except Exception as e: + _LOGGER.error("Failed to turn on the projector: %s", e) - async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument + def turn_off(self, **kwargs): """Send the power off command.""" - self._state = False - - try: - await self.jvc_client.power_off() - self.stop_processing_commands.set() - await self.clear_queue() - self.jvc_client.attributes.connection_active = False - # save state - except Exception as err: # pylint: disable=broad-except - _LOGGER.error("Error turning off projector: %s", err) - self._state = False - finally: - self.async_write_ha_state() - - async def make_updates(self, attribute_getters: list[tuple[Callable, str]]): - """Add all the attribute getters to the queue.""" - for getter, name in attribute_getters: - # you might be thinking why is this here? - # oh boy let me tell you - # TLDR priority queues need a unique ID to sort and you need to just dump one in - # otherwise you get a TypeError that home assistant HIDES from you and you spend a week figuring out - # why this function deadlocks for no reason, and that HA hides error raises - # because the underlying items are not sortable - unique_id = await self.generate_unique_id() - await self.attribute_queue.put((unique_id, getter, name)) - - # wait for attributes to be received - await self.attribute_queue.join() - # extra sleep to make sure all the updates are done - await asyncio.sleep(0.5) + with self.lock: + try: + self.jvc_client.power_off() + self._state = False + self._attributes["power_state"] = self._state + except Exception as e: + _LOGGER.error("Failed to turn off the projector: %s", e) - async def async_update_state(self, _): + def update(self): """Retrieve latest state.""" - if self.jvc_client.connection_open is True: - # certain commands can only run at certain times - # if they fail (i.e grayed out on menu) JVC will simply time out. Bad UX - # have to add specific commands in a precise order - attribute_getters = [] - # get power - attribute_getters.append((self.jvc_client.is_on, "power_state")) - await self.make_updates(attribute_getters) + with self.lock: + try: + self._state = self.jvc_client.is_on() + self._attributes["power_state"] = self._state - self._state = self.jvc_client.attributes.power_state - _LOGGER.debug("power state is : %s", self._state) + if self._state: + self._update_common_attributes() + self._update_model_specific_attributes() + self._update_hdr_attributes() - if self._state: - _LOGGER.debug("getting signal status and picture mode") - # takes a func and an attribute to write result into - attribute_getters.extend( - [ - (self.jvc_client.get_source_status, "signal_status"), - (self.jvc_client.get_picture_mode, "picture_mode"), - (self.jvc_client.get_software_version, "software_version"), - ] - ) - # determine how to proceed based on above - await self.make_updates(attribute_getters) - if self.jvc_client.attributes.signal_status is True: - _LOGGER.debug("getting content type and input mode") - attribute_getters.extend( - [ - (self.jvc_client.get_content_type, "content_type"), - ( - self.jvc_client.get_content_type_trans, - "content_type_trans", - ), - (self.jvc_client.get_input_mode, "input_mode"), - (self.jvc_client.get_anamorphic, "anamorphic_mode"), - (self.jvc_client.get_source_display, "resolution"), - ] - ) - if "Unsupported" not in self.jvc_client.model_family: - attribute_getters.extend( - [ - (self.jvc_client.get_install_mode, "installation_mode"), - (self.jvc_client.get_aspect_ratio, "aspect_ratio"), - (self.jvc_client.get_color_mode, "color_mode"), - (self.jvc_client.get_input_level, "input_level"), - (self.jvc_client.get_mask_mode, "mask_mode"), - ] - ) - if any(x in self.jvc_client.model_family for x in ["NX9", "NZ"]): - attribute_getters.append( - (self.jvc_client.get_eshift_mode, "eshift"), - ) - if "NZ" in self.jvc_client.model_family: - attribute_getters.extend( - [ - (self.jvc_client.get_laser_power, "laser_power"), - (self.jvc_client.get_laser_mode, "laser_mode"), - (self.jvc_client.is_ll_on, "low_latency"), - (self.jvc_client.get_lamp_time, "laser_time"), - ] - ) - else: - attribute_getters.extend( - [ - (self.jvc_client.get_lamp_power, "lamp_power"), - (self.jvc_client.get_lamp_time, "lamp_time"), - ] - ) - - await self.make_updates(attribute_getters) + except Exception as e: + _LOGGER.error("Failed to update the projector state: %s", e) - # get laser value if fw is a least 3.0 - if "NZ" in self.jvc_client.model_family: - try: - if float(self.jvc_client.attributes.software_version) >= 3.00: - attribute_getters.extend( - [ - (self.jvc_client.get_laser_value, "laser_value"), - ] - ) - except ValueError: - pass - # HDR stuff - if any( - x in self.jvc_client.attributes.content_type_trans - for x in ["hdr", "hlg"] - ): - if "NZ" in self.jvc_client.model_family: - attribute_getters.append( - ( - self.jvc_client.get_theater_optimizer_state, - "theater_optimizer", - ), - ) - attribute_getters.extend( - [ - (self.jvc_client.get_hdr_processing, "hdr_processing"), - (self.jvc_client.get_hdr_level, "hdr_level"), - (self.jvc_client.get_hdr_data, "hdr_data"), - ] + def _update_common_attributes(self): + """Update common attributes.""" + try: + self._attributes.update( + { + "low_latency": self.jvc_client.is_ll_on(), + "picture_mode": self.jvc_client.get_picture_mode(), + "input_mode": self.jvc_client.get_input_mode(), + } + ) + except TimeoutError as e: + _LOGGER.error("Timeout while updating common attributes: %s", e) + except TypeError as e: + _LOGGER.debug("Type error while updating common attributes: %s", e) + except Exception as e: + _LOGGER.error("Failed to update common attributes: %s", e) + + def _update_model_specific_attributes(self): + """Update model-specific attributes.""" + try: + if "Unsupported" not in self._model_family: + self._attributes.update( + { + "installation_mode": self.jvc_client.get_install_mode(), + "picture_mode": self.jvc_client.get_picture_mode(), + "aspect_ratio": self.jvc_client.get_aspect_ratio(), + "color_mode": self.jvc_client.get_color_mode(), + "input_level": self.jvc_client.get_input_level(), + "mask_mode": self.jvc_client.get_mask_mode(), + "signal_status": self.jvc_client.get_source_status(), + "resolution": self.jvc_client.get_source_display(), + "anamorphic_mode": self.jvc_client.get_anamorphic(), + "firmware_version": self.jvc_client.get_software_version(), + "low_latency": self.jvc_client.is_ll_on(), + } + ) + if self._attributes.get("signal_status"): + self._attributes.update( + { + "content_type": self.jvc_client.get_content_type(), + "content_type_trans": self.jvc_client.get_content_type_trans(), + } ) - - # get all the updates - await self.make_updates(attribute_getters) + if "NX9" in self._model_family or "NZ" in self._model_family: + self._attributes["eshift"] = self.jvc_client.get_eshift_mode() + if "NZ" in self._model_family: + self._attributes.update( + { + "laser_mode": self.jvc_client.get_laser_mode(), + "laser_power": self.jvc_client.get_laser_power(), + "laser_value": self.jvc_client.get_laser_value(), + "laser_time": self.jvc_client.get_lamp_time(), + } + ) else: - _LOGGER.debug("PJ is off") - # set the model and power - self.jvc_client.attributes.model = self.jvc_client.model_family - self.async_write_ha_state() - - async def async_send_command(self, command: Iterable[str], **kwargs): + self._attributes["lamp_power"] = self.jvc_client.get_lamp_power() + self._attributes["lamp_time"] = self.jvc_client.get_lamp_time() + except TimeoutError as e: + _LOGGER.error("Timeout while updating model-specific attributes: %s", e) + except TypeError as e: + _LOGGER.debug("Type error while updating model-specific attributes: %s", e) + except Exception as e: + _LOGGER.error("Failed to update model-specific attributes: %s", e) + + def _update_hdr_attributes(self): + """Update HDR-related attributes.""" + try: + if any( + x in self._attributes.get("content_type_trans") for x in ["hdr", "hlg"] + ): + if "NZ" in self._model_family: + self._attributes["theater_optimizer"] = ( + self.jvc_client.get_theater_optimizer_state() + ) + self._attributes.update( + { + "hdr_processing": self.jvc_client.get_hdr_processing(), + "hdr_level": self.jvc_client.get_hdr_level(), + "hdr_data": self.jvc_client.get_hdr_data(), + } + ) + except TimeoutError as e: + _LOGGER.error("Timeout while updating HDR attributes: %s", e) + except TypeError as e: + _LOGGER.debug("Type error while updating HDR attributes: %s", e) + except Exception as e: + _LOGGER.error("Failed to update HDR attributes: %s", e) + + def send_command(self, command: Iterable[str], **kwargs): """Send commands to a device.""" - _LOGGER.debug("adding command %s to queue", command) - # add counter to preserve cmd order - unique_id = await self.generate_unique_id() - await self.command_queue.put((0, (unique_id, command))) - _LOGGER.debug("command %s added to queue with counter %s", command, unique_id) - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -): - """Set up JVC Remote based on a config entry.""" - # Retrieve your setup data or coordinator from hass.data - coordinator = hass.data[DOMAIN] - - # You might need to adjust this part based on how your coordinator is structured - # and how it provides access to device/client information - name = entry.data.get(CONF_NAME) - options = ( - coordinator.options - ) # Assuming your coordinator has an attribute 'options' - jvc_client = coordinator # Assuming the coordinator acts as the client - - # Setup your entities and add them - _LOGGER.debug("Setting up JVC Projector with options: %s", options) - async_add_entities( - [JVCRemote(hass, entry, name, options, jvc_client)], update_before_add=False - ) + with self.lock: + try: + self.jvc_client.exec_command(command) + except Exception as e: + _LOGGER.error("Failed to send command %s: %s", command, e) diff --git a/custom_components/jvc_projectors/strings.json b/custom_components/jvc_projectors/strings.json deleted file mode 100644 index 2ac8710..0000000 --- a/custom_components/jvc_projectors/strings.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "config": { - "title": "JVC Projector", - "step": { - "user": { - "title": "Connect to your JVC Projector", - "data": { - "name": "[%key:common::config_flow::data::name%]", - "host": "[%key:common::config_flow::data::host%]", - "password": "[%key:common::config_flow::data::password%]" - }, - "data_description": { - "name": "Name for your projector", - "host": "IP address or hostname of projector", - "password": "Optional password if NZ series or higher" - } - } - }, - "error": { - "cannot_connect": "Failed to connect to the projector. Please check the host and try again." - }, - "success": { - "create_entry": "Successfully connected to your JVC Projector." - } - }, - "options": { - "step": { - "init": { - "title": "JVC Projector Options", - "description": "Configure your JVC Projector settings.", - "data": { - "name": "Name", - "host": "Host", - "password": "Password", - "timeout": "Timeout" - } - } - } - } -} diff --git a/info.md b/info.md index 6a70448..f7718a4 100644 --- a/info.md +++ b/info.md @@ -1,4 +1,4 @@ -# JVC Projectors Home Assistant Integration +# JVC Projector Remote Improved Home Assistant This is the Home Assistant JVC Component implementing my [JVC library](https://github.com/iloveicedgreentea/jvc_projector_improved) @@ -6,281 +6,35 @@ This is the Home Assistant JVC Component implementing my [JVC library](https://g All the features in my [JVC library](https://github.com/iloveicedgreentea/jvc_projector_improved) including: -- Config Flow/UI setup - Power - Picture Modes - Laser power and dimming -- Pretty much every JVC command -- Entity Attributes for current settings like power state, picture mode, laser mode, input, etc -- Async processing +- Low Latency meta-functions +- Optimal gaming and movie setting meta-functions +- and so on -Note: JVC projectors currently only support a single network connection at a time. If you're running other control systems or attempt to run the JVC AutoCal software, keep in mind you can only have one control system connected at a time. - -Note: Only NX and NZ series are officially supported but this should work with any JVC projector that has an ethernet port. +Because everything is async, it will run each button/command in the order it received. so commands won't disappear from the queue due to JVCs PJ server requiring the handshake. It uses a single persistent connection so any delay you see is because of HA processing. ## Installation This is currently only a custom component. Unlikely to make it into HA core because their process is just too burdensome. -Install HACS, then install the component by adding this repository as a custom repo. More details here - https://hacs.xyz/docs/faq/custom_repositories +Install HACS, then install the component by adding this as a custom repo +https://hacs.xyz/docs/faq/custom_repositories You can also just copy all the files into your custom_components folder but then you won't have automatic updates. ### Home Assistant Setup -This uses Config Flow. Install the custom component, restart, then add an integration as you normally do. Search JVC, and find the one that shows a box icon next to it. There is an official JVC Integration but it is limited and unrelated to this one. - -*upon HA restart, it will automatically reconnect. No action is needed from you* -### Adding attributes as sensors - -replace nz7 with the name of your remote entity ```yaml -sensor: - platform: template - sensors: - jvc_installation_mode: - value_template: > - {% if is_state('remote.nz7', 'on') %} - {{ states.remote.nz7.attributes.installation_mode }} - {% else %} - Off - {% endif %} -``` - -### Attributes - -These are the attributes supported for the entity -* power_state -* signal_status -* picture_mode -* installation_mode -* laser_power -* laser_mode -* lamp_power -* model -* content_type -* content_type_trans (the transition - sdr or hdr) -* hdr_data -* hdr_processing -* hdr_level -* theater_optimizer -* low_latency -* input_mode -* input_level -* color_mode -* aspect_ratio -* eshift -* mask_mode -* software_version -* lamp_time - -## Usage - -Use the `remote.send_command` service to send commands to the projector. - -`$command,$parameter` -example: "anamorphic,off" -example: "anamorphic,d" -example: "laser_dim,auto3" - -It also supports using remote codes as ASCII [found here](https://support.jvc.com/consumer/support/documents/DILAremoteControlGuide.pdf) (Code A only) - -example: "remote,2E" - -``` -Currently Supported Commands: - anamorphic - aperture - aspect_ratio - color_mode - content_type - content_type_trans - enhance - eshift_mode - get_model - get_software_version - graphic_mode - hdr_data - hdr_level - hdr_processing - input_level - input_mode - installation_mode - lamp_power - lamp_time - laser_mode - laser_power - low_latency - mask - menu - motion_enhance - picture_mode - power - remote - signal_3d - source_status - theater_optimizer - - -Currently Supported Parameters: -AnamorphicModes - off - a - b - c - d -ApertureModes - off - auto1 - auto2 -AspectRatioModes - zoom - auto - native -ColorSpaceModes - auto - YCbCr444 - YCbCr422 - RGB -ContentTypeTrans - sdr - hdr10_plus - hdr10 - hlg -ContentTypes - auto - sdr - hdr10_plus - hdr10 - hlg -EnhanceModes - zero - one - two - three - four - five - six - seven - eight - nine - ten -EshiftModes - off - on -GraphicModeModes - standard - hires1 - hires2 -HdrData - sdr - hdr - smpte - hybridlog - hdr10_plus - none -HdrLevel - auto - min2 - min1 - zero - plus1 - plus2 -HdrProcessing - hdr10_plus - static - frame_by_frame - scene_by_scene -InputLevel - standard - enhanced - superwhite - auto -InputModes - hdmi1 - hdmi2 -InstallationModes - mode1 - mode2 - mode3 - mode4 - mode5 - mode6 - mode7 - mode8 - mode9 - mode10 -LampPowerModes - normal - high -LaserModes - off - auto1 - auto2 - auto3 -LaserPowerModes - low - med - high -LowLatencyModes - off - on -MaskModes - on - off -MenuModes - menu - lens_control - up - down - back - left - right - ok -MotionEnhanceModes - off - low - high -PictureModes - film - cinema - natural - hdr - thx - frame_adapt_hdr - user1 - user2 - user3 - user4 - user5 - user6 - hlg - hdr_plus - pana_pq - filmmaker - frame_adapt_hdr2 - frame_adapt_hdr3 -PowerModes - off - on -PowerStates - standby - on - cooling - reserved - emergency -SourceStatuses - logo - no_signal - signal -TheaterOptimizer - off - on -ThreeD - auto - sbs - ou - 2d +# configuration.yaml +remote: + - platform: jvc_projectors + name: { friendly name } + password: { password } + host: { IP addr } + timeout: { seconds } (optional) + scan_interval: 15 # recommend 15-30. Attributes will poll in this interval ``` ## Useful Stuff @@ -289,39 +43,9 @@ I used this to re-create the JVC remote in HA. Add the YAML to your dashboard to Add this sensor to your configuration.yml. Replace the nz7 with the name of your entity. Restart HA. -### Automating HDR modes per Harmony activity -```yaml -alias: JVC - HDR PM Automation -description: "" -trigger: - - platform: state - entity_id: - - remote.nz7 - attribute: content_type -condition: - - condition: not - conditions: - - condition: state - entity_id: remote.nz7 - attribute: content_type - state: sdr -action: - - if: - - condition: state - entity_id: select.harmony_hub_2_activities - state: Game - then: - - service: remote.send_command - data: - command: picture_mode,hdr10 -mode: single - -``` - -### Remote in UI ```yaml sensor: -- platform: template + platform: template sensors: jvc_low_latency: value_template: > @@ -336,7 +60,7 @@ sensor: {% endif %} ``` -Here is something to get you started. You can add buttons and sensors as needed. +Add this to lovelace ```yaml type: grid @@ -466,5 +190,48 @@ cards: tap_action: action: toggle show_icon: false + - type: button + tap_action: + action: call-service + service: jvc_projectors.gaming_mode_hdr + service_data: {} + target: + entity_id: remote.nz7 + show_icon: false + show_name: true + hold_action: + action: none + name: Game HDR + - type: button + tap_action: + action: call-service + service: jvc_projectors.gaming_mode_sdr + service_data: {} + target: + entity_id: remote.nz7 + show_icon: false + name: Game SDR + - type: button + tap_action: + action: call-service + service: jvc_projectors.hdr_picture_mode + service_data: {} + target: + entity_id: remote.nz7 + show_icon: false + name: Film HDR + - type: button + tap_action: + action: call-service + service: jvc_projectors.sdr_picture_mode + service_data: {} + target: + entity_id: remote.nz7 + show_icon: false + name: Film SDR + - type: button + tap_action: + action: toggle + show_icon: false columns: 5 ``` diff --git a/readme.md b/readme.md index 6a70448..2c2e6b5 100644 --- a/readme.md +++ b/readme.md @@ -1,35 +1,46 @@ # JVC Projectors Home Assistant Integration +This is archived because I am dedicating my efforts to creating essentially Home Assistant but for home theaters specifically, in Go. + +If you have issues, switch to the "official" JVC integration. It doesn't have every feature and attribute at the moment though. + +This is a Home Assistant JVC Custom Component implementing my [JVC library](https://github.com/iloveicedgreentea/jvc_projector_python) -This is the Home Assistant JVC Component implementing my [JVC library](https://github.com/iloveicedgreentea/jvc_projector_improved) ## Features -All the features in my [JVC library](https://github.com/iloveicedgreentea/jvc_projector_improved) including: +All the features in my [JVC library](https://github.com/iloveicedgreentea/jvc_projector_python) including: -- Config Flow/UI setup - Power - Picture Modes - Laser power and dimming - Pretty much every JVC command -- Entity Attributes for current settings like power state, picture mode, laser mode, input, etc -- Async processing +- HA Attributes for current settings like power state, picture mode, laser mode, input, etc +- and so on -Note: JVC projectors currently only support a single network connection at a time. If you're running other control systems or attempt to run the JVC AutoCal software, keep in mind you can only have one control system connected at a time. -Note: Only NX and NZ series are officially supported but this should work with any JVC projector that has an ethernet port. +Note: JVC projectors currently only support a single network connection at a time. If you're running other control systems or attempt to run the JVC AutoCal software, keep in mind you can only have one control system connected at a time. ## Installation -This is currently only a custom component. Unlikely to make it into HA core because their process is just too burdensome. +This is a custom component. -Install HACS, then install the component by adding this repository as a custom repo. More details here - https://hacs.xyz/docs/faq/custom_repositories +Install HACS, then install the component by adding this as a custom repo +https://hacs.xyz/docs/faq/custom_repositories -You can also just copy all the files into your custom_components folder but then you won't have automatic updates. +You can also just copy all the files into your custom_components folder but then you won't get automatic updates. ### Home Assistant Setup -This uses Config Flow. Install the custom component, restart, then add an integration as you normally do. Search JVC, and find the one that shows a box icon next to it. There is an official JVC Integration but it is limited and unrelated to this one. -*upon HA restart, it will automatically reconnect. No action is needed from you* +```yaml +# configuration.yaml +remote: + - platform: jvc_projectors + name: { entity name } + password: { password } (optional for non-NZ) + host: { IP addr } +``` + +You can use the remote entity attributes in sensors, automations, etc. ### Adding attributes as sensors @@ -47,33 +58,6 @@ sensor: {% endif %} ``` -### Attributes - -These are the attributes supported for the entity -* power_state -* signal_status -* picture_mode -* installation_mode -* laser_power -* laser_mode -* lamp_power -* model -* content_type -* content_type_trans (the transition - sdr or hdr) -* hdr_data -* hdr_processing -* hdr_level -* theater_optimizer -* low_latency -* input_mode -* input_level -* color_mode -* aspect_ratio -* eshift -* mask_mode -* software_version -* lamp_time - ## Usage Use the `remote.send_command` service to send commands to the projector. @@ -282,189 +266,3 @@ ThreeD ou 2d ``` - -## Useful Stuff - -I used this to re-create the JVC remote in HA. Add the YAML to your dashboard to get a grid which resembles most of the remote. Other functions can be used via remote.send_command. See the library readme for details. - -Add this sensor to your configuration.yml. Replace the nz7 with the name of your entity. Restart HA. - -### Automating HDR modes per Harmony activity -```yaml -alias: JVC - HDR PM Automation -description: "" -trigger: - - platform: state - entity_id: - - remote.nz7 - attribute: content_type -condition: - - condition: not - conditions: - - condition: state - entity_id: remote.nz7 - attribute: content_type - state: sdr -action: - - if: - - condition: state - entity_id: select.harmony_hub_2_activities - state: Game - then: - - service: remote.send_command - data: - command: picture_mode,hdr10 -mode: single - -``` - -### Remote in UI -```yaml -sensor: -- platform: template - sensors: - jvc_low_latency: - value_template: > - {% if is_state('remote.nz7', 'on') %} - {% if states.remote.nz7.attributes.low_latency == false %} - Off - {% elif states.remote.nz7.attributes.low_latency == true %} - On - {% endif %} - {% else %} - Off - {% endif %} -``` - -Here is something to get you started. You can add buttons and sensors as needed. - -```yaml -type: grid -cards: - - type: button - name: Power - show_icon: false - entity: remote.nz7 - show_state: true - show_name: true - icon: mdi:power - - type: button - tap_action: - action: call-service - service: jvc_projectors.info - service_data: {} - target: - entity_id: remote.nz7 - show_icon: false - name: Info - hold_action: - action: none - - type: button - tap_action: - action: call-service - service: remote.send_command - service_data: - command: menu,up - target: - entity_id: remote.nz7 - show_name: false - show_icon: true - icon: mdi:arrow-up - hold_action: - action: none - - type: button - tap_action: - action: call-service - service: remote.send_command - service_data: - command: menu,menu - target: - entity_id: remote.nz7 - show_name: true - show_icon: false - name: Menu - hold_action: - action: none - - type: button - tap_action: - action: toggle - show_icon: false - - type: button - tap_action: - action: none - show_icon: false - entity: sensor.jvc_low_latency - show_name: true - show_state: true - name: Low Latency - hold_action: - action: none - - type: button - tap_action: - action: call-service - service: remote.send_command - service_data: - command: menu,left - target: - entity_id: remote.nz7 - show_name: false - icon: mdi:arrow-left - - type: button - tap_action: - action: call-service - service: remote.send_command - service_data: - command: menu, ok - target: - entity_id: remote.nz7 - name: OK - show_icon: false - - type: button - tap_action: - action: call-service - service: remote.send_command - service_data: - command: menu, right - target: - entity_id: remote.nz7 - show_name: false - icon: mdi:arrow-right - - type: button - tap_action: - action: toggle - show_icon: false - show_name: false - - type: button - tap_action: - action: toggle - show_icon: false - - type: button - tap_action: - action: call-service - service: remote.send_command - service_data: - command: menu,back - target: - entity_id: remote.nz7 - name: Back - show_icon: false - - type: button - tap_action: - action: call-service - service: remote.send_command - service_data: - command: menu,down - target: - entity_id: remote.nz7 - show_name: false - icon: mdi:arrow-down - - type: button - tap_action: - action: toggle - show_icon: false - - type: button - tap_action: - action: toggle - show_icon: false -columns: 5 -```