def devices_recharged(self, devices, event_times, event_locations=None): """ Create a available:maintenance_drop_off status_change event for each device. Parameters: devices: list The list of devices to recharge. event_times: datetime, list datetime: a reference to produce a random event time within the given hour. len(list) == len(devices): use the corresponding event_time for each device. event_locations: GeoJSON Feature, list, optional None: generate a random dropoff location for each device. Feature: use this as the dropoff location. len(list) == len(devices): use the corresponding event_location for each device. Returns: list A list of available:maintenance_drop_off status_change events. """ status_changes = [] for device in devices: if isinstance(event_times, datetime.datetime): # how many seconds until the next hour? diff = (60 - event_times.minute - 1) * 60 + ( 60 - event_times.second) # random datetime between event_times and then event_time = util.random_date_from( event_times, max_td=datetime.timedelta(seconds=diff)) elif len(event_times) == len(devices): # corresponding datetime event_time = event_times[devices.index(device)] if event_locations is None: # random point point = geometry.point_within(self.boundary) event_location = mds.geometry.to_feature( point, properties=dict(timestamp=event_time)) elif len(event_locations) == len(devices): # corresponding location event_location = event_locations[devices.index(device)] else: # given location event_location = event_locations # create the event for this device status_changes.append( self.device_recharged(device, event_time, event_location)) return status_changes
def status_change_event(self, device, event_type=None, event_type_reason=None, event_time=None, event_location=None, **kwargs): """ Create a status_change event from the provided data. Parameters: device: dict The device that generated the event. event_type: str, optional The type of status_change event. event_type_reason: str, optional The reason for this type of status_change event. event_time: datetime, optional The time when the event occurred. event_location: GeoJSON Feature, optional The location where the event occurred. Additional keyword parameters are passed into the event as attributes. Returns: dict A dict representation of the status_change data. """ if event_type is None or event_type_reason is None: event_type, event_type_reason = self.event_pair( event_type, event_type_reason) if event_time is None: event_type = util.random_date_from(datetime.datetime.utcnow()) if event_location is None: point = geometry.point_within(self.boundary) event_location = mds.geometry.to_feature( point, properties=dict(timestamp=event_time)) status_change = dict(event_type=event_type, event_type_reason=event_type_reason, event_time=event_time, event_location=event_location, publication_time=event_time) return {**device, **status_change, **kwargs}
def end_service(self, devices, end_time, locations=None): """ Create status_change removed:service_end events. Parameters: devices: list The list of devices to bring into service. end_time: datetime The approximate time of service end. locations: list, optional The corresponding end location for each device. By default, generate a random location. Returns: list The list of status_change events. """ status_changes = [] # device pickup likely doesn't happen right at close time # +7200 seconds == next 2 hours after close offset = datetime.timedelta(seconds=7200) for device in devices: # somewhere in the next :offset: event_time = util.random_date_from(end_time, max_td=offset) # use the device's index for the locations if provided # otherwise generate a random event_location if locations is None: point = geometry.point_within(self.boundary) else: point = mds.geometry.extract_point( locations[devices.index(device)]) # the status_change details feature = mds.geometry.to_feature( point, properties=dict(timestamp=event_time)) service_end = self.status_change_event( device, event_type="removed", event_type_reason="service_end", event_time=event_time, event_location=feature) # combine with device details and append status_changes.append({**device, **service_end}) return status_changes
def start_service(self, devices, start_time): """ Create status_change available:service_start events. Parameters: devices: list The list of devices to bring into service. start_time: datetime The approximate time of service start. Return: list The list of service_start events. """ status_changes = [] # device placement starts before operation open time # -7200 seconds == previous 2 hours from start offset = datetime.timedelta(seconds=-7200) for device in devices: # somewhere in the previous :offset: event_time = util.random_date_from(start_time, min_td=offset) point = geometry.point_within(self.boundary) feature = mds.geometry.to_feature( point, properties=dict(timestamp=event_time)) # the status_change details service_start = self.status_change_event( device, event_type="available", event_type_reason="service_start", event_time=event_time, event_location=feature) # reset the battery for electric devices if self.has_battery(device): self.recharge_battery(device) # combine with device details and append status_changes.append({**device, **service_start}) return status_changes
def device_trip(self, device, event_time=None, event_location=None, end_location=None, reference_time=None, min_td=datetime.timedelta(seconds=0), max_td=datetime.timedelta(seconds=0), speed=None): """ Create a trip and associated status_changes for a device. Parameters: device: dict The device that will take the trip. event_time: datetime, optional The time the trip should start. event_location: GeoJSON Feature, optional The location the trip should start. end_location: GeoJSON Feature, optional The location the trip should end. reference_time: datetime, optional The 0-point around which to calculate a random start time. min_td: timedelta, optional The minimum time in the past from reference_time. max_td: timedelta, optional The maximum time in the future of reference_time. speed: int, optional The average speed of the device in meters/second. Returns: tuple (status_changes: list, trip: dict) """ if (event_time is None) and (reference_time is not None): event_time = util.random_date_from(reference_time, min_td=min_td, max_td=max_td) if event_location is None: point = geometry.point_within(self.boundary) event_location = mds.geometry.to_feature(point, properties=dict(timestamp=event_time)) if speed is None: speed = self.speed # begin the trip status_changes = [self.start_trip(device, event_time, event_location)] # random trip duration (in seconds) # the gamma distribution is referenced in the literature, # see: https://static.tti.tamu.edu/tti.tamu.edu/documents/17-1.pdf # experimenting with the scale factors led to these parameterizations, * 60 to get seconds alpha, beta = 3, 4.5 trip_duration = random.gammavariate(alpha, beta) * 60 # account for traffic, turns, etc. trip_distance = trip_duration * speed * 0.8 # Model the accuracy as a rayleigh distribution with median ~5m accuracy = scipy.stats.rayleigh.rvs(scale=5) # drain the battery according to the speed and distance traveled if self.has_battery(device): amount = speed / 100 rate = trip_distance / (math.sqrt(trip_distance) * 200) self.drain_battery(device, amount=amount, rate=rate) # calculate out the ending time and location end_time = event_time + datetime.timedelta(seconds=trip_duration) if end_location is None: start_point = mds.geometry.extract_point(event_location) end_point = geometry.point_nearby(start_point, trip_distance, boundary=self.boundary) end_location = mds.geometry.to_feature(end_point, properties=dict(timestamp=end_time)) # generate the route object route = self.trip_route(event_location, end_location) # and finally the trip object trip = dict( accuracy=int(accuracy), trip_id=uuid.uuid4(), trip_duration=int(trip_duration), trip_distance=int(trip_distance), route=route, start_time=event_time, end_time=end_time ) if self.version >= Version("0.3.0"): trip[PUBLICATION_TIME] = end_time # add a parking_verification_url? if random.choice([True, False]): trip.update(parking_verification_url=util.random_file_url(device["provider_name"])) # add a standard_cost? if random.choice([True, False]): # $1.00 to start and $0.15 a minute thereafter trip.update(standard_cost=(100 + (math.floor(trip_duration/60) - 1) * 15)) # add an actual cost? if random.choice([True, False]): # randomize an actual_cost # $0.75 - $1.50 to start, and $0.12 - $0.20 a minute thereafter... start, rate = random.randint(75, 150), random.randint(12, 20) trip.update(actual_cost=(start + (math.floor(trip_duration/60) - 1) * rate)) # end the trip status_changes.append(self.end_trip(device, end_time, end_location)) # merge the device info into the trip trip = {**device, **trip} # cleanup leftover fields not part of a trip if BATTERY in trip: del trip[BATTERY] if EVENT_TIME in trip: del trip[EVENT_TIME] # return a list of the status_changes and the trip return [{**device, **sc} for sc in status_changes], trip
def vehicle_status(self, device, event_type=None, event_type_reason=None, event_time=None, event_location=None, current_location=None, battery_pct=None, **kwargs): """ Create a status event compatible with the vehicles endpoint. Parameters: device: dict The device that generated the event. event_type: str, optional Event type of most recent status change. event_type_reason: str, optional Event type reason of most recent status change. event_time: datetime, optional Date/time when last status change occurred. event_location: GeoJSON Feature, optional Location of vehicle's last event. current_location: GeoJSON Feature, optional Current location of vehicle if different from last event. battery_pct: decimal, optional Percent battery charge of device, expressed between 0 and 1. Additional keyword parameters are passed into the vehicle as attributes. Returns: dict A dict representation of the vehicle status data. """ if event_type is None or event_type_reason is None: event_type, event_type_reason = self.event_pair( event_type, event_type_reason) if event_time is None: event_time = util.random_date_from(datetime.datetime.utcnow()) if event_location is None: point = geometry.point_within(self.boundary) event_location = mds.geometry.to_feature( point, properties=dict(timestamp=event_time)) last_status = dict(last_event_time=event_time, last_event_type=event_type, last_event_type_reason=event_type_reason, last_event_location=event_location) if current_location: last_status.update(current_location=current_location) if battery_pct is not None: last_status.update(battery_pct=battery_pct) return {**device, **last_status, **kwargs}