Merge pull request #1484 from balloob/device-tracker-remove-old-conf-format

Device tracker remove old conf format
This commit is contained in:
Paulus Schoutsen 2016-03-06 21:41:59 -08:00
commit abc7243bc8
6 changed files with 151 additions and 236 deletions

View File

@ -1,8 +1,6 @@
"""
homeassistant.components.device_sun_light_trigger
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to turn on lights based on the state of the sun and
devices.
devices home.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/device_sun_light_trigger/
@ -12,9 +10,9 @@ from datetime import timedelta
import homeassistant.util.dt as dt_util
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from homeassistant.helpers.event import track_point_in_time, track_state_change
from . import device_tracker, group, light, sun
from homeassistant.helpers.event import track_point_in_time
from homeassistant.helpers.event_decorators import track_state_change
from homeassistant.loader import get_component
DOMAIN = "device_sun_light_trigger"
DEPENDENCIES = ['light', 'device_tracker', 'group', 'sun']
@ -29,28 +27,26 @@ CONF_LIGHT_GROUP = 'light_group'
CONF_DEVICE_GROUP = 'device_group'
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
def setup(hass, config):
""" Triggers to turn lights on or off based on device precense. """
logger = logging.getLogger(__name__)
device_tracker = get_component('device_tracker')
group = get_component('group')
light = get_component('light')
sun = get_component('sun')
disable_turn_off = 'disable_turn_off' in config[DOMAIN]
light_group = config[DOMAIN].get(CONF_LIGHT_GROUP,
light.ENTITY_ID_ALL_LIGHTS)
light_profile = config[DOMAIN].get(CONF_LIGHT_PROFILE, LIGHT_PROFILE)
device_group = config[DOMAIN].get(CONF_DEVICE_GROUP,
device_tracker.ENTITY_ID_ALL_DEVICES)
logger = logging.getLogger(__name__)
device_entity_ids = group.get_entity_ids(hass, device_group,
device_tracker.DOMAIN)
if not device_entity_ids:
logger.error("No devices found to track")
return False
# Get the light IDs from the specified group
@ -58,77 +54,68 @@ def setup(hass, config):
if not light_ids:
logger.error("No lights found to turn on ")
return False
def calc_time_for_light_when_sunset():
""" Calculates the time when to start fading lights in when sun sets.
Returns None if no next_setting data available. """
next_setting = sun.next_setting(hass)
if next_setting:
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
else:
if not next_setting:
return None
def schedule_light_on_sun_rise(entity, old_state, new_state):
"""The moment sun sets we want to have all the lights on.
We will schedule to have each light start after one another
and slowly transition in."""
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
def turn_light_on_before_sunset(light_id):
""" Helper function to turn on lights slowly if there
are devices home and the light is not on yet. """
if device_tracker.is_on(hass) and not light.is_on(hass, light_id):
if not device_tracker.is_on(hass) or light.is_on(hass, light_id):
return
light.turn_on(hass, light_id,
transition=LIGHT_TRANSITION_TIME.seconds,
profile=light_profile)
# Track every time sun rises so we can schedule a time-based
# pre-sun set event
@track_state_change(sun.ENTITY_ID, sun.STATE_BELOW_HORIZON,
sun.STATE_ABOVE_HORIZON)
def schedule_lights_at_sun_set(hass, entity, old_state, new_state):
"""The moment sun sets we want to have all the lights on.
We will schedule to have each light start after one another
and slowly transition in."""
start_point = calc_time_for_light_when_sunset()
if not start_point:
return
def turn_on(light_id):
""" Lambda can keep track of function parameters but not local
parameters. If we put the lambda directly in the below statement
only the last light will be turned on.. """
return lambda now: turn_light_on_before_sunset(light_id)
start_point = calc_time_for_light_when_sunset()
if start_point:
for index, light_id in enumerate(light_ids):
track_point_in_time(
hass, turn_on(light_id),
(start_point + index * LIGHT_TRANSITION_TIME))
# Track every time sun rises so we can schedule a time-based
# pre-sun set event
track_state_change(hass, sun.ENTITY_ID, schedule_light_on_sun_rise,
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
track_point_in_time(hass, turn_on(light_id),
start_point + index * LIGHT_TRANSITION_TIME)
# If the sun is already above horizon
# schedule the time-based pre-sun set event
if sun.is_on(hass):
schedule_light_on_sun_rise(None, None, None)
schedule_lights_at_sun_set(hass, None, None, None)
def check_light_on_dev_state_change(entity, old_state, new_state):
""" Function to handle tracked device state changes. """
@track_state_change(device_entity_ids, STATE_NOT_HOME, STATE_HOME)
def check_light_on_dev_state_change(hass, entity, old_state, new_state):
"""Handle tracked device state changes."""
# pylint: disable=unused-variable
lights_are_on = group.is_on(hass, light_group)
light_needed = not (lights_are_on or sun.is_on(hass))
# Specific device came home ?
if entity != device_tracker.ENTITY_ID_ALL_DEVICES and \
new_state.state == STATE_HOME:
# These variables are needed for the elif check
now = dt_util.now()
start_point = calc_time_for_light_when_sunset()
# Do we need lights?
if light_needed:
logger.info(
"Home coming event for %s. Turning lights on", entity)
logger.info("Home coming event for %s. Turning lights on", entity)
light.turn_on(hass, light_ids, profile=light_profile)
# Are we in the time span were we would turn on the lights
@ -141,7 +128,6 @@ def setup(hass, config):
# Check for every light if it would be on if someone was home
# when the fading in started and turn it on if so
for index, light_id in enumerate(light_ids):
if now > start_point + index * LIGHT_TRANSITION_TIME:
light.turn_on(hass, light_id)
@ -150,24 +136,16 @@ def setup(hass, config):
# will all the following then, break.
break
# Did all devices leave the house?
elif (entity == device_group and
new_state.state == STATE_NOT_HOME and lights_are_on and
not disable_turn_off):
if not disable_turn_off:
@track_state_change(device_group, STATE_HOME, STATE_NOT_HOME)
def turn_off_lights_when_all_leave(hass, entity, old_state, new_state):
"""Handle device group state change."""
# pylint: disable=unused-variable
if not group.is_on(hass, light_group):
return
logger.info(
"Everyone has left but there are lights on. Turning them off")
light.turn_off(hass, light_ids)
# Track home coming of each device
track_state_change(
hass, device_entity_ids, check_light_on_dev_state_change,
STATE_NOT_HOME, STATE_HOME)
# Track when all devices are gone to shut down lights
track_state_change(
hass, device_group, check_light_on_dev_state_change,
STATE_HOME, STATE_NOT_HOME)
return True

View File

@ -8,7 +8,6 @@ https://home-assistant.io/components/device_tracker/
"""
# pylint: disable=too-many-instance-attributes, too-many-arguments
# pylint: disable=too-many-locals
import csv
from datetime import timedelta
import logging
import os
@ -36,7 +35,6 @@ ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
ENTITY_ID_FORMAT = DOMAIN + '.{}'
CSV_DEVICES = "known_devices.csv"
YAML_DEVICES = 'known_devices.yaml'
CONF_TRACK_NEW = "track_new_devices"
@ -93,10 +91,6 @@ def see(hass, mac=None, dev_id=None, host_name=None, location_name=None,
def setup(hass, config):
""" Setup device tracker """
yaml_path = hass.config.path(YAML_DEVICES)
csv_path = hass.config.path(CSV_DEVICES)
if os.path.isfile(csv_path) and not os.path.isfile(yaml_path) and \
convert_csv_config(csv_path, yaml_path):
os.remove(csv_path)
conf = config.get(DOMAIN, {})
if isinstance(conf, list):
@ -370,21 +364,6 @@ class Device(Entity):
self.last_update_home = True
def convert_csv_config(csv_path, yaml_path):
""" Convert CSV config file format to YAML. """
used_ids = set()
with open(csv_path) as inp:
for row in csv.DictReader(inp):
dev_id = util.ensure_unique_string(
(util.slugify(row['name']) or DEVICE_DEFAULT_NAME).lower(),
used_ids)
used_ids.add(dev_id)
device = Device(None, None, None, row['track'] == '1', dev_id,
row['device'], row['name'], row['picture'])
update_config(yaml_path, dev_id, device)
return True
def load_config(path, hass, consider_home, home_range):
""" Load devices from YAML config file. """
if not os.path.isfile(path):

View File

@ -1,11 +1,11 @@
"""
homeassistant.components.group
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to group devices that can be turned on or off.
Provides functionality to group entities.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/group/
"""
import threading
import homeassistant.core as ha
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
@ -41,7 +41,7 @@ def _get_group_on_off(state):
def is_on(hass, entity_id):
""" Returns if the group state is in its ON-state. """
"""Test if the group state is in its ON-state."""
state = hass.states.get(entity_id)
if state:
@ -54,8 +54,7 @@ def is_on(hass, entity_id):
def expand_entity_ids(hass, entity_ids):
""" Returns the given list of entity ids and expands group ids into
the entity ids it represents if found. """
"""Return entity_ids with group entity ids replaced by their members."""
found_ids = []
for entity_id in entity_ids:
@ -86,7 +85,7 @@ def expand_entity_ids(hass, entity_ids):
def get_entity_ids(hass, entity_id, domain_filter=None):
""" Get the entity ids that make up this group. """
"""Get members of this group."""
entity_id = entity_id.lower()
try:
@ -107,7 +106,7 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
def setup(hass, config):
""" Sets up all groups found definded in the configuration. """
"""Set up all groups found definded in the configuration."""
for object_id, conf in config.get(DOMAIN, {}).items():
if not isinstance(conf, dict):
conf = {CONF_ENTITIES: conf}
@ -127,12 +126,13 @@ def setup(hass, config):
class Group(Entity):
""" Tracks a group of entity ids. """
"""Track a group of entity ids."""
# pylint: disable=too-many-instance-attributes, too-many-arguments
def __init__(self, hass, name, entity_ids=None, user_defined=True,
icon=None, view=False, object_id=None):
"""Initialize a group."""
self.hass = hass
self._name = name
self._state = STATE_UNKNOWN
@ -146,6 +146,7 @@ class Group(Entity):
self.group_on = None
self.group_off = None
self._assumed_state = False
self._lock = threading.Lock()
if entity_ids is not None:
self.update_tracked_entity_ids(entity_ids)
@ -154,26 +155,35 @@ class Group(Entity):
@property
def should_poll(self):
"""No need to poll because groups will update themselves."""
return False
@property
def name(self):
"""Name of the group."""
return self._name
@property
def state(self):
"""State of the group."""
return self._state
@property
def icon(self):
"""Icon of the group."""
return self._icon
@property
def hidden(self):
"""If group should be hidden or not.
true if group is a view or not user defined.
"""
return not self._user_defined or self._view
@property
def state_attributes(self):
"""State attributes for the group."""
data = {
ATTR_ENTITY_ID: self.tracking,
ATTR_ORDER: self._order,
@ -186,11 +196,11 @@ class Group(Entity):
@property
def assumed_state(self):
"""Return True if unable to access real state of entity."""
"""Test if any member has an assumed state."""
return self._assumed_state
def update_tracked_entity_ids(self, entity_ids):
""" Update the tracked entity IDs. """
"""Update the member entity IDs."""
self.stop()
self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
self.group_on, self.group_off = None, None
@ -200,7 +210,7 @@ class Group(Entity):
self.start()
def start(self):
""" Starts the tracking. """
"""Start tracking members."""
track_state_change(
self.hass, self.tracking, self._state_changed_listener)
@ -212,12 +222,12 @@ class Group(Entity):
ha.EVENT_STATE_CHANGED, self._state_changed_listener)
def update(self):
""" Query all the tracked states and determine current group state. """
"""Query all members and determine current group state."""
self._state = STATE_UNKNOWN
self._update_group_state()
def _state_changed_listener(self, entity_id, old_state, new_state):
""" Listener to receive state changes of tracked entities. """
"""Respond to a member state changing."""
self._update_group_state(new_state)
self.update_ha_state()
@ -242,8 +252,11 @@ class Group(Entity):
"""
# pylint: disable=too-many-branches
# To store current states of group entities. Might not be needed.
with self._lock:
states = None
gr_state, gr_on, gr_off = self._state, self.group_on, self.group_off
gr_state = self._state
gr_on = self.group_on
gr_off = self.group_off
# We have not determined type of group yet
if gr_on is None:
@ -283,8 +296,9 @@ class Group(Entity):
if states is None:
states = self._tracking_states
self._assumed_state = any(state.attributes.get(ATTR_ASSUMED_STATE)
for state in states)
self._assumed_state = any(
state.attributes.get(ATTR_ASSUMED_STATE) for state
in states)
elif tr_state.attributes.get(ATTR_ASSUMED_STATE):
self._assumed_state = True

View File

@ -1,21 +1,15 @@
"""
tests.components.device_tracker.test_init
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests the device tracker compoments.
"""
"""Tests for the device tracker compoment."""
# pylint: disable=protected-access,too-many-public-methods
import unittest
from unittest.mock import patch
from datetime import datetime, timedelta
import os
from homeassistant.config import load_yaml_config_file
from homeassistant.loader import get_component
import homeassistant.util.dt as dt_util
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN,
STATE_HOME, STATE_NOT_HOME, CONF_PLATFORM, DEVICE_DEFAULT_NAME)
STATE_HOME, STATE_NOT_HOME, CONF_PLATFORM)
import homeassistant.components.device_tracker as device_tracker
from tests.common import (
@ -51,55 +45,6 @@ class TestComponentsDeviceTracker(unittest.TestCase):
self.assertFalse(device_tracker.is_on(self.hass, entity_id))
def test_migrating_config(self):
csv_devices = self.hass.config.path(device_tracker.CSV_DEVICES)
self.assertFalse(os.path.isfile(csv_devices))
self.assertFalse(os.path.isfile(self.yaml_devices))
person1 = {
'mac': 'AB:CD:EF:GH:IJ:KL',
'name': 'Paulus',
'track': True,
'picture': 'http://placehold.it/200x200',
}
person2 = {
'mac': 'MN:OP:QR:ST:UV:WX:YZ',
'name': '',
'track': False,
'picture': None,
}
try:
with open(csv_devices, 'w') as fil:
fil.write('device,name,track,picture\n')
for pers in (person1, person2):
fil.write('{},{},{},{}\n'.format(
pers['mac'], pers['name'],
'1' if pers['track'] else '0', pers['picture'] or ''))
self.assertTrue(device_tracker.setup(self.hass, {}))
self.assertFalse(os.path.isfile(csv_devices))
self.assertTrue(os.path.isfile(self.yaml_devices))
yaml_config = load_yaml_config_file(self.yaml_devices)
self.assertEqual(2, len(yaml_config))
for pers, yaml_pers in zip(
(person1, person2), sorted(yaml_config.values(),
key=lambda pers: pers['mac'])):
for key, value in pers.items():
if key == 'name' and value == '':
value = DEVICE_DEFAULT_NAME
self.assertEqual(value, yaml_pers.get(key))
finally:
try:
os.remove(csv_devices)
except FileNotFoundError:
pass
def test_reading_yaml_config(self):
dev_id = 'test'
device = device_tracker.Device(

View File

@ -1,9 +1,4 @@
"""
tests.test_component_device_sun_light_trigger
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests device sun light trigger component.
"""
"""Tests device sun light trigger component."""
# pylint: disable=too-many-public-methods,protected-access
import os
import unittest
@ -12,32 +7,33 @@ import homeassistant.loader as loader
from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME
from homeassistant.components import (
device_tracker, light, sun, device_sun_light_trigger)
from homeassistant.helpers import event_decorators
from tests.common import (
get_test_config_dir, get_test_home_assistant, ensure_sun_risen,
ensure_sun_set)
KNOWN_DEV_CSV_PATH = os.path.join(get_test_config_dir(),
device_tracker.CSV_DEVICES)
KNOWN_DEV_YAML_PATH = os.path.join(get_test_config_dir(),
device_tracker.YAML_DEVICES)
def setUpModule(): # pylint: disable=invalid-name
""" Initalizes a Home Assistant server. """
with open(KNOWN_DEV_CSV_PATH, 'w') as fil:
fil.write('device,name,track,picture\n')
fil.write('DEV1,device 1,1,http://example.com/dev1.jpg\n')
fil.write('DEV2,device 2,1,http://example.com/dev2.jpg\n')
"""Write a device tracker known devices file to be used."""
device_tracker.update_config(
KNOWN_DEV_YAML_PATH, 'device_1', device_tracker.Device(
None, None, None, True, 'device_1', 'DEV1',
picture='http://example.com/dev1.jpg'))
device_tracker.update_config(
KNOWN_DEV_YAML_PATH, 'device_2', device_tracker.Device(
None, None, None, True, 'device_2', 'DEV2',
picture='http://example.com/dev2.jpg'))
def tearDownModule(): # pylint: disable=invalid-name
""" Stops the Home Assistant server. """
for fil in (KNOWN_DEV_CSV_PATH, KNOWN_DEV_YAML_PATH):
if os.path.isfile(fil):
os.remove(fil)
"""Remove device tracker known devices file."""
os.remove(KNOWN_DEV_YAML_PATH)
class TestDeviceSunLightTrigger(unittest.TestCase):
@ -45,6 +41,7 @@ class TestDeviceSunLightTrigger(unittest.TestCase):
def setUp(self): # pylint: disable=invalid-name
self.hass = get_test_home_assistant()
event_decorators.HASS = self.hass
self.scanner = loader.get_component(
'device_tracker.test').get_scanner(None, None)
@ -68,6 +65,7 @@ class TestDeviceSunLightTrigger(unittest.TestCase):
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
event_decorators.HASS = None
def test_lights_on_when_sun_sets(self):
""" Test lights go on when there is someone home and the sun sets. """

View File

@ -38,6 +38,7 @@ class TestEventDecoratorHelpers(unittest.TestCase):
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
event_decorators.HASS = None
def test_track_sunrise(self):
""" Test track sunrise decorator """