I thought I'd post this here to save a future developer several hours/days of trying to make this work. The following examples will help you implement previews in data entry flows just like the template or group integrations.
Entity previews
Currently preview entities are only supported by the frontend for config entry flows, option flows, subentry config flows, and repair flows. To implement a flow preview in a data entry flow, implement these steps:
1. Create the websocket endpoint
The example is specific to ConfigFlow or ConfigSubentryFlow flows but it is easily adaptable to an OptionsFlow or RepairsFlow.
# In config_flow.py
from homeassistant.components import websocket_api
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
FlowType,
SubentryFlowResult,
)
from homeassistant.const import (
ATTR_ICON,
ATTR_NAME
)
@websocket_api.websocket_command(
{
vol.Required("type"): "preview_name/start_preview", # "preview_name" corresponds to the value in async_show_form(..., preview="preview_name")
vol.Required("flow_id"): str,
vol.Required("flow_type"): vol.Any(FlowType.CONFIG_FLOW, FlowType.CONFIG_SUBENTRIES_FLOW),
vol.Required("user_input"): dict,
}
)
@websocket_api.async_response
async def ws_start_preview(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Generate a preview.
The client sends a message calling this endpoint when the form is rendered and when any field changes.
"""
# the current user input on the form
user_input: dict[str, Any] = msg["user_input"]
# Get the partial flow result
if msg["flow_type"] == FlowType.CONFIG_SUBENTRIES_FLOW:
flow_status: SubentryFlowResult = hass.config_entries.subentries.async_get(msg["flow_id"])
# TODO: get the config entry (if applicable or needed)
# handler for subentry flow is a tuple of str of form
# (entry_id, subentry_flow_type)
entry_id, _ = flow_status["handler"]
config_entry: ConfigEntry = hass.config_entries.async_get_entry(entry_id)
# Process the data and validate for errors (tip: use the
# same schema/validation used in the data entry flow step)
errors: dict[str, str] | None = None
preview_value: str = ...
config: dict[str, str] = {
ATTR_ICON: "mdi:eye",
ATTR_NAME: "Entity Preview"
}
...
@callback
def async_preview_updated(
state: str | None,
attributes: Mapping[str, Any] | None,
error: str | None,
domain: str | None, # e.g. "sensor", "select", "datetime" etc.
) -> None:
"""Forward preview updates to websocket."""
# Errors sent here will appear in the preview element on the data
# entry form. Note that this isn't required and data input validation
# errors should be sent using connection.send_message below. For example:
# Send ValueErrors to the client raised by the entity in _async_calculate_state.
if error is not None:
connection.send_message(
websocket_api.event_message(msg["id"], {"error": error})
)
return
connection.send_message(
websocket_api.event_message(
msg["id"],
{
"attributes": attributes,
"domain": domain, # see note below
"state": state,
},
)
)
# Send user_input errors to the client.
if errors is not None:
connection.send_message(
{
"id": msg["id"],
"type": websocket_api.TYPE_RESULT,
"success": False,
"error": {"code": "invalid_user_input", "message": errors},
}
)
return
# Create the preview entity.
preview = PreviewSensorEntity(hass, preview_value=preview_value, config=config)
connection.send_result(msg["id"])
connection.subscriptions[msg["id"]] = preview.async_start_preview(
async_preview_updated
)
Tip
domainin async_preview_callback refers to the preview entity's domain (e.g."sensor"or"select"). Ifdomainis not provided orNonethe frontend will fallback to render aSensorEntitywith applicable attributes.
2. Create the preview entity
Ideally one would leverage an entity class already created for an integration but a simple entity can be created at runtime.
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
class PreviewSensorEntity(SensorEntity):
"""Preview sensor entity for subentry flows."""
def __init__(self, hass: HomeAssistant, preview_value: str, config: dict[str, str]) -> None:
self.hass = hass
self._attr_native_value = preview_value
self._attr_icon = config.get(ATTR_ICON)
self._attr_name = config.get(ATTR_NAME)
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
self._attr_state_class = config.get(CONF_STATE_CLASS)
@callback
def async_start_preview(
self,
preview_callback: Callable[
[
str | None, # state
Mapping[str, Any] | None, # attributes
str | None, # error
str | None, # domain
],
None,
],
) -> CALLBACK_TYPE:
"""Start a preview."""
error: str | None = None
try:
calculated_state: CalculatedState = self._async_calculate_state()
preview_callback(
calculated_state.state,
calculated_state.attributes,
None, # error
SENSOR_DOMAIN,
)
except ValueError as ex:
error = str(ex)
if error:
preview_callback(None, None, error, None)
return self._call_on_remove_callbacks
3. Register the websocket preview command, and specify preview in async_show_form
Add a static method async_setup_preview to the flow handler and then notify the frontend to use the preview by specifying the preview name in async_show_form.
class ExampleConfigFlow(data_entry_flow.FlowHandler):
@staticmethod
async def async_setup_preview(hass: HomeAssistant) -> None:
"""Set up preview WS API."""
websocket_api.async_register_command(hass, ws_start_preview)
async def async_step_user(self, user_input: dict[str, Any] | None
) -> data_entry_flow.FlowResult:
...
return self.async_show_form(
step_id="user",
data_schema=FLOW_SCHEMA,
errors=errors,
# Same value set in the websocket_command schema in step 1:
preview="preview_name",
)