Migrate zone to use collection helpers (#30774)

This commit is contained in:
Paulus Schoutsen 2020-01-22 12:36:25 -08:00 committed by GitHub
parent 4c27d6b9aa
commit 0fba9e44ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 762 additions and 1209 deletions

View File

@ -271,8 +271,17 @@ async def async_handle_waypoint(hass, name_base, waypoint):
return
zone = zone_comp.Zone(
hass, pretty_name, lat, lon, rad, zone_comp.ICON_IMPORT, False
{
zone_comp.CONF_NAME: pretty_name,
zone_comp.CONF_LATITUDE: lat,
zone_comp.CONF_LONGITUDE: lon,
zone_comp.CONF_RADIUS: rad,
zone_comp.CONF_ICON: zone_comp.ICON_IMPORT,
zone_comp.CONF_PASSIVE: False,
},
False,
)
zone.hass = hass
zone.entity_id = entity_id
await zone.async_update_ha_state()

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430"
},
"step": {
"init": {
"data": {
"icon": "\u0418\u043a\u043e\u043d\u0430",
"latitude": "\u0428\u0438\u0440\u0438\u043d\u0430",
"longitude": "\u0414\u044a\u043b\u0436\u0438\u043d\u0430",
"name": "\u0418\u043c\u0435",
"passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u0430",
"radius": "\u0420\u0430\u0434\u0438\u0443\u0441"
},
"title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u0437\u043e\u043d\u0430\u0442\u0430"
}
},
"title": "\u0417\u043e\u043d\u0430"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "El nom ja existeix"
},
"step": {
"init": {
"data": {
"icon": "Icona",
"latitude": "Latitud",
"longitude": "Longitud",
"name": "Nom",
"passive": "Passiu",
"radius": "Radi"
},
"title": "Definici\u00f3 dels par\u00e0metres de la zona"
}
},
"title": "Zona"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "N\u00e1zev ji\u017e existuje"
},
"step": {
"init": {
"data": {
"icon": "Ikona",
"latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka",
"longitude": "Zem\u011bpisn\u00e1 d\u00e9lka",
"name": "N\u00e1zev",
"passive": "Pasivn\u00ed",
"radius": "Polom\u011br"
},
"title": "Definujte parametry z\u00f3ny"
}
},
"title": "Z\u00f3na"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Enw eisoes yn bodoli"
},
"step": {
"init": {
"data": {
"icon": "Eicon",
"latitude": "Lledred",
"longitude": "Hydred",
"name": "Enw",
"passive": "Goddefol",
"radius": "Radiws"
},
"title": "Ddiffinio paramedrau parth"
}
},
"title": "Parth"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Navnet findes allerede"
},
"step": {
"init": {
"data": {
"icon": "Ikon",
"latitude": "Breddegrad",
"longitude": "L\u00e6ngdegrad",
"name": "Navn",
"passive": "Passiv",
"radius": "Radius"
},
"title": "Definer zoneparametre"
}
},
"title": "Zone"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Name existiert bereits"
},
"step": {
"init": {
"data": {
"icon": "Symbol",
"latitude": "Breitengrad",
"longitude": "L\u00e4ngengrad",
"name": "Name",
"passive": "Passiv",
"radius": "Radius"
},
"title": "Definiere die Zonenparameter"
}
},
"title": "Zone"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Name already exists"
},
"step": {
"init": {
"data": {
"icon": "Icon",
"latitude": "Latitude",
"longitude": "Longitude",
"name": "Name",
"passive": "Passive",
"radius": "Radius"
},
"title": "Define zone parameters"
}
},
"title": "Zone"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "El nombre ya existe"
},
"step": {
"init": {
"data": {
"icon": "Icono",
"latitude": "Latitud",
"longitude": "Longitud",
"name": "Nombre",
"passive": "Pasivo",
"radius": "Radio"
},
"title": "Definir par\u00e1metros de zona"
}
},
"title": "Zona"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "El nombre ya existe"
},
"step": {
"init": {
"data": {
"icon": "Icono",
"latitude": "Latitud",
"longitude": "Longitud",
"name": "Nombre",
"passive": "Pasivo",
"radius": "Radio"
},
"title": "Definir par\u00e1metros de la zona"
}
},
"title": "Zona"
}
}

View File

@ -1,16 +0,0 @@
{
"config": {
"step": {
"init": {
"data": {
"icon": "Ikoon",
"latitude": "Laius",
"longitude": "Pikkus",
"name": "Nimi",
"radius": "Raadius"
},
"title": "M\u00e4\u00e4ra tsooni parameetrid"
}
}
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9"
},
"step": {
"init": {
"data": {
"icon": "Ic\u00f4ne",
"latitude": "Latitude",
"longitude": "Longitude",
"name": "Nom",
"passive": "Passif",
"radius": "Rayon"
},
"title": "D\u00e9finir les param\u00e8tres de la zone"
}
},
"title": "Zone"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "\u05d4\u05e9\u05dd \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05dd"
},
"step": {
"init": {
"data": {
"icon": "\u05e1\u05de\u05dc",
"latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
"longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
"name": "\u05e9\u05dd",
"passive": "\u05e4\u05e1\u05d9\u05d1\u05d9",
"radius": "\u05e8\u05d3\u05d9\u05d5\u05e1"
},
"title": "\u05d4\u05d2\u05d3\u05e8 \u05e4\u05e8\u05de\u05d8\u05e8\u05d9\u05dd \u05e9\u05dc \u05d0\u05d6\u05d5\u05e8"
}
},
"title": "\u05d0\u05d6\u05d5\u05e8"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Ime ve\u0107 postoji"
},
"step": {
"init": {
"data": {
"icon": "Ikona",
"latitude": "Zemljopisna \u0161irina",
"longitude": "Zemljopisna du\u017eina",
"name": "Ime",
"passive": "Pasivno",
"radius": "Radijus"
},
"title": "Definirajte parametre zone"
}
},
"title": "Zona"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik"
},
"step": {
"init": {
"data": {
"icon": "Ikon",
"latitude": "Sz\u00e9less\u00e9g",
"longitude": "Hossz\u00fas\u00e1g",
"name": "N\u00e9v",
"passive": "Passz\u00edv",
"radius": "Sug\u00e1r"
},
"title": "Z\u00f3na param\u00e9terek megad\u00e1sa"
}
},
"title": "Z\u00f3na"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Nama sudah ada"
},
"step": {
"init": {
"data": {
"icon": "Ikon",
"latitude": "Lintang",
"longitude": "Garis bujur",
"name": "Nama",
"passive": "Pasif",
"radius": "Radius"
},
"title": "Tentukan parameter zona"
}
},
"title": "Zona"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Il nome \u00e8 gi\u00e0 esistente"
},
"step": {
"init": {
"data": {
"icon": "Icona",
"latitude": "Latitudine",
"longitude": "Longitudine",
"name": "Nome",
"passive": "Passiva",
"radius": "Raggio"
},
"title": "Imposta i parametri della zona"
}
},
"title": "Zona"
}
}

View File

@ -1,13 +0,0 @@
{
"config": {
"step": {
"init": {
"data": {
"latitude": "\u7def\u5ea6",
"longitude": "\u7d4c\u5ea6",
"name": "\u540d\u524d"
}
}
}
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4"
},
"step": {
"init": {
"data": {
"icon": "\uc544\uc774\ucf58",
"latitude": "\uc704\ub3c4",
"longitude": "\uacbd\ub3c4",
"name": "\uc774\ub984",
"passive": "\uc790\ub3d9\ud654 \uc804\uc6a9",
"radius": "\ubc18\uacbd"
},
"title": "\uad6c\uc5ed \uc124\uc815"
}
},
"title": "\uad6c\uc5ed"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Numm g\u00ebtt et schonn"
},
"step": {
"init": {
"data": {
"icon": "Ikone",
"latitude": "Breedegrad",
"longitude": "L\u00e4ngegrad",
"name": "Numm",
"passive": "Passif",
"radius": "Radius"
},
"title": "D\u00e9fin\u00e9iert Zone Parameter"
}
},
"title": "Zone"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Naam bestaat al"
},
"step": {
"init": {
"data": {
"icon": "Pictogram",
"latitude": "Breedtegraad",
"longitude": "Lengtegraad",
"name": "Naam",
"passive": "Passief",
"radius": "Straal"
},
"title": "Definieer zone parameters"
}
},
"title": "Zone"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Namnet eksisterar allereie"
},
"step": {
"init": {
"data": {
"icon": "Ikon",
"latitude": "Breiddegrad",
"longitude": "Lengdegrad",
"name": "Namn",
"passive": "Passiv",
"radius": "Radius"
},
"title": "Definer soneparameterar"
}
},
"title": "Sone"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Navnet eksisterer allerede"
},
"step": {
"init": {
"data": {
"icon": "Ikon",
"latitude": "Breddegrad",
"longitude": "Lengdegrad",
"name": "Navn",
"passive": "Passiv",
"radius": "Radius"
},
"title": "Definer sone parametere"
}
},
"title": "Sone"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Nazwa ju\u017c istnieje"
},
"step": {
"init": {
"data": {
"icon": "Ikona",
"latitude": "Szeroko\u015b\u0107 geograficzna",
"longitude": "D\u0142ugo\u015b\u0107 geograficzna",
"name": "Nazwa",
"passive": "Pasywnie",
"radius": "Promie\u0144"
},
"title": "Zdefiniuj parametry strefy"
}
},
"title": "Strefa"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "O nome j\u00e1 existe"
},
"step": {
"init": {
"data": {
"icon": "\u00cdcone",
"latitude": "Latitude",
"longitude": "Longitude",
"name": "Nome",
"passive": "Passivo",
"radius": "Raio"
},
"title": "Definir par\u00e2metros da zona"
}
},
"title": "Zona"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Nome j\u00e1 existente"
},
"step": {
"init": {
"data": {
"icon": "\u00cdcone",
"latitude": "Latitude",
"longitude": "Longitude",
"name": "Nome",
"passive": "Passivo",
"radius": "Raio"
},
"title": "Definir os par\u00e2metros da zona"
}
},
"title": "Zona"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f."
},
"step": {
"init": {
"data": {
"icon": "\u0417\u043d\u0430\u0447\u043e\u043a",
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430",
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
"passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u0430\u044f",
"radius": "\u0420\u0430\u0434\u0438\u0443\u0441"
},
"title": "\u0417\u043e\u043d\u0430"
}
},
"title": "\u0417\u043e\u043d\u0430"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Ime \u017ee obstaja"
},
"step": {
"init": {
"data": {
"icon": "Ikona",
"latitude": "Zemljepisna \u0161irina",
"longitude": "Zemljepisna dol\u017eina",
"name": "Ime",
"passive": "Pasivno",
"radius": "Radij"
},
"title": "Dolo\u010dite parametre obmo\u010dja"
}
},
"title": "Obmo\u010dje"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "Namnet finns redan"
},
"step": {
"init": {
"data": {
"icon": "Ikon",
"latitude": "Latitud",
"longitude": "Longitud",
"name": "Namn",
"passive": "Passiv",
"radius": "Radie"
},
"title": "Definiera zonparametrar"
}
},
"title": "Zon"
}
}

View File

@ -1,17 +0,0 @@
{
"config": {
"error": {
"name_exists": "\u0e21\u0e35\u0e0a\u0e37\u0e48\u0e2d\u0e19\u0e35\u0e49\u0e2d\u0e22\u0e39\u0e48\u0e41\u0e25\u0e49\u0e27"
},
"step": {
"init": {
"data": {
"latitude": "\u0e40\u0e2a\u0e49\u0e19\u0e23\u0e38\u0e49\u0e07",
"longitude": "\u0e40\u0e2a\u0e49\u0e19\u0e41\u0e27\u0e07",
"name": "\u0e0a\u0e37\u0e48\u0e2d"
}
}
},
"title": "\u0e42\u0e0b\u0e19"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "\u0406\u043c'\u044f \u0432\u0436\u0435 \u0456\u0441\u043d\u0443\u0454"
},
"step": {
"init": {
"data": {
"icon": "\u0406\u043a\u043e\u043d\u043a\u0430",
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
"longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430",
"name": "\u041d\u0430\u0437\u0432\u0430",
"passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u0438\u0439",
"radius": "\u0420\u0430\u0434\u0456\u0443\u0441"
},
"title": "\u0412\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u0437\u043e\u043d\u0438"
}
},
"title": "\u0417\u043e\u043d\u0430"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "T\u00ean \u0111\u00e3 t\u1ed3n t\u1ea1i"
},
"step": {
"init": {
"data": {
"icon": "Bi\u1ec3u t\u01b0\u1ee3ng",
"latitude": "V\u0129 \u0111\u1ed9",
"longitude": "Kinh \u0111\u1ed9",
"name": "T\u00ean",
"passive": "Th\u1ee5 \u0111\u1ed9ng",
"radius": "B\u00e1n k\u00ednh"
},
"title": "X\u00e1c \u0111\u1ecbnh tham s\u1ed1 v\u00f9ng"
}
},
"title": "V\u00f9ng"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "\u540d\u79f0\u5df2\u5b58\u5728"
},
"step": {
"init": {
"data": {
"icon": "\u56fe\u6807",
"latitude": "\u7eac\u5ea6",
"longitude": "\u7ecf\u5ea6",
"name": "\u540d\u79f0",
"passive": "\u88ab\u52a8",
"radius": "\u534a\u5f84"
},
"title": "\u5b9a\u4e49\u533a\u57df\u76f8\u5173\u53d8\u91cf"
}
},
"title": "\u533a\u57df"
}
}

View File

@ -1,21 +0,0 @@
{
"config": {
"error": {
"name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728"
},
"step": {
"init": {
"data": {
"icon": "\u5716\u793a",
"latitude": "\u7def\u5ea6",
"longitude": "\u7d93\u5ea6",
"name": "\u540d\u7a31",
"passive": "\u88ab\u52d5",
"radius": "\u534a\u5f91"
},
"title": "\u5b9a\u7fa9\u5340\u57df\u53c3\u6578"
}
},
"title": "\u5340\u57df"
}
}

View File

@ -1,36 +1,41 @@
"""Support for the definition of zones."""
import logging
from typing import Set, cast
from typing import Dict, List, Optional, cast
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
ATTR_EDITABLE,
ATTR_HIDDEN,
ATTR_LATITUDE,
ATTR_LONGITUDE,
CONF_ICON,
CONF_ID,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
CONF_RADIUS,
EVENT_CORE_CONFIG_UPDATE,
SERVICE_RELOAD,
)
from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback
from homeassistant.helpers import (
collection,
config_validation as cv,
entity,
entity_component,
entity_registry,
service,
storage,
)
from homeassistant.core import State, callback
from homeassistant.helpers import config_per_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.loader import bind_hass
from homeassistant.util import slugify
from homeassistant.util.location import distance
from .config_flow import configured_zones
from .const import ATTR_PASSIVE, ATTR_RADIUS, CONF_PASSIVE, DOMAIN, HOME_ZONE
from .zone import Zone
# mypy: allow-untyped-calls, allow-untyped-defs
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Unnamed zone"
DEFAULT_PASSIVE = False
DEFAULT_RADIUS = 100
@ -40,29 +45,47 @@ ENTITY_ID_HOME = ENTITY_ID_FORMAT.format(HOME_ZONE)
ICON_HOME = "mdi:home"
ICON_IMPORT = "mdi:import"
# The config that zone accepts is the same as if it has platforms.
PLATFORM_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_LATITUDE): cv.latitude,
vol.Required(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float),
vol.Optional(CONF_PASSIVE, default=DEFAULT_PASSIVE): cv.boolean,
vol.Optional(CONF_ICON): cv.icon,
},
CREATE_FIELDS = {
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_LATITUDE): cv.latitude,
vol.Required(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float),
vol.Optional(CONF_PASSIVE, default=DEFAULT_PASSIVE): cv.boolean,
vol.Optional(CONF_ICON): cv.icon,
}
UPDATE_FIELDS = {
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_RADIUS): vol.Coerce(float),
vol.Optional(CONF_PASSIVE): cv.boolean,
vol.Optional(CONF_ICON): cv.icon,
}
CONFIG_SCHEMA = vol.Schema(
{vol.Optional(DOMAIN): vol.All(cv.ensure_list, [vol.Schema(CREATE_FIELDS)])},
extra=vol.ALLOW_EXTRA,
)
RELOAD_SERVICE_SCHEMA = vol.Schema({})
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
@bind_hass
def async_active_zone(hass, latitude, longitude, radius=0):
def async_active_zone(
hass: HomeAssistant, latitude: float, longitude: float, radius: int = 0
) -> Optional[State]:
"""Find the active zone for given latitude, longitude.
This method must be run in the event loop.
"""
# Sort entity IDs so that we are deterministic if equal distance to 2 zones
zones = (
hass.states.get(entity_id)
cast(State, hass.states.get(entity_id))
for entity_id in sorted(hass.states.async_entity_ids(DOMAIN))
)
@ -80,6 +103,9 @@ def async_active_zone(hass, latitude, longitude, radius=0):
zone.attributes[ATTR_LONGITUDE],
)
if zone_dist is None:
continue
within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS]
closer_zone = closest is None or zone_dist < min_dist # type: ignore
smaller_zone = (
@ -95,79 +121,227 @@ def async_active_zone(hass, latitude, longitude, radius=0):
return closest
async def async_setup(hass, config):
"""Set up configured zones as well as Home Assistant zone if necessary."""
hass.data[DOMAIN] = {}
entities: Set[str] = set()
zone_entries = configured_zones(hass)
for _, entry in config_per_platform(config, DOMAIN):
if slugify(entry[CONF_NAME]) not in zone_entries:
zone = Zone(
hass,
entry[CONF_NAME],
entry[CONF_LATITUDE],
entry[CONF_LONGITUDE],
entry.get(CONF_RADIUS),
entry.get(CONF_ICON),
entry.get(CONF_PASSIVE),
)
zone.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, entry[CONF_NAME], entities
)
hass.async_create_task(zone.async_update_ha_state())
entities.add(zone.entity_id)
def in_zone(zone: State, latitude: float, longitude: float, radius: float = 0) -> bool:
"""Test if given latitude, longitude is in given zone.
if ENTITY_ID_HOME in entities or HOME_ZONE in zone_entries:
return True
zone = Zone(
hass,
hass.config.location_name,
hass.config.latitude,
hass.config.longitude,
DEFAULT_RADIUS,
ICON_HOME,
False,
Async friendly.
"""
zone_dist = distance(
latitude,
longitude,
zone.attributes[ATTR_LATITUDE],
zone.attributes[ATTR_LONGITUDE],
)
zone.entity_id = ENTITY_ID_HOME
hass.async_create_task(zone.async_update_ha_state())
if zone_dist is None or zone.attributes[ATTR_RADIUS] is None:
return False
return zone_dist - radius < cast(float, zone.attributes[ATTR_RADIUS])
class ZoneStorageCollection(collection.StorageCollection):
"""Zone collection stored in storage."""
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
async def _process_create_data(self, data: Dict) -> Dict:
"""Validate the config is valid."""
return cast(Dict, self.CREATE_SCHEMA(data))
@callback
def core_config_updated(_):
def _get_suggested_id(self, info: Dict) -> str:
"""Suggest an ID based on the config."""
return cast(str, info[CONF_NAME])
async def _update_data(self, data: dict, update_data: Dict) -> Dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return {**data, **update_data}
class IDLessCollection(collection.ObservableCollection):
"""A collection without IDs."""
counter = 0
async def async_load(self, data: List[dict]) -> None:
"""Load the collection. Overrides existing data."""
for item_id in list(self.data):
await self.notify_change(collection.CHANGE_REMOVED, item_id, None)
self.data.clear()
for item in data:
self.counter += 1
item_id = f"fakeid-{self.counter}"
self.data[item_id] = item
await self.notify_change(collection.CHANGE_ADDED, item_id, item)
async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
"""Set up configured zones as well as Home Assistant zone if necessary."""
component = entity_component.EntityComponent(_LOGGER, DOMAIN, hass)
id_manager = collection.IDManager()
yaml_collection = IDLessCollection(
logging.getLogger(f"{__name__}.yaml_collection"), id_manager
)
collection.attach_entity_component_collection(
component, yaml_collection, lambda conf: Zone(conf, False)
)
storage_collection = ZoneStorageCollection(
storage.Store(hass, STORAGE_VERSION, STORAGE_KEY),
logging.getLogger(f"{__name__}_storage_collection"),
id_manager,
)
collection.attach_entity_component_collection(
component, storage_collection, lambda conf: Zone(conf, True)
)
if DOMAIN in config:
await yaml_collection.async_load(config[DOMAIN])
await storage_collection.async_load()
collection.StorageCollectionWebsocket(
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
).async_setup(hass)
async def _collection_changed(
change_type: str, item_id: str, config: Optional[Dict]
) -> None:
"""Handle a collection change: clean up entity registry on removals."""
if change_type != collection.CHANGE_REMOVED:
return
ent_reg = await entity_registry.async_get_registry(hass)
ent_reg.async_remove(
cast(str, ent_reg.async_get_entity_id(DOMAIN, DOMAIN, item_id))
)
storage_collection.async_add_listener(_collection_changed)
async def reload_service_handler(service_call: ServiceCall) -> None:
"""Remove all zones and load new ones from config."""
conf = await component.async_prepare_reload(skip_reset=True)
if conf is None:
return
await yaml_collection.async_load(conf[DOMAIN])
service.async_register_admin_service(
hass,
DOMAIN,
SERVICE_RELOAD,
reload_service_handler,
schema=RELOAD_SERVICE_SCHEMA,
)
if component.get_entity("zone.home"):
return True
home_zone = Zone(_home_conf(hass), True,)
home_zone.entity_id = ENTITY_ID_HOME
await component.async_add_entities([home_zone]) # type: ignore
async def core_config_updated(_: Event) -> None:
"""Handle core config updated."""
zone.name = hass.config.location_name
zone.latitude = hass.config.latitude
zone.longitude = hass.config.longitude
zone.async_write_ha_state()
await home_zone.async_update_config(_home_conf(hass))
hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, core_config_updated)
hass.data[DOMAIN] = storage_collection
return True
async def async_setup_entry(hass, config_entry):
@callback
def _home_conf(hass: HomeAssistant) -> Dict:
"""Return the home zone config."""
return {
CONF_NAME: hass.config.location_name,
CONF_LATITUDE: hass.config.latitude,
CONF_LONGITUDE: hass.config.longitude,
CONF_RADIUS: DEFAULT_RADIUS,
CONF_ICON: ICON_HOME,
CONF_PASSIVE: False,
}
async def async_setup_entry(
hass: HomeAssistant, config_entry: config_entries.ConfigEntry
) -> bool:
"""Set up zone as config entry."""
entry = config_entry.data
name = entry[CONF_NAME]
zone = Zone(
hass,
name,
entry[CONF_LATITUDE],
entry[CONF_LONGITUDE],
entry.get(CONF_RADIUS, DEFAULT_RADIUS),
entry.get(CONF_ICON),
entry.get(CONF_PASSIVE, DEFAULT_PASSIVE),
)
zone.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, name, None, hass)
hass.async_create_task(zone.async_update_ha_state())
hass.data[DOMAIN][slugify(name)] = zone
storage_collection = cast(ZoneStorageCollection, hass.data[DOMAIN])
data = dict(config_entry.data)
data.setdefault(CONF_PASSIVE, DEFAULT_PASSIVE)
data.setdefault(CONF_RADIUS, DEFAULT_RADIUS)
await storage_collection.async_create_item(data)
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
return True
async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
zones = hass.data[DOMAIN]
name = slugify(config_entry.data[CONF_NAME])
zone = zones.pop(name)
await zone.async_remove()
async def async_unload_entry(
hass: HomeAssistant, config_entry: config_entries.ConfigEntry
) -> bool:
"""Will be called once we remove it."""
return True
class Zone(entity.Entity):
"""Representation of a Zone."""
def __init__(self, config: Dict, editable: bool):
"""Initialize the zone."""
self._config = config
self._editable = editable
self._attrs: Optional[Dict] = None
self._generate_attrs()
@property
def state(self) -> str:
"""Return the state property really does nothing for a zone."""
return "zoning"
@property
def name(self) -> str:
"""Return name."""
return cast(str, self._config[CONF_NAME])
@property
def unique_id(self) -> Optional[str]:
"""Return unique ID."""
return self._config.get(CONF_ID)
@property
def icon(self) -> Optional[str]:
"""Return the icon if any."""
return self._config.get(CONF_ICON)
@property
def state_attributes(self) -> Optional[Dict]:
"""Return the state attributes of the zone."""
return self._attrs
async def async_update_config(self, config: Dict) -> None:
"""Handle when the config is updated."""
self._config = config
self._generate_attrs()
self.async_write_ha_state()
@callback
def _generate_attrs(self) -> None:
"""Generate new attrs based on config."""
self._attrs = {
ATTR_HIDDEN: True,
ATTR_LATITUDE: self._config[CONF_LATITUDE],
ATTR_LONGITUDE: self._config[CONF_LONGITUDE],
ATTR_RADIUS: self._config[CONF_RADIUS],
ATTR_PASSIVE: self._config[CONF_PASSIVE],
ATTR_EDITABLE: self._editable,
}

View File

@ -1,75 +1,13 @@
"""Config flow to configure zone component."""
from typing import Set
import voluptuous as vol
"""Config flow to configure zone component.
This is no longer in use. This file is around so that existing
config entries will remain to be loaded and then automatically
migrated to the storage collection.
"""
from homeassistant import config_entries
from homeassistant.const import (
CONF_ICON,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
CONF_RADIUS,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import slugify
from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE
# mypy: allow-untyped-defs, no-check-untyped-defs
from .const import DOMAIN # noqa # pylint:disable=unused-import
@callback
def configured_zones(hass: HomeAssistantType) -> Set[str]:
"""Return a set of the configured zones."""
return set(
(slugify(entry.data[CONF_NAME]))
for entry in (
hass.config_entries.async_entries(DOMAIN) if hass.config_entries else []
)
)
@config_entries.HANDLERS.register(DOMAIN)
class ZoneFlowHandler(config_entries.ConfigFlow):
"""Zone config flow."""
VERSION = 1
def __init__(self):
"""Initialize zone configuration flow."""
pass
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
errors = {}
if user_input is not None:
name = slugify(user_input[CONF_NAME])
if name not in configured_zones(self.hass) and name != HOME_ZONE:
return self.async_create_entry(
title=user_input[CONF_NAME], data=user_input
)
errors["base"] = "name_exists"
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(CONF_NAME): str,
vol.Required(CONF_LATITUDE): cv.latitude,
vol.Required(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_RADIUS): vol.Coerce(float),
vol.Optional(CONF_ICON): str,
vol.Optional(CONF_PASSIVE): bool,
}
),
errors=errors,
)
class ZoneConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Stub zone config flow class."""

View File

@ -1,7 +1,7 @@
{
"domain": "zone",
"name": "Zone",
"config_flow": true,
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/zone",
"requirements": [],
"dependencies": [],

View File

@ -1,21 +0,0 @@
{
"config": {
"title": "Zone",
"step": {
"init": {
"title": "Define zone parameters",
"data": {
"name": "Name",
"latitude": "Latitude",
"longitude": "Longitude",
"radius": "Radius",
"passive": "Passive",
"icon": "Icon"
}
}
},
"error": {
"name_exists": "Name already exists"
}
}
}

View File

@ -1,71 +0,0 @@
"""Zone entity and functionality."""
from typing import cast
from homeassistant.const import ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE
from homeassistant.core import State
from homeassistant.helpers.entity import Entity
from homeassistant.util.location import distance
from .const import ATTR_PASSIVE, ATTR_RADIUS
STATE = "zoning"
# mypy: allow-untyped-defs
def in_zone(zone: State, latitude: float, longitude: float, radius: float = 0) -> bool:
"""Test if given latitude, longitude is in given zone.
Async friendly.
"""
zone_dist = distance(
latitude,
longitude,
zone.attributes[ATTR_LATITUDE],
zone.attributes[ATTR_LONGITUDE],
)
if zone_dist is None or zone.attributes[ATTR_RADIUS] is None:
return False
return zone_dist - radius < cast(float, zone.attributes[ATTR_RADIUS])
class Zone(Entity):
"""Representation of a Zone."""
name = None
def __init__(self, hass, name, latitude, longitude, radius, icon, passive):
"""Initialize the zone."""
self.hass = hass
self.name = name
self.latitude = latitude
self.longitude = longitude
self._radius = radius
self._icon = icon
self._passive = passive
@property
def state(self):
"""Return the state property really does nothing for a zone."""
return STATE
@property
def icon(self):
"""Return the icon if any."""
return self._icon
@property
def state_attributes(self):
"""Return the state attributes of the zone."""
data = {
ATTR_HIDDEN: True,
ATTR_LATITUDE: self.latitude,
ATTR_LONGITUDE: self.longitude,
ATTR_RADIUS: self._radius,
}
if self._passive:
data[ATTR_PASSIVE] = self._passive
return data

View File

@ -98,6 +98,5 @@ FLOWS = [
"wled",
"wwlln",
"zha",
"zone",
"zwave"
]

View File

@ -114,7 +114,7 @@ class ObservableCollection(ABC):
class YamlCollection(ObservableCollection):
"""Offer a fake CRUD interface on top of static YAML."""
"""Offer a collection based on static data."""
async def async_load(self, data: List[dict]) -> None:
"""Load the YAML collection. Overrides existing data."""
@ -133,7 +133,7 @@ class YamlCollection(ObservableCollection):
event = CHANGE_ADDED
self.data[item_id] = item
await self.notify_change(event, item[CONF_ID], item)
await self.notify_change(event, item_id, item)
for item_id in old_ids:
self.data.pop(item_id)
@ -246,7 +246,7 @@ def attach_entity_component_collection(
"""Handle a collection change."""
if change_type == CHANGE_ADDED:
entity = create_entity(cast(dict, config))
await entity_component.async_add_entities([entity])
await entity_component.async_add_entities([entity]) # type: ignore
entities[item_id] = entity
return

View File

@ -473,7 +473,7 @@ def zone(
if latitude is None or longitude is None:
return False
return zone_cmp.zone.in_zone(
return zone_cmp.in_zone(
zone_ent, latitude, longitude, entity.attributes.get(ATTR_GPS_ACCURACY, 0)
)

View File

@ -284,7 +284,7 @@ class Entity(ABC):
self._async_write_ha_state()
@callback
def async_write_ha_state(self):
def async_write_ha_state(self) -> None:
"""Write the state to the state machine."""
if self.hass is None:
raise RuntimeError(f"Attribute hass is None for {self}")
@ -294,7 +294,7 @@ class Entity(ABC):
f"No entity id specified for entity {self.name}"
)
self._async_write_ha_state()
self._async_write_ha_state() # type: ignore
@callback
def _async_write_ha_state(self):

View File

@ -3,6 +3,8 @@ import asyncio
from datetime import timedelta
from itertools import chain
import logging
from types import ModuleType
from typing import Dict, Optional, cast
from homeassistant import config as conf_util
from homeassistant.config_entries import ConfigEntry
@ -13,6 +15,7 @@ from homeassistant.helpers import (
config_per_platform,
config_validation as cv,
discovery,
entity,
service,
)
from homeassistant.loader import async_get_integration, bind_hass
@ -38,15 +41,15 @@ async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None:
)
return
entity = entity_comp.get_entity(entity_id)
entity_obj = entity_comp.get_entity(entity_id)
if entity is None:
if entity_obj is None:
logging.getLogger(__name__).warning(
"Forced update failed. Entity %s not found.", entity_id
)
return
await entity.async_update_ha_state(True)
await entity_obj.async_update_ha_state(True)
class EntityComponent:
@ -59,7 +62,13 @@ class EntityComponent:
- Listen for discovery events for platforms related to the domain.
"""
def __init__(self, logger, domain, hass, scan_interval=DEFAULT_SCAN_INTERVAL):
def __init__(
self,
logger: logging.Logger,
domain: str,
hass: HomeAssistant,
scan_interval: timedelta = DEFAULT_SCAN_INTERVAL,
):
"""Initialize an entity component."""
self.logger = logger
self.hass = hass
@ -68,7 +77,9 @@ class EntityComponent:
self.config = None
self._platforms = {domain: self._async_init_entity_platform(domain, None)}
self._platforms: Dict[str, EntityPlatform] = {
domain: self._async_init_entity_platform(domain, None)
}
self.async_add_entities = self._platforms[domain].async_add_entities
self.add_entities = self._platforms[domain].add_entities
@ -81,12 +92,12 @@ class EntityComponent:
platform.entities.values() for platform in self._platforms.values()
)
def get_entity(self, entity_id):
def get_entity(self, entity_id: str) -> Optional[entity.Entity]:
"""Get an entity."""
for platform in self._platforms.values():
entity = platform.entities.get(entity_id)
if entity is not None:
return entity
entity_obj = cast(Optional[entity.Entity], platform.entities.get(entity_id))
if entity_obj is not None:
return entity_obj
return None
def setup(self, config):
@ -237,7 +248,7 @@ class EntityComponent:
if entity_id in platform.entities:
await platform.async_remove_entity(entity_id)
async def async_prepare_reload(self, *, skip_reset=False):
async def async_prepare_reload(self, *, skip_reset: bool = False) -> Optional[dict]:
"""Prepare reloading this entity component.
This method must be run in the event loop.
@ -250,25 +261,30 @@ class EntityComponent:
integration = await async_get_integration(self.hass, self.domain)
conf = await conf_util.async_process_component_config(
processed_conf = await conf_util.async_process_component_config(
self.hass, conf, integration
)
if conf is None:
if processed_conf is None:
return None
if not skip_reset:
await self._async_reset()
return conf
return processed_conf
def _async_init_entity_platform(
self, platform_type, platform, scan_interval=None, entity_namespace=None
):
self,
platform_type: str,
platform: Optional[ModuleType],
scan_interval: Optional[timedelta] = None,
entity_namespace: Optional[str] = None,
) -> EntityPlatform:
"""Initialize an entity platform."""
if scan_interval is None:
scan_interval = self.scan_interval
return EntityPlatform(
return EntityPlatform( # type: ignore
hass=self.hass,
logger=self.logger,
domain=self.domain,

View File

@ -14,6 +14,7 @@ class TestProximity(unittest.TestCase):
def setUp(self):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.hass.config.components.add("zone")
self.hass.states.set(
"zone.home",
"zoning",
@ -211,7 +212,7 @@ class TestProximity(unittest.TestCase):
self.hass.block_till_done()
state = self.hass.states.get("proximity.home")
assert state.attributes.get("nearest") == "test1"
assert state.attributes.get("dir_of_travel") == "towards"
assert state.attributes.get("dir_of_travel") == "away_from"
def test_device_tracker_test1_awaycloser(self):
"""Test for tracker state away closer."""
@ -245,7 +246,7 @@ class TestProximity(unittest.TestCase):
self.hass.block_till_done()
state = self.hass.states.get("proximity.home")
assert state.attributes.get("nearest") == "test1"
assert state.attributes.get("dir_of_travel") == "away_from"
assert state.attributes.get("dir_of_travel") == "towards"
def test_all_device_trackers_in_ignored_zone(self):
"""Test for tracker in ignored zone."""

View File

@ -1,60 +0,0 @@
"""Tests for zone config flow."""
from homeassistant.components.zone import config_flow
from homeassistant.components.zone.const import CONF_PASSIVE, DOMAIN, HOME_ZONE
from homeassistant.const import (
CONF_ICON,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
CONF_RADIUS,
)
from tests.common import MockConfigEntry
async def test_flow_works(hass):
"""Test that config flow works."""
flow = config_flow.ZoneFlowHandler()
flow.hass = hass
result = await flow.async_step_init(
user_input={
CONF_NAME: "Name",
CONF_LATITUDE: "1.1",
CONF_LONGITUDE: "2.2",
CONF_RADIUS: "100",
CONF_ICON: "mdi:home",
CONF_PASSIVE: True,
}
)
assert result["type"] == "create_entry"
assert result["title"] == "Name"
assert result["data"] == {
CONF_NAME: "Name",
CONF_LATITUDE: "1.1",
CONF_LONGITUDE: "2.2",
CONF_RADIUS: "100",
CONF_ICON: "mdi:home",
CONF_PASSIVE: True,
}
async def test_flow_requires_unique_name(hass):
"""Test that config flow verifies that each zones name is unique."""
MockConfigEntry(domain=DOMAIN, data={CONF_NAME: "Name"}).add_to_hass(hass)
flow = config_flow.ZoneFlowHandler()
flow.hass = hass
result = await flow.async_step_init(user_input={CONF_NAME: "Name"})
assert result["errors"] == {"base": "name_exists"}
async def test_flow_requires_name_different_from_home(hass):
"""Test that config flow verifies that each zones name is unique."""
flow = config_flow.ZoneFlowHandler()
flow.hass = hass
result = await flow.async_step_init(user_input={CONF_NAME: HOME_ZONE})
assert result["errors"] == {"base": "name_exists"}

View File

@ -1,229 +1,224 @@
"""Test zone component."""
import unittest
from unittest.mock import Mock
from asynctest 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,
SERVICE_RELOAD,
)
from homeassistant.core import Context
from homeassistant.exceptions import Unauthorized
from homeassistant.helpers import entity_registry
from tests.common import MockConfigEntry, get_test_home_assistant
from tests.common import MockConfigEntry
async def test_setup_entry_successful(hass):
"""Test setup entry is successful."""
entry = Mock()
entry.data = {
zone.CONF_NAME: "Test Zone",
zone.CONF_LATITUDE: 1.1,
zone.CONF_LONGITUDE: -2.2,
zone.CONF_RADIUS: True,
@pytest.fixture
def storage_setup(hass, hass_storage):
"""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):
"""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):
"""Test a successful setup."""
info = {
"name": "Test Zone",
"latitude": 32.880837,
"longitude": -117.237561,
"radius": 250,
"passive": True,
}
hass.data[zone.DOMAIN] = {}
assert await zone.async_setup_entry(hass, entry) is True
assert "test_zone" in hass.data[zone.DOMAIN]
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_unload_entry_successful(hass):
"""Test unload entry is successful."""
entry = Mock()
entry.data = {
zone.CONF_NAME: "Test Zone",
zone.CONF_LATITUDE: 1.1,
zone.CONF_LONGITUDE: -2.2,
}
hass.data[zone.DOMAIN] = {}
assert await zone.async_setup_entry(hass, entry) is True
assert await zone.async_unload_entry(hass, entry) is True
assert not hass.data[zone.DOMAIN]
async def test_setup_zone_skips_home_zone(hass):
"""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
class TestComponentZone(unittest.TestCase):
"""Test the zone component."""
async def test_setup_name_can_be_same_on_multiple_zones(hass):
"""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
def setUp(self): # pylint: disable=invalid-name
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
def tearDown(self): # pylint: disable=invalid-name
"""Stop down everything that was started."""
self.hass.stop()
async def test_active_zone_skips_passive_zones(hass):
"""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
def test_setup_no_zones_still_adds_home_zone(self):
"""Test if no config is passed in we still get the home zone."""
assert setup.setup_component(self.hass, zone.DOMAIN, {"zone": None})
assert len(self.hass.states.entity_ids("zone")) == 1
state = self.hass.states.get("zone.home")
assert self.hass.config.location_name == state.name
assert self.hass.config.latitude == state.attributes["latitude"]
assert self.hass.config.longitude == state.attributes["longitude"]
assert not state.attributes.get("passive", False)
def test_setup(self):
"""Test a successful setup."""
info = {
"name": "Test Zone",
"latitude": 32.880837,
"longitude": -117.237561,
"radius": 250,
"passive": True,
}
assert setup.setup_component(self.hass, zone.DOMAIN, {"zone": info})
async def test_active_zone_skips_passive_zones_2(hass):
"""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 "zone.active_zone" == active.entity_id
assert len(self.hass.states.entity_ids("zone")) == 2
state = self.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"]
def test_setup_zone_skips_home_zone(self):
"""Test that zone named Home should override hass home zone."""
info = {"name": "Home", "latitude": 1.1, "longitude": -2.2}
assert setup.setup_component(self.hass, zone.DOMAIN, {"zone": info})
async def test_active_zone_prefers_smaller_zone_if_same_distance(hass):
"""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,
},
]
},
)
assert len(self.hass.states.entity_ids("zone")) == 1
state = self.hass.states.get("zone.home")
assert info["name"] == state.name
active = zone.async_active_zone(hass, latitude, longitude)
assert "zone.small_zone" == active.entity_id
def test_setup_name_can_be_same_on_multiple_zones(self):
"""Test that zone named Home should override hass home zone."""
info = {"name": "Test Zone", "latitude": 1.1, "longitude": -2.2}
assert setup.setup_component(self.hass, zone.DOMAIN, {"zone": [info, info]})
assert len(self.hass.states.entity_ids("zone")) == 3
def test_setup_registered_zone_skips_home_zone(self):
"""Test that config entry named home should override hass home zone."""
entry = MockConfigEntry(domain=zone.DOMAIN, data={zone.CONF_NAME: "home"})
entry.add_to_hass(self.hass)
assert setup.setup_component(self.hass, zone.DOMAIN, {"zone": None})
assert len(self.hass.states.entity_ids("zone")) == 0
async def test_active_zone_prefers_smaller_zone_if_same_distance_2(hass):
"""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,
}
]
},
)
def test_setup_registered_zone_skips_configured_zone(self):
"""Test if config entry will override configured zone."""
entry = MockConfigEntry(domain=zone.DOMAIN, data={zone.CONF_NAME: "Test Zone"})
entry.add_to_hass(self.hass)
info = {"name": "Test Zone", "latitude": 1.1, "longitude": -2.2}
assert setup.setup_component(self.hass, zone.DOMAIN, {"zone": info})
active = zone.async_active_zone(hass, latitude, longitude)
assert "zone.smallest_zone" == active.entity_id
assert len(self.hass.states.entity_ids("zone")) == 1
state = self.hass.states.get("zone.test_zone")
assert not state
def test_active_zone_skips_passive_zones(self):
"""Test active and passive zones."""
assert setup.setup_component(
self.hass,
zone.DOMAIN,
{
"zone": [
{
"name": "Passive Zone",
"latitude": 32.880600,
"longitude": -117.237561,
"radius": 250,
"passive": True,
}
]
},
)
self.hass.block_till_done()
active = zone.async_active_zone(self.hass, 32.880600, -117.237561)
assert active is None
async def test_in_zone_works_for_passive_zones(hass):
"""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,
}
]
},
)
def test_active_zone_skips_passive_zones_2(self):
"""Test active and passive zones."""
assert setup.setup_component(
self.hass,
zone.DOMAIN,
{
"zone": [
{
"name": "Active Zone",
"latitude": 32.880800,
"longitude": -117.237561,
"radius": 500,
}
]
},
)
self.hass.block_till_done()
active = zone.async_active_zone(self.hass, 32.880700, -117.237561)
assert "zone.active_zone" == active.entity_id
def test_active_zone_prefers_smaller_zone_if_same_distance(self):
"""Test zone size preferences."""
latitude = 32.880600
longitude = -117.237561
assert setup.setup_component(
self.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(self.hass, latitude, longitude)
assert "zone.small_zone" == active.entity_id
def test_active_zone_prefers_smaller_zone_if_same_distance_2(self):
"""Test zone size preferences."""
latitude = 32.880600
longitude = -117.237561
assert setup.setup_component(
self.hass,
zone.DOMAIN,
{
"zone": [
{
"name": "Smallest Zone",
"latitude": latitude,
"longitude": longitude,
"radius": 50,
}
]
},
)
active = zone.async_active_zone(self.hass, latitude, longitude)
assert "zone.smallest_zone" == active.entity_id
def test_in_zone_works_for_passive_zones(self):
"""Test working in passive zones."""
latitude = 32.880600
longitude = -117.237561
assert setup.setup_component(
self.hass,
zone.DOMAIN,
{
"zone": [
{
"name": "Passive Zone",
"latitude": latitude,
"longitude": longitude,
"radius": 250,
"passive": True,
}
]
},
)
assert zone.zone.in_zone(
self.hass.states.get("zone.passive_zone"), latitude, longitude
)
assert zone.in_zone(hass.states.get("zone.passive_zone"), latitude, longitude)
async def test_core_config_update(hass):
@ -243,3 +238,252 @@ async def test_core_config_update(hass):
assert home_updated.name == "Updated Name"
assert home_updated.attributes["latitude"] == 10
assert home_updated.attributes["longitude"] == 20
async def test_reload(hass, hass_admin_user, hass_read_only_user):
"""Test reload service."""
count_start = len(hass.states.async_entity_ids())
ent_reg = await entity_registry.async_get_registry(hass)
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(ent_reg.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, 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.name == "from storage"
assert state.attributes.get(ATTR_EDITABLE)
async def test_editable_state_attribute(hass, storage_setup):
"""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 == "zoning"
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 not state.attributes.get(ATTR_EDITABLE)
async def test_ws_list(hass, hass_ws_client, storage_setup):
"""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, hass_ws_client, storage_setup):
"""Test WS delete cleans up entity registry."""
assert await storage_setup()
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.{input_id}"
ent_reg = await entity_registry.async_get_registry(hass)
state = hass.states.get(input_entity_id)
assert state is not None
assert ent_reg.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 ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None
async def test_update(hass, hass_ws_client, storage_setup):
"""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}"
ent_reg = await entity_registry.async_get_registry(hass)
state = hass.states.get(input_entity_id)
assert state.attributes["latitude"] == 1
assert state.attributes["longitude"] == 2
assert ent_reg.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, hass_ws_client, storage_setup):
"""Test create WS."""
assert await storage_setup(items=[])
input_id = "new_input"
input_entity_id = f"{DOMAIN}.{input_id}"
ent_reg = await entity_registry.async_get_registry(hass)
state = hass.states.get(input_entity_id)
assert state is None
assert ent_reg.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 == "zoning"
assert state.attributes["latitude"] == 3
assert state.attributes["longitude"] == 4
assert state.attributes["passive"] is True
async def test_import_config_entry(hass):
"""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"