mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 05:06:13 +01:00
Add additional JVC Projector entities (#161134)
This commit is contained in:
@@ -76,7 +76,7 @@ async def async_migrate_entities(
|
||||
def _update_entry(entry: RegistryEntry) -> dict[str, str] | None:
|
||||
"""Fix unique_id of power binary_sensor entry."""
|
||||
if entry.domain == Platform.BINARY_SENSOR and ":" not in entry.unique_id:
|
||||
if "_power" in entry.unique_id:
|
||||
if entry.unique_id.endswith("_power"):
|
||||
return {"new_unique_id": f"{coordinator.unique_id}_power"}
|
||||
return None
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import POWER
|
||||
from .coordinator import JVCConfigEntry, JvcProjectorDataUpdateCoordinator
|
||||
from .entity import JvcProjectorEntity
|
||||
|
||||
@@ -41,4 +40,4 @@ class JvcBinarySensor(JvcProjectorEntity, BinarySensorEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the JVC Projector is on."""
|
||||
return self.coordinator.data[POWER] in ON_STATUS
|
||||
return self.coordinator.data[cmd.Power.name] in ON_STATUS
|
||||
|
||||
@@ -3,7 +3,3 @@
|
||||
NAME = "JVC Projector"
|
||||
DOMAIN = "jvc_projector"
|
||||
MANUFACTURER = "JVC"
|
||||
|
||||
POWER = "power"
|
||||
INPUT = "input"
|
||||
SOURCE = "source"
|
||||
|
||||
@@ -2,29 +2,40 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from jvcprojector import (
|
||||
JvcProjector,
|
||||
JvcProjectorAuthError,
|
||||
JvcProjectorTimeoutError,
|
||||
command as cmd,
|
||||
)
|
||||
from jvcprojector import JvcProjector, JvcProjectorTimeoutError, command as cmd
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import INPUT, NAME, POWER
|
||||
from .const import NAME
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from jvcprojector import Command
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
INTERVAL_SLOW = timedelta(seconds=10)
|
||||
INTERVAL_FAST = timedelta(seconds=5)
|
||||
|
||||
CORE_COMMANDS: tuple[type[Command], ...] = (
|
||||
cmd.Power,
|
||||
cmd.Signal,
|
||||
cmd.Input,
|
||||
cmd.LightTime,
|
||||
)
|
||||
|
||||
TRANSLATIONS = str.maketrans({"+": "p", "%": "p", ":": "x"})
|
||||
|
||||
TIMEOUT_RETRIES = 12
|
||||
TIMEOUT_SLEEP = 1
|
||||
|
||||
type JVCConfigEntry = ConfigEntry[JvcProjectorDataUpdateCoordinator]
|
||||
|
||||
|
||||
@@ -51,27 +62,108 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||
assert config_entry.unique_id is not None
|
||||
self.unique_id = config_entry.unique_id
|
||||
|
||||
self.capabilities = self.device.capabilities()
|
||||
|
||||
self.state: dict[type[Command], str] = {}
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Get the latest state data."""
|
||||
state: dict[str, str | None] = {
|
||||
POWER: None,
|
||||
INPUT: None,
|
||||
}
|
||||
"""Update state with the current value of a command."""
|
||||
commands: set[type[Command]] = set(self.async_contexts())
|
||||
commands = commands.difference(CORE_COMMANDS)
|
||||
|
||||
try:
|
||||
state[POWER] = await self.device.get(cmd.Power)
|
||||
last_timeout: JvcProjectorTimeoutError | None = None
|
||||
|
||||
if state[POWER] == cmd.Power.ON:
|
||||
state[INPUT] = await self.device.get(cmd.Input)
|
||||
for _ in range(TIMEOUT_RETRIES):
|
||||
try:
|
||||
new_state = await self._get_device_state(commands)
|
||||
break
|
||||
except JvcProjectorTimeoutError as err:
|
||||
# Timeouts are expected when the projector loses signal and ignores commands for a brief time.
|
||||
last_timeout = err
|
||||
await asyncio.sleep(TIMEOUT_SLEEP)
|
||||
else:
|
||||
raise UpdateFailed(str(last_timeout)) from last_timeout
|
||||
|
||||
except JvcProjectorTimeoutError as err:
|
||||
raise UpdateFailed(f"Unable to connect to {self.device.host}") from err
|
||||
except JvcProjectorAuthError as err:
|
||||
raise ConfigEntryAuthFailed("Password authentication failed") from err
|
||||
# Clear state on signal loss
|
||||
if (
|
||||
new_state.get(cmd.Signal) == cmd.Signal.NONE
|
||||
and self.state.get(cmd.Signal) != cmd.Signal.NONE
|
||||
):
|
||||
self.state = {k: v for k, v in self.state.items() if k in CORE_COMMANDS}
|
||||
|
||||
if state[POWER] != cmd.Power.STANDBY:
|
||||
# Update state with new values
|
||||
for k, v in new_state.items():
|
||||
self.state[k] = v
|
||||
|
||||
if self.state[cmd.Power] != cmd.Power.STANDBY:
|
||||
self.update_interval = INTERVAL_FAST
|
||||
else:
|
||||
self.update_interval = INTERVAL_SLOW
|
||||
|
||||
return state
|
||||
return {k.name: v for k, v in self.state.items()}
|
||||
|
||||
async def _get_device_state(
|
||||
self, commands: set[type[Command]]
|
||||
) -> dict[type[Command], str]:
|
||||
"""Get the current state of the device."""
|
||||
new_state: dict[type[Command], str] = {}
|
||||
deferred_commands: list[type[Command]] = []
|
||||
|
||||
power = await self._update_command_state(cmd.Power, new_state)
|
||||
|
||||
if power == cmd.Power.ON:
|
||||
signal = await self._update_command_state(cmd.Signal, new_state)
|
||||
await self._update_command_state(cmd.Input, new_state)
|
||||
await self._update_command_state(cmd.LightTime, new_state)
|
||||
|
||||
if signal == cmd.Signal.SIGNAL:
|
||||
for command in commands:
|
||||
if command.depends:
|
||||
# Command has dependencies so defer until below
|
||||
deferred_commands.append(command)
|
||||
else:
|
||||
await self._update_command_state(command, new_state)
|
||||
|
||||
# Deferred commands should have had dependencies met above
|
||||
for command in deferred_commands:
|
||||
depend_command, depend_values = next(iter(command.depends.items()))
|
||||
value: str | None = None
|
||||
if depend_command in new_state:
|
||||
value = new_state[depend_command]
|
||||
elif depend_command in self.state:
|
||||
value = self.state[depend_command]
|
||||
if value and value in depend_values:
|
||||
await self._update_command_state(command, new_state)
|
||||
|
||||
elif self.state.get(cmd.Signal) != cmd.Signal.NONE:
|
||||
new_state[cmd.Signal] = cmd.Signal.NONE
|
||||
|
||||
return new_state
|
||||
|
||||
async def _update_command_state(
|
||||
self, command: type[Command], new_state: dict[type[Command], str]
|
||||
) -> str | None:
|
||||
"""Update state with the current value of a command."""
|
||||
value = await self.device.get(command)
|
||||
|
||||
if value != self.state.get(command):
|
||||
new_state[command] = value
|
||||
|
||||
return value
|
||||
|
||||
def get_options_map(self, command: str) -> dict[str, str]:
|
||||
"""Get the available options for a command."""
|
||||
capabilities = self.capabilities.get(command, {})
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(capabilities, dict)
|
||||
assert isinstance(capabilities.get("parameter", {}), dict)
|
||||
assert isinstance(capabilities.get("parameter", {}).get("read", {}), dict)
|
||||
|
||||
values = list(capabilities.get("parameter", {}).get("read", {}).values())
|
||||
|
||||
return {v: v.translate(TRANSLATIONS) for v in values}
|
||||
|
||||
def supports(self, command: type[Command]) -> bool:
|
||||
"""Check if the device supports a command."""
|
||||
return self.device.supports(command)
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from jvcprojector import JvcProjector
|
||||
from jvcprojector import Command, JvcProjector
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -20,9 +20,13 @@ class JvcProjectorEntity(CoordinatorEntity[JvcProjectorDataUpdateCoordinator]):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: JvcProjectorDataUpdateCoordinator) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: JvcProjectorDataUpdateCoordinator,
|
||||
command: type[Command] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
super().__init__(coordinator, command)
|
||||
|
||||
self._attr_unique_id = coordinator.unique_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"jvc_power": {
|
||||
"power": {
|
||||
"default": "mdi:projector-off",
|
||||
"state": {
|
||||
"on": "mdi:projector"
|
||||
@@ -9,17 +9,47 @@
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"anamorphic": {
|
||||
"default": "mdi:fit-to-screen-outline"
|
||||
},
|
||||
"clear_motion_drive": {
|
||||
"default": "mdi:blur"
|
||||
},
|
||||
"dynamic_control": {
|
||||
"default": "mdi:lightbulb-on-outline"
|
||||
},
|
||||
"input": {
|
||||
"default": "mdi:hdmi-port"
|
||||
},
|
||||
"installation_mode": {
|
||||
"default": "mdi:aspect-ratio"
|
||||
},
|
||||
"light_power": {
|
||||
"default": "mdi:lightbulb-on-outline"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"jvc_power_status": {
|
||||
"default": "mdi:power-plug-off",
|
||||
"color_depth": {
|
||||
"default": "mdi:palette-outline"
|
||||
},
|
||||
"color_space": {
|
||||
"default": "mdi:palette-outline"
|
||||
},
|
||||
"hdr": {
|
||||
"default": "mdi:image-filter-hdr-outline"
|
||||
},
|
||||
"hdr_processing": {
|
||||
"default": "mdi:image-filter-hdr-outline"
|
||||
},
|
||||
"picture_mode": {
|
||||
"default": "mdi:movie-roll"
|
||||
},
|
||||
"power": {
|
||||
"default": "mdi:power",
|
||||
"state": {
|
||||
"cooling": "mdi:snowflake",
|
||||
"error": "mdi:alert-circle",
|
||||
"on": "mdi:power-plug",
|
||||
"on": "mdi:power",
|
||||
"warming": "mdi:heat-wave"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import POWER
|
||||
from .coordinator import JVCConfigEntry
|
||||
from .entity import JvcProjectorEntity
|
||||
|
||||
@@ -65,6 +64,8 @@ RENAMED_COMMANDS: dict[str, str] = {
|
||||
"hdmi2": cmd.Remote.HDMI2,
|
||||
}
|
||||
|
||||
ON_STATUS = (cmd.Power.ON, cmd.Power.WARMING)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -86,7 +87,7 @@ class JvcProjectorRemote(JvcProjectorEntity, RemoteEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if the entity is on."""
|
||||
return self.coordinator.data[POWER] in (cmd.Power.ON, cmd.Power.WARMING)
|
||||
return self.coordinator.data.get(cmd.Power.name) in ON_STATUS
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
from jvcprojector import JvcProjector, command as cmd
|
||||
from jvcprojector import Command, command as cmd
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -20,17 +19,37 @@ from .entity import JvcProjectorEntity
|
||||
class JvcProjectorSelectDescription(SelectEntityDescription):
|
||||
"""Describes JVC Projector select entities."""
|
||||
|
||||
command: Callable[[JvcProjector, str], Awaitable[None]]
|
||||
command: type[Command]
|
||||
|
||||
|
||||
SELECTS: Final[list[JvcProjectorSelectDescription]] = [
|
||||
SELECTS: Final[tuple[JvcProjectorSelectDescription, ...]] = (
|
||||
JvcProjectorSelectDescription(key="input", command=cmd.Input),
|
||||
JvcProjectorSelectDescription(
|
||||
key="input",
|
||||
translation_key="input",
|
||||
options=[cmd.Input.HDMI1, cmd.Input.HDMI2],
|
||||
command=lambda device, option: device.set(cmd.Input, option),
|
||||
)
|
||||
]
|
||||
key="installation_mode",
|
||||
command=cmd.InstallationMode,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
JvcProjectorSelectDescription(
|
||||
key="light_power",
|
||||
command=cmd.LightPower,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
JvcProjectorSelectDescription(
|
||||
key="dynamic_control",
|
||||
command=cmd.DynamicControl,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
JvcProjectorSelectDescription(
|
||||
key="clear_motion_drive",
|
||||
command=cmd.ClearMotionDrive,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
JvcProjectorSelectDescription(
|
||||
key="anamorphic",
|
||||
command=cmd.Anamorphic,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -42,30 +61,45 @@ async def async_setup_entry(
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
JvcProjectorSelectEntity(coordinator, description) for description in SELECTS
|
||||
JvcProjectorSelectEntity(coordinator, description)
|
||||
for description in SELECTS
|
||||
if coordinator.supports(description.command)
|
||||
)
|
||||
|
||||
|
||||
class JvcProjectorSelectEntity(JvcProjectorEntity, SelectEntity):
|
||||
"""Representation of a JVC Projector select entity."""
|
||||
|
||||
entity_description: JvcProjectorSelectDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: JvcProjectorDataUpdateCoordinator,
|
||||
description: JvcProjectorSelectDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
super().__init__(coordinator, description.command)
|
||||
self.command: type[Command] = description.command
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.unique_id}_{description.key}"
|
||||
self._attr_translation_key = description.key
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{description.key}"
|
||||
|
||||
self._options_map: dict[str, str] = coordinator.get_options_map(
|
||||
self.command.name
|
||||
)
|
||||
|
||||
@property
|
||||
def options(self) -> list[str]:
|
||||
"""Return a list of selectable options."""
|
||||
return list(self._options_map.values())
|
||||
|
||||
@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]
|
||||
if value := self.coordinator.data.get(self.command.name):
|
||||
return self._options_map.get(value)
|
||||
return None
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
await self.entity_description.command(self.coordinator.device, option)
|
||||
value = next((k for k, v in self._options_map.items() if v == option), None)
|
||||
await self.coordinator.device.set(self.command, value)
|
||||
|
||||
@@ -2,33 +2,77 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from jvcprojector import command as cmd
|
||||
from dataclasses import dataclass
|
||||
|
||||
from jvcprojector import Command, command as cmd
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.const import EntityCategory, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import JVCConfigEntry, JvcProjectorDataUpdateCoordinator
|
||||
from .entity import JvcProjectorEntity
|
||||
|
||||
JVC_SENSORS = (
|
||||
SensorEntityDescription(
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class JvcProjectorSensorDescription(SensorEntityDescription):
|
||||
"""Describes JVC Projector sensor entities."""
|
||||
|
||||
command: type[Command]
|
||||
|
||||
|
||||
SENSORS: tuple[JvcProjectorSensorDescription, ...] = (
|
||||
JvcProjectorSensorDescription(
|
||||
key="power",
|
||||
translation_key="jvc_power_status",
|
||||
command=cmd.Power,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
),
|
||||
JvcProjectorSensorDescription(
|
||||
key="light_time",
|
||||
command=cmd.LightTime,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=UnitOfTime.HOURS,
|
||||
),
|
||||
JvcProjectorSensorDescription(
|
||||
key="color_depth",
|
||||
command=cmd.ColorDepth,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[
|
||||
cmd.Power.STANDBY,
|
||||
cmd.Power.ON,
|
||||
cmd.Power.WARMING,
|
||||
cmd.Power.COOLING,
|
||||
cmd.Power.ERROR,
|
||||
],
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
JvcProjectorSensorDescription(
|
||||
key="color_space",
|
||||
command=cmd.ColorSpace,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
JvcProjectorSensorDescription(
|
||||
key="hdr",
|
||||
command=cmd.Hdr,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
JvcProjectorSensorDescription(
|
||||
key="hdr_processing",
|
||||
command=cmd.HdrProcessing,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
JvcProjectorSensorDescription(
|
||||
key="picture_mode",
|
||||
command=cmd.PictureMode,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -42,24 +86,48 @@ async def async_setup_entry(
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
JvcSensor(coordinator, description) for description in JVC_SENSORS
|
||||
JvcProjectorSensorEntity(coordinator, description)
|
||||
for description in SENSORS
|
||||
if coordinator.supports(description.command)
|
||||
)
|
||||
|
||||
|
||||
class JvcSensor(JvcProjectorEntity, SensorEntity):
|
||||
class JvcProjectorSensorEntity(JvcProjectorEntity, SensorEntity):
|
||||
"""The entity class for JVC Projector integration."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: JvcProjectorDataUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
description: JvcProjectorSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the JVC Projector sensor."""
|
||||
super().__init__(coordinator)
|
||||
super().__init__(coordinator, description.command)
|
||||
self.command: type[Command] = description.command
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.unique_id}_{description.key}"
|
||||
self._attr_translation_key = description.key
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{description.key}"
|
||||
|
||||
self._options_map: dict[str, str] = {}
|
||||
if self.device_class == SensorDeviceClass.ENUM:
|
||||
self._options_map = coordinator.get_options_map(self.command.name)
|
||||
|
||||
@property
|
||||
def options(self) -> list[str] | None:
|
||||
"""Return a set of possible options."""
|
||||
if self.device_class == SensorDeviceClass.ENUM:
|
||||
return list(self._options_map.values())
|
||||
return None
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the native value."""
|
||||
return self.coordinator.data[self.entity_description.key]
|
||||
value = self.coordinator.data.get(self.command.name)
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if self.device_class == SensorDeviceClass.ENUM:
|
||||
return self._options_map.get(value)
|
||||
|
||||
return value
|
||||
|
||||
@@ -36,20 +36,134 @@
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"power": {
|
||||
"name": "[%key:component::binary_sensor::entity_component::power::name%]"
|
||||
"name": "Power"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"anamorphic": {
|
||||
"name": "Anamorphic",
|
||||
"state": {
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"c": "C",
|
||||
"d": "D",
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"clear_motion_drive": {
|
||||
"name": "Clear Motion Drive",
|
||||
"state": {
|
||||
"high": "[%key:common::state::high%]",
|
||||
"inverse-telecine": "Inverse Telecine",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"dynamic_control": {
|
||||
"name": "Dynamic Control",
|
||||
"state": {
|
||||
"balanced": "Balanced",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"mode-1": "Mode 1",
|
||||
"mode-2": "Mode 2",
|
||||
"mode-3": "Mode 3",
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"input": {
|
||||
"name": "Input",
|
||||
"state": {
|
||||
"hdmi1": "HDMI 1",
|
||||
"hdmi2": "HDMI 2"
|
||||
}
|
||||
},
|
||||
"installation_mode": {
|
||||
"name": "Installation Mode",
|
||||
"state": {
|
||||
"memory-1": "Memory 1",
|
||||
"memory-10": "Memory 10",
|
||||
"memory-2": "Memory 2",
|
||||
"memory-3": "Memory 3",
|
||||
"memory-4": "Memory 4",
|
||||
"memory-5": "Memory 5",
|
||||
"memory-6": "Memory 6",
|
||||
"memory-7": "Memory 7",
|
||||
"memory-8": "Memory 8",
|
||||
"memory-9": "Memory 9"
|
||||
}
|
||||
},
|
||||
"light_power": {
|
||||
"name": "Light Power",
|
||||
"state": {
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"mid": "[%key:common::state::medium%]",
|
||||
"normal": "[%key:common::state::normal%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"jvc_power_status": {
|
||||
"color_depth": {
|
||||
"name": "Color Depth",
|
||||
"state": {
|
||||
"8-bit": "8-bit",
|
||||
"10-bit": "10-bit",
|
||||
"12-bit": "12-bit"
|
||||
}
|
||||
},
|
||||
"color_space": {
|
||||
"name": "Color Space",
|
||||
"state": {
|
||||
"rgb": "RGB",
|
||||
"xv-color": "XV Color",
|
||||
"ycbcr-420": "YCbCr 4:2:0",
|
||||
"ycbcr-422": "YCbCr 4:2:2",
|
||||
"ycbcr-444": "YCbCr 4:4:4",
|
||||
"yuv": "YUV"
|
||||
}
|
||||
},
|
||||
"hdr": {
|
||||
"name": "HDR",
|
||||
"state": {
|
||||
"hdr": "HDR",
|
||||
"hdr10p": "HDR10+",
|
||||
"hybrid-log": "Hybrid Log",
|
||||
"none": "None",
|
||||
"sdr": "SDR",
|
||||
"smpte-st-2084": "SMPTE ST 2084"
|
||||
}
|
||||
},
|
||||
"hdr_processing": {
|
||||
"name": "HDR Processing",
|
||||
"state": {
|
||||
"frame-by-frame": "Frame-by-Frame",
|
||||
"hdr10p": "HDR10+",
|
||||
"scene-by-scene": "Scene-by-Scene",
|
||||
"static": "Static"
|
||||
}
|
||||
},
|
||||
"light_time": {
|
||||
"name": "Light Time"
|
||||
},
|
||||
"picture_mode": {
|
||||
"name": "Picture Mode",
|
||||
"state": {
|
||||
"frame-adapt-hdr": "Frame Adapt HDR",
|
||||
"frame-adapt-hdr2": "Frame Adapt HDR2",
|
||||
"frame-adapt-hdr3": "Frame Adapt HDR3",
|
||||
"hdr1": "HDR1",
|
||||
"hdr10": "HDR10",
|
||||
"hdr10-ll": "HDR10 LL",
|
||||
"hdr2": "HDR2",
|
||||
"last-setting": "Last Setting",
|
||||
"pana-pq": "Pana PQ",
|
||||
"user-4": "User 4",
|
||||
"user-5": "User 5",
|
||||
"user-6": "User 6"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"name": "Status",
|
||||
"state": {
|
||||
"cooling": "Cooling",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from jvcprojector import command as cmd
|
||||
from jvcprojector import Command, JvcProjectorTimeoutError, command as cmd
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.jvc_projector.const import DOMAIN
|
||||
@@ -15,6 +15,62 @@ from . import MOCK_HOST, MOCK_MAC, MOCK_MODEL, MOCK_PASSWORD, MOCK_PORT
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
FIXTURES: dict[str, dict[type[Command], str | type[Exception]]] = {
|
||||
"standby": {
|
||||
cmd.MacAddress: MOCK_MAC,
|
||||
cmd.ModelName: MOCK_MODEL,
|
||||
cmd.Power: "standby",
|
||||
cmd.Input: "hdmi1",
|
||||
cmd.Signal: "none",
|
||||
cmd.LightTime: "100",
|
||||
cmd.Source: JvcProjectorTimeoutError,
|
||||
cmd.Hdr: JvcProjectorTimeoutError,
|
||||
cmd.HdrProcessing: JvcProjectorTimeoutError,
|
||||
},
|
||||
"on": {
|
||||
cmd.MacAddress: MOCK_MAC,
|
||||
cmd.ModelName: MOCK_MODEL,
|
||||
cmd.Power: "on",
|
||||
cmd.Input: "hdmi1",
|
||||
cmd.Signal: "signal",
|
||||
cmd.LightTime: "100",
|
||||
cmd.Source: "4k",
|
||||
cmd.Hdr: "hdr",
|
||||
cmd.HdrProcessing: "static",
|
||||
},
|
||||
}
|
||||
|
||||
CAPABILITIES = {
|
||||
cmd.Power.name: {
|
||||
"name": cmd.Power.name,
|
||||
"parameter": {"read": {"0": "standby", "1": "on"}},
|
||||
},
|
||||
cmd.Input.name: {
|
||||
"name": cmd.Input.name,
|
||||
"parameter": {"read": {"6": "hdmi1", "7": "hdmi2"}},
|
||||
},
|
||||
cmd.Signal.name: {
|
||||
"name": cmd.Signal.name,
|
||||
"parameter": {"read": {"0": "none", "1": "signal"}},
|
||||
},
|
||||
cmd.Source.name: {
|
||||
"name": cmd.Source.name,
|
||||
"parameter": {"read": {"0": "4k"}},
|
||||
},
|
||||
cmd.Hdr.name: {
|
||||
"name": cmd.Hdr.name,
|
||||
"parameter": {"read": {"0": "sdr", "1": "hdr"}},
|
||||
},
|
||||
cmd.HdrProcessing.name: {
|
||||
"name": cmd.HdrProcessing.name,
|
||||
"parameter": {"read": {"0": "hdr", "1": "static"}},
|
||||
},
|
||||
cmd.LightTime.name: {
|
||||
"name": cmd.LightTime.name,
|
||||
"parameter": "empty",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_device")
|
||||
def fixture_mock_device(
|
||||
@@ -22,31 +78,36 @@ def fixture_mock_device(
|
||||
) -> Generator[MagicMock]:
|
||||
"""Return a mocked JVC Projector device."""
|
||||
target = "homeassistant.components.jvc_projector.JvcProjector"
|
||||
fixture: dict[str, str] = {
|
||||
"mac": MOCK_MAC,
|
||||
"power": "standby",
|
||||
"input": "hdmi-1",
|
||||
}
|
||||
fixture = FIXTURES["on"].copy()
|
||||
|
||||
if hasattr(request, "param"):
|
||||
target = request.param.get("target", target)
|
||||
fixture = request.param.get("get", fixture)
|
||||
if "fixture" in request.param:
|
||||
if isinstance(request.param["fixture"], str):
|
||||
fixture = FIXTURES[request.param["fixture"]].copy()
|
||||
else:
|
||||
fixture = request.param["fixture"].copy()
|
||||
|
||||
if "fixture_override" in request.param:
|
||||
fixture.update(request.param["fixture_override"])
|
||||
|
||||
async def device_get(command) -> str:
|
||||
if command is cmd.MacAddress:
|
||||
return fixture["mac"]
|
||||
if command is cmd.Power:
|
||||
return fixture["power"]
|
||||
if command is cmd.Input:
|
||||
return fixture["input"]
|
||||
raise ValueError(f"Fixture failure; unexpected command {command}")
|
||||
if command in fixture:
|
||||
value = fixture[command]
|
||||
if isinstance(value, type) and issubclass(value, Exception):
|
||||
raise value
|
||||
return value
|
||||
raise ValueError(f"Test fixture failure; unexpected command {command}")
|
||||
|
||||
with patch(target, autospec=True) as mock:
|
||||
device = mock.return_value
|
||||
device.ip = MOCK_HOST
|
||||
device.host = MOCK_HOST
|
||||
device.port = MOCK_PORT
|
||||
device.mac = MOCK_MAC
|
||||
device.model = MOCK_MODEL
|
||||
device.get.side_effect = device_get
|
||||
device.capabilities.return_value = CAPABILITIES
|
||||
yield device
|
||||
|
||||
|
||||
@@ -71,7 +132,11 @@ async def fixture_mock_integration(
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> MockConfigEntry:
|
||||
"""Return a mock ConfigEntry setup for the integration."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return mock_config_entry
|
||||
with (
|
||||
patch("homeassistant.components.jvc_projector.coordinator.TIMEOUT_RETRIES", 2),
|
||||
patch("homeassistant.components.jvc_projector.coordinator.TIMEOUT_SLEEP", 0.1),
|
||||
):
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return mock_config_entry
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from jvcprojector import JvcProjectorAuthError, JvcProjectorTimeoutError
|
||||
from jvcprojector import JvcProjectorTimeoutError, command as cmd
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.jvc_projector.coordinator import (
|
||||
@@ -14,11 +14,14 @@ from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import MOCK_MAC
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_device",
|
||||
[{"fixture": "standby"}],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_coordinator_update(
|
||||
hass: HomeAssistant,
|
||||
mock_device: AsyncMock,
|
||||
@@ -33,45 +36,25 @@ async def test_coordinator_update(
|
||||
assert coordinator.update_interval == INTERVAL_SLOW
|
||||
|
||||
|
||||
async def test_coordinator_setup_connect_error(
|
||||
async def test_coordinator_device_on(
|
||||
hass: HomeAssistant,
|
||||
mock_device: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test coordinator connect error."""
|
||||
mock_device.get.side_effect = JvcProjectorTimeoutError
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_coordinator_setup_auth_error(
|
||||
hass: HomeAssistant,
|
||||
mock_device: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test coordinator auth error."""
|
||||
mock_device.get.side_effect = JvcProjectorAuthError
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
"""Test coordinator changes update interval when device is on."""
|
||||
coordinator = mock_integration.runtime_data
|
||||
assert coordinator.update_interval == INTERVAL_FAST
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_device",
|
||||
[{"get": {"mac": MOCK_MAC, "power": "on", "input": "hdmi-1"}}],
|
||||
[{"fixture_override": {cmd.Power: JvcProjectorTimeoutError}}],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_coordinator_device_on(
|
||||
async def test_coordinator_setup_connect_error(
|
||||
hass: HomeAssistant,
|
||||
mock_device: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test coordinator changes update interval when device is 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 = mock_config_entry.runtime_data
|
||||
assert coordinator.update_interval == INTERVAL_FAST
|
||||
"""Test coordinator connect error."""
|
||||
assert mock_integration.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
@@ -43,7 +43,6 @@ async def test_commands(
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_device.get.call_count == 3
|
||||
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
@@ -51,7 +50,6 @@ async def test_commands(
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_device.get.call_count == 4
|
||||
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
@@ -59,7 +57,6 @@ async def test_commands(
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["ok"]},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_device.remote.call_count == 1
|
||||
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
@@ -67,7 +64,6 @@ async def test_commands(
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["hdmi1"]},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_device.remote.call_count == 2
|
||||
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
@@ -75,7 +71,6 @@ async def test_commands(
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["anamo"]},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_device.remote.call_count == 3
|
||||
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from jvcprojector import command as cmd
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.select import (
|
||||
ATTR_OPTIONS,
|
||||
@@ -14,18 +13,11 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_OPTION
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import MOCK_MAC
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
INPUT_ENTITY_ID = "select.jvc_projector_input"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_device",
|
||||
[{"get": {"mac": MOCK_MAC, "power": "on", "input": "hdmi-1"}}],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_input_select(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
"""Tests for the JVC Projector binary sensor device."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeassistant.components.jvc_projector.coordinator import INTERVAL_FAST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
POWER_ID = "sensor.jvc_projector_status"
|
||||
HDR_ENTITY_ID = "sensor.jvc_projector_hdr"
|
||||
HDR_PROCESSING_ENTITY_ID = "sensor.jvc_projector_hdr_processing"
|
||||
|
||||
|
||||
async def test_entity_state(
|
||||
@@ -20,5 +25,44 @@ async def test_entity_state(
|
||||
state = hass.states.get(POWER_ID)
|
||||
assert state
|
||||
assert entity_registry.async_get(state.entity_id)
|
||||
assert state.state == "on"
|
||||
|
||||
assert state.state == "standby"
|
||||
|
||||
async def test_enable_hdr_sensor(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_device,
|
||||
mock_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test enabling the HDR select (disabled by default)."""
|
||||
|
||||
# Test entity is disabled initially
|
||||
entry = entity_registry.async_get(HDR_ENTITY_ID)
|
||||
assert entry is not None
|
||||
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
|
||||
# Enable entity
|
||||
entity_registry.async_update_entity(HDR_ENTITY_ID, disabled_by=None)
|
||||
entity_registry.async_update_entity(HDR_PROCESSING_ENTITY_ID, disabled_by=None)
|
||||
|
||||
# Add to hass
|
||||
await hass.config_entries.async_reload(mock_integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify entity is enabled
|
||||
state = hass.states.get(HDR_ENTITY_ID)
|
||||
assert state is not None
|
||||
|
||||
# Allow deferred updates to run
|
||||
async_fire_time_changed(
|
||||
hass, utcnow() + timedelta(seconds=INTERVAL_FAST.seconds + 1)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Allow deferred updates to run again
|
||||
async_fire_time_changed(
|
||||
hass, utcnow() + timedelta(seconds=INTERVAL_FAST.seconds + 1)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(HDR_PROCESSING_ENTITY_ID) is not None
|
||||
|
||||
Reference in New Issue
Block a user