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
pull/92075/head
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_info,
async_get_addon_store_info,
async_get_yellow_settings,
async_install_addon,
async_reboot_host,
async_restart_addon,
async_set_addon_options,
async_set_yellow_settings,
async_start_addon,
async_stop_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)
@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:
"""Small API wrapper for Hass.io."""

View File

@ -1,15 +1,37 @@
"""Config flow for the Home Assistant Yellow integration."""
from __future__ import annotations
import logging
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.config_entries import ConfigEntry, ConfigFlow
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import selector
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):
"""Handle a config flow for Home Assistant Yellow."""
@ -35,6 +57,82 @@ class HomeAssistantYellowConfigFlow(ConfigFlow, domain=DOMAIN):
class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler):
"""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(
self,
) -> silabs_multiprotocol_addon.SerialPortSettings:

View File

@ -11,9 +11,31 @@
"addon_installed_other_device": {
"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": {
"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": {
"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%]"
@ -31,6 +53,8 @@
"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%]",
"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%]"
},
"progress": {

View File

@ -7,7 +7,9 @@ import aiohttp
from aiohttp import hdrs, web
import pytest
from homeassistant.components.hassio import handler
from homeassistant.components.hassio.handler import HassIO, HassioAPIError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
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"
else:
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."""
from unittest.mock import Mock, patch
import pytest
from homeassistant.components.homeassistant_yellow.const import DOMAIN
from homeassistant.components.zha.core.const import DOMAIN as ZHA_DOMAIN
from homeassistant.core import HomeAssistant
@ -9,6 +11,34 @@ from homeassistant.data_entry_flow import FlowResultType
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:
"""Test the config flow."""
mock_integration(hass, MockModule("hassio"))
@ -79,11 +109,17 @@ async def test_option_flow_install_multi_pan_addon(
)
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(
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
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["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)
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == FlowResultType.MENU
with patch(
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
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["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"])
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"