mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 00:07:10 +00:00
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:
parent
54c3f4f001
commit
f20a3313b0
68
homeassistant/components/geo_location/__init__.py
Normal file
68
homeassistant/components/geo_location/__init__.py
Normal 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
|
132
homeassistant/components/geo_location/demo.py
Normal file
132
homeassistant/components/geo_location/demo.py
Normal 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
|
1
tests/components/geo_location/__init__.py
Normal file
1
tests/components/geo_location/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""The tests for Geo Location platforms."""
|
63
tests/components/geo_location/test_demo.py
Normal file
63
tests/components/geo_location/test_demo.py
Normal 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)
|
20
tests/components/geo_location/test_init.py
Normal file
20
tests/components/geo_location/test_init.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user