{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Retail Demo Store - Location Workshop\n", "\n", "Welcome to the Retail Demo Store Location Workshop. In this module we're going to be seeing a very simple example of how Amazon Location Services can be used as the basis for location-sensitive marketing and transacting. By using location events to initiate user flows, we can catch shoppers when they are in the right place and time.\n", "\n", "In this case, we illustrate the use case of reminders for users to pick up orders. Here we make use of Amazon Pinpoint Transactional Messaging to send these transactional messages. We might equally send reminder campaigns based on recent purchases by users and products available or in overstock in stores nearby. If we were to do that we might use Pinpoint campaigns set up to message users based on events that we propagate from Location Services to Pinpoint using a similar method to that described here.\n", "\n", "*For a more in-depth example using Amazon Personalize to generate personalized offers, the use of \"Campaigns\" in Amazon Pinpoint to target users with unfinished purchases and incorporate Amazon Personalize output, and the maps functionality of Amazon Location Services to visualise user behaviour, see the Location Services demo on the UI - see \"Retail Geofencing and Location-aware Personalization\" in the demo guide for details of that functionality.*\n", "\n", "Recommended Time: 1 Hour" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Manual setup\n", "\n", "**IMPORTANT: Please read this section carefully**.\n", "\n", "Before you can successfully run the code below there are a small number of setup steps to do:\n", "\n", "1. Go to the retail store storefront that was created when you deployed the Retail Demo Store with CloudFormation and make a user. Ensure that you create a user with a valid email, that you use. **Take a note of the username that you create below:**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cognito_username = 'daemon' # CHANGE THIS TO YOUR USERNAME" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "2. Additionally ensure that when you go into Amazon Pinpoint, select a project, and Settings > Email, that the email channel is enabled and that you have a verified email address set up as default sender (this can be the same email address as you saved against the user)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note: at the bottom of this notebook is code for cleaning up the resources that you create." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Setup\n", "\n", "To get started, we need to perform a bit of setup. Walk through each of the following steps to configure your environment to interact with the Amazon Location Service." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Warning! IAM permissions for this notebook are set up by default for the following names \n", "# - if you change them you may no longer have permissions to run the below operations.\n", "resource_name = 'RetailDemoStoreLocationWorkshop' # We name all our Location resources this\n", "event_handler_function_name = 'LocationNotebookEventHandler' \n", "eventbridge_target_id = 'GeofenceEventHandlerTarger'\n", "eventbridge_rule_name = 'GeofenceEventHandlerRule'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Configure the name of resources you are going to create:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Import Dependencies and Setup Boto3 Python Clients\n", "\n", "Throughout this workshop we will need access to some common libraries and clients for connecting to AWS services. We also have to retrieve Uid from a SageMaker notebook instance tag." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# While Location is in beta, we need to grab its service description - this is up to date in boto3\n", "\n", "! pip install botocore==1.19.38 boto3==1.16.38" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Import Dependencies\n", "\n", "import boto3\n", "import json\n", "import time\n", "import botocore\n", "import os\n", "import requests\n", "from datetime import datetime\n", "\n", "from sagemaker import get_execution_role as sagemaker_get_execution_role\n", "from matplotlib import pyplot as plt\n", "\n", "# Setup Clients\n", "servicediscovery = boto3.client('servicediscovery')\n", "location = boto3.client('location')\n", "awslambda = boto3.client('lambda')\n", "events = boto3.client('events')\n", "\n", "# Retrieve this notebook instance's role\n", "sagemaker_role = sagemaker_get_execution_role()\n", "print('Sagemaker role:', sagemaker_role)\n", "\n", "# Get some data passed from CloudFormation\n", "with open('/opt/ml/metadata/resource-metadata.json') as f:\n", " data = json.load(f)\n", " sagemaker_client = boto3.client('sagemaker')\n", " sagemakerResponce = sagemaker_client.list_tags(ResourceArn=data[\"ResourceArn\"])\n", " for tag in sagemakerResponce[\"Tags\"]:\n", " if tag['Key'] == 'Uid':\n", " Uid = tag['Value']\n", " print('Uid:', Uid)\n", " if tag['Key'] == 'UserPoolId':\n", " UserPoolId = tag['Value']\n", " print('UserPoolId:', UserPoolId)\n", " if tag['Key'] == 'PinpointAppId':\n", " PinpointAppId = tag['Value']\n", " print('PinpointAppId:', PinpointAppId) \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Create Location resources" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create a Location Map\n", "\n", "While not necessary for the following steps, the following will create a map which you could in applications use to visualise geofences etc. that you create. Navigate to the [Location UI Console](https://console.aws.amazon.com/location/) to see the map, and check out the SDKs for other use options." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\n", "create_map_response = location.create_map(\n", " MapName=resource_name,\n", " Configuration={\n", " 'Style': 'VectorEsriNavigation'\n", " },\n", " Description='Retail Demo Store Sample',\n", " PricingPlan='RequestBasedUsage'\n", ")\n", "print(json.dumps(create_map_response, indent=4, default=str))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create a Location Geofence Collection\n", "\n", "The Geofence Collection is used to fire events when users cross into the geofence - it can be used to start user processes such as product collection or send located marketing messages." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "create_geofence_collection_response = location.create_geofence_collection(\n", " CollectionName=resource_name,\n", " Description='Retail Demo Store Sample',\n", " PricingPlan='RequestBasedUsage'\n", ")\n", "geofence_collection_arn = create_geofence_collection_response['CollectionArn']\n", "print(json.dumps(create_geofence_collection_response, indent=4, default=str))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create a Location Geofence\n", "\n", "We can add to Geofence Collections multiple geofences. Each one defines a region such that if a user moves from outside the union of the regions to the inside or visa versa, an event will be fired so that we can make use of this geographic aspect of user behaviour.\n", "\n", "The following geofence represents an area around at latitude/longitude 0,0 at a size of 1 degree." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "store_geofence = \\\n", " [[-1, 1],\n", " [-1,-1],\n", " [ 1,-1],\n", " [ 1, 1],\n", " [-1, 1]]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "put_geofence_response = location.put_geofence(\n", " CollectionName=resource_name,\n", " Geometry={\"Polygon\": [store_geofence]},\n", " GeofenceId=resource_name\n", ")\n", "\n", "print(json.dumps(put_geofence_response, indent=4, default=str))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that using the \"maps\" API of Location, it would be possible to visualise this Geofence on a map, along with other visualisations." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create a tracker\n", "\n", "A tracker is used to record the locations of any number of \"devices\" that can send updates about ther position to Location. You can imagine that Location may be sending position updates from retailer apps on their phone and so forth." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "create_tracker_response = location.create_tracker(\n", " TrackerName=resource_name,\n", " Description='Retail Demo Store Sample',\n", " PricingPlan='RequestBasedUsage'\n", ")\n", "\n", "print(json.dumps(create_tracker_response, indent=4, default=str))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Associate tracker with geofence\n", "\n", "By associating a tracker with a geofence collection, we add a link between the tracker and the geofence such that any devices in the tracker impinging the geofences in the geofence collection will fire an AWS EventBridge event." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "associate_tracker_consumer_response = location.associate_tracker_consumer(\n", " ConsumerArn=geofence_collection_arn,\n", " TrackerName=resource_name\n", ")\n", "\n", "print(json.dumps(associate_tracker_consumer_response, indent=4, default=str))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Handle Location Geofence Events\n", "\n", "As mentioned above, we have set up an EventBridge event to fire whenever a user comes near our fictional store. What kind of things might we do with such events? Here, we set up an AWS Lambda function to be called when such events fire, that checks whether a user has any orders that they have made and accordingly sends them an email telling them to come and pick it up in-store.\n", "\n", "For your reference, we provide here typical structure of a Location event. The DeviceID is filled in when the tracker is updated with a device's location via the Location API (in our application we fill DeviceID with a Cognito User ID e.g. \"daemon\"). \n", "\n", "Here is a typical structure of a Location enter event (exit events are also possible): \n", "\n", " {\n", " \"version\": \"0\",\n", " \"id\": \"12345678-9abc-def0-1234-56789abcdef0\",\n", " \"detail-type\": \"Location Geofence Event\",\n", " \"source\": \"aws.geo\",\n", " \"account\": \"YOUR_ACCOUNT_ID\",\n", " \"time\": \"2020-11-23T14:30:33Z\",\n", " \"region\": \"us-east-1\",\n", " \"resources\": [\n", " \"arn:aws:geo:us-east-1:YOUR_ACCOUNT_ID:geofence-collection/COLLECTIONID\",\n", " \"arn:aws:geo:us-east-1:YOUR_ACCOUNT_ID:tracker/TRACKERID\"\n", " ],\n", " \"detail\": {\n", " \"EventType\": \"ENTER\",\n", " \"GeofenceId\": \"GEOFENCEID\",\n", " \"DeviceId\": \"FILL_THIS_IN: WE_USE_COGNITO_USER\",\n", " \"SampleTime\": \"2020-11-23T14:30:32.867Z\",\n", " \"Position\": [-100,50]\n", " }\n", " }\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Write a Lambda handler\n", "\n", "Here we write a Lambda function to handle an event of this sort. It is going to use the Amazon Pinpoint [Transactional API](https://docs.aws.amazon.com/pinpoint/latest/developerguide/send-messages-sdk.html) to send email messages to users who have waiting orders. We will hit the orders microservice to check if orders are available. The Lambda function expects the Pinpoint App ID (Project ID), the Cognito User Pool ID in environment variables. The Lambda function is going to live in a subdirectory called \"location-event-handler\"." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "! [ -d location-event-handler ] && rm -r location-event-handler # remove Lambda bundle if exists\n", "! mkdir location-event-handler # We will put Lambda function and dependencies in here" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the following function, we receive an event and send an email to the user configured at the top of this notebook.\n", "\n", "We could also, for example, as an extra step, hit the orders service to retrieve order details.\n", "\n", "The ``%%writefile`` directive writes a Jupyter notebook cell as a file - that way you can see here what we are going to send as an AWS Lambda function. In later cells we will deploy this file as an AWS Lambda function." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%writefile location-event-handler/location-event-handler.py\n", "\n", "import boto3\n", "import logging\n", "import os \n", "import urllib3\n", "import json\n", "\n", "pinpoint = boto3.client('pinpoint')\n", "cognito_idp = boto3.client('cognito-idp')\n", "servicediscovery = boto3.client('servicediscovery')\n", "\n", "logger = logging.getLogger()\n", "logger.setLevel(logging.INFO)\n", "\n", "def send_email(to_email, subject, html_content, text_content):\n", " \"\"\"\n", " Send a default email to the address. Pull pinpoint app ID and from address from env.\n", " More information about this service:\n", " https://docs.aws.amazon.com/pinpoint/latest/developerguide/send-messages-email.html\n", " Character set is UTF-8.\n", " Args:\n", " to_email: Email to send to\n", " subject: Subject of email\n", " html_content: HTML version of email content\n", " text_content: Plain text version of email content\n", " \"\"\"\n", "\n", " pinpoint_app_id = os.environ['PinpointAppId']\n", " response = pinpoint.send_messages(\n", " ApplicationId=pinpoint_app_id,\n", " MessageRequest={\n", " 'Addresses': {\n", " to_email: {\n", " 'ChannelType': 'EMAIL'\n", " }\n", " },\n", " 'MessageConfiguration': {\n", " 'EmailMessage': {\n", " 'SimpleEmail': {\n", " 'Subject': {\n", " 'Charset': \"UTF-8\",\n", " 'Data': subject\n", " },\n", " 'HtmlPart': {\n", " 'Charset': \"UTF-8\",\n", " 'Data': html_content\n", " },\n", " 'TextPart': {\n", " 'Charset': \"UTF-8\",\n", " 'Data': text_content\n", " }\n", " }\n", " }\n", " }\n", " }\n", " )\n", " logger.info(f'Message sent to {to_email} and response: {response}')\n", "\n", "\n", "\n", "def send_pickup_email_minimal(to_email):\n", " \"\"\"\n", " Send it to customer saying order ready for pickup as email\n", " Args:\n", " to_email: Where to send the email to\n", " \"\"\"\n", "\n", " # Specify content:\n", " subject = \"Come pick up your order nearby!\"\n", " heading = \"You can pick up your order nearby!\"\n", " intro_text = \"\"\"\n", " Welcome, \n", " We are waiting for you at Level 3, Door 2 of your Local Retail Demo Store, and Steve from our team will be greeting you with your orders.\n", " Thank you for shopping!\"\"\"\n", " html_intro_text = intro_text.replace('\\n', '
')\n", "\n", " # Build HTML message\n", " html = f\"\"\"\n", "
\n", " \n", "{html_intro_text}\n", " \n", " \"\"\"\n", "\n", " # Build text message\n", " text = f\"\"\"\n", "{heading}\n", "{intro_text}\n", " \"\"\"\n", "\n", " logger.info(f\"Contents of email to {to_email} html: \\n{html}\")\n", " logger.info(f\"Contents of email to {to_email} text: \\n{text}\")\n", " send_email(to_email, subject, html, text)\n", "\n", " \n", "def get_order_details(shopper_user_id):\n", " \"\"\"\n", " Grab internal orders service URL from service discovery \n", " and get order details from the service for the given user.\n", " **For this to work, your Lambda needs to be set up on the VPC for the orders service.**\n", " See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html\n", " Args:\n", " shopper_user_id: User to get orders for.\n", " \"\"\"\n", "\n", " # Get the URL of the carts service\n", " response = servicediscovery.discover_instances(\n", " NamespaceName='retaildemostore.local',\n", " ServiceName='orders',\n", " MaxResults=1,\n", " HealthStatus='HEALTHY'\n", " )\n", " orders_service_url = response['Instances'][0]['Attributes']['AWS_INSTANCE_IPV4']\n", " logging.info(f'Orders Service Instance IP: {orders_service_url}')\n", "\n", " # Hit orders service to decide if there are waiting orders.\n", " shopper_user_name = 'user' + shopper_user_id\n", " orders_url = f'http://{orders_service_url}/orders/username/{shopper_user_name}'\n", " logging.info(f'Hitting {orders_url}')\n", " try: \n", " # To make this work properly, you need to make sure that the\n", " # Lambda is able to acces this IP - this is beyond the scope of this notebook.\n", " # See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html\n", " response = urllib3.PoolManager().request('GET', orders_url, retries=False)\n", " orders = json.loads(response.data)\n", " except Exception as e:\n", " logger.error('Could not get orders - assuming there is none. Exception: ')\n", " logger.error(e)\n", " orders=[]\n", " \n", " return orders\n", " \n", "\n", "def lambda_handler(event, context):\n", " \"\"\"Handle a Location Geofence enter event. Send an email for this user if they have some orders.\n", " Args:\n", " event: From EventBridge - contains information from Location\n", " context: We do not make use of this.\n", " \"\"\"\n", " logger.info('Environment:')\n", " logger.info(os.environ)\n", " logger.info('Event:')\n", " logger.info(event)\n", "\n", " if event['detail-type'] == \"Location Geofence Event\" and event['detail']['EventType'] == \"ENTER\":\n", " location_device_id = event['detail']['DeviceId']\n", " # We assume that the location device ID is just the cognito ID - see where we insert location changes below.\n", " cognito_user_id = location_device_id\n", " \n", " # Get email and shopper ID from Cognito profile\n", " user_pool_id = os.environ['UserPoolId']\n", " response = cognito_idp.admin_get_user(\n", " UserPoolId=user_pool_id,\n", " Username=cognito_user_id\n", " )\n", " user_attributes = {att['Name']: att['Value'] for att in response['UserAttributes']}\n", " to_email = user_attributes['email']\n", " shopper_user_id = user_attributes['custom:profile_user_id']\n", " \n", " orders= [\"An order!\"] # Let us assume that the customer has an order so we can get an email every time\n", " logging.info(f'Orders: {json.dumps(orders, indent=2)}')\n", "\n", " if len(orders) > 0:\n", " # There are waiting orders! Email about them.\n", " logger.info(f\"User {shopper_user_id} has some orders - send her/him an email\")\n", " send_pickup_email_minimal(to_email)\n", " else:\n", " logger.info(f\"User {shopper_user_id} has no orders\")\n", " \n", " logger.warning('END')\n", " \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let us ensure that that got written where we expect (for those who may not be familiar with ``%%writefile``)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!tail location-event-handler/location-event-handler.py" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If our Lambda function makes use of a non-core module we could install it as follows:\n", "\n", " !pip install --target ./location-event-handler MODULE" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Bundle all of that up into a Lambda function." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "! [ -f location-event-handler.zip ] && rm location-event-handler.zip # remove deployment bundle if exists\n", "!cd location-event-handler && zip -r9 ../location-event-handler.zip ." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Create the actual Lambda function in the cloud." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "create_function_response = awslambda.create_function(\n", " FunctionName='LocationNotebookEventHandler',\n", " Runtime=\"python3.8\",\n", " Role=sagemaker_role,\n", " Handler=\"location-event-handler.lambda_handler\",\n", " Code={'ZipFile': open('./location-event-handler.zip', 'rb').read()},\n", " Timeout=20,\n", " Environment = {'Variables':{\n", " 'UserPoolId': UserPoolId,\n", " 'PinpointAppId': PinpointAppId\n", " }}\n", ")\n", "\n", "function_arn = create_function_response['FunctionArn']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Connect Location Geofence events up to Lambda" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We are going to use the following event pattern with EventBridge to connect up Location with our processing Lambda. For more details about such event patterns see https://docs.aws.amazon.com/eventbridge/latest/userguide/eventbridge-and-event-patterns.html " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\n", "event_pattern = \\\n", " {\n", " \"source\": [\n", " \"aws.geo\"\n", " ],\n", " \"resources\": [\n", " geofence_collection_arn\n", " ],\n", " \"detail-type\":[\n", " \"Location Geofence Event\"\n", " ],\n", " \"detail\":{\n", " \"EventType\": [\"ENTER\"]\n", " }\n", " }\n", "print('Event pattern:\\n', json.dumps(event_pattern, indent=4))\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We put the event rule and add the Lambda function as its target:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "put_rule_response = events.put_rule(Name=eventbridge_rule_name,\n", " EventPattern=json.dumps(event_pattern))\n", "print(json.dumps(put_rule_response, indent=4))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "put_targets_response = events.put_targets(Rule=eventbridge_rule_name,\n", " Targets=[{'Id': eventbridge_target_id,\n", " 'Arn': function_arn}])\n", "print(json.dumps(put_targets_response, indent=4))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Add a permission to the Lambda function to allow the event rule we just created to invoke the function:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "add_lambda_permission_response = awslambda.add_permission(\n", " FunctionName='LocationNotebookEventHandler', \n", " StatementId='EventBridgeInvokePermission',\n", " Action=\"lambda:InvokeFunction\", \n", " Principal=\"events.amazonaws.com\",\n", " SourceArn=put_rule_response['RuleArn'],\n", ")\n", "print(json.dumps(add_lambda_permission_response, indent=4))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Track a device\n", "Let us simulate a customer entering and exiting the geofence so that we get the Geofence event firing, and our custom Lambda above firing. If you fail to get the emails, you can try visiting the Lambda function in the UI console ( https://console.aws.amazon.com/lambda/home#/functions/LocationNotebookEventHandler?tab=monitoring ), clicking on Monitoring, and checking the logs.\n", "\n", "The following route starts outside and enters the geofence." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "customer_route = [(i*0.1, 0) for i in range(30, -1, -1)]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for position in customer_route:\n", " # Use the UTC time here - important! Otherwise you will be putting future events and not trigger\n", " t = datetime.utcnow() \n", " time.sleep(2) # let us leave two seconds between each step\n", " print(f\"Updating position: {position[0]:0.1f}, {position[1]:0.1f}\")\n", " location.batch_update_device_position(\n", " TrackerName=resource_name,\n", " Updates=[{\n", " 'DeviceId': cognito_username,\n", " 'Position': position,\n", " 'SampleTime': t\n", " }])\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If your Pinpoint has been set up with a verified email, you should receive a message like this:\n", "\n", "\n", "\n", "Yay!\n", "\n", "If not, you can try visiting the Lambda function in the UI console ( https://console.aws.amazon.com/lambda/home#/functions/LocationNotebookEventHandler?tab=monitoring ), clicking on Monitoring, and checking the logs." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let us retrieve the latest position you provided for your \"DeviceId\" (in this Notebook we choose your Cognito username so that you get emails sent to you):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "location.get_device_position(DeviceId=cognito_username, TrackerName=resource_name)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Or, you can check that the history you provided like this:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for update in location.get_device_position_history(DeviceId=cognito_username, TrackerName=resource_name)['DevicePositions']:\n", " print(f\"{update['SampleTime'].isoformat()} -- {update['Position'][0]:0.1f}, {update['Position'][1]:0.1f}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that Location's time resolution of update is such that not every update to position will be recorded here.\n", "\n", "Let us do a plot of that path against the geofence, so we can visualise the whole thing:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "plt.figure(figsize=[6,5])\n", "for geofence in location.list_geofences(CollectionName=resource_name)['Entries']:\n", " for polygon in geofence['Geometry']['Polygon']:\n", " geox = [pos[0] for pos in polygon]\n", " geoy = [pos[1] for pos in polygon]\n", " plt.plot(geox, geoy, 'r-', label='Geofence')\n", "device_positions = location.get_device_position_history(DeviceId=cognito_username,TrackerName=resource_name)['DevicePositions']\n", "for ind, update in enumerate(device_positions):\n", " label = 'Location History' if ind==0 else None\n", " plt.plot(update['Position'][0], update['Position'][1], \n", " color=(1-ind/len(device_positions),ind/len(device_positions),0), \n", " linewidth=0, marker='x', label=label)\n", "plt.title(f\"Location history for DeviceId '{cognito_username}'\")\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Next steps\n", "\n", "\n", "The full deployed Location demo version of this workshop explores how Location may be used in many more ways. We suggest you investigate the code and get a feel for how these things are done. \n", "\n", "Additions might include, for example:\n", "\n", " - Full collection handling logic and notifications for stores when users are near to stores.\n", " - Integration of Location with Amazon Pinpoint marketing \"campaigns\" by propagating Location events into Pinpoint events. In the full demo we incorporate this into \"abandoned cart\" campaigns.\n", " - Further integration with Amazon Personalize by giving localised recommendations. In our full demo we provide coupon offers to users when then get close to the target store.\n", " - The sky is the limit!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Location Workshop Cleanup\n", "\n", "The remainder of this notebook will walk through deleting all of the resources created. You should only need to perform these steps if you have deployed the Retail Demo Store in your own AWS account and want to deprovision the Location resources. If you are participating in an AWS-led workshop, this process is likely not necessary.\n", "\n", "In order, we will delete event rules, lambda function, then location resources.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('Detaching from rule', eventbridge_rule_name, 'target', eventbridge_target_id)\n", "remove_targets_response = events.remove_targets(\n", " Rule=eventbridge_rule_name,\n", " Ids=[\n", " eventbridge_target_id\n", " ]\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('Deleting event rule', eventbridge_rule_name)\n", "delete_rule_response = events.delete_rule(\n", " Name=eventbridge_rule_name\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('Deleting function LocationNotebookEventHandler')\n", "delete_function_response = awslambda.delete_function(\n", " FunctionName='LocationNotebookEventHandler'\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('Disassociating tracker', resource_name, 'from geofence', geofence_collection_arn)\n", "disassociate_tracker_consumer_response = location.disassociate_tracker_consumer(\n", " ConsumerArn=geofence_collection_arn,\n", " TrackerName=resource_name\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('Deleting map with name', resource_name)\n", "delete_map_response = location.delete_map(MapName=resource_name)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('Deleting geofence collection with name', resource_name)\n", "delete_geofence_collection_response = location.delete_geofence_collection(CollectionName=resource_name)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('Deleting tracker with name', resource_name)\n", "delete_tracker_response = location.delete_tracker(TrackerName=resource_name)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Done! Have a nice day!" ] } ], "metadata": { "kernelspec": { "display_name": "conda_python3", "language": "python", "name": "conda_python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.13" } }, "nbformat": 4, "nbformat_minor": 4 }