diff --git a/.coveragerc b/.coveragerc index a988a50fd9f..9dc665d9c3c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1362,6 +1362,11 @@ omit = homeassistant/components/telnet/switch.py homeassistant/components/temper/sensor.py homeassistant/components/tensorflow/image_processing.py + homeassistant/components/teslemetry/__init__.py + homeassistant/components/teslemetry/climate.py + homeassistant/components/teslemetry/coordinator.py + homeassistant/components/teslemetry/entity.py + homeassistant/components/teslemetry/context.py homeassistant/components/tfiac/climate.py homeassistant/components/thermoworks_smoke/sensor.py homeassistant/components/thethingsnetwork/* diff --git a/CODEOWNERS b/CODEOWNERS index 339d4aca6ea..a423bbf8f76 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1344,6 +1344,8 @@ build.json @home-assistant/supervisor /tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core /homeassistant/components/tesla_wall_connector/ @einarhauks /tests/components/tesla_wall_connector/ @einarhauks +/homeassistant/components/teslemetry/ @Bre77 +/tests/components/teslemetry/ @Bre77 /homeassistant/components/tessie/ @Bre77 /tests/components/tessie/ @Bre77 /homeassistant/components/text/ @home-assistant/core diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py new file mode 100644 index 00000000000..0c2a16fa15b --- /dev/null +++ b/homeassistant/components/teslemetry/__init__.py @@ -0,0 +1,77 @@ +"""Teslemetry integration.""" +import asyncio +from typing import Final + +from tesla_fleet_api import Teslemetry +from tesla_fleet_api.exceptions import InvalidToken, PaymentRequired, TeslaFleetError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN, LOGGER +from .coordinator import TeslemetryVehicleDataCoordinator +from .models import TeslemetryVehicleData + +PLATFORMS: Final = [ + Platform.CLIMATE, +] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Teslemetry config.""" + + access_token = entry.data[CONF_ACCESS_TOKEN] + + # Create API connection + teslemetry = Teslemetry( + session=async_get_clientsession(hass), + access_token=access_token, + ) + try: + products = (await teslemetry.products())["response"] + except InvalidToken: + LOGGER.error("Access token is invalid, unable to connect to Teslemetry") + return False + except PaymentRequired: + LOGGER.error("Subscription required, unable to connect to Telemetry") + return False + except TeslaFleetError as e: + raise ConfigEntryNotReady from e + + # Create array of classes + data = [] + for product in products: + if "vin" not in product: + continue + vin = product["vin"] + + api = teslemetry.vehicle.specific(vin) + coordinator = TeslemetryVehicleDataCoordinator(hass, api) + data.append( + TeslemetryVehicleData( + api=api, + coordinator=coordinator, + vin=vin, + ) + ) + + # Do all coordinator first refresh simultaneously + await asyncio.gather( + *(vehicle.coordinator.async_config_entry_first_refresh() for vehicle in data) + ) + + # Setup Platforms + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Teslemetry Config.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py new file mode 100644 index 00000000000..cea56f35b15 --- /dev/null +++ b/homeassistant/components/teslemetry/climate.py @@ -0,0 +1,130 @@ +"""Climate platform for Teslemetry integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, TeslemetryClimateSide +from .context import handle_command +from .entity import TeslemetryVehicleEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Teslemetry Climate platform from a config entry.""" + data = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + TeslemetryClimateEntity(vehicle, TeslemetryClimateSide.DRIVER) + for vehicle in data + ) + + +class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): + """Vehicle Location Climate Class.""" + + _attr_precision = PRECISION_HALVES + _attr_min_temp = 15 + _attr_max_temp = 28 + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF] + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ) + _attr_preset_modes = ["off", "keep", "dog", "camp"] + + @property + def hvac_mode(self) -> HVACMode | None: + """Return hvac operation ie. heat, cool mode.""" + if self.get("climate_state_is_climate_on"): + return HVACMode.HEAT_COOL + return HVACMode.OFF + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self.get("climate_state_inside_temp") + + @property + def target_temperature(self) -> float | None: + """Return the temperature we try to reach.""" + return self.get(f"climate_state_{self.key}_setting") + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return self.get("climate_state_max_avail_temp", self._attr_max_temp) + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return self.get("climate_state_min_avail_temp", self._attr_min_temp) + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + return self.get("climate_state_climate_keeper_mode") + + async def async_turn_on(self) -> None: + """Set the climate state to on.""" + with handle_command(): + await self.wake_up_if_asleep() + await self.api.auto_conditioning_start() + self.set(("climate_state_is_climate_on", True)) + + async def async_turn_off(self) -> None: + """Set the climate state to off.""" + with handle_command(): + await self.wake_up_if_asleep() + await self.api.auto_conditioning_stop() + self.set( + ("climate_state_is_climate_on", False), + ("climate_state_climate_keeper_mode", "off"), + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set the climate temperature.""" + temp = kwargs[ATTR_TEMPERATURE] + with handle_command(): + await self.wake_up_if_asleep() + await self.api.set_temps( + driver_temp=temp, + passenger_temp=temp, + ) + + self.set((f"climate_state_{self.key}_setting", temp)) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set the climate mode and state.""" + if hvac_mode == HVACMode.OFF: + await self.async_turn_off() + else: + await self.async_turn_on() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the climate preset mode.""" + with handle_command(): + await self.wake_up_if_asleep() + await self.api.set_climate_keeper_mode( + climate_keeper_mode=self._attr_preset_modes.index(preset_mode) + ) + self.set( + ( + "climate_state_climate_keeper_mode", + preset_mode, + ), + ( + "climate_state_is_climate_on", + preset_mode != self._attr_preset_modes[0], + ), + ) diff --git a/homeassistant/components/teslemetry/config_flow.py b/homeassistant/components/teslemetry/config_flow.py new file mode 100644 index 00000000000..64a279132ad --- /dev/null +++ b/homeassistant/components/teslemetry/config_flow.py @@ -0,0 +1,63 @@ +"""Config Flow for Teslemetry integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from aiohttp import ClientConnectionError +from tesla_fleet_api import Teslemetry +from tesla_fleet_api.exceptions import InvalidToken, PaymentRequired, TeslaFleetError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN, LOGGER + +TESLEMETRY_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) +DESCRIPTION_PLACEHOLDERS = { + "short_url": "teslemetry.com/console", + "url": "[teslemetry.com/console](https://teslemetry.com/console)", +} + + +class TeslemetryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config Teslemetry API connection.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: Mapping[str, Any] | None = None + ) -> FlowResult: + """Get configuration from the user.""" + errors: dict[str, str] = {} + if user_input: + teslemetry = Teslemetry( + session=async_get_clientsession(self.hass), + access_token=user_input[CONF_ACCESS_TOKEN], + ) + try: + await teslemetry.test() + except InvalidToken: + errors[CONF_ACCESS_TOKEN] = "invalid_access_token" + except PaymentRequired: + errors["base"] = "subscription_required" + except ClientConnectionError: + errors["base"] = "cannot_connect" + except TeslaFleetError as e: + LOGGER.exception(str(e)) + errors["base"] = "unknown" + else: + return self.async_create_entry( + title="Teslemetry", + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=TESLEMETRY_SCHEMA, + description_placeholders=DESCRIPTION_PLACEHOLDERS, + errors=errors, + ) diff --git a/homeassistant/components/teslemetry/const.py b/homeassistant/components/teslemetry/const.py new file mode 100644 index 00000000000..9b31a3270ca --- /dev/null +++ b/homeassistant/components/teslemetry/const.py @@ -0,0 +1,31 @@ +"""Constants used by Teslemetry integration.""" +from __future__ import annotations + +from enum import StrEnum +import logging + +DOMAIN = "teslemetry" + +LOGGER = logging.getLogger(__package__) + +MODELS = { + "model3": "Model 3", + "modelx": "Model X", + "modely": "Model Y", + "models": "Model S", +} + + +class TeslemetryState(StrEnum): + """Teslemetry Vehicle States.""" + + ONLINE = "online" + ASLEEP = "asleep" + OFFLINE = "offline" + + +class TeslemetryClimateSide(StrEnum): + """Teslemetry Climate Keeper Modes.""" + + DRIVER = "driver_temp" + PASSENGER = "passenger_temp" diff --git a/homeassistant/components/teslemetry/context.py b/homeassistant/components/teslemetry/context.py new file mode 100644 index 00000000000..c2c9317f671 --- /dev/null +++ b/homeassistant/components/teslemetry/context.py @@ -0,0 +1,16 @@ +"""Teslemetry context managers.""" + +from contextlib import contextmanager + +from tesla_fleet_api.exceptions import TeslaFleetError + +from homeassistant.exceptions import HomeAssistantError + + +@contextmanager +def handle_command(): + """Handle wake up and errors.""" + try: + yield + except TeslaFleetError as e: + raise HomeAssistantError from e diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py new file mode 100644 index 00000000000..4f12b4a3111 --- /dev/null +++ b/homeassistant/components/teslemetry/coordinator.py @@ -0,0 +1,67 @@ +"""Teslemetry Data Coordinator.""" +from datetime import timedelta +from typing import Any + +from tesla_fleet_api.exceptions import TeslaFleetError, VehicleOffline +from tesla_fleet_api.vehiclespecific import VehicleSpecific + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import LOGGER, TeslemetryState + +SYNC_INTERVAL = 60 + + +class TeslemetryVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Class to manage fetching data from the Teslemetry API.""" + + def __init__(self, hass: HomeAssistant, api: VehicleSpecific) -> None: + """Initialize Teslemetry Data Update Coordinator.""" + super().__init__( + hass, + LOGGER, + name="Teslemetry Vehicle", + update_interval=timedelta(seconds=SYNC_INTERVAL), + ) + self.api = api + + async def async_config_entry_first_refresh(self) -> None: + """Perform first refresh.""" + try: + response = await self.api.wake_up() + if response["response"]["state"] != TeslemetryState.ONLINE: + # The first refresh will fail, so retry later + raise ConfigEntryNotReady("Vehicle is not online") + except TeslaFleetError as e: + # The first refresh will also fail, so retry later + raise ConfigEntryNotReady from e + await super().async_config_entry_first_refresh() + + async def _async_update_data(self) -> dict[str, Any]: + """Update vehicle data using Teslemetry API.""" + + try: + data = await self.api.vehicle_data() + except VehicleOffline: + self.data["state"] = TeslemetryState.OFFLINE + return self.data + except TeslaFleetError as e: + raise UpdateFailed(e.message) from e + + return self._flatten(data["response"]) + + def _flatten( + self, data: dict[str, Any], parent: str | None = None + ) -> dict[str, Any]: + """Flatten the data structure.""" + result = {} + for key, value in data.items(): + if parent: + key = f"{parent}_{key}" + if isinstance(value, dict): + result.update(self._flatten(value, key)) + else: + result[key] = value + return result diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py new file mode 100644 index 00000000000..c8fbc5910d8 --- /dev/null +++ b/homeassistant/components/teslemetry/entity.py @@ -0,0 +1,62 @@ +"""Teslemetry parent entity class.""" + +import asyncio +from typing import Any + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MODELS, TeslemetryState +from .coordinator import TeslemetryVehicleDataCoordinator +from .models import TeslemetryVehicleData + + +class TeslemetryVehicleEntity(CoordinatorEntity[TeslemetryVehicleDataCoordinator]): + """Parent class for Teslemetry Entities.""" + + _attr_has_entity_name = True + _wakelock = asyncio.Lock() + + def __init__( + self, + vehicle: TeslemetryVehicleData, + key: str, + ) -> None: + """Initialize common aspects of a Teslemetry entity.""" + super().__init__(vehicle.coordinator) + self.key = key + self.api = vehicle.api + + car_type = self.coordinator.data["vehicle_config_car_type"] + + self._attr_translation_key = key + self._attr_unique_id = f"{vehicle.vin}-{key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, vehicle.vin)}, + manufacturer="Tesla", + configuration_url="https://teslemetry.com/console", + name=self.coordinator.data["vehicle_state_vehicle_name"], + model=MODELS.get(car_type, car_type), + sw_version=self.coordinator.data["vehicle_state_car_version"].split(" ")[0], + hw_version=self.coordinator.data["vehicle_config_driver_assist"], + serial_number=vehicle.vin, + ) + + async def wake_up_if_asleep(self) -> None: + """Wake up the vehicle if its asleep.""" + async with self._wakelock: + while self.coordinator.data["state"] != TeslemetryState.ONLINE: + state = (await self.api.wake_up())["response"]["state"] + self.coordinator.data["state"] = state + if state != TeslemetryState.ONLINE: + await asyncio.sleep(5) + + def get(self, key: str | None = None, default: Any | None = None) -> Any: + """Return a specific value from coordinator data.""" + return self.coordinator.data.get(key or self.key, default) + + def set(self, *args: Any) -> None: + """Set a value in coordinator data.""" + for key, value in args: + self.coordinator.data[key] = value + self.async_write_ha_state() diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json new file mode 100644 index 00000000000..be6f6ae634c --- /dev/null +++ b/homeassistant/components/teslemetry/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "teslemetry", + "name": "Teslemetry", + "codeowners": ["@Bre77"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/teslemetry", + "iot_class": "cloud_polling", + "loggers": ["tesla-fleet-api"], + "requirements": ["tesla-fleet-api==0.2.0"] +} diff --git a/homeassistant/components/teslemetry/models.py b/homeassistant/components/teslemetry/models.py new file mode 100644 index 00000000000..9b90c0b0750 --- /dev/null +++ b/homeassistant/components/teslemetry/models.py @@ -0,0 +1,17 @@ +"""The Teslemetry integration models.""" +from __future__ import annotations + +from dataclasses import dataclass + +from tesla_fleet_api import VehicleSpecific + +from .coordinator import TeslemetryVehicleDataCoordinator + + +@dataclass +class TeslemetryVehicleData: + """Data for a vehicle in the Teslemetry integration.""" + + api: VehicleSpecific + coordinator: TeslemetryVehicleDataCoordinator + vin: str diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json new file mode 100644 index 00000000000..95b2266b2dd --- /dev/null +++ b/homeassistant/components/teslemetry/strings.json @@ -0,0 +1,35 @@ +{ + "config": { + "error": { + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", + "subscription_required": "Subscription required, please visit {short_url}", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "access_token": "[%key:common::config_flow::data::access_token%]" + }, + "description": "Enter an access token from {url}." + } + } + }, + "entity": { + "climate": { + "driver_temp": { + "name": "[%key:component::climate::title%]", + "state_attributes": { + "preset_mode": { + "state": { + "off": "Normal", + "keep": "Keep mode", + "dog": "Dog mode", + "camp": "Camp mode" + } + } + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a32c30293b9..3f26b6f907b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -515,6 +515,7 @@ FLOWS = { "tedee", "tellduslive", "tesla_wall_connector", + "teslemetry", "tessie", "thermobeacon", "thermopro", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index b935fa25fbc..f188201f847 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5947,6 +5947,12 @@ } } }, + "teslemetry": { + "name": "Teslemetry", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "tessie": { "name": "Tessie", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index c5b3a52ccea..f70bd86f29c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2656,6 +2656,9 @@ temperusb==1.6.1 # homeassistant.components.tensorflow # tensorflow==2.5.0 +# homeassistant.components.teslemetry +tesla-fleet-api==0.2.0 + # homeassistant.components.powerwall tesla-powerwall==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1156ba311a6..479d84bf57a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2015,6 +2015,9 @@ temescal==0.5 # homeassistant.components.temper temperusb==1.6.1 +# homeassistant.components.teslemetry +tesla-fleet-api==0.2.0 + # homeassistant.components.powerwall tesla-powerwall==0.5.0 diff --git a/tests/components/teslemetry/__init__.py b/tests/components/teslemetry/__init__.py new file mode 100644 index 00000000000..422a2ecaac9 --- /dev/null +++ b/tests/components/teslemetry/__init__.py @@ -0,0 +1 @@ +"""Tests for the Teslemetry integration.""" diff --git a/tests/components/teslemetry/const.py b/tests/components/teslemetry/const.py new file mode 100644 index 00000000000..527ef98efca --- /dev/null +++ b/tests/components/teslemetry/const.py @@ -0,0 +1,5 @@ +"""Constants for the teslemetry tests.""" + +from homeassistant.const import CONF_ACCESS_TOKEN + +CONFIG = {CONF_ACCESS_TOKEN: "1234567890"} diff --git a/tests/components/teslemetry/test_config_flow.py b/tests/components/teslemetry/test_config_flow.py new file mode 100644 index 00000000000..ca9b89bedb3 --- /dev/null +++ b/tests/components/teslemetry/test_config_flow.py @@ -0,0 +1,87 @@ +"""Test the Teslemetry config flow.""" + +from unittest.mock import AsyncMock, patch + +from aiohttp import ClientConnectionError +import pytest +from tesla_fleet_api.exceptions import InvalidToken, PaymentRequired, TeslaFleetError + +from homeassistant import config_entries +from homeassistant.components.teslemetry.const import DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .const import CONFIG + + +@pytest.fixture(autouse=True) +def teslemetry_config_entry_mock(): + """Mock Teslemetry api class.""" + with patch( + "homeassistant.components.teslemetry.config_flow.Teslemetry", + ) as teslemetry_config_entry_mock: + teslemetry_config_entry_mock.return_value.test = AsyncMock() + yield teslemetry_config_entry_mock + + +async def test_form( + hass: HomeAssistant, +) -> None: + """Test we get the form.""" + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result1["type"] == FlowResultType.FORM + assert not result1["errors"] + + with patch( + "homeassistant.components.teslemetry.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + CONFIG, + ) + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == CONFIG + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (InvalidToken, {CONF_ACCESS_TOKEN: "invalid_access_token"}), + (PaymentRequired, {"base": "subscription_required"}), + (ClientConnectionError, {"base": "cannot_connect"}), + (TeslaFleetError, {"base": "unknown"}), + ], +) +async def test_form_errors( + hass: HomeAssistant, side_effect, error, teslemetry_config_entry_mock +) -> None: + """Test errors are handled.""" + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + teslemetry_config_entry_mock.return_value.test.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + CONFIG, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == error + + # Complete the flow + teslemetry_config_entry_mock.return_value.test.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + CONFIG, + ) + assert result3["type"] == FlowResultType.CREATE_ENTRY