Add LED settings support to Home Assistant Yellow (#86451)

* Add LED control support to Home Assistant Yellow

* Fix the handlers

* Remove switch platform

* Allow configuring LED settings from the options flow

* Add missing translations

* Add tests

* Add tests
This commit is contained in:
Erik Montnemery 2023-04-26 17:02:52 +02:00 committed by GitHub
parent 64e4414a5e
commit ce99319ea5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 406 additions and 2 deletions

View file

@ -85,9 +85,12 @@ from .handler import ( # noqa: F401
async_get_addon_discovery_info, async_get_addon_discovery_info,
async_get_addon_info, async_get_addon_info,
async_get_addon_store_info, async_get_addon_store_info,
async_get_yellow_settings,
async_install_addon, async_install_addon,
async_reboot_host,
async_restart_addon, async_restart_addon,
async_set_addon_options, async_set_addon_options,
async_set_yellow_settings,
async_start_addon, async_start_addon,
async_stop_addon, async_stop_addon,
async_uninstall_addon, async_uninstall_addon,

View file

@ -262,6 +262,37 @@ async def async_apply_suggestion(hass: HomeAssistant, suggestion_uuid: str) -> b
return await hassio.send_command(command, timeout=None) return await hassio.send_command(command, timeout=None)
@api_data
async def async_get_yellow_settings(hass: HomeAssistant) -> dict[str, bool]:
"""Return settings specific to Home Assistant Yellow."""
hassio: HassIO = hass.data[DOMAIN]
return await hassio.send_command("/os/boards/yellow", method="get")
@api_data
async def async_set_yellow_settings(
hass: HomeAssistant, settings: dict[str, bool]
) -> dict:
"""Set settings specific to Home Assistant Yellow.
Returns an empty dict.
"""
hassio: HassIO = hass.data[DOMAIN]
return await hassio.send_command(
"/os/boards/yellow", method="post", payload=settings
)
@api_data
async def async_reboot_host(hass: HomeAssistant) -> dict:
"""Reboot the host.
Returns an empty dict.
"""
hassio: HassIO = hass.data[DOMAIN]
return await hassio.send_command("/host/reboot", method="post", timeout=60)
class HassIO: class HassIO:
"""Small API wrapper for Hass.io.""" """Small API wrapper for Hass.io."""

View file

@ -1,15 +1,37 @@
"""Config flow for the Home Assistant Yellow integration.""" """Config flow for the Home Assistant Yellow integration."""
from __future__ import annotations from __future__ import annotations
import logging
from typing import Any from typing import Any
import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant.components.hassio import (
HassioAPIError,
async_get_yellow_settings,
async_reboot_host,
async_set_yellow_settings,
)
from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon
from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import selector
from .const import DOMAIN, ZHA_HW_DISCOVERY_DATA from .const import DOMAIN, ZHA_HW_DISCOVERY_DATA
_LOGGER = logging.getLogger(__name__)
STEP_HW_SETTINGS_SCHEMA = vol.Schema(
{
vol.Required("disk_led"): selector.BooleanSelector(),
vol.Required("heartbeat_led"): selector.BooleanSelector(),
vol.Required("power_led"): selector.BooleanSelector(),
}
)
class HomeAssistantYellowConfigFlow(ConfigFlow, domain=DOMAIN): class HomeAssistantYellowConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Home Assistant Yellow.""" """Handle a config flow for Home Assistant Yellow."""
@ -35,6 +57,82 @@ class HomeAssistantYellowConfigFlow(ConfigFlow, domain=DOMAIN):
class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler): class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler):
"""Handle an option flow for Home Assistant Yellow.""" """Handle an option flow for Home Assistant Yellow."""
_hw_settings: dict[str, bool] | None = None
async def async_step_on_supervisor(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle logic when on Supervisor host."""
return self.async_show_menu(
step_id="main_menu",
menu_options=[
"hardware_settings",
"multipan_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 async_timeout.timeout(10):
await async_set_yellow_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 await self.async_step_confirm_reboot()
try:
async with async_timeout.timeout(10):
self._hw_settings: dict[str, bool] = await async_get_yellow_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)
async def async_step_confirm_reboot(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm reboot host."""
return self.async_show_menu(
step_id="reboot_menu",
menu_options=[
"reboot_now",
"reboot_later",
],
)
async def async_step_reboot_now(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Reboot now."""
await async_reboot_host(self.hass)
return self.async_create_entry(data={})
async def async_step_reboot_later(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Reboot later."""
return self.async_create_entry(data={})
async def async_step_multipan_settings(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle multipan settings."""
return await super().async_step_on_supervisor(user_input)
async def _async_serial_port_settings( async def _async_serial_port_settings(
self, self,
) -> silabs_multiprotocol_addon.SerialPortSettings: ) -> silabs_multiprotocol_addon.SerialPortSettings:

View file

@ -11,9 +11,31 @@
"addon_installed_other_device": { "addon_installed_other_device": {
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_installed_other_device::title%]" "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_installed_other_device::title%]"
}, },
"hardware_settings": {
"title": "Configure hardware settings",
"data": {
"disk_led": "Disk LED",
"heartbeat_led": "Heartbeat LED",
"power_led": "Power LED"
}
},
"install_addon": { "install_addon": {
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]" "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]"
}, },
"main_menu": {
"menu_options": {
"hardware_settings": "[%key:component::homeassistant_yellow::options::step::hardware_settings::title%]",
"multipan_settings": "Configure IEEE 802.15.4 radio multiprotocol support"
}
},
"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"
}
},
"show_revert_guide": { "show_revert_guide": {
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::title%]", "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::title%]",
"description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::description%]" "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::description%]"
@ -31,6 +53,8 @@
"addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]",
"addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]",
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
"read_hw_settings_error": "Failed to read hardware settings",
"write_hw_settings_error": "Failed to write hardware settings",
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]" "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]"
}, },
"progress": { "progress": {

View file

@ -7,7 +7,9 @@ import aiohttp
from aiohttp import hdrs, web from aiohttp import hdrs, web
import pytest import pytest
from homeassistant.components.hassio import handler
from homeassistant.components.hassio.handler import HassIO, HassioAPIError from homeassistant.components.hassio.handler import HassIO, HassioAPIError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from tests.test_util.aiohttp import AiohttpClientMocker from tests.test_util.aiohttp import AiohttpClientMocker
@ -360,3 +362,54 @@ async def test_api_headers(
assert received_request.headers[hdrs.CONTENT_TYPE] == "application/json" assert received_request.headers[hdrs.CONTENT_TYPE] == "application/json"
else: else:
assert received_request.headers[hdrs.CONTENT_TYPE] == "application/octet-stream" assert received_request.headers[hdrs.CONTENT_TYPE] == "application/octet-stream"
async def test_api_get_yellow_settings(
hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test setup with API ping."""
aioclient_mock.get(
"http://127.0.0.1/os/boards/yellow",
json={
"result": "ok",
"data": {"disk_led": True, "heartbeat_led": True, "power_led": True},
},
)
assert await handler.async_get_yellow_settings(hass) == {
"disk_led": True,
"heartbeat_led": True,
"power_led": True,
}
assert aioclient_mock.call_count == 1
async def test_api_set_yellow_settings(
hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test setup with API ping."""
aioclient_mock.post(
"http://127.0.0.1/os/boards/yellow",
json={"result": "ok", "data": {}},
)
assert (
await handler.async_set_yellow_settings(
hass, {"disk_led": True, "heartbeat_led": True, "power_led": True}
)
== {}
)
assert aioclient_mock.call_count == 1
async def test_api_reboot_host(
hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test setup with API ping."""
aioclient_mock.post(
"http://127.0.0.1/host/reboot",
json={"result": "ok", "data": {}},
)
assert await handler.async_reboot_host(hass) == {}
assert aioclient_mock.call_count == 1

View file

@ -1,6 +1,8 @@
"""Test the Home Assistant Yellow config flow.""" """Test the Home Assistant Yellow config flow."""
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest
from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.components.homeassistant_yellow.const import DOMAIN
from homeassistant.components.zha.core.const import DOMAIN as ZHA_DOMAIN from homeassistant.components.zha.core.const import DOMAIN as ZHA_DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -9,6 +11,34 @@ from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry, MockModule, mock_integration from tests.common import MockConfigEntry, MockModule, mock_integration
@pytest.fixture(name="get_yellow_settings")
def mock_get_yellow_settings():
"""Mock getting yellow settings."""
with patch(
"homeassistant.components.homeassistant_yellow.config_flow.async_get_yellow_settings",
return_value={"disk_led": True, "heartbeat_led": True, "power_led": True},
) as get_yellow_settings:
yield get_yellow_settings
@pytest.fixture(name="set_yellow_settings")
def mock_set_yellow_settings():
"""Mock setting yellow settings."""
with patch(
"homeassistant.components.homeassistant_yellow.config_flow.async_set_yellow_settings",
) as set_yellow_settings:
yield set_yellow_settings
@pytest.fixture(name="reboot_host")
def mock_reboot_host():
"""Mock rebooting host."""
with patch(
"homeassistant.components.homeassistant_yellow.config_flow.async_reboot_host",
) as reboot_host:
yield reboot_host
async def test_config_flow(hass: HomeAssistant) -> None: async def test_config_flow(hass: HomeAssistant) -> None:
"""Test the config flow.""" """Test the config flow."""
mock_integration(hass, MockModule("hassio")) mock_integration(hass, MockModule("hassio"))
@ -79,11 +109,17 @@ async def test_option_flow_install_multi_pan_addon(
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == FlowResultType.MENU
with patch( with patch(
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
side_effect=Mock(return_value=True), side_effect=Mock(return_value=True),
): ):
result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_configure(
result["flow_id"],
{"next_step_id": "multipan_settings"},
)
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "addon_not_installed" assert result["step_id"] == "addon_not_installed"
@ -155,11 +191,17 @@ async def test_option_flow_install_multi_pan_addon_zha(
) )
zha_config_entry.add_to_hass(hass) zha_config_entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == FlowResultType.MENU
with patch( with patch(
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
side_effect=Mock(return_value=True), side_effect=Mock(return_value=True),
): ):
result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_configure(
result["flow_id"],
{"next_step_id": "multipan_settings"},
)
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "addon_not_installed" assert result["step_id"] == "addon_not_installed"
@ -210,3 +252,156 @@ async def test_option_flow_install_multi_pan_addon_zha(
result = await hass.config_entries.options.async_configure(result["flow_id"]) result = await hass.config_entries.options.async_configure(result["flow_id"])
assert result["type"] == FlowResultType.CREATE_ENTRY assert result["type"] == FlowResultType.CREATE_ENTRY
@pytest.mark.parametrize(
("reboot_menu_choice", "reboot_calls"),
[("reboot_now", 1), ("reboot_later", 0)],
)
async def test_option_flow_led_settings(
hass: HomeAssistant,
get_yellow_settings,
set_yellow_settings,
reboot_host,
reboot_menu_choice,
reboot_calls,
) -> None:
"""Test updating LED settings."""
mock_integration(hass, MockModule("hassio"))
# Setup the config entry
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={},
title="Home Assistant Yellow",
)
config_entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "main_menu"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{"next_step_id": "hardware_settings"},
)
assert result["type"] == FlowResultType.FORM
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{"disk_led": False, "heartbeat_led": False, "power_led": False},
)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "reboot_menu"
set_yellow_settings.assert_called_once_with(
hass, {"disk_led": False, "heartbeat_led": False, "power_led": False}
)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{"next_step_id": reboot_menu_choice},
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert len(reboot_host.mock_calls) == reboot_calls
async def test_option_flow_led_settings_unchanged(
hass: HomeAssistant,
get_yellow_settings,
set_yellow_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 Yellow",
)
config_entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "main_menu"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{"next_step_id": "hardware_settings"},
)
assert result["type"] == FlowResultType.FORM
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{"disk_led": True, "heartbeat_led": True, "power_led": True},
)
assert result["type"] == FlowResultType.CREATE_ENTRY
set_yellow_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 Yellow",
)
config_entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "main_menu"
with patch(
"homeassistant.components.homeassistant_yellow.config_flow.async_get_yellow_settings",
side_effect=TimeoutError,
):
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{"next_step_id": "hardware_settings"},
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "read_hw_settings_error"
async def test_option_flow_led_settings_fail_2(
hass: HomeAssistant, get_yellow_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 Yellow",
)
config_entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == FlowResultType.MENU
assert result["step_id"] == "main_menu"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{"next_step_id": "hardware_settings"},
)
assert result["type"] == FlowResultType.FORM
with patch(
"homeassistant.components.homeassistant_yellow.config_flow.async_set_yellow_settings",
side_effect=TimeoutError,
):
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{"disk_led": False, "heartbeat_led": False, "power_led": False},
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "write_hw_settings_error"