mirror of
https://github.com/home-assistant/core.git
synced 2026-09-21 01:03:55 +01:00
Fix Hue MotionAware entities for zones covering the whole home (#182586)
This commit is contained in:
@@ -28,12 +28,12 @@ from homeassistant.components.switch import (
|
||||
from homeassistant.const import EntityCategory, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .bridge import HueBridge, HueConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .v2.entity import HueBaseEntity
|
||||
from .v2.helpers import get_motion_area_device_info
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -229,11 +229,8 @@ class HueMotionAreaConfigurationEnabledEntity(HueResourceEnabledEntity):
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(bridge, controller, resource)
|
||||
# link the switch to the group the MotionAware zone is associated with
|
||||
self.hue_group = controller.get_group(resource.id)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.hue_group.id)},
|
||||
)
|
||||
# link the switch to the room or zone the MotionAware zone is associated with
|
||||
self._attr_device_info = get_motion_area_device_info(bridge.api, resource)
|
||||
|
||||
|
||||
class HueMotionSensorEnabledEntity(HueResourceEnabledEntity):
|
||||
|
||||
@@ -41,6 +41,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from ..bridge import HueBridge, HueConfigEntry
|
||||
from ..const import DOMAIN
|
||||
from .entity import HueBaseEntity
|
||||
from .helpers import get_motion_area_device_info
|
||||
|
||||
type SensorType = (
|
||||
CameraMotion
|
||||
@@ -230,14 +231,12 @@ class HueMotionAwareSensor(HueMotionSensor):
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(bridge, controller, resource)
|
||||
# link the MotionAware sensor to the group the sensor is associated with
|
||||
self._motion_area_configuration = self.controller.get_motion_area_configuration(
|
||||
resource.id
|
||||
)
|
||||
group_id = self._motion_area_configuration.group.rid
|
||||
self.hue_group = self.bridge.api.groups[group_id]
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.hue_group.id)},
|
||||
# link the MotionAware sensor to the room or zone it is associated with
|
||||
self._attr_device_info = get_motion_area_device_info(
|
||||
self.bridge.api, self._motion_area_configuration
|
||||
)
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
"""Helper functions for Philips Hue v2."""
|
||||
|
||||
from aiohue.v2 import HueBridgeV2
|
||||
from aiohue.v2.models.motion_area_configuration import MotionAreaConfiguration
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from ..const import DOMAIN
|
||||
|
||||
|
||||
def normalize_hue_brightness(brightness: float | None) -> float | None:
|
||||
"""Return calculated brightness values."""
|
||||
@@ -30,3 +36,17 @@ def normalize_hue_colortemp(
|
||||
colortemp_mireds = color_util.color_temperature_kelvin_to_mired(colortemp_k)
|
||||
# Hue only accepts a range between min_mireds..max_mireds
|
||||
return min(max(colortemp_mireds, min_mireds), max_mireds)
|
||||
|
||||
|
||||
def get_motion_area_device_info(
|
||||
api: HueBridgeV2, motion_area: MotionAreaConfiguration
|
||||
) -> DeviceInfo:
|
||||
"""Return the device the entities of a MotionAware zone belong to.
|
||||
|
||||
Rooms and zones each have a device of their own. A MotionAware zone that
|
||||
covers the whole home points at `bridge_home` instead, which has none, so
|
||||
its entities are attached to the bridge.
|
||||
"""
|
||||
if (group := api.groups.get(motion_area.group.rid)) is None:
|
||||
return DeviceInfo(identifiers={(DOMAIN, api.config.bridge_id)})
|
||||
return DeviceInfo(identifiers={(DOMAIN, group.id)})
|
||||
|
||||
@@ -147,6 +147,16 @@ def v2_resources_test_data() -> JsonArrayType:
|
||||
return load_json_array_fixture("hue/v2_resources.json")
|
||||
|
||||
|
||||
def replace_resources(
|
||||
data: JsonArrayType, resources: list[dict[str, Any]]
|
||||
) -> JsonArrayType:
|
||||
"""Return the test data with each resource of the same id replaced."""
|
||||
replacements = {resource["id"]: resource for resource in resources}
|
||||
missing = replacements.keys() - {resource["id"] for resource in data}
|
||||
assert not missing, f"resource id(s) not present in the test data: {missing}"
|
||||
return [replacements.get(resource["id"], resource) for resource in data]
|
||||
|
||||
|
||||
def create_mock_api_v2() -> Mock:
|
||||
"""Create a mock V2 API."""
|
||||
api = Mock(spec=aiohue_v2.HueBridgeV2)
|
||||
|
||||
@@ -5,15 +5,24 @@ from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hue.const import DOMAIN
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.util.json import JsonArrayType
|
||||
|
||||
from .conftest import setup_platform
|
||||
from .const import FAKE_BINARY_SENSOR, FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY
|
||||
from .conftest import replace_resources, setup_platform
|
||||
from .const import (
|
||||
FAKE_BINARY_SENSOR,
|
||||
FAKE_BRIDGE,
|
||||
FAKE_DEVICE,
|
||||
FAKE_ZIGBEE_CONNECTIVITY,
|
||||
)
|
||||
|
||||
MOTION_AWARE_ENTITY_ID = "binary_sensor.test_room_test_room_motion_aware_sensor_1"
|
||||
MOTION_AREA_CONFIGURATION_ID = "5e6f7a8b-9c1d-4e2f-b3a4-5c6d7e8f9a0b"
|
||||
TEST_ROOM_ID = "6ddc9066-7e7d-4a03-a773-c73937968296"
|
||||
BRIDGE_HOME_ID = "a3fbc86a-bf4c-4c69-899d-d6eafc37e288"
|
||||
AREA_MOTION_SERVICE_IDS = {
|
||||
"convenience_area_motion": "4f317b69-9da0-4b4f-84f2-7ca07b9fe345",
|
||||
"security_area_motion": "8b7e4f82-9c3d-4e1a-a5f6-8d9c7b2a3e4f",
|
||||
@@ -57,16 +66,6 @@ def area_motion_service(
|
||||
return service
|
||||
|
||||
|
||||
def replace_resources(
|
||||
data: JsonArrayType, resources: list[dict[str, Any]]
|
||||
) -> JsonArrayType:
|
||||
"""Return the test data with each resource of the same id replaced."""
|
||||
replacements = {resource["id"]: resource for resource in resources}
|
||||
missing = replacements.keys() - {resource["id"] for resource in data}
|
||||
assert not missing, f"resource id(s) not present in the test data: {missing}"
|
||||
return [replacements.get(resource["id"], resource) for resource in data]
|
||||
|
||||
|
||||
async def test_binary_sensors(
|
||||
hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType
|
||||
) -> None:
|
||||
@@ -514,3 +513,57 @@ async def test_motion_aware_sensor_zone_not_reporting(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(MOTION_AWARE_ENTITY_ID).state == "off"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("group", "device_identifier"),
|
||||
[
|
||||
pytest.param(
|
||||
{"rid": TEST_ROOM_ID, "rtype": "room"},
|
||||
(DOMAIN, TEST_ROOM_ID),
|
||||
id="room",
|
||||
),
|
||||
pytest.param(
|
||||
{"rid": BRIDGE_HOME_ID, "rtype": "bridge_home"},
|
||||
(DOMAIN, FAKE_BRIDGE["bridge_id"]),
|
||||
id="whole_home",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_motion_aware_sensor_device(
|
||||
hass: HomeAssistant,
|
||||
mock_bridge_v2: Mock,
|
||||
v2_resources_test_data: JsonArrayType,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
group: dict[str, str],
|
||||
device_identifier: tuple[str, str],
|
||||
) -> None:
|
||||
"""Test the MotionAware sensor is attached to its room or zone, or to the bridge.
|
||||
|
||||
A MotionAware zone that covers the whole home points at `bridge_home`, which
|
||||
has no device of its own.
|
||||
"""
|
||||
motion_area_configuration = next(
|
||||
resource
|
||||
for resource in v2_resources_test_data
|
||||
if resource["id"] == MOTION_AREA_CONFIGURATION_ID
|
||||
)
|
||||
await mock_bridge_v2.api.load_test_data(
|
||||
replace_resources(
|
||||
v2_resources_test_data, [{**motion_area_configuration, "group": group}]
|
||||
)
|
||||
)
|
||||
await setup_platform(hass, mock_bridge_v2, Platform.BINARY_SENSOR)
|
||||
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
Platform.BINARY_SENSOR, DOMAIN, AREA_MOTION_SERVICE_IDS["security_area_motion"]
|
||||
)
|
||||
assert entity_id is not None
|
||||
assert hass.states.get(entity_id).state == "off"
|
||||
|
||||
device = device_registry.async_get_device_by_identifier(
|
||||
device_identifier, mock_bridge_v2.config_entry.entry_id
|
||||
)
|
||||
assert device is not None
|
||||
assert entity_registry.async_get(entity_id).device_id == device.id
|
||||
|
||||
@@ -10,11 +10,12 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.util.json import JsonArrayType
|
||||
|
||||
from .conftest import setup_platform
|
||||
from .conftest import replace_resources, setup_platform
|
||||
from .const import (
|
||||
FAKE_BEHAVIOR_INSTANCE,
|
||||
FAKE_BEHAVIOR_SCRIPT,
|
||||
FAKE_BINARY_SENSOR,
|
||||
FAKE_BRIDGE,
|
||||
FAKE_DEVICE,
|
||||
FAKE_PRESENCE_MIMICKING_INSTANCE,
|
||||
FAKE_PRESENCE_MIMICKING_SCRIPT,
|
||||
@@ -22,6 +23,8 @@ from .const import (
|
||||
)
|
||||
|
||||
TEST_ROOM_ID = "6ddc9066-7e7d-4a03-a773-c73937968296"
|
||||
BRIDGE_HOME_ID = "a3fbc86a-bf4c-4c69-899d-d6eafc37e288"
|
||||
MOTION_AREA_CONFIGURATION_ID = "5e6f7a8b-9c1d-4e2f-b3a4-5c6d7e8f9a0b"
|
||||
|
||||
|
||||
async def test_switch(
|
||||
@@ -59,26 +62,58 @@ async def test_switch(
|
||||
assert test_entity.attributes["device_class"] == "switch"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("group", "device_identifier"),
|
||||
[
|
||||
pytest.param(
|
||||
{"rid": TEST_ROOM_ID, "rtype": "room"},
|
||||
(DOMAIN, TEST_ROOM_ID),
|
||||
id="room",
|
||||
),
|
||||
pytest.param(
|
||||
{"rid": BRIDGE_HOME_ID, "rtype": "bridge_home"},
|
||||
(DOMAIN, FAKE_BRIDGE["bridge_id"]),
|
||||
id="whole_home",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_motionaware_switch_device(
|
||||
hass: HomeAssistant,
|
||||
mock_bridge_v2: Mock,
|
||||
v2_resources_test_data: JsonArrayType,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
group: dict[str, str],
|
||||
device_identifier: tuple[str, str],
|
||||
) -> None:
|
||||
"""Test the MotionAware switch is attached to the zone device, not the bridge."""
|
||||
await mock_bridge_v2.api.load_test_data(v2_resources_test_data)
|
||||
"""Test the MotionAware switch is attached to its room or zone, or to the bridge.
|
||||
|
||||
A MotionAware zone that covers the whole home points at `bridge_home`, which
|
||||
has no device of its own.
|
||||
"""
|
||||
motion_area_configuration = next(
|
||||
resource
|
||||
for resource in v2_resources_test_data
|
||||
if resource["id"] == MOTION_AREA_CONFIGURATION_ID
|
||||
)
|
||||
await mock_bridge_v2.api.load_test_data(
|
||||
replace_resources(
|
||||
v2_resources_test_data, [{**motion_area_configuration, "group": group}]
|
||||
)
|
||||
)
|
||||
await setup_platform(hass, mock_bridge_v2, Platform.SWITCH)
|
||||
|
||||
entity_entry = entity_registry.async_get("switch.test_room_test_room_motionaware")
|
||||
assert entity_entry is not None
|
||||
|
||||
zone_device = device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, TEST_ROOM_ID), mock_bridge_v2.config_entry.entry_id
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
Platform.SWITCH, DOMAIN, MOTION_AREA_CONFIGURATION_ID
|
||||
)
|
||||
assert zone_device is not None
|
||||
assert entity_entry.device_id == zone_device.id
|
||||
assert entity_id is not None
|
||||
assert hass.states.get(entity_id).state == "on"
|
||||
|
||||
device = device_registry.async_get_device_by_identifier(
|
||||
device_identifier, mock_bridge_v2.config_entry.entry_id
|
||||
)
|
||||
assert device is not None
|
||||
assert entity_registry.async_get(entity_id).device_id == device.id
|
||||
|
||||
|
||||
async def test_switch_turn_on_service(
|
||||
@@ -172,14 +207,13 @@ async def test_motionaware_switch_turn_on_off_service(
|
||||
assert mock_bridge_v2.mock_requests[0]["method"] == "put"
|
||||
assert (
|
||||
mock_bridge_v2.mock_requests[0]["path"]
|
||||
== "clip/v2/resource/motion_area_configuration/"
|
||||
"5e6f7a8b-9c1d-4e2f-b3a4-5c6d7e8f9a0b"
|
||||
== f"clip/v2/resource/motion_area_configuration/{MOTION_AREA_CONFIGURATION_ID}"
|
||||
)
|
||||
assert mock_bridge_v2.mock_requests[0]["json"]["enabled"] is False
|
||||
|
||||
# Now generate update event by emitting the json we've sent as incoming event
|
||||
event = {
|
||||
"id": "5e6f7a8b-9c1d-4e2f-b3a4-5c6d7e8f9a0b",
|
||||
"id": MOTION_AREA_CONFIGURATION_ID,
|
||||
"type": "motion_area_configuration",
|
||||
**mock_bridge_v2.mock_requests[0]["json"],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user