From f9b2e10f72efd5927c25c7a7342ba6e2af019c8a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Aug 2023 16:37:13 +0200 Subject: [PATCH] Add new board type (#99334) --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/hassio/__init__.py | 1 + .../homeassistant_green/__init__.py | 27 ++++++ .../homeassistant_green/config_flow.py | 22 +++++ .../components/homeassistant_green/const.py | 3 + .../homeassistant_green/hardware.py | 44 +++++++++ .../homeassistant_green/manifest.json | 9 ++ mypy.ini | 10 ++ script/hassfest/manifest.py | 1 + tests/components/hassio/test_init.py | 1 + .../homeassistant_green/__init__.py | 1 + .../homeassistant_green/test_config_flow.py | 58 +++++++++++ .../homeassistant_green/test_hardware.py | 96 +++++++++++++++++++ .../homeassistant_green/test_init.py | 75 +++++++++++++++ 15 files changed, 351 insertions(+) create mode 100644 homeassistant/components/homeassistant_green/__init__.py create mode 100644 homeassistant/components/homeassistant_green/config_flow.py create mode 100644 homeassistant/components/homeassistant_green/const.py create mode 100644 homeassistant/components/homeassistant_green/hardware.py create mode 100644 homeassistant/components/homeassistant_green/manifest.json create mode 100644 tests/components/homeassistant_green/__init__.py create mode 100644 tests/components/homeassistant_green/test_config_flow.py create mode 100644 tests/components/homeassistant_green/test_hardware.py create mode 100644 tests/components/homeassistant_green/test_init.py diff --git a/.strict-typing b/.strict-typing index b8dc93d6780..e8bca0a1abd 100644 --- a/.strict-typing +++ b/.strict-typing @@ -149,6 +149,7 @@ homeassistant.components.history.* homeassistant.components.homeassistant.exposed_entities homeassistant.components.homeassistant.triggers.event homeassistant.components.homeassistant_alerts.* +homeassistant.components.homeassistant_green.* homeassistant.components.homeassistant_hardware.* homeassistant.components.homeassistant_sky_connect.* homeassistant.components.homeassistant_yellow.* diff --git a/CODEOWNERS b/CODEOWNERS index f33d4052304..2d28671fce5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -521,6 +521,8 @@ build.json @home-assistant/supervisor /tests/components/homeassistant/ @home-assistant/core /homeassistant/components/homeassistant_alerts/ @home-assistant/core /tests/components/homeassistant_alerts/ @home-assistant/core +/homeassistant/components/homeassistant_green/ @home-assistant/core +/tests/components/homeassistant_green/ @home-assistant/core /homeassistant/components/homeassistant_hardware/ @home-assistant/core /tests/components/homeassistant_hardware/ @home-assistant/core /homeassistant/components/homeassistant_sky_connect/ @home-assistant/core diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 0e0d42149fc..72fb5ce5110 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -254,6 +254,7 @@ MAP_SERVICE_API = { } HARDWARE_INTEGRATIONS = { + "green": "homeassistant_green", "odroid-c2": "hardkernel", "odroid-c4": "hardkernel", "odroid-m1": "hardkernel", diff --git a/homeassistant/components/homeassistant_green/__init__.py b/homeassistant/components/homeassistant_green/__init__.py new file mode 100644 index 00000000000..fbcd2093778 --- /dev/null +++ b/homeassistant/components/homeassistant_green/__init__.py @@ -0,0 +1,27 @@ +"""The Home Assistant Green integration.""" +from __future__ import annotations + +from homeassistant.components.hassio import get_os_info +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a Home Assistant Green config entry.""" + if (os_info := get_os_info(hass)) is None: + # The hassio integration has not yet fetched data from the supervisor + raise ConfigEntryNotReady + + board: str | None + if (board := os_info.get("board")) is None or board != "green": + # Not running on a Home Assistant Green, Home Assistant may have been migrated + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return True diff --git a/homeassistant/components/homeassistant_green/config_flow.py b/homeassistant/components/homeassistant_green/config_flow.py new file mode 100644 index 00000000000..17ba9aacbc5 --- /dev/null +++ b/homeassistant/components/homeassistant_green/config_flow.py @@ -0,0 +1,22 @@ +"""Config flow for the Home Assistant Green integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class HomeAssistantGreenConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Home Assistant Green.""" + + VERSION = 1 + + async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult: + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return self.async_create_entry(title="Home Assistant Green", data={}) diff --git a/homeassistant/components/homeassistant_green/const.py b/homeassistant/components/homeassistant_green/const.py new file mode 100644 index 00000000000..9046a44c12b --- /dev/null +++ b/homeassistant/components/homeassistant_green/const.py @@ -0,0 +1,3 @@ +"""Constants for the Home Assistant Green integration.""" + +DOMAIN = "homeassistant_green" diff --git a/homeassistant/components/homeassistant_green/hardware.py b/homeassistant/components/homeassistant_green/hardware.py new file mode 100644 index 00000000000..2b5268f8d03 --- /dev/null +++ b/homeassistant/components/homeassistant_green/hardware.py @@ -0,0 +1,44 @@ +"""The Home Assistant Green hardware platform.""" +from __future__ import annotations + +from homeassistant.components.hardware.models import BoardInfo, HardwareInfo +from homeassistant.components.hassio import get_os_info +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +BOARD_NAME = "Home Assistant Green" +MANUFACTURER = "homeassistant" +MODEL = "green" + + +@callback +def async_info(hass: HomeAssistant) -> list[HardwareInfo]: + """Return board info.""" + if (os_info := get_os_info(hass)) is None: + raise HomeAssistantError + board: str | None + if (board := os_info.get("board")) is None: + raise HomeAssistantError + if not board == "green": + raise HomeAssistantError + + config_entries = [ + entry.entry_id for entry in hass.config_entries.async_entries(DOMAIN) + ] + + return [ + HardwareInfo( + board=BoardInfo( + hassio_board_id=board, + manufacturer=MANUFACTURER, + model=MODEL, + revision=None, + ), + config_entries=config_entries, + dongle=None, + name=BOARD_NAME, + url=None, + ) + ] diff --git a/homeassistant/components/homeassistant_green/manifest.json b/homeassistant/components/homeassistant_green/manifest.json new file mode 100644 index 00000000000..7c9dd0322ec --- /dev/null +++ b/homeassistant/components/homeassistant_green/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "homeassistant_green", + "name": "Home Assistant Green", + "codeowners": ["@home-assistant/core"], + "config_flow": false, + "dependencies": ["hardware", "hassio", "homeassistant_hardware"], + "documentation": "https://www.home-assistant.io/integrations/homeassistant_green", + "integration_type": "hardware" +} diff --git a/mypy.ini b/mypy.ini index 8278a19465c..82cce328c6a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1252,6 +1252,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.homeassistant_green.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.homeassistant_hardware.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 65e37aa515d..9323b8e86c0 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -71,6 +71,7 @@ NO_IOT_CLASS = [ "history", "homeassistant", "homeassistant_alerts", + "homeassistant_green", "homeassistant_hardware", "homeassistant_sky_connect", "homeassistant_yellow", diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 4b10c58036e..31ee73013da 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -891,6 +891,7 @@ async def test_coordinator_updates( @pytest.mark.parametrize( ("extra_os_info", "integration"), [ + ({"board": "green"}, "homeassistant_green"), ({"board": "odroid-c2"}, "hardkernel"), ({"board": "odroid-c4"}, "hardkernel"), ({"board": "odroid-n2"}, "hardkernel"), diff --git a/tests/components/homeassistant_green/__init__.py b/tests/components/homeassistant_green/__init__.py new file mode 100644 index 00000000000..a84e076d9c9 --- /dev/null +++ b/tests/components/homeassistant_green/__init__.py @@ -0,0 +1 @@ +"""Tests for the Home Assistant Green integration.""" diff --git a/tests/components/homeassistant_green/test_config_flow.py b/tests/components/homeassistant_green/test_config_flow.py new file mode 100644 index 00000000000..2eb7389af55 --- /dev/null +++ b/tests/components/homeassistant_green/test_config_flow.py @@ -0,0 +1,58 @@ +"""Test the Home Assistant Green config flow.""" +from unittest.mock import patch + +from homeassistant.components.homeassistant_green.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_config_flow(hass: HomeAssistant) -> None: + """Test the config flow.""" + mock_integration(hass, MockModule("hassio")) + + with patch( + "homeassistant.components.homeassistant_green.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "system"} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Home Assistant Green" + assert result["data"] == {} + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == {} + assert config_entry.title == "Home Assistant Green" + + +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=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_green.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "system"} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + mock_setup_entry.assert_not_called() diff --git a/tests/components/homeassistant_green/test_hardware.py b/tests/components/homeassistant_green/test_hardware.py new file mode 100644 index 00000000000..8aacf09978d --- /dev/null +++ b/tests/components/homeassistant_green/test_hardware.py @@ -0,0 +1,96 @@ +"""Test the Home Assistant Green hardware platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.homeassistant_green.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, MockModule, mock_integration +from tests.typing import WebSocketGenerator + + +async def test_hardware_info( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test we can get the board info.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_green.get_os_info", + return_value={"board": "green"}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.homeassistant_green.hardware.get_os_info", + return_value={"board": "green"}, + ): + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == { + "hardware": [ + { + "board": { + "hassio_board_id": "green", + "manufacturer": "homeassistant", + "model": "green", + "revision": None, + }, + "config_entries": [config_entry.entry_id], + "dongle": None, + "name": "Home Assistant Green", + "url": None, + } + ] + } + + +@pytest.mark.parametrize("os_info", [None, {"board": None}, {"board": "other"}]) +async def test_hardware_info_fail( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, os_info +) -> None: + """Test async_info raises if os_info is not as expected.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_green.get_os_info", + return_value={"board": "green"}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.homeassistant_green.hardware.get_os_info", + return_value=os_info, + ): + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == {"hardware": []} diff --git a/tests/components/homeassistant_green/test_init.py b/tests/components/homeassistant_green/test_init.py new file mode 100644 index 00000000000..f48aea3fdfb --- /dev/null +++ b/tests/components/homeassistant_green/test_init.py @@ -0,0 +1,75 @@ +"""Test the Home Assistant Green integration.""" +from unittest.mock import patch + +from homeassistant.components.homeassistant_green.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_setup_entry(hass: HomeAssistant) -> None: + """Test setup of a config entry.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_green.get_os_info", + return_value={"board": "green"}, + ) as mock_get_os_info: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + # Test unloading the config entry + assert await hass.config_entries.async_unload(config_entry.entry_id) + + +async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: + """Test setup of a config entry with wrong board type.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_green.get_os_info", + return_value={"board": "generic-x86-64"}, + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + +async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: + """Test setup of a config entry when hassio has not fetched os_info.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_green.get_os_info", + return_value=None, + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + assert config_entry.state == ConfigEntryState.SETUP_RETRY