forked from home-assistant/core
Compare commits
2 Commits
schema_obj
...
blueprint-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2dc4ffa76 | ||
|
|
410cbdd99b |
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -108,7 +108,7 @@ jobs:
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: OHF-Voice/intents-package
|
||||
repo: home-assistant/intents-package
|
||||
branch: main
|
||||
workflow: nightly.yaml
|
||||
workflow_conclusion: success
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
"""The blueprint integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_SELECTOR
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.selector import selector as create_selector
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import websocket_api
|
||||
@@ -29,3 +35,61 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the blueprint integration."""
|
||||
websocket_api.async_setup(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_find_relevant_blueprints(
|
||||
hass: HomeAssistant, device_id: str
|
||||
) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Find all blueprints relevant to a specific device."""
|
||||
results = {}
|
||||
entities = [
|
||||
entry
|
||||
for entry in er.async_entries_for_device(er.async_get(hass), device_id)
|
||||
if not entry.entity_category
|
||||
]
|
||||
|
||||
async def all_blueprints_generator(hass: HomeAssistant):
|
||||
"""Yield all blueprints from all domains."""
|
||||
blueprint_domains: dict[str, DomainBlueprints] = hass.data[DOMAIN]
|
||||
for blueprint_domain in blueprint_domains.values():
|
||||
blueprints = await blueprint_domain.async_get_blueprints()
|
||||
for blueprint in blueprints.values():
|
||||
yield blueprint
|
||||
|
||||
async for blueprint in all_blueprints_generator(hass):
|
||||
blueprint_input_matches: dict[str, list[str]] = {}
|
||||
|
||||
for info in blueprint.inputs.values():
|
||||
if (
|
||||
not info
|
||||
or not (selector_conf := info.get(CONF_SELECTOR))
|
||||
or "entity" not in selector_conf
|
||||
):
|
||||
continue
|
||||
|
||||
selector = create_selector(selector_conf)
|
||||
|
||||
matched = []
|
||||
|
||||
for entity in entities:
|
||||
try:
|
||||
entity.entity_id, selector(entity.entity_id)
|
||||
except vol.Invalid:
|
||||
continue
|
||||
|
||||
matched.append(entity.entity_id)
|
||||
|
||||
if matched:
|
||||
blueprint_input_matches[info[CONF_NAME]] = matched
|
||||
|
||||
if not blueprint_input_matches:
|
||||
continue
|
||||
|
||||
results.setdefault(blueprint.domain, []).append(
|
||||
{
|
||||
"blueprint": blueprint,
|
||||
"matched_input": blueprint_input_matches,
|
||||
}
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ovoenergy"],
|
||||
"requirements": ["ovoenergy==2.0.1"]
|
||||
"requirements": ["ovoenergy==2.0.0"]
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"requirements": ["uiprotect==7.14.0", "unifi-discovery==1.2.0"],
|
||||
"requirements": ["uiprotect==7.13.0", "unifi-discovery==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -1113,23 +1113,9 @@ class NumberSelector(Selector[NumberSelectorConfig]):
|
||||
return value
|
||||
|
||||
|
||||
class ObjectSelectorField(TypedDict):
|
||||
"""Class to represent an object selector fields dict."""
|
||||
|
||||
label: str
|
||||
required: bool
|
||||
selector: dict[str, Any]
|
||||
|
||||
|
||||
class ObjectSelectorConfig(BaseSelectorConfig):
|
||||
"""Class to represent an object selector config."""
|
||||
|
||||
fields: dict[str, ObjectSelectorField]
|
||||
multiple: bool
|
||||
label_field: str
|
||||
description_field: bool
|
||||
translation_key: str
|
||||
|
||||
|
||||
@SELECTORS.register("object")
|
||||
class ObjectSelector(Selector[ObjectSelectorConfig]):
|
||||
@@ -1137,21 +1123,7 @@ class ObjectSelector(Selector[ObjectSelectorConfig]):
|
||||
|
||||
selector_type = "object"
|
||||
|
||||
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional("fields"): {
|
||||
str: {
|
||||
vol.Required("selector"): dict,
|
||||
vol.Optional("required"): bool,
|
||||
vol.Optional("label"): str,
|
||||
}
|
||||
},
|
||||
vol.Optional("multiple", default=False): bool,
|
||||
vol.Optional("label_field"): str,
|
||||
vol.Optional("description_field"): str,
|
||||
vol.Optional("translation_key"): str,
|
||||
}
|
||||
)
|
||||
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
|
||||
|
||||
def __init__(self, config: ObjectSelectorConfig | None = None) -> None:
|
||||
"""Instantiate a selector."""
|
||||
|
||||
4
requirements_all.txt
generated
4
requirements_all.txt
generated
@@ -1632,7 +1632,7 @@ orvibo==1.1.2
|
||||
ourgroceries==1.5.4
|
||||
|
||||
# homeassistant.components.ovo_energy
|
||||
ovoenergy==2.0.1
|
||||
ovoenergy==2.0.0
|
||||
|
||||
# homeassistant.components.p1_monitor
|
||||
p1monitor==3.1.0
|
||||
@@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.14.0
|
||||
uiprotect==7.13.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
|
||||
4
requirements_test_all.txt
generated
4
requirements_test_all.txt
generated
@@ -1379,7 +1379,7 @@ oralb-ble==0.17.6
|
||||
ourgroceries==1.5.4
|
||||
|
||||
# homeassistant.components.ovo_energy
|
||||
ovoenergy==2.0.1
|
||||
ovoenergy==2.0.0
|
||||
|
||||
# homeassistant.components.p1_monitor
|
||||
p1monitor==3.1.0
|
||||
@@ -2458,7 +2458,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.14.0
|
||||
uiprotect==7.13.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
|
||||
@@ -245,6 +245,11 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
# opower > arrow > types-python-dateutil
|
||||
"arrow": {"types-python-dateutil"}
|
||||
},
|
||||
"ovo_energy": {
|
||||
# https://github.com/timmo001/ovoenergy/issues/132
|
||||
# ovoenergy > incremental > setuptools
|
||||
"incremental": {"setuptools"}
|
||||
},
|
||||
"pi_hole": {"hole": {"async-timeout"}},
|
||||
"pvpc_hourly_pricing": {"aiopvpc": {"async-timeout"}},
|
||||
"remote_rpi_gpio": {
|
||||
|
||||
@@ -1 +1,58 @@
|
||||
"""Tests for the blueprint init."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components import automation, blueprint
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_find_relevant_blueprints(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test finding relevant blueprints."""
|
||||
config_entry = MockConfigEntry()
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={("test_domain", "test_device")},
|
||||
name="Test Device",
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"person",
|
||||
"test_domain",
|
||||
"test_entity",
|
||||
device_id=device.id,
|
||||
original_name="Test Person",
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
hass.config,
|
||||
"path",
|
||||
return_value=Path(automation.__file__).parent / "blueprints",
|
||||
):
|
||||
automation.async_get_blueprints(hass)
|
||||
results = await blueprint.async_find_relevant_blueprints(hass, device.id)
|
||||
|
||||
for matches in results.values():
|
||||
for match in matches:
|
||||
match["blueprint"] = match["blueprint"].name
|
||||
|
||||
assert results == {
|
||||
"automation": [
|
||||
{
|
||||
"blueprint": "Motion-activated Light",
|
||||
"matched_input": {
|
||||
"Person": [
|
||||
"person.test_domain_test_entity",
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -73,10 +73,6 @@ def _init_host_mock(host_mock: MagicMock) -> None:
|
||||
host_mock.reboot = AsyncMock()
|
||||
host_mock.set_ptz_command = AsyncMock()
|
||||
host_mock.get_motion_state_all_ch = AsyncMock(return_value=False)
|
||||
host_mock.get_stream_source = AsyncMock()
|
||||
host_mock.get_snapshot = AsyncMock()
|
||||
host_mock.get_encoding = AsyncMock(return_value="h264")
|
||||
host_mock.ONVIF_event_callback = AsyncMock()
|
||||
host_mock.is_nvr = True
|
||||
host_mock.is_hub = False
|
||||
host_mock.mac_address = TEST_MAC
|
||||
@@ -109,6 +105,7 @@ def _init_host_mock(host_mock: MagicMock) -> None:
|
||||
host_mock.camera_uid.return_value = TEST_UID_CAM
|
||||
host_mock.camera_online.return_value = True
|
||||
host_mock.channel_for_uid.return_value = 0
|
||||
host_mock.get_encoding.return_value = "h264"
|
||||
host_mock.firmware_update_available.return_value = False
|
||||
host_mock.session_active = True
|
||||
host_mock.timeout = 60
|
||||
|
||||
@@ -21,11 +21,11 @@ async def test_motion_sensor(
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_host: MagicMock,
|
||||
reolink_connect: MagicMock,
|
||||
) -> None:
|
||||
"""Test binary sensor entity with motion sensor."""
|
||||
reolink_host.model = TEST_DUO_MODEL
|
||||
reolink_host.motion_detected.return_value = True
|
||||
reolink_connect.model = TEST_DUO_MODEL
|
||||
reolink_connect.motion_detected.return_value = True
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -34,7 +34,7 @@ async def test_motion_sensor(
|
||||
entity_id = f"{Platform.BINARY_SENSOR}.{TEST_NVR_NAME}_motion_lens_0"
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
|
||||
reolink_host.motion_detected.return_value = False
|
||||
reolink_connect.motion_detected.return_value = False
|
||||
freezer.tick(DEVICE_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
@@ -42,8 +42,8 @@ async def test_motion_sensor(
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
|
||||
# test ONVIF webhook callback
|
||||
reolink_host.motion_detected.return_value = True
|
||||
reolink_host.ONVIF_event_callback.return_value = [0]
|
||||
reolink_connect.motion_detected.return_value = True
|
||||
reolink_connect.ONVIF_event_callback.return_value = [0]
|
||||
webhook_id = config_entry.runtime_data.host.webhook_id
|
||||
client = await hass_client_no_auth()
|
||||
await client.post(f"/api/webhook/{webhook_id}", data="test_data")
|
||||
@@ -56,11 +56,11 @@ async def test_smart_ai_sensor(
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_host: MagicMock,
|
||||
reolink_connect: MagicMock,
|
||||
) -> None:
|
||||
"""Test smart ai binary sensor entity."""
|
||||
reolink_host.model = TEST_HOST_MODEL
|
||||
reolink_host.baichuan.smart_ai_state.return_value = True
|
||||
reolink_connect.model = TEST_HOST_MODEL
|
||||
reolink_connect.baichuan.smart_ai_state.return_value = True
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -69,7 +69,7 @@ async def test_smart_ai_sensor(
|
||||
entity_id = f"{Platform.BINARY_SENSOR}.{TEST_NVR_NAME}_crossline_zone1_person"
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
|
||||
reolink_host.baichuan.smart_ai_state.return_value = False
|
||||
reolink_connect.baichuan.smart_ai_state.return_value = False
|
||||
freezer.tick(DEVICE_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
@@ -80,7 +80,7 @@ async def test_smart_ai_sensor(
|
||||
async def test_tcp_callback(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_host: MagicMock,
|
||||
reolink_connect: MagicMock,
|
||||
) -> None:
|
||||
"""Test tcp callback using motion sensor."""
|
||||
|
||||
@@ -95,11 +95,11 @@ async def test_tcp_callback(
|
||||
|
||||
callback_mock = callback_mock_class()
|
||||
|
||||
reolink_host.model = TEST_HOST_MODEL
|
||||
reolink_host.baichuan.events_active = True
|
||||
reolink_host.baichuan.subscribe_events.reset_mock(side_effect=True)
|
||||
reolink_host.baichuan.register_callback = callback_mock.register_callback
|
||||
reolink_host.motion_detected.return_value = True
|
||||
reolink_connect.model = TEST_HOST_MODEL
|
||||
reolink_connect.baichuan.events_active = True
|
||||
reolink_connect.baichuan.subscribe_events.reset_mock(side_effect=True)
|
||||
reolink_connect.baichuan.register_callback = callback_mock.register_callback
|
||||
reolink_connect.motion_detected.return_value = True
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@@ -110,7 +110,7 @@ async def test_tcp_callback(
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
|
||||
# simulate a TCP push callback
|
||||
reolink_host.motion_detected.return_value = False
|
||||
reolink_connect.motion_detected.return_value = False
|
||||
assert callback_mock.callback_func is not None
|
||||
callback_mock.callback_func()
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ async def test_camera(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_host: MagicMock,
|
||||
reolink_connect: MagicMock,
|
||||
) -> None:
|
||||
"""Test camera entity with fluent."""
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
|
||||
@@ -37,26 +37,28 @@ async def test_camera(
|
||||
assert hass.states.get(entity_id).state == CameraState.IDLE
|
||||
|
||||
# check getting a image from the camera
|
||||
reolink_host.get_snapshot.return_value = b"image"
|
||||
reolink_connect.get_snapshot.return_value = b"image"
|
||||
assert (await async_get_image(hass, entity_id)).content == b"image"
|
||||
|
||||
reolink_host.get_snapshot.side_effect = ReolinkError("Test error")
|
||||
reolink_connect.get_snapshot.side_effect = ReolinkError("Test error")
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await async_get_image(hass, entity_id)
|
||||
|
||||
# check getting the stream source
|
||||
assert await async_get_stream_source(hass, entity_id) is not None
|
||||
|
||||
reolink_connect.get_snapshot.reset_mock(side_effect=True)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_camera_no_stream_source(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_host: MagicMock,
|
||||
reolink_connect: MagicMock,
|
||||
) -> None:
|
||||
"""Test camera entity with no stream source."""
|
||||
reolink_host.model = TEST_DUO_MODEL
|
||||
reolink_host.get_stream_source.return_value = None
|
||||
reolink_connect.model = TEST_DUO_MODEL
|
||||
reolink_connect.get_stream_source.return_value = None
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
@@ -15,8 +15,8 @@ from tests.typing import ClientSessionGenerator
|
||||
async def test_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
reolink_host: MagicMock,
|
||||
reolink_chime: Chime,
|
||||
reolink_connect: MagicMock,
|
||||
test_chime: Chime,
|
||||
config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
|
||||
@@ -590,28 +590,7 @@ def test_action_selector_schema(schema, valid_selections, invalid_selections) ->
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("schema", "valid_selections", "invalid_selections"),
|
||||
[
|
||||
({}, ("abc123",), ()),
|
||||
(
|
||||
{
|
||||
"fields": {
|
||||
"name": {
|
||||
"required": True,
|
||||
"selector": {"text": {}},
|
||||
},
|
||||
"percentage": {
|
||||
"selector": {"number": {}},
|
||||
},
|
||||
},
|
||||
"multiple": True,
|
||||
"label_field": "name",
|
||||
"description_field": "percentage",
|
||||
},
|
||||
(),
|
||||
(),
|
||||
),
|
||||
],
|
||||
[],
|
||||
[({}, ("abc123",), ())],
|
||||
)
|
||||
def test_object_selector_schema(schema, valid_selections, invalid_selections) -> None:
|
||||
"""Test object selector."""
|
||||
|
||||
Reference in New Issue
Block a user