diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 3303059d824..75b2535bd44 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -88,11 +88,13 @@ from .handler import ( # noqa: F401 async_get_addon_discovery_info, async_get_addon_info, async_get_addon_store_info, + async_get_green_settings, async_get_yellow_settings, async_install_addon, async_reboot_host, async_restart_addon, async_set_addon_options, + async_set_green_settings, async_set_yellow_settings, async_start_addon, async_stop_addon, diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 020a4365ec6..fe9e1ba1d2e 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -263,6 +263,27 @@ async def async_apply_suggestion(hass: HomeAssistant, suggestion_uuid: str) -> b return await hassio.send_command(command, timeout=None) +@api_data +async def async_get_green_settings(hass: HomeAssistant) -> dict[str, bool]: + """Return settings specific to Home Assistant Green.""" + hassio: HassIO = hass.data[DOMAIN] + return await hassio.send_command("/os/boards/green", method="get") + + +@api_data +async def async_set_green_settings( + hass: HomeAssistant, settings: dict[str, bool] +) -> dict: + """Set settings specific to Home Assistant Green. + + Returns an empty dict. + """ + hassio: HassIO = hass.data[DOMAIN] + return await hassio.send_command( + "/os/boards/green", method="post", payload=settings + ) + + @api_data async def async_get_yellow_settings(hass: HomeAssistant) -> dict[str, bool]: """Return settings specific to Home Assistant Yellow.""" diff --git a/homeassistant/components/homeassistant_green/config_flow.py b/homeassistant/components/homeassistant_green/config_flow.py index 17ba9aacbc5..c3491de430e 100644 --- a/homeassistant/components/homeassistant_green/config_flow.py +++ b/homeassistant/components/homeassistant_green/config_flow.py @@ -1,22 +1,100 @@ """Config flow for the Home Assistant Green integration.""" from __future__ import annotations +import asyncio +import logging from typing import Any -from homeassistant.config_entries import ConfigFlow +import aiohttp +import voluptuous as vol + +from homeassistant.components.hassio import ( + HassioAPIError, + async_get_green_settings, + async_set_green_settings, + is_hassio, +) +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + +STEP_HW_SETTINGS_SCHEMA = vol.Schema( + { + # Sorted to match front panel left to right + vol.Required("power_led"): selector.BooleanSelector(), + vol.Required("activity_led"): selector.BooleanSelector(), + vol.Required("system_health_led"): selector.BooleanSelector(), + } +) + class HomeAssistantGreenConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Home Assistant Green.""" VERSION = 1 + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> HomeAssistantGreenOptionsFlow: + """Return the options flow.""" + return HomeAssistantGreenOptionsFlow() + 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={}) + + +class HomeAssistantGreenOptionsFlow(OptionsFlow): + """Handle an option flow for Home Assistant Green.""" + + _hw_settings: dict[str, bool] | None = None + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if not is_hassio(self.hass): + return self.async_abort(reason="not_hassio") + + return await self.async_step_hardware_settings() + + async def async_step_hardware_settings( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle hardware settings.""" + + if user_input is not None: + if self._hw_settings == user_input: + return self.async_create_entry(data={}) + try: + async with asyncio.timeout(10): + await async_set_green_settings(self.hass, user_input) + except (aiohttp.ClientError, TimeoutError, HassioAPIError) as err: + _LOGGER.warning("Failed to write hardware settings", exc_info=err) + return self.async_abort(reason="write_hw_settings_error") + return self.async_create_entry(data={}) + + try: + async with asyncio.timeout(10): + self._hw_settings: dict[str, bool] = await async_get_green_settings( + self.hass + ) + except (aiohttp.ClientError, TimeoutError, HassioAPIError) as err: + _LOGGER.warning("Failed to read hardware settings", exc_info=err) + return self.async_abort(reason="read_hw_settings_error") + + schema = self.add_suggested_values_to_schema( + STEP_HW_SETTINGS_SCHEMA, self._hw_settings + ) + + return self.async_show_form(step_id="hardware_settings", data_schema=schema) diff --git a/homeassistant/components/homeassistant_green/strings.json b/homeassistant/components/homeassistant_green/strings.json new file mode 100644 index 00000000000..9066ca64e5c --- /dev/null +++ b/homeassistant/components/homeassistant_green/strings.json @@ -0,0 +1,28 @@ +{ + "options": { + "step": { + "hardware_settings": { + "title": "Configure hardware settings", + "data": { + "activity_led": "Green: activity LED", + "power_led": "White: power LED", + "system_health_led": "Yellow: system health LED" + } + }, + "reboot_menu": { + "title": "Reboot required", + "description": "The settings have changed, but the new settings will not take effect until the system is rebooted", + "menu_options": { + "reboot_later": "Reboot manually later", + "reboot_now": "Reboot now" + } + } + }, + "abort": { + "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", + "read_hw_settings_error": "Failed to read hardware settings", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "write_hw_settings_error": "Failed to write hardware settings" + } + } +} diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index d92a5335809..06c726360d9 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -364,6 +364,48 @@ async def test_api_headers( assert received_request.headers[hdrs.CONTENT_TYPE] == "application/octet-stream" +async def test_api_get_green_settings( + hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker +) -> None: + """Test setup with API ping.""" + aioclient_mock.get( + "http://127.0.0.1/os/boards/green", + json={ + "result": "ok", + "data": { + "activity_led": True, + "power_led": True, + "system_health_led": True, + }, + }, + ) + + assert await handler.async_get_green_settings(hass) == { + "activity_led": True, + "power_led": True, + "system_health_led": True, + } + assert aioclient_mock.call_count == 1 + + +async def test_api_set_green_settings( + hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker +) -> None: + """Test setup with API ping.""" + aioclient_mock.post( + "http://127.0.0.1/os/boards/green", + json={"result": "ok", "data": {}}, + ) + + assert ( + await handler.async_set_green_settings( + hass, {"activity_led": True, "power_led": True, "system_health_led": True} + ) + == {} + ) + assert aioclient_mock.call_count == 1 + + async def test_api_get_yellow_settings( hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker ) -> None: diff --git a/tests/components/homeassistant_green/test_config_flow.py b/tests/components/homeassistant_green/test_config_flow.py index 2eb7389af55..84af22509f9 100644 --- a/tests/components/homeassistant_green/test_config_flow.py +++ b/tests/components/homeassistant_green/test_config_flow.py @@ -1,6 +1,8 @@ """Test the Home Assistant Green config flow.""" from unittest.mock import patch +import pytest + from homeassistant.components.homeassistant_green.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -8,6 +10,29 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration +@pytest.fixture(name="get_green_settings") +def mock_get_green_settings(): + """Mock getting green settings.""" + with patch( + "homeassistant.components.homeassistant_green.config_flow.async_get_green_settings", + return_value={ + "activity_led": True, + "power_led": True, + "system_health_led": True, + }, + ) as get_green_settings: + yield get_green_settings + + +@pytest.fixture(name="set_green_settings") +def mock_set_green_settings(): + """Mock setting green settings.""" + with patch( + "homeassistant.components.homeassistant_green.config_flow.async_set_green_settings", + ) as set_green_settings: + yield set_green_settings + + async def test_config_flow(hass: HomeAssistant) -> None: """Test the config flow.""" mock_integration(hass, MockModule("hassio")) @@ -56,3 +81,142 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() + + +async def test_option_flow_non_hassio( + hass: HomeAssistant, +) -> None: + """Test installing the multi pan addon on a Core installation, without hassio.""" + 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.config_flow.is_hassio", + return_value=False, + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_hassio" + + +async def test_option_flow_led_settings( + hass: HomeAssistant, + get_green_settings, + set_green_settings, +) -> None: + """Test updating LED settings.""" + 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) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "hardware_settings" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"activity_led": False, "power_led": False, "system_health_led": False}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + set_green_settings.assert_called_once_with( + hass, {"activity_led": False, "power_led": False, "system_health_led": False} + ) + + +async def test_option_flow_led_settings_unchanged( + hass: HomeAssistant, + get_green_settings, + set_green_settings, +) -> None: + """Test updating LED settings.""" + 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) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "hardware_settings" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"activity_led": True, "power_led": True, "system_health_led": True}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + set_green_settings.assert_not_called() + + +async def test_option_flow_led_settings_fail_1(hass: HomeAssistant) -> None: + """Test updating LED settings.""" + 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.config_flow.async_get_green_settings", + side_effect=TimeoutError, + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "read_hw_settings_error" + + +async def test_option_flow_led_settings_fail_2( + hass: HomeAssistant, get_green_settings +) -> None: + """Test updating LED settings.""" + 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) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "hardware_settings" + + with patch( + "homeassistant.components.homeassistant_green.config_flow.async_set_green_settings", + side_effect=TimeoutError, + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"activity_led": False, "power_led": False, "system_health_led": False}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "write_hw_settings_error"