【連載記事】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"
                }
            }
        }
    }
}