mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
Split out geofency with a component and platform (#17933)
* Split out geofency with a component and platform * Make geofency component/device_tracker more async * Move geofency tests to new package * Remove coroutine in geofency callback * Lint * Fix coroutine in geofency callback * Fix incorrect patch
This commit is contained in:
parent
c41ca37a04
commit
bdba3852d0
@ -4,129 +4,26 @@ Support for the Geofency platform.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.geofency/
|
||||
"""
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE, ATTR_LONGITUDE, HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.components.geofency import TRACKER_UPDATE
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
ATTR_CURRENT_LATITUDE = 'currentLatitude'
|
||||
ATTR_CURRENT_LONGITUDE = 'currentLongitude'
|
||||
|
||||
BEACON_DEV_PREFIX = 'beacon'
|
||||
CONF_MOBILE_BEACONS = 'mobile_beacons'
|
||||
|
||||
LOCATION_ENTRY = '1'
|
||||
LOCATION_EXIT = '0'
|
||||
|
||||
URL = '/api/geofency'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MOBILE_BEACONS): vol.All(
|
||||
cv.ensure_list, [cv.string]),
|
||||
})
|
||||
DEPENDENCIES = ['geofency']
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Set up an endpoint for the Geofency application."""
|
||||
mobile_beacons = config.get(CONF_MOBILE_BEACONS) or []
|
||||
|
||||
hass.http.register_view(GeofencyView(see, mobile_beacons))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class GeofencyView(HomeAssistantView):
|
||||
"""View to handle Geofency requests."""
|
||||
|
||||
url = URL
|
||||
name = 'api:geofency'
|
||||
|
||||
def __init__(self, see, mobile_beacons):
|
||||
"""Initialize Geofency url endpoints."""
|
||||
self.see = see
|
||||
self.mobile_beacons = [slugify(beacon) for beacon in mobile_beacons]
|
||||
|
||||
async def post(self, request):
|
||||
"""Handle Geofency requests."""
|
||||
data = await request.post()
|
||||
hass = request.app['hass']
|
||||
|
||||
data = self._validate_data(data)
|
||||
if not data:
|
||||
return ("Invalid data", HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if self._is_mobile_beacon(data):
|
||||
return await self._set_location(hass, data, None)
|
||||
if data['entry'] == LOCATION_ENTRY:
|
||||
location_name = data['name']
|
||||
else:
|
||||
location_name = STATE_NOT_HOME
|
||||
if ATTR_CURRENT_LATITUDE in data:
|
||||
data[ATTR_LATITUDE] = data[ATTR_CURRENT_LATITUDE]
|
||||
data[ATTR_LONGITUDE] = data[ATTR_CURRENT_LONGITUDE]
|
||||
|
||||
return await self._set_location(hass, data, location_name)
|
||||
|
||||
@staticmethod
|
||||
def _validate_data(data):
|
||||
"""Validate POST payload."""
|
||||
data = data.copy()
|
||||
|
||||
required_attributes = ['address', 'device', 'entry',
|
||||
'latitude', 'longitude', 'name']
|
||||
|
||||
valid = True
|
||||
for attribute in required_attributes:
|
||||
if attribute not in data:
|
||||
valid = False
|
||||
_LOGGER.error("'%s' not specified in message", attribute)
|
||||
|
||||
if not valid:
|
||||
return False
|
||||
|
||||
data['address'] = data['address'].replace('\n', ' ')
|
||||
data['device'] = slugify(data['device'])
|
||||
data['name'] = slugify(data['name'])
|
||||
|
||||
gps_attributes = [ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
ATTR_CURRENT_LATITUDE, ATTR_CURRENT_LONGITUDE]
|
||||
|
||||
for attribute in gps_attributes:
|
||||
if attribute in data:
|
||||
data[attribute] = float(data[attribute])
|
||||
|
||||
return data
|
||||
|
||||
def _is_mobile_beacon(self, data):
|
||||
"""Check if we have a mobile beacon."""
|
||||
return 'beaconUUID' in data and data['name'] in self.mobile_beacons
|
||||
|
||||
@staticmethod
|
||||
def _device_name(data):
|
||||
"""Return name of device tracker."""
|
||||
if 'beaconUUID' in data:
|
||||
return "{}_{}".format(BEACON_DEV_PREFIX, data['name'])
|
||||
return data['device']
|
||||
|
||||
async def _set_location(self, hass, data, location_name):
|
||||
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Set up the Geofency device tracker."""
|
||||
async def _set_location(device, gps, location_name, attributes):
|
||||
"""Fire HA event to set location."""
|
||||
device = self._device_name(data)
|
||||
await async_see(
|
||||
dev_id=device,
|
||||
gps=gps,
|
||||
location_name=location_name,
|
||||
attributes=attributes
|
||||
)
|
||||
|
||||
await hass.async_add_job(
|
||||
partial(self.see, dev_id=device,
|
||||
gps=(data[ATTR_LATITUDE], data[ATTR_LONGITUDE]),
|
||||
location_name=location_name,
|
||||
attributes=data))
|
||||
|
||||
return "Setting location for {}".format(device)
|
||||
async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location)
|
||||
return True
|
||||
|
146
homeassistant/components/geofency/__init__.py
Normal file
146
homeassistant/components/geofency/__init__.py
Normal file
@ -0,0 +1,146 @@
|
||||
"""
|
||||
Support for Geofency.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/geofency/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME, \
|
||||
ATTR_LATITUDE, ATTR_LONGITUDE
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'geofency'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
CONF_MOBILE_BEACONS = 'mobile_beacons'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(DOMAIN): vol.Schema({
|
||||
vol.Optional(CONF_MOBILE_BEACONS, default=[]): vol.All(
|
||||
cv.ensure_list,
|
||||
[cv.string]
|
||||
),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
ATTR_CURRENT_LATITUDE = 'currentLatitude'
|
||||
ATTR_CURRENT_LONGITUDE = 'currentLongitude'
|
||||
|
||||
BEACON_DEV_PREFIX = 'beacon'
|
||||
|
||||
LOCATION_ENTRY = '1'
|
||||
LOCATION_EXIT = '0'
|
||||
|
||||
URL = '/api/geofency'
|
||||
|
||||
TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass, hass_config):
|
||||
"""Set up the Geofency component."""
|
||||
config = hass_config[DOMAIN]
|
||||
mobile_beacons = config[CONF_MOBILE_BEACONS]
|
||||
hass.data[DOMAIN] = [slugify(beacon) for beacon in mobile_beacons]
|
||||
hass.http.register_view(GeofencyView(hass.data[DOMAIN]))
|
||||
|
||||
hass.async_create_task(
|
||||
async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class GeofencyView(HomeAssistantView):
|
||||
"""View to handle Geofency requests."""
|
||||
|
||||
url = URL
|
||||
name = 'api:geofency'
|
||||
|
||||
def __init__(self, mobile_beacons):
|
||||
"""Initialize Geofency url endpoints."""
|
||||
self.mobile_beacons = mobile_beacons
|
||||
|
||||
async def post(self, request):
|
||||
"""Handle Geofency requests."""
|
||||
data = await request.post()
|
||||
hass = request.app['hass']
|
||||
|
||||
data = self._validate_data(data)
|
||||
if not data:
|
||||
return "Invalid data", HTTP_UNPROCESSABLE_ENTITY
|
||||
|
||||
if self._is_mobile_beacon(data):
|
||||
return await self._set_location(hass, data, None)
|
||||
if data['entry'] == LOCATION_ENTRY:
|
||||
location_name = data['name']
|
||||
else:
|
||||
location_name = STATE_NOT_HOME
|
||||
if ATTR_CURRENT_LATITUDE in data:
|
||||
data[ATTR_LATITUDE] = data[ATTR_CURRENT_LATITUDE]
|
||||
data[ATTR_LONGITUDE] = data[ATTR_CURRENT_LONGITUDE]
|
||||
|
||||
return await self._set_location(hass, data, location_name)
|
||||
|
||||
@staticmethod
|
||||
def _validate_data(data):
|
||||
"""Validate POST payload."""
|
||||
data = data.copy()
|
||||
|
||||
required_attributes = ['address', 'device', 'entry',
|
||||
'latitude', 'longitude', 'name']
|
||||
|
||||
valid = True
|
||||
for attribute in required_attributes:
|
||||
if attribute not in data:
|
||||
valid = False
|
||||
_LOGGER.error("'%s' not specified in message", attribute)
|
||||
|
||||
if not valid:
|
||||
return {}
|
||||
|
||||
data['address'] = data['address'].replace('\n', ' ')
|
||||
data['device'] = slugify(data['device'])
|
||||
data['name'] = slugify(data['name'])
|
||||
|
||||
gps_attributes = [ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
ATTR_CURRENT_LATITUDE, ATTR_CURRENT_LONGITUDE]
|
||||
|
||||
for attribute in gps_attributes:
|
||||
if attribute in data:
|
||||
data[attribute] = float(data[attribute])
|
||||
|
||||
return data
|
||||
|
||||
def _is_mobile_beacon(self, data):
|
||||
"""Check if we have a mobile beacon."""
|
||||
return 'beaconUUID' in data and data['name'] in self.mobile_beacons
|
||||
|
||||
@staticmethod
|
||||
def _device_name(data):
|
||||
"""Return name of device tracker."""
|
||||
if 'beaconUUID' in data:
|
||||
return "{}_{}".format(BEACON_DEV_PREFIX, data['name'])
|
||||
return data['device']
|
||||
|
||||
async def _set_location(self, hass, data, location_name):
|
||||
"""Fire HA event to set location."""
|
||||
device = self._device_name(data)
|
||||
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
TRACKER_UPDATE,
|
||||
device,
|
||||
(data[ATTR_LATITUDE], data[ATTR_LONGITUDE]),
|
||||
location_name,
|
||||
data
|
||||
)
|
||||
|
||||
return "Setting location for {}".format(device)
|
@ -1322,19 +1322,19 @@ class TestDeviceTrackerOwnTrackConfigs(BaseMQTT):
|
||||
mock_component(self.hass, 'group')
|
||||
mock_component(self.hass, 'zone')
|
||||
|
||||
patch_load = patch(
|
||||
self.patch_load = patch(
|
||||
'homeassistant.components.device_tracker.async_load_config',
|
||||
return_value=mock_coro([]))
|
||||
patch_load.start()
|
||||
self.addCleanup(patch_load.stop)
|
||||
self.patch_load.start()
|
||||
|
||||
patch_save = patch('homeassistant.components.device_tracker.'
|
||||
'DeviceTracker.async_update_config')
|
||||
patch_save.start()
|
||||
self.addCleanup(patch_save.stop)
|
||||
self.patch_save = patch('homeassistant.components.device_tracker.'
|
||||
'DeviceTracker.async_update_config')
|
||||
self.patch_save.start()
|
||||
|
||||
def teardown_method(self, method):
|
||||
"""Tear down resources."""
|
||||
self.patch_load.stop()
|
||||
self.patch_save.stop()
|
||||
self.hass.stop()
|
||||
|
||||
@patch('homeassistant.components.device_tracker.owntracks.get_cipher',
|
||||
|
1
tests/components/geofency/__init__.py
Normal file
1
tests/components/geofency/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Geofency component."""
|
@ -1,16 +1,14 @@
|
||||
"""The tests for the Geofency device tracker platform."""
|
||||
# pylint: disable=redefined-outer-name
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import zone
|
||||
import homeassistant.components.device_tracker as device_tracker
|
||||
from homeassistant.components.device_tracker.geofency import (
|
||||
CONF_MOBILE_BEACONS, URL)
|
||||
from homeassistant.components.geofency import (
|
||||
CONF_MOBILE_BEACONS, URL, DOMAIN)
|
||||
from homeassistant.const import (
|
||||
CONF_PLATFORM, HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, STATE_HOME,
|
||||
HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, STATE_HOME,
|
||||
STATE_NOT_HOME)
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import slugify
|
||||
@ -110,9 +108,8 @@ BEACON_EXIT_CAR = {
|
||||
def geofency_client(loop, hass, aiohttp_client):
|
||||
"""Geofency mock client."""
|
||||
assert loop.run_until_complete(async_setup_component(
|
||||
hass, device_tracker.DOMAIN, {
|
||||
device_tracker.DOMAIN: {
|
||||
CONF_PLATFORM: 'geofency',
|
||||
hass, DOMAIN, {
|
||||
DOMAIN: {
|
||||
CONF_MOBILE_BEACONS: ['Car 1']
|
||||
}}))
|
||||
|
||||
@ -133,11 +130,10 @@ def setup_zones(loop, hass):
|
||||
}}))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_data_validation(geofency_client):
|
||||
async def test_data_validation(geofency_client):
|
||||
"""Test data validation."""
|
||||
# No data
|
||||
req = yield from geofency_client.post(URL)
|
||||
req = await geofency_client.post(URL)
|
||||
assert req.status == HTTP_UNPROCESSABLE_ENTITY
|
||||
|
||||
missing_attributes = ['address', 'device',
|
||||
@ -147,15 +143,15 @@ def test_data_validation(geofency_client):
|
||||
for attribute in missing_attributes:
|
||||
copy = GPS_ENTER_HOME.copy()
|
||||
del copy[attribute]
|
||||
req = yield from geofency_client.post(URL, data=copy)
|
||||
req = await geofency_client.post(URL, data=copy)
|
||||
assert req.status == HTTP_UNPROCESSABLE_ENTITY
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_gps_enter_and_exit_home(hass, geofency_client):
|
||||
async def test_gps_enter_and_exit_home(hass, geofency_client):
|
||||
"""Test GPS based zone enter and exit."""
|
||||
# Enter the Home zone
|
||||
req = yield from geofency_client.post(URL, data=GPS_ENTER_HOME)
|
||||
req = await geofency_client.post(URL, data=GPS_ENTER_HOME)
|
||||
await hass.async_block_till_done()
|
||||
assert req.status == HTTP_OK
|
||||
device_name = slugify(GPS_ENTER_HOME['device'])
|
||||
state_name = hass.states.get('{}.{}'.format(
|
||||
@ -163,7 +159,8 @@ def test_gps_enter_and_exit_home(hass, geofency_client):
|
||||
assert STATE_HOME == state_name
|
||||
|
||||
# Exit the Home zone
|
||||
req = yield from geofency_client.post(URL, data=GPS_EXIT_HOME)
|
||||
req = await geofency_client.post(URL, data=GPS_EXIT_HOME)
|
||||
await hass.async_block_till_done()
|
||||
assert req.status == HTTP_OK
|
||||
device_name = slugify(GPS_EXIT_HOME['device'])
|
||||
state_name = hass.states.get('{}.{}'.format(
|
||||
@ -175,7 +172,8 @@ def test_gps_enter_and_exit_home(hass, geofency_client):
|
||||
data['currentLatitude'] = NOT_HOME_LATITUDE
|
||||
data['currentLongitude'] = NOT_HOME_LONGITUDE
|
||||
|
||||
req = yield from geofency_client.post(URL, data=data)
|
||||
req = await geofency_client.post(URL, data=data)
|
||||
await hass.async_block_till_done()
|
||||
assert req.status == HTTP_OK
|
||||
device_name = slugify(GPS_EXIT_HOME['device'])
|
||||
current_latitude = hass.states.get('{}.{}'.format(
|
||||
@ -186,11 +184,11 @@ def test_gps_enter_and_exit_home(hass, geofency_client):
|
||||
assert NOT_HOME_LONGITUDE == current_longitude
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_beacon_enter_and_exit_home(hass, geofency_client):
|
||||
async def test_beacon_enter_and_exit_home(hass, geofency_client):
|
||||
"""Test iBeacon based zone enter and exit - a.k.a stationary iBeacon."""
|
||||
# Enter the Home zone
|
||||
req = yield from geofency_client.post(URL, data=BEACON_ENTER_HOME)
|
||||
req = await geofency_client.post(URL, data=BEACON_ENTER_HOME)
|
||||
await hass.async_block_till_done()
|
||||
assert req.status == HTTP_OK
|
||||
device_name = slugify("beacon_{}".format(BEACON_ENTER_HOME['name']))
|
||||
state_name = hass.states.get('{}.{}'.format(
|
||||
@ -198,7 +196,8 @@ def test_beacon_enter_and_exit_home(hass, geofency_client):
|
||||
assert STATE_HOME == state_name
|
||||
|
||||
# Exit the Home zone
|
||||
req = yield from geofency_client.post(URL, data=BEACON_EXIT_HOME)
|
||||
req = await geofency_client.post(URL, data=BEACON_EXIT_HOME)
|
||||
await hass.async_block_till_done()
|
||||
assert req.status == HTTP_OK
|
||||
device_name = slugify("beacon_{}".format(BEACON_ENTER_HOME['name']))
|
||||
state_name = hass.states.get('{}.{}'.format(
|
||||
@ -206,11 +205,11 @@ def test_beacon_enter_and_exit_home(hass, geofency_client):
|
||||
assert STATE_NOT_HOME == state_name
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_beacon_enter_and_exit_car(hass, geofency_client):
|
||||
async def test_beacon_enter_and_exit_car(hass, geofency_client):
|
||||
"""Test use of mobile iBeacon."""
|
||||
# Enter the Car away from Home zone
|
||||
req = yield from geofency_client.post(URL, data=BEACON_ENTER_CAR)
|
||||
req = await geofency_client.post(URL, data=BEACON_ENTER_CAR)
|
||||
await hass.async_block_till_done()
|
||||
assert req.status == HTTP_OK
|
||||
device_name = slugify("beacon_{}".format(BEACON_ENTER_CAR['name']))
|
||||
state_name = hass.states.get('{}.{}'.format(
|
||||
@ -218,7 +217,8 @@ def test_beacon_enter_and_exit_car(hass, geofency_client):
|
||||
assert STATE_NOT_HOME == state_name
|
||||
|
||||
# Exit the Car away from Home zone
|
||||
req = yield from geofency_client.post(URL, data=BEACON_EXIT_CAR)
|
||||
req = await geofency_client.post(URL, data=BEACON_EXIT_CAR)
|
||||
await hass.async_block_till_done()
|
||||
assert req.status == HTTP_OK
|
||||
device_name = slugify("beacon_{}".format(BEACON_ENTER_CAR['name']))
|
||||
state_name = hass.states.get('{}.{}'.format(
|
||||
@ -229,7 +229,8 @@ def test_beacon_enter_and_exit_car(hass, geofency_client):
|
||||
data = BEACON_ENTER_CAR.copy()
|
||||
data['latitude'] = HOME_LATITUDE
|
||||
data['longitude'] = HOME_LONGITUDE
|
||||
req = yield from geofency_client.post(URL, data=data)
|
||||
req = await geofency_client.post(URL, data=data)
|
||||
await hass.async_block_till_done()
|
||||
assert req.status == HTTP_OK
|
||||
device_name = slugify("beacon_{}".format(data['name']))
|
||||
state_name = hass.states.get('{}.{}'.format(
|
||||
@ -237,7 +238,8 @@ def test_beacon_enter_and_exit_car(hass, geofency_client):
|
||||
assert STATE_HOME == state_name
|
||||
|
||||
# Exit the Car in the Home zone
|
||||
req = yield from geofency_client.post(URL, data=data)
|
||||
req = await geofency_client.post(URL, data=data)
|
||||
await hass.async_block_till_done()
|
||||
assert req.status == HTTP_OK
|
||||
device_name = slugify("beacon_{}".format(data['name']))
|
||||
state_name = hass.states.get('{}.{}'.format(
|
Loading…
x
Reference in New Issue
Block a user