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 testspull/92075/head
parent
64e4414a5e
commit
ce99319ea5
|
@ -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,
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue