Add LED control support to Home Assistant Green (#100922)

* Add LED control support to Home Assistant Green

* Add strings.json

* Sort alphabetically

* Reorder LED schema

* Improve test coverage

* Apply suggestions from code review

Co-authored-by: Stefan Agner <stefan@agner.ch>

* Sort + fix test

* Remove reboot menu

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
This commit is contained in:
Erik Montnemery 2023-09-28 17:45:10 +02:00 committed by GitHub
parent d8520088e7
commit dc78d15abc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 336 additions and 1 deletions

View file

@ -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,

View file

@ -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."""

View file

@ -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)

View file

@ -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"
}
}
}

View file

@ -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:

View file

@ -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"