diff --git a/CODEOWNERS b/CODEOWNERS index 6146338ddc1335..f6ffb7ad5cbb5f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -758,6 +758,8 @@ CLAUDE.md @home-assistant/core /tests/components/homewizard/ @DCSBL /homeassistant/components/honeywell/ @rdfurman @mkmer /tests/components/honeywell/ @rdfurman @mkmer +/homeassistant/components/honeywell_string_lights/ @balloob +/tests/components/honeywell_string_lights/ @balloob /homeassistant/components/hr_energy_qube/ @MattieGit /tests/components/hr_energy_qube/ @MattieGit /homeassistant/components/html5/ @alexyao2015 @tr4nt0r diff --git a/homeassistant/brands/honeywell.json b/homeassistant/brands/honeywell.json index 37cd6d8ce732e0..001db20de07afe 100644 --- a/homeassistant/brands/honeywell.json +++ b/homeassistant/brands/honeywell.json @@ -1,5 +1,5 @@ { "domain": "honeywell", "name": "Honeywell", - "integrations": ["lyric", "evohome", "honeywell"] + "integrations": ["lyric", "evohome", "honeywell", "honeywell_string_lights"] } diff --git a/homeassistant/components/honeywell_string_lights/__init__.py b/homeassistant/components/honeywell_string_lights/__init__.py new file mode 100644 index 00000000000000..f5c7b4b09a5d33 --- /dev/null +++ b/homeassistant/components/honeywell_string_lights/__init__.py @@ -0,0 +1,20 @@ +"""The Honeywell String Lights integration.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +PLATFORMS: list[Platform] = [Platform.LIGHT] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Honeywell String Lights from a config entry.""" + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/honeywell_string_lights/config_flow.py b/homeassistant/components/honeywell_string_lights/config_flow.py new file mode 100644 index 00000000000000..f659a1403d4b23 --- /dev/null +++ b/homeassistant/components/honeywell_string_lights/config_flow.py @@ -0,0 +1,61 @@ +"""Config flow for the Honeywell String Lights integration.""" + +from __future__ import annotations + +from typing import Any + +from rf_protocols import RadioFrequencyCommand +import voluptuous as vol + +from homeassistant.components.radio_frequency import async_get_transmitters +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er, selector + +from .const import CONF_TRANSMITTER, DOMAIN +from .light import COMMANDS + + +class HoneywellStringLightsConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Honeywell String Lights.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + sample_command: RadioFrequencyCommand = await self.hass.async_add_executor_job( + COMMANDS.load_command, "turn_on" + ) + try: + transmitters = async_get_transmitters( + self.hass, sample_command.frequency, sample_command.modulation + ) + except HomeAssistantError: + return self.async_abort(reason="no_transmitters") + + if not transmitters: + return self.async_abort(reason="no_compatible_transmitters") + + if user_input is not None: + registry = er.async_get(self.hass) + entity_entry = registry.async_get(user_input[CONF_TRANSMITTER]) + assert entity_entry is not None + await self.async_set_unique_id(entity_entry.id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title="Honeywell String Lights", + data={CONF_TRANSMITTER: entity_entry.id}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_TRANSMITTER): selector.EntitySelector( + selector.EntitySelectorConfig(include_entities=transmitters), + ), + } + ), + ) diff --git a/homeassistant/components/honeywell_string_lights/const.py b/homeassistant/components/honeywell_string_lights/const.py new file mode 100644 index 00000000000000..c55c712f6c7a5f --- /dev/null +++ b/homeassistant/components/honeywell_string_lights/const.py @@ -0,0 +1,9 @@ +"""Constants for the Honeywell String Lights integration.""" + +from __future__ import annotations + +from typing import Final + +DOMAIN: Final = "honeywell_string_lights" + +CONF_TRANSMITTER: Final = "transmitter" diff --git a/homeassistant/components/honeywell_string_lights/entity.py b/homeassistant/components/honeywell_string_lights/entity.py new file mode 100644 index 00000000000000..76363e1efa4278 --- /dev/null +++ b/homeassistant/components/honeywell_string_lights/entity.py @@ -0,0 +1,76 @@ +"""Common entity for Honeywell String Lights integration.""" + +from __future__ import annotations + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import Event, EventStateChangedData, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_state_change_event + +from .const import CONF_TRANSMITTER, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class HoneywellStringLightsEntity(Entity): + """Honeywell String Lights base entity.""" + + _attr_has_entity_name = True + + def __init__(self, entry: ConfigEntry) -> None: + """Initialize the entity.""" + self._transmitter = entry.data[CONF_TRANSMITTER] + self._attr_unique_id = entry.entry_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, + manufacturer="Honeywell", + model="String Lights", + ) + + async def async_added_to_hass(self) -> None: + """Subscribe to transmitter entity state changes.""" + await super().async_added_to_hass() + + transmitter_entity_id = er.async_validate_entity_id( + er.async_get(self.hass), self._transmitter + ) + + @callback + def _async_transmitter_state_changed( + event: Event[EventStateChangedData], + ) -> None: + """Handle transmitter entity state changes.""" + new_state = event.data["new_state"] + transmitter_available = ( + new_state is not None and new_state.state != STATE_UNAVAILABLE + ) + if transmitter_available != self.available: + _LOGGER.info( + "Transmitter %s used by %s is %s", + transmitter_entity_id, + self.entity_id, + "available" if transmitter_available else "unavailable", + ) + + self._attr_available = transmitter_available + self.async_write_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, + [transmitter_entity_id], + _async_transmitter_state_changed, + ) + ) + + # Set initial availability based on current transmitter entity state + transmitter_state = self.hass.states.get(transmitter_entity_id) + self._attr_available = ( + transmitter_state is not None + and transmitter_state.state != STATE_UNAVAILABLE + ) diff --git a/homeassistant/components/honeywell_string_lights/light.py b/homeassistant/components/honeywell_string_lights/light.py new file mode 100644 index 00000000000000..d430e1f90e8c67 --- /dev/null +++ b/homeassistant/components/honeywell_string_lights/light.py @@ -0,0 +1,65 @@ +"""Light platform for Honeywell String Lights.""" + +from __future__ import annotations + +from typing import Any + +from rf_protocols import get_codes + +from homeassistant.components.light import ColorMode, LightEntity +from homeassistant.components.radio_frequency import async_send_command +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity + +from .entity import HoneywellStringLightsEntity + +PARALLEL_UPDATES = 1 + +COMMANDS = get_codes("honeywell/string_lights") + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Honeywell String Lights light platform.""" + async_add_entities([HoneywellStringLight(config_entry)]) + + +class HoneywellStringLight(HoneywellStringLightsEntity, LightEntity, RestoreEntity): + """Representation of a Honeywell String Lights set controlled via RF.""" + + _attr_assumed_state = True + _attr_color_mode = ColorMode.ONOFF + _attr_supported_color_modes = {ColorMode.ONOFF} + _attr_name = None + _attr_should_poll = False + + async def async_added_to_hass(self) -> None: + """Restore last known state.""" + await super().async_added_to_hass() + if (last_state := await self.async_get_last_state()) is not None: + self._attr_is_on = last_state.state == STATE_ON + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + await self._async_send_command("turn_on") + self._attr_is_on = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + await self._async_send_command("turn_off") + self._attr_is_on = False + self.async_write_ha_state() + + async def _async_send_command(self, name: str) -> None: + """Load the named command and send it via the configured transmitter.""" + command = await COMMANDS.async_load_command(name) + await async_send_command( + self.hass, self._transmitter, command, context=self._context + ) diff --git a/homeassistant/components/honeywell_string_lights/manifest.json b/homeassistant/components/honeywell_string_lights/manifest.json new file mode 100644 index 00000000000000..9924b711414631 --- /dev/null +++ b/homeassistant/components/honeywell_string_lights/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "honeywell_string_lights", + "name": "Honeywell String Lights", + "codeowners": ["@balloob"], + "config_flow": true, + "dependencies": ["radio_frequency"], + "documentation": "https://www.home-assistant.io/integrations/honeywell_string_lights", + "integration_type": "device", + "iot_class": "assumed_state", + "quality_scale": "bronze", + "requirements": ["rf-protocols==2.1.0"] +} diff --git a/homeassistant/components/honeywell_string_lights/quality_scale.yaml b/homeassistant/components/honeywell_string_lights/quality_scale.yaml new file mode 100644 index 00000000000000..54bcb3f12c1a0a --- /dev/null +++ b/homeassistant/components/honeywell_string_lights/quality_scale.yaml @@ -0,0 +1,124 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: | + This integration does not register custom service actions. + appropriate-polling: + status: exempt + comment: | + This integration transmits RF commands and does not poll. + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: | + This integration does not register custom service actions. + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: done + entity-unique-id: done + has-entity-name: done + runtime-data: + status: exempt + comment: | + This integration does not use runtime data. + test-before-configure: + status: exempt + comment: | + RF transmission is a one-way broadcast with no device to contact. + test-before-setup: + status: exempt + comment: | + RF transmission is a one-way broadcast with no device to contact. + unique-config-entry: done + # Silver + action-exceptions: done + config-entry-unloading: done + docs-configuration-parameters: + status: exempt + comment: | + This integration has no options. + docs-installation-parameters: todo + entity-unavailable: + status: exempt + comment: | + RF transmission is a one-way broadcast; the light uses assumed state. + integration-owner: done + log-when-unavailable: + status: exempt + comment: | + RF transmission is a one-way broadcast; the light uses assumed state. + parallel-updates: done + reauthentication-flow: + status: exempt + comment: | + This integration does not authenticate. + test-coverage: todo + # Gold + devices: done + diagnostics: todo + discovery-update-info: + status: exempt + comment: | + This integration does not support discovery. + discovery: + status: exempt + comment: | + RF devices cannot be discovered. + docs-data-update: + status: exempt + comment: | + RF transmission is one-way; there is no data update. + docs-examples: todo + docs-known-limitations: todo + docs-supported-devices: todo + docs-supported-functions: todo + docs-troubleshooting: todo + docs-use-cases: todo + dynamic-devices: + status: exempt + comment: | + Each config entry represents a single static device. + entity-category: + status: exempt + comment: | + The single entity represents the primary device function. + entity-device-class: + status: exempt + comment: | + Light entities do not have device classes. + entity-disabled-by-default: + status: exempt + comment: | + The single entity represents the primary device function. + entity-translations: + status: exempt + comment: | + The entity uses the device name. + exception-translations: todo + icon-translations: + status: exempt + comment: | + Light uses the default icon for its state. + reconfiguration-flow: todo + repair-issues: + status: exempt + comment: | + No known repairable issues. + stale-devices: + status: exempt + comment: | + Each config entry represents a single static device. + + # Platinum + async-dependency: done + inject-websession: + status: exempt + comment: | + This integration does not use a web session. + strict-typing: todo diff --git a/homeassistant/components/honeywell_string_lights/strings.json b/homeassistant/components/honeywell_string_lights/strings.json new file mode 100644 index 00000000000000..a5c995ace08703 --- /dev/null +++ b/homeassistant/components/honeywell_string_lights/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_compatible_transmitters": "No radio frequency transmitter supports 433.92 MHz OOK transmissions. Please add a compatible transmitter first.", + "no_transmitters": "No radio frequency transmitters are available. Please set up a transmitter first." + }, + "step": { + "user": { + "data": { + "transmitter": "Radio frequency transmitter" + }, + "data_description": { + "transmitter": "The radio frequency transmitter used to control the Honeywell String Lights." + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 810338d54878db..fbd8bed90f3e3b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -311,6 +311,7 @@ "homewizard", "homeworks", "honeywell", + "honeywell_string_lights", "hr_energy_qube", "html5", "huawei_lte", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 550fe74d22ae8c..f7466b5891b286 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2975,6 +2975,12 @@ "config_flow": true, "iot_class": "cloud_polling", "name": "Honeywell Total Connect Comfort (US)" + }, + "honeywell_string_lights": { + "integration_type": "device", + "config_flow": true, + "iot_class": "assumed_state", + "name": "Honeywell String Lights" } } }, diff --git a/requirements_all.txt b/requirements_all.txt index b89fb6f4be7c14..d25547077ef5ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2840,6 +2840,7 @@ renson-endura-delta==1.7.2 # homeassistant.components.reolink reolink-aio==0.19.1 +# homeassistant.components.honeywell_string_lights # homeassistant.components.radio_frequency rf-protocols==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bf6b20f230632..17c1d0d087cf6d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2424,6 +2424,7 @@ renson-endura-delta==1.7.2 # homeassistant.components.reolink reolink-aio==0.19.1 +# homeassistant.components.honeywell_string_lights # homeassistant.components.radio_frequency rf-protocols==2.1.0 diff --git a/tests/components/honeywell_string_lights/__init__.py b/tests/components/honeywell_string_lights/__init__.py new file mode 100644 index 00000000000000..948d9ef3ec3e02 --- /dev/null +++ b/tests/components/honeywell_string_lights/__init__.py @@ -0,0 +1 @@ +"""Tests for the Honeywell String Lights integration.""" diff --git a/tests/components/honeywell_string_lights/conftest.py b/tests/components/honeywell_string_lights/conftest.py new file mode 100644 index 00000000000000..e164c7f4a0cde1 --- /dev/null +++ b/tests/components/honeywell_string_lights/conftest.py @@ -0,0 +1,48 @@ +"""Common fixtures for the Honeywell String Lights tests.""" + +from __future__ import annotations + +import pytest + +from homeassistant.components.honeywell_string_lights.const import ( + CONF_TRANSMITTER, + DOMAIN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry +from tests.components.radio_frequency.conftest import ( + MockRadioFrequencyEntity, + init_integration, # noqa: F401 + mock_rf_entity, # noqa: F401 +) + +TRANSMITTER_ENTITY_ID = "radio_frequency.test_rf_transmitter" + + +@pytest.fixture +def mock_config_entry( + hass: HomeAssistant, + mock_rf_entity: MockRadioFrequencyEntity, # noqa: F811 +) -> MockConfigEntry: + """Return a mock config entry for Honeywell String Lights.""" + entity_registry = er.async_get(hass) + entity_entry = entity_registry.async_get(TRANSMITTER_ENTITY_ID) + return MockConfigEntry( + domain=DOMAIN, + title="Honeywell String Lights", + data={CONF_TRANSMITTER: entity_entry.id}, + unique_id=entity_entry.id, + ) + + +@pytest.fixture +async def init_string_lights( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> MockConfigEntry: + """Set up the Honeywell String Lights integration.""" + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + return mock_config_entry diff --git a/tests/components/honeywell_string_lights/test_config_flow.py b/tests/components/honeywell_string_lights/test_config_flow.py new file mode 100644 index 00000000000000..3826e2b50b748c --- /dev/null +++ b/tests/components/honeywell_string_lights/test_config_flow.py @@ -0,0 +1,90 @@ +"""Test the Honeywell String Lights config flow.""" + +from __future__ import annotations + +from homeassistant.components.honeywell_string_lights.const import ( + CONF_TRANSMITTER, + DOMAIN, +) +from homeassistant.components.radio_frequency import DATA_COMPONENT, DOMAIN as RF_DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from .conftest import TRANSMITTER_ENTITY_ID + +from tests.common import MockConfigEntry +from tests.components.radio_frequency.conftest import MockRadioFrequencyEntity + + +async def test_user_flow( + hass: HomeAssistant, + mock_rf_entity: MockRadioFrequencyEntity, + entity_registry: er.EntityRegistry, +) -> None: + """Test the user config flow creates an entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_TRANSMITTER: TRANSMITTER_ENTITY_ID}, + ) + + entity_entry = entity_registry.async_get(TRANSMITTER_ENTITY_ID) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Honeywell String Lights" + assert result["data"] == {CONF_TRANSMITTER: entity_entry.id} + assert result["result"].unique_id == entity_entry.id + + +async def test_unique_id_already_configured( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test aborting when the same transmitter is already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_TRANSMITTER: TRANSMITTER_ENTITY_ID}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_no_transmitters(hass: HomeAssistant) -> None: + """Test the flow aborts when no RF transmitters are registered at all.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_transmitters" + + +async def test_no_compatible_transmitters(hass: HomeAssistant) -> None: + """Test aborting when transmitters exist but none support 433.92 MHz OOK.""" + assert await async_setup_component(hass, RF_DOMAIN, {}) + await hass.async_block_till_done() + incompatible = MockRadioFrequencyEntity( + "incompatible", frequency_ranges=[(868_000_000, 869_000_000)] + ) + await hass.data[DATA_COMPONENT].async_add_entities([incompatible]) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_compatible_transmitters" diff --git a/tests/components/honeywell_string_lights/test_light.py b/tests/components/honeywell_string_lights/test_light.py new file mode 100644 index 00000000000000..f2955f2db2e5d3 --- /dev/null +++ b/tests/components/honeywell_string_lights/test_light.py @@ -0,0 +1,102 @@ +"""Tests for the Honeywell String Lights light platform.""" + +from __future__ import annotations + +from homeassistant.components.honeywell_string_lights.light import COMMANDS +from homeassistant.components.light import ( + DOMAIN as LIGHT_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import Context, HomeAssistant, State + +from tests.common import MockConfigEntry, mock_restore_cache +from tests.components.radio_frequency.conftest import MockRadioFrequencyEntity + +ENTITY_ID = "light.honeywell_string_lights" + + +async def test_turn_on_off_sends_commands( + hass: HomeAssistant, + mock_rf_entity: MockRadioFrequencyEntity, + init_string_lights: MockConfigEntry, +) -> None: + """Test turning the light on and off sends the correct RF commands.""" + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_ASSUMED_STATE] is True + + context = Context() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + context=context, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + assert state.context is context + assert len(mock_rf_entity.send_command_calls) == 1 + command = mock_rf_entity.send_command_calls[0] + assert command.command is COMMANDS.load_command("turn_on") + assert command.context is context + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + context=context, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_OFF + assert state.context is context + assert len(mock_rf_entity.send_command_calls) == 2 + command = mock_rf_entity.send_command_calls[1] + assert command.command is COMMANDS.load_command("turn_off") + assert command.context is context + + +async def test_restore_state( + hass: HomeAssistant, + mock_rf_entity: MockRadioFrequencyEntity, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the light restores its previous on state.""" + mock_restore_cache(hass, [State(ENTITY_ID, STATE_ON)]) + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + + +async def test_unload_entry( + hass: HomeAssistant, init_string_lights: MockConfigEntry +) -> None: + """Test unloading the config entry removes the entity.""" + assert hass.states.get(ENTITY_ID) is not None + + assert await hass.config_entries.async_unload(init_string_lights.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/radio_frequency/conftest.py b/tests/components/radio_frequency/conftest.py index 69538e3e18f580..e4e651204e6bde 100644 --- a/tests/components/radio_frequency/conftest.py +++ b/tests/components/radio_frequency/conftest.py @@ -1,6 +1,6 @@ """Common fixtures for the Radio Frequency tests.""" -from typing import override +from typing import NamedTuple, override import pytest from rf_protocols import ModulationType, RadioFrequencyCommand @@ -21,6 +21,13 @@ async def init_integration(hass: HomeAssistant) -> None: await hass.async_block_till_done() +class MockCommand(NamedTuple): + """Data structure to store calls to async_send_command.""" + + command: RadioFrequencyCommand + context: object | None + + class MockRadioFrequencyCommand(RadioFrequencyCommand): """Mock RF command for testing.""" @@ -60,7 +67,7 @@ def __init__( if frequency_ranges is None else frequency_ranges ) - self.send_command_calls: list[RadioFrequencyCommand] = [] + self.send_command_calls: list[MockCommand] = [] @property def supported_frequency_ranges(self) -> list[tuple[int, int]]: @@ -69,7 +76,9 @@ def supported_frequency_ranges(self) -> list[tuple[int, int]]: async def async_send_command(self, command: RadioFrequencyCommand) -> None: """Mock send command.""" - self.send_command_calls.append(command) + self.send_command_calls.append( + MockCommand(command=command, context=self._context) + ) @pytest.fixture diff --git a/tests/components/radio_frequency/test_init.py b/tests/components/radio_frequency/test_init.py index 35e9129f4549f7..f8c42c198a45c6 100644 --- a/tests/components/radio_frequency/test_init.py +++ b/tests/components/radio_frequency/test_init.py @@ -80,7 +80,7 @@ async def test_async_send_command_success( await async_send_command(hass, ENTITY_ID, command) assert len(mock_rf_entity.send_command_calls) == 1 - assert mock_rf_entity.send_command_calls[0] is command + assert mock_rf_entity.send_command_calls[0].command is command state = hass.states.get(ENTITY_ID) assert state is not None