def distance(self, lat: float, lon: float) -> Optional[float]: """Calculate distance from Open Peer Power. Async friendly. """ return self.units.length( location.distance(self.latitude, self.longitude, lat, lon), "m" )
def test_get_distance(): """Test getting the distance.""" meters = location_util.distance( COORDINATES_PARIS[0], COORDINATES_PARIS[1], COORDINATES_NEW_YORK[0], COORDINATES_NEW_YORK[1], ) assert meters / 1000 - DISTANCE_KM < 0.01
def test_get_distance_to_same_place(): """Test getting the distance.""" meters = location_util.distance( COORDINATES_PARIS[0], COORDINATES_PARIS[1], COORDINATES_PARIS[0], COORDINATES_PARIS[1], ) assert meters == 0
def distance(opp, *args): """Calculate distance. Will calculate distance from home to a point or between points. Points can be passed in using state objects or lat/lng coordinates. """ locations = [] to_process = list(args) while to_process: value = to_process.pop(0) if isinstance(value, str) and not valid_entity_id(value): point_state = None else: point_state = _resolve_state(opp, value) if point_state is None: # We expect this and next value to be lat&lng if not to_process: _LOGGER.warning( "Distance:Expected latitude and longitude, got %s", value) return None value_2 = to_process.pop(0) latitude = convert(value, float) longitude = convert(value_2, float) if latitude is None or longitude is None: _LOGGER.warning( "Distance:Unable to process latitude and longitude: %s, %s", value, value_2, ) return None else: if not loc_helper.has_location(point_state): _LOGGER.warning( "Distance:State does not contain valid location: %s", point_state) return None latitude = point_state.attributes.get(ATTR_LATITUDE) longitude = point_state.attributes.get(ATTR_LONGITUDE) locations.append((latitude, longitude)) if len(locations) == 1: return opp.config.distance(*locations[0]) return opp.config.units.length( loc_util.distance(*locations[0] + locations[1]), LENGTH_METERS)
async def async_setup_platform(opp, config, async_add_entities, discovery_info=None): """Set up the CityBikes platform.""" if PLATFORM not in opp.data: opp.data[PLATFORM] = {MONITORED_NETWORKS: {}} latitude = config.get(CONF_LATITUDE, opp.config.latitude) longitude = config.get(CONF_LONGITUDE, opp.config.longitude) network_id = config.get(CONF_NETWORK) stations_list = set(config.get(CONF_STATIONS_LIST, [])) radius = config.get(CONF_RADIUS, 0) name = config[CONF_NAME] if not opp.config.units.is_metric: radius = distance.convert(radius, LENGTH_FEET, LENGTH_METERS) # Create a single instance of CityBikesNetworks. networks = opp.data.setdefault(CITYBIKES_NETWORKS, CityBikesNetworks(opp)) if not network_id: network_id = await networks.get_closest_network_id(latitude, longitude) if network_id not in opp.data[PLATFORM][MONITORED_NETWORKS]: network = CityBikesNetwork(opp, network_id) opp.data[PLATFORM][MONITORED_NETWORKS][network_id] = network opp.async_create_task(network.async_refresh()) async_track_time_interval(opp, network.async_refresh, SCAN_INTERVAL) else: network = opp.data[PLATFORM][MONITORED_NETWORKS][network_id] await network.ready.wait() devices = [] for station in network.stations: dist = location.distance(latitude, longitude, station[ATTR_LATITUDE], station[ATTR_LONGITUDE]) station_id = station[ATTR_ID] station_uid = str(station.get(ATTR_EXTRA, {}).get(ATTR_UID, "")) if radius > dist or stations_list.intersection( (station_id, station_uid)): if name: uid = "_".join([network.network_id, name, station_id]) else: uid = "_".join([network.network_id, station_id]) entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, uid, opp=opp) devices.append(CityBikesStation(network, station_id, entity_id)) async_add_entities(devices, True)
def in_zone(zone: State, latitude: float, longitude: float, radius: float = 0) -> bool: """Test if given latitude, longitude is in given zone. Async friendly. """ zone_dist = distance( latitude, longitude, zone.attributes[ATTR_LATITUDE], zone.attributes[ATTR_LONGITUDE], ) if zone_dist is None or zone.attributes[ATTR_RADIUS] is None: return False return zone_dist - radius < cast(float, zone.attributes[ATTR_RADIUS])
def closest(latitude: float, longitude: float, states: Sequence[State]) -> Optional[State]: """Return closest state to point. Async friendly. """ with_location = [state for state in states if has_location(state)] if not with_location: return None return min( with_location, key=lambda state: loc_util.distance( state.attributes.get(ATTR_LATITUDE), state.attributes.get(ATTR_LONGITUDE), latitude, longitude, ), )
def async_active_zone(opp: OpenPeerPower, latitude: float, longitude: float, radius: int = 0) -> State | None: """Find the active zone for given latitude, longitude. This method must be run in the event loop. """ # Sort entity IDs so that we are deterministic if equal distance to 2 zones zones = (cast(State, opp.states.get(entity_id)) for entity_id in sorted(opp.states.async_entity_ids(DOMAIN))) min_dist = None closest = None for zone in zones: if zone.state == STATE_UNAVAILABLE or zone.attributes.get( ATTR_PASSIVE): continue zone_dist = distance( latitude, longitude, zone.attributes[ATTR_LATITUDE], zone.attributes[ATTR_LONGITUDE], ) if zone_dist is None: continue within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS] closer_zone = closest is None or zone_dist < min_dist # type: ignore smaller_zone = (zone_dist == min_dist and zone.attributes[ATTR_RADIUS] < cast( State, closest).attributes[ATTR_RADIUS]) if within_zone and (closer_zone or smaller_zone): min_dist = zone_dist closest = zone return closest
def update(self): """Update device state.""" currently_tracked = set() flight_metadata = {} states = self._session.get(OPENSKY_API_URL).json().get(ATTR_STATES) for state in states: flight = dict(zip(OPENSKY_API_FIELDS, state)) callsign = flight[ATTR_CALLSIGN].strip() if callsign != "": flight_metadata[callsign] = flight else: continue missing_location = (flight.get(ATTR_LONGITUDE) is None or flight.get(ATTR_LATITUDE) is None) if missing_location: continue if flight.get(ATTR_ON_GROUND): continue distance = util_location.distance( self._latitude, self._longitude, flight.get(ATTR_LATITUDE), flight.get(ATTR_LONGITUDE), ) if distance is None or distance > self._radius: continue altitude = flight.get(ATTR_ALTITUDE) if altitude > self._altitude and self._altitude != 0: continue currently_tracked.add(callsign) if self._previously_tracked is not None: entries = currently_tracked - self._previously_tracked exits = self._previously_tracked - currently_tracked self._handle_boundary(entries, EVENT_OPENSKY_ENTRY, flight_metadata) self._handle_boundary(exits, EVENT_OPENSKY_EXIT, flight_metadata) self._state = len(currently_tracked) self._previously_tracked = currently_tracked
async def get_closest_network_id(self, latitude, longitude): """Return the id of the network closest to provided location.""" try: await self.networks_loading.acquire() if self.networks is None: networks = await async_citybikes_request( self.opp, NETWORKS_URI, NETWORKS_RESPONSE_SCHEMA) self.networks = networks[ATTR_NETWORKS_LIST] result = None minimum_dist = None for network in self.networks: network_latitude = network[ATTR_LOCATION][ATTR_LATITUDE] network_longitude = network[ATTR_LOCATION][ATTR_LONGITUDE] dist = location.distance(latitude, longitude, network_latitude, network_longitude) if minimum_dist is None or dist < minimum_dist: minimum_dist = dist result = network[ATTR_ID] return result except CityBikesRequestError as err: raise PlatformNotReady from err finally: self.networks_loading.release()
def check_proximity_state_change(self, entity, old_state, new_state): """Perform the proximity checking.""" entity_name = new_state.name devices_to_calculate = False devices_in_zone = "" zone_state = self.opp.states.get(self.proximity_zone) proximity_latitude = zone_state.attributes.get(ATTR_LATITUDE) proximity_longitude = zone_state.attributes.get(ATTR_LONGITUDE) # Check for devices in the monitored zone. for device in self.proximity_devices: device_state = self.opp.states.get(device) if device_state is None: devices_to_calculate = True continue 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 = f"{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.schedule_update_op_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.schedule_update_op_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.opp.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[ATTR_LATITUDE], device_state.attributes[ATTR_LONGITUDE], ) # Add the device and distance to a dictionary. distances_to_zone[device] = round( convert(dist_to_zone, LENGTH_METERS, self.unit_of_measurement), 1) # Loop through each of the distances collected and work out the # closest. closest_device: str = None dist_to_zone: float = None for device in distances_to_zone: if not dist_to_zone or 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.opp.states.get(closest_device) self.nearest = device_state.name self.schedule_update_op_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.schedule_update_op_state() return # Reset the variables distance_travelled = 0 # Calculate the distance travelled. old_distance = distance( proximity_latitude, proximity_longitude, old_state.attributes[ATTR_LATITUDE], old_state.attributes[ATTR_LONGITUDE], ) new_distance = distance( proximity_latitude, proximity_longitude, new_state.attributes[ATTR_LATITUDE], new_state.attributes[ATTR_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.schedule_update_op_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)
def _determine_interval(self) -> int: """Calculate new interval between two API fetch (in minutes).""" intervals = {"default": self._max_interval} for device in self._devices.values(): # Max interval if no location if device.location is None: continue current_zone = run_callback_threadsafe( self.opp.loop, async_active_zone, self.opp, device.location[DEVICE_LOCATION_LATITUDE], device.location[DEVICE_LOCATION_LONGITUDE], device.location[DEVICE_LOCATION_HORIZONTAL_ACCURACY], ).result() # Max interval if in zone if current_zone is not None: continue zones = ( self.opp.states.get(entity_id) for entity_id in sorted(self.opp.states.entity_ids("zone"))) distances = [] for zone_state in zones: zone_state_lat = zone_state.attributes[ DEVICE_LOCATION_LATITUDE] zone_state_long = zone_state.attributes[ DEVICE_LOCATION_LONGITUDE] zone_distance = distance( device.location[DEVICE_LOCATION_LATITUDE], device.location[DEVICE_LOCATION_LONGITUDE], zone_state_lat, zone_state_long, ) distances.append(round(zone_distance / 1000, 1)) # Max interval if no zone if not distances: continue mindistance = min(distances) # Calculate out how long it would take for the device to drive # to the nearest zone at 120 km/h: interval = round(mindistance / 2, 0) # Never poll more than once per minute interval = max(interval, 1) if interval > 180: # Three hour drive? # This is far enough that they might be flying interval = self._max_interval if (device.battery_level is not None and device.battery_level <= 33 and mindistance > 3): # Low battery - let's check half as often interval = interval * 2 intervals[device.name] = interval return max( int(min(intervals.items(), key=operator.itemgetter(1))[1]), self._max_interval, )