用 Twilio/SendGrid 为 IoT 地理围栏触发函数添加短信与邮件通知
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
本文围绕 IoT-For-Beginners 运输项目第 4 课(Geofences)的课后作业展开。该作业要求在已有的 Azure Functions 地理围栏触发器中,通过 Azure Functions 输出绑定接入 Twilio SMS 或 SendGrid 邮件服务,在 GPS 坐标进入或离开地理围栏时发送实时通知,并保证"只在进入或只在离开时触发一次"。读完本文,你将掌握 Azure Functions 输出绑定(Output Binding)的配置方法、Python 函数中
func.Out参数的使用方式,以及如何在无状态 Serverless 代码中实现"进入/离开二选一"的通知逻辑。
任务背景:当前代码只记录距离,不发送通知
在完成第 4 课主体内容后,你的gps-trigger函数应用里已经有两个事件触发器:
iot-hub-trigger:负责把 IoT Hub 收到的 GPS 遥测数据写入 Blob 存储;geofence-trigger:通过独立的geofence消费组读取同一批事件,调用 Azure Maps Geofence API 判断坐标与地理围栏的距离,并仅把结果写入日志。
仓库中现成的实现位于 geofence-trigger/init.py:
def main(events: List[func.EventHubEvent]): maps_key = os.environ['MAPS_KEY'] geofence_udid = os.environ['GEOFENCE_UDID'] for event in events: event_body = json.loads(event.get_body().decode('utf-8')) lat = event_body['gps']['lat'] lon = event_body['gps']['lon'] url = 'https://atlas.microsoft.com/spatial/geofence/json' params = { 'api-version': 1.0, 'deviceId': 'gps-sensor', 'subscription-key': maps_key, 'udid' : geofence_udid, 'lat' : lat, 'lon' : lon } response = requests.get(url, params=params) response_body = json.loads(response.text) distance = response_body['geometries'][0]['distance'] if distance == 999: logging.info('Point is outside geofence') elif distance > 0: logging.info(f'Point is just outside geofence by a distance of {distance}m') elif distance == -999: logging.info(f'Point is inside geofence') else: logging.info(f'Point is just inside geofence by a distance of {distance}m')可以看到,函数结尾只有logging.info。本作业的交付目标,就是把这段"仅记录"的代码升级为"会通知"的代码:当坐标进入或离开围栏时,通过短信或邮件把结果推送出去。
配套的触发绑定配置位于 geofence-trigger/function.json,它通过eventHubTrigger接入geofence消费组:
{ "scriptFile": "__init__.py", "bindings": [ { "type": "eventHubTrigger", "name": "events", "direction": "in", "eventHubName": "samples-workitems", "connection": "IOT_HUB_CONNECTION_STRING", "cardinality": "many", "consumerGroup": "geofence", "dataType": "binary" } ] }理解距离语义:通知条件从何而来
设计通知逻辑之前,必须先理解 Azure Maps Geofence API 返回的distance字段的四种取值(详见 README.md 与仓库图片 search-buffer-and-distance.png):
| 取值 | 含义 |
|---|---|
999 | 点位于围栏外,且超出搜索缓冲区(默认 50m) |
> 0(小于缓冲区) | 点位于围栏外,距离围栏边缘若干米 |
-999 | 点位于围栏内,且深入围栏超过搜索缓冲区 |
< 0(大于 -缓冲区) | 点位于围栏内,距离围栏边缘若干米 |
简言之:负数代表"在围栏内",正数代表"在围栏外",绝对值小于缓冲区时给出精确距离(米),否则以 999/-999 表示"远在缓冲区之外"。searchBuffer默认 50m,可通过参数在 0~500m 范围内调整,用于抵消 GPS 本身的定位误差。
这一语义正是"进入/离开判断"的直接依据:distance < 0视为进入(inside),distance >= 0视为离开(outside)。
Azure Functions 绑定机制:无需写 HTTP 客户端代码
Azure Functions 的**绑定(Bindings)**机制允许函数以声明方式接入外部服务:触发器(Trigger)负责函数何时执行,输入/输出绑定(Input/Output Bindings)负责数据如何进出。你只需要在function.json中声明绑定,运行时就会自动完成与服务端的连接、鉴权和序列化,函数体内部只需读写绑定参数即可。
Twilio 与 SendGrid 都是 Azure Functions 官方支持的第三方绑定:
- Twilio SMS 绑定(
twilioSms输出绑定):向指定手机号发送文本短信; - SendGrid 绑定(
sendGrid输出绑定):发送 HTML/纯文本邮件。
提示:本作业原文明确要求先阅读 Microsoft 官方文档《Twilio binding for Azure Functions》与《Azure Functions SendGrid bindings》(以 Python 标签页为准),再动手配置,以保证绑定字段与当前运行时版本匹配。下文给出的是与 Azure Functions Python 运行时一致的绑定结构与代码写法。
前置准备:注册与获取凭据
在修改任何代码之前,需要准备两项服务的凭据:
- Twilio(短信方案)
- 到 Twilio 官网注册免费账号;
- 在控制台获取Account SID与Auth Token;
- 开通一个带号码的试用号码(Trial Number),该号码将作为短信发送方
from; - 验证你的接收手机号(试用账号只能向已验证号码发送)。
- SendGrid(邮件方案)
- 到 SendGrid 官网注册账号;
- 生成API Key(权限至少包含 Mail Send);
- 在 Azure 中创建一个 SendGrid 资源,或直接使用 SendGrid 站点生成的 API Key。
方案一:Twilio SMS 输出绑定
1. 修改 function.json,添加 twilioSms 输出绑定
在 geofence-trigger/function.json 的bindings数组中追加一个输出绑定:
{ "type": "twilioSms", "name": "message", "accountSidSetting": "TwilioAccountSid", "authTokenSetting": "TwilioAuthToken", "from": "+15551234567", "direction": "out" }字段说明:
| 字段 | 说明 |
|---|---|
type | 固定为twilioSms,声明这是一个 Twilio 短信输出绑定 |
name | 绑定在 Python 函数中对应的参数名,这里为message |
accountSidSetting | 存放 Account SID 的应用设置键名,实际值写在local.settings.json/ Application Settings 中 |
authTokenSetting | 存放 Auth Token 的应用设置键名 |
from | 发送方号码,即 Twilio 分配的号码,格式+1xxxxxxxxxx |
direction | 固定为out,表示输出方向 |
2. 在 local.settings.json 中添加凭据
仓库中的 local.settings.json 已预留了MAPS_KEY与GEOFENCE_UDID两个键位。在此基础上补充 Twilio 的 SID 与 Token(键名必须与accountSidSetting、authTokenSetting指向的键名完全一致):
{ "IsEncrypted": false, "Values": { "FUNCTIONS_WORKER_RUNTIME": "python", "AzureWebJobsStorage": "UseDevelopmentStorage=true", "IOT_HUB_CONNECTION_STRING": "<connection string>", "STORAGE_CONNECTION_STRING": "<connection string>", "MAPS_KEY": "<subscription key>", "GEOFENCE_UDID" : "<UDID>", "TwilioAccountSid": "<your account sid>", "TwilioAuthToken": "<your auth token>" } }3. 修改init.py,发送短信
修改main函数签名以接收输出绑定参数,并在"进入围栏"时发送短信:
from typing import List import logging import azure.functions as func import json import os import requests def main(events: List[func.EventHubEvent], message: func.Out[str]): maps_key = os.environ['MAPS_KEY'] geofence_udid = os.environ['GEOFENCE_UDID'] for event in events: event_body = json.loads(event.get_body().decode('utf-8')) lat = event_body['gps']['lat'] lon = event_body['gps']['lon'] url = 'https://atlas.microsoft.com/spatial/geofence/json' params = { 'api-version': 1.0, 'deviceId': 'gps-sensor', 'subscription-key': maps_key, 'udid' : geofence_udid, 'lat' : lat, 'lon' : lon } response = requests.get(url, params=params) response_body = json.loads(response.text) distance = response_body['geometries'][0]['distance'] # 进入围栏(distance 为负)时发送一次短信通知 if distance < 0: message.set( f"Your GPS sensor has entered the geofence! " f"Location: {lat},{lon}" ) logging.info('Sending SMS: point is inside geofence')关键点:
func.Out[str]:Python 运行时把输出绑定作为参数注入,调用message.set(...)即触发发送;- 通知内容:
set()接收的字符串会作为短信正文发送; - 只发一次:本作业要求"进入或离开——但不能两者都发"。上面示例只处理
distance < 0(进入)分支,离开分支保持原日志输出。若想改为"只在离开时通知",则只保留distance >= 0分支即可。
4. 实现"进入/离开二选一"的注意点
Azure Functions 触发器是有状态重放机制:IoT Hub 允许从消费组重放未处理事件(参见 README 中关于消费组的说明)。如果函数在"进入"与"离开"两个分支都调用message.set(),那么卡车在地理围栏边界反复横穿时,就会产生大量冗余短信。正确做法是:
- 只保留一个方向的通知分支(作业评分也以此为准:成功做到"只在进入或只在离开时收到通知"为"优秀");
- 更严格的工程做法是记录上一次状态(例如写入 Blob 或使用持久化状态),仅在状态发生翻转时发送通知——这属于生产级增强,作业不强制要求。
方案二:SendGrid 邮件输出绑定
如果选择邮件通知,则在function.json中添加sendGrid输出绑定:
{ "type": "sendGrid", "name": "message", "apiKey": "SendGridApiKey", "from": "sender@example.com", "to": "recipient@example.com", "subject": "Geofence alert", "direction": "out" }字段说明:
| 字段 | 说明 |
|---|---|
type | 固定为sendGrid |
name | Python 函数中的绑定参数名 |
apiKey | 存放 SendGrid API Key 的应用设置键名 |
from | 发件人邮箱 |
to | 收件人邮箱 |
subject | 邮件主题 |
direction | 固定为out |
在local.settings.json的Values中补充:
"SendGridApiKey": "<your sendgrid api key>"Python 代码中,除使用func.Out[str]并message.set(...)发送纯文本外,也可以set()一个字典来自定义收件人、主题与正文:
message.set({ "personalizations": [{ "to": [{"email": "recipient@example.com"}] }], "subject": "Geofence alert", "content": [{ "type": "text/plain", "value": f"Your GPS sensor has entered the geofence! Location: {lat},{lon}" }] })两种方案二选一即可满足作业要求,不必同时实现。
本地运行与云端部署
本地验证:启动 Functions 运行时,让虚拟 IoT 设备或真实 GPS 传感器发送坐标。确认:
- 当坐标在围栏内且满足通知条件时,收到短信/邮件;
- 当坐标在围栏外时,不会收到通知(或反之,取决于你选择的方向);
- 日志中能看到
geofence-trigger处理事件的输出。
避免重复数据:本地运行时若云端 Functions 仍在运行,两个实例会通过同一消费组重复消费事件。可先停掉云端函数应用,完成验证后再启动:
az functionapp stop --resource-group gps-sensor --name <functions_app_name> az functionapp start --resource-group gps-sensor --name <functions_app_name>部署云端:将修改后的代码部署到 Azure Functions 应用,同时务必同步上传新增的 Application Settings(
TwilioAccountSid、TwilioAuthToken或SendGridApiKey)。本地local.settings.json中的值不会被自动部署到云端,这一点最容易遗漏。
验收标准(Rubric)
作业原文给出的评分表即本文实现的验收清单:
| 标准 | 优秀 | 合格 | 待改进 |
|---|---|---|---|
| 配置 Functions 绑定并收到邮件或短信 | 成功配置绑定,并仅在坐标进入或离开围栏时收到通知(二选一) | 能配置绑定,但无法发送邮件/SMS,或进入与离开两种状态都触发了通知 | 未能配置绑定,也未收到任何邮件/SMS |
对照此表自查时,重点核对两点:绑定是否真正生效(而不是仅仅写好配置但没发出去),以及通知是否只在单一方向触发。
小结与延伸
本作业的核心收获有三层:
- 绑定声明式接入:通过
function.json中的twilioSms/sendGrid输出绑定,无代码接入第三方通信服务,凭据统一托管在应用设置中; - 状态判断与去重:基于 Azure Maps Geofence API 的
distance正负语义(仓库中 geofence-trigger/init.py 已演示四种取值的分支处理),实现"进入或离开二选一"的通知策略; - 本地与云端的配置一致性:新增绑定键名必须同时出现在本地
local.settings.json与云端 Application Settings 中。
想继续深入,可以扩展实现"多地理围栏"支持:Azure Maps 允许在同一 GeoJSONFeatureCollection中通过多个Feature(各带唯一geometryId)上传多个多边形(参考 README.md 的挑战任务),再让通知逻辑根据geometries数组找出最近或命中的那个围栏进行推送。完整参考代码位于仓库 3-transport/lessons/4-geofences/code/functions 目录,原始作业说明见 assignment.md。
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考