From 907861effdef46e61950c4c82b2801375e9acade Mon Sep 17 00:00:00 2001 From: Steve Easley Date: Sat, 17 Jan 2026 09:44:29 -0500 Subject: [PATCH] Bump pyjvcprojector to 2.0.0 (#160739) Co-authored-by: Joost Lekkerkerker --- .../components/jvc_projector/__init__.py | 29 ++++- .../components/jvc_projector/binary_sensor.py | 14 +-- .../components/jvc_projector/config_flow.py | 15 ++- .../components/jvc_projector/const.py | 4 + .../components/jvc_projector/coordinator.py | 36 +++--- .../components/jvc_projector/entity.py | 2 +- .../components/jvc_projector/manifest.json | 2 +- .../components/jvc_projector/remote.py | 113 ++++++++++-------- .../components/jvc_projector/select.py | 10 +- .../components/jvc_projector/sensor.py | 12 +- .../components/jvc_projector/strings.json | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/jvc_projector/__init__.py | 5 +- tests/components/jvc_projector/conftest.py | 25 +++- .../jvc_projector/test_binary_sensor.py | 41 +++++++ .../jvc_projector/test_config_flow.py | 22 ++-- .../jvc_projector/test_coordinator.py | 21 ++-- tests/components/jvc_projector/test_init.py | 28 ++++- tests/components/jvc_projector/test_remote.py | 22 +++- tests/components/jvc_projector/test_select.py | 18 ++- tests/components/jvc_projector/test_sensor.py | 2 +- 22 files changed, 292 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/jvc_projector/__init__.py b/homeassistant/components/jvc_projector/__init__.py index ad7e333ca130..547b0a677611 100644 --- a/homeassistant/components/jvc_projector/__init__.py +++ b/homeassistant/components/jvc_projector/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorConnectError +from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorTimeoutError from homeassistant.const import ( CONF_HOST, @@ -11,8 +11,9 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from .coordinator import JVCConfigEntry, JvcProjectorDataUpdateCoordinator @@ -28,8 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: JVCConfigEntry) -> bool: ) try: - await device.connect(True) - except JvcProjectorConnectError as err: + await device.connect() + except JvcProjectorTimeoutError as err: await device.disconnect() raise ConfigEntryNotReady( f"Unable to connect to {entry.data[CONF_HOST]}" @@ -50,6 +51,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: JVCConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect) ) + await async_migrate_entities(hass, entry, coordinator) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -60,3 +63,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: JVCConfigEntry) -> bool if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): await entry.runtime_data.device.disconnect() return unload_ok + + +async def async_migrate_entities( + hass: HomeAssistant, + config_entry: JVCConfigEntry, + coordinator: JvcProjectorDataUpdateCoordinator, +) -> None: + """Migrate old entities as needed.""" + + @callback + 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: + return {"new_unique_id": f"{coordinator.unique_id}_power"} + return None + + await async_migrate_entries(hass, config_entry.entry_id, _update_entry) diff --git a/homeassistant/components/jvc_projector/binary_sensor.py b/homeassistant/components/jvc_projector/binary_sensor.py index 7ae762988399..86e3e104f32d 100644 --- a/homeassistant/components/jvc_projector/binary_sensor.py +++ b/homeassistant/components/jvc_projector/binary_sensor.py @@ -2,16 +2,17 @@ from __future__ import annotations -from jvcprojector import const +from jvcprojector import command as cmd 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 -ON_STATUS = (const.ON, const.WARMING) +ON_STATUS = (cmd.Power.ON, cmd.Power.WARMING) async def async_setup_entry( @@ -21,14 +22,13 @@ async def async_setup_entry( ) -> None: """Set up the JVC Projector platform from a config entry.""" coordinator = entry.runtime_data - async_add_entities([JvcBinarySensor(coordinator)]) class JvcBinarySensor(JvcProjectorEntity, BinarySensorEntity): """The entity class for JVC Projector Binary Sensor.""" - _attr_translation_key = "jvc_power" + _attr_translation_key = "power" def __init__( self, @@ -36,9 +36,9 @@ class JvcBinarySensor(JvcProjectorEntity, BinarySensorEntity): ) -> None: """Initialize the JVC Projector sensor.""" super().__init__(coordinator) - self._attr_unique_id = f"{coordinator.device.mac}_power" + self._attr_unique_id = f"{coordinator.unique_id}_power" @property def is_on(self) -> bool: - """Return true if the JVC is on.""" - return self.coordinator.data["power"] in ON_STATUS + """Return true if the JVC Projector is on.""" + return self.coordinator.data[POWER] in ON_STATUS diff --git a/homeassistant/components/jvc_projector/config_flow.py b/homeassistant/components/jvc_projector/config_flow.py index 5d9bedd75919..26131f687c20 100644 --- a/homeassistant/components/jvc_projector/config_flow.py +++ b/homeassistant/components/jvc_projector/config_flow.py @@ -5,7 +5,12 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorConnectError +from jvcprojector import ( + JvcProjector, + JvcProjectorAuthError, + JvcProjectorTimeoutError, + command as cmd, +) from jvcprojector.projector import DEFAULT_PORT import voluptuous as vol @@ -40,7 +45,7 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN): mac = await get_mac_address(host, port, password) except InvalidHost: errors["base"] = "invalid_host" - except JvcProjectorConnectError: + except JvcProjectorTimeoutError: errors["base"] = "cannot_connect" except JvcProjectorAuthError: errors["base"] = "invalid_auth" @@ -91,7 +96,7 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN): try: await get_mac_address(host, port, password) - except JvcProjectorConnectError: + except JvcProjectorTimeoutError: errors["base"] = "cannot_connect" except JvcProjectorAuthError: errors["base"] = "invalid_auth" @@ -115,7 +120,7 @@ async def get_mac_address(host: str, port: int, password: str | None) -> str: """Get device mac address for config flow.""" device = JvcProjector(host, port=port, password=password) try: - await device.connect(True) + await device.connect() + return await device.get(cmd.MacAddress) finally: await device.disconnect() - return device.mac diff --git a/homeassistant/components/jvc_projector/const.py b/homeassistant/components/jvc_projector/const.py index e15aa93bfa5d..d0dbd1f73f86 100644 --- a/homeassistant/components/jvc_projector/const.py +++ b/homeassistant/components/jvc_projector/const.py @@ -3,3 +3,7 @@ NAME = "JVC Projector" DOMAIN = "jvc_projector" MANUFACTURER = "JVC" + +POWER = "power" +INPUT = "input" +SOURCE = "source" diff --git a/homeassistant/components/jvc_projector/coordinator.py b/homeassistant/components/jvc_projector/coordinator.py index db97b05f9805..58ca14a37388 100644 --- a/homeassistant/components/jvc_projector/coordinator.py +++ b/homeassistant/components/jvc_projector/coordinator.py @@ -4,22 +4,21 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any +from typing import TYPE_CHECKING, Any from jvcprojector import ( JvcProjector, JvcProjectorAuthError, - JvcProjectorConnectError, - const, + JvcProjectorTimeoutError, + command as cmd, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import NAME +from .const import INPUT, NAME, POWER _LOGGER = logging.getLogger(__name__) @@ -46,26 +45,33 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]): update_interval=INTERVAL_SLOW, ) - self.device = device - self.unique_id = format_mac(device.mac) + self.device: JvcProjector = device + + if TYPE_CHECKING: + assert config_entry.unique_id is not None + self.unique_id = config_entry.unique_id async def _async_update_data(self) -> dict[str, Any]: """Get the latest state data.""" + state: dict[str, str | None] = { + POWER: None, + INPUT: None, + } + try: - state = await self.device.get_state() - except JvcProjectorConnectError as err: + state[POWER] = await self.device.get(cmd.Power) + + if state[POWER] == cmd.Power.ON: + state[INPUT] = await self.device.get(cmd.Input) + + 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 - old_interval = self.update_interval - - if state[const.POWER] != const.STANDBY: + if state[POWER] != cmd.Power.STANDBY: self.update_interval = INTERVAL_FAST else: self.update_interval = INTERVAL_SLOW - if self.update_interval != old_interval: - _LOGGER.debug("Changed update interval to %s", self.update_interval) - return state diff --git a/homeassistant/components/jvc_projector/entity.py b/homeassistant/components/jvc_projector/entity.py index a88fba03cb03..317bc5ce6542 100644 --- a/homeassistant/components/jvc_projector/entity.py +++ b/homeassistant/components/jvc_projector/entity.py @@ -26,7 +26,7 @@ class JvcProjectorEntity(CoordinatorEntity[JvcProjectorDataUpdateCoordinator]): self._attr_unique_id = coordinator.unique_id self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.unique_id)}, + identifiers={(DOMAIN, self._attr_unique_id)}, name=NAME, model=self.device.model, manufacturer=MANUFACTURER, diff --git a/homeassistant/components/jvc_projector/manifest.json b/homeassistant/components/jvc_projector/manifest.json index 530373850ebc..46f04de70940 100644 --- a/homeassistant/components/jvc_projector/manifest.json +++ b/homeassistant/components/jvc_projector/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["jvcprojector"], - "requirements": ["pyjvcprojector==1.1.3"] + "requirements": ["pyjvcprojector==2.0.0"] } diff --git a/homeassistant/components/jvc_projector/remote.py b/homeassistant/components/jvc_projector/remote.py index 22855a9f8013..f2e436f41d07 100644 --- a/homeassistant/components/jvc_projector/remote.py +++ b/homeassistant/components/jvc_projector/remote.py @@ -7,54 +7,62 @@ from collections.abc import Iterable import logging from typing import Any -from jvcprojector import const +from jvcprojector import command as cmd from homeassistant.components.remote import RemoteEntity 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 -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, - "mode_4": const.REMOTE_MODE_4, - "mode_5": const.REMOTE_MODE_5, - "mode_6": const.REMOTE_MODE_6, - "mode_7": const.REMOTE_MODE_7, - "mode_8": const.REMOTE_MODE_8, - "mode_9": const.REMOTE_MODE_9, - "mode_10": const.REMOTE_MODE_10, - "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, +COMMANDS: list[str] = [ + cmd.Remote.MENU, + cmd.Remote.UP, + cmd.Remote.DOWN, + cmd.Remote.LEFT, + cmd.Remote.RIGHT, + cmd.Remote.OK, + cmd.Remote.BACK, + cmd.Remote.MPC, + cmd.Remote.HIDE, + cmd.Remote.INFO, + cmd.Remote.INPUT, + cmd.Remote.CMD, + cmd.Remote.ADVANCED_MENU, + cmd.Remote.PICTURE_MODE, + cmd.Remote.COLOR_PROFILE, + cmd.Remote.LENS_CONTROL, + cmd.Remote.SETTING_MEMORY, + cmd.Remote.GAMMA_SETTINGS, + cmd.Remote.HDMI1, + cmd.Remote.HDMI2, + cmd.Remote.MODE_1, + cmd.Remote.MODE_2, + cmd.Remote.MODE_3, + cmd.Remote.MODE_4, + cmd.Remote.MODE_5, + cmd.Remote.MODE_6, + cmd.Remote.MODE_7, + cmd.Remote.MODE_8, + cmd.Remote.MODE_9, + cmd.Remote.MODE_10, + cmd.Remote.GAMMA, + cmd.Remote.NATURAL, + cmd.Remote.CINEMA, + cmd.Remote.COLOR_TEMP, + cmd.Remote.ANAMORPHIC, + cmd.Remote.LENS_APERTURE, + cmd.Remote.V3D_FORMAT, +] + +RENAMED_COMMANDS: dict[str, str] = { + "anamo": cmd.Remote.ANAMORPHIC, + "lens_ap": cmd.Remote.LENS_APERTURE, + "hdmi1": cmd.Remote.HDMI1, + "hdmi2": cmd.Remote.HDMI2, } _LOGGER = logging.getLogger(__name__) @@ -77,25 +85,34 @@ class JvcProjectorRemote(JvcProjectorEntity, RemoteEntity): @property def is_on(self) -> bool: - """Return True if entity is on.""" - return self.coordinator.data["power"] in [const.ON, const.WARMING] + """Return True if the entity is on.""" + return self.coordinator.data[POWER] in (cmd.Power.ON, cmd.Power.WARMING) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - await self.device.power_on() + await self.device.set(cmd.Power, cmd.Power.ON) await asyncio.sleep(1) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - await self.device.power_off() + await self.device.set(cmd.Power, cmd.Power.OFF) await asyncio.sleep(1) await self.coordinator.async_refresh() async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a remote command to the device.""" - 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]) + for send_command in command: + # Legacy name replace + if send_command in RENAMED_COMMANDS: + send_command = RENAMED_COMMANDS[send_command] + + # Legacy name fixup + if "_" in send_command: + send_command = send_command.replace("_", "-") + + if send_command not in COMMANDS: + raise HomeAssistantError(f"{send_command} is not a known command") + + _LOGGER.debug("Sending command '%s'", send_command) + await self.device.remote(send_command) diff --git a/homeassistant/components/jvc_projector/select.py b/homeassistant/components/jvc_projector/select.py index b83695609cbc..861c2846a0ae 100644 --- a/homeassistant/components/jvc_projector/select.py +++ b/homeassistant/components/jvc_projector/select.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import Final -from jvcprojector import JvcProjector, const +from jvcprojector import JvcProjector, command as cmd from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.core import HomeAssistant @@ -23,16 +23,12 @@ class JvcProjectorSelectDescription(SelectEntityDescription): command: Callable[[JvcProjector, str], Awaitable[None]] -OPTIONS: Final[dict[str, dict[str, str]]] = { - "input": {const.HDMI1: const.REMOTE_HDMI_1, const.HDMI2: const.REMOTE_HDMI_2} -} - SELECTS: Final[list[JvcProjectorSelectDescription]] = [ JvcProjectorSelectDescription( key="input", translation_key="input", - options=list(OPTIONS["input"]), - command=lambda device, option: device.remote(OPTIONS["input"][option]), + options=[cmd.Input.HDMI1, cmd.Input.HDMI2], + command=lambda device, option: device.set(cmd.Input, option), ) ] diff --git a/homeassistant/components/jvc_projector/sensor.py b/homeassistant/components/jvc_projector/sensor.py index 7a7799bc4eed..dd0c16e6fffd 100644 --- a/homeassistant/components/jvc_projector/sensor.py +++ b/homeassistant/components/jvc_projector/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from jvcprojector import const +from jvcprojector import command as cmd from homeassistant.components.sensor import ( SensorDeviceClass, @@ -23,11 +23,11 @@ JVC_SENSORS = ( device_class=SensorDeviceClass.ENUM, entity_category=EntityCategory.DIAGNOSTIC, options=[ - const.STANDBY, - const.ON, - const.WARMING, - const.COOLING, - const.ERROR, + cmd.Power.STANDBY, + cmd.Power.ON, + cmd.Power.WARMING, + cmd.Power.COOLING, + cmd.Power.ERROR, ], ), ) diff --git a/homeassistant/components/jvc_projector/strings.json b/homeassistant/components/jvc_projector/strings.json index a536b2f282a3..89c54ce5f2c5 100644 --- a/homeassistant/components/jvc_projector/strings.json +++ b/homeassistant/components/jvc_projector/strings.json @@ -35,7 +35,7 @@ }, "entity": { "binary_sensor": { - "jvc_power": { + "power": { "name": "[%key:component::binary_sensor::entity_component::power::name%]" } }, @@ -50,7 +50,7 @@ }, "sensor": { "jvc_power_status": { - "name": "Power status", + "name": "Status", "state": { "cooling": "Cooling", "error": "[%key:common::state::error%]", diff --git a/requirements_all.txt b/requirements_all.txt index d330b2d53647..b8806f54fc46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2132,7 +2132,7 @@ pyitachip2ir==0.0.7 pyituran==0.1.5 # homeassistant.components.jvc_projector -pyjvcprojector==1.1.3 +pyjvcprojector==2.0.0 # homeassistant.components.kaleidescape pykaleidescape==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83818a398116..b17d48e5551e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1806,7 +1806,7 @@ pyisy==3.4.1 pyituran==0.1.5 # homeassistant.components.jvc_projector -pyjvcprojector==1.1.3 +pyjvcprojector==2.0.0 # homeassistant.components.kaleidescape pykaleidescape==1.0.2 diff --git a/tests/components/jvc_projector/__init__.py b/tests/components/jvc_projector/__init__.py index d8554e8f4cd7..d96e004114ca 100644 --- a/tests/components/jvc_projector/__init__.py +++ b/tests/components/jvc_projector/__init__.py @@ -3,5 +3,6 @@ MOCK_HOST = "127.0.0.1" MOCK_PORT = 20554 MOCK_PASSWORD = "jvcpasswd" -MOCK_MAC = "jvcmac" -MOCK_MODEL = "jvcmodel" +MOCK_MAC = "E0DADC0A1234" +MOCK_MAC_FORMATED = "e0:da:dc:0a:12:34" +MOCK_MODEL = "B2A2" diff --git a/tests/components/jvc_projector/conftest.py b/tests/components/jvc_projector/conftest.py index 3115cbfe2522..57603e0a0551 100644 --- a/tests/components/jvc_projector/conftest.py +++ b/tests/components/jvc_projector/conftest.py @@ -3,11 +3,13 @@ from collections.abc import Generator from unittest.mock import MagicMock, patch +from jvcprojector import command as cmd import pytest from homeassistant.components.jvc_projector.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import format_mac from . import MOCK_HOST, MOCK_MAC, MOCK_MODEL, MOCK_PASSWORD, MOCK_PORT @@ -20,16 +22,31 @@ 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", + } + if hasattr(request, "param"): - target = request.param + target = request.param.get("target", target) + fixture = request.param.get("get", fixture) + + 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}") with patch(target, autospec=True) as mock: device = mock.return_value device.host = MOCK_HOST device.port = MOCK_PORT - device.mac = MOCK_MAC device.model = MOCK_MODEL - device.get_state.return_value = {"power": "standby", "input": "hdmi1"} + device.get.side_effect = device_get yield device @@ -38,7 +55,7 @@ def fixture_mock_config_entry() -> MockConfigEntry: """Return a mock config entry.""" return MockConfigEntry( domain=DOMAIN, - unique_id=MOCK_MAC, + unique_id=format_mac(MOCK_MAC), version=1, data={ CONF_HOST: MOCK_HOST, diff --git a/tests/components/jvc_projector/test_binary_sensor.py b/tests/components/jvc_projector/test_binary_sensor.py index b327538991ca..e7884c9c7841 100644 --- a/tests/components/jvc_projector/test_binary_sensor.py +++ b/tests/components/jvc_projector/test_binary_sensor.py @@ -2,9 +2,13 @@ from unittest.mock import MagicMock +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.jvc_projector.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from . import MOCK_MAC, MOCK_MAC_FORMATED + from tests.common import MockConfigEntry ENTITY_ID = "binary_sensor.jvc_projector_power" @@ -20,3 +24,40 @@ async def test_entity_state( entity = hass.states.get(ENTITY_ID) assert entity assert entity_registry.async_get(entity.entity_id) + + +async def test_migrate_old_unique_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_device: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Tests power binary sensor unique id is reformatted.""" + mock_config_entry.add_to_hass(hass) + + # Entity to be migrated + entity_registry.async_get_or_create( + BINARY_SENSOR_DOMAIN, + DOMAIN, + f"{MOCK_MAC}_power", + config_entry=mock_config_entry, + suggested_object_id="jvc_projector_power", + ) + + # Ignored entity to get to 100% coverage + entity_registry.async_get_or_create( + BINARY_SENSOR_DOMAIN, + DOMAIN, + f"{MOCK_MAC}_ignore", + config_entry=mock_config_entry, + suggested_object_id="jvc_projector_ignore", + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entity = hass.states.get(ENTITY_ID) + assert entity + entry = entity_registry.async_get(entity.entity_id) + assert entry + assert entry.unique_id == f"{MOCK_MAC_FORMATED}_power" diff --git a/tests/components/jvc_projector/test_config_flow.py b/tests/components/jvc_projector/test_config_flow.py index d7eb0995bbd5..d1d4287ab04c 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 import JvcProjectorAuthError, JvcProjectorTimeoutError import pytest from homeassistant.components.jvc_projector.const import DOMAIN @@ -18,7 +18,7 @@ from tests.common import MockConfigEntry TARGET = "homeassistant.components.jvc_projector.config_flow.JvcProjector" -@pytest.mark.parametrize("mock_device", [TARGET], indirect=True) +@pytest.mark.parametrize("mock_device", [{"target": TARGET}], indirect=True) async def test_user_config_flow_success( hass: HomeAssistant, mock_device: AsyncMock ) -> None: @@ -46,12 +46,12 @@ async def test_user_config_flow_success( assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD -@pytest.mark.parametrize("mock_device", [TARGET], indirect=True) +@pytest.mark.parametrize("mock_device", [{"target": TARGET}], indirect=True) async def test_user_config_flow_bad_connect_errors( hass: HomeAssistant, mock_device: AsyncMock ) -> None: """Test errors when connection error occurs.""" - mock_device.connect.side_effect = JvcProjectorConnectError + mock_device.connect.side_effect = JvcProjectorTimeoutError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -80,7 +80,7 @@ async def test_user_config_flow_bad_connect_errors( assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD -@pytest.mark.parametrize("mock_device", [TARGET], indirect=True) +@pytest.mark.parametrize("mock_device", [{"target": TARGET}], indirect=True) async def test_user_config_flow_device_exists_abort( hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry ) -> None: @@ -94,7 +94,7 @@ async def test_user_config_flow_device_exists_abort( assert result["reason"] == "already_configured" -@pytest.mark.parametrize("mock_device", [TARGET], indirect=True) +@pytest.mark.parametrize("mock_device", [{"target": TARGET}], indirect=True) async def test_user_config_flow_bad_host_errors( hass: HomeAssistant, mock_device: AsyncMock ) -> None: @@ -124,7 +124,7 @@ async def test_user_config_flow_bad_host_errors( assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD -@pytest.mark.parametrize("mock_device", [TARGET], indirect=True) +@pytest.mark.parametrize("mock_device", [{"target": TARGET}], indirect=True) async def test_user_config_flow_bad_auth_errors( hass: HomeAssistant, mock_device: AsyncMock ) -> None: @@ -158,7 +158,7 @@ async def test_user_config_flow_bad_auth_errors( assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD -@pytest.mark.parametrize("mock_device", [TARGET], indirect=True) +@pytest.mark.parametrize("mock_device", [{"target": TARGET}], indirect=True) async def test_reauth_config_flow_success( hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry ) -> None: @@ -180,7 +180,7 @@ async def test_reauth_config_flow_success( assert mock_integration.data[CONF_PASSWORD] == MOCK_PASSWORD -@pytest.mark.parametrize("mock_device", [TARGET], indirect=True) +@pytest.mark.parametrize("mock_device", [{"target": TARGET}], indirect=True) async def test_reauth_config_flow_auth_error( hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry ) -> None: @@ -221,12 +221,12 @@ async def test_reauth_config_flow_auth_error( assert mock_integration.data[CONF_PASSWORD] == MOCK_PASSWORD -@pytest.mark.parametrize("mock_device", [TARGET], indirect=True) +@pytest.mark.parametrize("mock_device", [{"target": TARGET}], indirect=True) async def test_reauth_config_flow_connect_error( hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry ) -> None: """Test reauth config flow when connect fails.""" - mock_device.connect.side_effect = JvcProjectorConnectError + mock_device.connect.side_effect = JvcProjectorTimeoutError result = await mock_integration.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM diff --git a/tests/components/jvc_projector/test_coordinator.py b/tests/components/jvc_projector/test_coordinator.py index b9211250aff7..569e894044d7 100644 --- a/tests/components/jvc_projector/test_coordinator.py +++ b/tests/components/jvc_projector/test_coordinator.py @@ -3,7 +3,8 @@ from datetime import timedelta from unittest.mock import AsyncMock -from jvcprojector import JvcProjectorAuthError, JvcProjectorConnectError +from jvcprojector import JvcProjectorAuthError, JvcProjectorTimeoutError +import pytest from homeassistant.components.jvc_projector.coordinator import ( INTERVAL_FAST, @@ -13,6 +14,8 @@ 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 @@ -22,49 +25,51 @@ async def test_coordinator_update( mock_integration: MockConfigEntry, ) -> None: """Test coordinator update runs.""" - mock_device.get_state.return_value = {"power": "standby", "input": "hdmi1"} async_fire_time_changed( hass, utcnow() + timedelta(seconds=INTERVAL_SLOW.seconds + 1) ) await hass.async_block_till_done() - assert mock_device.get_state.call_count == 3 coordinator = mock_integration.runtime_data assert coordinator.update_interval == INTERVAL_SLOW -async def test_coordinator_connect_error( +async def test_coordinator_setup_connect_error( hass: HomeAssistant, mock_device: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test coordinator connect error.""" - mock_device.get_state.side_effect = JvcProjectorConnectError + 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_auth_error( +async def test_coordinator_setup_auth_error( hass: HomeAssistant, mock_device: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test coordinator auth error.""" - mock_device.get_state.side_effect = JvcProjectorAuthError + 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 +@pytest.mark.parametrize( + "mock_device", + [{"get": {"mac": MOCK_MAC, "power": "on", "input": "hdmi-1"}}], + indirect=True, +) async def test_coordinator_device_on( hass: HomeAssistant, mock_device: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test coordinator changes update interval when device is on.""" - mock_device.get_state.return_value = {"power": "on", "input": "hdmi1"} mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/jvc_projector/test_init.py b/tests/components/jvc_projector/test_init.py index baf088a5dba6..938196808451 100644 --- a/tests/components/jvc_projector/test_init.py +++ b/tests/components/jvc_projector/test_init.py @@ -2,12 +2,14 @@ from unittest.mock import AsyncMock -from jvcprojector import JvcProjectorAuthError, JvcProjectorConnectError +from jvcprojector import JvcProjectorAuthError, JvcProjectorTimeoutError from homeassistant.components.jvc_projector.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import format_mac from . import MOCK_MAC @@ -21,9 +23,10 @@ async def test_init( mock_integration: MockConfigEntry, ) -> None: """Test initialization.""" - device = device_registry.async_get_device(identifiers={(DOMAIN, MOCK_MAC)}) + mac = format_mac(MOCK_MAC) + device = device_registry.async_get_device(identifiers={(DOMAIN, mac)}) assert device is not None - assert device.identifiers == {(DOMAIN, MOCK_MAC)} + assert device.identifiers == {(DOMAIN, mac)} async def test_unload_config_entry( @@ -38,6 +41,23 @@ async def test_unload_config_entry( await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() + assert mock_device.disconnect.call_count == 1 + + +async def test_disconnect_on_hass_stop( + hass: HomeAssistant, + mock_device: AsyncMock, + mock_integration: MockConfigEntry, +) -> None: + """Test device disconnects when Home Assistant stops.""" + assert mock_integration.state is ConfigEntryState.LOADED + assert mock_device.disconnect.call_count == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert mock_device.disconnect.call_count == 1 + async def test_config_entry_connect_error( hass: HomeAssistant, @@ -45,7 +65,7 @@ async def test_config_entry_connect_error( mock_config_entry: MockConfigEntry, ) -> None: """Test config entry with connect error.""" - mock_device.connect.side_effect = JvcProjectorConnectError + mock_device.connect.side_effect = JvcProjectorTimeoutError mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) diff --git a/tests/components/jvc_projector/test_remote.py b/tests/components/jvc_projector/test_remote.py index 28bf835e0329..56e929d4898f 100644 --- a/tests/components/jvc_projector/test_remote.py +++ b/tests/components/jvc_projector/test_remote.py @@ -43,7 +43,7 @@ async def test_commands( {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True, ) - assert mock_device.power_on.call_count == 1 + assert mock_device.get.call_count == 3 await hass.services.async_call( REMOTE_DOMAIN, @@ -51,7 +51,7 @@ async def test_commands( {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True, ) - assert mock_device.power_off.call_count == 1 + assert mock_device.get.call_count == 4 await hass.services.async_call( REMOTE_DOMAIN, @@ -64,11 +64,27 @@ 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: ["hdmi1"]}, blocking=True, ) assert mock_device.remote.call_count == 2 + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["anamo"]}, + blocking=True, + ) + assert mock_device.remote.call_count == 3 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["picture_mode"]}, + blocking=True, + ) + assert mock_device.remote.call_count == 4 + async def test_unknown_command( hass: HomeAssistant, diff --git a/tests/components/jvc_projector/test_select.py b/tests/components/jvc_projector/test_select.py index a52133bd688e..5e8bed6eec70 100644 --- a/tests/components/jvc_projector/test_select.py +++ b/tests/components/jvc_projector/test_select.py @@ -2,7 +2,8 @@ from unittest.mock import MagicMock -from jvcprojector import const +from jvcprojector import command as cmd +import pytest from homeassistant.components.select import ( ATTR_OPTIONS, @@ -13,11 +14,18 @@ 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, @@ -28,17 +36,15 @@ async def 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 + assert entity.attributes.get(ATTR_OPTIONS) == [cmd.Input.HDMI1, cmd.Input.HDMI2] await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, { ATTR_ENTITY_ID: INPUT_ENTITY_ID, - ATTR_OPTION: const.HDMI2, + ATTR_OPTION: cmd.Input.HDMI2, }, blocking=True, ) - - mock_device.remote.assert_called_once_with(const.REMOTE_HDMI_2) + mock_device.set.assert_called_once_with(cmd.Input, cmd.Input.HDMI2) diff --git a/tests/components/jvc_projector/test_sensor.py b/tests/components/jvc_projector/test_sensor.py index 1827363e5ad9..87ebe737dec9 100644 --- a/tests/components/jvc_projector/test_sensor.py +++ b/tests/components/jvc_projector/test_sensor.py @@ -7,7 +7,7 @@ from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -POWER_ID = "sensor.jvc_projector_power_status" +POWER_ID = "sensor.jvc_projector_status" async def test_entity_state(