diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index a8cf5b5d417..c96c013275b 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -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,32 +54,37 @@ 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 + return next_setting - LIGHT_TRANSITION_TIME * len(light_ids) - def schedule_light_on_sun_rise(entity, old_state, new_state): + 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 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.""" - 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): - - light.turn_on(hass, light_id, - transition=LIGHT_TRANSITION_TIME.seconds, - profile=light_profile) + 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 @@ -91,83 +92,60 @@ def setup(hass, config): 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) + for index, light_id in enumerate(light_ids): + 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() - # 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) + light.turn_on(hass, light_ids, profile=light_profile) - # Do we need lights? - if light_needed: + # Are we in the time span were we would turn on the lights + # if someone would be home? + # Check this by seeing if current time is later then the point + # in time when we would start putting the lights on. + elif (start_point and + start_point < now < sun.next_setting(hass)): - logger.info( - "Home coming event for %s. Turning lights on", entity) + # 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) - light.turn_on(hass, light_ids, profile=light_profile) + else: + # If this light didn't happen to be turned on yet so + # will all the following then, break. + break - # Are we in the time span were we would turn on the lights - # if someone would be home? - # Check this by seeing if current time is later then the point - # in time when we would start putting the lights on. - elif (start_point and - start_point < now < sun.next_setting(hass)): - - # 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) - - else: - # If this light didn't happen to be turned on yet so - # 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 diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 8c3fd62a0f8..b646f93e2d0 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -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): diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 3d157f32eea..b1ad9f478cc 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -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, @@ -32,7 +32,7 @@ _GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME), def _get_group_on_off(state): - """ Determine the group on/off states based on a state. """ + """Determine the group on/off states based on a state.""" for states in _GROUP_TYPES: if state in states: return states @@ -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,24 +210,24 @@ 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) def stop(self): - """ Unregisters the group from Home Assistant. """ + """Unregisters the group from Home Assistant.""" self.hass.states.remove(self.entity_id) self.hass.bus.remove_listener( 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,49 +252,53 @@ class Group(Entity): """ # pylint: disable=too-many-branches # To store current states of group entities. Might not be needed. - states = None - gr_state, gr_on, gr_off = self._state, self.group_on, self.group_off + with self._lock: + states = None + 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: - if tr_state is None: - states = self._tracking_states + # We have not determined type of group yet + if gr_on is None: + if tr_state is None: + states = self._tracking_states - for state in states: - gr_on, gr_off = \ - _get_group_on_off(state.state) - if gr_on is not None: - break - else: - gr_on, gr_off = _get_group_on_off(tr_state.state) + for state in states: + gr_on, gr_off = \ + _get_group_on_off(state.state) + if gr_on is not None: + break + else: + gr_on, gr_off = _get_group_on_off(tr_state.state) - if gr_on is not None: - self.group_on, self.group_off = gr_on, gr_off + if gr_on is not None: + self.group_on, self.group_off = gr_on, gr_off - # We cannot determine state of the group - if gr_on is None: - return + # We cannot determine state of the group + if gr_on is None: + return - if tr_state is None or (gr_state == gr_on and - tr_state.state == gr_off): - if states is None: - states = self._tracking_states + if tr_state is None or (gr_state == gr_on and + tr_state.state == gr_off): + if states is None: + states = self._tracking_states - if any(state.state == gr_on for state in states): - self._state = gr_on - else: - self._state = gr_off + if any(state.state == gr_on for state in states): + self._state = gr_on + else: + self._state = gr_off - elif tr_state.state in (gr_on, gr_off): - self._state = tr_state.state + elif tr_state.state in (gr_on, gr_off): + self._state = tr_state.state - if tr_state is None or self._assumed_state and \ - not tr_state.attributes.get(ATTR_ASSUMED_STATE): - if states is None: - states = self._tracking_states + if tr_state is None or self._assumed_state and \ + not tr_state.attributes.get(ATTR_ASSUMED_STATE): + 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 + elif tr_state.attributes.get(ATTR_ASSUMED_STATE): + self._assumed_state = True diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 49fe33366a9..c106163d0d1 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -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( diff --git a/tests/components/test_device_sun_light_trigger.py b/tests/components/test_device_sun_light_trigger.py index 255f371b9a7..0309ac1fb75 100644 --- a/tests/components/test_device_sun_light_trigger.py +++ b/tests/components/test_device_sun_light_trigger.py @@ -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. """ diff --git a/tests/helpers/test_event_decorators.py b/tests/helpers/test_event_decorators.py index 0d87be43740..99fcf8c5d0f 100644 --- a/tests/helpers/test_event_decorators.py +++ b/tests/helpers/test_event_decorators.py @@ -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 """