mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Migrate zone to use collection helpers (#30774)
This commit is contained in:
parent
4c27d6b9aa
commit
0fba9e44ed
@ -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()
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"icon": "Ikoon",
|
||||
"latitude": "Laius",
|
||||
"longitude": "Pikkus",
|
||||
"name": "Nimi",
|
||||
"radius": "Raadius"
|
||||
},
|
||||
"title": "M\u00e4\u00e4ra tsooni parameetrid"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"latitude": "\u7def\u5ea6",
|
||||
"longitude": "\u7d4c\u5ea6",
|
||||
"name": "\u540d\u524d"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "zone",
|
||||
"name": "Zone",
|
||||
"config_flow": true,
|
||||
"config_flow": false,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zone",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -98,6 +98,5 @@ FLOWS = [
|
||||
"wled",
|
||||
"wwlln",
|
||||
"zha",
|
||||
"zone",
|
||||
"zwave"
|
||||
]
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
@ -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"}
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user