Radio frequency entity
Share RF transmitters between integrations, so one integration owns the radio and another one knows the codes of a device.
The radio frequency domain sits between RF hardware and the devices it controls. Plenty of cheap devices listen to simple radio codes on 433 MHz or 868 MHz: garage doors, wireless sockets, string lights, cooker hoods. One integration owns the radio, like an ESPHome board or a Broadlink hub. Another integration knows a device and the codes it listens to. The radio frequency domain lets them work together without knowing about each other.
There are two roles:
- Transmitter integrations own hardware that sends RF signals. They provide an entity based on
RadioFrequencyTransmitterEntity. - Consumer integrations know a device and its codes. They do not create radio frequency entities. They create regular entities, like a switch or a light, and send through a transmitter the user picked, usually in the config flow.
How the roles connect through the radio frequency domain:
flowchart TB
Consumers["<b>Consumer integrations</b><br/>KlikAanKlikUit<br/>Novy Cooker Hood<br/>Honeywell String Lights"]
subgraph rf["radio_frequency domain"]
Helpers["<b>Helpers and consumer base class</b><br/>async_get_transmitters, async_send_command,<br/>RadioFrequencyTransmitterConsumerEntity"]
Base["<b>Base class</b><br/>RadioFrequencyTransmitterEntity"]
end
Hardware["<b>Transmitter integrations</b><br/>ESPHome, Broadlink"]
Consumers -->|"use"| Helpers
Helpers -->|"send through"| Base
Hardware -->|"implement"| BaseEverything lives in homeassistant.components.radio_frequency. The commands come from the rf-protocols library, and the domain re-exports its RadioFrequencyCommand and ModulationType. Unlike the infrared entity, there is no receiver role: radio frequency entities only transmit.
These properties are specific to radio frequency entities. The generic properties every entity has are on the entity class.
| Name | Type | Default | Description |
|---|---|---|---|
supported_frequency_ranges |
list[tuple[int, int]] |
Required | The frequency ranges the hardware can transmit on, as (min_hz, max_hz) tuples. It is an abstract property: implement it as a property, not as an _attr_ attribute. |
The base class sets should_poll to False and manages the state. It extends RestoreEntity, so the time of the last command survives a restart.
Radio frequency entities have no supported features. What a transmitter can do is described by its frequency ranges and its modulation. Right now, the only modulation is ModulationType.OOK (on-off keying), and every transmitter is assumed to support it.
A transmitter implements two things: the frequency ranges, and sending a command.
async_send_command receives a RadioFrequencyCommand. Read what you need from it: frequency in Hz, modulation, repeat_count, the optional symbol_rate and output_power, and get_raw_timings(), which returns the signal as alternating pulses (positive) and spaces (negative) in microseconds. Raise HomeAssistantError when sending fails.
Do not call async_send_command on the entity from other code. Consumers go through the async_send_command helper, which checks the frequency and modulation, passes the context, and updates the state after a successful send.
Consumers never hold a reference to the transmitter entity. They work with an entity ID (or its registry ID) and these helpers:
| Helper | Description |
|---|---|
async_get_transmitters(hass, frequency, modulation) |
The entity IDs of all transmitters that support the frequency and modulation. An empty list means none of them fit. Raises HomeAssistantError when the domain is not loaded or there are no transmitters at all. |
async_send_command(hass, entity_id_or_uuid, command, context=None) |
Sends a command through a transmitter. Raises HomeAssistantError when the domain is not loaded, the transmitter does not exist, or it does not support the frequency or modulation of the command. |
The RadioFrequencyTransmitterConsumerEntity base class does the bookkeeping for you. Set _rf_transmitter_entity_id_or_uuid and call await self._send_command(command). The base class keeps your entity available only while the transmitter is, follows the transmitter when the user renames its entity ID, and passes on the context of the action. Mix it in before the entity class of your platform.
For config flows and the frontend, there is also a radio_frequency/list WebSocket command (admin only). It lists every transmitter with its device, config entry, frequency ranges, and supported modulations.
| Method | Description |
|---|---|
async_send_command(command) |
Required. Send the command over the air. There is no sync variant. |
supported_frequency_ranges |
Required property. Return the (min_hz, max_hz) ranges. |
supports_frequency and supports_modulation are final: the base class implements them from the frequency ranges and the modulation.
The state is the time the last command was sent, as an ISO 8601 timestamp with milliseconds, like 2026-10-10T08:15:00.123+00:00, or unknown before the first one. It is final: the base class manages it.
A transmitter for a 433 MHz radio on a network hub.
from homeassistant.components.radio_frequency import (
RadioFrequencyCommand,
RadioFrequencyTransmitterEntity,
)
from homeassistant.exceptions import HomeAssistantError
class MyRfTransmitter(RadioFrequencyTransmitterEntity):
"""The 433 MHz radio of a hub."""
_attr_has_entity_name = True
_attr_translation_key = "rf_transmitter"
def __init__(self, hub: MyHub) -> None:
"""Initialize the transmitter."""
self._hub = hub
self._attr_unique_id = f"{hub.serial}_rf"
@property
def supported_frequency_ranges(self) -> list[tuple[int, int]]:
"""Return the bands this radio can transmit on."""
return [(433_050_000, 434_790_000)]
async def async_send_command(
self, command: RadioFrequencyCommand
) -> None:
"""Send the command."""
try:
await self._hub.send_rf(
frequency=command.frequency,
timings=command.get_raw_timings(),
repeat=command.repeat_count,
)
except MyHubError as err:
raise HomeAssistantError(f"Sending failed: {err}") from errOn the consumer side, a switch for a wireless socket is a few lines on top of RadioFrequencyTransmitterConsumerEntity:
from typing import Any
from rf_protocols.commands.kaku import KakuCommand
from homeassistant.components.radio_frequency import (
RadioFrequencyTransmitterConsumerEntity,
)
from homeassistant.components.switch import SwitchEntity
class MySocket(RadioFrequencyTransmitterConsumerEntity, SwitchEntity):
"""A wireless socket that listens to KlikAanKlikUit codes."""
_attr_has_entity_name = True
_attr_name = None
_attr_assumed_state = True
def __init__(
self, entry_id: str, transmitter: str, code: int, channel: int
) -> None:
"""Initialize the socket."""
self._attr_unique_id = entry_id
self._rf_transmitter_entity_id_or_uuid = transmitter
self._code = code
self._channel = channel
async def async_turn_on(self, **kwargs: Any) -> None:
"""Send the on code."""
await self._send_command(
KakuCommand(
id=self._code, group=False, channel=self._channel, on=True
)
)
self._attr_is_on = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Send the off code."""
await self._send_command(
KakuCommand(
id=self._code, group=False, channel=self._channel, on=False
)
)
self._attr_is_on = False
self.async_write_ha_state()