diff --git a/homeassistant/components/jvc_projector/__init__.py b/homeassistant/components/jvc_projector/__init__.py index 8ce1fb46e3d1af..c24818fd70f943 100644 --- a/homeassistant/components/jvc_projector/__init__.py +++ b/homeassistant/components/jvc_projector/__init__.py @@ -2,7 +2,8 @@ from __future__ import annotations -from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorConnectError +from jvcprojector.device import JvcProjectorAuthError +from jvcprojector.projector import JvcProjector, JvcProjectorConnectError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -15,18 +16,20 @@ from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from .const import DOMAIN from .coordinator import JvcProjectorDataUpdateCoordinator +type JVCConfigEntry = ConfigEntry[JvcProjectorDataUpdateCoordinator] + PLATFORMS = [Platform.BINARY_SENSOR, Platform.REMOTE, Platform.SELECT, Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: JVCConfigEntry) -> bool: """Set up integration from a config entry.""" device = JvcProjector( host=entry.data[CONF_HOST], port=entry.data[CONF_PORT], password=entry.data[CONF_PASSWORD], + timeout=1, ) try: @@ -43,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = JvcProjectorDataUpdateCoordinator(hass, device) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator async def disconnect(event: Event) -> None: await device.disconnect() @@ -57,9 +60,8 @@ async def disconnect(event: Event) -> None: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: JVCConfigEntry) -> bool: """Unload config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - await hass.data[DOMAIN][entry.entry_id].device.disconnect() - hass.data[DOMAIN].pop(entry.entry_id) + await entry.runtime_data.device.disconnect() return unload_ok diff --git a/homeassistant/components/jvc_projector/binary_sensor.py b/homeassistant/components/jvc_projector/binary_sensor.py index 7e8788aa0a63ab..f2c54c2140fd49 100644 --- a/homeassistant/components/jvc_projector/binary_sensor.py +++ b/homeassistant/components/jvc_projector/binary_sensor.py @@ -2,43 +2,96 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass + from jvcprojector import const -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.config_entries import ConfigEntry +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import JvcProjectorDataUpdateCoordinator -from .const import DOMAIN +from . import JVCConfigEntry, JvcProjectorDataUpdateCoordinator from .entity import JvcProjectorEntity -ON_STATUS = (const.ON, const.WARMING) + +@dataclass(frozen=True, kw_only=True) +class JVCBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describe JVC binary sensor entity.""" + + value_fn: Callable[[str | None], bool | None] = lambda x: x == "on" + enabled_default: bool | Callable[[JvcProjectorEntity], bool] = True + + +JVC_BINARY_SENSORS = ( + JVCBinarySensorEntityDescription( + key=const.KEY_POWER, + translation_key="jvc_power", + device_class=BinarySensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda x: x in (const.ON, const.WARMING) if x is not None else None, + ), + JVCBinarySensorEntityDescription( + key=const.KEY_LOW_LATENCY, + translation_key="jvc_low_latency_status", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda x: x == const.ON if x is not None else None, + ), + JVCBinarySensorEntityDescription( + key=const.KEY_ESHIFT, + translation_key="jvc_eshift", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda x: x == const.ON if x is not None else None, + enabled_default=JvcProjectorEntity.has_eshift, + ), + JVCBinarySensorEntityDescription( + key=const.KEY_SOURCE, + translation_key="jvc_source_status", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda x: x == const.SIGNAL if x is not None else None, + ), +) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: JVCConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the JVC Projector platform from a config entry.""" - coordinator: JvcProjectorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - - async_add_entities([JvcBinarySensor(coordinator)]) + """Set up the JVC Projector binary sensor platform from a config entry.""" + coordinator = entry.runtime_data + async_add_entities( + JvcBinarySensor(coordinator, description) for description in JVC_BINARY_SENSORS + ) class JvcBinarySensor(JvcProjectorEntity, BinarySensorEntity): """The entity class for JVC Projector Binary Sensor.""" - _attr_translation_key = "jvc_power" + entity_description: JVCBinarySensorEntityDescription def __init__( self, coordinator: JvcProjectorDataUpdateCoordinator, + description: JVCBinarySensorEntityDescription, ) -> None: - """Initialize the JVC Projector sensor.""" + """Initialize the JVC Projector binary sensor.""" super().__init__(coordinator) - self._attr_unique_id = f"{coordinator.device.mac}_power" + self.entity_description = description + self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" + self._attr_entity_registry_enabled_default = ( + description.enabled_default(self) + if callable(description.enabled_default) + else description.enabled_default + ) @property - def is_on(self) -> bool: - """Return true if the JVC is on.""" - return self.coordinator.data["power"] in ON_STATUS + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + value = self.coordinator.data.get(self.entity_description.key) + if value is None: + return None + return self.entity_description.value_fn(value) diff --git a/homeassistant/components/jvc_projector/config_flow.py b/homeassistant/components/jvc_projector/config_flow.py index 7fbfb17a976281..2ec3e50cd58db2 100644 --- a/homeassistant/components/jvc_projector/config_flow.py +++ b/homeassistant/components/jvc_projector/config_flow.py @@ -5,15 +5,16 @@ from collections.abc import Mapping from typing import Any -from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorConnectError -from jvcprojector.projector import DEFAULT_PORT +from jvcprojector.device import JvcProjectorAuthError +from jvcprojector.projector import DEFAULT_PORT, JvcProjector, JvcProjectorConnectError import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.helpers.device_registry import format_mac from homeassistant.util.network import is_host_valid +from . import JVCConfigEntry from .const import DOMAIN, NAME @@ -22,7 +23,7 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - _reauth_entry: ConfigEntry | None = None + _reauth_entry: JVCConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/jvc_projector/const.py b/homeassistant/components/jvc_projector/const.py index e15aa93bfa5db0..1ad7bf112afd06 100644 --- a/homeassistant/components/jvc_projector/const.py +++ b/homeassistant/components/jvc_projector/const.py @@ -1,5 +1,40 @@ """Constants for the jvc_projector integration.""" +from jvcprojector import const + NAME = "JVC Projector" DOMAIN = "jvc_projector" MANUFACTURER = "JVC" + +REMOTE_COMMANDS = { + "menu": const.REMOTE_MENU, + "up": const.REMOTE_UP, + "down": const.REMOTE_DOWN, + "left": const.REMOTE_LEFT, + "right": const.REMOTE_RIGHT, + "ok": const.REMOTE_OK, + "back": const.REMOTE_BACK, + "mpc": const.REMOTE_MPC, + "hide": const.REMOTE_HIDE, + "info": const.REMOTE_INFO, + "input": const.REMOTE_INPUT, + "cmd": const.REMOTE_CMD, + "advanced_menu": const.REMOTE_ADVANCED_MENU, + "picture_mode": const.REMOTE_PICTURE_MODE, + "color_profile": const.REMOTE_COLOR_PROFILE, + "lens_control": const.REMOTE_LENS_CONTROL, + "setting_memory": const.REMOTE_SETTING_MEMORY, + "gamma_settings": const.REMOTE_GAMMA_SETTINGS, + "hdmi_1": const.REMOTE_HDMI_1, + "hdmi_2": const.REMOTE_HDMI_2, + "mode_1": const.REMOTE_MODE_1, + "mode_2": const.REMOTE_MODE_2, + "mode_3": const.REMOTE_MODE_3, + "lens_ap": const.REMOTE_LENS_AP, + "gamma": const.REMOTE_GAMMA, + "color_temp": const.REMOTE_COLOR_TEMP, + "natural": const.REMOTE_NATURAL, + "cinema": const.REMOTE_CINEMA, + "anamo": const.REMOTE_ANAMO, + "3d_format": const.REMOTE_3D_FORMAT, +} diff --git a/homeassistant/components/jvc_projector/coordinator.py b/homeassistant/components/jvc_projector/coordinator.py index 874253b3324b8d..4191675f7b1c18 100644 --- a/homeassistant/components/jvc_projector/coordinator.py +++ b/homeassistant/components/jvc_projector/coordinator.py @@ -5,12 +5,8 @@ from datetime import timedelta import logging -from jvcprojector import ( - JvcProjector, - JvcProjectorAuthError, - JvcProjectorConnectError, - const, -) +from jvcprojector.device import JvcProjectorAuthError +from jvcprojector.projector import JvcProjector, JvcProjectorConnectError, const from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -43,7 +39,9 @@ def __init__(self, hass: HomeAssistant, device: JvcProjector) -> None: async def _async_update_data(self) -> dict[str, str]: """Get the latest state data.""" try: - state = await self.device.get_state() + state_mapping = await self.device.get_state() + # Only include non-None values in the final state dict + state = {k: v for k, v in state_mapping.items() if v is not None} except JvcProjectorConnectError as err: raise UpdateFailed(f"Unable to connect to {self.device.host}") from err except JvcProjectorAuthError as err: diff --git a/homeassistant/components/jvc_projector/entity.py b/homeassistant/components/jvc_projector/entity.py index a88fba03cb03d0..435de30aee5eba 100644 --- a/homeassistant/components/jvc_projector/entity.py +++ b/homeassistant/components/jvc_projector/entity.py @@ -2,18 +2,14 @@ from __future__ import annotations -import logging +from jvcprojector.projector import JvcProjector -from jvcprojector import JvcProjector - -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER, NAME from .coordinator import JvcProjectorDataUpdateCoordinator -_LOGGER = logging.getLogger(__name__) - class JvcProjectorEntity(CoordinatorEntity[JvcProjectorDataUpdateCoordinator]): """Defines a base JVC Projector entity.""" @@ -30,9 +26,23 @@ def __init__(self, coordinator: JvcProjectorDataUpdateCoordinator) -> None: name=NAME, model=self.device.model, manufacturer=MANUFACTURER, + sw_version=self.device.version, + connections={(CONNECTION_NETWORK_MAC, self.device.mac)}, ) @property def device(self) -> JvcProjector: """Return the device representing the projector.""" return self.coordinator.device + + @property + def has_eshift(self) -> bool: + """Return if device has e-shift.""" + return ( + "NZ" in self.device.model or "NX9" in self.device.model + ) # nx9 is the only lamp model with eshift + + @property + def has_laser(self) -> bool: + """Return if device has laser.""" + return "NZ" in self.device.model diff --git a/homeassistant/components/jvc_projector/icons.json b/homeassistant/components/jvc_projector/icons.json index a0404b328e1419..4fca5d00572afe 100644 --- a/homeassistant/components/jvc_projector/icons.json +++ b/homeassistant/components/jvc_projector/icons.json @@ -11,6 +11,12 @@ "select": { "input": { "default": "mdi:hdmi-port" + }, + "anamorphic": { + "default": "mdi:stretch-to-page-outline" + }, + "laser_power": { + "default": "mdi:brightness-5" } }, "sensor": { diff --git a/homeassistant/components/jvc_projector/remote.py b/homeassistant/components/jvc_projector/remote.py index b69d3b0118b3f3..c5b6780e89e315 100644 --- a/homeassistant/components/jvc_projector/remote.py +++ b/homeassistant/components/jvc_projector/remote.py @@ -10,55 +10,21 @@ from jvcprojector import const from homeassistant.components.remote import RemoteEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import JVCConfigEntry +from .const import REMOTE_COMMANDS from .entity import JvcProjectorEntity -COMMANDS = { - "menu": const.REMOTE_MENU, - "up": const.REMOTE_UP, - "down": const.REMOTE_DOWN, - "left": const.REMOTE_LEFT, - "right": const.REMOTE_RIGHT, - "ok": const.REMOTE_OK, - "back": const.REMOTE_BACK, - "mpc": const.REMOTE_MPC, - "hide": const.REMOTE_HIDE, - "info": const.REMOTE_INFO, - "input": const.REMOTE_INPUT, - "cmd": const.REMOTE_CMD, - "advanced_menu": const.REMOTE_ADVANCED_MENU, - "picture_mode": const.REMOTE_PICTURE_MODE, - "color_profile": const.REMOTE_COLOR_PROFILE, - "lens_control": const.REMOTE_LENS_CONTROL, - "setting_memory": const.REMOTE_SETTING_MEMORY, - "gamma_settings": const.REMOTE_GAMMA_SETTINGS, - "hdmi_1": const.REMOTE_HDMI_1, - "hdmi_2": const.REMOTE_HDMI_2, - "mode_1": const.REMOTE_MODE_1, - "mode_2": const.REMOTE_MODE_2, - "mode_3": const.REMOTE_MODE_3, - "lens_ap": const.REMOTE_LENS_AP, - "gamma": const.REMOTE_GAMMA, - "color_temp": const.REMOTE_COLOR_TEMP, - "natural": const.REMOTE_NATURAL, - "cinema": const.REMOTE_CINEMA, - "anamo": const.REMOTE_ANAMO, - "3d_format": const.REMOTE_3D_FORMAT, -} - _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: JVCConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the JVC Projector platform from a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities([JvcProjectorRemote(coordinator)], True) @@ -86,8 +52,23 @@ async def async_turn_off(self, **kwargs: Any) -> None: async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a remote command to the device.""" + _LOGGER.debug("Sending command '%s'", command) + for cmd in command: - if cmd not in COMMANDS: - raise HomeAssistantError(f"{cmd} is not a known command") - _LOGGER.debug("Sending command '%s'", cmd) - await self.device.remote(COMMANDS[cmd]) + _LOGGER.debug("Processing command '%s'", cmd) + + # Split command and value + parts = cmd.split(",", 1) + if len(parts) != 2: + raise ValueError(f"Invalid command format: {cmd}") + + cmd_name, value = parts + cmd_name = cmd_name.strip().lower() + value = value.strip() + + if cmd_name == "remote": + if value not in REMOTE_COMMANDS: + raise ValueError(f"Unknown remote command: {value}") + await self.device.remote(REMOTE_COMMANDS[value]) + else: + await self.device.send_command(cmd_name, value) diff --git a/homeassistant/components/jvc_projector/select.py b/homeassistant/components/jvc_projector/select.py index 1395637fad1df4..4f6757bbac88d3 100644 --- a/homeassistant/components/jvc_projector/select.py +++ b/homeassistant/components/jvc_projector/select.py @@ -4,17 +4,14 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass -from typing import Final -from jvcprojector import JvcProjector, const +from jvcprojector.projector import JvcProjector, const from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import JvcProjectorDataUpdateCoordinator -from .const import DOMAIN +from . import JVCConfigEntry, JvcProjectorDataUpdateCoordinator from .entity import JvcProjectorEntity @@ -23,32 +20,78 @@ class JvcProjectorSelectDescription(SelectEntityDescription): """Describes JVC Projector select entities.""" command: Callable[[JvcProjector, str], Awaitable[None]] + enabled_default: bool | Callable[[JvcProjectorEntity], bool] = True + translation_key_override: str | None | Callable[[JvcProjectorEntity], str] = None -OPTIONS: Final[dict[str, dict[str, str]]] = { - "input": {const.HDMI1: const.REMOTE_HDMI_1, const.HDMI2: const.REMOTE_HDMI_2} -} +# type safe command function for a select +def create_select_command(key: str) -> Callable[[JvcProjector, str], Awaitable[None]]: + """Create a command function for a select.""" -SELECTS: Final[list[JvcProjectorSelectDescription]] = [ + async def command(device: JvcProjector, option: str) -> None: + await device.send_command(key, option) + + return command + + +# these options correspond to a command and its possible values +# note low latency is intentionally excluded because you can't just turn it on you need to meet conditions first so you should instead switch picture modes +JVC_SELECTS = ( JvcProjectorSelectDescription( - key="input", - translation_key="input", - options=list(OPTIONS["input"]), - command=lambda device, option: device.remote(OPTIONS["input"][option]), - ) -] + key=const.KEY_INPUT, + translation_key=const.KEY_INPUT, + options=const.VAL_FUNCTION_INPUT, + command=create_select_command(const.KEY_INPUT), + ), + JvcProjectorSelectDescription( + key=const.KEY_INSTALLATION_MODE, + translation_key=const.KEY_INSTALLATION_MODE, + options=const.VAL_INSTALLATION_MODE, + command=create_select_command(const.KEY_INSTALLATION_MODE), + ), + JvcProjectorSelectDescription( + key=const.KEY_ANAMORPHIC, + translation_key=const.KEY_ANAMORPHIC, + options=const.VAL_ANAMORPHIC, + command=create_select_command(const.KEY_ANAMORPHIC), + ), + JvcProjectorSelectDescription( + key=const.KEY_ESHIFT, + translation_key=const.KEY_ESHIFT, + options=const.VAL_TOGGLE, + command=create_select_command(const.KEY_ESHIFT), + enabled_default=JvcProjectorEntity.has_eshift, + ), + JvcProjectorSelectDescription( + key=const.KEY_LASER_POWER, + translation_key=const.KEY_LASER_POWER, + translation_key_override=lambda entity: const.KEY_LASER_POWER + if entity.has_laser + else "lamp_power", + options=const.VAL_LASER_POWER, + command=create_select_command(const.KEY_LASER_POWER), + ), + JvcProjectorSelectDescription( + key=const.KEY_LASER_DIMMING, + translation_key=const.KEY_LASER_DIMMING, + options=const.VAL_LASER_DIMMING, + command=create_select_command(const.KEY_LASER_DIMMING), + enabled_default=JvcProjectorEntity.has_laser, + ), +) async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: JVCConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the JVC Projector platform from a config entry.""" - coordinator: JvcProjectorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( - JvcProjectorSelectEntity(coordinator, description) for description in SELECTS + JvcProjectorSelectEntity(coordinator, description) + for description in JVC_SELECTS ) @@ -66,11 +109,24 @@ def __init__( super().__init__(coordinator) self.entity_description = description self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" + self._attr_entity_registry_enabled_default = ( + description.enabled_default(self) + if callable(description.enabled_default) + else description.enabled_default + ) + # allow for translation key override with callable + self._attr_translation_key = ( + description.translation_key_override(self) + if callable(description.translation_key_override) + else description.translation_key_override + if description.translation_key_override is not None + else description.translation_key + ) @property def current_option(self) -> str | None: """Return the selected entity option to represent the entity state.""" - return self.coordinator.data[self.entity_description.key] + return self.coordinator.data.get(self.entity_description.key) async def async_select_option(self, option: str) -> None: """Change the selected option.""" diff --git a/homeassistant/components/jvc_projector/sensor.py b/homeassistant/components/jvc_projector/sensor.py index 9be04b367e64e9..7233bfbc769243 100644 --- a/homeassistant/components/jvc_projector/sensor.py +++ b/homeassistant/components/jvc_projector/sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from dataclasses import dataclass + from jvcprojector import const from homeassistant.components.sensor import ( @@ -9,38 +11,131 @@ SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import JvcProjectorDataUpdateCoordinator -from .const import DOMAIN +from . import JVCConfigEntry, JvcProjectorDataUpdateCoordinator from .entity import JvcProjectorEntity + +@dataclass(frozen=True, kw_only=True) +class JVCSensorEntityDescription(SensorEntityDescription): + """Describe JVC sensor entity.""" + + enabled_default: bool = True + + JVC_SENSORS = ( - SensorEntityDescription( - key="power", + JVCSensorEntityDescription( + key=const.KEY_POWER, translation_key="jvc_power_status", device_class=SensorDeviceClass.ENUM, entity_category=EntityCategory.DIAGNOSTIC, - options=[ - const.STANDBY, - const.ON, - const.WARMING, - const.COOLING, - const.ERROR, - ], + options=const.VAL_POWER, + ), + JVCSensorEntityDescription( + key=const.KEY_PICTURE_MODE, + translation_key="jvc_picture_mode", + entity_category=EntityCategory.DIAGNOSTIC, + ), + JVCSensorEntityDescription( + key=const.KEY_LASER_VALUE, + translation_key="jvc_laser_value", + entity_category=EntityCategory.DIAGNOSTIC, + enabled_default=False, + ), + JVCSensorEntityDescription( + key=const.KEY_LASER_TIME, + translation_key="jvc_laser_time", + entity_category=EntityCategory.DIAGNOSTIC, + enabled_default=False, + ), + JVCSensorEntityDescription( + key=const.KEY_HDR_CONTENT_TYPE, + translation_key="jvc_hdr_content_type", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=const.VAL_HDR_CONTENT_TYPE, + enabled_default=False, + ), + # niche sensors that are disabled by default + JVCSensorEntityDescription( + key=const.KEY_HDR, + translation_key="jvc_hdr_mode", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=const.VAL_HDR_MODES, + enabled_default=False, + ), + JVCSensorEntityDescription( + key=const.KEY_HDMI_INPUT_LEVEL, + translation_key="jvc_hdmi_input_level", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=const.VAL_HDMI_INPUT_LEVEL, + enabled_default=False, + ), + JVCSensorEntityDescription( + key=const.KEY_HDMI_COLOR_SPACE, + translation_key="jvc_hdmi_color_space", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=const.VAL_HDMI_COLOR_SPACE, + enabled_default=False, + ), + JVCSensorEntityDescription( + key=const.KEY_COLOR_PROFILE, + translation_key="jvc_color_profile", + entity_category=EntityCategory.DIAGNOSTIC, + enabled_default=False, + ), + JVCSensorEntityDescription( + key=const.KEY_GRAPHICS_MODE, + translation_key="jvc_graphics_mode", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=const.VAL_GRAPHICS_MODE, + enabled_default=False, + ), + JVCSensorEntityDescription( + key=const.KEY_COLOR_SPACE, + translation_key="jvc_color_space", + entity_category=EntityCategory.DIAGNOSTIC, + enabled_default=False, + ), + JVCSensorEntityDescription( + key=const.KEY_MOTION_ENHANCE, + translation_key="jvc_motion_enhance", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=const.VAL_MOTION_ENHANCE, + enabled_default=False, + ), + JVCSensorEntityDescription( + key=const.KEY_CLEAR_MOTION_DRIVE, + translation_key="jvc_clear_motion_drive", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=const.VAL_CLEAR_MOTION_DRIVE, + enabled_default=False, + ), + JVCSensorEntityDescription( + key=const.KEY_HDR_PROCESSING, + translation_key="jvc_hdr_processing_mode", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=["0", "1", "2", "3"], # translated + enabled_default=False, ), ) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: JVCConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the JVC Projector platform from a config entry.""" - coordinator: JvcProjectorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - + coordinator = entry.runtime_data async_add_entities( JvcSensor(coordinator, description) for description in JVC_SENSORS ) @@ -52,14 +147,15 @@ class JvcSensor(JvcProjectorEntity, SensorEntity): def __init__( self, coordinator: JvcProjectorDataUpdateCoordinator, - description: SensorEntityDescription, + description: JVCSensorEntityDescription, ) -> None: """Initialize the JVC Projector sensor.""" super().__init__(coordinator) self.entity_description = description self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" + self._attr_entity_registry_enabled_default = description.enabled_default @property def native_value(self) -> str | None: """Return the native value.""" - return self.coordinator.data[self.entity_description.key] + return self.coordinator.data.get(self.entity_description.key) diff --git a/homeassistant/components/jvc_projector/strings.json b/homeassistant/components/jvc_projector/strings.json index b89139cbab3e94..ee999a4a047f32 100644 --- a/homeassistant/components/jvc_projector/strings.json +++ b/homeassistant/components/jvc_projector/strings.json @@ -36,6 +36,15 @@ "binary_sensor": { "jvc_power": { "name": "[%key:component::sensor::entity_component::power::name%]" + }, + "jvc_source_status": { + "name": "Source status" + }, + "jvc_low_latency_status": { + "name": "Low latency" + }, + "jvc_eshift": { + "name": "e-shift" } }, "select": { @@ -45,6 +54,62 @@ "hdmi1": "HDMI 1", "hdmi2": "HDMI 2" } + }, + "eshift": { + "name": "Eshift", + "state": { + "on": "On", + "off": "Off" + } + }, + "installation_mode": { + "name": "Installation Mode", + "state": { + "mode1": "Mode 1", + "mode2": "Mode 2", + "mode3": "Mode 3", + "mode4": "Mode 4", + "mode5": "Mode 5", + "mode6": "Mode 6", + "mode7": "Mode 7", + "mode8": "Mode 8", + "mode9": "Mode 9", + "mode10": "Mode 10" + } + }, + "anamorphic": { + "name": "Anamorphic", + "state": { + "off": "Off", + "a": "Mode A", + "b": "Mode B", + "c": "Mode C", + "d": "Mode D" + } + }, + "laser_power": { + "name": "Laser Power", + "state": { + "low": "Low", + "medium": "Medium", + "high": "High" + } + }, + "lamp_power": { + "name": "Lamp Power", + "state": { + "low": "Low", + "high": "High" + } + }, + "laser_dimming": { + "name": "Laser Dimming", + "state": { + "off": "Off", + "auto1": "Auto 1", + "auto2": "Auto 2", + "auto3": "Auto 3" + } } }, "sensor": { @@ -57,6 +122,130 @@ "cooling": "Cooling", "error": "Error" } + }, + "jvc_hdmi_input": { + "name": "HDMI input", + "state": { + "hdmi1": "HDMI 1", + "hdmi2": "HDMI 2" + } + }, + "jvc_picture_mode": { + "name": "Picture mode" + }, + "jvc_installation_mode": { + "name": "Installation mode" + }, + "jvc_anamorphic_mode": { + "name": "Anamorphic mode", + "state": { + "off": "[%key:common::state::off%]", + "a": "Mode A", + "b": "Mode B", + "c": "Mode C", + "d": "Mode D" + } + }, + "jvc_hdr_mode": { + "name": "HDR mode", + "state": { + "none": "None", + "hdr10": "HDR10", + "hdr10_plus": "HDR10+", + "hlg": "HLG", + "sdr": "SDR" + } + }, + "jvc_hdmi_input_level": { + "name": "HDMI input level", + "state": { + "standard": "Standard", + "enhanced": "Enhanced", + "super_white": "Super White", + "auto": "Auto" + } + }, + "jvc_hdmi_color_space": { + "name": "HDMI color space" + }, + "jvc_color_profile": { + "name": "Color profile" + }, + "jvc_graphics_mode": { + "name": "Graphic mode", + "state": { + "standard": "Standard", + "high-res": "High Resolution", + "high-res2": "High Resolution 2", + "off": "[%key:common::state::off%]" + } + }, + "jvc_color_space": { + "name": "Color space", + "state": { + "rgb": "RGB", + "yuv": "YUV" + } + }, + "jvc_laser_dimming_mode": { + "name": "Laser dimming", + "state": { + "off": "[%key:common::state::off%]", + "auto1": "Auto 1", + "auto2": "Auto 2", + "auto3": "Auto 3" + } + }, + "jvc_laser_value": { + "name": "Laser value" + }, + "jvc_laser_power": { + "name": "Laser/Lamp power", + "state": { + "low": "Low", + "medium": "Medium", + "high": "High" + } + }, + "jvc_laser_time": { + "name": "Laser/Lamp time" + }, + "jvc_motion_enhance": { + "name": "Motion enhance", + "state": { + "off": "[%key:common::state::off%]", + "low": "Low", + "high": "High" + } + }, + "jvc_clear_motion_drive": { + "name": "Clear motion drive", + "state": { + "off": "[%key:common::state::off%]", + "none": "None", + "low": "Low", + "high": "High", + "inverse_telecine": "Inverse Telecine" + } + }, + "jvc_hdr_processing_mode": { + "name": "HDR processing", + "state": { + "0": "HDR10+", + "1": "Static", + "2": "Frame by Frame", + "3": "Scene by Scene" + } + }, + "jvc_hdr_content_type": { + "name": "HDR content type", + "state": { + "auto": "Auto", + "sdr": "SDR", + "none": "None", + "hdr10": "HDR10", + "hlg": "HLG" + } } } } diff --git a/tests/components/jvc_projector/__init__.py b/tests/components/jvc_projector/__init__.py index d8554e8f4cd738..1ea60b2fa3c555 100644 --- a/tests/components/jvc_projector/__init__.py +++ b/tests/components/jvc_projector/__init__.py @@ -1,7 +1,20 @@ """Tests for JVC Projector integration.""" +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + MOCK_HOST = "127.0.0.1" MOCK_PORT = 20554 MOCK_PASSWORD = "jvcpasswd" MOCK_MAC = "jvcmac" -MOCK_MODEL = "jvcmodel" +MOCK_MODEL = "jvcmodelNZ" + + +# used to load a single platform in a test +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/jvc_projector/conftest.py b/tests/components/jvc_projector/conftest.py index 3115cbfe2522f9..8f76fa73820e2e 100644 --- a/tests/components/jvc_projector/conftest.py +++ b/tests/components/jvc_projector/conftest.py @@ -1,6 +1,7 @@ """Fixtures for JVC Projector integration.""" from collections.abc import Generator +import logging from unittest.mock import MagicMock, patch import pytest @@ -29,7 +30,30 @@ def fixture_mock_device( device.port = MOCK_PORT device.mac = MOCK_MAC device.model = MOCK_MODEL - device.get_state.return_value = {"power": "standby", "input": "hdmi1"} + device.get_state.return_value = { + "power": "standby", + "input": "hdmi1", + "source": "signal", + "picture_mode": "natural", + "low_latency": "off", + "installation_mode": "mode1", + "eshift": "on", + "laser_dimming": "auto1", + "laser_value": "100", + "laser_power": "high", + "laser_time": "1000", + "hdr_content_type": "sdr", + "anamorphic": "off", + "hdr": "none", + "hdmi_input_level": "enhanced", + "hdmi_color_space": "rgb", + "color_profile": "bt2020(wide)", + "graphics_mode": "standard", + "color_space": "rgb", + "motion_enhance": "low", + "clear_motion_drive": "low", + "hdr_processing": "1", + } yield device @@ -58,3 +82,9 @@ async def fixture_mock_integration( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() return mock_config_entry + + +@pytest.fixture(autouse=True) +def configure_logging(): + """Configure logging for tests.""" + logging.getLogger().handlers = [logging.NullHandler()] diff --git a/tests/components/jvc_projector/snapshots/test_binary_sensor.ambr b/tests/components/jvc_projector/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000000..6cb91c3a24c98a --- /dev/null +++ b/tests/components/jvc_projector/snapshots/test_binary_sensor.ambr @@ -0,0 +1,232 @@ +# serializer version: 1 +# name: test_binary_sensor_states[binary_sensor.jvc_projector_e_shift-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.jvc_projector_e_shift', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'e-shift', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_eshift', + 'unique_id': 'jvcmac_eshift', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states[binary_sensor.jvc_projector_e_shift-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'JVC Projector e-shift', + }), + 'context': , + 'entity_id': 'binary_sensor.jvc_projector_e_shift', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_states[binary_sensor.jvc_projector_eshift-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.jvc_projector_eshift', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'eshift', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_eshift', + 'unique_id': 'jvcmac_eshift', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states[binary_sensor.jvc_projector_eshift-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'JVC Projector eshift', + }), + 'context': , + 'entity_id': 'binary_sensor.jvc_projector_eshift', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_states[binary_sensor.jvc_projector_low_latency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.jvc_projector_low_latency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Low latency', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_low_latency_status', + 'unique_id': 'jvcmac_low_latency', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states[binary_sensor.jvc_projector_low_latency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'JVC Projector Low latency', + }), + 'context': , + 'entity_id': 'binary_sensor.jvc_projector_low_latency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states[binary_sensor.jvc_projector_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.jvc_projector_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_power', + 'unique_id': 'jvcmac_power', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states[binary_sensor.jvc_projector_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'JVC Projector Power', + }), + 'context': , + 'entity_id': 'binary_sensor.jvc_projector_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states[binary_sensor.jvc_projector_source_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.jvc_projector_source_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Source status', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_source_status', + 'unique_id': 'jvcmac_source', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states[binary_sensor.jvc_projector_source_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'JVC Projector Source status', + }), + 'context': , + 'entity_id': 'binary_sensor.jvc_projector_source_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/jvc_projector/snapshots/test_sensor.ambr b/tests/components/jvc_projector/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..5caecd49da97e9 --- /dev/null +++ b/tests/components/jvc_projector/snapshots/test_sensor.ambr @@ -0,0 +1,777 @@ +# serializer version: 1 +# name: test_entity_state[sensor.jvc_projector_clear_motion_drive-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'none', + 'low', + 'high', + 'inverse_telecine', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_clear_motion_drive', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Clear motion drive', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_clear_motion_drive', + 'unique_id': 'jvcmac_clear_motion_drive', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_clear_motion_drive-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'JVC Projector Clear motion drive', + 'options': list([ + 'off', + 'none', + 'low', + 'high', + 'inverse_telecine', + ]), + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_clear_motion_drive', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'low', + }) +# --- +# name: test_entity_state[sensor.jvc_projector_color_profile-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_color_profile', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Color profile', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_color_profile', + 'unique_id': 'jvcmac_color_profile', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_color_profile-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'JVC Projector Color profile', + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_color_profile', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'bt2020(wide)', + }) +# --- +# name: test_entity_state[sensor.jvc_projector_color_space-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_color_space', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Color space', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_color_space', + 'unique_id': 'jvcmac_color_space', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_color_space-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'JVC Projector Color space', + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_color_space', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'rgb', + }) +# --- +# name: test_entity_state[sensor.jvc_projector_graphic_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'standard', + 'high-res', + 'high-res2', + 'off', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_graphic_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Graphic mode', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_graphics_mode', + 'unique_id': 'jvcmac_graphics_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_graphic_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'JVC Projector Graphic mode', + 'options': list([ + 'standard', + 'high-res', + 'high-res2', + 'off', + ]), + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_graphic_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'standard', + }) +# --- +# name: test_entity_state[sensor.jvc_projector_hdmi_color_space-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'auto', + 'ycbcr(4:4:4)', + 'ycbcr(4:2:2)', + 'rgb', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_hdmi_color_space', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HDMI color space', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_hdmi_color_space', + 'unique_id': 'jvcmac_hdmi_color_space', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_hdmi_color_space-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'JVC Projector HDMI color space', + 'options': list([ + 'auto', + 'ycbcr(4:4:4)', + 'ycbcr(4:2:2)', + 'rgb', + ]), + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_hdmi_color_space', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'rgb', + }) +# --- +# name: test_entity_state[sensor.jvc_projector_hdmi_input_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'standard', + 'enhanced', + 'super_white', + 'auto', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_hdmi_input_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HDMI input level', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_hdmi_input_level', + 'unique_id': 'jvcmac_hdmi_input_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_hdmi_input_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'JVC Projector HDMI input level', + 'options': list([ + 'standard', + 'enhanced', + 'super_white', + 'auto', + ]), + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_hdmi_input_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'enhanced', + }) +# --- +# name: test_entity_state[sensor.jvc_projector_hdr_content_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'auto', + 'sdr', + 'none', + 'hdr10', + 'hlg', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_hdr_content_type', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HDR content type', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_hdr_content_type', + 'unique_id': 'jvcmac_hdr_content_type', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_hdr_content_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'JVC Projector HDR content type', + 'options': list([ + 'auto', + 'sdr', + 'none', + 'hdr10', + 'hlg', + ]), + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_hdr_content_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'sdr', + }) +# --- +# name: test_entity_state[sensor.jvc_projector_hdr_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'sdr', + 'hdr', + 'smpte_st_2084', + 'hybrid_log', + 'none', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_hdr_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HDR mode', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_hdr_mode', + 'unique_id': 'jvcmac_hdr', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_hdr_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'JVC Projector HDR mode', + 'options': list([ + 'sdr', + 'hdr', + 'smpte_st_2084', + 'hybrid_log', + 'none', + ]), + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_hdr_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_entity_state[sensor.jvc_projector_hdr_processing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + '3', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_hdr_processing', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HDR processing', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_hdr_processing_mode', + 'unique_id': 'jvcmac_hdr_processing', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_hdr_processing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'JVC Projector HDR processing', + 'options': list([ + '0', + '1', + '2', + '3', + ]), + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_hdr_processing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_entity_state[sensor.jvc_projector_laser_lamp_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_laser_lamp_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Laser/Lamp time', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_laser_time', + 'unique_id': 'jvcmac_laser_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_laser_lamp_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'JVC Projector Laser/Lamp time', + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_laser_lamp_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1000', + }) +# --- +# name: test_entity_state[sensor.jvc_projector_laser_value-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_laser_value', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Laser value', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_laser_value', + 'unique_id': 'jvcmac_laser_value', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_laser_value-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'JVC Projector Laser value', + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_laser_value', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_entity_state[sensor.jvc_projector_motion_enhance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'low', + 'high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_motion_enhance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion enhance', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_motion_enhance', + 'unique_id': 'jvcmac_motion_enhance', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_motion_enhance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'JVC Projector Motion enhance', + 'options': list([ + 'off', + 'low', + 'high', + ]), + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_motion_enhance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'low', + }) +# --- +# name: test_entity_state[sensor.jvc_projector_picture_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_picture_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Picture mode', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_picture_mode', + 'unique_id': 'jvcmac_picture_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_picture_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'JVC Projector Picture mode', + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_picture_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'natural', + }) +# --- +# name: test_entity_state[sensor.jvc_projector_power_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'standby', + 'on', + 'cooling', + 'warming', + 'error', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jvc_projector_power_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power status', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'jvc_power_status', + 'unique_id': 'jvcmac_power', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state[sensor.jvc_projector_power_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'JVC Projector Power status', + 'options': list([ + 'standby', + 'on', + 'cooling', + 'warming', + 'error', + ]), + }), + 'context': , + 'entity_id': 'sensor.jvc_projector_power_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'standby', + }) +# --- diff --git a/tests/components/jvc_projector/test_binary_sensor.py b/tests/components/jvc_projector/test_binary_sensor.py index b327538991ca3c..5170fbbfe3c60d 100644 --- a/tests/components/jvc_projector/test_binary_sensor.py +++ b/tests/components/jvc_projector/test_binary_sensor.py @@ -1,22 +1,39 @@ -"""Tests for the JVC Projector binary sensor device.""" +"""Tests for the JVC Projector binary sensor devices.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +import homeassistant.helpers.entity_registry as er -from tests.common import MockConfigEntry +from . import setup_integration -ENTITY_ID = "binary_sensor.jvc_projector_power" +from tests.common import MockConfigEntry, snapshot_platform -async def test_entity_state( +async def test_binary_sensor_states( hass: HomeAssistant, + snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, mock_device: MagicMock, - mock_integration: MockConfigEntry, + mock_config_entry: MockConfigEntry, ) -> None: - """Tests entity state is registered.""" - entity = hass.states.get(ENTITY_ID) - assert entity - assert entity_registry.async_get(entity.entity_id) + """Test setup of the binary sensor entities and their states.""" + # Set up mock device with all binary sensor states + mock_device.get_state.return_value = { + "power": "standby", + "low_latency": "off", + "eshift": "on", + "source": "signal", + } + + # Set up integration with only binary sensor platform + with patch( + "homeassistant.components.jvc_projector.PLATFORMS", [Platform.BINARY_SENSOR] + ): + await setup_integration(hass, mock_config_entry) + + # Validate all binary sensor states via snapshot + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/jvc_projector/test_config_flow.py b/tests/components/jvc_projector/test_config_flow.py index 282411540a4c29..94cd80848a4066 100644 --- a/tests/components/jvc_projector/test_config_flow.py +++ b/tests/components/jvc_projector/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from jvcprojector import JvcProjectorAuthError, JvcProjectorConnectError +from jvcprojector.error import JvcProjectorAuthError, JvcProjectorConnectError import pytest from homeassistant.components.jvc_projector.const import DOMAIN @@ -82,9 +82,10 @@ async def test_user_config_flow_bad_connect_errors( @pytest.mark.parametrize("mock_device", [TARGET], indirect=True) async def test_user_config_flow_device_exists_abort( - hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry + hass: HomeAssistant, mock_device: AsyncMock, mock_config_entry: MockConfigEntry ) -> None: """Test flow aborts when device already configured.""" + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -160,14 +161,15 @@ async def test_user_config_flow_bad_auth_errors( @pytest.mark.parametrize("mock_device", [TARGET], indirect=True) async def test_reauth_config_flow_success( - hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry + hass: HomeAssistant, mock_device: AsyncMock, mock_config_entry: MockConfigEntry ) -> None: """Test reauth config flow success.""" + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": SOURCE_REAUTH, - "entry_id": mock_integration.entry_id, + "entry_id": mock_config_entry.entry_id, }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) @@ -182,23 +184,23 @@ async def test_reauth_config_flow_success( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" - assert mock_integration.data[CONF_HOST] == MOCK_HOST - assert mock_integration.data[CONF_PORT] == MOCK_PORT - assert mock_integration.data[CONF_PASSWORD] == MOCK_PASSWORD + assert mock_config_entry.data[CONF_HOST] == MOCK_HOST + assert mock_config_entry.data[CONF_PORT] == MOCK_PORT + assert mock_config_entry.data[CONF_PASSWORD] == MOCK_PASSWORD @pytest.mark.parametrize("mock_device", [TARGET], indirect=True) async def test_reauth_config_flow_auth_error( - hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry + hass: HomeAssistant, mock_device: AsyncMock, mock_config_entry: MockConfigEntry ) -> None: """Test reauth config flow when connect fails.""" mock_device.connect.side_effect = JvcProjectorAuthError - + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": SOURCE_REAUTH, - "entry_id": mock_integration.entry_id, + "entry_id": mock_config_entry.entry_id, }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) @@ -222,7 +224,7 @@ async def test_reauth_config_flow_auth_error( DOMAIN, context={ "source": SOURCE_REAUTH, - "entry_id": mock_integration.entry_id, + "entry_id": mock_config_entry.entry_id, }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) @@ -237,23 +239,23 @@ async def test_reauth_config_flow_auth_error( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" - assert mock_integration.data[CONF_HOST] == MOCK_HOST - assert mock_integration.data[CONF_PORT] == MOCK_PORT - assert mock_integration.data[CONF_PASSWORD] == MOCK_PASSWORD + assert mock_config_entry.data[CONF_HOST] == MOCK_HOST + assert mock_config_entry.data[CONF_PORT] == MOCK_PORT + assert mock_config_entry.data[CONF_PASSWORD] == MOCK_PASSWORD @pytest.mark.parametrize("mock_device", [TARGET], indirect=True) async def test_reauth_config_flow_connect_error( - hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry + hass: HomeAssistant, mock_device: AsyncMock, mock_config_entry: MockConfigEntry ) -> None: """Test reauth config flow when connect fails.""" mock_device.connect.side_effect = JvcProjectorConnectError - + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": SOURCE_REAUTH, - "entry_id": mock_integration.entry_id, + "entry_id": mock_config_entry.entry_id, }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) @@ -277,7 +279,7 @@ async def test_reauth_config_flow_connect_error( DOMAIN, context={ "source": SOURCE_REAUTH, - "entry_id": mock_integration.entry_id, + "entry_id": mock_config_entry.entry_id, }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) @@ -292,6 +294,6 @@ async def test_reauth_config_flow_connect_error( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" - assert mock_integration.data[CONF_HOST] == MOCK_HOST - assert mock_integration.data[CONF_PORT] == MOCK_PORT - assert mock_integration.data[CONF_PASSWORD] == MOCK_PASSWORD + assert mock_config_entry.data[CONF_HOST] == MOCK_HOST + assert mock_config_entry.data[CONF_PORT] == MOCK_PORT + assert mock_config_entry.data[CONF_PASSWORD] == MOCK_PASSWORD diff --git a/tests/components/jvc_projector/test_coordinator.py b/tests/components/jvc_projector/test_coordinator.py index 24297348653026..fc556b681eb778 100644 --- a/tests/components/jvc_projector/test_coordinator.py +++ b/tests/components/jvc_projector/test_coordinator.py @@ -3,9 +3,8 @@ from datetime import timedelta from unittest.mock import AsyncMock -from jvcprojector import JvcProjectorAuthError, JvcProjectorConnectError +from jvcprojector.error import JvcProjectorAuthError, JvcProjectorConnectError -from homeassistant.components.jvc_projector import DOMAIN from homeassistant.components.jvc_projector.coordinator import ( INTERVAL_FAST, INTERVAL_SLOW, @@ -29,7 +28,7 @@ async def test_coordinator_update( ) await hass.async_block_till_done() assert mock_device.get_state.call_count == 3 - coordinator = hass.data[DOMAIN][mock_integration.entry_id] + coordinator = mock_integration.runtime_data assert coordinator.update_interval == INTERVAL_SLOW @@ -69,5 +68,5 @@ async def test_coordinator_device_on( mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - coordinator = hass.data[DOMAIN][mock_config_entry.entry_id] + coordinator = mock_config_entry.runtime_data assert coordinator.update_interval == INTERVAL_FAST diff --git a/tests/components/jvc_projector/test_init.py b/tests/components/jvc_projector/test_init.py index ef9de41ca3233a..d61b2a17e75203 100644 --- a/tests/components/jvc_projector/test_init.py +++ b/tests/components/jvc_projector/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from jvcprojector import JvcProjectorAuthError, JvcProjectorConnectError +from jvcprojector.error import JvcProjectorAuthError, JvcProjectorConnectError from homeassistant.components.jvc_projector.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -32,14 +32,11 @@ async def test_unload_config_entry( mock_integration: MockConfigEntry, ) -> None: """Test config entry loading and unloading.""" - mock_config_entry = mock_integration - assert mock_config_entry.state is ConfigEntryState.LOADED + assert mock_integration.state is ConfigEntryState.LOADED - await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.config_entries.async_unload(mock_integration.entry_id) await hass.async_block_till_done() - assert mock_config_entry.entry_id not in hass.data[DOMAIN] - async def test_config_entry_connect_error( hass: HomeAssistant, diff --git a/tests/components/jvc_projector/test_remote.py b/tests/components/jvc_projector/test_remote.py index 28bf835e0329e8..73746676861206 100644 --- a/tests/components/jvc_projector/test_remote.py +++ b/tests/components/jvc_projector/test_remote.py @@ -1,6 +1,6 @@ """Tests for JVC Projector remote platform.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest @@ -9,11 +9,17 @@ DOMAIN as REMOTE_DOMAIN, SERVICE_SEND_COMMAND, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + Platform, +) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er +from . import setup_integration + from tests.common import MockConfigEntry ENTITY_ID = "remote.jvc_projector" @@ -23,9 +29,11 @@ async def test_entity_state( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_device: MagicMock, - mock_integration: MockConfigEntry, + mock_config_entry: MockConfigEntry, ) -> None: """Tests entity state is registered.""" + with patch("homeassistant.components.jvc_projector.PLATFORMS", [Platform.REMOTE]): + await setup_integration(hass, mock_config_entry) entity = hass.states.get(ENTITY_ID) assert entity assert entity_registry.async_get(entity.entity_id) @@ -34,9 +42,11 @@ async def test_entity_state( async def test_commands( hass: HomeAssistant, mock_device: MagicMock, - mock_integration: MockConfigEntry, + mock_config_entry: MockConfigEntry, ) -> None: """Test service call are called.""" + with patch("homeassistant.components.jvc_projector.PLATFORMS", [Platform.REMOTE]): + await setup_integration(hass, mock_config_entry) await hass.services.async_call( REMOTE_DOMAIN, SERVICE_TURN_ON, @@ -56,7 +66,7 @@ async def test_commands( await hass.services.async_call( REMOTE_DOMAIN, SERVICE_SEND_COMMAND, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["ok"]}, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["remote, hdmi_1"]}, blocking=True, ) assert mock_device.remote.call_count == 1 @@ -64,23 +74,53 @@ async def test_commands( await hass.services.async_call( REMOTE_DOMAIN, SERVICE_SEND_COMMAND, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["hdmi_1"]}, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["laser_power, medium"]}, blocking=True, ) - assert mock_device.remote.call_count == 2 + assert mock_device.send_command.call_count == 1 + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["laser_power, high"]}, + blocking=True, + ) + assert mock_device.send_command.call_count == 2 -async def test_unknown_command( + +async def test_bad_format_command( hass: HomeAssistant, mock_device: MagicMock, - mock_integration: MockConfigEntry, + mock_config_entry: MockConfigEntry, ) -> None: """Test unknown service call errors.""" - with pytest.raises(HomeAssistantError) as err: + with patch("homeassistant.components.jvc_projector.PLATFORMS", [Platform.REMOTE]): + await setup_integration(hass, mock_config_entry) + with pytest.raises(ValueError) as err: await hass.services.async_call( REMOTE_DOMAIN, SERVICE_SEND_COMMAND, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["bad"]}, blocking=True, ) + assert str(err.value) == "Invalid command format: bad" + + +async def test_unknown_command( + hass: HomeAssistant, + mock_device: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unknown service call errors.""" + with patch("homeassistant.components.jvc_projector.PLATFORMS", [Platform.REMOTE]): + await setup_integration(hass, mock_config_entry) + mock_device.send_command.side_effect = ValueError("bad is not a known command") + + with pytest.raises(ValueError) as err: + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["bad, bad"]}, + blocking=True, + ) assert str(err.value) == "bad is not a known command" diff --git a/tests/components/jvc_projector/test_select.py b/tests/components/jvc_projector/test_select.py index a52133bd688e2f..eade9905e4d21f 100644 --- a/tests/components/jvc_projector/test_select.py +++ b/tests/components/jvc_projector/test_select.py @@ -1,44 +1,116 @@ """Tests for JVC Projector select platform.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from jvcprojector import const +from homeassistant.components.jvc_projector.entity import JvcProjectorEntity +from homeassistant.components.jvc_projector.select import JVC_SELECTS from homeassistant.components.select import ( ATTR_OPTIONS, DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_OPTION +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from . import setup_integration + from tests.common import MockConfigEntry -INPUT_ENTITY_ID = "select.jvc_projector_input" +async def test_all_selects( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_device: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test all selects are created correctly.""" + with patch("homeassistant.components.jvc_projector.PLATFORMS", [Platform.SELECT]): + await setup_integration(hass, mock_config_entry) + + # Test each defined select entity + for item in JVC_SELECTS: + # Verify the entity was created + entity_id = f"select.jvc_projector_{item.key}" + entity = hass.states.get(entity_id) + assert entity, f"Entity {entity_id} was not created" + + # Verify the entity's options + assert entity.attributes.get(ATTR_OPTIONS) == item.options + + # Verify the current state matches what's in the mock device + expected_state = mock_device.get_state.return_value.get(item.key) + if expected_state: + assert entity.state == expected_state + + +async def test_override( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_device: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test all selects are created correctly.""" + # things not having NZ should not have laser entity + mock_device.model = "NX9" + + with patch("homeassistant.components.jvc_projector.PLATFORMS", [Platform.SELECT]): + await setup_integration(hass, mock_config_entry) + + # Test override for lamps + for item in JVC_SELECTS: + if item.key == "laser_power": + assert mock_device.model == "NX9" + assert item.translation_key_override is not None + + coordinator = mock_config_entry.runtime_data + entity_id = "select.jvc_projector_lamp_power" + state = hass.states.get(entity_id) + + ent = JvcProjectorEntity(coordinator) + assert ent.has_laser is False + assert ent.has_eshift is True + assert state.name == "JVC Projector Lamp Power" -async def test_input_select( + +async def test_select_option( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_device: MagicMock, - mock_integration: MockConfigEntry, + mock_config_entry: MockConfigEntry, ) -> None: - """Test input select.""" - entity = hass.states.get(INPUT_ENTITY_ID) - assert entity - assert entity.attributes.get(ATTR_FRIENDLY_NAME) == "JVC Projector Input" - assert entity.attributes.get(ATTR_OPTIONS) == [const.HDMI1, const.HDMI2] - assert entity.state == const.HDMI1 - - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: INPUT_ENTITY_ID, - ATTR_OPTION: const.HDMI2, - }, - blocking=True, - ) - - mock_device.remote.assert_called_once_with(const.REMOTE_HDMI_2) + """Test selecting an option for each select.""" + with patch("homeassistant.components.jvc_projector.PLATFORMS", [Platform.SELECT]): + await setup_integration(hass, mock_config_entry) + + # Test each select's option selection + test_cases = [ + ("input", const.HDMI2), + ("eshift", const.OFF), + ("laser_power", const.HIGH), + ("installation_mode", "mode2"), + ("anamorphic", const.ANAMORPHIC_A), + ("laser_dimming", const.AUTO1), + ] + + for cmd_key, test_value in test_cases: + entity_id = f"select.jvc_projector_{cmd_key}" + + # Call the service to change the option + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: test_value, + }, + blocking=True, + ) + + # Verify the command was sent + mock_device.send_command.assert_any_call(cmd_key, test_value) + + # Reset the mock for the next test + mock_device.send_command.reset_mock() diff --git a/tests/components/jvc_projector/test_sensor.py b/tests/components/jvc_projector/test_sensor.py index 1827363e5ad9dd..3877ee4f52dc95 100644 --- a/tests/components/jvc_projector/test_sensor.py +++ b/tests/components/jvc_projector/test_sensor.py @@ -1,24 +1,51 @@ """Tests for the JVC Projector binary sensor device.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry +from . import setup_integration -POWER_ID = "sensor.jvc_projector_power_status" +from tests.common import MockConfigEntry, snapshot_platform +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_entity_state( hass: HomeAssistant, + snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, mock_device: MagicMock, - mock_integration: MockConfigEntry, + mock_config_entry: MockConfigEntry, ) -> None: """Tests entity state is registered.""" - state = hass.states.get(POWER_ID) - assert state - assert entity_registry.async_get(state.entity_id) + with patch("homeassistant.components.jvc_projector.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + # use a snapshot to validate state of entities + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_disabled_entity( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_device: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Tests entity is disabled by default.""" + with patch("homeassistant.components.jvc_projector.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + DISABLED_ID = "sensor.jvc_projector_color_space" + + assert hass.states.get(DISABLED_ID) is None - assert state.state == "standby" + # Entity should exist in registry but be disabled + entity = entity_registry.async_get(DISABLED_ID) + assert entity + assert entity.disabled + assert entity.entity_category == "diagnostic"