core/tests/components/zone/test_init.py

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] == []