Geo Location component (#15953)

* initial working version of a geo location component and georss platform

* ensure that custom attributes don't override built-in ones

* bugfixes and tests

* fixing tests because of introduction of new component using same fixture

* improving test cases

* removing potentially unavailable attribute from debug message output

* completing test suite

* cleaning up debug messages; sorting entries in group view by distance

* ability to define the desired state attribute and corresponding unit of measurement; sort devices in group by configured state; find centroid for map if event is defined by polygon; updated tests

* sort entries in group; code clean-ups

* fixing indentation

* added requirements of new component and platform

* fixed various lint issues

* fixed more lint issues

* introducing demo geo location platform; refactored geo location component and geo rss platform to fit

* removing geo rss events platform; added unit tests for geo location platform and demo platform

* reverting change in debug message for feedreader to avoid confusion with new geo location component

* updated requirements after removing georss platform

* removed unused imports

* fixing a lint issue and a test case

* simplifying component code; moving code into demo platform; fixing tests

* removed grouping from demo platform; small refactorings

* automating the entity id generation (the use of an entity namespace achieves the same thing)

* undoing changes made for the georss platform

* simplified test cases

* small tweaks to test case

* rounding all state attribute values

* fixing lint; removing distance from state attributes

* fixed test

* renamed add_devices to add_entities; tweaked test to gain more control over the timed update in the demo platform

* reusing utcnow variable instead of patched method

* fixed test by avoiding to make assumptions about order of list of entity ids

* adding test for the geo location event class
This commit is contained in:
Malte Franken 2018-08-30 21:58:23 +10:00 committed by Martin Hjelmare
parent 54c3f4f001
commit f20a3313b0
5 changed files with 284 additions and 0 deletions

View File

@ -0,0 +1,68 @@
"""
Geo Location component.
This component covers platforms that deal with external events that contain
a geo location related to the installed HA instance.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/geo_location/
"""
import logging
from datetime import timedelta
from typing import Optional
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
_LOGGER = logging.getLogger(__name__)
ATTR_DISTANCE = 'distance'
DOMAIN = 'geo_location'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
GROUP_NAME_ALL_EVENTS = 'All Geo Location Events'
SCAN_INTERVAL = timedelta(seconds=60)
async def async_setup(hass, config):
"""Set up this component."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_EVENTS)
await component.async_setup(config)
return True
class GeoLocationEvent(Entity):
"""This represents an external event with an associated geo location."""
@property
def state(self):
"""Return the state of the sensor."""
if self.distance is not None:
return round(self.distance, 1)
return None
@property
def distance(self) -> Optional[float]:
"""Return distance value of this external event."""
return None
@property
def latitude(self) -> Optional[float]:
"""Return latitude value of this external event."""
return None
@property
def longitude(self) -> Optional[float]:
"""Return longitude value of this external event."""
return None
@property
def state_attributes(self):
"""Return the state attributes of this external event."""
data = {}
if self.latitude is not None:
data[ATTR_LATITUDE] = round(self.latitude, 5)
if self.longitude is not None:
data[ATTR_LONGITUDE] = round(self.longitude, 5)
return data

View File

@ -0,0 +1,132 @@
"""
Demo platform for the geo location component.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
import logging
import random
from datetime import timedelta
from math import pi, cos, sin, radians
from typing import Optional
from homeassistant.components.geo_location import GeoLocationEvent
from homeassistant.helpers.event import track_time_interval
_LOGGER = logging.getLogger(__name__)
AVG_KM_PER_DEGREE = 111.0
DEFAULT_UNIT_OF_MEASUREMENT = "km"
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1)
MAX_RADIUS_IN_KM = 50
NUMBER_OF_DEMO_DEVICES = 5
EVENT_NAMES = ["Bushfire", "Hazard Reduction", "Grass Fire", "Burn off",
"Structure Fire", "Fire Alarm", "Thunderstorm", "Tornado",
"Cyclone", "Waterspout", "Dust Storm", "Blizzard", "Ice Storm",
"Earthquake", "Tsunami"]
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Demo geo locations."""
DemoManager(hass, add_entities)
class DemoManager:
"""Device manager for demo geo location events."""
def __init__(self, hass, add_entities):
"""Initialise the demo geo location event manager."""
self._hass = hass
self._add_entities = add_entities
self._managed_devices = []
self._update(count=NUMBER_OF_DEMO_DEVICES)
self._init_regular_updates()
def _generate_random_event(self):
"""Generate a random event in vicinity of this HA instance."""
home_latitude = self._hass.config.latitude
home_longitude = self._hass.config.longitude
# Approx. 111km per degree (north-south).
radius_in_degrees = random.random() * MAX_RADIUS_IN_KM / \
AVG_KM_PER_DEGREE
radius_in_km = radius_in_degrees * AVG_KM_PER_DEGREE
angle = random.random() * 2 * pi
# Compute coordinates based on radius and angle. Adjust longitude value
# based on HA's latitude.
latitude = home_latitude + radius_in_degrees * sin(angle)
longitude = home_longitude + radius_in_degrees * cos(angle) / \
cos(radians(home_latitude))
event_name = random.choice(EVENT_NAMES)
return DemoGeoLocationEvent(event_name, radius_in_km, latitude,
longitude, DEFAULT_UNIT_OF_MEASUREMENT)
def _init_regular_updates(self):
"""Schedule regular updates based on configured time interval."""
track_time_interval(self._hass, lambda now: self._update(),
DEFAULT_UPDATE_INTERVAL)
def _update(self, count=1):
"""Remove events and add new random events."""
# Remove devices.
for _ in range(1, count + 1):
if self._managed_devices:
device = random.choice(self._managed_devices)
if device:
_LOGGER.debug("Removing %s", device)
self._managed_devices.remove(device)
self._hass.add_job(device.async_remove())
# Generate new devices from events.
new_devices = []
for _ in range(1, count + 1):
new_device = self._generate_random_event()
_LOGGER.debug("Adding %s", new_device)
new_devices.append(new_device)
self._managed_devices.append(new_device)
self._add_entities(new_devices)
class DemoGeoLocationEvent(GeoLocationEvent):
"""This represents a demo geo location event."""
def __init__(self, name, distance, latitude, longitude,
unit_of_measurement):
"""Initialize entity with data provided."""
self._name = name
self._distance = distance
self._latitude = latitude
self._longitude = longitude
self._unit_of_measurement = unit_of_measurement
@property
def name(self) -> Optional[str]:
"""Return the name of the event."""
return self._name
@property
def should_poll(self):
"""No polling needed for a demo geo location event."""
return False
@property
def distance(self) -> Optional[float]:
"""Return distance value of this external event."""
return self._distance
@property
def latitude(self) -> Optional[float]:
"""Return latitude value of this external event."""
return self._latitude
@property
def longitude(self) -> Optional[float]:
"""Return longitude value of this external event."""
return self._longitude
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement

View File

@ -0,0 +1 @@
"""The tests for Geo Location platforms."""

View File

@ -0,0 +1,63 @@
"""The tests for the demo platform."""
import unittest
from unittest.mock import patch
from homeassistant.components import geo_location
from homeassistant.components.geo_location.demo import \
NUMBER_OF_DEMO_DEVICES, DEFAULT_UNIT_OF_MEASUREMENT, \
DEFAULT_UPDATE_INTERVAL
from homeassistant.setup import setup_component
from tests.common import get_test_home_assistant, assert_setup_component, \
fire_time_changed
import homeassistant.util.dt as dt_util
CONFIG = {
geo_location.DOMAIN: [
{
'platform': 'demo'
}
]
}
class TestDemoPlatform(unittest.TestCase):
"""Test the demo platform."""
def setUp(self):
"""Initialize values for this testcase class."""
self.hass = get_test_home_assistant()
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
def test_setup_platform(self):
"""Test setup of demo platform via configuration."""
utcnow = dt_util.utcnow()
# Patching 'utcnow' to gain more control over the timed update.
with patch('homeassistant.util.dt.utcnow', return_value=utcnow):
with assert_setup_component(1, geo_location.DOMAIN):
self.assertTrue(setup_component(self.hass, geo_location.DOMAIN,
CONFIG))
# In this test, only entities of the geo location domain have been
# generated.
all_states = self.hass.states.all()
assert len(all_states) == NUMBER_OF_DEMO_DEVICES
# Check a single device's attributes.
state_first_entry = all_states[0]
self.assertAlmostEqual(state_first_entry.attributes['latitude'],
self.hass.config.latitude, delta=1.0)
self.assertAlmostEqual(state_first_entry.attributes['longitude'],
self.hass.config.longitude, delta=1.0)
assert state_first_entry.attributes['unit_of_measurement'] == \
DEFAULT_UNIT_OF_MEASUREMENT
# Update (replaces 1 device).
fire_time_changed(self.hass, utcnow + DEFAULT_UPDATE_INTERVAL)
self.hass.block_till_done()
# Get all states again, ensure that the number of states is still
# the same, but the lists are different.
all_states_updated = self.hass.states.all()
assert len(all_states_updated) == NUMBER_OF_DEMO_DEVICES
self.assertNotEqual(all_states, all_states_updated)

View File

@ -0,0 +1,20 @@
"""The tests for the geo location component."""
from homeassistant.components import geo_location
from homeassistant.components.geo_location import GeoLocationEvent
from homeassistant.setup import async_setup_component
async def test_setup_component(hass):
"""Simple test setup of component."""
result = await async_setup_component(hass, geo_location.DOMAIN)
assert result
async def test_event(hass):
"""Simple test of the geo location event class."""
entity = GeoLocationEvent()
assert entity.state is None
assert entity.distance is None
assert entity.latitude is None
assert entity.longitude is None