-
-
Notifications
You must be signed in to change notification settings - Fork 31.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add Thread integration * Add get/set operational dataset as TLVS * Add create operational dataset * Add set thread state * Adjust after rebase * Improve HTTP status handling * Improve test coverage * Change domains from thread to otbr * Setup otbr from a config entry * Add files * Store URL in config entry data * Make sure manifest is not sorted * Remove useless async * Call the JSON parser more * Don't raise exceptions without messages * Remove stuff which will be needed in the future * Remove more future stuff * Use API library * Bump library to 1.0.1
- Loading branch information
1 parent
9b835f8
commit 11b9a0b
Showing
11 changed files
with
289 additions
and
0 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
"""The Open Thread Border Router integration.""" | ||
from __future__ import annotations | ||
|
||
import dataclasses | ||
|
||
import python_otbr_api | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import HomeAssistantError | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
||
from .const import DOMAIN | ||
|
||
|
||
@dataclasses.dataclass | ||
class OTBRData: | ||
"""Container for OTBR data.""" | ||
|
||
url: str | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up an Open Thread Border Router config entry.""" | ||
|
||
hass.data[DOMAIN] = OTBRData(entry.data["url"]) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
hass.data.pop(DOMAIN) | ||
return True | ||
|
||
|
||
def _async_get_thread_rest_service_url(hass) -> str: | ||
"""Return Thread REST API URL.""" | ||
otbr_data: OTBRData | None = hass.data.get(DOMAIN) | ||
if not otbr_data: | ||
raise HomeAssistantError("otbr not setup") | ||
|
||
return otbr_data.url | ||
|
||
|
||
async def async_get_active_dataset_tlvs(hass: HomeAssistant) -> bytes | None: | ||
"""Get current active operational dataset in TLVS format, or None. | ||
Returns None if there is no active operational dataset. | ||
Raises if the http status is 400 or higher or if the response is invalid. | ||
""" | ||
|
||
api = python_otbr_api.OTBR( | ||
_async_get_thread_rest_service_url(hass), async_get_clientsession(hass), 10 | ||
) | ||
try: | ||
return await api.get_active_dataset_tlvs() | ||
except python_otbr_api.OTBRError as exc: | ||
raise HomeAssistantError("Failed to call OTBR API") from exc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
"""Config flow for the Open Thread Border Router integration.""" | ||
from __future__ import annotations | ||
|
||
from homeassistant.components.hassio import HassioServiceInfo | ||
from homeassistant.config_entries import ConfigFlow | ||
from homeassistant.data_entry_flow import FlowResult | ||
|
||
from .const import DOMAIN | ||
|
||
|
||
class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for Home Assistant Sky Connect.""" | ||
|
||
VERSION = 1 | ||
|
||
async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: | ||
"""Handle hassio discovery.""" | ||
if self._async_current_entries(): | ||
return self.async_abort(reason="single_instance_allowed") | ||
|
||
config = discovery_info.config | ||
return self.async_create_entry( | ||
title="Thread", | ||
data={"url": f"http://{config['host']}:{config['port']}"}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
"""Constants for the Open Thread Border Router integration.""" | ||
|
||
DOMAIN = "otbr" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"codeowners": ["@home-assistant/core"], | ||
"after_dependencies": ["hassio"], | ||
"domain": "otbr", | ||
"iot_class": "local_polling", | ||
"config_flow": false, | ||
"documentation": "https://www.home-assistant.io/integrations/otbr", | ||
"integration_type": "system", | ||
"name": "Thread", | ||
"requirements": ["python-otbr-api==1.0.1"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""Tests for the Thread integration.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
"""Test fixtures for the Home Assistant Sky Connect integration.""" | ||
|
||
import pytest | ||
|
||
from homeassistant.components import otbr | ||
|
||
from tests.common import MockConfigEntry | ||
|
||
CONFIG_ENTRY_DATA = {"url": "http://core-silabs-multiprotocol:8081"} | ||
|
||
|
||
@pytest.fixture(name="thread_config_entry") | ||
async def thread_config_entry_fixture(hass): | ||
"""Mock Thread config entry.""" | ||
config_entry = MockConfigEntry( | ||
data=CONFIG_ENTRY_DATA, | ||
domain=otbr.DOMAIN, | ||
options={}, | ||
title="Thread", | ||
) | ||
config_entry.add_to_hass(hass) | ||
assert await hass.config_entries.async_setup(config_entry.entry_id) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
"""Test the Open Thread Border Router config flow.""" | ||
from unittest.mock import patch | ||
|
||
from homeassistant.components import hassio, otbr | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.data_entry_flow import FlowResultType | ||
|
||
from tests.common import MockConfigEntry, MockModule, mock_integration | ||
|
||
HASSIO_DATA = hassio.HassioServiceInfo( | ||
config={"host": "blah", "port": "bluh"}, | ||
name="blah", | ||
slug="blah", | ||
) | ||
|
||
|
||
async def test_hassio_discovery_flow(hass: HomeAssistant) -> None: | ||
"""Test the hassio discovery flow.""" | ||
with patch( | ||
"homeassistant.components.otbr.async_setup_entry", | ||
return_value=True, | ||
) as mock_setup_entry: | ||
result = await hass.config_entries.flow.async_init( | ||
otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA | ||
) | ||
|
||
expected_data = { | ||
"url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", | ||
} | ||
|
||
assert result["type"] == FlowResultType.CREATE_ENTRY | ||
assert result["title"] == "Thread" | ||
assert result["data"] == expected_data | ||
assert result["options"] == {} | ||
assert len(mock_setup_entry.mock_calls) == 1 | ||
|
||
config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] | ||
assert config_entry.data == expected_data | ||
assert config_entry.options == {} | ||
assert config_entry.title == "Thread" | ||
assert config_entry.unique_id is None | ||
|
||
|
||
async def test_config_flow_single_entry(hass: HomeAssistant) -> None: | ||
"""Test only a single entry is allowed.""" | ||
mock_integration(hass, MockModule("hassio")) | ||
|
||
# Setup the config entry | ||
config_entry = MockConfigEntry( | ||
data={}, | ||
domain=otbr.DOMAIN, | ||
options={}, | ||
title="Thread", | ||
) | ||
config_entry.add_to_hass(hass) | ||
|
||
with patch( | ||
"homeassistant.components.homeassistant_yellow.async_setup_entry", | ||
return_value=True, | ||
) as mock_setup_entry: | ||
result = await hass.config_entries.flow.async_init( | ||
otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA | ||
) | ||
|
||
assert result["type"] == FlowResultType.ABORT | ||
assert result["reason"] == "single_instance_allowed" | ||
mock_setup_entry.assert_not_called() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
"""Test the Open Thread Border Router integration.""" | ||
|
||
from http import HTTPStatus | ||
|
||
import pytest | ||
|
||
from homeassistant.components import otbr | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import HomeAssistantError | ||
|
||
from tests.test_util.aiohttp import AiohttpClientMocker | ||
|
||
BASE_URL = "http://core-silabs-multiprotocol:8081" | ||
|
||
|
||
async def test_remove_entry( | ||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry | ||
): | ||
"""Test async_get_thread_state.""" | ||
|
||
aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text="0E") | ||
|
||
assert await otbr.async_get_active_dataset_tlvs(hass) == bytes.fromhex("0E") | ||
|
||
config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] | ||
await hass.config_entries.async_remove(config_entry.entry_id) | ||
|
||
with pytest.raises(HomeAssistantError): | ||
assert await otbr.async_get_active_dataset_tlvs(hass) | ||
|
||
|
||
async def test_get_active_dataset_tlvs( | ||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry | ||
): | ||
"""Test async_get_active_dataset_tlvs.""" | ||
|
||
mock_response = ( | ||
"0E080000000000010000000300001035060004001FFFE00208F642646DA209B1C00708FDF57B5A" | ||
"0FE2AAF60510DE98B5BA1A528FEE049D4B4B01835375030D4F70656E5468726561642048410102" | ||
"25A40410F5DD18371BFD29E1A601EF6FFAD94C030C0402A0F7F8" | ||
) | ||
|
||
aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text=mock_response) | ||
|
||
assert await otbr.async_get_active_dataset_tlvs(hass) == bytes.fromhex( | ||
mock_response | ||
) | ||
|
||
|
||
async def test_get_active_dataset_tlvs_empty( | ||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry | ||
): | ||
"""Test async_get_active_dataset_tlvs.""" | ||
|
||
aioclient_mock.get(f"{BASE_URL}/node/dataset/active", status=HTTPStatus.NO_CONTENT) | ||
assert await otbr.async_get_active_dataset_tlvs(hass) is None | ||
|
||
|
||
async def test_get_active_dataset_tlvs_addon_not_installed(hass: HomeAssistant): | ||
"""Test async_get_active_dataset_tlvs when the multi-PAN addon is not installed.""" | ||
|
||
with pytest.raises(HomeAssistantError): | ||
await otbr.async_get_active_dataset_tlvs(hass) | ||
|
||
|
||
async def test_get_active_dataset_tlvs_404( | ||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry | ||
): | ||
"""Test async_get_active_dataset_tlvs with error.""" | ||
|
||
aioclient_mock.get(f"{BASE_URL}/node/dataset/active", status=HTTPStatus.NOT_FOUND) | ||
with pytest.raises(HomeAssistantError): | ||
await otbr.async_get_active_dataset_tlvs(hass) | ||
|
||
|
||
async def test_get_active_dataset_tlvs_201( | ||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry | ||
): | ||
"""Test async_get_active_dataset_tlvs with error.""" | ||
|
||
aioclient_mock.get(f"{BASE_URL}/node/dataset/active", status=HTTPStatus.CREATED) | ||
with pytest.raises(HomeAssistantError): | ||
assert await otbr.async_get_active_dataset_tlvs(hass) is None | ||
|
||
|
||
async def test_get_active_dataset_tlvs_invalid( | ||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry | ||
): | ||
"""Test async_get_active_dataset_tlvs with error.""" | ||
|
||
aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text="unexpected") | ||
with pytest.raises(HomeAssistantError): | ||
assert await otbr.async_get_active_dataset_tlvs(hass) is None |