どうも、サイコパスのりおです。
つい最近AWSがにゃんテックという、猫とテクノロジーを組み合わせたもののセミナーを開いたようですね。
こちらも負けていてはいかんと思っていた矢先、以下のような依頼が飛んできました。

ということで、今回はにゃんテックとSAPを組み合わせた製品をご紹介します。
概要
Raspberry Piの前を猫が通ると人感センサーが反応し、カメラモジュールを使用して写真を撮影します。
次にその写真(バイナリデータ)をSAP Leonard ML Foundation上の画像判定APIに送信します。
猫と判定された場合はRaspberry PiからSwitchBotを起動してリモコンのボタンを押下し、デカダンスいわおさんの首輪に電気ショックを流します。
またその裏で、SAP S/4 HANA上でマタタビの購買依頼伝票を作成し、さらに猫が集まるようにしてあげましょう。
(注:電気ショックは最低値でもかなりの痛みを伴う可能性があります。こちらでは一切の責任を取りませんので、ご自身の判断で実装してください。)
必要なもの
| 製品 | 補足 | 
|---|---|
| Raspberry Pi 3 model:B | |
| 人感センサー | |
| Raspberry Pi Camera V2 | |
| SwitchBot | |
| 犬のしつけ用電気ショック首輪 | 人に使います | 
| SAP S/4 HANA 1709 | オンプレミスver | 
実装手順
1. 人感センサーを用いた自動撮影
2. 画像分類APIによる猫判定
3. 家主への電気ショック
4. SAP S/4 HANA上での購買依頼伝票の作成
1. 人感センサーを用いた自動撮影
こちらは既に多くの方が実装されているため、細かい説明は割愛させていただきます。
以下のリンクが参考になりますので、適宜ご確認ください。
・ 【Raspberry Pi】自作人感センサーの使い方と活用法
・ Raspberry Piとカメラモジュールを接続する
ソースコードは以下の通りです。
from datetime import datetime
import time
import RPi.GPIO as GPIO
class HumanSensor:
    __interval = None
    __sleeptime = None
    __GPIO_PIN = None
   
    def __init__(self):
        self.__interval = 3
        self.__sleeptime = 5
        self.__GPIO_PIN = 18
    def humanDetect(self):
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(self.__GPIO_PIN, GPIO.IN)
        try:
            # センサー感知
            if(GPIO.input(self.__GPIO_PIN) == GPIO.HIGH):
                print('人感センサーの反応あり')
                return True
            else:
                print('人感センサーの反応なし')
                time.sleep(self.__interval)
        except KeyboardInterrupt:
            print("終了処理中...")
        finally:
            GPIO.cleanup()
import picamera
class Camera:
    def takePicture():
        camera = picamera.PiCamera()
        #'picture.jpg'という名前で画像を保存する
        camera.capture('picture.jpg')
        print('撮影しました')
        camera.close()
human_sensor.pyで人感センサーを起動し、センサーが検知したらTrueをreturnします。camera_v2.pyでは写真を撮影してjpeg形式で保存するようなロジックを実装しています。
この写真は次のパートで画像判定APIに送られます。
2. 画像分類APIによる猫判定
SAP Leonard MLには機械学習系の便利なAPIがあります。今回はその中でも以下の画像分類APIを使用します。
こちらのAPIを使用するに当たってAPIキーが必要となるので、以下のボタンを押下してコピーしておきましょう。

上記APIを呼び出し、猫か否か判定するスクリプトは以下の通りです。
import requests
import json
import re
class ImageClassification:
   
    __url = None
    __apikey = None
    image = None
   
    def __init__(self, image):
        self.__url = 'https://sandbox.api.sap.com/mlfs/api/v2/image/classification'
        self.__apikey = "自身のAPIキー"
        self.image = image
   
    def imageClassify(self):
        #urlとヘッダをセット
        api_url = self.__url
        headers = {"APIKey": self.__apikey}
        #バイナリイメージをセット
        byte_image = open(self.image, 'rb').read()
        files = {'files': ('cat1.jpeg', byte_image, 'image/jpeg')}
       
        print('画像判定中')
        #画像分類APIにPOSTを行う
        string_response = requests.post(api_url, headers=headers, files=files)
        #String型で帰ってきたレスポンスをJSON形式に変換
        json_response = json.loads(string_response.text)
        #レスポンスから最も確率の高い結果を抽出
        prediction = json_response["predictions"][0]["results"][0]["label"]
        result = '画像判定結果:' + prediction
        print(result)
       
        #結果に'cat'が含まれている場合
        if re.search('cat', prediction):
            print('猫を発見')
            return True
        #結果に'cat'が含まれていない場合
        else:
            print('猫ではない')
            return False
APIに対してPOSTメソッドで画像のバイナリデータを送ると、文字列で推定値が返ってきます。
その中から最も可能性が高いと推測されたものの名称を取り出し、「cat」という文字列が含まれている場合はTrueを、含まれていない場合はFalseを返すようにしています。
3. 家主への電気ショック
次にSwitchBotをRaspberry Piから操作し、猫の画像が返ってきた場合にリモコンを操作して首輪に電気ショックが流れるようにします。
操作にあたってはいくつかライブラリをインストールする必要があるので、以下の記事を参考に環境を構築してください。
(ものによってはライブラリのインストールエラーが起こるので、適宜別の資料を参考にしてください。)
・ Raspberry PiからSwitch Botを操作する
・ 暗くなったら窓シャッターを自動的に閉める(Raspberry Pi Zero W + Switch Bot)
正しくインストール等が出来ていれば、コマンドプロンプト上で
"sudo python /home/pi/python-host/switchbot.py SwitchBotのMACアドレス Press"
と叩くとSwitchBotが動作するかと思います。
(後ほどpythonスクリプト内にこのコマンドを叩くロジックを組み込みます。)
電気ショックリモコンのボタンにSwitchBot がいい感じで被さるようにセットすれば、SwitchBot起動時に首輪に電気ショックが流れます。

4. SAP S/4 HANA上での購買依頼伝票の作成
さて、家主に電気ショックを浴びせている間に裏でマタタビを発注し、もっと猫が集まるようにしてあげましょう。
SAP S/4 HANAに購買依頼伝票作成用のODataサービスを実装し、POSTメソッドにより伝票が作成されるようにします。こちらは前回のたらいシステムの記事(実装パートのステップ2と3)をまるっと利用します(以下リンク先参照)。
・ たらい落としでSAP S/4 HANA上に購買依頼伝票を作成してみた
ODataサービス実装は上記のリンクの通りで、Raspberry Piから呼び出す際には以下のクラスから呼び出します。
import urllib.request
import urllib.error 
import lxml
import base64
import json
import re
import ssl
import time
from bs4 import BeautifulSoup
from collections import defaultdict
class HttpMethod:
    
    __url = None
    __user = None
    __password = None
    #コンストラクタの定義
    def __init__(self, url):
        self.__url = url
        self.__user = <<ユーザ名>>
        self.__password = <<パスワード>>        
    #GETメソッドによるCSRFトークン、cookieの取得
    def getMethod(self):
        #BasicAuthの設定
        baseuserpass = base64.b64encode('{}:{}'.format(self.__user, self.__password).encode('utf-8'))
        #ヘッダ情報をセット
        headers_getmethod ={'Authorization': 'Basic ' + baseuserpass.decode('utf-8'),
                            'X-CSRF-Token': 'Fetch'
                           }
        
        #リクエストを作成
        req_getmethod = urllib.request.Request(self.__url, headers=headers_getmethod)
        #SSL認証エラーを無視する(注:信頼できるサイトでのみ実行すること)
        ssl._create_default_https_context = ssl._create_unverified_context
        #リクエストを送信し、レスポンスを取得
        res_getmethod = urllib.request.urlopen(req_getmethod)
        #GETメソッドを通じてCSRFトークンを取得
        XCSRFToken = res_getmethod.info().get('x-csrf-token')
        #通信をcloseする
        res_getmethod.close();
        
        #次にレスポンスヘッダからcookieを取得する
        cookies = {}
        count = 1
        #3行目までがcookieに関する情報であり、それ以降の情報は取得不要
        for cookie in res_getmethod.info().values() :
            cookies['cookie' + str(count)] = cookie
            count += 1
            if count >= 4:
                break
        
        #CSRFトークンとcookieを返す
        return XCSRFToken, cookies
    
    #POSTメソッドによる購買依頼伝票の作成
    def postMethod(self, XCSRFToken, cookies, sw_counter):
        #BasicAuthの設定
        baseuserpass = base64.b64encode('{}:{}'.format(self.__user, self.__password).encode('utf-8'))
        #ヘッダに同一キーで複数の値をセットできるようにオブジェクトを作成
        headers_postmethod = defaultdict(list)
        #ヘッダ情報を設定
        headers_postmethod = {'Authorization': 'Basic ' + baseuserpass.decode('utf-8'),
                              'X-CSRF-Token' : XCSRFToken,
                              'Content-Type' : 'application/xml',
                              'cookie'       : cookies['cookie1'],
                              'cookie'       : cookies['cookie2'],
                              'cookie'       : cookies['cookie3']
                             }
        #ボディ情報を設定
        xmlPostBody = """<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<entry xml:base=\"http://<<ルートURL>>/sap/opu/odata/sap/YSASAKI_PR_CREATE_SRV/\" xmlns=\"http://www.w3.org/2005/Atom\" xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\" xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\">
         <id>http://<<ルートURL>>/sap/opu/odata/sap/YSASAKI_PR_CREATE_SRV/PurchaseReqSet('000000000000300002')</id>
         <title type=\"text\">PurchaseReqSet('000000000000300002')</title>
         <updated>2019-11-28T07:00:49Z</updated>
         <category term=\"YSASAKI_PR_CREATE_SRV.PurchaseReq\" scheme=\"http://schemas.microsoft.com/ado/2007/08/dataservices/scheme\"/>
         <link href=\"PurchaseReqSet('000000000000300002')\" rel=\"edit" title="PurchaseReq\"/>
         <content type=\"application/xml\">
          <m:properties>
           <d:Matnr>000000000000200002</d:Matnr>
           <d:Menge>%d</d:Menge>
           <d:Meins>PC</d:Meins>
          </m:properties>
         </content>
        </entry>"""% sw_counter #ボタンの押下秒数x10の値を数量にセット
        # POSTリクエスト送信
        bytesXMLPostBody = xmlPostBody.encode('UTF-8')
        req = urllib.request.Request(self.__url, data=bytesXMLPostBody, headers=headers_postmethod, method='POST')
        try:
            with urllib.request.urlopen(req) as response:
                
                #レスポンスを取得する        
                response_body = response.read().decode("utf-8")
                soup = BeautifulSoup(response_body, "lxml")
                
                #レスポンスから購買依頼伝票番号を取得する
                pr_number = re.sub('\\D', '', str(soup.title))
                return pr_number
                
        #例外処理        
        except urllib.error.HTTPError as err:
            soup = BeautifulSoup(err, "lxml")
            print(soup)
これで材料は全て揃いました。それでは上記のクラス/メソッドを呼び出すためのメインプログラムを作成しましょう。
import human_sensor
import camera_v2
import image_classification
import time
import subprocess
import http_method
while True:
    #人感センサーが反応した場合
    if human_sensor.HumanSensor().humanDetect():
        #カメラモジュールを使用して撮影を行う
        camera_v2.Camera.takePicture()
        #画像分類APIを用いて猫か否かを判定する
        cat_image = image_classification.ImageClassification("/home/pi/Documents/cat_detection/picture.jpg").imageClassify()
        #猫だった場合
        if cat_image:
            #SwitchBotを作動させる
            subprocess.call("sudo python /home/pi/python-host/switchbot.py <<switchbotのMACアドレス>> Press", shell=True)
            
            #ODataサービスにアクセスするクラスをインスタンス化する
            url = "https://<<ルートURL>>/sap/opu/odata/sap/YSASAKI_PR_CREATE_SRV/PurchaseReqSet"
            http = http_method.HttpMethod(url)
            #GETによりCSRFトークンとcookieを取得する
            XCSRFToken, cookies = http.getMethod()
            #POSTにより購買依頼伝票を作成する
            pr_number = http.postMethod(XCSRFToken, cookies, 1000)
            print('購買依頼伝票:'+ pr_number +'を作成しました。')
            exit
    else:
        time.sleep(2)
        
    print('----------------------------------------------------------')
完成
では早速動かしてみましょう。
#chillSAP の登壇で使用した『にゃんテックxSAP(電気ショックver)やってみた』の動画を共有します。
— サイコパスのりお (@norio_psycho) February 13, 2020
皆さんも気になる人にやってもらってはいかがでしょうか?#SAP #IoT #RaspberryPi #Python #猫 #にゃんテック #電気ショック pic.twitter.com/m0pDGS7I59
これでいつ猫がやってきても電流を流せますね♪
みなさんもぜひ試してみてください。
(注:電流の強さにはくれぐれも注意してください。こちらでは一切の責任を取りかねます。)

