【連載記事】Azureを使って一人暮らしの父を見守る(3)Logic Appsのタイマーで定期チェック偏
はじめに
一人暮らしの父(80歳Over)を見守るため、Webカメラで動体監視を行い、毎日リビングで過ごしているかを見守るシステムを構築します。
何本かに記事を分けて構築を進めましたが、今回が最終回で以下の構成になりました。
今回の記事では黄色の範囲について説明しています。
本エントリでは、Azure Logic AppsのタイマートリガーとLINEとの連携にスポットを当てています。
動体検知イベントを受けた後のLogic AppsとLINEとの連携については「(2)Logic AppsとLINE連携偏」を参照ください。
WebカメラとAzure Logic Appsとの連携については「(1)WebカメラとLogic Appsの連携偏」を参照ください。
今回やること
前回までの構築で、以下のことはできるようになりました。
- 動体検知時にLINEに毎回通知
- LINEにメッセージを書くと、前回検知日時を返信
このままですと、日中動体検知の度にLINEに通知が来てしまい、正直それはそれで面倒です。
ただし、朝一番に父がリビングへ下りてきた際には「あっ、今日もいつもの時間に起きてきたナ」を知りたいため、通知させようと思います。他に、外出予定でもないのに何時間も日中に動体検知がされていない場合も心配なので通知させようと思います。
なので、以下の機能を追加します。
- 朝イチの動体検知時のみLINEに通知
- 定期的に前回動体検知時間を見て、一定時間経っているときにもLINEに通知
朝イチのみLINEへ通知
日付が変わった場合の初回のみという判定は、前回までに作成したLogic Appsに、動体検知イベントのLINEへ通知前に一つ判定ロジックを追加しました。
条件の中身は以下のとおりですが、Azure Table Storageから取ってきた前回の動体検知日時と現在日時それぞれの「曜日」を0~6の値で取得して、「異なったら」日が変わった(朝イチ)と判定しています。
dayOfWeek(items('For_each_4')?['datetime'])
dayOfWeek(body('changetimezone'))
一週間まるまる動体検知イベントが無かったら誤動作すると思いますが、それは運用でカバーしますw
この条件を入れるだけで、一日になんどもLINE通知は来ることがなくなりました。
定期チェック
新規に同じAzureリソースグループ内にLogic Appsを作成します。
トリガーは繰り返しで毎時1回起動するようにしました。
現在時刻を日本時間に変換するときに、2つ使っています。片方は年月日時分秒取っていますが、もう片方は時間のみ取得しています。
この後の条件分岐で、現在「時」が6時~20時の間だけ動作するようにしました。夜中や早朝にLINEで通知されてもしんどいので。
その後は前回の記事にあるように、Azure Table Storageから前回動体検知日時を取得して、経過時間が120分以上だったときにLINEで通知を送るようにしました。
動作確認
文面はこれから変更はすると思いますが、とりあえず目的は達成できたかんじです。
朝イチの動体検知通知と、LINEメッセージ送信後の返信
定期チェックで一定時間以上動体検知していない場合の通知メッセージ(画面は開発中のため5分以上で通知されています)
おわりに
とりあえず完成させることができましたが、Logic Appsを本格的にやっていなかったため、関数の指定方法など色々苦労もありました。
個人的にはFunctionsでPython使ったほうが早く書けた気もしますが、ノーコードで(私にとっては)実用的なシステムが手軽に作れたと思います。
付録:Logic Appsのコード
このまままるっと再利用できるとは思いませんが、今回構築したLogic Appsのコードを以下に示します。
アクションの名前とかデフォルトのままだったり、気分で変更していたりとなかなか汚いコードですが、参考になればと。
[イベント取得時]
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"For_each": {
"actions": {
"条件": {
"actions": {
"For_each_4": {
"actions": {
"条件_2": {
"actions": {
"For_each_3": {
"actions": {
"HTTP_2": {
"inputs": {
"body": {
"messages": [
{
"text": "本日初めて動体検知しました。\n@{body('changetimezone')}",
"type": "text"
}
]
},
"headers": {
"Authorization": "Bearer ********=",
"Content-Type": "application/json"
},
"method": "POST",
"uri": "https://api.line.me/v2/bot/message/broadcast"
},
"runAfter": {},
"type": "Http"
}
},
"foreach": "@body('JSON_の解析_2')?['value']",
"runAfter": {},
"type": "Foreach"
}
},
"expression": {
"and": [
{
"not": {
"equals": [
"@dayOfWeek(items('For_each_4')?['datetime'])",
"@dayOfWeek(body('changetimezone'))"
]
}
}
]
},
"runAfter": {},
"type": "If"
}
},
"foreach": "@body('JSON_の解析_2')?['value']",
"runAfter": {
"最新のモーション検出日時を保存": [
"Succeeded"
]
},
"type": "Foreach"
},
"JSON_の解析_2": {
"inputs": {
"content": "@body('最終検出日時を取得2')",
"schema": {
"properties": {
"odata.metadata": {
"type": "string"
},
"value": {
"items": {
"properties": {
"datetime": {
"type": "string"
},
"odata.etag": {
"type": "string"
}
},
"required": [
"odata.etag",
"datetime"
],
"type": "object"
},
"type": "array"
}
},
"type": "object"
}
},
"runAfter": {
"最終検出日時を取得2": [
"Succeeded"
]
},
"type": "ParseJson"
},
"最新のモーション検出日時を保存": {
"inputs": {
"body": {
"datetime": "@{body('changetimezone')}"
},
"host": {
"connection": {
"name": "@parameters('$connections')['azuretables']['connectionId']"
}
},
"method": "put",
"path": "/Tables/@{encodeURIComponent('motion')}/entities(PartitionKey='@{encodeURIComponent('motion')}',RowKey='@{encodeURIComponent('lastmotion')}')"
},
"runAfter": {
"JSON_の解析_2": [
"Succeeded"
]
},
"type": "ApiConnection"
},
"最終検出日時を取得2": {
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['azuretables']['connectionId']"
}
},
"method": "get",
"path": "/Tables/@{encodeURIComponent('motion')}/entities",
"queries": {
"$filter": "RowKey eq 'lastmotion'",
"$select": "datetime"
}
},
"runAfter": {},
"type": "ApiConnection"
}
},
"else": {
"actions": {
"For_each_2": {
"actions": {
"変数の設定": {
"inputs": {
"name": "経過時間",
"value": "@div(div(sub(ticks(actionBody('changetimezone')),ticks(items('For_each_2')?['datetime'])),10000000),60)"
},
"runAfter": {},
"type": "SetVariable"
}
},
"foreach": "@body('JSON_の解析')?['value']",
"runAfter": {
"JSON_の解析": [
"Succeeded"
]
},
"type": "Foreach"
},
"For_each_5": {
"actions": {
"HTTP": {
"inputs": {
"body": {
"messages": [
{
"text": "前回は@{items('For_each_5')?['datetime']}に動体検知しています。\n(@{variables('経過時間')}分経過)",
"type": "text"
}
],
"to": "@{items('For_each')?['source']?['userId']}"
},
"headers": {
"Authorization": "Bearer ********=",
"Content-Type": "application/json"
},
"method": "POST",
"uri": "https://api.line.me/v2/bot/message/push"
},
"runAfter": {},
"type": "Http"
}
},
"foreach": "@body('JSON_の解析')?['value']",
"runAfter": {
"For_each_2": [
"Succeeded"
]
},
"type": "Foreach"
},
"JSON_の解析": {
"inputs": {
"content": "@body('最終検出日時を取得')",
"schema": {
"properties": {
"odata.metadata": {
"type": "string"
},
"value": {
"items": {
"properties": {
"datetime": {
"type": "string"
},
"odata.etag": {
"type": "string"
}
},
"required": [
"odata.etag",
"datetime"
],
"type": "object"
},
"type": "array"
}
},
"type": "object"
}
},
"runAfter": {
"最終検出日時を取得": [
"Succeeded"
]
},
"type": "ParseJson"
},
"最終検出日時を取得": {
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['azuretables']['connectionId']"
}
},
"method": "get",
"path": "/Tables/@{encodeURIComponent('motion')}/entities",
"queries": {
"$filter": "RowKey eq 'lastmotion'",
"$select": "datetime"
}
},
"runAfter": {},
"type": "ApiConnection"
}
}
},
"expression": {
"and": [
{
"equals": [
"@items('For_each')?['type']",
"motion"
]
}
]
},
"runAfter": {},
"type": "If"
}
},
"foreach": "@triggerBody()?['events']",
"runAfter": {
"変数を初期化する": [
"Succeeded"
]
},
"type": "Foreach"
},
"changetimezone": {
"inputs": {
"baseTime": "@body('現在の時刻')",
"destinationTimeZone": "Tokyo Standard Time",
"formatString": "u",
"sourceTimeZone": "UTC"
},
"kind": "ConvertTimeZone",
"runAfter": {
"現在の時刻": [
"Succeeded"
]
},
"type": "Expression"
},
"変数を初期化する": {
"inputs": {
"variables": [
{
"name": "経過時間",
"type": "integer"
}
]
},
"runAfter": {
"changetimezone": [
"Succeeded"
]
},
"type": "InitializeVariable"
},
"現在の時刻": {
"inputs": {},
"kind": "CurrentTime",
"runAfter": {},
"type": "Expression"
}
},
"contentVersion": "1.0.0.0",
"outputs": {},
"parameters": {
"$connections": {
"defaultValue": {},
"type": "Object"
}
},
"triggers": {
"manual": {
"inputs": {
"schema": {
"properties": {
"events": {
"items": {
"properties": {
"message": {
"properties": {
"id": {
"type": "string"
},
"text": {
"type": "string"
},
"type": {
"type": "string"
}
},
"type": "object"
},
"replyToken": {
"type": "string"
},
"source": {
"properties": {
"type": {
"type": "string"
},
"userId": {
"type": "string"
}
},
"type": "object"
},
"timestamp": {
"type": "integer"
},
"type": {
"type": "string"
}
},
"required": [
"replyToken",
"type",
"timestamp",
"source",
"message"
],
"type": "object"
},
"type": "array"
}
},
"type": "object"
}
},
"kind": "Http",
"type": "Request"
}
}
},
"parameters": {
"$connections": {
"value": {
"azuretables": {
"connectionId": "/subscriptions/*+*******/resourceGroups/motion-test-rg/providers/Microsoft.Web/connections/azuretables-1",
"connectionName": "azuretables-1",
"id": "/subscriptions/********/providers/Microsoft.Web/locations/japaneast/managedApis/azuretables"
}
}
}
}
}
[周期実行]
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"changetimezone": {
"inputs": {
"baseTime": "@body('現在の時刻')",
"destinationTimeZone": "Tokyo Standard Time",
"formatString": "u",
"sourceTimeZone": "UTC"
},
"kind": "ConvertTimeZone",
"runAfter": {
"現在の時刻": [
"Succeeded"
]
},
"type": "Expression"
},
"変数を初期化する": {
"inputs": {
"variables": [
{
"name": "経過時間",
"type": "integer"
}
]
},
"runAfter": {},
"type": "InitializeVariable"
},
"時のみ取得": {
"inputs": {
"baseTime": "@body('現在の時刻')",
"destinationTimeZone": "Tokyo Standard Time",
"formatString": "HH",
"sourceTimeZone": "UTC"
},
"kind": "ConvertTimeZone",
"runAfter": {
"changetimezone": [
"Succeeded"
]
},
"type": "Expression"
},
"条件": {
"actions": {
"For_each": {
"actions": {
"変数の設定": {
"inputs": {
"name": "経過時間",
"value": "@div(div(sub(ticks(actionBody('changetimezone')),ticks(items('For_each')?['datetime'])),10000000),60)"
},
"runAfter": {},
"type": "SetVariable"
},
"条件_2": {
"actions": {
"HTTP": {
"inputs": {
"body": {
"messages": [
{
"text": "前回動体検知してから @{variables('経過時間')}分経過しています。",
"type": "text"
}
]
},
"headers": {
"Authorization": "Bearer ********=",
"Content-Type": "application/json"
},
"method": "POST",
"uri": "https://api.line.me/v2/bot/message/broadcast"
},
"runAfter": {},
"type": "Http"
}
},
"expression": {
"and": [
{
"greater": [
"@variables('経過時間')",
120
]
}
]
},
"runAfter": {
"変数の設定": [
"Succeeded"
]
},
"type": "If"
}
},
"foreach": "@body('JSON_の解析')?['value']",
"runAfter": {
"JSON_の解析": [
"Succeeded"
]
},
"type": "Foreach"
},
"JSON_の解析": {
"inputs": {
"content": "@body('エンティティの取得')",
"schema": {
"properties": {
"odata.metadata": {
"type": "string"
},
"value": {
"items": {
"properties": {
"datetime": {
"type": "string"
},
"odata.etag": {
"type": "string"
}
},
"required": [
"odata.etag",
"datetime"
],
"type": "object"
},
"type": "array"
}
},
"type": "object"
}
},
"runAfter": {
"エンティティの取得": [
"Succeeded"
]
},
"type": "ParseJson"
},
"エンティティの取得": {
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['azuretables']['connectionId']"
}
},
"method": "get",
"path": "/Tables/@{encodeURIComponent('motion')}/entities",
"queries": {
"$filter": "RowKey eq 'lastmotion'",
"$select": "datetime"
}
},
"runAfter": {},
"type": "ApiConnection"
}
},
"expression": {
"and": [
{
"greater": [
"@int(body('時のみ取得'))",
5
]
},
{
"lessOrEquals": [
"@int(body('時のみ取得'))",
20
]
}
]
},
"runAfter": {
"時のみ取得": [
"Succeeded"
]
},
"type": "If"
},
"現在の時刻": {
"inputs": {},
"kind": "CurrentTime",
"runAfter": {
"変数を初期化する": [
"Succeeded"
]
},
"type": "Expression"
}
},
"contentVersion": "1.0.0.0",
"outputs": {},
"parameters": {
"$connections": {
"defaultValue": {},
"type": "Object"
}
},
"triggers": {
"繰り返し": {
"recurrence": {
"frequency": "Hour",
"interval": 1
},
"type": "Recurrence"
}
}
},
"parameters": {
"$connections": {
"value": {
"azuretables": {
"connectionId": "/subscriptions********/resourceGroups/motion-test-rg/providers/Microsoft.Web/connections/azuretables-1",
"connectionName": "azuretables-1",
"id": "/subscriptions/********/providers/Microsoft.Web/locations/japaneast/managedApis/azuretables"
}
}
}
}
}