mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 02:07:09 +00:00
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
@ -9,6 +9,8 @@ from __future__ import annotations
|
|||||||
import datetime
|
import datetime
|
||||||
from random import random
|
from random import random
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.recorder import DOMAIN as RECORDER_DOMAIN, get_instance
|
from homeassistant.components.recorder import DOMAIN as RECORDER_DOMAIN, get_instance
|
||||||
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
|
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
|
||||||
from homeassistant.components.recorder.statistics import (
|
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.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import Platform, UnitOfEnergy, UnitOfTemperature, UnitOfVolume
|
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 import config_validation as cv
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
@ -40,6 +42,15 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
|||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
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:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the demo environment."""
|
"""Set up the demo environment."""
|
||||||
@ -48,6 +59,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
DOMAIN, context={"source": SOURCE_IMPORT}, data={}
|
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
|
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",
|
"title": "This is not a fixable problem",
|
||||||
"description": "This issue is never going to give up."
|
"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
|
from functools import lru_cache
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.loader import Integration, async_get_integrations
|
from homeassistant.loader import Integration, async_get_integrations
|
||||||
@ -21,12 +21,34 @@ ICON_CACHE: HassKey[_IconsCache] = HassKey("icon_cache")
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_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(
|
def _load_icons_files(
|
||||||
icons_files: dict[str, pathlib.Path],
|
icons_files: dict[str, pathlib.Path],
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""Load and parse icons.json files."""
|
"""Load and parse icons.json files."""
|
||||||
return {
|
return {
|
||||||
component: load_json_object(icons_file)
|
component: _load_icons_file(icons_file)
|
||||||
for component, icons_file in icons_files.items()
|
for component, icons_file in icons_files.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import voluptuous as vol
|
|||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.icon import convert_shorthand_service_icon
|
||||||
|
|
||||||
from .model import Config, Integration
|
from .model import Config, Integration
|
||||||
from .translations import translation_key_validator
|
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:
|
def icon_schema(integration_type: str, no_entity_platform: bool) -> vol.Schema:
|
||||||
"""Create an icon 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}}
|
{str: {"fix_flow": DATA_ENTRY_ICONS_SCHEMA}}
|
||||||
),
|
),
|
||||||
vol.Optional("options"): 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
|
from unittest.mock import ANY
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.kitchen_sink import DOMAIN
|
from homeassistant.components.kitchen_sink import DOMAIN
|
||||||
from homeassistant.components.recorder import get_instance
|
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
|
# Test services icons are available
|
||||||
icons = await icon.async_get_icons(hass, "services")
|
icons = await icon.async_get_icons(hass, "services")
|
||||||
assert len(icons) == 1
|
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
|
# Ensure icons file for platform isn't loaded, as that isn't supported
|
||||||
icons = await icon.async_get_icons(hass, "entity")
|
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")
|
icons = await icon.async_get_icons(hass, "services")
|
||||||
assert len(icons) == 2
|
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
|
# Load another one
|
||||||
hass.config.components.add("test_embedded")
|
hass.config.components.add("test_embedded")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user