Allow specifying icons for service sections (#124656)
* Allow specifying icons for service sections * Improve kitchen_sink example
This commit is contained in:
parent
e9830f0835
commit
c772c4a2d5
8 changed files with 157 additions and 6 deletions
|
@ -9,6 +9,8 @@ from __future__ import annotations
|
|||
import datetime
|
||||
from random import random
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.recorder import DOMAIN as RECORDER_DOMAIN, get_instance
|
||||
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
|
||||
from homeassistant.components.recorder.statistics import (
|
||||
|
@ -18,7 +20,7 @@ from homeassistant.components.recorder.statistics import (
|
|||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import Platform, UnitOfEnergy, UnitOfTemperature, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
@ -40,6 +42,15 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
|||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
SCHEMA_SERVICE_TEST_SERVICE_1 = vol.Schema(
|
||||
{
|
||||
vol.Required("field_1"): vol.Coerce(int),
|
||||
vol.Required("field_2"): vol.In(["off", "auto", "cool"]),
|
||||
vol.Optional("field_3"): vol.Coerce(int),
|
||||
vol.Optional("field_4"): vol.In(["forwards", "reverse"]),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the demo environment."""
|
||||
|
@ -48,6 +59,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
DOMAIN, context={"source": SOURCE_IMPORT}, data={}
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def service_handler(call: ServiceCall | None = None) -> None:
|
||||
"""Do nothing."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, "test_service_1", service_handler, SCHEMA_SERVICE_TEST_SERVICE_1
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -7,5 +7,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"test_service_1": {
|
||||
"service": "mdi:flask",
|
||||
"sections": {
|
||||
"advanced_fields": "mdi:test-tube"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
32
homeassistant/components/kitchen_sink/services.yaml
Normal file
32
homeassistant/components/kitchen_sink/services.yaml
Normal file
|
@ -0,0 +1,32 @@
|
|||
test_service_1:
|
||||
fields:
|
||||
field_1:
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 60
|
||||
unit_of_measurement: seconds
|
||||
field_2:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "off"
|
||||
- "auto"
|
||||
- "cool"
|
||||
advanced_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
field_3:
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 24
|
||||
unit_of_measurement: hours
|
||||
field_4:
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "forward"
|
||||
- "reverse"
|
|
@ -71,5 +71,35 @@
|
|||
"title": "This is not a fixable problem",
|
||||
"description": "This issue is never going to give up."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"test_service_1": {
|
||||
"name": "Test service 1",
|
||||
"description": "Fake service for testing",
|
||||
"fields": {
|
||||
"field_1": {
|
||||
"name": "Field 1",
|
||||
"description": "Number of seconds"
|
||||
},
|
||||
"field_2": {
|
||||
"name": "Field 2",
|
||||
"description": "Mode"
|
||||
},
|
||||
"field_3": {
|
||||
"name": "Field 3",
|
||||
"description": "Number of hours"
|
||||
},
|
||||
"field_4": {
|
||||
"name": "Field 4",
|
||||
"description": "Direction"
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"advanced_fields": {
|
||||
"name": "Advanced options",
|
||||
"description": "Some very advanced things"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ from collections.abc import Iterable
|
|||
from functools import lru_cache
|
||||
import logging
|
||||
import pathlib
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.loader import Integration, async_get_integrations
|
||||
|
@ -21,12 +21,34 @@ ICON_CACHE: HassKey[_IconsCache] = HassKey("icon_cache")
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def convert_shorthand_service_icon(
|
||||
value: str | dict[str, str | dict[str, str]],
|
||||
) -> dict[str, str | dict[str, str]]:
|
||||
"""Convert shorthand service icon to dict."""
|
||||
if isinstance(value, str):
|
||||
return {"service": value}
|
||||
return value
|
||||
|
||||
|
||||
def _load_icons_file(
|
||||
icons_file: pathlib.Path,
|
||||
) -> dict[str, Any]:
|
||||
"""Load and parse an icons.json file."""
|
||||
icons = load_json_object(icons_file)
|
||||
if "services" not in icons:
|
||||
return icons
|
||||
services = cast(dict[str, str | dict[str, str | dict[str, str]]], icons["services"])
|
||||
for service, service_icons in services.items():
|
||||
services[service] = convert_shorthand_service_icon(service_icons)
|
||||
return icons
|
||||
|
||||
|
||||
def _load_icons_files(
|
||||
icons_files: dict[str, pathlib.Path],
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
"""Load and parse icons.json files."""
|
||||
return {
|
||||
component: load_json_object(icons_file)
|
||||
component: _load_icons_file(icons_file)
|
||||
for component, icons_file in icons_files.items()
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import voluptuous as vol
|
|||
from voluptuous.humanize import humanize_error
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.icon import convert_shorthand_service_icon
|
||||
|
||||
from .model import Config, Integration
|
||||
from .translations import translation_key_validator
|
||||
|
@ -60,6 +61,22 @@ DATA_ENTRY_ICONS_SCHEMA = vol.Schema(
|
|||
)
|
||||
|
||||
|
||||
SERVICE_ICONS_SCHEMA = cv.schema_with_slug_keys(
|
||||
vol.All(
|
||||
convert_shorthand_service_icon,
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional("service"): icon_value_validator,
|
||||
vol.Optional("sections"): cv.schema_with_slug_keys(
|
||||
icon_value_validator, slug_validator=translation_key_validator
|
||||
),
|
||||
}
|
||||
),
|
||||
),
|
||||
slug_validator=translation_key_validator,
|
||||
)
|
||||
|
||||
|
||||
def icon_schema(integration_type: str, no_entity_platform: bool) -> vol.Schema:
|
||||
"""Create an icon schema."""
|
||||
|
||||
|
@ -91,7 +108,7 @@ def icon_schema(integration_type: str, no_entity_platform: bool) -> vol.Schema:
|
|||
{str: {"fix_flow": DATA_ENTRY_ICONS_SCHEMA}}
|
||||
),
|
||||
vol.Optional("options"): DATA_ENTRY_ICONS_SCHEMA,
|
||||
vol.Optional("services"): state_validator,
|
||||
vol.Optional("services"): SERVICE_ICONS_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ from http import HTTPStatus
|
|||
from unittest.mock import ANY
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.kitchen_sink import DOMAIN
|
||||
from homeassistant.components.recorder import get_instance
|
||||
|
@ -324,3 +325,24 @@ async def test_issues_created(
|
|||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
async def test_service(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test we can call the service."""
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
|
||||
with pytest.raises(vol.error.MultipleInvalid):
|
||||
await hass.services.async_call(DOMAIN, "test_service_1", blocking=True)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN, "test_service_1", {"field_1": 1, "field_2": "auto"}, blocking=True
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"test_service_1",
|
||||
{"field_1": 1, "field_2": "auto", "field_3": 1, "field_4": "forwards"},
|
||||
blocking=True,
|
||||
)
|
||||
|
|
|
@ -101,7 +101,7 @@ async def test_get_icons(hass: HomeAssistant) -> None:
|
|||
# Test services icons are available
|
||||
icons = await icon.async_get_icons(hass, "services")
|
||||
assert len(icons) == 1
|
||||
assert icons["switch"]["turn_off"] == "mdi:toggle-switch-variant-off"
|
||||
assert icons["switch"]["turn_off"] == {"service": "mdi:toggle-switch-variant-off"}
|
||||
|
||||
# Ensure icons file for platform isn't loaded, as that isn't supported
|
||||
icons = await icon.async_get_icons(hass, "entity")
|
||||
|
@ -126,7 +126,7 @@ async def test_get_icons(hass: HomeAssistant) -> None:
|
|||
|
||||
icons = await icon.async_get_icons(hass, "services")
|
||||
assert len(icons) == 2
|
||||
assert icons["test_package"]["enable_god_mode"] == "mdi:shield"
|
||||
assert icons["test_package"]["enable_god_mode"] == {"service": "mdi:shield"}
|
||||
|
||||
# Load another one
|
||||
hass.config.components.add("test_embedded")
|
||||
|
|
Loading…
Add table
Reference in a new issue