Change zone's state to be number of person entities in the zone (#64910)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
pull/64990/head
Erik Montnemery 2022-01-26 18:00:43 +01:00 committed by GitHub
parent ccf018a5fc
commit 9ff49e9c3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 164 additions and 7 deletions

View File

@ -1,12 +1,14 @@
"""Support for the definition of zones.""" """Support for the definition of zones."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
import logging import logging
from typing import Any, cast from typing import Any, cast
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.person import DOMAIN as PERSON_DOMAIN
from homeassistant.const import ( from homeassistant.const import (
ATTR_EDITABLE, ATTR_EDITABLE,
ATTR_LATITUDE, ATTR_LATITUDE,
@ -21,12 +23,20 @@ from homeassistant.const import (
SERVICE_RELOAD, SERVICE_RELOAD,
STATE_UNAVAILABLE, 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 ( from homeassistant.helpers import (
collection, collection,
config_validation as cv, config_validation as cv,
entity, entity,
entity_component, entity_component,
event,
service, service,
storage, storage,
) )
@ -285,7 +295,9 @@ class Zone(entity.Entity):
self._config = config self._config = config
self.editable = True self.editable = True
self._attrs: dict | None = None self._attrs: dict | None = None
self._remove_listener: Callable[[], None] | None = None
self._generate_attrs() self._generate_attrs()
self._persons_in_zone: set[str] = set()
@classmethod @classmethod
def from_yaml(cls, config: dict) -> Zone: def from_yaml(cls, config: dict) -> Zone:
@ -296,9 +308,9 @@ class Zone(entity.Entity):
return zone return zone
@property @property
def state(self) -> str: def state(self) -> int:
"""Return the state property really does nothing for a zone.""" """Return the state property really does nothing for a zone."""
return "zoning" return len(self._persons_in_zone)
@property @property
def name(self) -> str: def name(self) -> str:
@ -333,6 +345,37 @@ class Zone(entity.Entity):
self._generate_attrs() self._generate_attrs()
self.async_write_ha_state() 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 @callback
def _generate_attrs(self) -> None: def _generate_attrs(self) -> None:
"""Generate new attrs based on config.""" """Generate new attrs based on config."""

View File

@ -316,7 +316,7 @@ async def test_load_from_storage(hass, storage_setup):
"""Test set up from storage.""" """Test set up from storage."""
assert await storage_setup() assert await storage_setup()
state = hass.states.get(f"{DOMAIN}.from_storage") state = hass.states.get(f"{DOMAIN}.from_storage")
assert state.state == "zoning" assert state.state == "0"
assert state.name == "from storage" assert state.name == "from storage"
assert state.attributes.get(ATTR_EDITABLE) 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") 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_FRIENDLY_NAME) == "from storage"
assert state.attributes.get(ATTR_EDITABLE) assert state.attributes.get(ATTR_EDITABLE)
state = hass.states.get(f"{DOMAIN}.yaml_option") state = hass.states.get(f"{DOMAIN}.yaml_option")
assert state.state == "zoning" assert state.state == "0"
assert not state.attributes.get(ATTR_EDITABLE) 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"] assert resp["success"]
state = hass.states.get(input_entity_id) state = hass.states.get(input_entity_id)
assert state.state == "zoning" assert state.state == "0"
assert state.attributes["latitude"] == 3 assert state.attributes["latitude"] == 3
assert state.attributes["longitude"] == 4 assert state.attributes["longitude"] == 4
assert state.attributes["passive"] is True 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.async_active_zone(hass, 0.0, 0.01) is None
assert zone.in_zone(hass.states.get("zone.bla"), 0, 0) is False 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"