やる気駆動型エンジニアの備忘録

WPF(XAML+C#)の話題を中心に.NET/Android/CI やたまに趣味に関するブログです

Azure DevOps拡張機能で自作アプリを呼び出せるかどうか試してみた

Azure DevOps 拡張機能PowerShell を使用して開発することができますが、ちょっと PowerShell 力が足りないときに .NET 使いたいなーと思うことがありました。 しかし、リファレンス上では .NET アプリを使う方法については記載されていません。なので、未対応なのかと思いましたが「ひょっとして、PowerShell から自作した .NET アプリを呼び出すことができるのではないか?」と思い、少し試すことにしました。

結論

結論から言うと「制限はあるものの Azure DevOps の拡張機能で自作アプリ(アセンブリ)を呼び出すことは可能」でした。 制限については後ほど説明します。 また、今回は Azure Pipelines タスクの拡張機能として作成しています。 もしかすると Web ページを表示するようなタイプの拡張機能だと、この方法では実現できない可能性はあります(未検証)。

開発環境

自作アプリは .NET Core 3.1 で作成したコンソールアプリです。 このコンソールアプリを PowerShell で呼び出すような想定で作りました。

フォルダ構成

今回のフォルダ構成は細かいファイル配置は省いて次のようになっています。

/CallDotNetExample
│  .gitignore
│  package-lock.json
│  package.json
│  vss-extension.json
│  
├─console
│  │  console.csproj
│  └  Program.cs
│                      
└─src
    │  Startup.ps1
    │  task.json
    │  
    ├─dotnet
    │      console.exe
    │      console.pdb
    │      
    ├─node_modules
    │      
    │          
    └─ps_modules
        └─VstsTaskSdk

重要なのはconsole/Program.cs, src/Starup.ps1, dotnetフォルダです。

console/Program.csは、自作したコンソールアプリです。src/Startup.ps1から呼び出されることを想定しています。 src/Startup.ps1は、自作したコンソールアプリを呼び出します。この PowerShell は、Azure DevOps 拡張機能が実行された際に最初に呼ばれるエントリポイントです。 dotnetフォルダは、ビルドした自作コンソールアプリの出力先フォルダです。src/Startup.ps1は、このフォルダ配下にあるアセンブリを実行するようにしています。

実装したコード

それぞれのコードは以下のようになっています。

using System;

namespace console
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("Hello World from dotnet console app!");
                System.IO.File.WriteAllText(System.IO.Path.Combine(Environment.CurrentDirectory, "example.txt"), "Hello World from dotnet console app!");
            }
            catch (System.Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }
    }
}
[CmdletBinding()]
param()

try {
    $samplestring = Get-VstsInput -Name "samplestring"

    Write-Host "Hello world from Starup.ps1."

    Start-Process -FilePath "${PSScriptRoot}\dotnet\console.exe" -Wait -NoNewWindow
    
} finally {
    Trace-VstsLeavingInvocation $MyInvocation
}

Start-Process-Waitにしているのは、自作コンソールアプリを呼び出したあとに処理を待つため。 そして、-NoNewWindowにしないとコンソール出力の結果が Azure Pipelines のログに出力されないためです。

実行結果

これを実行すると次のようになりました。 期待したとおり、PowerShellHello worldC#Hello world が出力されています。

f:id:iyemon018:20210228225336p:plain

制限

今回調査した限りだと次のような制限事項があります。

アップロードサイズ

Azure DevOps 拡張機能ではパッケージしたファイルのサイズに制限があります。 試しに .NET アプリをシングルファイルにしてアップロードしようとしたところ以下のようなメッセージが表示されました。

Upload Error

The extension package size 'XXX bytes' exceeds the maximum package size '26214400 bytes'

なので、アップロード可能なサイズは26.2 MBまで。 Hello world だけのアプリをシングルファイルで生成しても 30 MB 程度はあったので、.NET のシングルファイル生成によるパッケージは不可能です。

また、アップロードファイルのサイズについて、削減する方法をまとめてくれているブログがありました。といってもこっちは正規の方法(Node.js + TypeScript)で作られているようなので、本記事とは関係は薄いです。

levelup.gitconnected.com

環境依存

上記にも関連しますが、シングルファイル生成によるパッケージが不可能ということは、拡張機能で呼び出す .NET アプリが環境に依存してしまうということです。 Azure Pipelines の特性上、Microsoftホスティングしている環境上で実行される場合は .NETフレームワークバージョンに制限があります。というより LTS でない古いバージョンは使えません(例えば、現状では .NET Core 3.0 などは Microsoftホスティングしている環境にはインストールされていない)。 この対策としてシングルファイル生成によるパッケージングができればどの環境でも動作できるのでは?と考えましたが、結果は上記のとおりでした。

他の対策は「常に .NET アプリ側を常に最新バージョンに対応させる」か、「プライベートホストを使って環境を固定する」かでしょうか。 どちらが良いかはプロダクトによるでしょうが、いずれにしろコストは掛かります。 このコストと C# / .NET が使えるメリットは考慮しておく必要があります。

検証していないこと

管理者権限で実行する

多分無理ですが、管理者権限でアプリを実行することは検証していません。 うろ覚えですが、たしか Azure DevOps 拡張機能自体、管理者権限では実行されなかったと記憶しているのでアプリ側も同じく不可能かと。

といっても管理者権限で実行したいようなケースは無いとは思いますが。

どこまでの機能が使えるか

少なくとも拡張機能の実行フォルダにファイルを出力することはできます。 しかし、それ以外の機能、例えば HttpClient で Web API を呼び出すなどのようなことが可能かどうかまでは検証できていません。 OSS ライブラリを使う場合、ものによっては Azure DevOps 側で制限がかけられる機能を使っている可能性もゼロではありません。 アプリによってはここがボトルネックになることが多いでしょうから予め検証は必要です。

まとめ

実現可能とはいえ、普通なら PowerShell でいいじゃんと多くの人が思うでしょうから、あまりにもニッチな需要かもしれません。 制限事項もあるので使い所も難しいケースもあるでしょう。 それでも使い慣れた言語が使えるというのはそれ単体では絶大なメリットだと思うので、どうしても…という場合には選択肢に追加しても良さそうです。

Azure Functionsで自作したカスタムコネクタをPower Automateで使おうとしたらハマった事象と原因と回避策について

随分とタイトルの示す範囲が大きくなりましたが、そういうことです。

やろうとしていたことは「Azure Functionsで作成した関数アプリをPower Automateで使う」という至ってシンプルなものでした。 しかし、いざ実践してみると期待した通りに動作しなかったため、しばらく悩み続けてなんとか解消した話です。

事象

Azure Functionsで作成した関数アプリをカスタムコネクタを使って呼び出すようにしました。 新規にフローを作成してカスタムコネクタのアクションを実行すると失敗し、次のようなメッセージが表示されました。

ResponseSwaggerSchemaValidationFailure. API 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' が、種類 'OpenApiConnection' のワークフロー操作 '<API名>' について無効な応答を返しました。エラーの詳細: 'API 操作 '<API名>' では、プロパティ 'body' の種類を 'Object' にする必要がありますが、種類が 'String' です。'

内容的には json の応答を Object から String にしろと言うことのようです。


2020/11/16 追記 (コメント頂きましたので修正)

内容的にはカスタムコネクタでは Object が来る想定だったけど String が来たよ、ということのようです。


ちなみにAzure Functionsでは、作成した関数の応答は json で返していました。

ただし、この現象が発生するのは実際のフローのみで、カスタムコネクタの[テスト]タブで実行した場合や、HTTP アクションで同じ URL 呼び出しを行っても同じ現象は発生しませんでした。

似たような事象が発生していないか Power Automate で探したところ、ドンピシャなスレッドがありました。が、質問した方はカスタムコネクタを修正して対応したようです。(良いのかな…)

Error in Power Automate Flow When Using Custom Connector -requires the property 'body' to be of type 'String' but is of type 'Object'

原因

色々試した結果、原因はAzure Functions側にあることがわかりました。 応答する際に jsonシリアライズしたのは良いのですが、json 文字列を body に設定してしまっていました。

はい、つまり完全に私のしょーもないミスが原因ですね。

具体的には次のようなコードになっていました。

[FunctionName("function")]
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req, ILogger log)
{
    ...省略
    var json = Newtonsoft.Json.JsonConvert.SerializeObject(result, _settings);

    return new OkObjectResult(json);
}

このまま関数を呼び出すと応答結果のContent-Typeは、text/plainになってしまいます。 要はこれをapplication/jsonにすれば良いわけですね。

対策

今回のケースでは応答は json で返し、シリアライズする際の書式設定はNewtonsoft.Json.JsonSerializerSettingsで設定していたため、Microsoft.AspNetCore.Mvc.JsonResultで応答する手法を取りました。 この方法にたどり着くまでに twitter 上でしばてぃさん(@shibatea365)に何度もアドバイスいただきました。感謝の極みです😂

さて、変更後のコードはこのようになります。

[FunctionName("function")]
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req, ILogger log)
{
    ...省略
    return new JsonResult(response, _settings);
}

なお、JsonResultを使う場合はMicrosoft.AspNetCore.Mvc.NewtonsoftJsonパッケージを追加する必要があります。 また、csprojファイルに<FrameworkReference Include="Microsoft.AspNetCore.App" />を追加しないと以下のような事象が発生するそうなのでご注意を。

MethodNotFound when trying to call JsonResult ctor with JsonSerializerSettings #1907

これを実行するとContent-Typeapplication/jsonになり、Power Autoamte のフローも成功します。

最後に

本当にしょうもなことが原因で何日もハマってしまいました… Azure Functionsについてはまだまだ初学者であったとは言え、今回のような事象はユニットテストでレスポンス ヘッダーまでテストして入れば検知できたはずです(やってませんでした!)。 ちょっと手を抜いた結果がこれです。 手を抜くの絶対ダメ!!!

(生活)スマートホーム デバイスで生活が快適になりました

先日の Amazon プライムデーセールで Alexa とスマートホームバイスを購入したのですが、あまりに便利だったので紹介します。

購入したもの

EZCON は Amazon Alexa とセットで購入しました。 また、余談ですが FREDI スマート電源タップ は2020年11月1日まで50%OFFクーポンがあります。クーポンコード「DEN102013」(早期終了するかも)。

私の家にはすでに Google Home が2台あったのですが、新たに Alexa が追加されました。

EZCON は Alexa から操作できますが、Google Home には対応していないため操作できません。 Gosund スマートプラグ Wi-Fiスマートコンセント と FREDI スマート電源タップ は Alexa と Google Home どちらでも操作できます(というよりもアプリの Smart Life が対応している)。

EZCONの設定方法

事前に EZCON アプリをインストールしておきましょう。

EZCON

EZCON

  • LinkJapan Inc.
  • ライフスタイル
  • 無料
apps.apple.com

とても簡単です。が、いちいち文章で説明するよりもこちらの動画を見るほうが良いでしょう。 例えば証明のリモコンを登録するような場合はこの動画のように設定します。

play.google.com


照明の登録方法

一つ注意が必要なのが EZCON が Wi-Fi の 2.4GHz にしか対応していないことです。 5GHz では使えないので事前に確認する必要があります。

スマートプラグ/スマート電源タップの設定方法

こちらは Smart Life アプリから設定します。

Smart Life - Smart Living

Smart Life - Smart Living

  • Yu xiang
  • ライフスタイル
  • 無料
apps.apple.com

play.google.com

接続する機器によって設定方法が若干異なるのですが、付属の取扱説明書を読めばOKです。 また、FREDI スマート電源タップ は1口づつ ON/OFF を切り替えられるため、例えばヒーターやサーキュレータを別々の口に接続しておいて独立して切り替えが可能となっています。 これがめちゃくちゃ便利!

スマートフォンからリモコンとして使う

EZCON

EZCON アプリは設定が完了するとこんな感じになります。 部屋ごとにタブが別れていて登録デバイスを一覧で見ることができます。

f:id:iyemon018:20201025222732j:plain

スマートタブでは音声認識やタイマーによる自動設定も可能になっています。

Smart Life

Smart Life も EZCON と同じようにデバイスの一覧が表示されます。 EZCON との違いとして"ワンクリック実行"というものがあります。 これは特定のデバイス制御を一括で実行できる機能で、一度設定すればプラグに接続したヒーターを付けたり、すべてのプラグを OFF にしたりといったことが可能です。

f:id:iyemon018:20201025195534j:plain f:id:iyemon018:20201025195532j:plain

こちらはスマート プラグの画面です。シンプルに ON/OFF のみです。

f:id:iyemon018:20201025195425j:plain

このスマート プラグを使うまで知らなかったのですが、使用電力量を計測してくれる機能もあります。 ↓↓↓こんな感じで現在使用している電力量と合計量を見ることができます。

これを利用したら家中のコンセントにスマートプラグを接続して使用電力量の可視化もできそうですね。

f:id:iyemon018:20201025195416j:plain

こちらはスマート電源タップの画面です。 電源タップはすべての電源を一括で ON/OFF するボタンとそれぞれの口ごとに ON/OFF 切り替えることができます。 ボタン名はデフォルトでは"スイッチn"ですが、これはタップして別名に変更することができます。 私の場合は下2つをサーキュレーターとヒータに割り当てています。

f:id:iyemon018:20201025195517j:plain

1点残念部分として電源タップ側には電力使用量を計測する仕組みはありません。 スマートプラグが¥1,600ほど、電源タップが¥4,000ほどと考えるとあってほしい機能ですが…

スマートスピーカーと連携して使う

Google Home と Alexa でそれぞれアプリで連携できるようにしました。

Google Homeアプリ

Google Home アプリはスマートスピーカーChrome Cat もまとめて確認できます。 数が多くなってくるとごちゃごちゃしそうなので、アイコンや色を変えたりとかできるといいですがそうはなっていません。

f:id:iyemon018:20201025195525j:plain f:id:iyemon018:20201025195520j:plain

ルーティンで特定のフレーズからデバイスを操作できるようにしておけば、帰宅時や外出時にまとめてデバイスを制御することができます。

Alexa

Alexa は最初に部屋(グループ)を選択するため Google Home と違ってシンプルです。 ただし、1タップ分の操作が増えるのと、この時点では何のデバイスがどこの部屋にあるのかは分かりません。新しくデバイスを追加したときとか、家族で共有するときとかは戸惑うかも?

f:id:iyemon018:20201025224215j:plain f:id:iyemon018:20201025224221j:plainf:id:iyemon018:20201025195529j:plain

Alexa も Google Home と同じく、特定フレーズからデバイス制御が可能です。 Google Home との違いは Alexa だと EZCON も操作できる、というところです。

スマートスピーカーが2種類あるので、両方に同じフレーズで同じデバイスを操作できるようにしました。 個人的には Google Home の方が好きなのですが、ここは妥協しています…。

生活シーンで使う

我が家では今年娘が誕生して毎日育児に追われているのですが、このスマートホームバイスが大変役に立ちます。

例えば外出するときはどうしてもやることが多くてバタバタしますが、「Alexa、いってきます」というだけで家中の各種家電が一斉に OFF になります。 エアコンや照明など、リモコンが複数あるとどうしても置き場所を探しますが、音声による操作なら抱っこひもを使った場合でもベビーカーの場合でも大変楽です。 他にも、これからの季節はヒーターやホットカーペットを使う家庭もあるでしょうし、そういったところにスマートホームバイスがあれば、まとめて切り替えができるので使い勝手が良さそうですね。

あとは帰宅時に前もって電源を ON にすることで、寒い日でも帰宅直後からエアコンで十分に温まった部屋で過ごすことなどもできます。

そして、個人的に最も使ってよかったところが-子供をあやして寝かしつけたあと、照明を OFF にするのですが、アプリから操作することによってリモコンを探す手間などが省けます。 また、スマホが手元になかったり抱っこしているときなどはスマートスピーカーから操作することもできます。 フレーズを登録しておいて、照明の OFF と同時に安眠できるような音楽を再生するのもいいでしょう。 リビングでくつろいでいるときに子供が起きたりしたらすぐさま駆けつけることになりますが、別の部屋にいてもつけっぱなしにしていたヒーターを OFF にしたりすることもできます。

まだ使ったことは無いのですが、Smart Life だと気温の変化や GPS で特定の場所に移動したことをトリガーとしてデバイスの制御が可能になっていたりもします。 寒くなってきたら暖房、熱くなってきたら冷房を着けるといった具合です。

こういった機能をうまく使えば今以上に生活が楽になりそうですね。

他のスマートホームバイスの選択肢

スマートホームバイスと言ってもその種類はそこそこあります。 パッと見はそれぞれの差異が分かりづらいのですが、特に注意したいのが「デバイスの制御に使用するアプリ」と「連動可能なスマートスピーカー」です。

上記でもすでに紹介していますが、リモコンとプラグで操作するためのアプリは異なります。 私が調べた限りですが、プラグもメーカーによって使えるアプリが異なる場合があります。 例えば Meross というメーカーでは同じく Meross というアプリでしか操作できません。

せっかく家電をまとめて操作できるようになったのにアプリを複数使用するのでは本末転倒になってしまいます。 使うアプリの種類はせいぜい2種類が限度でしょう。 デバイス購入の前に口コミなどから利用可能なアプリを調べておくのが良いでしょう。

次に連動可能なスマートスピーカーですが、上記の EZCON なんかがそうです。 こちらのリモコンは Alexa とのセット販売&タイムセールによる大幅割引だったので、Google Home と連携できなくても妥協できますが、リモコンも決して安くはないです(定価 ¥3,980 くらい)。 EZCON はまだ Nature なんかの高性能リモコンよりも安価ですが、それでも手持ちのスマートスピーカーと連携できなくて困る可能性もあります。

スマートスピーカーとの連携は必要だ!という方はこのあたりも注意しないといけません。


使ってみると想像以上にできることが多くて最初は戸惑いますが、これは一度経験したら手放せなくなるタイプの UX です。 スマートホームバイスは他にも多種多様なものがあるので、自宅に適したものを探してみてください。 (個人的には LED 電球とか気になりますね…これ使って子供と遊べそうですし、ナイトライト代わりにもなりそう。)

(Power Automate)OData フィルタで日時を指定する方法について

先日、Power Automate で OData のフィルタを指定する際に日時を条件に設定する必要がありましたが、その指定方法にハマったのでメモしておきます。

ちなみに OData は Microsoft Graph API の操作を行うためのクエリ言語です。

docs.microsoft.com

環境

やろうとしたことは「SharePoint のイベントリストに登録したデータから特定の期間のデータを取得する」というものです。

例えば以下のようなイベントが登録されているものとします。

f:id:iyemon018:20201010160954p:plain

ここから、2020年10月10日~2020年10月12日までのイベントを取得するイメージです。

まずはフローを実行する

f:id:iyemon018:20201010161328p:plain

イベントを取得する場合は SharePoint の"複数の項目の取得"から取得します。 なお、イベントはリスト名の候補には出てこないのでカスタム入力する必要があります。

問題はこの"フィルター クエリ"の箇所です。 まずは、フィルター クエリを指定せずに実行した結果の JSON を見てみましょう(不要な部分は省略しています)。

{
   "statusCode":200,
   "headers":{
        //省略
   },
   "body":{
      "value":[
         {
            "@odata.etag":"\"1\"",
            "ItemInternalId":"2",
            "ID":2,
            "Title":"テスト10月10日",
            "EventDate":"2020-10-10T00:00:00Z",
            "EndDate":"2020-10-10T23:59:00Z",
            "EventType":0,
            "Modified":"2020-10-10T07:08:30Z",
            "Created":"2020-10-10T07:08:30Z",
            // 省略
         },
         {
            "@odata.etag":"\"1\"",
            "ItemInternalId":"3",
            "ID":3,
            "Title":"Test記念日",
            "EventDate":"2020-10-12T00:00:00Z",
            "EndDate":"2020-10-12T23:59:00Z",
            "EventType":0,
            "Modified":"2020-10-10T07:09:02Z",
            "Created":"2020-10-10T07:09:02Z",
            // 省略
      ]
   }
}

これを見るとEventDateが開始日時、EndDateが終了日時であることが分かります。 なので、今回の要件を満たすにはフィルター クエリには「2020年10月10日 <= 開始日時 <= 2020年10月12日」を指定すれば良いですね。

ちなみになんでフィルター クエリなのか?というとこっちのほうが実行時間が短いからです(多分)。 例えば、フィルター クエリを指定せずに全データを取得してから Apply to each で必要な期間だけ取得することもできますが、データ量が増えてくるとどうしても時間がかかります。 特に Power Automate は。 この時必要になる時間は(正確には検証していませんが)フィルター クエリを指定したほうが短いです。 Apply to each を使う場合、データを削除しない限りは実行時間が短くなることは無いので、この方法を採用しています。

フィルター クエリを設定する

ドキュメントを見ると次のような論理演算子がサポートされています。

  • eq: 等しい
  • ne: 等しくない
  • lt: より小さい
  • gt: より大きい
  • le: 以下
  • ge: 以上
  • and: かつ
  • or: または
  • in: 含まれる
  • not: 否定

上記のフィルター条件を指定する場合、は次のようになります。

EventDate ge 2020-10-10T00:00:00Z and EventDate le 2020-10-12T00:00:00Z

では実行しましょう。

f:id:iyemon018:20201010162751p:plain

はい。失敗しますね。

エラー詳細には次のようなメッセージが出力されると思います。

式 "EventDate ge 2020-10-10T00:00:00Z and EventDate le 2020-10-12T00:00:00Z" が無効です。 Creating query failed.
clientRequestId: <id>
serviceRequestId: <id>

この問題を修正するには次のようにフィルター クエリを修正します。

EventDate ge '2020-10-10T00:00:00Z' and EventDate le '2020-10-12T00:00:00Z'

変わった箇所は、日時を ' (シングルクォーテーション)でくくっているところです。 これを実行するとフローは成功します。

f:id:iyemon018:20201010163139p:plain

スケジュールを指定して実行している場合は以下のように変数に日時を保存しておいてフィルターを指定するのが良いと思います。

formatDateTime(variables('FromDateTime'), 'yyyy-MM-ddT00:00:00Z')


SharePoint などでリストから特定の情報をフィルタすることはよくあると思います。 日時が含まれるケースは少ないかもしれませんが、そういった場合こそハマりがちなので参考にして頂ければ。

(Power Automate)Plannerに登録したタスクの期限が近い場合に通知するフローを作成する

現在、私が所属しているチームではタスク管理ツールにMicrosoft Plannerを使用しています。 単純な親子関係のないタスクであれば簡単に管理できて、チェックリストや期限の設定もでき、進捗状況や割当状態をグラフやカレンダーで可視化することができます。 また、Web会議は Teams を主に使用していることもあり、連携するならこれが今の所ベストな選択肢となっています。 感覚的には Trello と同じように使えるのもポイントです。

しかし、Planner そのものに期限を通知するための仕組みはありません。 そのため、業務で利用する際に「遅延しているタスク」がモリモリ増えていって、このままではタスク管理している意味がない!と思い、Power Automate で毎日通知するようにしました。

今回はその方法についてまとめておきます。

環境

  • Power Automate
  • Planner
  • チャットツール

チャットツールは何でも良いです。Slack や Teams ならコネクタがあるのでそちらを使いましょう。 弊社では Chatwork を使っていたのでカスタムコネクタを作ってメッセージを送るようにしました。

今回は Power Automate がメインで、カスタムコネクタや Planner についての話はしません。

やりたいこと

やりたいことは以下の3つです。

  1. 当日が期限のタスク一覧を通知する
  2. その週が期限のタスク一覧を通知する
  3. 遅延しているタスク一覧を通知する

実際のところ、この3つは通知が必要なタスクかどうかの判断以外はすべて同じなので1つだけ説明して、他は差分箇所のみ紹介します。 そしてこれを実行すると以下のようなメッセージが表示されます。

f:id:iyemon018:20200913165236p:plain

通知するメッセージには以下の情報を記載します。

  • 期限
  • タスクタイトル
  • 割り当てられた担当者の名前

フローを作成する

フローは次のような構成になっています。 全体のざっくりとした流れはスケジュール実行→Plannerからタスク取得→通知用メッセージ初期化→条件に一致するタスクを収集・メッセージの整形→チャットに送信となっています。 詳細は番号にそって説明します。

f:id:iyemon018:20200913165945p:plain

1. スケジュールの設定

説明するまでもないですね。通知したい時間を設定します。 タスクの通知なので、できれば朝一番か業務開始直前とかが良いと思います。

2. タスクを一覧表示します

Planner から該当するグループ・プランのタスクを収集します。 こちらも説明は不要ですね。 取得したいグループ・プランIDを指定するだけです。

3. 変数を初期化する/4. 変数を初期化する 2

今回、変数は2つ用意しました。 1つは期限であるタスク一覧を通知するためのメッセージで、もう1つは実際に通知するメッセージです。 参考までに今回は以下のように設定しました。

③期限であるタスク一覧を通知するためのメッセージ
・名前:task_list
・種類:文字列
・値:(何も設定しない)

④実際に通知するメッセージ
・名前:chatwork_send_message
・種類:文字列
・値:(何も設定しない)

なぜこんな回りくどいやり方をするかと言うと、今日が期限のタスクが0件の場合にどう通知するかを考慮したためです。

今日が期限のタスクが0件の場合、通知された側としては何が表示されていればいいでしょうか? 今回のケースでは「今日が期限のタスクは0件です。」と通知するようにしました。

仮に、「通知しない」という選択をした場合、フローが動いているのかどうか通知を見た側はわかりません。 そうなるとフローが間違っているのか期限になるタスクが無いのか判断できません。 そうなるよりは通知は増えますが、「0件です」と表現したほうがわかりやすいと思います。 (チームによってどう通知するべきかは変わるのでここは調整してください)

この要件を満たしたいので、「3. 変数を初期化する」に期限が今日であるタスク一覧の通知用メッセージを代入し、「4. 変数を初期化する 2」に実際に通知するメッセージを代入します。

5. タスク一覧を収集してメッセージを整形する

ここが一番のメインです。収集したタスクを条件に一致するかどうか判断し、一致する場合はメッセージを作成します。 収集するタスクは複数あるのでループで処理していきます。 更に詳しく見ていきます。

f:id:iyemon018:20200913171214p:plain

ポイントは2つ。「a. 通知すべきタスクかどうかを判断する」の条件と「b. 割当先ユーザーごとにメッセージを整形する」のメッセージ整形部分です。

a.通知すべきタスクかどうかを判断する

ここでは、「今日が期限のタスクかどうか」を判断します。 条件式には次のものを設定しています。

  • <empty(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime'])>次の値に等しい<false>
    Planner では期限dueDateTimeは空欄でも許容されます。空欄の場合はこれ以降の条件式で比較した場合、エラーになるためこの条件を先頭に持ってきています。
  • <値 達成率>次の値未満<100>
    Planner の達成率は数値で表現され、範囲は 0 ~ 100 となっています。100 だと達成してしまうため、100 未満としています。
  • <ticks(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime']) >次の値以上<ticks(addDays(utcNow(), 0))>
    期限が UTC の現在日時以降かどうかを判断しています。わざわざaddDaysで 0 日を加算しているのは、この後の条件式と比較しやすいためです。 また、判定する期間を変える場合(例えば今週期限のタスクを収集するといったケース)、0 の部分を変更するだけでいいので少し楽できます。
  • <ticks(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime'])>次の値未満<ticks(addDays(utcNow(), 1))>
    期限が UTC の現在日付+1日以前かどうかを判断しています。上の条件と組み合わせて"今日"という条件を作り出しています。

b.割当先ユーザーごとにメッセージを整形する

条件に一致したタスクを収集できたら次はメッセージの整形です。 Planner では、1つのタスクに対して複数ユーザーを割り当てることができます。 通知するのであればタスクに割り当てられた全員に通知するべきです。

通知メッセージの内容は要件次第ですが、今回は1つのタスクに複数ユーザーが割り当てられた場合、ユーザーごとに1行メッセージを追加していくことにしています。 「割当先ユーザーごとにメッセージを整形する」がループになっているのはそのためです。 このループでは割り当て先ユーザーIDを値としています。 実は、Planner でタスクを収集しても割り当てられたユーザー名は取得できません。 そこで、ユーザーIDを使います。

このユーザーIDは、Microsoft 365 の ID に紐付けられています。 なので、Microsoft 365 の「ユーザー プロフィールの取得(V2)」を選択し、"ユーザー(UPN)"に Planner から取得したユーザーID を指定してやればOKです。

そうすればメッセージの整形に取得したユーザーの表示名を使用することができます。

6. 期限が今日のタスクがあるか判定する

ここまでで、期限が条件に一致するタスクを通知するためのメッセージを作っているので、そのメッセージが空であれば「タスクが無いことを通知する」。そうでなければ「条件に一致したタスク情報一覧を通知する」ようにしています。

f:id:iyemon018:20200913173159p:plain

条件式は、具体的には以下のような式になっています。

empty(variables('task_list'))

task_listは「③期限であるタスク一覧を通知するためのメッセージ」が入っています。 この値が空である(empty(variables('task_list'))trueである)ということは「今日が期限のタスクが1つも無い」ということになります。 逆にこの値に何か入っている(empty(variables('task_list'))falseである)ということは「今日が期限のタスクが少なくとも1つはある」ということになります。

条件式がtrueの場合とfalseの場合とでそれぞれ通知したいメッセージを作ります。 タスクがない場合は通知しないという選択肢もありますが、フローが失敗している場合も通知はされないため、「フローが成功してかつ、今日が期限のタスクがない」ということを共有したかったので、タスクがないときも通知は実施しています。

このあたりはフローの目的やフローをどこに通知するか等によって決めておくべきでしょう。

7. チャットに新しいメッセージを追加する

ここは説明するまでもないですが、メッセージを特定のチャットルームに通知しています。 できれば、どのフローから通知されているかもメッセージに含めておくと良いでしょう。

iyemon018.hatenablog.com

期限を変えてみる

フローが作成できたので、他の期限を設定します。 フローを別名保存しておきましょう。

ここで大事なのが、期限ごとのフローで同じタスクを通知しないというところです。 制御するためにはa.通知すべきタスクかどうかを判断するの条件を変えてやればOKです。

その週が期限のタスク一覧を通知する

条件は単純です。「通知の翌日からその週の最終日まで」とすれば良いのです。 具体的には、次のとおりです。

  • <empty(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime'])>次の値に等しい<false>(変更なし)
  • <値 達成率>次の値未満<100>(変更なし)
  • <ticks(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime']) >次の値以上<ticks(addDays(utcNow(), 1))
  • <ticks(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime'])>次の値未満<ticks(addDays(utcNow(), 6))

太字にした箇所を UTC 現在日時の1日後~6日後で期限の日付を比較しています。ここでticks(addDays(utcNow(), 0))としてしまうと、今日が期限のタスクも取得できてしまいます。

遅延しているタスク一覧を通知する

こちらは更に単純です。

  • <empty(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime'])>次の値に等しい<false>(変更なし)
  • <値 達成率>次の値未満<100>(変更なし)
  • <ticks(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime']) >次の値以下ticks(addDays(utcNow(), 0))

タスクの期限が UTC の現在日時よりも過去の日時かどうかを判断しているだけです。


Planner は通知機能が弱いので Power Automate で良い感じに通知してあげるとかなり使い勝手が良くなります。 もっと良い方法をご存じの方は是非コメントください!