ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

DockerでLaravel開発環境構築

こんにちはtkyです。

最近業務でPHPを使った開発をしているのですが、Laravelの開発環境をメモとして残しておきます。

※そろそろAndroidやりたい・・・!!どこかにAnroid開発転がってないですかね? 🤔

本記事でできるようになること

  • Laravel開発するためのDocker開発環境テンプレートが構築できる
  • swaggerでAPI仕様書も一緒に書ける状態にする

本記事で触れないこと

  • Laravelとは,、Dockerとは、のような説明
  • DBの構築については各プロダクトによるので割愛します
  • Laravelのコードは各プロダクトによるのでgithubには入れていません。

環境はgithubで公開しています。基本的にREADME見たらここから先の手順を見なくても構築できるようになっています。

github.com

Docker Compose で以下のような環境を構築します。

ローカルマシン上の server/ 配下と各Dockerコンポーネントをマウントして、Laravel環境を構築してnginxで公開するイメージです。

nodeも含めて全部Dockerで完結できるようにしました。API作るので一緒にswaggerも入れました。

f:id:ticktakclock:20190714021359p:plain:w500
docker環境図

私が開発環境するに当たり、参考にさせていただきました。ありがとうございます。 qiita.com

とりあえずdocker-compose.yml見せて

そうなりますよね。私もそう思います。別途 .env ファイルを用意してください。github上に .env-sample を用意していますのでrenameすればOKです。

version: '3.7'
services:
  php:
    container_name: ${PHP_NAME}
    build: ${PHP_CONTAINER_DIR}
    ports:
      - ${PHP_PORT}:${PHP_PORT}
    volumes:
      - ${LOCAL_DIR}:/var/www

  nginx:
    image: nginx
    container_name: ${NGINX_NAME}
    ports:
      - ${NGINX_PORT}:${NGINX_PORT}
    volumes:
      - ${LOCAL_DIR}:/var/www
      - ${NGINX_CONF}:/etc/nginx/conf.d/default.conf
    depends_on:
      - php

  db:
    container_name: ${DB_NAME}
    build: ${DB_CONTAINER_DIR}
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USERNAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      TZ: 'Asia/Tokyo'
    restart: always
    volumes:
      - ./docker/db/data:/var/lib/mysql
      - ./docker/db/sql:/docker-entrypoint-initdb.d
    ports:
      - ${DB_PORT}:${DB_PORT}

  swagger:
    image: swaggerapi/swagger-editor
    volumes:
      - ./docker/swagger/${SWAGGER_YAML}:/usr/share/nginx/html/${SWAGGER_YAML}
    environment:
      API_URL: ${SWAGGER_YAML}
    ports:
      - ${SWAGGER_PORT}:${SWAGGER_PORT}

  node:
    container_name: node
    build:
      context: ./
      dockerfile: ${NODE_DOCKERFILE}
    tty: true
    volumes:
      - ${LOCAL_DIR}:/app
    ports:
      - ${NODE_PORT}:${NODE_PORT}
      - ${NODE_UI_PORT}:${NODE_UI_PORT}

cloneしたらやること

.envファイルを用意します。.env-sampleをrenameすれば良いです。各々の環境によって値は変更して大丈夫です。

$ cp .env-sample .env

コンテナたちを起動していきます。

$ docker-compose up -d

phpのコンテナに入って、laravelのプロジェクトを作成します。

$ docker-compose exec php bash
$ laravel new

ここまで来たら http://localhost/ にアクセスするとLaravelのページが開いているかと思われます。

このhttp://localhost/ はnginxによって立てられているサーバーとなります。

f:id:ticktakclock:20190713204647p:plain:w500
laravel環境が立ち上がったときのhttp://localhost/の画面

これでLaravel環境構築としては完了です。 ここからは実際に開発を効率化するためにもう少し手を加えていきたいと思います。

browser syncの導入

webpack.min.js を以下のように書きます。 webpack.mix-sample.js をコピーしても良いです。

webpack.min.js

mix.js('resources/js/app.js', 'public/js')
    .sass('resources/sass/app.scss', 'public/css')
    .browserSync({
        proxy: {
            target: 'http://nginx', //nginxのコンテナサービス名と一致させる
        },
        files: [
            './resources/**/*',
            './app/**/*',
            './config/**/*',
            './routes/**/*',
            './public/**/*',
        ],
        open: false,
        reloadOnRestart: true,
    });

nodeのコンテナに入ります。

$ docker-compose exec node /bin/sh
$ npm i
$ npm run hot

run hotしたときにいくつかのnode_modulesが新規インストールされます。コマンドが終了してしまうのでもう一度 npm run hot します。

この状態で http://localhost:3000/ を見に行くとLaravelのページが開いているかと思われます。 PHPのコードを編集して保存するとブラウザリロードがかかって即時ページが反映されているかと思われます。

あとはゴリゴリ書くだけ!!!

sassのコンパイルについて

実は npm run hot だとsassコンパイルがうまくできません。 そのため今の開発案件では npm run watch を使用しています。

メモリ上に展開されるファイルをserveするhotコマンドとローカルマシン上のディレクトリに展開されるファイルをserveするwatchコマンドという違いがあります。

nodeコンテナ上で

$ docker-compose exec node /bin/sh
$ npm i node-sass
$ npm run watch

こうすると /server/public/ 配下にコンパイルされたsassやjsが展開されるようになり、sass更新時、即時ブラウザ反映されるようになります。

またこのとき、常々public配下のファイルが変更されるため、 /serverpublic/css/serverpublic/js.gitignore で管理対象外にすることをおすすめします。

ついでにAPI仕様も

API仕様書もかけるようにswaggerも入れておきました。くらいです。 不要であれば消してください。

http://localhost:8080/ で仕様書が見れるようになります。

終わりに

今回、初のPHPチャレンジでLaravelを触ったりDockerでの環境構築にトライしたりいろいろと勉強になりました。 Dockerfileの書き方も結構理解が進みました。

FormData.get()を使う時は対応ブラウザに気をつけよう

こんにちは、tkyです。

最近ReactのSPAやっていて、今日はその開発の中でハマったjavascriptのことを書きます。

何があったの

APImutipart/form-data なPOSTメソッドを叩く時って、要件によりしばしばあると思いますが、 ある日社内の検証端末(iPad)で動作確認していた時、POSTできない不具合が発生しました。

formData.get is not a function

やろうとしていたことは何か

  1. formdataに特定のパラメータが入っているかどうかPOST前に確認したかった
  2. 基本Google Chromeの検証ツール上で動作確認していた

※ブラウザの対応要件は chromesafariの2種でした。iOSAndroid上でも動作すること、がブラウザ要件となります。ieとedgeは要件から切り捨てています 😎

何が原因だったの

実はiOSsafari (以降、Safari on iOS)でForm.get()が非サポートのため使用できなかった、というものでした。

developer.mozilla.org

あれ?でも別のiPhoneだと動くんだけど・・・? 🤔

そうなのです。特定の端末だとPOSTできるのです。

f:id:ticktakclock:20190606131450p:plain:w200
POSTできる場合、Formdata.get()が存在する。他のメソッドも存在する。

f:id:ticktakclock:20190606131407p:plain:w200
POSTできない場合、Formdata.get()が存在しない

では状況を整理します。端末が少なかったのでエミュレータでの確認となります。

あるiOSバージョンを境にして切り分けができました。

iOSバージョン 結果
10.2 NG
11.1 NG
11.2 NG
11.3 OK
11.4 OK
12.0 OK

iOS11.2と11.3を境に何が起こったのか

iOS11.3に搭載されているのはSafari11.1ですが、変更点は以下でした。

developer.apple.com

Service Workerの対応が一番しっくりきますかね。ServiceWorkerの実装が入ったことでWeb Worker側の基本実装も入り、Web Workerを利用するFormData側にも良い方向で影響があったのだろう。が今の見解ですが、あくまで推測なので違っていたらスミマセン。

対応ブラウザとバージョンには気をつけよう

開発時、ググって実装して動いたーでコミットしてしまう時、ありますよね。 Form.get()だけでなく、ちゃんと各ブラウザの対応状況は確認しておく癖をつけておくのが良いなと思いました。

BiometricPromptで指紋認証機能を実装する

こんにちは、tkyです。

今回はAndroidBiometricPromptを使って指紋認証機能を試してみたいと思います。

developer.android.com

こんな感じで動いてます。 コードと動作確認gifはgithubを参照してください。

github.com

f:id:ticktakclock:20190314144934p:plain:w200

BiometricPrompt

API 28から使用できる新しい認証用のフレームワークです。以前はFingerprintManagerというものでしたが、こちらは 非推奨 となりAPI28からはBiometricPromptを使用するようにとの公式からのお達しが出ております。

パーミッション

<uses-permission android:name="android.permission.USE_BIOMETRIC" />

コード

BiometricPromptのビルダーを使います。 ここにダイアログに使う情報をセットして、authenticateしますが、ダイアログの情報を先に入れないとインスタンス作れないのちょっとめんどくさい。

val cancellationSignal = CancellationSignal()
BiometricPrompt.Builder(context)
    .setTitle("生体認証")
    .setSubtitle("サブタイトルを添えて")
    .setDescription("詳細説明をここに記載します")
    .setNegativeButton("キャンセル", context.mainExecutor, DialogInterface.OnClickListener { dialog, which ->
        cancellationSignal.cancel()
    })
    .build()
    .authenticate(cancellationSignal, context.mainExecutor, object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
            super.onAuthenticationError(errorCode, errString)
        }
        override fun onAuthenticationFailed() {
            super.onAuthenticationFailed()
        }

        override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) {
            super.onAuthenticationHelp(helpCode, helpString)
        }
        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
            super.onAuthenticationSucceeded(result)
        }
    })

AndroidXのBiometricPrompt

前述でFingerprintManagerがAPI28から非推奨となりますが、P以上であればBiometricPrompt、未満であればFingerprintManagerという実装をすることになります。 BiometricPromptはsupportライブラリがないため、仮に実装の必要が出た場合、以下が必要になってくるというということです。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)

この課題を解決するために登場するのがAndroidX BiometricPromptです。 それぞれパッケージは以下のようになっています。

  • 通常のBiometricPrompt
    • android.hardware.biometrics.BiometricPrompt
  • androidxのBiometricPrompt
    • androidx.biometrics.BiometricsPrompt

コード

パーミッションは通常のBiometricPrompt同様で必要です。AndroidXにおいてはgradleに追記が必要です。

implementation 'androidx.biometric:biometric:1.0.0-alpha03'

AndroidXではダイアログを生成するBuilderと認証するためのクラスが分かれています。 BiometricPromptのコンストラクタに必要な情報を流して、authenticateする時にダイアログ情報をセットして認証させる流れです。

val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("AndroidXによる生体認証")
    .setSubtitle("サブタイトルを添えて")
    .setDescription("詳細説明をここに記載します")
    .setNegativeButtonText("Negativeボタン")
    .build()
// context.mainExecutorはAPI28からなので自分でmainExecutorを作成
BiometricPrompt(activity, mainExecutor, object : BiometricPrompt.AuthenticationCallback(){
    override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
        super.onAuthenticationError(errorCode, errString)
    }
    override fun onAuthenticationFailed() {
        super.onAuthenticationFailed()
    }
    override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
        super.onAuthenticationSucceeded(result)
    }
}).authenticate(promptInfo)

動作確認

いろいろな端末で動作確認してみます。

XperiaZ3 (Android 5.0.2 API21)

API21でBiometricPromptを使おうとすると当たり前ですがNoClassDefFoundErrorが発生します。 githubの実装では落ちないようにcatchしてToast出すだけしています。

java.lang.NoClassDefFoundError: Failed resolution of: Landroid/hardware/biometrics/BiometricPrompt$Builder;

AndroidXのBiometricPromptであれば、ちゃんとコールできています。 が、指紋認証センサーは搭載されていないので、BiometricPrompt側で認証エラーが発生します。

HUAWEI SHT-AL09(Android8.0.0 API26)

APi26なので同様にBiometricPromptを使おうとするとNoClassDefFoundErrorとなりますが、 この子は指紋認証センサーが搭載されているので、AndroidXのBiometricPromptが動作していることが確認できました。

Pixel 3 (Android 9.0.0 API28)

言わずもがな、どちらも動作できました。

最後に

指紋認証を試してみました。今の所「その端末がユーザのものであることを確認する」以上の使い方が思いつきませんでした・・・ この流れでWebAuthnとかFIDO2とか踏み込むかどうか・・・

また、AndroidX中の実装を気になって見てみたら、バージョンで切り分けてBiometricPromptとFingerprintManagerをゴリゴリしてました😅僕らでバージョン分岐しなくて良くなったのは事実です!

参考

Fingerprint Authentication using BiometricPrompt Compat

DroidKaigi2019 2日目に行ってみた感想とか

こんにちは、tkyです。

2日目も行ってきました。1日目と同じようにセッション参加したtkyの個人的感想を述べる内容となっております。

droidkaigi.jp

1日目のレポートはこちらをご覧ください

ticktakclock.hatenablog.com

この日見た講演はなんと8講演。疲れた・・・けどとても濃い1日でした

  • Dialogflowによる自然言語処理(NLP)を用いたボイスコマンド音声認識の精度向上(10:30~)
  • Wi-Fi RTTによる屋内測位アプリを作ろう(11:20~)
  • All About Test of Flutter(12:50~)
  • Lifecycle, LiveData, ViewModels - The inner wiring(14:50~)
  • multi-module Androidアプリケーション(15:40~)
  • Navigation Architecture Component によるアプリ内遷移の管理(16:50~)
  • Android Thingsでのプロダクト開発(17:40~)
  • BLEアプリ設計パターン(18:30~)

最初は朝ごはん

会社に行くより早く電車に乗り、9:40くらいに会場到着。サンドイッチやコーヒー、紙パックジュースなどを堪能できました。

エキシビジョンルームにて朝食が配布されています。 f:id:ticktakclock:20190209140100j:plain

Dialogflowによる自然言語処理(NLP)を用いたボイスコマンド音声認識の精度向上

speakerdeck.com

www.youtube.com

Dialogflowはgooglehomeアプリを作成する際にも使われますので、興味アリアリです。

googlehomeのアプリをサンプルで作った時にDialogFlowを使ったことがあるのですが、Androidアプリからも使ってみたいですね!聞くところによるとv1のsdkは2019.9にDeplicatedになるので要注意だそうです。v2との使い勝手はどう異なるのだろうか。

DialogFlowについては本ブログでも別途触れてみたいなと思います。

やっぱり、固有名詞を検出するにはEntityを設定しまくるしかないのですね。音声認識の揺れを吸収するためのアノテーション作業などはやったことないので後ほど見てみたいと思います。

Wi-Fi RTTによる屋内測位アプリを作ろう

speakerdeck.com

www.youtube.com

WiFi RTT(Round Trip Time IEEE 802.11mc)全然知らないから気になってました。

基本的にFusedLocationを使って位置情報を取得します。これを使うことでGooglePlayServicesを経由して位置情報を取得するようにできます。 WifiRTTは屋内でも使用できる位置測位用の規格であるため、アクセスポイントを4つ使うと精度は1mとかなり良くなる状況だそうです。

アクセスポイントからの距離はわかるが、アクセスポイントがどこにあるかは知っておく必要がある、なるほど 👀 測位方法はピタゴラスの定理連立方程式を組み合わせて実現するようです。

少なくとも3点測位ポイントがあり、その3点のポイントを線で結んでできる空間内にいる必要があることが測位方法から見ても理解できるかと思われます。

API28からscanのAPIが色々変更担っているようでDeplicatedにもなっている模様

精度を出すための条件がなかなか厳しそうですね。アクセスポイントの設置場所や地図の選定など、やることは色々ありそうです。 他のセンサーも併用して使うとより精度の向上が図れそうと思いました。

ここでお昼ご飯

1日目のお弁当を撮影しそこねましたが、2日目はちゃんと撮影できました、この後のFlutterのセッションルームにて着席して昼食休憩です。

f:id:ticktakclock:20190209140452j:plain

All About Test of Flutter

docs.google.com

www.youtube.com

you brideという婚活支援サービスがFlutterで作成されているようです。

今回のgithubも要チェケです。

github.com

自分のコードに自身を持って確かなものにするためにテストを書く。いい話だ。

名言も飛び出ましたw

人間は超優秀全自動アサーション関数

自動テストにするか、人力テストどちらを取るかは、継続してコスト回収できるかどうかで判断すると良さそうです。

めちゃめちゃ良い発表スライドで、終わった後もガッツリ見てます!

質問時間にて、未実装部分についてはスキップ関数にするのか、Failさせるのが良いのか議論が最後に行われていました。 単純にFailした場合と未実装なのでFailなのかがパット見わからないので、メンバーのテストリテラシ状況に合わせて選択するのが良いのでは、という結論にいたりました。

また、テストレポートについては現状HTMLなどで見れるようなものはこのとき回答はありませんでした。コマンドライン上でもギリ見やすそう・・・?

小休憩

ちょっとだけ疲れたので小休憩です。

Clip Roomさんのブースでデザインガイドラインの一部を見せていただいたりしました。やっぱりデザイナーさんとエンジニアがコミュニケーションとれる状況良いですね。

そしてまたコーヒー・・・美味しいんですよね〜 f:id:ticktakclock:20190209140654j:plain

Lifecycle, LiveData, ViewModels - The inner wiring

www.youtube.com

architecture componentの講演となります。英語のセッションですが、同時翻訳なので安心?して聞けます。 Architecture ComponentのLifecycleコンポーネントなどなどの解説となります。

LiveDataはデータホルダーであり、streamが終わるという考えはなく、ライフサイクルで使われるため、本質が異なるということですね。 しばしばLiveData vs RxJavaというように比較される事が多いのですが、そもそも本質が異なるためにLiveDataとRxJavaがともに存在する、という認識が出てきました。

翻訳が非常に聞き取りやすく個人的に理解が進みました。

multi-module Androidアプリケーション

https://speakerdeck.com/sansanbuildersbox/multi-module-android-application

www.youtube.com

なんとかマルチモジュールの講演入れました!

SanSanのEightではモジュール40個で構成されている、すごい分割数だ・・・

モジュールに分けることでプロジェクト内のコンパイル単位を分割できたり依存管理できるのか良いところですね。

  • ビルドが高速化できる
  • コードの依存関係を強制できる
  • モジュールごとにテスト実行できる
  • Kotlinのinternal修飾子でモジュール内で可視性を定義できる
  • Dynamic Feature Moduleでインストール時の容量を下げる

色々メリットがありますね!

gradle plugin 3.0になってからのimplementation指定と過去のcompile指定の挙動の違いについてちゃんと理解していなかったのですが、解説を聞いてスッキリしました。

gradleのビルド結果は--scanオプションで見れる!へぇ! incrimental buildの場合だと並列マルチモジュールだとビルドが早いですね。Annotation processingの実行をコストが抑えられることがポイントのようです。

realmを使用したマルチモジュール化の高速化例も紹介いただいて理解が進みました。チーム全体のマルチモジュールについての理解の底上げも重要ですね〜。

どうマルチモジュール化したら良いのか

モジュール分けする際にレイヤーで分けるのか機能で分けるのか、結構迷うのですが、

app → data  → domain
  → ui       → domain

という構成でまとめると良さそう!

機能ごとに分けるときも画面遷移のIntentをinterfaceとしてapp側で持ってあげて各画面にDIすることで依存関係をフラットにすることができるので、1対多(画面)の構成が作れそうだと思いました。

eightでは機能ごとにモジュールを分けているみたいですね。

すごくためになるセッションでした!

Navigation Architecture Component によるアプリ内遷移の管理

speakerdeck.com

www.youtube.com

Architecture Componentに関する講演です。

画面遷移時の課題を解決できるようなライブラリとツール群のことですね。

Navigation Architectureにおいてはどの画面がスタート地点になる画面なのかをガイドラインとして定義されているようです。 deep linkについても「同じ画面にいるなら、同じ画面スタックが形成されているべき」というような原則が存在しています。

navigation を定義するxml内でfragmentを定義し、fragment layoutの中にapp:navandroid:nameの記述を書くことでNavigationの遷移を実現できるみたいです。

あとはJavaとKotlinでSafeArgsの指定の仕方が異なるようですね。JavaはBuilderパターン、Kotlinはnamed argumentで指定、という具合です。

最近のAndroidの開発に触れていないので、結構新鮮でした。

Android Thingsでのプロダクト開発

DroidKaigi2019 AndroidThingsでのプロダクト開発 - Google スライド

www.youtube.com

Android Thingsはgoogleが提供するIoT向けプラットフォームのことです。 Androidなので既存のシステムを利用できるところは強いですね。ハードウェアレイヤもGoogleがサポートしているようなのでこういったところも強みになりそうです。

最初はmuiの製品紹介から入ります。スマートディスプレイの代替としての製品のようです。 muiプロダクトデモを実際に見せていただいて実感がわきます・・・!!!

mui.jp

AndroidThingsとAOSP,Linuxとの違いについては、カーネルのカスタマイズができるかどうか、SoCの選択肢があるかどうか、OTAができるかどうか、が選定のポイントとなりそうです。

メリットは以下。

  • Peripheral I/O APIがある(I2CやUARTのラッパーAPIがある)
  • OTAできる
  • AndroidStudio使える
  • Android Frameworkも使える

一般のデベロッパーがさわれる領域にPeripherarl I/O APIの入出力をフレームワークに統合するためのUserDriver(Input Driver)というものが結構微妙(画面描画あたり、座標空間周り)らしいですね。UserDriver(InputDriver)とはI2CのIOをボタン押下イベントに変換するようなものらしいです。

基本カーネルフレームワークGoogleが完全に制御しているので一般デベロッパーは触ることができず。 先程まで強みだと思っていた点が回り回ってデメリットになってきました・・・カーネルをカスタマイズできない点については、ラズベリーパイで発生するclock stretching問題において重要な欠点のようです。というのもこの問題を暫定で解決するためにI2Cのボーレートを変更する必要があるそうですが、OTAでファームウェアを流し込んだ時にその編集部分のコードが上書きで消えてしまうそうです。事実上OTAできないじゃん!ということになるわけですね・・・闇が見えてきました

新規でThingsのプロダクト化が難しい状況で有ることがわかりました・・・w セッションが悲しい雰囲気に包まれてしょんぼりだったのですが、トライ&エラーの結果を共有していただけた非常にためになるセッションでした!!

BLEアプリ設計パターン

speakerdeck.com

www.youtube.com

BLEのつらみについては触れず、設計ノウハウにフォーカスしたお話です。

BLEはHTTPSのようなプロトコルとどう違うのか、実際にどのように設計しているのかをノウハウ共有いただけるセッションでした。Qrio Lockはいわゆるスマートロックで、BLE通信を使って鍵の開け締めを行っているようです。

通信という意味ではWifiという選択肢もありますが、省電力や近接検知ができるという観点で、BLEの選択肢が出てくるということですね。

Webの世界はRetrofitのようなライブラリを使うと非常に楽になったしますが、BLEは20バイト制限があるので、 20バイト以上のデータを扱うときはパケットを分割して受け取った側でくっつけるような、HTTPのプロトコルが担保する領域を自分で担保する必要があるようです。なるほど・・・

BLEが仕様で搭載しているセキュリティではできない自前でセキュリティを担保する必要も出てくるとのこと。なかなかつらみが出てきましたね!!

「ロック(スマートロック機器)の名前」は情報をデバイスで持つべきか、サーバで持つべきか、という観点も非常に興味深いです。

→結論的にはwebサーバ側で情報を持って、デバイス側は極力シンプルな構成にすることが良さそうです。

登場人物がスマートロックデバイススマホ、webサーバが存在しており、この3者がうまく同期している必要があるのもなかなか難しい課題です。 この際の通信の整合性を担保するためにべき等なAPIにする必要があることも重要ですね。

実際にアプリはMVVMで作成されているようですが、テストに関するメリットは高く、 しかしながら責務分けに関するベストプラクティスを探すようなコストも高くなってしまうこともあるとのことです。

さいごに

2日間有休を使って参加しましたが、最高に楽しかったです。こういうの参加するとやる気出ますね。来年も行きたい。。。行けるようにがんばります。 色々と新しい単語もピックアップできたのでこれからも継続して勉強していきましょー

いただいたものたち f:id:ticktakclock:20190209141110j:plain

DroidKaigi2019 1日目に行ってみた感想とか

こんにちは、tkyです。

DroidKaigi2019に行ってきました。1日目のレポです。

droidkaigi.jp

2日目のレポはこちらをご覧ください。

ticktakclock.hatenablog.com

行ってきた講演は以下です。お昼後、yanzmさんの講演見たかったのですが、満席御礼で入れず・・・

  • マテリアルデザインの起源とベースとなる哲学(13:50~)
  • 辛いと評判のAndroid BLEを頑張って使い続けた話(14:00~)
  • Chrome Custom Tabsの仕組みから学ぶプロセス間通信(14:50~)
  • Understanding Kotlin Coroutines コルーチンで進化するアプリケーション開発(15:40~)
  • Chrome + WebAuthnで実現できるパスワードレスなユーザ認証体験と開発者の課題(17:10~)

お昼に到着

いざ! f:id:ticktakclock:20190209134738j:plain

yanzmさんの「LiveData と Coroutines で実装する DDD の戦術的設計」を見たかったのですが、なんと満員!さすがというべきか入れませんでした・・・あとで配信見ます!

会場の様子です。

f:id:ticktakclock:20190209130852j:plain

そして急遽見たかったもう一つの講演に向かいました。

マテリアルデザインの起源とベースとなる哲学

speakerdeck.com

www.youtube.com

ちょうどマテリアルデザインを取り入れたUIを作っているプロダクトの最中だったので、興味アリアリの内容です。

そもそも誰がマテリアルデザインを作ったのかさえ知らなかったので恐縮ですが、すごい面白い講演でした。

そもそも誰が作ったのか・・・・ googleのデザイン責任者であるマティアス・デュアルテさんが作ったそうですね。昔のですが記事が出てました。

https://www.gizmodo.jp/2014/07/_android.html

この方の歴史的背景や知識を見ることでマテリアルデザインの思想と哲学を見ていく、という内容でした。

マテリアルデザインについては私は根本的なところで理解がかけていたようで、本講演を受けて改めてマテリアルデザインガイドを査読していきたいです。

辛いと評判のAndroid BLEを頑張って使い続けた話

www.slideshare.net

www.youtube.com

BLEつらいねーーー!!!!wBLEを多少触ったことがある身分としては皆さん同じようなところにつらみを感じているらしく、共感でしたww

BLEは接続のためには数ステップやることがありますが、AndroidではそれらすべてのAPIが非同期のコールバックとして返ってきます。今であればRxやKotlinコルーチンを使うことで被害を最小限に抑えられそうです。

突然切断される現象やサービス接続からいきなり通信するとエラーになる現象などはタイマーやリトライ処理をしてあげることでできる限り救う。

つらみのなかでやりようを見出していくそのスタイルに共感です。

Android バージョン x BTチップ x BTプロトコル・スタック x 独自省電力 x デバイス側のチップ相性 =可能性無限大ですね!!!!😇

RxAndrodiBleというライブラリでBLEの処理をRxで書くことができるのですが、昔なかなかRx2に対応されなくてRx2にラップする処理を自前で書いていたりしました。。 しかし今はRx2対応されているようなのでまた見てみても良いかもですね!

Chrome Custom Tabsの仕組みから学ぶプロセス間通信

www.slideshare.net

www.youtube.com

Chrome Custom Tabsの仕組みにおいては別にGoogleChromeだけが保つ機能ではなく、CustomTabsと呼ばれるサービスに対応したアプリが提供できる機能となっているようです。

こういった仕組みを提供、実現するためにはAIDLという他のアプリケーション(サービス、=別プロセス)とやり取りするためのインターフェースを定義・実装することで実現できます。

実装面でいうとCustomTabsServiceっていうAbstractクラスがあって、それを継承したクラスを作ればとりあえずCustomTabsを提供するサービスが作れるみたいです、こう聞くと少しハードルが下がる・・・?😅

最近モダンな開発にフォーカスが当たる中、Androidフレームワークに関する機能にふれる機会が減ってきていたので非常に勉強になる講演でした。

Understanding Kotlin Coroutines コルーチンで進化するアプリケーション開発

speakerdeck.com

www.youtube.com

リリースされて間もない機能なので要チェケです。

  • どのようにコルーチンを使うか、学ぶか
  • どのようにコルーチンを適用するか
  • コルーチンをいつ使うか

という点にフォーカスを当てた講演でした。

Concurrency and Parallels(平行性と並列性)というワードが印象に残っています。 コルーチンスコープについては基本的にアプリ内のライフサイクルに基づいてスコープを定義して動作させるのが良さそうです。

また、ほかアーキテクチャへの適用として重要になる考え方としてモジュール間の関心に着目するという点です。 データフローが統一するならRx、独立性の高い開発モデルを主眼とするならモジュール間でコルーチンを適用することを検討しても良さそうです。

このあたり私も初見の類に入るので自分の中でしっかりと理解して行きたいです。

小休憩

少し疲れたので休憩がてらにコーヒーを飲みに来ました。 このアルファベッティカフェさんがDroidKaigiに出張に来てくださり、プロのバリスタさんによるコーヒーを飲むことができてよかったです。普通にお店に行きたい・・・

f:id:ticktakclock:20190209135302j:plain

yahoo japanブースにてTRILLグッズを頂いたり、その他様々なブースを覗いたりしました。

f:id:ticktakclock:20190209135135j:plain

Chrome + WebAuthnで実現できるパスワードレスなユーザ認証体験と開発者の課題

speakerdeck.com

www.youtube.com

本日ラスト講演です。

パスワードについての現状の課題、コストを考え人間が適用できる認証方式を考えた結果のWeb Authentication APIという1つの選択肢のようですね。

正直なところ私が無知すぎて単語を追うのが精一杯でした・・・ 😇

パスワードの方式として「忘れた、パスワード再設定フローもない」はいわゆる詰んだ状態になりますが、web authnの仕組み(要はデバイスを使った指紋認証と組み合わせたもの)だと「スマホをなくした/壊れた」ら詰んだ状態になる、ということなので、詰んだ状態からどのように回避するかが課題になりそうです。

アフターパーティー

いよいよアフターパーティです 🕺

f:id:ticktakclock:20190209133619j:plain

f:id:ticktakclock:20190209134334j:plain

なお写真はpixel3で撮影したものです。ポートレートめっちゃよく撮れる😎

お食事も美味しかったし、登壇者のかたと会話できましたし良い体験ができました。

終わりに

2日目は朝ご飯食べに早めに行こうと思います!引き続き2日目のレポートもどうぞ!

頂いたものたち f:id:ticktakclock:20190209134402j:plain

Flutter Androidのソースコードを読む(FlutterActivity編)

こんにちは、tkyです。

前回FlutterApplication.javaのコードを読んで、Dartを動作させるための初期設定を査読しました。

今回はFlutterActivity.javaのコードを読みながら、アプリ起動するまでを追ってみようと思います。 調べた軌跡を残しているので今回もそれなりに長い記事となっております。

前回も書きましたが・・・今回読んでいくのはこれ

flutter.jar

Android Studioにはjarのclassファイルに定義ジャンプすることができるので⌘ + クリック(winの場合は ctrl + クリック)で読めますが、 ここで読めるのはあくまでjavaで書かれた領域だけなのでネイティブコード(C/C++の領域のことを指します)も見ることを考慮して flutter/engineもcloneしておきます。

github.com

目次

  • FlutterActivity.javaを探す

FlutterActivity

以下にそれぞれあります。engine側のコードがビルドされてflutter.jarになります。

  • engine側
    • engine/shell/platform/android/io/flutter/app/FlutterActivity.java
  • flutter.jar側
    • io/flutter/app/FlutterActivity.java

コンストラクタを見てみましょう。FlutterActivityDelegateにほぼ集約されてそうな感じです。

public class FlutterActivity extends Activity implements Provider, PluginRegistry, ViewFactory {
    private final FlutterActivityDelegate delegate = new FlutterActivityDelegate(this, this);
    private final FlutterActivityEvents eventDelegate;
    private final Provider viewProvider;
    private final PluginRegistry pluginRegistry;

    public FlutterActivity() {
        this.eventDelegate = this.delegate;
        this.viewProvider = this.delegate;
        this.pluginRegistry = this.delegate;
    }

FlutterActivityDelegateとは

名前が名前だけあって、Activityがやる処理の委譲クラスです。 ここでは大体以下のことをしているようでした。

  • flutterViewの保持
  • ライフサイクルメソッド(onCreate, onStart, onResume等)の処理
  • Intentの処理

ライフサイクルに従って適切にViewに状態を伝える役割をしているようですね。

onCreateで何をしているのか

FlutterMain.ensureInitializationComplete()、flutterViewの生成とIntentの処理をしていました。ここではflutterViewにフォーカスを当てて見てみます。

ensureInitializationComplete

初期化完了させるための確定処理、という意味で捉えます。

FlutterApplicationの処理にて初期化した、shared ibraryのpathやvm snapshot, isolateのpathを使い、vm shellの起動パラメータの生成処理を行います。 その後、nativeInit()を呼び出しています。

このインターフェースの定義はどこにあるのかというと以下にあります。

engine/shell/platform/android/flutter_main.cc
{
    .name = "nativeInit",
    .signature = "(Landroid/content/Context;[Ljava/lang/String;Ljava/"
           "lang/String;Ljava/lang/String;Ljava/lang/String;)V",
    .fnPtr = reinterpret_cast<void*>(&Init),
},

flutter_main.ccはJNIのインターフェース定義を行っています。 flutter_main.ccはSystem.loadLibrary("flutter");にてlibflutter.soがロードされたときに、 engine/shell/platform/android/library_loader.ccのonLoad内でflutter_main.ccが実行されます。 ※要するに、JNIの定義設定処理はFlutterApplication.javaのonCreate処理で行われているということになります。

nativeInitの実際の処理はdartのキャッシュに関する処理をしているようですが、今の私の理解を超えていたので一旦おいておきます。。

flutterViewの生成

ViewFactoryはFlutterActivityに実装されていますが、デフォルト実装でnullが返るようになっているのでonCreateで必ずFlutterViewを生成することになりそうです。

this.flutterView = this.viewFactory.createFlutterView(this.activity);
if (this.flutterView == null) {
    FlutterNativeView nativeView = this.viewFactory.createFlutterNativeView();
    this.flutterView = new FlutterView(this.activity, (AttributeSet)null, nativeView);
    this.flutterView.setLayoutParams(matchParent);
    this.activity.setContentView(this.flutterView);
    this.launchView = this.createLaunchView();
    if (this.launchView != null) {
        this.addLaunchView();
    }
}

viewFactory.createFlutterNativeView

FlutterViewはSurfaceViewを継承しているViewであることはわかりますね。flutterで作られたアプリはこのsurfaceView上に描画されている、という理解です。

ではFlutterNativeViewは何なのか?実装を見るとJNI側に処理を渡すためのクラスになっているようです。

FlutterNativeViewにはnative修飾子のメソッドがいくつか定義されています。このメソッドの実装はC++側の実装となり、flutter.jarだと此処から先は確認することができません。

この先はengine側のソースコードを見ることになりますが、後ほど見ていくこととします。

あとのFlutterViewの処理は標準のプラグインの初期化処理らしき実装があるのがわかります。 この辺は自身でプラグインを作成したらわかりそうな気がする・・・!!ので機会を見てプラグイン作成にもチャレンジしてみたいと思います。

this.mFlutterLocalizationChannel = new MethodChannel(this, "flutter/localization", JSONMethodCodec.INSTANCE);
this.mFlutterNavigationChannel = new MethodChannel(this, "flutter/navigation", JSONMethodCodec.INSTANCE);
this.mFlutterKeyEventChannel = new BasicMessageChannel(this, "flutter/keyevent", JSONMessageCodec.INSTANCE);
this.mFlutterLifecycleChannel = new BasicMessageChannel(this, "flutter/lifecycle", StringCodec.INSTANCE);
this.mFlutterSystemChannel = new BasicMessageChannel(this, "flutter/system", JSONMessageCodec.INSTANCE);
this.mFlutterSettingsChannel = new BasicMessageChannel(this, "flutter/settings", JSONMessageCodec.INSTANCE);
PlatformPlugin platformPlugin = new PlatformPlugin(activity);
MethodChannel flutterPlatformChannel = new MethodChannel(this, "flutter/platform", JSONMethodCodec.INSTANCE);

そしてこのflutterViewをactivityでsetContentViewしてsurfaceViewが全面に表示されるようになります。

surfaceCallback

ではここからsurfaceCallbackの処理を追っかけてみたいと思います。いきなりネイティブ インターフェースがが出てきましたね。

this.mSurfaceCallback = new Callback() {
    public void surfaceCreated(SurfaceHolder holder) {
        FlutterView.this.assertAttached();
        FlutterView.nativeSurfaceCreated(FlutterView.this.mNativeView.get(), holder.getSurface());
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        FlutterView.this.assertAttached();
        FlutterView.nativeSurfaceChanged(FlutterView.this.mNativeView.get(), width, height);
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        FlutterView.this.assertAttached();
        FlutterView.nativeSurfaceDestroyed(FlutterView.this.mNativeView.get());
    }
};

nativeSurfaceCreatedの実装はどこにあるのか

FlutterViewに直接定義があったのですが、engine側では以下にあることがわかりました。

engine/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java

これはengineのコードとflutter.jarのコードは若干異なることをしめしていますが、もともとflutter.jarは逆コンパイルしているため、最適化されているのだろうと想定しています。

// engine/shell/platform/android/platform_view_android_jni.cc
{
  .name = "nativeSurfaceCreated",
  .signature = "(JLandroid/view/Surface;)V",
  .fnPtr = reinterpret_cast<void*>(&shell::SurfaceCreated),
},

PlatformView.SurfaceCreated

Androidにはplatform_view_androidiosにはplatform_view_iosがそれぞれplatform_viewを継承している構成になっています。

// engine/shell/platform/android/platform_view_android.cc
void PlatformViewAndroid::NotifyCreated(
    fml::RefPtr<AndroidNativeWindow> native_window) {
  if (android_surface_) {
    InstallFirstFrameCallback();
    android_surface_->SetNativeWindow(native_window);
  }
  PlatformView::NotifyCreated();
}
// engine/shell/common/platform_view.cc
void PlatformView::NotifyCreated() {
  delegate_.OnPlatformViewCreated(CreateRenderingSurface());
}
// engine/shell/common/shell.cc   
void Shell::OnPlatformViewCreated(std::unique_ptr<Surface> surface) {
・・・省略・・・
    if (rasterizer) {
      rasterizer->Setup(std::move(surface));
    }
・・・省略・・・
}

このRasterrizerというクラスでなにかしていそうです。これのincludeを見てみるとskiaで描画している雰囲気がバンバン出ています。

#include "third_party/skia/include/core/SkEncodedImageFormat.h"
#include "third_party/skia/include/core/SkImageEncoder.h"
#include "third_party/skia/include/core/SkPictureRecorder.h"
#include "third_party/skia/include/core/SkSerialProcs.h"
#include "third_party/skia/include/core/SkSurface.h"
#include "third_party/skia/include/core/SkSurfaceCharacterization.h"
#include "third_party/skia/include/utils/SkBase64.h"
・・・省略・・・
// engine/shell/common/rasterizer.cc
void Rasterizer::Setup(std::unique_ptr<Surface> surface) {
  surface_ = std::move(surface);
  compositor_context_->OnGrContextCreated();
}
・・・省略・・・
void Rasterizer::DoDraw(std::unique_ptr<flow::LayerTree> layer_tree) {
・・・省略・・・
bool Rasterizer::DrawToSurface(flow::LayerTree& layer_tree) {
・・・省略・・・

nativeSurfaceChangedを見てみる

nativeSurfaceChangedも同様に処理を追っていきます。先程のようにJNIまで探り当てて、platform_view_android側の処理を見つけ出します。

// engine/shell/platform/android/platform_view_android_jni.cc
static void SurfaceChanged(JNIEnv* env,
    jobject jcaller,
    jlong shell_holder,
    jint width,
    jint height) {
  ANDROID_SHELL_HOLDER->GetPlatformView()->NotifyChanged(
      SkISize::Make(width, height));
}
// engine/shell/platform/android/platform_view_android.cc
void PlatformViewAndroid::NotifyChanged(const SkISize& size) {
  if (!android_surface_) {
    return;
  }
  fml::AutoResetWaitableEvent latch;
  fml::TaskRunner::RunNowOrPostTask(
      task_runners_.GetGPUTaskRunner(),  //
      [&latch, surface = android_surface_.get(), size]() {
        surface->OnScreenSurfaceResize(size);
        latch.Signal();
      });
  latch.Wait();
}

調べていくとsurface_glにたどり着いてしまったのですが、ライフサイクル的にはsurfaceが変更されたときのイベントなのでネイティブ的にはsurfaceの初期化などをしていそうですね。

// engine/shell/platform/android/android_surface_gl.cc
bool AndroidSurfaceGL::OnScreenSurfaceResize(const SkISize& size) const {
  FML_DCHECK(onscreen_context_ && onscreen_context_->IsValid());
  return onscreen_context_->Resize(size);
}

nativeSurfaceDestroyed

こちらも調べていきます。おそらくラスタライザのteardownにつながってるんじゃないかなぁ、という推測をしながら調べてみます。

// engine/shell/platform/android/platform_view_android_jni.cc
static void SurfaceDestroyed(JNIEnv* env, jobject jcaller, jlong shell_holder) {
  ANDROID_SHELL_HOLDER->GetPlatformView()->NotifyDestroyed();
}
// engine/shell/common/platform_view.cc
void PlatformView::NotifyDestroyed() {
  delegate_.OnPlatformViewDestroyed();
}
// engine/shell/common/rasterizer.cc

void Shell::OnPlatformViewDestroyed() {
・・・省略・・・
    if (rasterizer) {
      rasterizer->Teardown();
    }
・・・省略・・・
}

ビンゴですね〜!

おわりに

少しflutter androidがどんな感じで描画しようとしているのか雰囲気がわかったような、わからないような・・・ もう少しsurfacecreatedから実際にdrawが実行されるまでの処理を追いかけて順次記事更新していこうと思います。

(プログラミング不要)Google Home + SwitchBotでエアコンとお風呂のスイッチを入れる

こんにちは、tkyです。

プログラミング要素なしです。Google Home経由でエアコンの操作とお風呂の追い焚きスイッチを入れてみようと思います。 ちなみにGoogle HomeがなくてもSwitchBot Hub PlusとSwitchBotがあれば家の外からスマホ経由でエアコンのスイッチ入れられます。

何作ったの

これができるようにしました。

シナリオ1:
tky「OKグーグル、エアコンつけて」
googlehome「はい、エアコンをONにします」
シナリオ2:
tky「OKグーグル、お風呂つけて」(追い焚きの意味)
googlehome「はい、お風呂をONにします」
サブシナリオ:帰宅途中
tky「(家は寒い/暑いからなぁ・・・)エアコンと、追い焚きをしておこう(ポチポチ」
〜帰宅〜
tky「家あったけ/涼しーー!!!すぐ風呂入れるーーー!!!」

用意するもの

サブシナリオだけであればGoogle Homeは不要です。

Google Home

音声でスイッチを入れるために使用します。厳密に言うとSwitchBot Hub Plusにアクセスするために使用します。

音声操作が不要であれば、「Google Assistantアプリ」または「SwitchBotアプリ」があれば大丈夫です。

SwitchBot Hub Plus

色々できることはあるのですが、以下のことをするために使用します。 - 外から(インターネット経由で)アクセスする - 赤外線コードを発信する(今回の場合エアコンのリモコン) - SwitchBotを操作する

SwitchBot

この子には我が家の追い焚きスイッチを押してもらいます。 SwitchBotと専用アプリはBLE通信で行うため、外に出ているとアプリから直接SwitchBotが操作できません。 そのため、SwitchBot Hub Plusを介して制御することになります。

!!!!注意!!!!

本品を購入する前に本記事のSwitchBotの取り付け例(画像)を見てください。 スイッチの形状や家の構造によって取り付けられないケースもあります。

各アプリはインストールしておきましょう

play.google.com

Google Home

Google Home

  • Google LLC
  • ライフスタイル
  • 無料

  • SwitchBot

play.google.com

SwitchBot

SwitchBot

  • wonderlabs, Incorporated
  • ライフスタイル
  • 無料

エアコン操作

まず、SwitchBot Hub Plusでエアコンの操作からしていきます。 SwitchBot Hub Plusを開封して、電源をつなぎリモコン操作できそうな位置に配置します。

SwitchBot側

続いて、アプリ側です。アプリを開くとすでにデバイスが見つかっている状態になっているかと思われます。 この辺のつなげ方は取扱説明書見たほうが早いです。 『Hub Plus D0』と書かれているところの丸アイコン部分をタップします。

f:id:ticktakclock:20190113185928p:plain:w200

「新しいデバイスを追加する」をタップします。

f:id:ticktakclock:20190113190135p:plain:w200

エアコンを使いたいのでエアコンを選びます。

f:id:ticktakclock:20190113190148p:plain:w200

SwitchBot Hub Plusに向かってエアコンのリモコンの暖房ボタンでも押しましょう。

f:id:ticktakclock:20190113190332p:plain:w200

f:id:ticktakclock:20190113194750p:plain:w200

1つリモコンボタンを送信すると、エアコンがONになるかと思われます。

f:id:ticktakclock:20190113190346p:plain:w200

アプリからリモコン操作してみます。この時点でエアコンがONにならない場合、正しく設定できていないか、非対応(さすがに少ないでしょうが・・・)な可能性が出てきます。

単純にアプリから操作するだけで良い場合は、この状態で家の外からエアコンONできますのでここで完結です。追い焚き操作に移りましょう。

GoogleHomeから操作したい場合は次の手順に進んでください。

Google Home

Google Homeアプリを開くとはじめにGoogle Homeバイスとの接続を行う必要があります。

こちらは画面のUIに沿ってボタンを押していくだけでセットアップが完了しますので、テンポよく設定していきます。

続いて、SwitchBotとリンクしていきます。設定からデバイスを追加(プラスマーク)、を選びます。

f:id:ticktakclock:20190113191532p:plain:w200 f:id:ticktakclock:20190113191911p:plain:w200 f:id:ticktakclock:20190113191926p:plain:w200

サービス一覧から『SwitchBot』を探し出します。検索するとすぐに見つかります。

f:id:ticktakclock:20190113192049p:plain:w200

ログインを求められる場合はログインしましょう。

f:id:ticktakclock:20190113192117p:plain:w200

そうすると、今SwitchBotで使用できるデバイス一覧が表示され、どの機器をGoogleHomeとリンクするか尋ねてきます。まぁSwitchBot Hubしかないので1つ選びましょう。

f:id:ticktakclock:20190113192148p:plain:w200

ここで設定する部屋の名前は「リビング」、デバイス名を「エアコン」としましょう。 これで完了です。 tky「OKグーグル、エアコンつけて」 Google Home「はい、リビングエアコンをONにします」 が実現できるようになります。

お風呂の追い焚き操作

SwitchBot側

いよいよ追い焚きを操作してみたいと思います。こちらはSwitchBotデバイスを使用します。

まず、デバイスの電源を入れましょう。その後、SwitchBotアプリで見てみます。

いましたね。画面に表示されているデバイスのアイコン部分を押してみましょう。 デバイスがウィーンと動きましたか?動作確認OKです。 また、Google Homeから操作を受け付けるために、「Hub Plus D0」=>「SwitchBot」=>「お風呂(デバイス名です)」からGoogle Homeからのアクセス許可をつけておきましょう。

f:id:ticktakclock:20190113193639p:plain:w200 f:id:ticktakclock:20190113193654p:plain:w200

これをこのように取り付けます。裏はシールになっているため、貼り付けられるだけの面積が必要です。 3.5cm x 4.5cmくらいの領域があれば十分に貼り付けられるでしょう。取り付け例です。

f:id:ticktakclock:20190113194714p:plain:w200

Google Home

すでに、SwitchBotサービスをリンクしているので使えるのかな?と思いきや実はリンクした時点のデバイスだけが使えるようで、その後追加したデバイスを使用できるようにするには、一度リンク削除して、再度リンクする必要があるみたいです。(微妙・・・) 仕方なし、前回同様、一度リンクを削除して、再度サービス一覧から「SwitchBot」を選び直します。 そうすると、今SwitchBotで使用できるデバイス一覧が表示されます。SwitchBot Hub PlusとSwitchBot両方あることが確認できます。 もちろん両方選択します。

追い焚きデバイスは「お風呂」と命名しました。これで完了です。

tky「OKグーグル、お風呂つけて」 Google Home「はい、お風呂をONにします(ウィ〜ン ポチ」 が実現できるようになります。

快適に利用するために

これはSwitchBotアプリだけでしか使えないのですが、スケジュール機能があります。 簡単に言うと毎朝7:00にエアコンをONにしたり、毎晩19:00に追い焚きをONにしたり、そういうことができるようになります。 控えめに言って最高だと思います。

おわりに

いかがでしたか?プログラミング要素なくても案外家電操作できる良い時代になりましたね。 リモコンについては汎用性が高く、TV、扇風機、LED照明、(そこまでしなくても・・・wという感はありますが)色々と楽しめそうです!