From 48893d4daa75efeeae54c9d1af211063d5f727dd Mon Sep 17 00:00:00 2001 From: Steve Easley Date: Thu, 12 Feb 2026 08:21:34 -0500 Subject: [PATCH] Add JVC Projector switch platform (#161899) Co-authored-by: Joostlek --- .../components/jvc_projector/__init__.py | 8 +- .../components/jvc_projector/icons.json | 8 ++ .../components/jvc_projector/strings.json | 8 ++ .../components/jvc_projector/switch.py | 82 +++++++++++++++ tests/components/jvc_projector/conftest.py | 6 ++ .../jvc_projector/snapshots/test_switch.ambr | 99 +++++++++++++++++++ tests/components/jvc_projector/test_switch.py | 70 +++++++++++++ 7 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/jvc_projector/switch.py create mode 100644 tests/components/jvc_projector/snapshots/test_switch.ambr create mode 100644 tests/components/jvc_projector/test_switch.py diff --git a/homeassistant/components/jvc_projector/__init__.py b/homeassistant/components/jvc_projector/__init__.py index a1bbef674d0..a12bea0e158 100644 --- a/homeassistant/components/jvc_projector/__init__.py +++ b/homeassistant/components/jvc_projector/__init__.py @@ -17,7 +17,13 @@ from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_e from .coordinator import JVCConfigEntry, JvcProjectorDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.REMOTE, Platform.SELECT, Platform.SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.REMOTE, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: JVCConfigEntry) -> bool: diff --git a/homeassistant/components/jvc_projector/icons.json b/homeassistant/components/jvc_projector/icons.json index 803267c221f..9280ea6f1e4 100644 --- a/homeassistant/components/jvc_projector/icons.json +++ b/homeassistant/components/jvc_projector/icons.json @@ -53,6 +53,14 @@ "warming": "mdi:heat-wave" } } + }, + "switch": { + "eshift": { + "default": "mdi:blur-linear" + }, + "low_latency_mode": { + "default": "mdi:gamepad-round-outline" + } } } } diff --git a/homeassistant/components/jvc_projector/strings.json b/homeassistant/components/jvc_projector/strings.json index 01b192e0270..dee8a8f661a 100644 --- a/homeassistant/components/jvc_projector/strings.json +++ b/homeassistant/components/jvc_projector/strings.json @@ -173,6 +173,14 @@ "warming": "Warming" } } + }, + "switch": { + "eshift": { + "name": "E-Shift" + }, + "low_latency_mode": { + "name": "Low latency mode" + } } } } diff --git a/homeassistant/components/jvc_projector/switch.py b/homeassistant/components/jvc_projector/switch.py new file mode 100644 index 00000000000..ae80c7bf109 --- /dev/null +++ b/homeassistant/components/jvc_projector/switch.py @@ -0,0 +1,82 @@ +"""Switch platform for the jvc_projector integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Final + +from jvcprojector import Command, command as cmd + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import JVCConfigEntry, JvcProjectorDataUpdateCoordinator +from .entity import JvcProjectorEntity + + +@dataclass(frozen=True, kw_only=True) +class JvcProjectorSwitchDescription(SwitchEntityDescription): + """Describes JVC Projector switch entities.""" + + command: type[Command] + + +SWITCHES: Final[tuple[JvcProjectorSwitchDescription, ...]] = ( + JvcProjectorSwitchDescription( + key="low_latency_mode", + command=cmd.LowLatencyMode, + entity_registry_enabled_default=False, + ), + JvcProjectorSwitchDescription( + key="eshift", + command=cmd.EShift, + entity_registry_enabled_default=False, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: JVCConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the JVC Projector switch platform from a config entry.""" + coordinator = entry.runtime_data + + async_add_entities( + JvcProjectorSwitchEntity(coordinator, description) + for description in SWITCHES + if coordinator.supports(description.command) + ) + + +class JvcProjectorSwitchEntity(JvcProjectorEntity, SwitchEntity): + """JVC Projector class for switch entities.""" + + def __init__( + self, + coordinator: JvcProjectorDataUpdateCoordinator, + description: JvcProjectorSwitchDescription, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator, description.command) + self.command: type[Command] = description.command + + self.entity_description = description + self._attr_translation_key = description.key + self._attr_unique_id = f"{self._attr_unique_id}_{description.key}" + + @property + def is_on(self) -> bool: + """Return True if the entity is on.""" + return self.coordinator.data.get(self.command.name) == STATE_ON + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.coordinator.device.set(self.command, STATE_ON) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.coordinator.device.set(self.command, STATE_OFF) diff --git a/tests/components/jvc_projector/conftest.py b/tests/components/jvc_projector/conftest.py index 505eeebabe2..e61cd1208e2 100644 --- a/tests/components/jvc_projector/conftest.py +++ b/tests/components/jvc_projector/conftest.py @@ -26,6 +26,7 @@ FIXTURES: dict[str, dict[type[Command], str | type[Exception]]] = { cmd.Source: JvcProjectorTimeoutError, cmd.Hdr: JvcProjectorTimeoutError, cmd.HdrProcessing: JvcProjectorTimeoutError, + cmd.EShift: JvcProjectorTimeoutError, }, "on": { cmd.MacAddress: MOCK_MAC, @@ -37,6 +38,7 @@ FIXTURES: dict[str, dict[type[Command], str | type[Exception]]] = { cmd.Source: "4k", cmd.Hdr: "hdr", cmd.HdrProcessing: "static", + cmd.EShift: "on", }, } @@ -69,6 +71,10 @@ CAPABILITIES = { "name": cmd.LightTime.name, "parameter": "empty", }, + cmd.EShift.name: { + "name": cmd.EShift.name, + "parameter": {"read": {"0": "off", "1": "on"}}, + }, } diff --git a/tests/components/jvc_projector/snapshots/test_switch.ambr b/tests/components/jvc_projector/snapshots/test_switch.ambr new file mode 100644 index 00000000000..1ea160ae591 --- /dev/null +++ b/tests/components/jvc_projector/snapshots/test_switch.ambr @@ -0,0 +1,99 @@ +# serializer version: 1 +# name: test_entities[switch.jvc_projector_e_shift-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.jvc_projector_e_shift', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'E-Shift', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'E-Shift', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'eshift', + 'unique_id': 'e0:da:dc:0a:12:34_eshift', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.jvc_projector_e_shift-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'JVC Projector E-Shift', + }), + 'context': , + 'entity_id': 'switch.jvc_projector_e_shift', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[switch.jvc_projector_low_latency_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.jvc_projector_low_latency_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Low latency mode', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Low latency mode', + 'platform': 'jvc_projector', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'low_latency_mode', + 'unique_id': 'e0:da:dc:0a:12:34_low_latency_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.jvc_projector_low_latency_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'JVC Projector Low latency mode', + }), + 'context': , + 'entity_id': 'switch.jvc_projector_low_latency_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/jvc_projector/test_switch.py b/tests/components/jvc_projector/test_switch.py new file mode 100644 index 00000000000..62d3b6df904 --- /dev/null +++ b/tests/components/jvc_projector/test_switch.py @@ -0,0 +1,70 @@ +"""Tests for JVC Projector switch platform.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from jvcprojector import command as cmd +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + +ESHIFT_ENTITY_ID = "switch.jvc_projector_e_shift" +LOW_LATENCY_ENTITY_ID = "switch.jvc_projector_low_latency_mode" + + +@pytest.fixture(autouse=True) +def platform() -> Generator[AsyncMock]: + """Fixture for platform.""" + with patch("homeassistant.components.jvc_projector.PLATFORMS", [Platform.SWITCH]): + yield + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entities( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test entities.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_switch_entities( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test switch entities.""" + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ESHIFT_ENTITY_ID}, + blocking=True, + ) + + mock_device.set.assert_any_call(cmd.EShift, "off") + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ESHIFT_ENTITY_ID}, + blocking=True, + ) + + mock_device.set.assert_any_call(cmd.EShift, "on")