""" homeassistant.components.proximity ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Component to monitor the proximity of devices to a particular zone and the direction of travel. For more details about this component, please refer to the documentation at https://home-assistant.io/components/proximity/ """ import logging from homeassistant.helpers.event import track_state_change from homeassistant.helpers.entity import Entity from homeassistant.util.location import distance DEPENDENCIES = ['zone', 'device_tracker'] DOMAIN = 'proximity' # Default tolerance DEFAULT_TOLERANCE = 1 # Default zone DEFAULT_PROXIMITY_ZONE = 'home' # Entity attributes ATTR_DIST_FROM = 'dist_to_zone' ATTR_DIR_OF_TRAVEL = 'dir_of_travel' ATTR_NEAREST = 'nearest' ATTR_FRIENDLY_NAME = 'friendly_name' _LOGGER = logging.getLogger(__name__) def setup(hass, config): # pylint: disable=too-many-locals,too-many-statements """ Get the zones and offsets from configuration.yaml. """ ignored_zones = [] if 'ignored_zones' in config[DOMAIN]: for variable in config[DOMAIN]['ignored_zones']: ignored_zones.append(variable) # Get the devices from configuration.yaml if 'devices' not in config[DOMAIN]: _LOGGER.error('devices not found in config') return False proximity_devices = [] for variable in config[DOMAIN]['devices']: proximity_devices.append(variable) # Gget the direction of travel tolerance from configuration.yaml tolerance = config[DOMAIN].get('tolerance', DEFAULT_TOLERANCE) # Get the zone to monitor proximity to from configuration.yaml proximity_zone = config[DOMAIN].get('zone', DEFAULT_PROXIMITY_ZONE) entity_id = DOMAIN + '.' + proximity_zone proximity_zone = 'zone.' + proximity_zone state = hass.states.get(proximity_zone) zone_friendly_name = (state.name).lower() # set the default values dist_to_zone = 'not set' dir_of_travel = 'not set' nearest = 'not set' proximity = Proximity(hass, zone_friendly_name, dist_to_zone, dir_of_travel, nearest, ignored_zones, proximity_devices, tolerance, proximity_zone) proximity.entity_id = entity_id proximity.update_ha_state() # Main command to monitor proximity of devices track_state_change(hass, proximity_devices, proximity.check_proximity_state_change) # Tells the bootstrapper that the component was successfully initialized return True class Proximity(Entity): # pylint: disable=too-many-instance-attributes """ Represents a Proximity. """ def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel, nearest, ignored_zones, proximity_devices, tolerance, proximity_zone): # pylint: disable=too-many-arguments self.hass = hass self.friendly_name = zone_friendly_name self.dist_to = dist_to self.dir_of_travel = dir_of_travel self.nearest = nearest self.ignored_zones = ignored_zones self.proximity_devices = proximity_devices self.tolerance = tolerance self.proximity_zone = proximity_zone @property def state(self): """ Returns the state. """ return self.dist_to @property def unit_of_measurement(self): """ Unit of measurement of this entity """ return "km" @property def state_attributes(self): """ Returns the state attributes. """ return { ATTR_DIR_OF_TRAVEL: self.dir_of_travel, ATTR_NEAREST: self.nearest, ATTR_FRIENDLY_NAME: self.friendly_name } def check_proximity_state_change(self, entity, old_state, new_state): # pylint: disable=too-many-branches,too-many-statements,too-many-locals """ Function to perform the proximity checking. """ entity_name = new_state.name devices_to_calculate = False devices_in_zone = '' zone_state = self.hass.states.get(self.proximity_zone) proximity_latitude = zone_state.attributes.get('latitude') proximity_longitude = zone_state.attributes.get('longitude') # Check for devices in the monitored zone for device in self.proximity_devices: device_state = self.hass.states.get(device) if device_state.state not in self.ignored_zones: devices_to_calculate = True # Check the location of all devices if (device_state.state).lower() == (self.friendly_name).lower(): device_friendly = device_state.name if devices_in_zone != '': devices_in_zone = devices_in_zone + ', ' devices_in_zone = devices_in_zone + device_friendly # No-one to track so reset the entity if not devices_to_calculate: self.dist_to = 'not set' self.dir_of_travel = 'not set' self.nearest = 'not set' self.update_ha_state() return # At least one device is in the monitored zone so update the entity if devices_in_zone != '': self.dist_to = 0 self.dir_of_travel = 'arrived' self.nearest = devices_in_zone self.update_ha_state() return # We can't check proximity because latitude and longitude don't exist if 'latitude' not in new_state.attributes: return # Collect distances to the zone for all devices distances_to_zone = {} for device in self.proximity_devices: # Ignore devices in an ignored zone device_state = self.hass.states.get(device) if device_state.state in self.ignored_zones: continue # Ignore devices if proximity cannot be calculated if 'latitude' not in device_state.attributes: continue # Calculate the distance to the proximity zone dist_to_zone = distance(proximity_latitude, proximity_longitude, device_state.attributes['latitude'], device_state.attributes['longitude']) # Add the device and distance to a dictionary distances_to_zone[device] = round(dist_to_zone / 1000, 1) # Loop through each of the distances collected and work out the closest closest_device = '' dist_to_zone = 1000000 for device in distances_to_zone: if distances_to_zone[device] < dist_to_zone: closest_device = device dist_to_zone = distances_to_zone[device] # If the closest device is one of the other devices if closest_device != entity: self.dist_to = round(distances_to_zone[closest_device]) self.dir_of_travel = 'unknown' device_state = self.hass.states.get(closest_device) self.nearest = device_state.name self.update_ha_state() return # Stop if we cannot calculate the direction of travel (i.e. we don't # have a previous state and a current LAT and LONG) if old_state is None or 'latitude' not in old_state.attributes: self.dist_to = round(distances_to_zone[entity]) self.dir_of_travel = 'unknown' self.nearest = entity_name self.update_ha_state() return # Reset the variables distance_travelled = 0 # Calculate the distance travelled old_distance = distance(proximity_latitude, proximity_longitude, old_state.attributes['latitude'], old_state.attributes['longitude']) new_distance = distance(proximity_latitude, proximity_longitude, new_state.attributes['latitude'], new_state.attributes['longitude']) distance_travelled = round(new_distance - old_distance, 1) # Check for tolerance if distance_travelled < self.tolerance * -1: direction_of_travel = 'towards' elif distance_travelled > self.tolerance: direction_of_travel = 'away_from' else: direction_of_travel = 'stationary' # Update the proximity entity self.dist_to = round(dist_to_zone) self.dir_of_travel = direction_of_travel self.nearest = entity_name self.update_ha_state() _LOGGER.debug('proximity.%s update entity: distance=%s: direction=%s: ' 'device=%s', self.friendly_name, round(dist_to_zone), direction_of_travel, entity_name) _LOGGER.info('%s: proximity calculation complete', entity_name)