mirror of
https://github.com/home-assistant/core.git
synced 2026-04-02 08:26:41 +01:00
Move jvc_projector sensor entities to select domain (#165194)
This commit is contained in:
@@ -151,7 +151,9 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||
|
||||
return value
|
||||
|
||||
def get_options_map(self, command: str) -> dict[str, str]:
|
||||
def get_options_map(
|
||||
self, command: str, *, snake_case: bool = False
|
||||
) -> dict[str, str]:
|
||||
"""Get the available options for a command."""
|
||||
capabilities = self.capabilities.get(command, {})
|
||||
|
||||
@@ -162,7 +164,10 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||
|
||||
values = list(capabilities.get("parameter", {}).get("read", {}).values())
|
||||
|
||||
return {v: v.translate(TRANSLATIONS) for v in values}
|
||||
options = {v: v.translate(TRANSLATIONS) for v in values}
|
||||
if snake_case:
|
||||
return {k: v.replace("-", "_") for k, v in options.items()}
|
||||
return options
|
||||
|
||||
def supports(self, command: type[Command]) -> bool:
|
||||
"""Check if the device supports a command."""
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
"dynamic_control": {
|
||||
"default": "mdi:lightbulb-on-outline"
|
||||
},
|
||||
"hdr_processing": {
|
||||
"default": "mdi:image-filter-hdr-outline"
|
||||
},
|
||||
"input": {
|
||||
"default": "mdi:hdmi-port"
|
||||
},
|
||||
@@ -26,6 +29,9 @@
|
||||
},
|
||||
"light_power": {
|
||||
"default": "mdi:lightbulb-on-outline"
|
||||
},
|
||||
"picture_mode": {
|
||||
"default": "mdi:movie-roll"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
||||
@@ -20,6 +20,7 @@ class JvcProjectorSelectDescription(SelectEntityDescription):
|
||||
"""Describes JVC Projector select entities."""
|
||||
|
||||
command: type[Command]
|
||||
snake_case_states: bool = False
|
||||
|
||||
|
||||
SELECTS: Final[tuple[JvcProjectorSelectDescription, ...]] = (
|
||||
@@ -49,6 +50,18 @@ SELECTS: Final[tuple[JvcProjectorSelectDescription, ...]] = (
|
||||
command=cmd.Anamorphic,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
JvcProjectorSelectDescription(
|
||||
key="hdr_processing",
|
||||
command=cmd.HdrProcessing,
|
||||
entity_registry_enabled_default=False,
|
||||
snake_case_states=True,
|
||||
),
|
||||
JvcProjectorSelectDescription(
|
||||
key="picture_mode",
|
||||
command=cmd.PictureMode,
|
||||
entity_registry_enabled_default=False,
|
||||
snake_case_states=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -84,7 +97,8 @@ class JvcProjectorSelectEntity(JvcProjectorEntity, SelectEntity):
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{description.key}"
|
||||
|
||||
self._options_map: dict[str, str] = coordinator.get_options_map(
|
||||
self.command.name
|
||||
self.command.name,
|
||||
snake_case=description.snake_case_states,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -7,16 +7,19 @@ from dataclasses import dataclass
|
||||
from jvcprojector import Command, command as cmd
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import JVCConfigEntry, JvcProjectorDataUpdateCoordinator
|
||||
from .entity import JvcProjectorEntity
|
||||
from .util import deprecate_entity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -84,12 +87,29 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the JVC Projector platform from a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
async_add_entities(
|
||||
JvcProjectorSensorEntity(coordinator, description)
|
||||
for description in SENSORS
|
||||
if coordinator.supports(description.command)
|
||||
)
|
||||
entities: list[JvcProjectorSensorEntity] = []
|
||||
for description in SENSORS:
|
||||
if not coordinator.supports(description.command):
|
||||
continue
|
||||
if description.key in (
|
||||
"hdr_processing",
|
||||
"picture_mode",
|
||||
) and not deprecate_entity(
|
||||
hass,
|
||||
entity_registry,
|
||||
SENSOR_DOMAIN,
|
||||
f"{coordinator.unique_id}_{description.key}",
|
||||
f"deprecated_sensor_{entry.entry_id}_{description.key}",
|
||||
"deprecated_sensor",
|
||||
f"{coordinator.unique_id}_{description.key}",
|
||||
f"select.jvc_projector_{description.key}",
|
||||
):
|
||||
continue
|
||||
entities.append(JvcProjectorSensorEntity(coordinator, description))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class JvcProjectorSensorEntity(JvcProjectorEntity, SensorEntity):
|
||||
|
||||
@@ -71,6 +71,15 @@
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"hdr_processing": {
|
||||
"name": "HDR Processing",
|
||||
"state": {
|
||||
"frame_by_frame": "Frame-by-Frame",
|
||||
"hdr10p": "HDR10+",
|
||||
"scene_by_scene": "Scene-by-Scene",
|
||||
"static": "Static"
|
||||
}
|
||||
},
|
||||
"input": {
|
||||
"name": "Input",
|
||||
"state": {
|
||||
@@ -101,6 +110,23 @@
|
||||
"mid": "[%key:common::state::medium%]",
|
||||
"normal": "[%key:common::state::normal%]"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -156,7 +182,7 @@
|
||||
"hdr10": "HDR10",
|
||||
"hdr10-ll": "HDR10 LL",
|
||||
"hdr2": "HDR2",
|
||||
"last-setting": "Last Setting",
|
||||
"last-setting": "Last setting",
|
||||
"pana-pq": "Pana PQ",
|
||||
"user-4": "User 4",
|
||||
"user-5": "User 5",
|
||||
@@ -182,5 +208,15 @@
|
||||
"name": "Low latency mode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_sensor": {
|
||||
"description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with `{replacement_entity_id}`.\n\nUpdate your dashboards, templates, automations and scripts to use the replacement entity, then disable the deprecated sensor to have it removed after the next restart.",
|
||||
"title": "Deprecated sensor detected"
|
||||
},
|
||||
"deprecated_sensor_scripts": {
|
||||
"description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with `{replacement_entity_id}`.\n\nThe sensor was used in the following automations or scripts:\n{items}\n\nUpdate the above automations or scripts to use the replacement entity, then disable the deprecated sensor to have it removed after the next restart.",
|
||||
"title": "[%key:component::jvc_projector::issues::deprecated_sensor::title%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
104
homeassistant/components/jvc_projector/util.py
Normal file
104
homeassistant/components/jvc_projector/util.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""Utility helpers for the jvc_projector integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def deprecate_entity(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
platform_domain: str,
|
||||
entity_unique_id: str,
|
||||
issue_id: str,
|
||||
issue_string: str,
|
||||
replacement_entity_unique_id: str,
|
||||
replacement_entity_id: str,
|
||||
version: str = "2026.9.0",
|
||||
) -> bool:
|
||||
"""Create an issue for deprecated entities."""
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
platform_domain, DOMAIN, entity_unique_id
|
||||
):
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
if not entity_entry:
|
||||
async_delete_issue(hass, DOMAIN, issue_id)
|
||||
return False
|
||||
|
||||
items = get_automations_and_scripts_using_entity(hass, entity_id)
|
||||
if entity_entry.disabled and not items:
|
||||
entity_registry.async_remove(entity_id)
|
||||
async_delete_issue(hass, DOMAIN, issue_id)
|
||||
return False
|
||||
|
||||
translation_key = issue_string
|
||||
placeholders = {
|
||||
"entity_id": entity_id,
|
||||
"entity_name": entity_entry.name or entity_entry.original_name or "Unknown",
|
||||
"replacement_entity_id": (
|
||||
entity_registry.async_get_entity_id(
|
||||
Platform.SELECT, DOMAIN, replacement_entity_unique_id
|
||||
)
|
||||
or replacement_entity_id
|
||||
),
|
||||
}
|
||||
if items:
|
||||
translation_key = f"{translation_key}_scripts"
|
||||
placeholders["items"] = "\n".join(items)
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
breaks_in_ha_version=version,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders=placeholders,
|
||||
)
|
||||
return True
|
||||
|
||||
async_delete_issue(hass, DOMAIN, issue_id)
|
||||
return False
|
||||
|
||||
|
||||
def get_automations_and_scripts_using_entity(
|
||||
hass: HomeAssistant,
|
||||
entity_id: str,
|
||||
) -> list[str]:
|
||||
"""Get automations and scripts using an entity."""
|
||||
# These helpers return referencing automation/script entity IDs.
|
||||
automations = automations_with_entity(hass, entity_id)
|
||||
scripts = scripts_with_entity(hass, entity_id)
|
||||
if not automations and not scripts:
|
||||
return []
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
items: list[str] = []
|
||||
|
||||
for integration, entities in (
|
||||
("automation", automations),
|
||||
("script", scripts),
|
||||
):
|
||||
for used_entity_id in entities:
|
||||
# Prefer entity-registry metadata so we can render edit links.
|
||||
if item := entity_registry.async_get(used_entity_id):
|
||||
items.append(
|
||||
f"- [{item.original_name}](/config/{integration}/edit/{item.unique_id})"
|
||||
)
|
||||
else:
|
||||
# Keep unresolved references as plain text so they still count as usage.
|
||||
items.append(f"- `{used_entity_id}`")
|
||||
|
||||
return items
|
||||
@@ -1,15 +1,16 @@
|
||||
"""Tests for JVC Projector config entry."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
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.const import EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.issue_registry import IssueRegistry
|
||||
|
||||
from . import MOCK_MAC
|
||||
|
||||
@@ -87,3 +88,48 @@ async def test_config_entry_auth_error(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
async def test_deprecated_sensor_issue_lifecycle(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: IssueRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test deprecated sensor cleanup and issue lifecycle."""
|
||||
sensor_unique_id = f"{format_mac(MOCK_MAC)}_hdr_processing"
|
||||
issue_id = f"deprecated_sensor_{mock_integration.entry_id}_hdr_processing"
|
||||
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(Platform.SENSOR, DOMAIN, sensor_unique_id)
|
||||
is None
|
||||
)
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
|
||||
|
||||
sensor_entry = entity_registry.async_get_or_create(
|
||||
Platform.SENSOR,
|
||||
DOMAIN,
|
||||
sensor_unique_id,
|
||||
config_entry=mock_integration,
|
||||
suggested_object_id="jvc_projector_hdr_processing",
|
||||
disabled_by=er.RegistryEntryDisabler.INTEGRATION,
|
||||
)
|
||||
entity_id = sensor_entry.entity_id
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.jvc_projector.util.get_automations_and_scripts_using_entity",
|
||||
return_value=["- [Test Automation](/config/automation/edit/test_automation)"],
|
||||
):
|
||||
await hass.config_entries.async_reload(mock_integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert issue is not None
|
||||
assert issue.translation_key == "deprecated_sensor_scripts"
|
||||
assert entity_registry.async_get(entity_id) is not None
|
||||
|
||||
await hass.config_entries.async_reload(mock_integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity_registry.async_get(entity_id) is None
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"""Tests for JVC Projector select platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from jvcprojector import command as cmd
|
||||
|
||||
from homeassistant.components.jvc_projector.coordinator import INTERVAL_FAST
|
||||
from homeassistant.components.select import (
|
||||
ATTR_OPTIONS,
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
@@ -13,9 +16,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 tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
INPUT_ENTITY_ID = "select.jvc_projector_input"
|
||||
HDR_PROCESSING_ENTITY_ID = "select.jvc_projector_hdr_processing"
|
||||
HDR_SENSOR_ENTITY_ID = "sensor.jvc_projector_hdr"
|
||||
|
||||
|
||||
async def test_input_select(
|
||||
@@ -40,3 +45,43 @@ async def test_input_select(
|
||||
blocking=True,
|
||||
)
|
||||
mock_device.set.assert_called_once_with(cmd.Input, cmd.Input.HDMI2)
|
||||
|
||||
|
||||
async def test_enable_hdr_processing_select(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_device: MagicMock,
|
||||
mock_integration: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test enabling the HDR processing select (disabled by default)."""
|
||||
entry = entity_registry.async_get(HDR_PROCESSING_ENTITY_ID)
|
||||
assert entry is not None
|
||||
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
|
||||
hdr_sensor_entry = entity_registry.async_get(HDR_SENSOR_ENTITY_ID)
|
||||
assert hdr_sensor_entry is not None
|
||||
assert hdr_sensor_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
|
||||
entity_registry.async_update_entity(HDR_SENSOR_ENTITY_ID, disabled_by=None)
|
||||
entity_registry.async_update_entity(HDR_PROCESSING_ENTITY_ID, disabled_by=None)
|
||||
await hass.config_entries.async_reload(mock_integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(HDR_PROCESSING_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == "unknown"
|
||||
|
||||
freezer.tick(timedelta(seconds=INTERVAL_FAST.seconds + 1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(HDR_PROCESSING_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == "static"
|
||||
|
||||
freezer.tick(timedelta(seconds=INTERVAL_FAST.seconds + 1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(HDR_PROCESSING_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == "static"
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
"""Tests for the JVC Projector binary sensor device."""
|
||||
"""Tests for JVC Projector sensor platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeassistant.components.jvc_projector.coordinator import INTERVAL_FAST
|
||||
from jvcprojector import command as cmd
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
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(
|
||||
@@ -34,7 +32,7 @@ async def test_enable_hdr_sensor(
|
||||
mock_device,
|
||||
mock_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test enabling the HDR select (disabled by default)."""
|
||||
"""Test enabling the HDR sensor (disabled by default)."""
|
||||
|
||||
# Test entity is disabled initially
|
||||
entry = entity_registry.async_get(HDR_ENTITY_ID)
|
||||
@@ -43,8 +41,6 @@ async def test_enable_hdr_sensor(
|
||||
|
||||
# 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()
|
||||
@@ -53,16 +49,18 @@ async def test_enable_hdr_sensor(
|
||||
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)
|
||||
)
|
||||
|
||||
async def test_unsupported_sensor_not_added(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_device: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test unsupported sensor descriptions are skipped."""
|
||||
mock_device.supports.side_effect = lambda command: command is not cmd.ColorDepth
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
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
|
||||
assert entity_registry.async_get("sensor.jvc_projector_color_depth") is None
|
||||
|
||||
Reference in New Issue
Block a user