From 9ff49e9c3ada9eec27f7bf8a1e78109ff0348a80 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jan 2022 18:00:43 +0100 Subject: [PATCH] Change zone's state to be number of person entities in the zone (#64910) Co-authored-by: Paulus Schoutsen --- homeassistant/components/zone/__init__.py | 49 ++++++++- tests/components/zone/test_init.py | 122 +++++++++++++++++++++- 2 files changed, 164 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index d4474d793ab..18ef5f34b7b 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -1,12 +1,14 @@ """Support for the definition of zones.""" from __future__ import annotations +from collections.abc import Callable import logging from typing import Any, cast import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.person import DOMAIN as PERSON_DOMAIN from homeassistant.const import ( ATTR_EDITABLE, ATTR_LATITUDE, @@ -21,12 +23,20 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, ) -from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback +from homeassistant.core import ( + Event, + HomeAssistant, + ServiceCall, + State, + callback, + split_entity_id, +) from homeassistant.helpers import ( collection, config_validation as cv, entity, entity_component, + event, service, storage, ) @@ -285,7 +295,9 @@ class Zone(entity.Entity): self._config = config self.editable = True self._attrs: dict | None = None + self._remove_listener: Callable[[], None] | None = None self._generate_attrs() + self._persons_in_zone: set[str] = set() @classmethod def from_yaml(cls, config: dict) -> Zone: @@ -296,9 +308,9 @@ class Zone(entity.Entity): return zone @property - def state(self) -> str: + def state(self) -> int: """Return the state property really does nothing for a zone.""" - return "zoning" + return len(self._persons_in_zone) @property def name(self) -> str: @@ -333,6 +345,37 @@ class Zone(entity.Entity): self._generate_attrs() self.async_write_ha_state() + @callback + def _person_state_change_listener(self, evt: Event) -> None: + object_id = split_entity_id(self.entity_id)[1] + person_entity_id = evt.data["entity_id"] + cur_count = len(self._persons_in_zone) + if evt.data["new_state"] and evt.data["new_state"].state == object_id: + self._persons_in_zone.add(person_entity_id) + elif person_entity_id in self._persons_in_zone: + self._persons_in_zone.remove(person_entity_id) + + if len(self._persons_in_zone) != cur_count: + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + persons = self.hass.states.async_entity_ids(PERSON_DOMAIN) + object_id = split_entity_id(self.entity_id)[1] + for person in persons: + state = self.hass.states.get(person) + if state and state.state == object_id: + self._persons_in_zone.add(person) + + self.async_on_remove( + event.async_track_state_change_filtered( + self.hass, + event.TrackStates(False, set(), {PERSON_DOMAIN}), + self._person_state_change_listener, + ).async_remove + ) + @callback def _generate_attrs(self) -> None: """Generate new attrs based on config.""" diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index 8d0fddb921c..399afd480c7 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -316,7 +316,7 @@ async def test_load_from_storage(hass, storage_setup): """Test set up from storage.""" assert await storage_setup() state = hass.states.get(f"{DOMAIN}.from_storage") - assert state.state == "zoning" + assert state.state == "0" assert state.name == "from storage" assert state.attributes.get(ATTR_EDITABLE) @@ -328,12 +328,12 @@ async def test_editable_state_attribute(hass, storage_setup): ) state = hass.states.get(f"{DOMAIN}.from_storage") - assert state.state == "zoning" + 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 == "zoning" + assert state.state == "0" assert not state.attributes.get(ATTR_EDITABLE) @@ -457,7 +457,7 @@ async def test_ws_create(hass, hass_ws_client, storage_setup): assert resp["success"] state = hass.states.get(input_entity_id) - assert state.state == "zoning" + assert state.state == "0" assert state.attributes["latitude"] == 3 assert state.attributes["longitude"] == 4 assert state.attributes["passive"] is True @@ -503,3 +503,117 @@ async def test_unavailable_zone(hass): 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): + """Test the state of a zone.""" + 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 state.state == "0" + + # 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.state == "1" + + # Person entity enters zone + hass.states.async_set("person.person2", "test_zone") + await hass.async_block_till_done() + state = hass.states.get("zone.test_zone") + assert state.state == "2" + + # 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.state == "1" + + # Person entity removed + hass.states.async_remove("person.person2") + await hass.async_block_till_done() + state = hass.states.get("zone.test_zone") + assert state.state == "0" + + +async def test_state_2(hass): + """Test the state of a zone.""" + hass.states.async_set("person.person1", "unknown") + hass.states.async_set("person.person2", "unknown") + + 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 state.state == "0" + + # 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.state == "1" + + # Person entity enters zone + hass.states.async_set("person.person2", "test_zone") + await hass.async_block_till_done() + state = hass.states.get("zone.test_zone") + assert state.state == "2" + + # 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.state == "1" + + # Person entity removed + hass.states.async_remove("person.person2") + await hass.async_block_till_done() + state = hass.states.get("zone.test_zone") + assert state.state == "0" + + +async def test_state_3(hass): + """Test the state of a zone.""" + hass.states.async_set("person.person1", "test_zone") + hass.states.async_set("person.person2", "test_zone") + + 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 state.state == "2" + + # 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.state == "1" + + # Person entity removed + hass.states.async_remove("person.person2") + await hass.async_block_till_done() + state = hass.states.get("zone.test_zone") + assert state.state == "0"