mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 07:07:28 +00:00
Add icon translations support (#103294)
Co-authored-by: Robert Resch <robert@resch.dev> Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
parent
ed449a5abd
commit
01372024f5
178
homeassistant/components/binary_sensor/icons.json
Normal file
178
homeassistant/components/binary_sensor/icons.json
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
{
|
||||||
|
"entity_component": {
|
||||||
|
"_": {
|
||||||
|
"default": "mdi:radiobox-blank",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:checkbox-marked-circle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"battery": {
|
||||||
|
"default": "mdi:battery",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:battery-outline"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"battery_charging": {
|
||||||
|
"default": "mdi:battery",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:battery-charging"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"carbon_monoxide": {
|
||||||
|
"default": "mdi:smoke-detector",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:smoke-detector-alert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cold": {
|
||||||
|
"default": "mdi:thermometer",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:snowflake"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connectivity": {
|
||||||
|
"default": "mdi:close-network-outline",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:check-network-outline"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"door": {
|
||||||
|
"default": "mdi:door-closed",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:door-open"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"garage_door": {
|
||||||
|
"default": "mdi:garage",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:garage-open"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gas": {
|
||||||
|
"default": "mdi:check-circle",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:alert-circle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"heat": {
|
||||||
|
"default": "mdi:thermometer",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:fire"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
"default": "mdi:brightness-5",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:brightness-7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lock": {
|
||||||
|
"default": "mdi:lock",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:lock-open"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"moisture": {
|
||||||
|
"default": "mdi:water-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:water"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"motion": {
|
||||||
|
"default": "mdi:motion-sensor-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:motion-sensor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"moving": {
|
||||||
|
"default": "mdi:arrow-right",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:octagon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"occupancy": {
|
||||||
|
"default": "mdi:home-outline",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:home"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"opening": {
|
||||||
|
"default": "mdi:square",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:square-outline"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plug": {
|
||||||
|
"default": "mdi:power-plug-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:power-plug"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"power": {
|
||||||
|
"default": "mdi:power-plug-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:power-plug"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"presence": {
|
||||||
|
"default": "mdi:home-outline",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:home"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"problem": {
|
||||||
|
"default": "mdi:check-circle",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:alert-circle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"running": {
|
||||||
|
"default": "mdi:stop",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:play"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"safety": {
|
||||||
|
"default": "mdi:check-circle",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:alert-circle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"smoke": {
|
||||||
|
"default": "mdi:smoke-detector-variant",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:smoke-detector-variant-alert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sound": {
|
||||||
|
"default": "mdi:music-note-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:music-note"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tamper": {
|
||||||
|
"default": "mdi:check-circle",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:alert-circle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"default": "mdi:package",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:package-up"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vibration": {
|
||||||
|
"default": "mdi:crop-portrait",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:vibrate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"default": "mdi:window-closed",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:window-open"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
62
homeassistant/components/demo/icons.json
Normal file
62
homeassistant/components/demo/icons.json
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"climate": {
|
||||||
|
"ubercool": {
|
||||||
|
"state_attributes": {
|
||||||
|
"fan_mode": {
|
||||||
|
"state": {
|
||||||
|
"auto_high": "mdi:fan-auto",
|
||||||
|
"auto_low": "mdi:fan-auto",
|
||||||
|
"on_high": "mdi:fan-chevron-up",
|
||||||
|
"on_low": "mdi:fan-chevron-down"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swing_mode": {
|
||||||
|
"state": {
|
||||||
|
"1": "mdi:numeric-1",
|
||||||
|
"2": "mdi:numeric-2",
|
||||||
|
"3": "mdi:numeric-3",
|
||||||
|
"auto": "mdi:arrow-oscillating",
|
||||||
|
"off": "mdi:arrow-oscillating-off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"volume": {
|
||||||
|
"default": "mdi:volume-high"
|
||||||
|
},
|
||||||
|
"pwm": {
|
||||||
|
"default": "mdi:square-wave"
|
||||||
|
},
|
||||||
|
"range": {
|
||||||
|
"default": "mdi:square-wave"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"select": {
|
||||||
|
"speed": {
|
||||||
|
"state": {
|
||||||
|
"light_speed": "mdi:speedometer-slow",
|
||||||
|
"ludicrous_speed": "mdi:speedometer-medium",
|
||||||
|
"ridiculous_speed": "mdi:speedometer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sensor": {
|
||||||
|
"thermostat_mode": {
|
||||||
|
"state": {
|
||||||
|
"away": "mdi:home-export-outline",
|
||||||
|
"comfort": "mdi:home-account",
|
||||||
|
"eco": "mdi:leaf",
|
||||||
|
"sleep": "mdi:weather-night"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switch": {
|
||||||
|
"air_conditioner": {
|
||||||
|
"default": "mdi:air-conditioner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,7 @@ async def async_setup_entry(
|
|||||||
"volume1",
|
"volume1",
|
||||||
"volume",
|
"volume",
|
||||||
42.0,
|
42.0,
|
||||||
"mdi:volume-high",
|
"volume",
|
||||||
False,
|
False,
|
||||||
mode=NumberMode.SLIDER,
|
mode=NumberMode.SLIDER,
|
||||||
),
|
),
|
||||||
@ -31,7 +31,7 @@ async def async_setup_entry(
|
|||||||
"pwm1",
|
"pwm1",
|
||||||
"PWM 1",
|
"PWM 1",
|
||||||
0.42,
|
0.42,
|
||||||
"mdi:square-wave",
|
"pwm",
|
||||||
False,
|
False,
|
||||||
native_min_value=0.0,
|
native_min_value=0.0,
|
||||||
native_max_value=1.0,
|
native_max_value=1.0,
|
||||||
@ -42,7 +42,7 @@ async def async_setup_entry(
|
|||||||
"large_range",
|
"large_range",
|
||||||
"Large Range",
|
"Large Range",
|
||||||
500,
|
500,
|
||||||
"mdi:square-wave",
|
"range",
|
||||||
False,
|
False,
|
||||||
native_min_value=1,
|
native_min_value=1,
|
||||||
native_max_value=1000,
|
native_max_value=1000,
|
||||||
@ -52,7 +52,7 @@ async def async_setup_entry(
|
|||||||
"small_range",
|
"small_range",
|
||||||
"Small Range",
|
"Small Range",
|
||||||
128,
|
128,
|
||||||
"mdi:square-wave",
|
"range",
|
||||||
False,
|
False,
|
||||||
native_min_value=1,
|
native_min_value=1,
|
||||||
native_max_value=255,
|
native_max_value=255,
|
||||||
@ -62,7 +62,7 @@ async def async_setup_entry(
|
|||||||
"temp1",
|
"temp1",
|
||||||
"Temperature setting",
|
"Temperature setting",
|
||||||
22,
|
22,
|
||||||
"mdi:thermometer",
|
None,
|
||||||
False,
|
False,
|
||||||
device_class=NumberDeviceClass.TEMPERATURE,
|
device_class=NumberDeviceClass.TEMPERATURE,
|
||||||
native_min_value=15.0,
|
native_min_value=15.0,
|
||||||
@ -87,7 +87,7 @@ class DemoNumber(NumberEntity):
|
|||||||
unique_id: str,
|
unique_id: str,
|
||||||
device_name: str,
|
device_name: str,
|
||||||
state: float,
|
state: float,
|
||||||
icon: str,
|
translation_key: str | None,
|
||||||
assumed_state: bool,
|
assumed_state: bool,
|
||||||
*,
|
*,
|
||||||
device_class: NumberDeviceClass | None = None,
|
device_class: NumberDeviceClass | None = None,
|
||||||
@ -100,7 +100,7 @@ class DemoNumber(NumberEntity):
|
|||||||
"""Initialize the Demo Number entity."""
|
"""Initialize the Demo Number entity."""
|
||||||
self._attr_assumed_state = assumed_state
|
self._attr_assumed_state = assumed_state
|
||||||
self._attr_device_class = device_class
|
self._attr_device_class = device_class
|
||||||
self._attr_icon = icon
|
self._attr_translation_key = translation_key
|
||||||
self._attr_mode = mode
|
self._attr_mode = mode
|
||||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||||
self._attr_native_value = state
|
self._attr_native_value = state
|
||||||
|
@ -19,8 +19,8 @@ async def async_setup_entry(
|
|||||||
"""Set up the Demo config entry."""
|
"""Set up the Demo config entry."""
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
DemoRemote("Remote One", False, None),
|
DemoRemote("Remote One", False),
|
||||||
DemoRemote("Remote Two", True, "mdi:remote"),
|
DemoRemote("Remote Two", True),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,11 +30,10 @@ class DemoRemote(RemoteEntity):
|
|||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
|
||||||
def __init__(self, name: str | None, state: bool, icon: str | None) -> None:
|
def __init__(self, name: str | None, state: bool) -> None:
|
||||||
"""Initialize the Demo Remote."""
|
"""Initialize the Demo Remote."""
|
||||||
self._attr_name = name or DEVICE_DEFAULT_NAME
|
self._attr_name = name or DEVICE_DEFAULT_NAME
|
||||||
self._attr_is_on = state
|
self._attr_is_on = state
|
||||||
self._attr_icon = icon
|
|
||||||
self._last_command_sent: str | None = None
|
self._last_command_sent: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -21,7 +21,6 @@ async def async_setup_entry(
|
|||||||
DemoSelect(
|
DemoSelect(
|
||||||
unique_id="speed",
|
unique_id="speed",
|
||||||
device_name="Speed",
|
device_name="Speed",
|
||||||
icon="mdi:speedometer",
|
|
||||||
current_option="ridiculous_speed",
|
current_option="ridiculous_speed",
|
||||||
options=[
|
options=[
|
||||||
"light_speed",
|
"light_speed",
|
||||||
@ -45,7 +44,6 @@ class DemoSelect(SelectEntity):
|
|||||||
self,
|
self,
|
||||||
unique_id: str,
|
unique_id: str,
|
||||||
device_name: str,
|
device_name: str,
|
||||||
icon: str,
|
|
||||||
current_option: str | None,
|
current_option: str | None,
|
||||||
options: list[str],
|
options: list[str],
|
||||||
translation_key: str,
|
translation_key: str,
|
||||||
@ -53,7 +51,6 @@ class DemoSelect(SelectEntity):
|
|||||||
"""Initialize the Demo select entity."""
|
"""Initialize the Demo select entity."""
|
||||||
self._attr_unique_id = unique_id
|
self._attr_unique_id = unique_id
|
||||||
self._attr_current_option = current_option
|
self._attr_current_option = current_option
|
||||||
self._attr_icon = icon
|
|
||||||
self._attr_options = options
|
self._attr_options = options
|
||||||
self._attr_translation_key = translation_key
|
self._attr_translation_key = translation_key
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
|
@ -20,13 +20,13 @@ async def async_setup_entry(
|
|||||||
"""Set up the demo switch platform."""
|
"""Set up the demo switch platform."""
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
DemoSwitch("switch1", "Decorative Lights", True, None, True),
|
DemoSwitch("switch1", "Decorative Lights", True, True),
|
||||||
DemoSwitch(
|
DemoSwitch(
|
||||||
"switch2",
|
"switch2",
|
||||||
"AC",
|
"AC",
|
||||||
False,
|
False,
|
||||||
"mdi:air-conditioner",
|
|
||||||
False,
|
False,
|
||||||
|
translation_key="air_conditioner",
|
||||||
device_class=SwitchDeviceClass.OUTLET,
|
device_class=SwitchDeviceClass.OUTLET,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@ -45,14 +45,14 @@ class DemoSwitch(SwitchEntity):
|
|||||||
unique_id: str,
|
unique_id: str,
|
||||||
device_name: str,
|
device_name: str,
|
||||||
state: bool,
|
state: bool,
|
||||||
icon: str | None,
|
|
||||||
assumed: bool,
|
assumed: bool,
|
||||||
|
translation_key: str | None = None,
|
||||||
device_class: SwitchDeviceClass | None = None,
|
device_class: SwitchDeviceClass | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Demo switch."""
|
"""Initialize the Demo switch."""
|
||||||
self._attr_assumed_state = assumed
|
self._attr_assumed_state = assumed
|
||||||
self._attr_device_class = device_class
|
self._attr_device_class = device_class
|
||||||
self._attr_icon = icon
|
self._attr_translation_key = translation_key
|
||||||
self._attr_is_on = state
|
self._attr_is_on = state
|
||||||
self._attr_unique_id = unique_id
|
self._attr_unique_id = unique_id
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
|
@ -21,20 +21,17 @@ async def async_setup_entry(
|
|||||||
DemoText(
|
DemoText(
|
||||||
unique_id="text",
|
unique_id="text",
|
||||||
device_name="Text",
|
device_name="Text",
|
||||||
icon=None,
|
|
||||||
native_value="Hello world",
|
native_value="Hello world",
|
||||||
),
|
),
|
||||||
DemoText(
|
DemoText(
|
||||||
unique_id="password",
|
unique_id="password",
|
||||||
device_name="Password",
|
device_name="Password",
|
||||||
icon="mdi:text",
|
|
||||||
native_value="Hello world",
|
native_value="Hello world",
|
||||||
mode=TextMode.PASSWORD,
|
mode=TextMode.PASSWORD,
|
||||||
),
|
),
|
||||||
DemoText(
|
DemoText(
|
||||||
unique_id="text_1_to_5_char",
|
unique_id="text_1_to_5_char",
|
||||||
device_name="Text with 1 to 5 characters",
|
device_name="Text with 1 to 5 characters",
|
||||||
icon="mdi:text",
|
|
||||||
native_value="Hello",
|
native_value="Hello",
|
||||||
native_min=1,
|
native_min=1,
|
||||||
native_max=5,
|
native_max=5,
|
||||||
@ -42,7 +39,6 @@ async def async_setup_entry(
|
|||||||
DemoText(
|
DemoText(
|
||||||
unique_id="text_lowercase",
|
unique_id="text_lowercase",
|
||||||
device_name="Text with only lower case characters",
|
device_name="Text with only lower case characters",
|
||||||
icon="mdi:text",
|
|
||||||
native_value="world",
|
native_value="world",
|
||||||
pattern=r"[a-z]+",
|
pattern=r"[a-z]+",
|
||||||
),
|
),
|
||||||
@ -61,7 +57,6 @@ class DemoText(TextEntity):
|
|||||||
self,
|
self,
|
||||||
unique_id: str,
|
unique_id: str,
|
||||||
device_name: str,
|
device_name: str,
|
||||||
icon: str | None,
|
|
||||||
native_value: str | None,
|
native_value: str | None,
|
||||||
mode: TextMode = TextMode.TEXT,
|
mode: TextMode = TextMode.TEXT,
|
||||||
native_max: int | None = None,
|
native_max: int | None = None,
|
||||||
@ -71,7 +66,6 @@ class DemoText(TextEntity):
|
|||||||
"""Initialize the Demo text entity."""
|
"""Initialize the Demo text entity."""
|
||||||
self._attr_unique_id = unique_id
|
self._attr_unique_id = unique_id
|
||||||
self._attr_native_value = native_value
|
self._attr_native_value = native_value
|
||||||
self._attr_icon = icon
|
|
||||||
self._attr_mode = mode
|
self._attr_mode = mode
|
||||||
if native_max is not None:
|
if native_max is not None:
|
||||||
self._attr_native_max = native_max
|
self._attr_native_max = native_max
|
||||||
|
@ -18,7 +18,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the demo time platform."""
|
"""Set up the demo time platform."""
|
||||||
async_add_entities([DemoTime("time", "Time", time(12, 0, 0), "mdi:clock", False)])
|
async_add_entities([DemoTime("time", "Time", time(12, 0, 0), False)])
|
||||||
|
|
||||||
|
|
||||||
class DemoTime(TimeEntity):
|
class DemoTime(TimeEntity):
|
||||||
@ -33,12 +33,10 @@ class DemoTime(TimeEntity):
|
|||||||
unique_id: str,
|
unique_id: str,
|
||||||
device_name: str,
|
device_name: str,
|
||||||
state: time,
|
state: time,
|
||||||
icon: str,
|
|
||||||
assumed_state: bool,
|
assumed_state: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Demo time entity."""
|
"""Initialize the Demo time entity."""
|
||||||
self._attr_assumed_state = assumed_state
|
self._attr_assumed_state = assumed_state
|
||||||
self._attr_icon = icon
|
|
||||||
self._attr_native_value = state
|
self._attr_native_value = state
|
||||||
self._attr_unique_id = unique_id
|
self._attr_unique_id = unique_id
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||||
from homeassistant.helpers import service
|
from homeassistant.helpers import service
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.icon import async_get_icons
|
||||||
from homeassistant.helpers.json import json_dumps_sorted
|
from homeassistant.helpers.json import json_dumps_sorted
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.helpers.translation import async_get_translations
|
from homeassistant.helpers.translation import async_get_translations
|
||||||
@ -344,6 +345,7 @@ def _frontend_root(dev_repo_path: str | None) -> pathlib.Path:
|
|||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the serving of the frontend."""
|
"""Set up the serving of the frontend."""
|
||||||
await async_setup_frontend_storage(hass)
|
await async_setup_frontend_storage(hass)
|
||||||
|
websocket_api.async_register_command(hass, websocket_get_icons)
|
||||||
websocket_api.async_register_command(hass, websocket_get_panels)
|
websocket_api.async_register_command(hass, websocket_get_panels)
|
||||||
websocket_api.async_register_command(hass, websocket_get_themes)
|
websocket_api.async_register_command(hass, websocket_get_themes)
|
||||||
websocket_api.async_register_command(hass, websocket_get_translations)
|
websocket_api.async_register_command(hass, websocket_get_translations)
|
||||||
@ -647,6 +649,28 @@ class ManifestJSONView(HomeAssistantView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
"type": "frontend/get_icons",
|
||||||
|
vol.Required("category"): vol.In({"entity", "entity_component", "services"}),
|
||||||
|
vol.Optional("integration"): vol.All(cv.ensure_list, [str]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def websocket_get_icons(
|
||||||
|
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Handle get icons command."""
|
||||||
|
resources = await async_get_icons(
|
||||||
|
hass,
|
||||||
|
msg["category"],
|
||||||
|
msg.get("integration"),
|
||||||
|
)
|
||||||
|
connection.send_message(
|
||||||
|
websocket_api.result_message(msg["id"], {"resources": resources})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@websocket_api.websocket_command({"type": "get_panels"})
|
@websocket_api.websocket_command({"type": "get_panels"})
|
||||||
def websocket_get_panels(
|
def websocket_get_panels(
|
||||||
|
24
homeassistant/components/switch/icons.json
Normal file
24
homeassistant/components/switch/icons.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"entity_component": {
|
||||||
|
"_": {
|
||||||
|
"default": "mdi:toggle-switch-variant"
|
||||||
|
},
|
||||||
|
"switch": {
|
||||||
|
"default": "mdi:toggle-switch-variant",
|
||||||
|
"state": {
|
||||||
|
"off": "mdi:toggle-switch-variant-off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outlet": {
|
||||||
|
"default": "mdi:power-plug",
|
||||||
|
"state": {
|
||||||
|
"off": "mdi:power-plug-off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"toggle": "mdi:toggle-switch-variant",
|
||||||
|
"turn_off": "mdi:toggle-switch-variant-off",
|
||||||
|
"turn_on": "mdi:toggle-switch-variant"
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,165 @@
|
|||||||
"""Icon helper methods."""
|
"""Icon helper methods."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from collections.abc import Iterable
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.loader import Integration, async_get_integrations
|
||||||
|
from homeassistant.util.json import load_json_object
|
||||||
|
|
||||||
|
from .translation import build_resources
|
||||||
|
|
||||||
|
ICON_LOAD_LOCK = "icon_load_lock"
|
||||||
|
ICON_CACHE = "icon_cache"
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _component_icons_path(component: str, integration: Integration) -> str | None:
|
||||||
|
"""Return the icons json file location for a component.
|
||||||
|
|
||||||
|
Ex: components/hue/icons.json
|
||||||
|
If component is just a single file, will return None.
|
||||||
|
"""
|
||||||
|
domain = component.rpartition(".")[-1]
|
||||||
|
|
||||||
|
# If it's a component that is just one file, we don't support icons
|
||||||
|
# Example custom_components/my_component.py
|
||||||
|
if integration.file_path.name != domain:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return str(integration.file_path / "icons.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_icons_files(icons_files: dict[str, str]) -> dict[str, dict[str, Any]]:
|
||||||
|
"""Load and parse icons.json files."""
|
||||||
|
return {
|
||||||
|
component: load_json_object(icons_file)
|
||||||
|
for component, icons_file in icons_files.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_get_component_icons(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
components: set[str],
|
||||||
|
integrations: dict[str, Integration],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Load icons."""
|
||||||
|
icons: dict[str, Any] = {}
|
||||||
|
|
||||||
|
# Determine files to load
|
||||||
|
files_to_load = {}
|
||||||
|
for loaded in components:
|
||||||
|
domain = loaded.rpartition(".")[-1]
|
||||||
|
if (path := _component_icons_path(loaded, integrations[domain])) is None:
|
||||||
|
icons[loaded] = {}
|
||||||
|
else:
|
||||||
|
files_to_load[loaded] = path
|
||||||
|
|
||||||
|
# Load files
|
||||||
|
if files_to_load and (
|
||||||
|
load_icons_job := hass.async_add_executor_job(_load_icons_files, files_to_load)
|
||||||
|
):
|
||||||
|
icons |= await load_icons_job
|
||||||
|
|
||||||
|
return icons
|
||||||
|
|
||||||
|
|
||||||
|
class _IconsCache:
|
||||||
|
"""Cache for icons."""
|
||||||
|
|
||||||
|
__slots__ = ("_hass", "_loaded", "_cache")
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize the cache."""
|
||||||
|
self._hass = hass
|
||||||
|
self._loaded: set[str] = set()
|
||||||
|
self._cache: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
|
async def async_fetch(
|
||||||
|
self,
|
||||||
|
category: str,
|
||||||
|
components: set[str],
|
||||||
|
) -> dict[str, dict[str, Any]]:
|
||||||
|
"""Load resources into the cache."""
|
||||||
|
if components_to_load := components - self._loaded:
|
||||||
|
await self._async_load(components_to_load)
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: result
|
||||||
|
for component in components
|
||||||
|
if (result := self._cache.get(category, {}).get(component))
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _async_load(self, components: set[str]) -> None:
|
||||||
|
"""Populate the cache for a given set of components."""
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Cache miss for: %s",
|
||||||
|
", ".join(components),
|
||||||
|
)
|
||||||
|
|
||||||
|
integrations: dict[str, Integration] = {}
|
||||||
|
domains = list({loaded.rpartition(".")[-1] for loaded in components})
|
||||||
|
ints_or_excs = await async_get_integrations(self._hass, domains)
|
||||||
|
for domain, int_or_exc in ints_or_excs.items():
|
||||||
|
if isinstance(int_or_exc, Exception):
|
||||||
|
raise int_or_exc
|
||||||
|
integrations[domain] = int_or_exc
|
||||||
|
|
||||||
|
icons = await _async_get_component_icons(self._hass, components, integrations)
|
||||||
|
|
||||||
|
self._build_category_cache(components, icons)
|
||||||
|
self._loaded.update(components)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _build_category_cache(
|
||||||
|
self,
|
||||||
|
components: set[str],
|
||||||
|
icons: dict[str, dict[str, Any]],
|
||||||
|
) -> None:
|
||||||
|
"""Extract resources into the cache."""
|
||||||
|
resource: dict[str, Any] | str
|
||||||
|
categories: set[str] = set()
|
||||||
|
for resource in icons.values():
|
||||||
|
categories.update(resource)
|
||||||
|
|
||||||
|
for category in categories:
|
||||||
|
new_resources = build_resources(icons, components, category)
|
||||||
|
for component, resource in new_resources.items():
|
||||||
|
category_cache: dict[str, Any] = self._cache.setdefault(category, {})
|
||||||
|
category_cache[component] = resource
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_icons(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
category: str,
|
||||||
|
integrations: Iterable[str] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return all icons of integrations.
|
||||||
|
|
||||||
|
If integration specified, load it for that one; otherwise default to loaded
|
||||||
|
intgrations.
|
||||||
|
"""
|
||||||
|
lock = hass.data.setdefault(ICON_LOAD_LOCK, asyncio.Lock())
|
||||||
|
|
||||||
|
if integrations:
|
||||||
|
components = set(integrations)
|
||||||
|
else:
|
||||||
|
components = {
|
||||||
|
component for component in hass.config.components if "." not in component
|
||||||
|
}
|
||||||
|
async with lock:
|
||||||
|
if ICON_CACHE in hass.data:
|
||||||
|
cache: _IconsCache = hass.data[ICON_CACHE]
|
||||||
|
else:
|
||||||
|
cache = hass.data[ICON_CACHE] = _IconsCache(hass)
|
||||||
|
|
||||||
|
return await cache.async_fetch(category, components)
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
|
@ -129,7 +129,7 @@ def _merge_resources(
|
|||||||
return resources
|
return resources
|
||||||
|
|
||||||
|
|
||||||
def _build_resources(
|
def build_resources(
|
||||||
translation_strings: dict[str, dict[str, Any]],
|
translation_strings: dict[str, dict[str, Any]],
|
||||||
components: set[str],
|
components: set[str],
|
||||||
category: str,
|
category: str,
|
||||||
@ -304,7 +304,7 @@ class _TranslationCache:
|
|||||||
translation_strings, components, category
|
translation_strings, components, category
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
new_resources = _build_resources(
|
new_resources = build_resources(
|
||||||
translation_strings, components, category
|
translation_strings, components, category
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ from . import (
|
|||||||
dependencies,
|
dependencies,
|
||||||
dhcp,
|
dhcp,
|
||||||
docker,
|
docker,
|
||||||
|
icons,
|
||||||
json,
|
json,
|
||||||
manifest,
|
manifest,
|
||||||
metadata,
|
metadata,
|
||||||
@ -38,6 +39,7 @@ INTEGRATION_PLUGINS = [
|
|||||||
config_schema,
|
config_schema,
|
||||||
dependencies,
|
dependencies,
|
||||||
dhcp,
|
dhcp,
|
||||||
|
icons,
|
||||||
json,
|
json,
|
||||||
manifest,
|
manifest,
|
||||||
mqtt,
|
mqtt,
|
||||||
|
112
script/hassfest/icons.py
Normal file
112
script/hassfest/icons.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
"""Validate integration icon translation files."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
import voluptuous as vol
|
||||||
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
from .model import Config, Integration
|
||||||
|
from .translations import translation_key_validator
|
||||||
|
|
||||||
|
|
||||||
|
def icon_value_validator(value: Any) -> str:
|
||||||
|
"""Validate that the icon is a valid icon."""
|
||||||
|
value = cv.string_with_no_html(value)
|
||||||
|
if not value.startswith("mdi:"):
|
||||||
|
raise vol.Invalid(
|
||||||
|
"The icon needs to be a valid icon from Material Design Icons and start with `mdi:`"
|
||||||
|
)
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def require_default_icon_validator(value: dict) -> dict:
|
||||||
|
"""Validate that a default icon is set."""
|
||||||
|
if "_" not in value:
|
||||||
|
raise vol.Invalid(
|
||||||
|
"An entity component needs to have a default icon defined with `_`"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def icon_schema(integration_type: str) -> vol.Schema:
|
||||||
|
"""Create a icon schema."""
|
||||||
|
|
||||||
|
state_validator = cv.schema_with_slug_keys(
|
||||||
|
icon_value_validator,
|
||||||
|
slug_validator=translation_key_validator,
|
||||||
|
)
|
||||||
|
|
||||||
|
def icon_schema_slug(marker: type[vol.Marker]) -> dict[vol.Marker, Any]:
|
||||||
|
return {
|
||||||
|
marker("default"): icon_value_validator,
|
||||||
|
vol.Optional("state"): state_validator,
|
||||||
|
vol.Optional("state_attributes"): cv.schema_with_slug_keys(
|
||||||
|
{
|
||||||
|
marker("default"): icon_value_validator,
|
||||||
|
marker("state"): state_validator,
|
||||||
|
},
|
||||||
|
slug_validator=translation_key_validator,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
base_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional("services"): state_validator,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if integration_type == "entity":
|
||||||
|
return base_schema.extend(
|
||||||
|
{
|
||||||
|
vol.Required("entity_component"): vol.All(
|
||||||
|
cv.schema_with_slug_keys(
|
||||||
|
icon_schema_slug(vol.Required),
|
||||||
|
slug_validator=vol.Any("_", cv.slug),
|
||||||
|
),
|
||||||
|
require_default_icon_validator,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return base_schema.extend(
|
||||||
|
{
|
||||||
|
vol.Required("entity"): cv.schema_with_slug_keys(
|
||||||
|
cv.schema_with_slug_keys(
|
||||||
|
icon_schema_slug(vol.Optional),
|
||||||
|
slug_validator=translation_key_validator,
|
||||||
|
),
|
||||||
|
slug_validator=cv.slug,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_icon_file(config: Config, integration: Integration) -> None: # noqa: C901
|
||||||
|
"""Validate icon file for integration."""
|
||||||
|
icons_file = integration.path / "icons.json"
|
||||||
|
if not icons_file.is_file():
|
||||||
|
return
|
||||||
|
|
||||||
|
name = str(icons_file.relative_to(integration.path))
|
||||||
|
|
||||||
|
try:
|
||||||
|
icons = orjson.loads(icons_file.read_text())
|
||||||
|
except ValueError as err:
|
||||||
|
integration.add_error("icons", f"Invalid JSON in {name}: {err}")
|
||||||
|
return
|
||||||
|
|
||||||
|
schema = icon_schema(integration.integration_type)
|
||||||
|
|
||||||
|
try:
|
||||||
|
schema(icons)
|
||||||
|
except vol.Invalid as err:
|
||||||
|
integration.add_error("icons", f"Invalid {name}: {humanize_error(icons, err)}")
|
||||||
|
|
||||||
|
|
||||||
|
def validate(integrations: dict[str, Integration], config: Config) -> None:
|
||||||
|
"""Handle JSON files inside integrations."""
|
||||||
|
for integration in integrations.values():
|
||||||
|
validate_icon_file(config, integration)
|
@ -23,7 +23,7 @@ from homeassistant.setup import async_setup_component
|
|||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from tests.common import MockUser, async_capture_events, async_fire_time_changed
|
from tests.common import MockUser, async_capture_events, async_fire_time_changed
|
||||||
from tests.typing import WebSocketGenerator
|
from tests.typing import MockHAClientWebSocket, WebSocketGenerator
|
||||||
|
|
||||||
MOCK_THEMES = {
|
MOCK_THEMES = {
|
||||||
"happy": {"primary-color": "red", "app-header-background-color": "blue"},
|
"happy": {"primary-color": "red", "app-header-background-color": "blue"},
|
||||||
@ -664,3 +664,76 @@ async def test_static_path_cache(hass: HomeAssistant, mock_http_client) -> None:
|
|||||||
# and again to make sure the cache works
|
# and again to make sure the cache works
|
||||||
resp = await mock_http_client.get("/static/does-not-exist", allow_redirects=False)
|
resp = await mock_http_client.get("/static/does-not-exist", allow_redirects=False)
|
||||||
assert resp.status == 404
|
assert resp.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_icons(hass: HomeAssistant, ws_client: MockHAClientWebSocket) -> None:
|
||||||
|
"""Test get_icons command."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.frontend.async_get_icons",
|
||||||
|
side_effect=lambda hass, category, integrations: {},
|
||||||
|
):
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "frontend/get_icons",
|
||||||
|
"category": "entity_component",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert msg["id"] == 5
|
||||||
|
assert msg["type"] == TYPE_RESULT
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {"resources": {}}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_icons_for_integrations(
|
||||||
|
hass: HomeAssistant, ws_client: MockHAClientWebSocket
|
||||||
|
) -> None:
|
||||||
|
"""Test get_icons for integrations command."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.frontend.async_get_icons",
|
||||||
|
side_effect=lambda hass, category, integrations: {
|
||||||
|
integration: {} for integration in integrations
|
||||||
|
},
|
||||||
|
):
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "frontend/get_icons",
|
||||||
|
"integration": ["frontend", "http"],
|
||||||
|
"category": "entity",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert msg["id"] == 5
|
||||||
|
assert msg["type"] == TYPE_RESULT
|
||||||
|
assert msg["success"]
|
||||||
|
assert set(msg["result"]["resources"]) == {"frontend", "http"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_icons_for_single_integration(
|
||||||
|
hass: HomeAssistant, ws_client: MockHAClientWebSocket
|
||||||
|
) -> None:
|
||||||
|
"""Test get_icons for integration command."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.frontend.async_get_icons",
|
||||||
|
side_effect=lambda hass, category, integrations: {
|
||||||
|
integration: {} for integration in integrations
|
||||||
|
},
|
||||||
|
):
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "frontend/get_icons",
|
||||||
|
"integration": "http",
|
||||||
|
"category": "entity",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert msg["id"] == 5
|
||||||
|
assert msg["type"] == TYPE_RESULT
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {"resources": {"http": {}}}
|
||||||
|
@ -971,7 +971,6 @@ async def test_device_class_switch(
|
|||||||
None,
|
None,
|
||||||
"Demo Sensor",
|
"Demo Sensor",
|
||||||
state=False,
|
state=False,
|
||||||
icon="mdi:switch",
|
|
||||||
assumed=False,
|
assumed=False,
|
||||||
device_class=device_class,
|
device_class=device_class,
|
||||||
)
|
)
|
||||||
|
@ -1,18 +1,26 @@
|
|||||||
"""Test Home Assistant icon util methods."""
|
"""Test Home Assistant icon util methods."""
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import icon
|
||||||
|
from homeassistant.loader import IntegrationNotFound
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
|
||||||
def test_battery_icon() -> None:
|
def test_battery_icon() -> None:
|
||||||
"""Test icon generator for battery sensor."""
|
"""Test icon generator for battery sensor."""
|
||||||
from homeassistant.helpers.icon import icon_for_battery_level
|
assert icon.icon_for_battery_level(None, True) == "mdi:battery-unknown"
|
||||||
|
assert icon.icon_for_battery_level(None, False) == "mdi:battery-unknown"
|
||||||
|
|
||||||
assert icon_for_battery_level(None, True) == "mdi:battery-unknown"
|
assert icon.icon_for_battery_level(5, True) == "mdi:battery-outline"
|
||||||
assert icon_for_battery_level(None, False) == "mdi:battery-unknown"
|
assert icon.icon_for_battery_level(5, False) == "mdi:battery-alert"
|
||||||
|
|
||||||
assert icon_for_battery_level(5, True) == "mdi:battery-outline"
|
assert icon.icon_for_battery_level(100, True) == "mdi:battery-charging-100"
|
||||||
assert icon_for_battery_level(5, False) == "mdi:battery-alert"
|
assert icon.icon_for_battery_level(100, False) == "mdi:battery"
|
||||||
|
|
||||||
assert icon_for_battery_level(100, True) == "mdi:battery-charging-100"
|
|
||||||
assert icon_for_battery_level(100, False) == "mdi:battery"
|
|
||||||
|
|
||||||
iconbase = "mdi:battery"
|
iconbase = "mdi:battery"
|
||||||
for level in range(0, 100, 5):
|
for level in range(0, 100, 5):
|
||||||
@ -20,8 +28,8 @@ def test_battery_icon() -> None:
|
|||||||
"Level: %d. icon: %s, charging: %s"
|
"Level: %d. icon: %s, charging: %s"
|
||||||
% (
|
% (
|
||||||
level,
|
level,
|
||||||
icon_for_battery_level(level, False),
|
icon.icon_for_battery_level(level, False),
|
||||||
icon_for_battery_level(level, True),
|
icon.icon_for_battery_level(level, True),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if level <= 10:
|
if level <= 10:
|
||||||
@ -42,17 +50,183 @@ def test_battery_icon() -> None:
|
|||||||
postfix = "-alert"
|
postfix = "-alert"
|
||||||
else:
|
else:
|
||||||
postfix = ""
|
postfix = ""
|
||||||
assert iconbase + postfix == icon_for_battery_level(level, False)
|
assert iconbase + postfix == icon.icon_for_battery_level(level, False)
|
||||||
assert iconbase + postfix_charging == icon_for_battery_level(level, True)
|
assert iconbase + postfix_charging == icon.icon_for_battery_level(level, True)
|
||||||
|
|
||||||
|
|
||||||
def test_signal_icon() -> None:
|
def test_signal_icon() -> None:
|
||||||
"""Test icon generator for signal sensor."""
|
"""Test icon generator for signal sensor."""
|
||||||
from homeassistant.helpers.icon import icon_for_signal_level
|
assert icon.icon_for_signal_level(None) == "mdi:signal-cellular-outline"
|
||||||
|
assert icon.icon_for_signal_level(0) == "mdi:signal-cellular-outline"
|
||||||
|
assert icon.icon_for_signal_level(5) == "mdi:signal-cellular-1"
|
||||||
|
assert icon.icon_for_signal_level(40) == "mdi:signal-cellular-2"
|
||||||
|
assert icon.icon_for_signal_level(80) == "mdi:signal-cellular-3"
|
||||||
|
assert icon.icon_for_signal_level(100) == "mdi:signal-cellular-3"
|
||||||
|
|
||||||
assert icon_for_signal_level(None) == "mdi:signal-cellular-outline"
|
|
||||||
assert icon_for_signal_level(0) == "mdi:signal-cellular-outline"
|
def test_load_icons_files(hass: HomeAssistant) -> None:
|
||||||
assert icon_for_signal_level(5) == "mdi:signal-cellular-1"
|
"""Test the load icons files function."""
|
||||||
assert icon_for_signal_level(40) == "mdi:signal-cellular-2"
|
file1 = hass.config.path("custom_components", "test", "icons.json")
|
||||||
assert icon_for_signal_level(80) == "mdi:signal-cellular-3"
|
file2 = hass.config.path("custom_components", "test", "invalid.json")
|
||||||
assert icon_for_signal_level(100) == "mdi:signal-cellular-3"
|
assert icon._load_icons_files({"test": file1, "invalid": file2}) == {
|
||||||
|
"test": {
|
||||||
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"something": {
|
||||||
|
"state": {"away": "mdi:home-outline", "home": "mdi:home"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
||||||
|
async def test_get_icons(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the get icon helper."""
|
||||||
|
icons = await icon.async_get_icons(hass, "entity")
|
||||||
|
assert icons == {}
|
||||||
|
|
||||||
|
icons = await icon.async_get_icons(hass, "entity_component")
|
||||||
|
assert icons == {}
|
||||||
|
|
||||||
|
# Set up test switch component
|
||||||
|
assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}})
|
||||||
|
|
||||||
|
# Test getting icons for the entity component
|
||||||
|
icons = await icon.async_get_icons(hass, "entity_component")
|
||||||
|
assert icons["switch"]["_"]["default"] == "mdi:toggle-switch-variant"
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# Ensure icons file for platform isn't loaded, as that isn't supported
|
||||||
|
icons = await icon.async_get_icons(hass, "entity")
|
||||||
|
assert icons == {}
|
||||||
|
icons = await icon.async_get_icons(hass, "entity", ["test.switch"])
|
||||||
|
assert icons == {}
|
||||||
|
|
||||||
|
# Load up an custom integration
|
||||||
|
hass.config.components.add("test_package")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
icons = await icon.async_get_icons(hass, "entity")
|
||||||
|
assert len(icons) == 1
|
||||||
|
|
||||||
|
assert icons == {
|
||||||
|
"test_package": {
|
||||||
|
"switch": {
|
||||||
|
"something": {"state": {"away": "mdi:home-outline", "home": "mdi:home"}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
icons = await icon.async_get_icons(hass, "services")
|
||||||
|
assert len(icons) == 2
|
||||||
|
assert icons["test_package"]["enable_god_mode"] == "mdi:shield"
|
||||||
|
|
||||||
|
# Load another one
|
||||||
|
hass.config.components.add("test_embedded")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
icons = await icon.async_get_icons(hass, "entity")
|
||||||
|
assert len(icons) == 2
|
||||||
|
|
||||||
|
assert icons["test_package"] == {
|
||||||
|
"switch": {
|
||||||
|
"something": {"state": {"away": "mdi:home-outline", "home": "mdi:home"}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test getting non-existing integration
|
||||||
|
with pytest.raises(
|
||||||
|
IntegrationNotFound, match="Integration 'non_existing' not found"
|
||||||
|
):
|
||||||
|
await icon.async_get_icons(hass, "entity", ["non_existing"])
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_icons_while_loading_components(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the get icons helper loads icons."""
|
||||||
|
integration = Mock(file_path=pathlib.Path(__file__))
|
||||||
|
integration.name = "Component 1"
|
||||||
|
hass.config.components.add("component1")
|
||||||
|
load_count = 0
|
||||||
|
|
||||||
|
def mock_load_icons_files(files):
|
||||||
|
"""Mock load icon files."""
|
||||||
|
nonlocal load_count
|
||||||
|
load_count += 1
|
||||||
|
return {"component1": {"entity": {"climate": {"test": {"icon": "mdi:home"}}}}}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.icon._component_icons_path",
|
||||||
|
return_value="choochoo.json",
|
||||||
|
), patch(
|
||||||
|
"homeassistant.helpers.icon._load_icons_files",
|
||||||
|
mock_load_icons_files,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.helpers.icon.async_get_integrations",
|
||||||
|
return_value={"component1": integration},
|
||||||
|
):
|
||||||
|
times = 5
|
||||||
|
all_icons = [await icon.async_get_icons(hass, "entity") for _ in range(times)]
|
||||||
|
|
||||||
|
assert all_icons == [
|
||||||
|
{"component1": {"climate": {"test": {"icon": "mdi:home"}}}}
|
||||||
|
for _ in range(times)
|
||||||
|
]
|
||||||
|
assert load_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_caching(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we cache data."""
|
||||||
|
hass.config.components.add("binary_sensor")
|
||||||
|
hass.config.components.add("switch")
|
||||||
|
|
||||||
|
# Patch with same method so we can count invocations
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.icon.build_resources",
|
||||||
|
side_effect=icon.build_resources,
|
||||||
|
) as mock_build:
|
||||||
|
load1 = await icon.async_get_icons(hass, "entity_component")
|
||||||
|
assert len(mock_build.mock_calls) == 2
|
||||||
|
|
||||||
|
load2 = await icon.async_get_icons(hass, "entity_component")
|
||||||
|
assert len(mock_build.mock_calls) == 2
|
||||||
|
|
||||||
|
assert load1 == load2
|
||||||
|
|
||||||
|
assert load1["binary_sensor"]
|
||||||
|
assert load1["switch"]
|
||||||
|
|
||||||
|
load_switch_only = await icon.async_get_icons(
|
||||||
|
hass, "entity_component", integrations={"switch"}
|
||||||
|
)
|
||||||
|
assert load_switch_only
|
||||||
|
assert list(load_switch_only) == ["switch"]
|
||||||
|
|
||||||
|
load_binary_sensor_only = await icon.async_get_icons(
|
||||||
|
hass, "entity_component", integrations={"binary_sensor"}
|
||||||
|
)
|
||||||
|
assert load_binary_sensor_only
|
||||||
|
assert list(load_binary_sensor_only) == ["binary_sensor"]
|
||||||
|
|
||||||
|
# Check if new loaded component, trigger load
|
||||||
|
hass.config.components.add("media_player")
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.icon._load_icons_files",
|
||||||
|
side_effect=icon._load_icons_files,
|
||||||
|
) as mock_load:
|
||||||
|
load_sensor_only = await icon.async_get_icons(
|
||||||
|
hass, "entity_component", integrations={"switch"}
|
||||||
|
)
|
||||||
|
assert load_sensor_only
|
||||||
|
assert len(mock_load.mock_calls) == 0
|
||||||
|
|
||||||
|
await icon.async_get_icons(
|
||||||
|
hass, "entity_component", integrations={"media_player"}
|
||||||
|
)
|
||||||
|
assert len(mock_load.mock_calls) == 1
|
||||||
|
@ -478,8 +478,8 @@ async def test_caching(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
# Patch with same method so we can count invocations
|
# Patch with same method so we can count invocations
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.helpers.translation._build_resources",
|
"homeassistant.helpers.translation.build_resources",
|
||||||
side_effect=translation._build_resources,
|
side_effect=translation.build_resources,
|
||||||
) as mock_build:
|
) as mock_build:
|
||||||
load_sensor_only = await translation.async_get_translations(
|
load_sensor_only = await translation.async_get_translations(
|
||||||
hass, "en", "title", integrations={"sensor"}
|
hass, "en", "title", integrations={"sensor"}
|
||||||
|
12
tests/testing_config/custom_components/test/icons.json
Normal file
12
tests/testing_config/custom_components/test/icons.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"something": {
|
||||||
|
"state": {
|
||||||
|
"away": "mdi:home-outline",
|
||||||
|
"home": "mdi:home"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"something": {
|
||||||
|
"state": {
|
||||||
|
"away": "mdi:home-outline",
|
||||||
|
"home": "mdi:home"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"something": {
|
||||||
|
"state": {
|
||||||
|
"away": "mdi:home-outline",
|
||||||
|
"home": "mdi:home"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"enable_god_mode": "mdi:shield"
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user