Compare commits

..

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] e08ba030a0 Merge branch 'dev' into radio-frequency-consumer-entity
Co-authored-by: piitaya <5878303+piitaya@users.noreply.github.com>
2026-05-21 13:35:41 +00:00
Paulus Schoutsen ba9504a70d Migrate honeywell_string_lights and novy_cooker_hood to use RadioFrequencyTransmitterConsumerEntity
Replace hand-rolled availability tracking in both integrations with
the new RadioFrequencyTransmitterConsumerEntity base class. The base
entity classes now only provide device info.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-17 16:27:32 -04:00
Paulus Schoutsen 5c5d9828f8 Add helper consumer entity for radio_frequency
Move DATA_COMPONENT to const.py, move async_send_command to a new
helpers.py, and add RadioFrequencyTransmitterConsumerEntity base class
that tracks the availability of the underlying RF transmitter entity.

Mirrors the refactoring done for the infrared integration in #170854.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-17 16:23:55 -04:00
12 changed files with 241 additions and 346 deletions
@@ -1,18 +1,11 @@
"""Provide functionality to keep track of devices."""
import asyncio
from typing import Any
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401
from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from .config_entry import ( # noqa: F401
DATA_COMPONENT,
BaseScannerEntity,
BaseTrackerEntity,
ScannerEntity,
ScannerEntityDescription,
TrackerEntity,
@@ -40,8 +33,6 @@ from .const import ( # noqa: F401
DEFAULT_TRACK_NEW,
DOMAIN,
ENTITY_ID_FORMAT,
LOGGER,
PLATFORM_TYPE_LEGACY,
SCAN_INTERVAL,
SourceType,
)
@@ -53,9 +44,7 @@ from .legacy import ( # noqa: F401
SOURCE_TYPES,
AsyncSeeCallback,
DeviceScanner,
DeviceTracker,
SeeCallback,
async_create_platform_type,
async_setup_integration as async_setup_legacy_integration,
see,
)
@@ -68,43 +57,5 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the device tracker."""
component = hass.data[DATA_COMPONENT] = EntityComponent[BaseTrackerEntity](
LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
component.config = {}
component.register_shutdown()
# The tracker is loaded in the async_setup_legacy_integration task so
# we create a future to avoid waiting on it here so that only
# async_platform_discovered will have to wait in the rare event
# a custom component still uses the legacy device tracker discovery.
tracker_future: asyncio.Future[DeviceTracker] = hass.loop.create_future()
async def async_platform_discovered(
p_type: str, info: dict[str, Any] | None
) -> None:
"""Load a platform."""
platform = await async_create_platform_type(hass, config, p_type, {})
if platform is None:
return
if platform.type != PLATFORM_TYPE_LEGACY:
await component.async_setup_platform(p_type, {}, info)
return
tracker = await tracker_future
await platform.async_setup_legacy(hass, tracker, info)
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
#
# Legacy and platforms load in a non-awaited tracked task
# to ensure device tracker setup can continue and config
# entry integrations are not waiting for legacy device
# tracker platforms to be set up.
#
hass.async_create_task(
async_setup_legacy_integration(hass, config, tracker_future),
eager_start=True,
)
async_setup_legacy_integration(hass, config)
return True
@@ -37,7 +37,11 @@ from homeassistant.const import (
)
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers import (
config_validation as cv,
discovery,
entity_registry as er,
)
from homeassistant.helpers.event import (
async_track_time_interval,
async_track_utc_time_change,
@@ -200,7 +204,40 @@ def see(
hass.services.call(DOMAIN, SERVICE_SEE, data)
async def async_setup_integration(
@callback
def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> None:
"""Set up the legacy integration."""
# The tracker is loaded in the _async_setup_integration task so
# we create a future to avoid waiting on it here so that only
# async_platform_discovered will have to wait in the rare event
# a custom component still uses the legacy device tracker discovery.
tracker_future: asyncio.Future[DeviceTracker] = hass.loop.create_future()
async def async_platform_discovered(
p_type: str, info: dict[str, Any] | None
) -> None:
"""Load a platform."""
platform = await async_create_platform_type(hass, config, p_type, {})
if platform is None or platform.type != PLATFORM_TYPE_LEGACY:
return
tracker = await tracker_future
await platform.async_setup_legacy(hass, tracker, info)
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
#
# Legacy and platforms load in a non-awaited tracked task
# to ensure device tracker setup can continue and config
# entry integrations are not waiting for legacy device
# tracker platforms to be set up.
#
hass.async_create_task(
_async_setup_integration(hass, config, tracker_future), eager_start=True
)
async def _async_setup_integration(
hass: HomeAssistant,
config: ConfigType,
tracker_future: asyncio.Future[DeviceTracker],
@@ -1,18 +1,10 @@
"""Common entity for Honeywell String Lights integration."""
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__)
from .const import DOMAIN
class HoneywellStringLightsEntity(Entity):
@@ -22,53 +14,9 @@ class HoneywellStringLightsEntity(Entity):
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
)
@@ -5,13 +5,16 @@ from typing import Any
from rf_protocols.codes.honeywell.string_lights import CODES
from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.components.radio_frequency import async_send_command
from homeassistant.components.radio_frequency import (
RadioFrequencyTransmitterConsumerEntity,
)
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 .const import CONF_TRANSMITTER
from .entity import HoneywellStringLightsEntity
PARALLEL_UPDATES = 1
@@ -26,14 +29,23 @@ async def async_setup_entry(
async_add_entities([HoneywellStringLight(config_entry)])
class HoneywellStringLight(HoneywellStringLightsEntity, LightEntity, RestoreEntity):
class HoneywellStringLight(
HoneywellStringLightsEntity,
RadioFrequencyTransmitterConsumerEntity,
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
def __init__(self, entry: ConfigEntry) -> None:
"""Initialize the entity."""
super().__init__(entry)
self._rf_transmitter_entity_id = entry.data[CONF_TRANSMITTER]
async def async_added_to_hass(self) -> None:
"""Restore last known state."""
@@ -43,19 +55,17 @@ class HoneywellStringLight(HoneywellStringLightsEntity, LightEntity, RestoreEnti
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
await self._async_send_command("turn_on")
await self._async_send_rf_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")
await self._async_send_rf_command("turn_off")
self._attr_is_on = False
self.async_write_ha_state()
async def _async_send_command(self, name: str) -> None:
async def _async_send_rf_command(self, name: str) -> None:
"""Load the named command and send it via the configured transmitter."""
command = await CODES.async_load_command(name)
await async_send_command(
self.hass, self._transmitter, command, context=self._context
)
await self._send_command(command)
@@ -1,18 +1,10 @@
"""Common entity for the Novy Cooker Hood integration."""
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__)
from .const import DOMAIN
class NovyCookerHoodEntity(Entity):
@@ -20,55 +12,11 @@ class NovyCookerHoodEntity(Entity):
_attr_assumed_state = True
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(self, entry: ConfigEntry) -> None:
"""Initialize the entity."""
self._transmitter = entry.data[CONF_TRANSMITTER]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
manufacturer="Novy",
model="Cooker Hood",
)
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,
)
)
transmitter_state = self.hass.states.get(transmitter_entity_id)
self._attr_available = (
transmitter_state is not None
and transmitter_state.state != STATE_UNAVAILABLE
)
@@ -6,7 +6,9 @@ from typing import Any
from rf_protocols.codes.novy.cooker_hood import get_codes_for_code
from homeassistant.components.fan import ATTR_PERCENTAGE, FanEntity, FanEntityFeature
from homeassistant.components.radio_frequency import async_send_command
from homeassistant.components.radio_frequency import (
RadioFrequencyTransmitterConsumerEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CODE
from homeassistant.core import HomeAssistant
@@ -18,7 +20,7 @@ from homeassistant.util.percentage import (
)
from .commands import COMMAND_MINUS, COMMAND_PLUS
from .const import SPEED_COUNT
from .const import CONF_TRANSMITTER, SPEED_COUNT
from .entity import NovyCookerHoodEntity
PARALLEL_UPDATES = 1
@@ -35,7 +37,12 @@ async def async_setup_entry(
async_add_entities([NovyCookerHoodFan(config_entry)])
class NovyCookerHoodFan(NovyCookerHoodEntity, FanEntity, RestoreEntity):
class NovyCookerHoodFan(
NovyCookerHoodEntity,
RadioFrequencyTransmitterConsumerEntity,
FanEntity,
RestoreEntity,
):
"""Calibration-based fan: each change resets to off then climbs to target."""
_attr_name = None
@@ -49,6 +56,7 @@ class NovyCookerHoodFan(NovyCookerHoodEntity, FanEntity, RestoreEntity):
def __init__(self, entry: ConfigEntry) -> None:
"""Initialize the fan."""
super().__init__(entry)
self._rf_transmitter_entity_id = entry.data[CONF_TRANSMITTER]
self._codes = get_codes_for_code(entry.data[CONF_CODE])
self._level = 0
self._attr_unique_id = entry.entry_id
@@ -105,7 +113,7 @@ class NovyCookerHoodFan(NovyCookerHoodEntity, FanEntity, RestoreEntity):
steps = self._steps_from_percentage(percentage_step)
plus = await self._codes.async_load_command(COMMAND_PLUS)
for _ in range(steps):
await self._async_send(plus)
await self._send_command(plus)
self._level = min(SPEED_COUNT, self._level + steps)
self.async_write_ha_state()
@@ -114,7 +122,7 @@ class NovyCookerHoodFan(NovyCookerHoodEntity, FanEntity, RestoreEntity):
steps = self._steps_from_percentage(percentage_step)
minus = await self._codes.async_load_command(COMMAND_MINUS)
for _ in range(steps):
await self._async_send(minus)
await self._send_command(minus)
self._level = max(0, self._level - steps)
self.async_write_ha_state()
@@ -129,16 +137,10 @@ class NovyCookerHoodFan(NovyCookerHoodEntity, FanEntity, RestoreEntity):
"""Reset to off with `SPEED_COUNT` minus presses, then climb to level."""
minus = await self._codes.async_load_command(COMMAND_MINUS)
for _ in range(SPEED_COUNT):
await self._async_send(minus)
await self._send_command(minus)
if level > 0:
plus = await self._codes.async_load_command(COMMAND_PLUS)
for _ in range(level):
await self._async_send(plus)
await self._send_command(plus)
self._level = level
self.async_write_ha_state()
async def _async_send(self, command: Any) -> None:
"""Send a single RF command via the configured transmitter."""
await async_send_command(
self.hass, self._transmitter, command, context=self._context
)
@@ -5,7 +5,9 @@ from typing import Any
from rf_protocols.codes.novy.cooker_hood import get_codes_for_code
from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.components.radio_frequency import async_send_command
from homeassistant.components.radio_frequency import (
RadioFrequencyTransmitterConsumerEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CODE, STATE_ON
from homeassistant.core import HomeAssistant
@@ -13,6 +15,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from .commands import COMMAND_LIGHT
from .const import CONF_TRANSMITTER
from .entity import NovyCookerHoodEntity
PARALLEL_UPDATES = 1
@@ -27,7 +30,12 @@ async def async_setup_entry(
async_add_entities([NovyCookerHoodLight(config_entry)])
class NovyCookerHoodLight(NovyCookerHoodEntity, LightEntity, RestoreEntity):
class NovyCookerHoodLight(
NovyCookerHoodEntity,
RadioFrequencyTransmitterConsumerEntity,
LightEntity,
RestoreEntity,
):
"""Novy cooker hood light toggled via a single RF press."""
_attr_color_mode = ColorMode.ONOFF
@@ -37,6 +45,7 @@ class NovyCookerHoodLight(NovyCookerHoodEntity, LightEntity, RestoreEntity):
def __init__(self, entry: ConfigEntry) -> None:
"""Initialize the light."""
super().__init__(entry)
self._rf_transmitter_entity_id = entry.data[CONF_TRANSMITTER]
self._codes = get_codes_for_code(entry.data[CONF_CODE])
self._attr_unique_id = entry.entry_id
@@ -48,19 +57,17 @@ class NovyCookerHoodLight(NovyCookerHoodEntity, LightEntity, RestoreEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on by sending the toggle command."""
await self._async_send_command(COMMAND_LIGHT)
await self._async_send_rf_command(COMMAND_LIGHT)
self._attr_is_on = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off by sending the toggle command."""
await self._async_send_command(COMMAND_LIGHT)
await self._async_send_rf_command(COMMAND_LIGHT)
self._attr_is_on = False
self.async_write_ha_state()
async def _async_send_command(self, name: str) -> None:
async def _async_send_rf_command(self, name: str) -> None:
"""Load the named command and send it via the configured transmitter."""
command = await self._codes.async_load_command(name)
await async_send_command(
self.hass, self._transmitter, command, context=self._context
)
await self._send_command(command)
@@ -6,22 +6,24 @@ import logging
from rf_protocols import ModulationType, RadioFrequencyCommand
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
from .const import DOMAIN
from .const import DATA_COMPONENT, DOMAIN
from .entity import (
RadioFrequencyTransmitterEntity,
RadioFrequencyTransmitterEntityDescription,
)
from .helpers import RadioFrequencyTransmitterConsumerEntity, async_send_command
__all__ = [
"DOMAIN",
"ModulationType",
"RadioFrequencyCommand",
"RadioFrequencyTransmitterConsumerEntity",
"RadioFrequencyTransmitterEntity",
"RadioFrequencyTransmitterEntityDescription",
"async_get_transmitters",
@@ -30,9 +32,6 @@ __all__ = [
_LOGGER = logging.getLogger(__name__)
DATA_COMPONENT: HassKey[EntityComponent[RadioFrequencyTransmitterEntity]] = HassKey(
DOMAIN
)
ENTITY_ID_FORMAT = DOMAIN + ".{}"
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
@@ -95,60 +94,3 @@ def async_get_transmitters(
if entity.supports_modulation(modulation)
and entity.supports_frequency(frequency)
]
async def async_send_command(
hass: HomeAssistant,
entity_id_or_uuid: str,
command: RadioFrequencyCommand,
context: Context | None = None,
) -> None:
"""Send an RF command to the specified radio_frequency entity.
Raises:
vol.Invalid: If `entity_id_or_uuid` is not a valid entity ID or known entity
registry UUID.
HomeAssistantError: If the radio_frequency component is not loaded or the
resolved entity is not found.
"""
component = hass.data.get(DATA_COMPONENT)
if component is None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="component_not_loaded",
)
ent_reg = er.async_get(hass)
entity_id = er.async_validate_entity_id(ent_reg, entity_id_or_uuid)
entity = component.get_entity(entity_id)
if entity is None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="entity_not_found",
translation_placeholders={"entity_id": entity_id},
)
if not entity.supports_frequency(command.frequency):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="unsupported_frequency",
translation_placeholders={
"entity_id": entity_id,
"frequency": str(command.frequency),
},
)
if not entity.supports_modulation(command.modulation):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="unsupported_modulation",
translation_placeholders={
"entity_id": entity_id,
"modulation": command.modulation,
},
)
if context is not None:
entity.async_set_context(context)
await entity.async_send_command_internal(command)
@@ -2,4 +2,12 @@
from typing import Final
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.util.hass_dict import HassKey
from .entity import RadioFrequencyTransmitterEntity
DOMAIN: Final = "radio_frequency"
DATA_COMPONENT: HassKey[EntityComponent[RadioFrequencyTransmitterEntity]] = HassKey(
DOMAIN
)
@@ -0,0 +1,134 @@
"""Helper base entities for integrations that consume RF transmitters."""
import logging
from rf_protocols import RadioFrequencyCommand
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import (
Context,
Event,
EventStateChangedData,
HomeAssistant,
callback,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_state_change_event
from .const import DATA_COMPONENT, DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_send_command(
hass: HomeAssistant,
entity_id_or_uuid: str,
command: RadioFrequencyCommand,
context: Context | None = None,
) -> None:
"""Send an RF command to the specified radio_frequency entity.
Raises:
vol.Invalid: If `entity_id_or_uuid` is not a valid entity ID or known entity
registry UUID.
HomeAssistantError: If the radio_frequency component is not loaded or the
resolved entity is not found.
"""
component = hass.data.get(DATA_COMPONENT)
if component is None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="component_not_loaded",
)
ent_reg = er.async_get(hass)
entity_id = er.async_validate_entity_id(ent_reg, entity_id_or_uuid)
entity = component.get_entity(entity_id)
if entity is None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="entity_not_found",
translation_placeholders={"entity_id": entity_id},
)
if not entity.supports_frequency(command.frequency):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="unsupported_frequency",
translation_placeholders={
"entity_id": entity_id,
"frequency": str(command.frequency),
},
)
if not entity.supports_modulation(command.modulation):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="unsupported_modulation",
translation_placeholders={
"entity_id": entity_id,
"modulation": command.modulation,
},
)
if context is not None:
entity.async_set_context(context)
await entity.async_send_command_internal(command)
class RadioFrequencyTransmitterConsumerEntity(Entity):
"""Base entity for integrations that send commands via an RF transmitter.
Tracks the availability of the underlying RF transmitter entity.
"""
_attr_should_poll = False
_rf_transmitter_entity_id: str
async def async_added_to_hass(self) -> None:
"""Subscribe to RF entity state changes."""
await super().async_added_to_hass()
# Resolve UUID to entity ID if needed
self._rf_transmitter_entity_id = er.async_validate_entity_id(
er.async_get(self.hass), self._rf_transmitter_entity_id
)
self.async_on_remove(
async_track_state_change_event(
self.hass,
[self._rf_transmitter_entity_id],
self._async_rf_state_changed,
)
)
# Set initial availability based on current RF entity state
rf_state = self.hass.states.get(self._rf_transmitter_entity_id)
self._attr_available = (
rf_state is not None and rf_state.state != STATE_UNAVAILABLE
)
async def _send_command(self, command: RadioFrequencyCommand) -> None:
"""Send an RF command through the RF transmitter entity."""
await async_send_command(
self.hass, self._rf_transmitter_entity_id, command, context=self._context
)
@callback
def _async_rf_state_changed(self, event: Event[EventStateChangedData]) -> None:
"""Handle RF entity state changes."""
new_state = event.data["new_state"]
rf_available = new_state is not None and new_state.state != STATE_UNAVAILABLE
if rf_available != self.available:
_LOGGER.info(
"Radio frequency entity %s used by %s is %s",
self._rf_transmitter_entity_id,
self.entity_id,
"available" if rf_available else "unavailable",
)
self._attr_available = rf_available
self.async_write_ha_state()
@@ -38,12 +38,12 @@ async def async_setup_entry(
)
# pylint: disable-next=home-assistant-missing-has-entity-name
class OmadaClientScannerEntity(
CoordinatorEntity[OmadaClientsCoordinator], ScannerEntity
):
"""Entity for a client connected to the Omada network."""
_attr_has_entity_name = True
_client_details: OmadaWirelessClient | None = None
def __init__(
+1 -93
View File
@@ -8,12 +8,7 @@ from unittest.mock import call, patch
import pytest
from homeassistant.components import device_tracker, zone
from homeassistant.components.device_tracker import (
SourceType,
TrackerEntity,
const,
legacy,
)
from homeassistant.components.device_tracker import SourceType, const, legacy
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
ATTR_FRIENDLY_NAME,
@@ -24,15 +19,11 @@ from homeassistant.const import (
CONF_PLATFORM,
STATE_HOME,
STATE_NOT_HOME,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import discovery
from homeassistant.helpers.discovery import DiscoveryInfoType
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
@@ -40,13 +31,9 @@ from . import common
from .common import MockScanner, mock_legacy_device_tracker_setup
from tests.common import (
MockModule,
MockPlatform,
RegistryEntryWithDefaults,
assert_setup_component,
async_fire_time_changed,
mock_integration,
mock_platform,
mock_registry,
mock_restore_cache,
patch_yaml_files,
@@ -742,82 +729,3 @@ def test_see_schema_allowing_ios_calls() -> None:
"hostname": "beer",
}
)
async def test_modern_platform_setup(hass: HomeAssistant) -> None:
"""Test modern platform setup."""
test_domain = "test"
entity1 = TrackerEntity()
entity1.entity_id = "device_tracker.test1"
entity1._attr_source_type = SourceType.ROUTER
entity2 = TrackerEntity()
entity2.entity_id = "device_tracker.test2"
entity2._attr_location_name = "home"
entity2._attr_location_accuracy = 1
entity2._attr_latitude = 10.0
entity2._attr_longitude = 5.0
entity2._attr_source_type = SourceType.GPS
entity3 = TrackerEntity()
entity3.entity_id = "device_tracker.test3"
entity3._attr_location_name = "not_home"
entity3._attr_source_type = SourceType.ROUTER
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> bool:
async_add_entities([entity1, entity2, entity3])
return True
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.async_create_task(
discovery.async_load_platform(
hass, "device_tracker", test_domain, {}, config
)
)
return True
mock_integration(
hass,
MockModule(test_domain, async_setup=async_setup),
)
mock_platform(
hass,
f"{test_domain}.device_tracker",
MockPlatform(async_setup_platform=async_setup_platform),
)
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(hass, "device_tracker", {})
await async_setup_component(hass, test_domain, {})
await hass.async_block_till_done()
state = hass.states.get(entity1.entity_id)
assert state
assert state.state == STATE_UNKNOWN
assert state.attributes == {"in_zones": [], "source_type": SourceType.ROUTER}
state = hass.states.get(entity2.entity_id)
assert state
assert state.state == STATE_HOME
assert state.attributes == {
"in_zones": [],
"source_type": SourceType.GPS,
"latitude": 10.0,
"longitude": 5.0,
"gps_accuracy": 1,
}
state = hass.states.get(entity3.entity_id)
assert state
assert state.state == STATE_NOT_HOME
assert state.attributes == {
"in_zones": [],
"source_type": SourceType.ROUTER,
}