Add OTBR option flow

This commit is contained in:
Erik
2023-03-06 08:39:09 +01:00
parent 3f32c5d2ad
commit 52fcef2e03
5 changed files with 234 additions and 9 deletions
@@ -71,6 +71,11 @@ class OTBRData:
"""Get current active operational dataset in TLVS format, or None."""
return await self.api.get_active_dataset_tlvs()
@_handle_otbr_error
async def set_active_dataset_tlvs(self, dataset: bytes) -> None:
"""Set current active operational dataset in TLVS format."""
await self.api.set_active_dataset_tlvs(dataset)
@_handle_otbr_error
async def create_active_dataset(
self, dataset: python_otbr_api.OperationalDataSet
+168 -3
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
import logging
from typing import TYPE_CHECKING, Any
import aiohttp
import python_otbr_api
@@ -10,14 +11,27 @@ from python_otbr_api import tlv_parser
import voluptuous as vol
from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.components.thread import async_get_preferred_dataset
from homeassistant.config_entries import ConfigFlow
from homeassistant.components.thread import (
async_add_dataset,
async_get_preferred_dataset,
async_set_preferred_dataset,
)
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_URL
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEFAULT_CHANNEL, DOMAIN
if TYPE_CHECKING:
from . import OTBRData
_LOGGER = logging.getLogger(__name__)
@@ -26,6 +40,14 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> OTBROptionsFlow:
"""Get the options flow for this handler."""
return OTBROptionsFlow(config_entry)
async def _connect_and_create_dataset(self, url: str) -> None:
"""Connect to the OTBR and create a dataset if it doesn't have one."""
api = python_otbr_api.OTBR(url, async_get_clientsession(self.hass), 10)
@@ -101,5 +123,148 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(DOMAIN)
return self.async_create_entry(
title="Open Thread Border Router",
data={"url": url},
data={CONF_URL: url},
)
class OTBROptionsFlow(OptionsFlowWithConfigEntry):
"""Handle OTBR options."""
async def async_step_init(self, user_input: None = None) -> FlowResult:
"""Manage the OTBR options."""
if DOMAIN not in self.hass.data:
return self.async_abort(reason="config_entry_not_setup")
data: OTBRData = self.hass.data[DOMAIN]
menu_options = ["create_network"]
placeholders = {}
otbr_dataset: dict[tlv_parser.MeshcopTLVType, str] | None = None
preferred_dataset: dict[tlv_parser.MeshcopTLVType, str] | None = None
try:
if otbr_dataset_tlv := await data.get_active_dataset_tlvs():
otbr_dataset = tlv_parser.parse_tlv(otbr_dataset_tlv.hex())
except HomeAssistantError:
_LOGGER.warning("Could not read active dataset", exc_info=True)
if preferred_dataset_tlv := await async_get_preferred_dataset(self.hass):
preferred_dataset = tlv_parser.parse_tlv(preferred_dataset_tlv)
if otbr_dataset and otbr_dataset != preferred_dataset:
menu_options.append("prefer_otbr_network")
placeholders["otbr_network"] = otbr_dataset.get(
tlv_parser.MeshcopTLVType.NETWORKNAME, ""
)
if preferred_dataset and preferred_dataset != otbr_dataset:
menu_options.append("use_preferred_network")
placeholders["preferred_network"] = preferred_dataset.get(
tlv_parser.MeshcopTLVType.NETWORKNAME, ""
)
self.context["placeholders"] = placeholders
return self.async_show_menu(
step_id="thread_network_menu",
menu_options=menu_options,
description_placeholders=placeholders,
)
async def async_step_create_network(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Create a new network, and optionally mark it as preferred."""
if user_input is not None:
data: OTBRData = self.hass.data[DOMAIN]
# We currently have no way to know which channel zha is using, assume it's
# the default
zha_channel = DEFAULT_CHANNEL
try:
# Disable the router, create a new network and read it back
await data.set_enabled(False)
await data.create_active_dataset(
python_otbr_api.OperationalDataSet(
channel=zha_channel, network_name="home-assistant"
)
)
await data.set_enabled(True)
dataset_tlvs = await data.get_active_dataset_tlvs()
except HomeAssistantError:
_LOGGER.warning("Failed to create new Thread network", exc_info=True)
return self.async_abort(reason="unknown")
if not dataset_tlvs:
_LOGGER.warning("Got empty network")
return self.async_abort(reason="unknown")
dataset_id = await async_add_dataset(
self.hass, self._config_entry.title, dataset_tlvs.hex()
)
if user_input["set_as_preferred"]:
await async_set_preferred_dataset(self.hass, dataset_id)
return self.async_create_entry(data={})
return self.async_show_form(
step_id="create_network",
data_schema=vol.Schema(
{vol.Required("set_as_preferred", default=True): bool}
),
)
async def async_step_prefer_otbr_network(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Make the OTBR's network the preferred network."""
if user_input is not None:
data: OTBRData = self.hass.data[DOMAIN]
try:
dataset_tlvs = await data.get_active_dataset_tlvs()
except HomeAssistantError:
_LOGGER.warning("Failed to get the active network", exc_info=True)
return self.async_abort(reason="unknown")
if not dataset_tlvs:
_LOGGER.warning("Got empty network")
return self.async_abort(reason="unknown")
dataset_id = await async_add_dataset(
self.hass, self._config_entry.title, dataset_tlvs.hex()
)
await async_set_preferred_dataset(self.hass, dataset_id)
return self.async_create_entry(data={})
return self.async_show_form(
step_id="prefer_otbr_network",
description_placeholders=self.context["placeholders"],
)
async def async_step_use_preferred_network(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Make the OTBR use the preferred network."""
if not (thread_dataset_tlv := await async_get_preferred_dataset(self.hass)):
_LOGGER.warning("No preferred network")
return self.async_abort(reason="unknown")
if user_input is not None:
data: OTBRData = self.hass.data[DOMAIN]
try:
await data.set_enabled(False)
await data.set_active_dataset_tlvs(bytes.fromhex(thread_dataset_tlv))
await data.set_enabled(True)
except HomeAssistantError:
return self.async_abort(reason="unknown")
return self.async_create_entry(data={})
return self.async_show_form(
step_id="use_preferred_network",
description_placeholders=self.context["placeholders"],
)
@@ -15,6 +15,37 @@
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"options": {
"step": {
"thread_network_menu": {
"title": "Reconfigure the OTBR's Thread network",
"menu_options": {
"create_network": "[%key:component::otbr::options::step::create_network::title%]",
"prefer_otbr_network": "[%key:component::otbr::options::step::prefer_otbr_network::title%]",
"use_preferred_network": "[%key:component::otbr::options::step::use_preferred_network::title%]"
}
},
"create_network": {
"title": "Reset Thread network",
"data": {
"set_as_preferred": "Make the new network the preferred network"
},
"description": "This will reset the Home Assistant Border Router to its factory defaults and form a new Thread network.\n\nThe old network may no longer be available, and any devices that were attached to this network may need to be recomissioned."
},
"prefer_otbr_network": {
"title": "Prefer Thread network ''{otbr_network}''",
"description": "Use this Thread network when commissioning Thread devices with Home Assistant.\n\nAlready commisioned devices will remain on their current networks."
},
"use_preferred_network": {
"title": "Join Thread network ''{preferred_network}''",
"description": "This will change the network your Home Assistant Border Router is attached to.\n\nThe old network may no longer be available, and any devices that were attached to this network may need to be recomissioned."
}
},
"abort": {
"config_entry_not_setup": "The OTBR config entry is not setup",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"issues": {
"insecure_thread_network": {
"title": "Insecure Thread network settings detected",
+7 -1
View File
@@ -6,7 +6,12 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .dataset_store import DatasetEntry, async_add_dataset, async_get_preferred_dataset
from .dataset_store import (
DatasetEntry,
async_add_dataset,
async_get_preferred_dataset,
async_set_preferred_dataset,
)
from .websocket_api import async_setup as async_setup_ws_api
__all__ = [
@@ -14,6 +19,7 @@ __all__ = [
"DatasetEntry",
"async_add_dataset",
"async_get_preferred_dataset",
"async_set_preferred_dataset",
]
@@ -82,13 +82,16 @@ class DatasetStore:
)
@callback
def async_add(self, source: str, tlv: str) -> None:
def async_add(self, source: str, tlv: str) -> str:
"""Add dataset, does nothing if it already exists."""
# Make sure the tlv is valid
dataset = tlv_parser.parse_tlv(tlv)
# Bail out if the dataset already exists
if any(entry for entry in self.datasets.values() if entry.dataset == dataset):
return
if entry := next(
(entry for entry in self.datasets.values() if entry.dataset == dataset),
None,
):
return entry.id
entry = DatasetEntry(source=source, tlv=tlv)
self.datasets[entry.id] = entry
@@ -96,6 +99,7 @@ class DatasetStore:
if self.preferred_dataset is None:
self.preferred_dataset = entry.id
self.async_schedule_save()
return entry.id
@callback
def async_delete(self, dataset_id: str) -> None:
@@ -110,6 +114,14 @@ class DatasetStore:
"""Get dataset by id."""
return self.datasets.get(dataset_id)
@callback
def async_set_preferred(self, dataset_id: str) -> None:
"""Set preferred dataset."""
if dataset_id not in self.datasets:
raise DatasetPreferredError("attempt to set unknown preferred dataset")
self.preferred_dataset = dataset_id
self.async_schedule_save()
async def async_load(self) -> None:
"""Load the datasets."""
data = await self._store.async_load()
@@ -153,10 +165,10 @@ async def async_get_store(hass: HomeAssistant) -> DatasetStore:
return store
async def async_add_dataset(hass: HomeAssistant, source: str, tlv: str) -> None:
async def async_add_dataset(hass: HomeAssistant, source: str, tlv: str) -> str:
"""Add a dataset."""
store = await async_get_store(hass)
store.async_add(source, tlv)
return store.async_add(source, tlv)
async def async_get_preferred_dataset(hass: HomeAssistant) -> str | None:
@@ -167,3 +179,9 @@ async def async_get_preferred_dataset(hass: HomeAssistant) -> str | None:
) is None:
return None
return entry.tlv
async def async_set_preferred_dataset(hass: HomeAssistant, dataset_id: str) -> None:
"""Set the preferred dataset."""
store = await async_get_store(hass)
store.async_set_preferred(dataset_id)