kun432's blog

Alexaなどスマートスピーカーの話題中心に、Voiceflowの日本語情報を発信してます。たまにAWSやkubernetesなど。

〜スマートスピーカーやVoiceflowの記事は右メニューのカテゴリからどうぞ。〜

Raspberry Piで観葉植物の監視をする②

f:id:kun432:20220219171625p:plain

前回の続き。

土壌水分センサーがなんとなくうまく動いていないさそう。ということで、ちょっとコードを追っかけてみた。

公式サンプルコード

Python全然わからないけど、

  • get_adcで各センサの電圧値を取得
  • valmapに電圧値を渡してパーセンテージ変換

してるように見える。

def valmap(value, istart, istop, ostart, ostop):
    value = ostart + (ostop - ostart) * ((value - istart) / (istop - istart))
    if value > ostop:
       value = ostop
    return value
                moisture1 = round(valmap(sensor1, 5, 3.5, 0, 100), 0)

5Vなら0%、3.5Vなら100%、というように考えて、センサー値のパーセンテージを算出してるみたいなんだけど、この53.5はどこから算出された数字なのかな?

soil-moistore-sensors.pyから余計なものを取っ払ってセンサーの電圧値だけを表示するコードを書いてみた。

import signal
import sys
import time
import spidev

spi_ch = 0

# Enable SPI
spi = spidev.SpiDev(0, spi_ch)
spi.max_speed_hz = 1200000

def close(signal, frame):
    sys.exit(0)

signal.signal(signal.SIGINT, close)

def get_adc(channel):

    # Make sure ADC channel is 0 or 1
    if channel != 0:
        channel = 1

    # Construct SPI message
    #  First bit (Start): Logic high (1)
    #  Second bit (SGL/DIFF): 1 to select single mode
    #  Third bit (ODD/SIGN): Select channel (0 or 1)
    #  Fourth bit (MSFB): 0 for LSB first
    #  Next 12 bits: 0 (don't care)
    msg = 0b11
    msg = ((msg << 1) + channel) << 5
    msg = [msg, 0b00000000]
    reply = spi.xfer2(msg)

    # Construct single integer out of the reply (2 bytes)
    adc = 0
    for n in reply:
        adc = (adc << 8) + n

    # Last bit (0) is not part of ADC value, shift to remove it
    adc = adc >> 1

    # Calculate voltage form ADC value
    # considering the soil moisture sensor is working at 5V
    voltage = (5 * adc) / 1024

    return voltage

if __name__ == '__main__':
    # Report the channel 0 and channel 1 voltages to the terminal
    try:
        while True:
            adc_0 = get_adc(0)
            adc_1 = get_adc(1)
            sensor1 = round(adc_0, 2)
            sensor2 = round(adc_1, 2)
            print("Soil Moisture Sensor 1:", sensor1, " Soil Moisture Sensor 2:", sensor2)
            time.sleep(0.5)
    except: KeyboardInterrupt

実行してみた。Sensor 1の方。まず水につけない場合。

$ python test-soil-moistore-sensors.py
Soil Moisture Sensor 1: 3.58  Soil Moisture Sensor 2: 0.0
Soil Moisture Sensor 1: 3.57  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 3.57  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 3.57  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 3.57  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 3.57  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 3.57  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 3.58  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 3.58  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 3.57  Soil Moisture Sensor 2: 0.01
(...snip...)

水につけた場合。

$ python test-soil-moistore-sensors.py
Soil Moisture Sensor 1: 1.9  Soil Moisture Sensor 2: 0.0
Soil Moisture Sensor 1: 1.9  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 1.9  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 1.9  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 1.9  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 1.89  Soil Moisture Sensor 2: 0.02
Soil Moisture Sensor 1: 1.9  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 1.9  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 1.9  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 1.89  Soil Moisture Sensor 2: 0.01
Soil Moisture Sensor 1: 1.9  Soil Moisture Sensor 2: 0.0
Soil Moisture Sensor 1: 1.9  Soil Moisture Sensor 2: 0.0
(...snip...)

REPLでvalmapを実行してみる。

$ python
Python 3.9.2 (default, Mar 12 2021, 04:06:34)
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def valmap(value, istart, istop, ostart, ostop):
...     value = ostart + (ostop - ostart) * ((value - istart) / (istop - istart))
...     if value > ostop:
...        value = ostop
...     return value
...
>>> print(round(valmap(3.67, 5, 3.5, 0, 100),0))
95.0
>>> print(round(valmap(1.9, 5, 3.5, 0, 100),0))
100

うーん、この計算だと、水分0のときでも95%ぐらいになってしまうよね・・・

さすがにget_adcの中を読み解くのは電子工作初心者には厳しいし、何が間違っているのかはわからないけど、とりあえず実際の値を踏まえて算出するように書き換えてみる。

import signal
import sys
import time
import spidev
import RPi.GPIO as GPIO

# Pin 15 on Raspberry Pi corresponds to GPIO 22
LED1 = 15
# Pin 16 on Raspberry Pi corresponds to GPIO 23
LED2 = 16

MAX=3.57
MIN=1.91

spi_ch = 0

# Enable SPI
spi = spidev.SpiDev(0, spi_ch)
spi.max_speed_hz = 1200000

# to use Raspberry Pi board pin numbers
GPIO.setmode(GPIO.BOARD)
GPIO.setwarnings(False)

# set up GPIO output channel
GPIO.setup(LED1, GPIO.OUT)
GPIO.setup(LED2, GPIO.OUT)

def close(signal, frame):
    GPIO.output(LED1, 0)
    GPIO.output(LED2, 0)
    sys.exit(0)

signal.signal(signal.SIGINT, close)

def valmap(value, istart, istop, ostart, ostop):
    value = ostart + (ostop - ostart) * ((value - istart) / (istop - istart))
    if value > ostop:
       value = ostop
    if value < ostart:
       value = ostart
    return value

def get_adc(channel):

    # Make sure ADC channel is 0 or 1
    if channel != 0:
        channel = 1

    # Construct SPI message
    #  First bit (Start): Logic high (1)
    #  Second bit (SGL/DIFF): 1 to select single mode
    #  Third bit (ODD/SIGN): Select channel (0 or 1)
    #  Fourth bit (MSFB): 0 for LSB first
    #  Next 12 bits: 0 (don't care)
    msg = 0b11
    msg = ((msg << 1) + channel) << 5
    msg = [msg, 0b00000000]
    reply = spi.xfer2(msg)

    # Construct single integer out of the reply (2 bytes)
    adc = 0
    for n in reply:
        adc = (adc << 8) + n

    # Last bit (0) is not part of ADC value, shift to remove it
    adc = adc >> 1

    # Calculate voltage form ADC value
    # considering the soil moisture sensor is working at 5V
    voltage = (5 * adc) / 1024

    return voltage

if __name__ == '__main__':
    # Report the channel 0 and channel 1 voltages to the terminal
    try:
        while True:
            adc_0 = get_adc(0)
            adc_1 = get_adc(1)
            sensor1 = round(adc_0, 2)
            if sensor1 < 0.5:
                moisture1 = 0
            else:
                moisture1 = round(valmap(sensor1, MAX, MIN, 0, 100), 0)
            sensor2 = round(adc_1, 2)
            if sensor2 < 0.5:
                moisture2 = 0
            else:
                moisture2 = round(valmap(sensor2, 5, 3.5, 0, 100), 0)
            print(f"Soil Moisture Sensor 1: {moisture1}% ({sensor1}) Soil Moisture Sensor 2: {moisture2}% ({sensor2})")
            if moisture1 < 40 or moisture2 < 40:
                GPIO.output(LED1, 1)
                GPIO.output(LED2, 0)
            else:
                GPIO.output(LED1, 0)
                GPIO.output(LED2, 1)
            time.sleep(0.5)
    finally:
        GPIO.cleanup()

テストで算出した電圧の最大値・最小値を定義しておいて、それに比例したパーセンテージを出すようにしてみた。最大値・最小値の範囲を超えるものはカットしてる。ついでに電圧も表示するようにした。

水につけていない状態。

$ python soil-moistore-sensors.py
Soil Moisture Sensor 1: 1.0% (3.56) Soil Moisture Sensor 2: 0% (0.0)
Soil Moisture Sensor 1: 1.0% (3.56) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 1.0% (3.56) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 1.0% (3.56) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 1.0% (3.56) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 1.0% (3.56) Soil Moisture Sensor 2: 0% (0.0)
Soil Moisture Sensor 1: 1.0% (3.56) Soil Moisture Sensor 2: 0% (0.0)
Soil Moisture Sensor 1: 1.0% (3.56) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 1.0% (3.56) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 1.0% (3.56) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 1.0% (3.56) Soil Moisture Sensor 2: 0% (0.01)
$ python soil-moistore-sensors.py
Soil Moisture Sensor 1: 99.0% (1.93) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 99.0% (1.93) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 99.0% (1.93) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 99.0% (1.93) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 99.0% (1.93) Soil Moisture Sensor 2: 0% (0.0)
Soil Moisture Sensor 1: 99.0% (1.93) Soil Moisture Sensor 2: 0% (0.0)
Soil Moisture Sensor 1: 99.0% (1.93) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 98.0% (1.94) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 99.0% (1.93) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 99.0% (1.93) Soil Moisture Sensor 2: 0% (0.01)
Soil Moisture Sensor 1: 99.0% (1.93) Soil Moisture Sensor 2: 0% (0.01)

もうちょっと細かく詰めないといけないと思うけど、とりあえずそれっぽい数字にはなったんじゃないかなー。センサーの値があんまり安定しないけど、まあそんなシビアなものでもないのでこれで十分かなという気がしてる。

Pythonほとんど書かないけど、見様見真似でなんとかなるもんだ。

参考

とても参考になりました。

Raspberry Piで観葉植物の監視をする①

f:id:kun432:20220219171625p:plain

以前に空気コンディションモニタを作った際に使った「Anavi」のuHAT、他にも色々あるみたいです。

その中で、最近ちょっと個人的にハマっている観葉植物向けに、必要なセンサーを組み合わせた「Gardening uHAT」があったので購入してみました。

今回はとりあえず各センサーの使い方まで。

目次

必要なもの

Raspberry Pi

f:id:kun432:20220326185807j:plain

実は在庫がどこにもなくて、これが一番ハードルが高いかも。以前買ったままで使ってなかったRaspberry Pi Zero Wがたまたまあって、uHATをつなげるにはGPIOピンが必要、ということで自分でハンダ付けしました。ちなみに人生ほぼ初のハンダ付けで、いろいろわかってなくてちょっと苦労しました。

今にして思えばなぜWHにしなかったのかとほんと後悔・・・でもおかげで学べたし、なんとか動いてよかった。

Anavi Gardening uHAT

f:id:kun432:20220326190032j:plain

今回のキモ。3パターンのパッケージがありますが、Developer Kitにしました。Developer Kitだと以下のセンサーが入ってます。

  • 土壌水分センサー x 2
  • 温湿度センサー(HTU21D)
  • 光センサー(BH1750)
  • 防水温度センサー(DS18B20)

全部つなぐとこんな感じです。

f:id:kun432:20220326202343j:plain

あと、本体にLEDが2つあります。

f:id:kun432:20220326203910j:plain

観葉植物

f:id:kun432:20220326181902j:plain

近所で見つけて買ってきた緋牡丹サボテンです。オレンジでほんとかわいい。いつか株分けとかしてみたい。

手順

OSイメージの書き込み

ZERO WなのでOSは"Raspberry Pi OS 32-bit"です。あと、今まで全然気づかなかったのだけど、Raspberry Pi ImagerでOS初期設定とか出来ちゃうんですね。

f:id:kun432:20220326203211j:plain

f:id:kun432:20220326203238j:plain

とりあえず、

  • ホスト名
  • SSH有効化
    • ユーザ名・パスワードの設定
  • WiFi設定
  • ロケール設定

あたりを設定してSDメモリに書き込みます。これならモニタもキーボードもいらないし、ホント便利ですね。

セットアップ

RPiを起動して、あとはマニュアルに従ってやっていくだけですが、一応一通りやってみます。

OSアップデート

$ sudo apt update
$ sudo apt upgrade

パッケージをインストール。マニュアルの下の方にもパッケージ追加する箇所がありますがここでまとめて。wiringpiはとりあえず不要。wiringpiもまだ必要ですね

$ sudo apt install -y git i2c-tools vim python3-rpi.gpio python3-dev
$ cd /tmp
$ wget https://project-downloads.drogon.net/wiringpi-latest.deb
$sudo dpkg -i wiringpi-latest.deb

raspi-configで各種インタフェースの有効化

$ sudo raspi-config

"Interfacing Options"を選択

f:id:kun432:20220326204459p:plain

以下のインタフェースを選択して、それぞれ有効化していきます。

  • SPI
  • I2C
  • 1-Wire

f:id:kun432:20220326204558p:plain

f:id:kun432:20220326204614p:plain

f:id:kun432:20220326204720p:plain

全部のインタフェースを有効にしたら、raspi-configを終了して、一旦再起動します。

f:id:kun432:20220326204730p:plain

各センサーの使い方

ここから各センサーの使い方を見ていきます。

公式のサンプルがあるのでまずはそちらをgit cloneします。

$ cd ~
$ git clone https://github.com/AnaviTechnology/anavi-examples.git
土壌水分センサー

公式のサンプルにPythonのコードが含まれています。

$ cd ~/anavi-examples/anavi-gardening-uhat/soil-moistore-sensors/python/
$ python3 soil-moistore-sensors.py
Soil Moisture Sensor 1: 95.0 % Soil Moisture Sensor 2: 0 %
Soil Moisture Sensor 1: 95.0 % Soil Moisture Sensor 2: 0 %
Soil Moisture Sensor 1: 95.0 % Soil Moisture Sensor 2: 0 %
...snip...

今回はセンサー1つだけなので、Sensor 1のほうですね。水につけてなくても95%もあるの?とりあえず水につけてみる。

...snip...
Soil Moisture Sensor 1: 100 % Soil Moisture Sensor 2: 0 %
Soil Moisture Sensor 1: 100 % Soil Moisture Sensor 2: 0 %
Soil Moisture Sensor 1: 100 % Soil Moisture Sensor 2: 0 %
...snip...

一応100%にはなる。これはちょっとコードを読んでみないといけないかも。とりあえずセンサーとしては動いていることは確認できたので良しとする。

温湿度センサー(HTU21D)

こちらも公式のサンプルを。Infrared pHATと同じですね。

$ cd ~/anavi-examples/sensors/HTU21D/c/
$ make
gcc -c -o HTU21D.o HTU21D.c -I.
gcc -c -o HTU21D-example.o HTU21D-example.c -I.
gcc -o HTU21D HTU21D.o HTU21D-example.o -I. -lwiringPi
$ ./HTU21D
HTU21D Sensor Module
25.29C
40.23%rh

取れていますね。

光センサー(BH1750)

こちらも公式のサンプルを。Infrared pHATと同じ。

$ cd ~/anavi-examples/sensors/BH1750/c/
$ make
$ ./BH1750
BH1750 Sensor Module
Light: 76 Lux
防水温度センサー(DS18B20)

こちらはサンプル不要です。

$ ls /sys/bus/w1/devices/
28-03089779830a  w1_bus_master1

$ cat /sys/bus/w1/devices/*/w1_slave
81 01 55 05 7f a5 a5 66 a3 : crc=a3 YES
81 01 55 05 7f a5 a5 66 a3 t=24062

t=の後の数字が温度です。1000で割ると摂氏温度になるようなので、上の例だと24℃ってことですね。

LED

最後にセンサーではないですが、LEDの操作。サンプルがあります。

$ cd ~/anavi-examples/anavi-gardening-uhat/led/python
$ python ./blink.py

f:id:kun432:20220326213801g:plain

まとめ

これで各センサーやLEDが操作できるところまできました。次回は実際に植物の監視的なことをやっていきます。

AlexaスキルのテストをBotiumでやってみた①

f:id:kun432:20210131175240p:plain

Alexaスキルのテスト、どうしてますか?色々やり方はあると思います。

今回はVoiceflowのcommunity forumで知った「Botium」を使ってみたいと思います。

目次

Botiumとは?

Botiumはチャットボットのテスト自動化プラットフォームです。

無料プランはありますが、E2Eや音声テストなどは有償プランじゃないと使えません。有償プランの価格は載ってないですけど、ちょっと調べてみた感じ、そこそこかかるみたいですね・・・・

ただし、ツールライブラリだけを使うなら無料かつOSSになっているので、今回はそちらでやってみましょう。

Botiumのツール

Botiumのツールは以下のようになっています。

  • Botium Core
    • Botiumのコアライブラリ
  • Botium CLI
    • Botium Coreを使ったCLI。テストもできます。
  • Botium Bindings
    • Botium Coreとテストランナーをつなげるもの。テストランナーにはMocha/Jasmin/Jestなどが使える。Botium CLIを使わずに、自前でテストランナーをつかいた場合はこちらを選ぶようです。
  • Botium Box
    • BotiumのCLIやライブラリを制御するためのGUIプラットフォーム。有償。

あと、上記には書いてないですが、以下もあります。

  • Botium Connector
    • チャットボットやフレームワーク、プラットフォームなどとつなげるためのもの。Alexaの場合はbotium-connector-alexa-smapiを使ってつなげる。

今回使うのは以下のみです。

  • botium-cli(botium core含む)
  • botium-connector

Botiumのインストールと基本的な使い方

botium-cliをインストールします。

$ npm install -g botium-cli

テストプロジェクトを初期化します。ディレクトリを作成してそこで行うのが良いでしょう。

$ mkdir alexa-test-sample && cd alexa-test-sample

$ botium-cli init
Botium Configuration File written to "./botium.json".
Botium Convo File written to "/path-to-somewhere/alexa-test-sample/give_me_a_picture.convo.txt".
Botium initialization ready. You should now run "botium-cli run --verbose --convos ." to verify.

botium-cli initを実行するとテストの雛形が作成されます。

$ tree
.
├── botium.json
└── give_me_a_picture.convo.txt

botium.jsonにテストプロジェクトの設定を記載します。デフォルトで用意されているテスト(botium-connector-echo)の設定が行われているようです。

{
  "botium": {
    "Capabilities": {
      "PROJECTNAME": "My Botium Project",
      "CONTAINERMODE": "echo"
    },
    "Sources": {},
    "Envs": {}
  }
}

.convo.txtがテストシナリオを記載するファイルになります。botium-cliは実行時に現在のディレクトリから*.convo.txtを探してテストを行うようになっているようですね。こちらもデフォルトで用意されているテスト用のシナリオが記載されているようですが、見て分かる通り、mochaやjestなどと違って、非常に簡易かつわかりやすい感じの記述になっていますね。

give me picture

#me
Hello, Bot!

#bot
You said: Hello, Bot!

#me
give me a picture

#bot
Here is a picture
MEDIA logo.png

では早速実行してみましょう。

$ botium-cli run --verbose --convos
  botium-cli Using Botium configuration file ./botium.json +0ms
  botium-cli-run command options: {
  botium-cli-run   _: [ 'run' ],
  botium-cli-run   verbose: true,
  botium-cli-run   v: true,
  botium-cli-run   convos: [ '.' ],
  botium-cli-run   C: [ '.' ],
  botium-cli-run   config: './botium.json',
  botium-cli-run   c: './botium.json',
  botium-cli-run   output: 'spec',
  botium-cli-run   testsuitename: 'Botium Test-Suite',
  botium-cli-run   n: 'Botium Test-Suite',
  botium-cli-run   expandutterances: false,
  botium-cli-run   expandscriptingmemory: false,
  botium-cli-run   timeout: 60,
  botium-cli-run   '$0': '../../.volta/tools/image/packages/botium-cli/bin/botium-cli'
  botium-cli-run } +0ms
  botium-cli-run Mocha Reporter "spec", options: undefined +0ms
  botium-core-BotDriver Loaded Botium configuration files /path-in-somewhere/alexa-test/botium.json +0ms
  botium-core-ScriptingProvider ReadConvosFromDirectory(.) found filenames: botium.json,give_me_a_picture.convo.txt +0ms
  botium-core-ScriptingProvider ReadConvosFromDirectory(.) found convos:
  botium-core-ScriptingProvider  1 give me picture ({ convoDir: '.', filename: 'give_me_a_picture.convo.txt' }): Line 3: #me - Hello, Bot! | Line 6: #bot - You said: Hello, Bot! | Line 9: #me - give me a picture | Line 12: #bot - Here is a picture MEDIA(logo.png) +4ms
  botium-core-ScriptingProvider ReadConvosFromDirectory(.) found utterances:
  botium-core-ScriptingProvider  none +1ms
  botium-core-ScriptingProvider ReadConvosFromDirectory(.) found partial convos:
  botium-core-ScriptingProvider  none +0ms
  botium-core-ScriptingProvider ReadConvosFromDirectory(.) scripting memories:
  botium-core-ScriptingProvider  none +0ms
  botium-cli-run ready reading convos (1), expanding convos ... +238ms
  botium-core-ScriptingProvider ExpandConvos - Using utterances expansion mode: all +0ms
  botium-cli-run ready expanding convos and utterances, number of test cases: (1). +23ms
  botium-cli-run adding test case give me picture (from: { convoDir: '.', filename: 'give_me_a_picture.convo.txt' }) +2ms


  Botium Test-Suite
  botium-core-BotDriver Build - Botium Core Version: 1.11.15 +280ms
  botium-core-BotDriver Build - Capabilites: {
  botium-core-BotDriver   PROJECTNAME: 'My Botium Project',
  botium-core-BotDriver   TESTSESSIONNAME: 'Botium Test Session',
  botium-core-BotDriver   TESTCASENAME: 'Botium Test Case',
  botium-core-BotDriver   TEMPDIR: 'botiumwork',
  botium-core-BotDriver   CLEANUPTEMPDIR: true,
  botium-core-BotDriver   WAITFORBOTTIMEOUT: 10000,
  botium-core-BotDriver   SIMULATE_WRITING_SPEED: false,
  botium-core-BotDriver   SIMPLEREST_PING_RETRIES: 6,
  botium-core-BotDriver   SIMPLEREST_PING_TIMEOUT: 10000,
  botium-core-BotDriver   SIMPLEREST_PING_VERB: 'GET',
  botium-core-BotDriver   SIMPLEREST_PING_UPDATE_CONTEXT: true,
  botium-core-BotDriver   SIMPLEREST_PING_PROCESS_RESPONSE: false,
  botium-core-BotDriver   SIMPLEREST_INIT_PROCESS_RESPONSE: false,
  botium-core-BotDriver   SIMPLEREST_STOP_RETRIES: 6,
  botium-core-BotDriver   SIMPLEREST_STOP_TIMEOUT: 10000,
  botium-core-BotDriver   SIMPLEREST_STOP_VERB: 'GET',
  botium-core-BotDriver   SIMPLEREST_START_RETRIES: 6,
  botium-core-BotDriver   SIMPLEREST_START_TIMEOUT: 10000,
  botium-core-BotDriver   SIMPLEREST_START_UPDATE_CONTEXT: true,
  botium-core-BotDriver   SIMPLEREST_START_PROCESS_RESPONSE: true,
  botium-core-BotDriver   SIMPLEREST_START_VERB: 'GET',
  botium-core-BotDriver   SIMPLEREST_POLL_VERB: 'GET',
  botium-core-BotDriver   SIMPLEREST_POLL_INTERVAL: 1000,
  botium-core-BotDriver   SIMPLEREST_POLL_UPDATE_CONTEXT: true,
  botium-core-BotDriver   SIMPLEREST_METHOD: 'GET',
  botium-core-BotDriver   SIMPLEREST_IGNORE_EMPTY: true,
  botium-core-BotDriver   SIMPLEREST_TIMEOUT: 10000,
  botium-core-BotDriver   SIMPLEREST_EXTRA_OPTIONS: {},
  botium-core-BotDriver   SIMPLEREST_STRICT_SSL: true,
  botium-core-BotDriver   SIMPLEREST_INBOUND_UPDATE_CONTEXT: true,
  botium-core-BotDriver   SIMPLEREST_CONTEXT_MERGE_OR_REPLACE: 'MERGE',
  botium-core-BotDriver   SCRIPTING_TXT_EOL: '\n',
  botium-core-BotDriver   SCRIPTING_XLSX_EOL_WRITE: '\r\n',
  botium-core-BotDriver   SCRIPTING_XLSX_HASHEADERS: true,
  botium-core-BotDriver   SCRIPTING_CSV_SKIP_HEADER: true,
  botium-core-BotDriver   SCRIPTING_CSV_QUOTE: '"',
  botium-core-BotDriver   SCRIPTING_CSV_ESCAPE: '"',
  botium-core-BotDriver   SCRIPTING_NORMALIZE_TEXT: true,
  botium-core-BotDriver   SCRIPTING_ENABLE_MEMORY: false,
  botium-core-BotDriver   SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS: false,
  botium-core-BotDriver   SCRIPTING_MATCHING_MODE: 'wildcardIgnoreCase',
  botium-core-BotDriver   SCRIPTING_UTTEXPANSION_MODE: 'all',
  botium-core-BotDriver   SCRIPTING_UTTEXPANSION_RANDOM_COUNT: 1,
  botium-core-BotDriver   SCRIPTING_UTTEXPANSION_NAMING_MODE: 'justLineTag',
  botium-core-BotDriver   SCRIPTING_UTTEXPANSION_NAMING_UTTERANCE_MAX: '16',
  botium-core-BotDriver   SCRIPTING_MEMORYEXPANSION_KEEP_ORIG: false,
  botium-core-BotDriver   SCRIPTING_FORCE_BOT_CONSUMED: false,
  botium-core-BotDriver   ASSERTERS: [],
  botium-core-BotDriver   LOGIC_HOOKS: [],
  botium-core-BotDriver   USER_INPUTS: [],
  botium-core-BotDriver   SECURITY_ALLOW_UNSAFE: true,
  botium-core-BotDriver   CONTAINERMODE: 'echo',
  botium-core-BotDriver   CONFIG: './botium.json'
  botium-core-BotDriver } +0ms
  botium-core-BotDriver Build - Sources : { LOCALPATH: '.', GITPATH: 'git', GITBRANCH: 'master', GITDIR: '.' } +1ms
  botium-core-BotDriver Build - Envs : { IS_BOTIUM_CONTAINER: true } +0ms
  botium-connector-PluginConnectorContainer-helper Botium plugin botium-connector-echo loaded. Plugin version is 0.0.16 +0ms
  botium-cli-run running testcase give me picture +56ms
  botium-core-Convo give me picture/Line 3: user says (cleaned by binary and base64 data and sourceData) {
  botium-core-Convo   "sender": "me",
  botium-core-Convo   "channel": null,
  botium-core-Convo   "not": false,
  botium-core-Convo   "optional": false,
  botium-core-Convo   "messageText": "Hello, Bot!",
  botium-core-Convo   "media": null,
  botium-core-Convo   "buttons": null,
  botium-core-Convo   "cards": null,
  botium-core-Convo   "forms": null,
  botium-core-Convo   "attachments": null,
  botium-core-Convo   "asserters": [],
  botium-core-Convo   "userInputs": [],
  botium-core-Convo   "logicHooks": []
  botium-core-Convo } +0ms
  botium-connector-echo UserSays called, echo back +0ms
  botium-core-Convo give me picture wait for bot  +1ms
  botium-core-Convo give me picture: bot says (cleaned by binary and base64 data and sourceData) {
  botium-core-Convo   "sender": "bot",
  botium-core-Convo   "messageText": "You said: Hello, Bot!",
  botium-core-Convo   "channel": "default"
  botium-core-Convo } +1ms
  botium-core-ScriptingMemory fill start: {} +0ms
  botium-core-ScriptingProvider assertBotResponse give me picture/Line 6 (Line 3: #me - Hello, Bot!) BOT: You said: Hello, Bot! = You said: Hello, Bot! ... +86ms
  botium-core-Convo give me picture/Line 9: user says (cleaned by binary and base64 data and sourceData) {
  botium-core-Convo   "sender": "me",
  botium-core-Convo   "channel": null,
  botium-core-Convo   "not": false,
  botium-core-Convo   "optional": false,
  botium-core-Convo   "messageText": "give me a picture",
  botium-core-Convo   "media": null,
  botium-core-Convo   "buttons": null,
  botium-core-Convo   "cards": null,
  botium-core-Convo   "forms": null,
  botium-core-Convo   "attachments": null,
  botium-core-Convo   "asserters": [],
  botium-core-Convo   "userInputs": [],
  botium-core-Convo   "logicHooks": []
  botium-core-Convo } +2ms
  botium-connector-echo UserSays called, echo back +3ms
  botium-core-Convo give me picture wait for bot  +0ms
  botium-core-Convo give me picture: bot says (cleaned by binary and base64 data and sourceData) {
  botium-core-Convo   "sender": "bot",
  botium-core-Convo   "messageText": "Here is a picture",
  botium-core-Convo   "media": [
  botium-core-Convo     {
  botium-core-Convo       "altText": "Botium Logo",
  botium-core-Convo       "mediaUri": "https://www.botium.ai/wp-content/uploads/2020/03/logo.png"
  botium-core-Convo     }
  botium-core-Convo   ],
  botium-core-Convo   "nlp": {
  botium-core-Convo     "intent": {
  botium-core-Convo       "name": "picture",
  botium-core-Convo       "confidence": 0.8
  botium-core-Convo     }
  botium-core-Convo   },
  botium-core-Convo   "channel": "default"
  botium-core-Convo } +1ms
  botium-core-ScriptingMemory fill start: {} +3ms
  botium-core-ScriptingProvider assertBotResponse give me picture/Line 12 (Line 9: #me - give me a picture) BOT: Here is a picture = Here is a picture ... +3ms
  botium-cli-run give me picture ready, calling done function. +10ms
    ✔ give me picture
  botium-connector-BaseContainer Cleanup rimrafing temp dir /Users/kun432/repository/alexa-test/botiumwork/My Botium Project 20220321 223340 fLHQW +0ms


  1 passing (54ms)

いろいろ出力されますが、最後に1 passingと表示されれば、convo.txtのシナリオ通りにボットとの会話が行われたということになります。

これでサンプルのテスト雛形は不要なのですべて削除してください。

$ rm -rf *

Alexaスキル向けテストプロジェクトの作成

Alexa用のbotium connectorをインストールします。

$ npm install -g botium-connector-alexa-smapi

Alexaスキル向けテストプロジェクトの初期化を行います。

$ botium-connector-alexa-smapi-cli init

ここから対話形式で設定を行います。画面の説明に従って設定を行えばOKです。順に行きましょう。

This wizard will guide you through the Botium Connector setup. Please follow the instructions.
It involves Copy&Paste from a web browser to this terminal window.



######## Step 1/3 - Create Amazon Security Profile ########
 1. Go to this url: https://developer.amazon.com/home.html
 2. Open "Settings" / "Security Profiles" and create a new profile or select an existing one
 3. Add this url to the "Allowed Return URLs": https://s3.amazonaws.com/ask-cli/response_parser.html


Copy & Paste the "Client-ID" here:

ターミナルを一旦このままにしておいて、https://developer.amazon.com/home.htmlにアクセス、ログインして、「設定」→「セキュリティプロファイル」と進み、既存のセキュリティプロファイルを選択するか、セキュリティプロファイルを新規作成します。ここでは新規に作成してみましょう。

f:id:kun432:20220321225052p:plain

f:id:kun432:20220321225100p:plain

f:id:kun432:20220321225109p:plain

セキュリティプロファイル名と説明を適当に記載して、保存します。

f:id:kun432:20220321225336p:plain

セキュリティプロファイルが作成されたら、「ウェブ設定」のタブをクリックします。

f:id:kun432:20220321225831p:plain

右下の編集をクリックします。

f:id:kun432:20220321225959p:plain

「許可された返信URL 」に先ほどターミナルに表示されていたURL(https://s3.amazonaws.com/ask-cli/response_parser.html)を入力して、保存します。

f:id:kun432:20220321230045p:plain

次に、セキュリティプロファイルのウェブ設定で表示されている「クライアントID」をコピーして、

f:id:kun432:20220321230443p:plain

ターミナルに貼り付けてENTER。

Copy & Paste the "Client-ID" here: amzn1.application-oa2-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

次にクライアントシークレットについて聞かれます。

Copy & Paste the "Client-Secret" here:

「シークレットを表示」をクリック

f:id:kun432:20220321231139p:plain

表示された「クライアントシークレット」をコピーして、

f:id:kun432:20220321231001p:plain

ターミナルに貼り付けてENTER。

Copy & Paste the "Client-Secret" here: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

次に表示されているURLにアクセスします。

######## Step 2/3 - Get Amazon Authorization Code ########
 1. Paste the following url to your browser and follow the instructions:
    https://www.amazon.com/ap/oa?response_type=code&client_id=amzn1.application-oa2-client.(...snip...)

Copy&Paste the Authorization Code you received:

Alexa開発者アカウント(amazon.co.jpアカウント)でログインします。

f:id:kun432:20220321231900p:plain

権限を聞かれるので許可します。

f:id:kun432:20220321232021p:plain

認証コードが生成されますので、これをコピーして、

f:id:kun432:20220321232030p:plain

ターミナルに貼り付けてENTER。

Copy&Paste the Authorization Code you received: XXXXXXXXXXXXXXXXX

Alexa開発者コンソール上で作成しているスキルが一覧表示されますので、テストを行うスキルの番号を入力します。今回はテスト用に用意したスキルを選択しました。

######## Step 3/3 - Selecting vendor id and skill ########


Using vendor id "XXXXXXXXXXXXXX" (hoge) for your account


Found 50 skills for your account
 1: "テストスキル1" (amzn1.ask.skill.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX1)
 2: "テストスキル2" (amzn1.ask.skill.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX2)
...snip...
 19: "トリップアドバイザー" (amzn1.ask.skill.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX3)
...snip...
Enter Number of skill to use: 19

以下のように表示されればOKです。入力した情報をもとにbotium.jsonが生成されます。

######## Ready - Creating botium.json ########
Done.

中身を少し見てみましょう。

{
  "botium": {
    "Capabilities": {
      "CONTAINERMODE": "alexa-smapi",
      "ALEXA_SMAPI_REFRESHTOKEN": "XXX(...snip...)XXX",
      "ALEXA_SMAPI_CLIENTID": "amzn1.application-oa2-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      "ALEXA_SMAPI_CLIENTSECRET": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      "ALEXA_SMAPI_VENDORID": "XXXXXXXXXXXXXX",
      "ALEXA_SMAPI_SKILLID": "amzn1.ask.skill.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX3"
    }
  }
}

CONTAINERMODEalexa-smapiになっていて、対話形式で入力したものが設定されているのがわかると思います。少しだけ追加します。

{
  "botium": {
    "Capabilities": {
      "PROJECTNAME": "トリップアドバイザー",      // 追加
      "ALEXA_SMAPI_LOCALE": "ja-JP",     // 追加
      "CONTAINERMODE": "alexa-smapi",
      "ALEXA_SMAPI_REFRESHTOKEN": "XXX(...snip...)XXX",
      "ALEXA_SMAPI_CLIENTID": "amzn1.application-oa2-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      "ALEXA_SMAPI_CLIENTSECRET": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      "ALEXA_SMAPI_VENDORID": "XXXXXXXXXXXXXX",
      "ALEXA_SMAPI_SKILLID": "amzn1.ask.skill.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX3"
    }
  }
}

PROJECTNAMEはテストプロジェクトの名前です。これはあってもなくてもいいです。ALEXA_SMAPI_LOCALEはスキルのロケールです。これが定義されていない場合、どうやらen-USでテストが行われ、en-US向けにスキルが作成されていない場合は失敗します。日本語の場合はja-JPを設定しておきます。なお、説明のためにコメントを入れていますが、JSONではコメントが使えないので実際に設定する際には入れないようにしてください。

Botiumを使ったAlexaスキルのテスト

ではテストシナリオを作りましょう。その前にサンプルのスキルの会話フローをご紹介します。今回のサンプルはVoiceflowで作成しています。

f:id:kun432:20220321234411p:plain

行きたい都市名を言うと、おすすめの観光地を教えてくれるというものです。実際に作る場合はもっと複雑になると思いますが、テストのためのサンプルなので。ハッピーパスはこういう感じになります。

f:id:kun432:20220321234806p:plain

ではテストシナリオです。tripadvisor_happypath01.convo.txtという名前でシナリオを作りました。*.convo.txtであればなんでもよいです。

トリップアドバイザーのテスト:Happy Path #1

#me
トリップアドバイザーを開いて

#bot
はじめまして、トリップアドバイザースキルをご利用いただきありがとうございます。このスキルでは行きたい日本の都市名をいうとおすすめの観光名所を提案します。例えば「京都に行きたい」と言ってみてください。

#me
京都 かな

#bot
京都 ですね。京都 は清水寺がおすすめです。

注意する点としては、スロットおよび変数の前後には半角スペースが必要になります。これはVoiceflowの仕様によるかもしれません(Voiceflowでは必ず必要になる。通常のAlexaスキルだと不要かも)

ではテストを実行してみましょう。

$ botium-cli run


  Botium Test-Suite
    ✔ トリップアドバイザーのテスト:Happy Path #1 (10767ms)


  1 passing (12s)

はい、問題なくテスト成功していますね。

失敗の場合も見てみましょう。シナリオファイルを少し修正します。

#bot
京都 ですね。京都 は東大寺がおすすめです。

実際にはシナリオ側が間違っているのですが、エラーの確認のため、ということで。実行してみましょう。

$ botium-cli run


  Botium Test-Suite
    1) トリップアドバイザーのテスト:Happy Path #1


  0 passing (12s)
  1 failing

  1) Botium Test-Suite
       トリップアドバイザーのテスト:Happy Path #1:
     Error: トリップアドバイザーのテスト:Happy Path #1/Line 12: Bot response (on Line 9: #me - 京都 かな) "京都 ですね。京都 は清水寺がおすすめです。" expected to match "京都 ですね。京都 は東大寺がおすすめです。"
########################################
ASSERTION FAILED in TextMatchAsserter - Expected: ["京都 ですね。京都 は東大寺がおすすめです。"]  - Actual: "京都 ですね。京都 は清水寺がおすすめです。"
INPUT: 京都 かな
------------ TRANSCRIPT ----------------------------
#me: トリップアドバイザーを開いて
#bot: <speak>はじめまして、トリップアドバイザースキルをご利用いただきありがとうございます。このスキルでは行きたい日本の都市名をいうとおすすめの観光名所を提案します。例えば「京都に行きたい」と言ってみてください。</speak>
#me: 京都 かな
#bot: <speak>京都 ですね。京都 は清水寺がおすすめです。</speak>
      at wrapBotiumError (/path-in-somewhere/botium-cli/lib/node_modules/botium-cli/src/run/index.js:76:12)
      at finish (/path-in-somewhere/botium-cli/lib/node_modules/botium-cli/src/run/index.js:205:24)
      at /path-in-somewhere/botium-cli/lib/node_modules/botium-cli/src/run/index.js:218:11

はい、想定とテスト結果が異なっていることがわかりますね。このようにしてテストを行えばよいわけです。

また、テストファイルは複数用意することができます。同じパスに以下のファイルを追加します。

tripadvisor_happypath02.convo.txt

トリップアドバイザーのテスト:Happy Path #2

#me
トリップアドバイザーを開いて

#bot
はじめまして、トリップアドバイザースキルをご利用いただきありがとうございます。このスキルでは行きたい日本の都市名をいうとおすすめの観光名所を提案します。例えば「京都に行きたい」と言ってみてください。

#me
奈良 に行きたい

#bot
奈良 ですね。奈良 は東大寺がおすすめです。

tripadvisor_nomatch_city.convo.txt

トリップアドバイザーのテスト:都市名該当なし

#me
トリップアドバイザーを開いて

#bot
はじめまして、トリップアドバイザースキルをご利用いただきありがとうございます。このスキルでは行きたい日本の都市名をいうとおすすめの観光名所を提案します。例えば「京都に行きたい」と言ってみてください。

#me
神戸

#bot
ごめんなさい、神戸 はまだ対応していません。

では実行してみましょう。

$ botium-cli run


  Botium Test-Suite
    ✔ トリップアドバイザーのテスト:Happy Path #1 (10344ms)
    ✔ トリップアドバイザーのテスト:Happy Path #2 (10557ms)
    ✔ トリップアドバイザーのテスト:都市名該当なし (7728ms)


  3 passing (29s)

こんな感じでまとめてテストを実行してくれます。

まとめ

Botiumのシナリオはとても簡易でわかりやすいですね。mocha/jestなどは多少なりともコードの書き方を覚えておく必要がありますが、Botiumなら開発者じゃなくてもテスト用のシナリオをかけそうです。

とはいえ、これだけだと複雑なテストを書くには足りないですね。ということで、次回はシナリオの書き方についていろいろ見ていきたいと思います。