mirror of https://github.com/home-assistant/core
669 lines
20 KiB
Python
669 lines
20 KiB
Python
"""Test zone component."""
|
|
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from homeassistant import setup
|
|
from homeassistant.components import zone
|
|
from homeassistant.components.zone import DOMAIN
|
|
from homeassistant.const import (
|
|
ATTR_EDITABLE,
|
|
ATTR_FRIENDLY_NAME,
|
|
ATTR_ICON,
|
|
ATTR_NAME,
|
|
ATTR_PERSONS,
|
|
SERVICE_RELOAD,
|
|
)
|
|
from homeassistant.core import Context, HomeAssistant
|
|
from homeassistant.exceptions import Unauthorized
|
|
from homeassistant.helpers import entity_registry as er
|
|
|
|
from tests.common import MockConfigEntry, MockUser
|
|
from tests.typing import WebSocketGenerator
|
|
|
|
|
|
@pytest.fixture
|
|
def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]):
|
|
"""Storage setup."""
|
|
|
|
async def _storage(items=None, config=None):
|
|
if items is None:
|
|
hass_storage[DOMAIN] = {
|
|
"key": DOMAIN,
|
|
"version": 1,
|
|
"data": {
|
|
"items": [
|
|
{
|
|
"id": "from_storage",
|
|
"name": "from storage",
|
|
"latitude": 1,
|
|
"longitude": 2,
|
|
"radius": 3,
|
|
"passive": False,
|
|
"icon": "mdi:from-storage",
|
|
}
|
|
]
|
|
},
|
|
}
|
|
else:
|
|
hass_storage[DOMAIN] = {
|
|
"key": DOMAIN,
|
|
"version": 1,
|
|
"data": {"items": items},
|
|
}
|
|
if config is None:
|
|
config = {}
|
|
return await setup.async_setup_component(hass, DOMAIN, config)
|
|
|
|
return _storage
|
|
|
|
|
|
async def test_setup_no_zones_still_adds_home_zone(hass: HomeAssistant) -> None:
|
|
"""Test if no config is passed in we still get the home zone."""
|
|
assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": None})
|
|
assert len(hass.states.async_entity_ids("zone")) == 1
|
|
state = hass.states.get("zone.home")
|
|
assert hass.config.location_name == state.name
|
|
assert hass.config.latitude == state.attributes["latitude"]
|
|
assert hass.config.longitude == state.attributes["longitude"]
|
|
assert not state.attributes.get("passive", False)
|
|
|
|
|
|
async def test_setup(hass: HomeAssistant) -> None:
|
|
"""Test a successful setup."""
|
|
info = {
|
|
"name": "Test Zone",
|
|
"latitude": 32.880837,
|
|
"longitude": -117.237561,
|
|
"radius": 250,
|
|
"passive": True,
|
|
}
|
|
assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info})
|
|
|
|
assert len(hass.states.async_entity_ids("zone")) == 2
|
|
state = hass.states.get("zone.test_zone")
|
|
assert info["name"] == state.name
|
|
assert info["latitude"] == state.attributes["latitude"]
|
|
assert info["longitude"] == state.attributes["longitude"]
|
|
assert info["radius"] == state.attributes["radius"]
|
|
assert info["passive"] == state.attributes["passive"]
|
|
|
|
|
|
async def test_setup_zone_skips_home_zone(hass: HomeAssistant) -> None:
|
|
"""Test that zone named Home should override hass home zone."""
|
|
info = {"name": "Home", "latitude": 1.1, "longitude": -2.2}
|
|
assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info})
|
|
|
|
assert len(hass.states.async_entity_ids("zone")) == 1
|
|
state = hass.states.get("zone.home")
|
|
assert info["name"] == state.name
|
|
|
|
|
|
async def test_setup_name_can_be_same_on_multiple_zones(hass: HomeAssistant) -> None:
|
|
"""Test that zone named Home should override hass home zone."""
|
|
info = {"name": "Test Zone", "latitude": 1.1, "longitude": -2.2}
|
|
assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": [info, info]})
|
|
assert len(hass.states.async_entity_ids("zone")) == 3
|
|
|
|
|
|
async def test_active_zone_skips_passive_zones(hass: HomeAssistant) -> None:
|
|
"""Test active and passive zones."""
|
|
assert await setup.async_setup_component(
|
|
hass,
|
|
zone.DOMAIN,
|
|
{
|
|
"zone": [
|
|
{
|
|
"name": "Passive Zone",
|
|
"latitude": 32.880600,
|
|
"longitude": -117.237561,
|
|
"radius": 250,
|
|
"passive": True,
|
|
}
|
|
]
|
|
},
|
|
)
|
|
await hass.async_block_till_done()
|
|
active = zone.async_active_zone(hass, 32.880600, -117.237561)
|
|
assert active is None
|
|
|
|
|
|
async def test_active_zone_skips_passive_zones_2(hass: HomeAssistant) -> None:
|
|
"""Test active and passive zones."""
|
|
assert await setup.async_setup_component(
|
|
hass,
|
|
zone.DOMAIN,
|
|
{
|
|
"zone": [
|
|
{
|
|
"name": "Active Zone",
|
|
"latitude": 32.880800,
|
|
"longitude": -117.237561,
|
|
"radius": 500,
|
|
}
|
|
]
|
|
},
|
|
)
|
|
await hass.async_block_till_done()
|
|
active = zone.async_active_zone(hass, 32.880700, -117.237561)
|
|
assert active.entity_id == "zone.active_zone"
|
|
|
|
|
|
async def test_active_zone_prefers_smaller_zone_if_same_distance(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test zone size preferences."""
|
|
latitude = 32.880600
|
|
longitude = -117.237561
|
|
assert await setup.async_setup_component(
|
|
hass,
|
|
zone.DOMAIN,
|
|
{
|
|
"zone": [
|
|
{
|
|
"name": "Small Zone",
|
|
"latitude": latitude,
|
|
"longitude": longitude,
|
|
"radius": 250,
|
|
},
|
|
{
|
|
"name": "Big Zone",
|
|
"latitude": latitude,
|
|
"longitude": longitude,
|
|
"radius": 500,
|
|
},
|
|
]
|
|
},
|
|
)
|
|
|
|
active = zone.async_active_zone(hass, latitude, longitude)
|
|
assert active.entity_id == "zone.small_zone"
|
|
|
|
|
|
async def test_active_zone_prefers_smaller_zone_if_same_distance_2(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test zone size preferences."""
|
|
latitude = 32.880600
|
|
longitude = -117.237561
|
|
assert await setup.async_setup_component(
|
|
hass,
|
|
zone.DOMAIN,
|
|
{
|
|
"zone": [
|
|
{
|
|
"name": "Smallest Zone",
|
|
"latitude": latitude,
|
|
"longitude": longitude,
|
|
"radius": 50,
|
|
}
|
|
]
|
|
},
|
|
)
|
|
|
|
active = zone.async_active_zone(hass, latitude, longitude)
|
|
assert active.entity_id == "zone.smallest_zone"
|
|
|
|
|
|
async def test_in_zone_works_for_passive_zones(hass: HomeAssistant) -> None:
|
|
"""Test working in passive zones."""
|
|
latitude = 32.880600
|
|
longitude = -117.237561
|
|
assert await setup.async_setup_component(
|
|
hass,
|
|
zone.DOMAIN,
|
|
{
|
|
"zone": [
|
|
{
|
|
"name": "Passive Zone",
|
|
"latitude": latitude,
|
|
"longitude": longitude,
|
|
"radius": 250,
|
|
"passive": True,
|
|
}
|
|
]
|
|
},
|
|
)
|
|
|
|
assert zone.in_zone(hass.states.get("zone.passive_zone"), latitude, longitude)
|
|
|
|
|
|
async def test_async_active_zone_with_non_zero_radius(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test async_active_zone with a non-zero radius."""
|
|
latitude = 32.880600
|
|
longitude = -117.237561
|
|
|
|
assert await setup.async_setup_component(
|
|
hass,
|
|
zone.DOMAIN,
|
|
{
|
|
"zone": [
|
|
{
|
|
"name": "Small Zone",
|
|
"latitude": 32.980600,
|
|
"longitude": -117.137561,
|
|
"radius": 50000,
|
|
},
|
|
{
|
|
"name": "Big Zone",
|
|
"latitude": 32.980600,
|
|
"longitude": -117.137561,
|
|
"radius": 100000,
|
|
},
|
|
]
|
|
},
|
|
)
|
|
|
|
home_state = hass.states.get("zone.home")
|
|
assert home_state.attributes["radius"] == 100
|
|
assert home_state.attributes["latitude"] == 32.87336
|
|
assert home_state.attributes["longitude"] == -117.22743
|
|
|
|
active = zone.async_active_zone(hass, latitude, longitude, 5000)
|
|
assert active.entity_id == "zone.home"
|
|
|
|
active = zone.async_active_zone(hass, latitude, longitude, 0)
|
|
assert active.entity_id == "zone.small_zone"
|
|
|
|
|
|
async def test_core_config_update(hass: HomeAssistant) -> None:
|
|
"""Test updating core config will update home zone."""
|
|
assert await setup.async_setup_component(hass, "zone", {})
|
|
|
|
home = hass.states.get("zone.home")
|
|
|
|
await hass.config.async_update(
|
|
location_name="Updated Name", latitude=10, longitude=20
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
home_updated = hass.states.get("zone.home")
|
|
|
|
assert home is not home_updated
|
|
assert home_updated.name == "Updated Name"
|
|
assert home_updated.attributes["latitude"] == 10
|
|
assert home_updated.attributes["longitude"] == 20
|
|
|
|
|
|
async def test_reload(
|
|
hass: HomeAssistant,
|
|
entity_registry: er.EntityRegistry,
|
|
hass_admin_user: MockUser,
|
|
hass_read_only_user: MockUser,
|
|
) -> None:
|
|
"""Test reload service."""
|
|
count_start = len(hass.states.async_entity_ids())
|
|
|
|
assert await setup.async_setup_component(
|
|
hass,
|
|
DOMAIN,
|
|
{
|
|
DOMAIN: [
|
|
{"name": "yaml 1", "latitude": 1, "longitude": 2},
|
|
{"name": "yaml 2", "latitude": 3, "longitude": 4},
|
|
],
|
|
},
|
|
)
|
|
|
|
assert count_start + 3 == len(hass.states.async_entity_ids())
|
|
|
|
state_1 = hass.states.get("zone.yaml_1")
|
|
state_2 = hass.states.get("zone.yaml_2")
|
|
state_3 = hass.states.get("zone.yaml_3")
|
|
|
|
assert state_1 is not None
|
|
assert state_1.attributes["latitude"] == 1
|
|
assert state_1.attributes["longitude"] == 2
|
|
assert state_2 is not None
|
|
assert state_2.attributes["latitude"] == 3
|
|
assert state_2.attributes["longitude"] == 4
|
|
assert state_3 is None
|
|
assert len(entity_registry.entities) == 0
|
|
|
|
with patch(
|
|
"homeassistant.config.load_yaml_config_file",
|
|
autospec=True,
|
|
return_value={
|
|
DOMAIN: [
|
|
{"name": "yaml 2", "latitude": 3, "longitude": 4},
|
|
{"name": "yaml 3", "latitude": 5, "longitude": 6},
|
|
]
|
|
},
|
|
):
|
|
with pytest.raises(Unauthorized):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_RELOAD,
|
|
blocking=True,
|
|
context=Context(user_id=hass_read_only_user.id),
|
|
)
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_RELOAD,
|
|
blocking=True,
|
|
context=Context(user_id=hass_admin_user.id),
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
assert count_start + 3 == len(hass.states.async_entity_ids())
|
|
|
|
state_1 = hass.states.get("zone.yaml_1")
|
|
state_2 = hass.states.get("zone.yaml_2")
|
|
state_3 = hass.states.get("zone.yaml_3")
|
|
|
|
assert state_1 is None
|
|
assert state_2 is not None
|
|
assert state_2.attributes["latitude"] == 3
|
|
assert state_2.attributes["longitude"] == 4
|
|
assert state_3 is not None
|
|
assert state_3.attributes["latitude"] == 5
|
|
assert state_3.attributes["longitude"] == 6
|
|
|
|
|
|
async def test_load_from_storage(hass: HomeAssistant, storage_setup) -> None:
|
|
"""Test set up from storage."""
|
|
assert await storage_setup()
|
|
state = hass.states.get(f"{DOMAIN}.from_storage")
|
|
assert state.state == "0"
|
|
assert state.name == "from storage"
|
|
assert state.attributes.get(ATTR_EDITABLE)
|
|
|
|
|
|
async def test_editable_state_attribute(hass: HomeAssistant, storage_setup) -> None:
|
|
"""Test editable attribute."""
|
|
assert await storage_setup(
|
|
config={DOMAIN: [{"name": "yaml option", "latitude": 3, "longitude": 4}]}
|
|
)
|
|
|
|
state = hass.states.get(f"{DOMAIN}.from_storage")
|
|
assert state.state == "0"
|
|
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage"
|
|
assert state.attributes.get(ATTR_EDITABLE)
|
|
|
|
state = hass.states.get(f"{DOMAIN}.yaml_option")
|
|
assert state.state == "0"
|
|
assert not state.attributes.get(ATTR_EDITABLE)
|
|
|
|
|
|
async def test_ws_list(
|
|
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, storage_setup
|
|
) -> None:
|
|
"""Test listing via WS."""
|
|
assert await storage_setup(
|
|
config={DOMAIN: [{"name": "yaml option", "latitude": 3, "longitude": 4}]}
|
|
)
|
|
|
|
client = await hass_ws_client(hass)
|
|
|
|
await client.send_json({"id": 6, "type": f"{DOMAIN}/list"})
|
|
resp = await client.receive_json()
|
|
assert resp["success"]
|
|
|
|
storage_ent = "from_storage"
|
|
yaml_ent = "from_yaml"
|
|
result = {item["id"]: item for item in resp["result"]}
|
|
|
|
assert len(result) == 1
|
|
assert storage_ent in result
|
|
assert yaml_ent not in result
|
|
assert result[storage_ent][ATTR_NAME] == "from storage"
|
|
|
|
|
|
async def test_ws_delete(
|
|
hass: HomeAssistant,
|
|
hass_ws_client: WebSocketGenerator,
|
|
entity_registry: er.EntityRegistry,
|
|
storage_setup,
|
|
) -> None:
|
|
"""Test WS delete cleans up entity registry."""
|
|
assert await storage_setup()
|
|
|
|
input_id = "from_storage"
|
|
input_entity_id = f"{DOMAIN}.{input_id}"
|
|
|
|
state = hass.states.get(input_entity_id)
|
|
assert state is not None
|
|
assert entity_registry.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None
|
|
|
|
client = await hass_ws_client(hass)
|
|
|
|
await client.send_json(
|
|
{"id": 6, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": f"{input_id}"}
|
|
)
|
|
resp = await client.receive_json()
|
|
assert resp["success"]
|
|
|
|
state = hass.states.get(input_entity_id)
|
|
assert state is None
|
|
assert entity_registry.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None
|
|
|
|
|
|
async def test_update(
|
|
hass: HomeAssistant,
|
|
hass_ws_client: WebSocketGenerator,
|
|
entity_registry: er.EntityRegistry,
|
|
storage_setup,
|
|
) -> None:
|
|
"""Test updating min/max updates the state."""
|
|
|
|
items = [
|
|
{
|
|
"id": "from_storage",
|
|
"name": "from storage",
|
|
"latitude": 1,
|
|
"longitude": 2,
|
|
"radius": 3,
|
|
"passive": False,
|
|
}
|
|
]
|
|
assert await storage_setup(items)
|
|
|
|
input_id = "from_storage"
|
|
input_entity_id = f"{DOMAIN}.{input_id}"
|
|
|
|
state = hass.states.get(input_entity_id)
|
|
assert state.attributes["latitude"] == 1
|
|
assert state.attributes["longitude"] == 2
|
|
assert entity_registry.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None
|
|
|
|
client = await hass_ws_client(hass)
|
|
|
|
await client.send_json(
|
|
{
|
|
"id": 6,
|
|
"type": f"{DOMAIN}/update",
|
|
f"{DOMAIN}_id": f"{input_id}",
|
|
"latitude": 3,
|
|
"longitude": 4,
|
|
"passive": True,
|
|
}
|
|
)
|
|
resp = await client.receive_json()
|
|
assert resp["success"]
|
|
|
|
state = hass.states.get(input_entity_id)
|
|
assert state.attributes["latitude"] == 3
|
|
assert state.attributes["longitude"] == 4
|
|
assert state.attributes["passive"] is True
|
|
|
|
|
|
async def test_ws_create(
|
|
hass: HomeAssistant,
|
|
hass_ws_client: WebSocketGenerator,
|
|
entity_registry: er.EntityRegistry,
|
|
storage_setup,
|
|
) -> None:
|
|
"""Test create WS."""
|
|
assert await storage_setup(items=[])
|
|
|
|
input_id = "new_input"
|
|
input_entity_id = f"{DOMAIN}.{input_id}"
|
|
|
|
state = hass.states.get(input_entity_id)
|
|
assert state is None
|
|
assert entity_registry.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None
|
|
|
|
client = await hass_ws_client(hass)
|
|
|
|
await client.send_json(
|
|
{
|
|
"id": 6,
|
|
"type": f"{DOMAIN}/create",
|
|
"name": "New Input",
|
|
"latitude": 3,
|
|
"longitude": 4,
|
|
"passive": True,
|
|
}
|
|
)
|
|
resp = await client.receive_json()
|
|
assert resp["success"]
|
|
|
|
state = hass.states.get(input_entity_id)
|
|
assert state.state == "0"
|
|
assert state.attributes["latitude"] == 3
|
|
assert state.attributes["longitude"] == 4
|
|
assert state.attributes["passive"] is True
|
|
|
|
|
|
async def test_import_config_entry(hass: HomeAssistant) -> None:
|
|
"""Test we import config entry and then delete it."""
|
|
entry = MockConfigEntry(
|
|
domain="zone",
|
|
data={
|
|
"name": "from config entry",
|
|
"latitude": 1,
|
|
"longitude": 2,
|
|
"radius": 3,
|
|
"passive": False,
|
|
"icon": "mdi:from-config-entry",
|
|
},
|
|
)
|
|
entry.add_to_hass(hass)
|
|
assert await setup.async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
assert len(hass.config_entries.async_entries()) == 0
|
|
|
|
state = hass.states.get("zone.from_config_entry")
|
|
assert state is not None
|
|
assert state.attributes[zone.ATTR_LATITUDE] == 1
|
|
assert state.attributes[zone.ATTR_LONGITUDE] == 2
|
|
assert state.attributes[zone.ATTR_RADIUS] == 3
|
|
assert state.attributes[zone.ATTR_PASSIVE] is False
|
|
assert state.attributes[ATTR_ICON] == "mdi:from-config-entry"
|
|
|
|
|
|
async def test_zone_empty_setup(hass: HomeAssistant) -> None:
|
|
"""Set up zone with empty config."""
|
|
assert await setup.async_setup_component(hass, DOMAIN, {"zone": {}})
|
|
|
|
|
|
async def test_unavailable_zone(hass: HomeAssistant) -> None:
|
|
"""Test active zone with unavailable zones."""
|
|
assert await setup.async_setup_component(hass, DOMAIN, {"zone": {}})
|
|
hass.states.async_set("zone.bla", "unavailable", {"restored": True})
|
|
|
|
assert zone.async_active_zone(hass, 0.0, 0.01) is None
|
|
|
|
assert zone.in_zone(hass.states.get("zone.bla"), 0, 0) is False
|
|
|
|
|
|
async def test_state(hass: HomeAssistant) -> None:
|
|
"""Test the state of a zone."""
|
|
info = {
|
|
"name": "Test Zone",
|
|
"latitude": 32.880837,
|
|
"longitude": -117.237561,
|
|
"radius": 250,
|
|
"passive": False,
|
|
}
|
|
assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info})
|
|
|
|
assert len(hass.states.async_entity_ids("zone")) == 2
|
|
state = hass.states.get("zone.test_zone")
|
|
assert state.state == "0"
|
|
assert state.attributes[ATTR_PERSONS] == []
|
|
|
|
# Person entity enters zone
|
|
hass.states.async_set(
|
|
"person.person1",
|
|
"Test Zone",
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("zone.test_zone")
|
|
assert state
|
|
assert state.state == "1"
|
|
assert state.attributes[ATTR_PERSONS] == ["person.person1"]
|
|
|
|
state = hass.states.get("zone.home")
|
|
assert state
|
|
assert state.state == "0"
|
|
assert state.attributes[ATTR_PERSONS] == []
|
|
|
|
# Person entity enters zone (case insensitive)
|
|
hass.states.async_set(
|
|
"person.person2",
|
|
"TEST zone",
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("zone.test_zone")
|
|
assert state
|
|
assert state.state == "2"
|
|
assert sorted(state.attributes[ATTR_PERSONS]) == [
|
|
"person.person1",
|
|
"person.person2",
|
|
]
|
|
|
|
state = hass.states.get("zone.home")
|
|
assert state
|
|
assert state.state == "0"
|
|
assert state.attributes[ATTR_PERSONS] == []
|
|
|
|
# Person entity enters another zone
|
|
hass.states.async_set(
|
|
"person.person1",
|
|
"home",
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("zone.test_zone")
|
|
assert state
|
|
assert state.state == "1"
|
|
assert state.attributes[ATTR_PERSONS] == ["person.person2"]
|
|
|
|
state = hass.states.get("zone.home")
|
|
assert state
|
|
assert state.state == "1"
|
|
assert state.attributes[ATTR_PERSONS] == ["person.person1"]
|
|
|
|
# Person entity enters not_home
|
|
hass.states.async_set(
|
|
"person.person1",
|
|
"not_home",
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("zone.test_zone")
|
|
assert state
|
|
assert state.state == "1"
|
|
assert state.attributes[ATTR_PERSONS] == ["person.person2"]
|
|
|
|
# Person entity removed
|
|
hass.states.async_remove("person.person2")
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("zone.test_zone")
|
|
assert state
|
|
assert state.state == "0"
|
|
assert state.attributes[ATTR_PERSONS] == []
|
|
|
|
state = hass.states.get("zone.home")
|
|
assert state
|
|
assert state.state == "0"
|
|
assert state.attributes[ATTR_PERSONS] == []
|