MongoDB Realmで、超初心者が無料でAndroidアプリのバックエンド処理を作ってみた

# はじめに
この記事は、
[こちらの記事のIoTセンサデータを](https://qiita.com/c60evaporator/items/283d0569eba58830f86e)
**モバイル連携クラウドサービス「MongoDB Realm」を通じて、スマホアプリとデータ連携**させた記事となります
![summary.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/85316a54-98bb-0b23-e803-f827b7472687.png)

[スマホアプリ本体の作成に関しては、こちらで記事にしております](https://qiita.com/c60evaporator/items/31d9d405b9904af54e87)

MongoDB Realmの利用法については、[こちらの記事の内容を応用しております](https://qiita.com/c60evaporator/items/8cd83abfeb3b232413d1)

**まずは上の3つの記事をご覧頂いてから**、本記事を見て頂けますと幸いです。

# スマホアプリとバックエンド処理
詳しくは[上の記事](https://qiita.com/c60evaporator/items/8cd83abfeb3b232413d1)に記載しましたが、
サーバ連携したスマホアプリの作成には、

・オンプレサーバを立てて
・DB作成して
・API公開して
・Androidアプリのバックエンド処理を作成して
・AndroidアプリのUIを作成

というように、
**バックエンド側に多大な開発コストが掛かる**ことが多いそうです。
このような背景の中、バックエンド開発工数削減を目的としたクラウドサービスが増えています。

## MongoDB Realmとは？
上記のようなクラウドサービスに参入すべく、MongoDB社がモバイル向けDB「Realm」を買収してで作られたサービスが、
**[MongoDB Realm](https://codezine.jp/article/detail/12422)**
です。
![mongodb_realm02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/b0f81c9a-7f80-2ca6-b26e-7873e9c84900.png)
なんとこのサービス、無料枠でデータ取得処理をカンタンに実装できるだけでなく、
**データのリアルタイム同期**まで出来てしまいます。

今回は、上記MongoDB Realmを使用して、IoTアプリのバックエンド処理、
具体的には

**A. Raspberry Piで取得したセンサデータをクラウドDBにアップロードする**
**B. 上記データをAndroidアプリ(自作クラスのインスタンス)と同期してスマホ表示する**

という処理を実装してみます。

# 必要なもの
**・各種温湿度センサ**　([対応しているセンサはこちら参照](https://qiita.com/c60evaporator/items/283d0569eba58830f86e#%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E3%82%BB%E3%83%B3%E3%82%B5%E4%B8%80%E8%A6%A7))
**・Raspberry Pi** (今回はRaspberry Pi3 ModelBを使用)
**・ラズパイ内のPython実行環境** (今回はプリセットのPython3.7.3使用)
**・開発用PC**(今回はWindows10を使用)
**・Android Studio** (Androidアプリ開発環境)
**・MongoDB Atlasのアカウント** (無料で作成できます)
**(・Androidスマホ(動作確認用))**

# 手順
下記のような手順で、目的の処理を実装します。

1. MongoDB Atlasの初期設定とサーバDBの作成
2. RaspberryPiからサーバDBへのデータアップロード
3. MongoDB Realmのサーバ側初期設定
4. Androidアプリ作成

1,2が前章のA (RaspberryPi → クラウドDBへのアップロード)
3,4が前章のB (クラウドDB → Androidアプリ内とのデータ同期)
に相当します

## 1. MongoDB Atlasの初期設定とサーバDBの作成
サーバ側DBをクラウドサービスであるMongoDB Atlasに作成します

### MongoDB Atlasの構造について
Organization > Project > Cluster > DB > Collecion
という構造になっています。
DBが通常のRDBMSのDBに、CollectionがRDBMSのテーブルに相当します。
ClusterはMongoDB独自の構造で、[こちら](https://docs.ncloud.com/ja/database/database-10-3.html)を見る限り、分散処理の効率を上げるためのカテゴリだそうです。
Organization、ProjectはクラウドのAtlas独自の構造です。詳細は不明ですが、チームやプロジェクト毎の管理を前提としたカテゴリのようです。

### MongoDB Atlasへの登録
[MongoDB Atlasのトップページ](https://www.mongodb.com/cloud/atlas)にアクセスし、「Start free」を押します
![01_mongoatlastop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/bd853608-d2bd-279c-c58a-7f002f7945f3.png)

各種情報を入力し、登録します
![02_register.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/d169c93d-fd67-3b27-70cb-2a9ac706198b.png)

### クラスタの作成、ユーザの作成、ホワイトリストの設定
[こちら](https://www.virment.com/how-to-use-mongo-db-atlas/)をご参照いただければと思います。

なお、Realmを使用してスマホアプリと連携したい場合、MongoDBのバージョンを4.4とする必要がありますが、
2020/8現在、バージョン4.4を指定できるのは、プロバイダにAWSのN. Virginiaを指定したときのみなので、ご注意下さい。
私は下記のクラスタ設定としました。
![05_selectplan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/ed90c820-b238-8d2a-76f8-ff53fe8444ef.png)

### センサデータアップロード用DBおよびCollectionの作成
後でプログラムから作成することも可能ですが、誤実行を防ぐために手動で作成します。

**クラスタ表示画面で「COLLECTIONS」を押します**
![11_selectcollection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/0e0c17af-cb73-a82a-92da-002020077b33.png)

**「Add My Own Data」をクリックします**
![12_addmyowndata.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/d5862917-e829-edc6-8302-e023fec2043e.png)
**DB名(好きな名前)とCollection(テーブルに相当)名"sensors"を入力し、「Create」を押します**
![13_createdb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/740a9f18-4dcf-d43c-53ec-7e7179b90769.png)
**入力したDBおよび"sensors"コレクションが生成されていれば成功です**
![14_confirmcollection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/92eeae71-1375-22f2-b621-13fd120842b0.png)

### センサ一覧コレクションの作成
使用するセンサの一覧も、コレクションとして管理します。

DB名の横に出てくる＋を押します
![16_create_sensor_list_collection1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/4f96cd0d-edf1-caa4-7e7f-3e0a385f7fd7.png)

コレクション名に"sensor_lists"と入力します
![17_create_sensor_list_collection2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/762b6637-a09f-5beb-ed9c-49e1a78d97fb.png)

手作業でドキュメントを追加し、使用するすべてのセンサの数だけ情報を入力します
![18_create_sensor_list_collection3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/afd666ab-ed0a-ea7b-8b12-e42c82454bcf.png)

各フィールドの意味は下記となります
`no:連番を付ける`
`sensorname:センサ名`
`place:設置場所(indoor, outdoor, kitchenの3種類を想定)`
`temperature:温度情報の有無(true or false)`
`humidity:湿度情報の有無(true or false)`
`aircon:エアコンOn-Off情報の有無(Nature Remoのみtrueになる想定)`
`power:消費電力情報の有無(Nature Remoのみtrueになる想定)`
`_partition:あとでMongoDB Realmでの同期に使用(固定値"Project HomeIoT")`

### DBアクセス方法の確認
次節に備え、各種プログラム言語からDBにアクセスする方法を確認します

**クラスタ表示画面で「CONNECT」を押します**
![08_connect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/74a4e9e6-6b37-e99c-3ad9-9708dc4539ce.png)

**「Connect your application」を押します**
![09_connectapp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/d7e9945a-7975-2a2b-799b-5095d4424459.png)
**下図のように、言語を選択してアクセス方法を確認します**
![10_dbaccessfromapp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/73c67e20-f8c4-69f5-0785-28776f26c956.png)
**上記アクセス方法はあとで使うので、pythonでの内容を控えておいてください**


## 2. RaspberryPiからサーバDBへのデータアップロード
[こちらの記事をベースにします](https://qiita.com/c60evaporator/items/283d0569eba58830f86e)
**まずは上記記事の内容を実装頂ければと思います**

### 2-1. RaspberryPi内スクリプトの書き換え
今回はMongoDB Atlasへのアップロード処理を加えるために、
config.ini
DeviceList.csv
remo.py
sensors_to_spreadsheet.py
を、下記のように書き換えます。

#### config.ini
DB関係の設定を追加します

```config.ini
[Path]
CSVOutput = CSV出力先を指定
LogOutput = ログ出力先を指定

[Process]
DBUploadRetry = 2

[DB]
UserName = MongoDB Atlasのユーザ名をここに記載（ダブルクオーテーションは不要）
ClusterName = MongoDB Atlasのクラスタ名をここに記載（ダブルクオーテーションは不要）
DBName = MongoDB AtlasのDB名をここに記載（ダブルクオーテーションは不要）
TableName = MongoDB Atlasのコレクション名をここに記載（ダブルクオーテーションは不要）
```

#### DeviceList.csv
No列を追加します(使用するデバイスに合わせて適宜記載を変えてください)

```DeviceList.csv
No,DeviceName,SensorType,MacAddress,Timeout,Retry,Offset_Temp,Offset_Humid,API_URL,Token
1,SwitchBot_Thermo1,SwitchBot_Thermo,[SwitchBotのMacアドレス],4,3,0,0,,
2,Inkbird_IBSTH1_Mini1,Inkbird_IBSTH1mini,[IBS-TH1 miniのMacアドレス],0,2,0,0,,
3,Inkbird_IBSTH1_1,Inkbird_IBSTH1,[IBS-TH1のMacアドレス],0,2,0,0,,
4,Remo1,Nature_Remo,,0,2,0,0,https://api.nature.global/,[Nature Remoのアクセストークン]
5,Omron_USB1,Omron_USB_EP,[Omron USB型のMacアドレス],0,2,0,0,,
6,Omron_BAG1,Omron_BAG_EP,[Omron BAG型のMacアドレス],3,2,0,0,,
```
#### remo.py

```remo.py
import json
import requests
import glob
import pandas as pd

#Remoデータ取得クラス
class GetRemoData():
    def get_sensor_data(self, Token, API_URL):
        headers = {
            'accept': 'application/json',
            'Authorization': 'Bearer ' + Token,
        }
        response = requests.get(f"{API_URL}/1/devices", headers=headers)
        rjson = response.json()
        return self._decodeSensorData(rjson)

    def get_aircon_power_data(self, Token, API_URL):
        headers = {
            'accept': 'application/json',
            'Authorization': 'Bearer ' + Token,
        }
        response = requests.get(f"{API_URL}/1/appliances", headers=headers)
        rjson = response.json()
        return self._decodeAirconPowerData(rjson)

    def calc_human_motion(self, Human_last, csvdir):
        filelist = glob.glob(f"{csvdir}/*/*.csv")
        if len(filelist) == 0:
            return 0
        filelist.sort()
        df = pd.read_csv(filelist[-1])
        if df.Human_last[len(df) - 1] != Human_last:
            return 1
        else:
            return 0

    # センサデータを取り出してdict形式に変換
    def _decodeSensorData(self, rjson):
        for device in rjson:
            #Remoのデータ
            if device['firmware_version'].split('/')[0] == 'Remo':
                sensorValue = {
                    'SensorType': 'Remo_Sensor',
                    'Temperature': device['newest_events']['te']['val'],
                    'Humidity': device['newest_events']['hu']['val'],
                    'Light': device['newest_events']['il']['val'],
                    'Human_last': device['newest_events']['mo']['created_at']
                }
        return sensorValue

    # エアコンおよび電力データを取り出してdict形式に変換
    def _decodeAirconPowerData(self, rjson):
        Value = {}
        for appliance in rjson:
            #エアコン
            if appliance['type'] == 'AC':
                Value['TempSetting'] = appliance['settings']['temp']
                Value['Mode'] = appliance['settings']['mode']
                Value['AirVolume'] = appliance['settings']['vol']
                Value['AirDirection'] = appliance['settings']['dir']
                Value['Power'] = appliance['settings']['button']
            #スマートメータの電力データ
            elif appliance['type'] == 'EL_SMART_METER':
                for meterValue in appliance['smart_meter']['echonetlite_properties']:
                    if meterValue['name'] == 'normal_direction_cumulative_electric_energy':
                        Value['CumulativeEnergy'] = float(meterValue['val'])/100
                    elif meterValue['name'] == 'measured_instantaneous':
                        Value['Watt'] = int(meterValue['val'])        
        #値を取得できていないとき、Noneとする
        if len(Value) == 0:
            Value = None
        return Value
```
#### sensors_to_spreadsheet.py
メイン処理部分とoutput_mongodb_atlasメソッドに、MongoDB Atlasへのアップロード処理を追加します。

```sensors_to_spreadsheet.py
from bluepy import btle
from omron_env import OmronBroadcastScanDelegate, GetOmronConnectModeData
from inkbird_ibsth1 import GetIBSTH1Data
from switchbot import SwitchbotScanDelegate
from remo import GetRemoData
from mesh import GetMeshFromSpreadsheet
from datetime import datetime, timedelta
import os
import csv
import configparser
import pandas as pd
import requests
import logging
import subprocess
import pymongo
from pit import Pit

#グローバル変数
global masterdate

######オムロン環境センサ（BAG型）の値取得######
def getdata_omron_bag(device):
    #値が得られないとき、最大device.Retry回スキャンを繰り返す
    for i in range(device.Retry):
        #omron_envのセンサ値取得デリゲートを、スキャン時実行に設定
        scanner = btle.Scanner().withDelegate(OmronBroadcastScanDelegate())
        #スキャンしてセンサ値取得
        try:
            scanner.scan(device.Timeout)
        #スキャンでエラーが出たらBluetoothアダプタ再起動
        except:
            restart_hci0(device.DeviceName)
        #値取得できたらループ終了
        if scanner.delegate.sensorValue is not None:
            break
        #値取得できなかったらログに書き込む
        else:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}, timeout{device.Timeout}]')
    
    #値取得できていたら、POSTするデータをdictに格納
    if scanner.delegate.sensorValue is not None:
        #POSTするデータ
        data = {        
            'DeviceName': device.DeviceName,        
            'Date_Master': masterdate,
            'Date': datetime.today(),
            'Temperature': scanner.delegate.sensorValue['Temperature'],
            'Humidity': scanner.delegate.sensorValue['Humidity'],
            'Light': scanner.delegate.sensorValue['Light'],
            'UV': scanner.delegate.sensorValue['UV'],
            'Pressure': scanner.delegate.sensorValue['Pressure'],
            'Noise': scanner.delegate.sensorValue['Noise'],
            'BatteryVoltage': scanner.delegate.sensorValue['BatteryVoltage']
        }
        return data
    #値取得できていなかったら、ログ出力してBluetoothアダプタ再起動
    else:
        logging.error(f'cannot get data [date{str(masterdate)}, device{device.DeviceName}, timeout{device.Timeout}]')
        restart_hci0(device.DeviceName)
        return None

######オムロン環境センサ（USB型）のデータ取得######
def getdata_omron_usb(device):
    #値が得られないとき、最大device.Retry回スキャンを繰り返す
    for i in range(device.Retry):
        try:
            sensorValue = GetOmronConnectModeData().get_env_usb_data(device.MacAddress)
        #エラー出たらログ出力
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}]')
            sensorValue = None
            continue
        else:
            break
    
    #値取得できていたら、POSTするデータをdictに格納
    if sensorValue is not None:
        #POSTするデータ
        data = {        
            'DeviceName': device.DeviceName,        
            'Date_Master': masterdate,
            'Date': datetime.today(),
            'Temperature': sensorValue['Temperature'],
            'Humidity': sensorValue['Humidity'],
            'Light': sensorValue['Light'],
            'Pressure': sensorValue['Pressure'],
            'Noise': sensorValue['Noise'],
            'eTVOC': sensorValue['eTVOC'],
            'eCO2': sensorValue['eCO2']
        }
        return data
    #値取得できていなかったら、ログ出力してBluetoothアダプタ再起動
    else:
        logging.error(f'cannot get data [loop{str(device.Retry)}, date{str(masterdate)}, device{device.DeviceName}]')
        restart_hci0(device.DeviceName)
        return None

######Inkbird IBS-TH1のデータ取得######
def getdata_ibsth1(device):
    #値が得られないとき、最大device.Retry回スキャンを繰り返す
    for i in range(device.Retry):
        try:
            sensorValue = GetIBSTH1Data().get_ibsth1_data(device.MacAddress, device.SensorType)
        #エラー出たらログ出力
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}]')
            sensorValue = None
            continue
        else:
            break

    if sensorValue is not None:
        #POSTするデータ
        data = {        
            'DeviceName': device.DeviceName,        
            'Date_Master': masterdate,
            'Date': datetime.today(),
            'Temperature': sensorValue['Temperature'],
            'Humidity': sensorValue['Humidity']
        }
        return data
    #値取得できていなかったら、ログ出力してBluetoothアダプタ再起動
    else:
        logging.error(f'cannot get data [loop{str(device.Retry)}, date{str(masterdate)}, device{device.DeviceName}]')
        restart_hci0(device.DeviceName)
        return None

######SwitchBot温湿度計のデータ取得######
def getdata_switchbot_thermo(device):
    #値が得られないとき、最大device.Retry回スキャンを繰り返す
    for i in range(device.Retry):
        #switchbotのセンサ値取得デリゲートを設定
        scanner = btle.Scanner().withDelegate(SwitchbotScanDelegate(str.lower(device.MacAddress)))
        #スキャンしてセンサ値取得
        try:
            scanner.scan(device.Timeout)
        #スキャンでエラーが出たらBluetoothアダプタ再起動
        except:
            restart_hci0(device.DeviceName)
        #値取得できたらループ終了
        if scanner.delegate.sensorValue is not None:
            break
        #値取得できなかったらログに書き込む
        else:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}, timeout{device.Timeout}]')
    
    #値取得できていたら、POSTするデータをdictに格納
    if scanner.delegate.sensorValue is not None:
        #POSTするデータ
        data = {        
            'DeviceName': device.DeviceName,
            'Date_Master': masterdate,
            'Date': datetime.today(),
            'Temperature': scanner.delegate.sensorValue['Temperature'],
            'Humidity': float(scanner.delegate.sensorValue['Humidity']),
            'BatteryVoltage': scanner.delegate.sensorValue['BatteryVoltage']
        }
        return data
    #取得できていなかったら、ログ出力してBluetoothアダプタ再起動
    else:
        logging.error(f'cannot get data [loop{str(device.Retry)}, date{str(masterdate)}, device{device.DeviceName}, timeout{device.Timeout}]')
        restart_hci0(device.DeviceName)
        return None

######Nature Remoのデータ取得######
def getdata_remo(device, csvpath):
    #センサデータ値が得られないとき、最大device.Retry回スキャンを繰り返す
    for i in range(device.Retry):
        try:
            sensorValue = GetRemoData().get_sensor_data(device.Token, device.API_URL)
        #エラー出たらログ出力
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}, sensor]')
            sensorValue = None
            continue
        else:
            break
    #エアコンおよび電力データ値が得られないとき、最大device.Retry回スキャンを繰り返す
    for i in range(device.Retry):
        try:
            airconPowerValue = GetRemoData().get_aircon_power_data(device.Token, device.API_URL)
        #エラー出たらログ出力
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}, aircon]')
            sensorValue = None
            continue
        else:
            break
        
    #値取得できていたら、POSTするデータをdictに格納
    if sensorValue is not None:
        #センサデータ
        data = {        
            'DeviceName': device.DeviceName,        
            'Date_Master': masterdate,
            'Date': datetime.today(),
            'Temperature': float(sensorValue['Temperature']),
            'Humidity': float(sensorValue['Humidity']),
            'Light': sensorValue['Light'],
            'Human_last': sensorValue['Human_last'],
            'HumanMotion': GetRemoData().calc_human_motion(sensorValue['Human_last'], f'{csvpath}/{device.DeviceName}')
        }
        #エアコン＆電力データ
        if airconPowerValue is not None:
            data['TempSetting'] = int(airconPowerValue['TempSetting'])
            data['AirconMode'] = airconPowerValue['Mode']
            data['AirVolume'] = airconPowerValue['AirVolume']
            data['AirDirection'] = airconPowerValue['AirDirection']
            data['AirconPower'] = airconPowerValue['Power']
            if data['AirconPower'] == "":
                data['AirconPower'] = 'power-on_maybe'
            #電力
            if 'CumulativeEnergy' in airconPowerValue:
                data['CumulativeEnergy'] = float(airconPowerValue['CumulativeEnergy'])
            if 'Watt' in airconPowerValue:
                data['Watt'] = int(airconPowerValue['Watt'])
        return data
    #取得できていなかったら、ログ出力（WiFi経由なのでBluetoothアダプタ再起動はしない）
    else:
        logging.error(f'cannot get data [loop{str(device.Retry)}, date{str(masterdate)}, device{device.DeviceName}]')
        return None

######データのCSV出力######
def output_csv(data, csvpath):
    dvname = data['DeviceName']
    monthstr = masterdate.strftime('%Y%m')
    #出力先フォルダ名
    outdir = f'{csvpath}/{dvname}/{masterdate.year}'
    #出力先フォルダが存在しないとき、新規作成
    os.makedirs(outdir, exist_ok=True)
    #出力ファイルのパス
    outpath = f'{outdir}/{dvname}_{monthstr}.csv'
    
    #出力ファイル存在しないとき、新たに作成
    if not os.path.exists(outpath):        
        with open(outpath, 'w') as f:
            writer = csv.DictWriter(f, data.keys())
            writer.writeheader()
            writer.writerow(data)
    #出力ファイル存在するとき、1行追加
    else:
        with open(outpath, 'a') as f:
            writer = csv.DictWriter(f, data.keys())
            writer.writerow(data)

######MongoDB Atlasにアップロードする処理######
def output_mongodb_atlas(all_values_dict, user_name, cluster_name, db_name, collection_name, retry):
    passwd = ****#適宜パスワード隠蔽処理を作成してください
    for i in range(retry):
        try:
            client = pymongo.MongoClient(f"mongodb+srv://{user_name}:{passwd}@{cluster_name}.jipvx.mongodb.net/{db_name}?retryWrites=true&w=majority")
            db = client[db_name]
            collection = db[collection_name]
            result = collection.insert_one(all_values_dict)
        #エラー出たらログ出力
        except:
            if i == retry:
                logging.error(f'cannot upload to DB [loop{str(i)}, date{str(masterdate)}]')
            else:
                logging.warning(f'retry to upload to DB [loop{str(i)}, date{str(masterdate)}]')
            continue
        else:
            break

######Googleスプレッドシートにアップロードする処理######
def output_spreadsheet(all_values_dict_str):
    #APIのURL
    url = 'GAS APIのURLをここに記載'
    #APIにデータをPOST
    response = requests.post(url, json=all_values_dict_str)
    print(response.text)

######Bluetoothアダプタ再起動######
def restart_hci0(devicename):
    passwd = 'RaspberryPiのパスワードを入力'#適宜隠蔽してください
    subprocess.run(('sudo','-S','hciconfig','hci0','down'), input=passwd, check=True)
    subprocess.run(('sudo','-S','hciconfig','hci0','up'), input=passwd, check=True)
    logging.error(f'restart bluetooth adapter [date{str(masterdate)}, device{devicename}]')


######メイン######
if __name__ == '__main__':
    #開始時刻を取得
    startdate = datetime.today()
    #開始時刻を分単位で丸める
    masterdate = startdate.replace(second=0, microsecond=0)   
    if startdate.second >= 30:
        masterdate += timedelta(minutes=1)

    #設定ファイルとデバイスリスト読込
    cfg = configparser.ConfigParser()
    cfg.read('./config.ini', encoding='utf-8')
    df_devicelist = pd.read_csv('./DeviceList.csv')
    #全センサ数とデータ取得成功数
    sensor_num = len(df_devicelist)
    success_num = 0

    #ログの初期化
    logname = f"/sensorlog_{str(masterdate.strftime('%y%m%d'))}.log"
    logging.basicConfig(filename=cfg['Path']['LogOutput'] + logname, level=logging.INFO)

    #取得した全データ保持用dict
    all_values_dict = None
    #上記dictの文字列バージョン（GAS Post用、datetime型がJSON化できないため）
    all_values_dict_str = None

    #データ取得開始時刻
    scan_start_date = datetime.today()

    ######デバイスごとにデータ取得######
    for device in df_devicelist.itertuples():
        #Omron環境センサBAG型（BroadCast接続）
        if device.SensorType in ['Omron_BAG_EP','Omron_BAG_IM']:
            data = getdata_omron_bag(device)
        #Omron環境センサUSB型（Connectモード接続）
        elif device.SensorType in ['Omron_USB_EP','Omron_USB_IM']:
            data = getdata_omron_usb(device)
        #Inkbird IBS-TH1
        elif device.SensorType in ['Inkbird_IBSTH1mini','Inkbird_IBSTH1']:
            data = getdata_ibsth1(device)
        #SwitchBot温湿度計
        elif device.SensorType == 'SwitchBot_Thermo':
            data = getdata_switchbot_thermo(device)
        #remo
        elif device.SensorType == 'Nature_Remo':
            data = getdata_remo(device, cfg['Path']['CSVOutput'])
        #mesh
        elif device.SensorType == 'Sony_MeshHuman':
            data = getdata_mesh_human(device)
        #上記以外
        else:
            data = None        

        #データが存在するとき、全データ保持用Dictに追加し、CSV出力
        if data is not None:
            #all_values_dictがNoneのとき、新たに辞書を作成
            if all_values_dict is None:
                #all_values_dictを作成（最初なのでDate_MasterとDate_ScanStartも追加）
                all_values_dict = {'Date_Master':data['Date_Master'], 'Date_ScanStart':scan_start_date}
                all_values_dict.update(dict([('no'+format(device.No,'02d')+'_'+k, v) for k,v in data.items() if k != 'Date_Master']))
                #dataを文字列変換してall_values_dict_strを作成(最初なのでDate_ScanStartを追加)
                data_str = dict([(k, str(v)) for k,v in data.items()])
                data_str['Date_ScanStart'] = str(scan_start_date)
                all_values_dict_str = {data_str['DeviceName']: data_str}
            #all_values_dictがNoneでないとき、既存の辞書に追加
            else:
                #all_values_dictに追加（最初でないのでDate_Masterは除外）
                all_values_dict.update(dict([('no'+format(device.No,'02d')+'_'+k, v) for k,v in data.items() if k != 'Date_Master']))
                #all_values_dict_strに追加
                data_str = dict([(k, str(v)) for k,v in data.items()])
                all_values_dict_str[data_str['DeviceName']] = data_str

            #CSV出力
            output_csv(data_str, cfg['Path']['CSVOutput'])
            #成功数プラス
            success_num+=1

    ######MongoDB Atlasにアップロードする処理######
    all_values_dict['_partition'] = 'Project HomeIoT'#あとでMongoDB Realmの同期に使用するフィールド
    output_mongodb_atlas(all_values_dict, cfg['DB']['UserName'], cfg['DB']['ClusterName'], cfg['DB']['DBName'], cfg['DB']['TableName'], int(cfg['Process']['DBUploadRetry']))

    ######Googleスプレッドシートにアップロードする処理######
    output_spreadsheet(all_values_dict_str)

    #処理終了をログ出力
    logging.info(f'[masterdate{str(masterdate)} startdate{str(startdate)} enddate{str(datetime.today())} success{str(success_num)}/{str(sensor_num)}]')
```

sensors_to_spreadsheet.py内の
`output_mongodb_atlas()`
メソッドで、MongoDB Atlasにドキュメントデータ「all_values_dict」をPostしています。

**上記output_mongodb_atlasメソッド内の処理と、前章の最後で控えたMongoDB Atlasへのpythonからのアクセス内容が一致していることを確認してください**

### 2-2. DBへのアップロード動作確認
cronでRaspberryPi内のスクリプトを定期実行し、下図のようにクラウド画面上でコレクションへのアップロードが確認できれば成功です
![15_confirm_post.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/95c78f87-8376-f945-5261-76d58ea8e29e.png)


## 3. MongoDB Realmのサーバ側初期設定
サーバ・クライアント連携システムであるMongoDB Realmの、サーバ側設定を実施します。
[こちらを参考にさせて頂きました](https://docs.mongodb.com/realm/tutorial/realm-app/)

### 3-1. Realmアプリケーションの作成
MongoDB Atlasのクラスタ設定画面から、「Realm」タブをクリックします。
![1_select_realm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/5a47c0bc-b2c7-5f9f-041c-e884571f0acf.png)!

今回の用途に合わせ、「Mobile」「Android」「No, I'm starting from scratch(新規作成)」を選択します
![2_start_new_realm_app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/9e0c411a-4382-53f6-2527-7f358d05d527.png)

アプリケーション名と、連携するMongoDB Atlasのクラスタを指定します。
※今回は「HomeIoTAppと名前を付けました」
![3_cretate_realm_app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/60485d99-f47c-be85-9c1d-2e8a3bdeeec0.png)

後で使うので、アプリケーションIDを控えておきます
![04_copy_appid.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/e6c0663b-edd1-66e6-fdb6-088cc70d4d80.png)

### 3-2. ルールの作成
ルール作成対象のMongoDB Atlasコレクションを選択します
![05_configure_collection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/10adfae3-a534-a76e-a49c-9185f9a27649.png)

#### スキーマの作成
下記手順でスキーマを作成します

Schemaタブを開き、「GENERATE SCHEMA」をクリック
![06_generate_schema1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/3a63a8c8-2eda-358e-8a95-7b4b5a89ac06.png)

下記手順で、コレクションの中身からスキーマを自動生成してくれます
![07_generate_schema2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/f17ad31e-8e14-b4b1-e541-94868877ddd6.png)

bsonTypeおよび必須フィールドを追記した上で、生成されたスキーマが想定と一致しているか確認し、SAVEします
※"title"は後でKotlinのデータモデルクラス名と一致させる必要があるので、複数形を付けないPascalCase命名規則推奨
![08_generate_schema3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/e4b92c47-1311-fdf6-f092-ce47343028ba.png)


同様の手順で、sensor_listsコレクションのスキーマも作成します
※"title"を"SensorList"に変更するのを忘れないでください
![19_schema_sensor_list.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/d3b5b22b-df25-4178-1a2b-2720f884a792.png)

```json:sensor_listsコレクションのスキーマ
{
  "title": "SensorList",
  "bsonType": "object",
  "required": ["_id"],
  "properties": {
    "_id": {
      "bsonType": "objectId"
    },
    "_partition": {
      "bsonType": "string"
    },
    "aircon": {
      "bsonType": "bool"
    },
    "humidity": {
      "bsonType": "bool"
    },
    "no": {
      "bsonType": "int"
    },
    "place": {
      "bsonType": "string"
    },
    "power": {
      "bsonType": "bool"
    },
    "sensorname": {
      "bsonType": "string"
    },
    "temperature": {
      "bsonType": "bool"
    }
  }
}
```

### ユーザ権限の設定
Androidアプリ側からの、Email + パスワードによる認証を許可します。
Usersタブ → Providersタブをクリックします
![14_configure_user_authentication1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/595d8336-9fa1-66b4-07cc-8947ec28eefc.png)
Provider EnabledをONに
User Confirmation MethodをAutomatically confirm usersに
Password Reset MethodをRun a password reset functionに
Functionを、新たに作成したresetFuncに設定する
![15_configure_user_authentication2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/456b7d92-97ec-3663-f6a2-64f994b4850f.png)

### 同期の設定
下図のように、同期を設定します
![21_add_sync.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/baf59268-6a94-9441-aea6-4a9a1b0f3cf2.png)

### 変更をデプロイ
ここまでで設定に一区切りついたので、変更をアプリケーションに反映（デプロイ）させます。
画面の上の方に出てくるREVIEW&DEPLOYをクリックして、変更内容を確認したうえでDEPLOYをクリックします。
![18_deploy_change.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/d49cdc28-f259-5d74-bc24-4e1239baf62b.png)
以後も設定に変更を加えた際は、適宜デプロイします


## 4. Androidアプリの作成
MongoDB Realmによりサーバとデータ同期可能なAndoroidアプリを作成します。
Android Studioを使用します。
[基本には公式ドキュメントを参考にしました](https://docs.mongodb.com/realm/tutorial/android-kotlin/)
### 4-1. プロジェクトの作成
Android Studioを開き、「Start a new Android Studio Project」をクリックします。
出てきたテンプレート選択画面で、「Empty Activity」を選択します。
![30_select_template.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/def57ffb-f141-832e-6556-4d8febda27df.png)

好きなプロジェクト名、パッケージ名を記入し、
言語：Kotlin、Minimum SDK：API21を選択します。
![31_configure_project.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/295cab63-95d6-0783-1756-ed029ba811e4.png)

### 4-2. Realmプラグインのインストール
必要なプラグインをインストールします
#### Project全体へのインストール
左のタブから、2つあるBuild.gradleのうちProjectの方に以下の記述を追記し、プロジェクト全体にrealmプラグインをインストールします。
（2ヵ所のmavenはベータ版用の処理との記載なので、今後なくなるかもしれません）
![32_install_project_gradle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/6b599549-63bf-f873-baec-f1350a3d1932.png)

```gradle:build.gradle(Project)
buildscript {
      :
    repositories {
          :
        maven {
            url 'http://oss.jfrog.org/artifactory/oss-snapshot-local'
        }
    }
    dependencies {
          :
        classpath "io.realm:realm-gradle-plugin:10.0.0-BETA.5"
    }
allprojects {
    repositories {
          :
        maven {
            url 'http://oss.jfrog.org/artifactory/oss-snapshot-local'
        }
    }
}
  :
```

#### アプリケーションへのインストール
左のタブから、2つあるBuild.gradleのうちModule: appの方に以下の記述を追記し、アプリケーションにrealmプラグインをインストールします。
※上で控えたアプリケーションIDが必要になります
※kotlin-kaptはrealm-androidが依存するモジュールなので、後者より上に記載する必要あり
![33_install_app_gradle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/955554ec-9689-fbd5-4265-fe6e9ac8d375.png)

```gradle:build.gradle(Module：app)
  :
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
  :
android {
      :
    buildTypes {
        def appId = "上で控えたアプリケーションID"  // Replace with proper Application ID
        debug {
            buildConfigField "String", "MONGODB_REALM_APP_ID", "\"${appId}\""
        }
        release {
            buildConfigField "String", "MONGODB_REALM_APP_ID", "\"${appId}\""
            minifyEnabled false
            signingConfig signingConfigs.debug
        }
    }

    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}

realm {
    syncEnabled = true
}

dependencies {
      :
    implementation 'com.google.android.material:material:1.2.0'
      :
    implementation "io.realm:android-adapters:4.0.0"
    implementation "androidx.recyclerview:recyclerview:1.1.0"
      :
}
```

ここで一度File → Close Projectでプロジェクトを開きなおしたのち、
プログラムを実行して正常にアプリが起動するか確認します（各種ライブラリがエラーを出さないか確認）
![34_run_emulator.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/9b34cc06-ba3d-5eb0-c885-cd6f562a4903.png)
私の場合起動までに1分以上かかったので、気長に待ちましょう。

### 4-3. データモデルの作成
サーバDBのコレクションからデータを受け取るためのKotlinクラス(データモデル)を作成します。
具体的には下記の2つのクラスを作成します

`Sensor.kt：センサデータアップロード用コレクションに対応`
`SensorList.kt：sensor_listsコレクションに対応`

#### modelパッケージの作成
下記の手順で、モデルを保持するパッケージ（フォルダ）を作成します。

javaフォルダ内のパッケージフォルダを右クリックし、New → Package
![35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/45fcf5d1-3184-ccbd-0e1e-e37a871b2ea6.png)

パッケージに"model"という名前を付けます
![36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/476571f4-98cd-c9dc-bbd1-b4b712560197.png)

#### Sensor.kt
作成したパッケージ名を右クリックし、New → Kotlin File/Class
![37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/56be6e7d-4c2a-151f-1ee1-291ffef4c125.png)
Classを選択し、"Sensor"と名前を付けてEnter
![38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/fe8ea25e-20a8-c766-f827-76b5a4fe5208.png)

MongoDB Realmのクラウド画面より下記操作で、Kotlinでのデータモデル記述法を確認する
![39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/38896903-af0e-d2e8-7910-541e594b09bd.png)

上記操作で確認した内容をコピペし、SensorData.ktに貼り付ける。
下記のような内容となるはずです。

```Sensor.kt
package com.mongodb.homeiotviewer.model

import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey
import java.util.Date;
import org.bson.types.ObjectId;

open class Sensor (
    @PrimaryKey var _id: ObjectId = ObjectId(),
    var Date_Master: Date = Date(),
    var Date_ScanStart: Date? = null,
    var _partition: String? = null,
    var no01_Date: Date? = null,
    var no01_DeviceName: String? = null,
    var no01_Humidity: Double? = null,
    var no01_Temperature: Double? = null,
      :
    中略
      :
    var no07_Humidity: Double? = null,
    var no07_Temperature: Double? = null,
    ): RealmObject() {}
```

#### SensorList.kt
上記と同様の方法で、modelフォルダ内にSensorList.ktを作成する

```SensorList.kt
package com.mongodb.homeiotviewer.model

import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey
import org.bson.types.ObjectId;

open class SensorList (
    @PrimaryKey var _id: ObjectId = ObjectId(),
    var _partition: String? = null,
    var aircon: Boolean? = null,
    var humidity: Boolean? = null,
    var no: Long? = null,
    var place: String? = null,
    var power: Boolean? = null,
    var sensorname: String? = null,
    var temperature: Boolean? = null
): RealmObject() {}
```

### 4-4. グローバルインスタンス生成用クラス作成
アプリ全体で共有する、グローバルインスタンスを定義します。
具体的には下記となります

`app: Realmアプリケーションのインスタンス (Appクラス)`

#### クラスの作成
上記グローバルインスタンスを生成するクラスを、下記手順で作成します。
![35_make_global_realmapp_instance.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/8782eae5-f8cc-fdb4-2a15-08f3b6d622a9.png)

```InitRealm.kt
package com.mongodb.homeiotviewer

import android.app.Application
import android.util.Log

import io.realm.Realm
import io.realm.log.LogLevel
import io.realm.log.RealmLog
import io.realm.mongodb.App
import io.realm.mongodb.AppConfiguration

//Realmアプリケーションのインスタンス（グローバルインスタンスとして、アプリケーション全体で共有する）
lateinit var app: App

// global Kotlin extension that resolves to the short version
// of the name of the current class. Used for labelling logs.
inline fun <reified T> T.TAG(): String = T::class.java.simpleName

/*
* InitRealm: Sets up the Realm App and enables Realm-specific logging in debug mode.
*/
class InitRealm : Application() {

    override fun onCreate() {
        super.onCreate()
        //Realmライブラリの初期化
        Realm.init(this)
        //Realmアプリケーションにアクセスしインスタンス化
        app = App(
            AppConfiguration.Builder(BuildConfig.MONGODB_REALM_APP_ID)
                .build())

        // デバッグモード時に追加ロギングを有効に
        if (BuildConfig.DEBUG) {
            RealmLog.setLevel(LogLevel.ALL)
        }

        Log.v(TAG(), "Initialized the Realm App configuration for: ${app.configuration.appId}")
    }
}
```
#### マニフェストに登録
上記クラスをマニフェストに登録します。これによりアプリ起動時に上記インスタンスが初期化されるようです。
![40_add_globalinstance_class_to_manifest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/e00b526d-8b41-ae12-35e9-499905f414b6.png)
AndroidManifest.xmlに、下記のように追記します

```AndroidManifest.xml
    <application
          :
        android:name="com.mongodb.homeiotviewer.InitRealm">
          :
```

### 4-5. ログイン画面の作成
MongoDB Realmのユーザアカウント作成orログインする画面を作成します。

#### ログイン画面アクティビティの作成
下記操作で、ログイン画面のアクティビティ（Androidアプリにおける画面GUIに相当する部品）を作成します
javaフォルダ内のパッケージフォルダを右クリックし、New → Activity → Empty Activity
![37_make_login_activity1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/096ee540-06bf-c56e-25f6-fed241b13bd1.png)
名前を"LoginActivity"に変え、Finish
![38_make_login_activity2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/477ed9f4-251a-556d-69d2-b83f65b3424a.png)
#### ログイン画面のレイアウト作成
下記操作で、画面レイアウトactivity_login.xmlを編集します

先ほどの操作でapp → res → layoutに生成したactivity_login.xmlを開く
![39_edit_login_activity_layout.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/7e55f248-99d7-b5d1-07b8-d8c338e6ecb6.png)
activity_login.xmlを下記のように編集

```activity_login.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".LoginActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingLeft="24dp"
        android:paddingTop="12dp"
        android:paddingRight="24dp">

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp">

            <EditText
                android:id="@+id/input_username"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/username"
                android:inputType="text" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp">

            <EditText
                android:id="@+id/input_password"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/password"
                android:inputType="textPassword" />
        </com.google.android.material.textfield.TextInputLayout>

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/button_login"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:layout_marginBottom="12dp"
            android:padding="12dp"
            android:text="@string/login" />

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/button_create"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="24dp"
            android:padding="12dp"
            android:text="@string/create_account" />
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
```
上記だけだと変数指定部分でエラーが出るので、res → values 
→ strings.xmlに下記内容を追記します
![40_edit_stringsxml.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/4aa6efee-3503-e1b6-c053-c86ee2d2fa3b.png)

```strings.xml
<resources>
    <string name="app_name">RealmTutorial</string>//ここはプロジェクト名が元々記載されている
    <string name="username">Email</string>
    <string name="password">Password</string>
    <string name="create_account">Create account</string>
    <string name="login">Login</string>
    <string name="more">\u22EE</string>
    <string name="new_task">Create new task</string>
    <string name="logout">Log Out</string>
</resources>
```

Designタブで確認すると、下図のようなレイアウトになっているはずです。
![41_confirm_login_activity_layout.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/7bb12b7c-a343-a4f7-54cc-fccc2d1f1b71.png)

#### MainActivityからログイン画面への遷移作成
ログイン中ユーザが存在しないときはログイン画面に遷移するよう、
java → パッケージ名のフォルダ内にあるMainActivity.ktを下記のように書き換えます

```MainActivity.kt
package com.mongodb.homeiotviewer//パッケージ名に合わせて修正

import android.content.Intent
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import io.realm.mongodb.User

class MainActivity : AppCompatActivity() {
    private var user: User? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onStart() {
        super.onStart()
        //ログイン中ユーザの取得
        try {
            user = app.currentUser()
        } catch (e: IllegalStateException) {
            Log.w(TAG(), e)
        }
        //ログイン中ユーザが存在しない時、ログイン画面を表示する
        if (user == null) {
            // if no user is currently logged in, start the login activity so the user can authenticate
            startActivity(Intent(this, LoginActivity::class.java))
        }
    }
}
```

#### ログイン処理クラスの作成
ログイン画面でボタンを押した際の処理を、下記のように実装します。

java → パッケージ名のフォルダ内にあるLoginActivity.ktを下記のように書き換えます

```LoginActivity.kt
package com.mongodb.homeiotviewer

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import io.realm.mongodb.Credentials

class LoginActivity : AppCompatActivity() {
    //各種入力フォームを保持するインスタンス
    private lateinit var username: EditText//ユーザ名(Eメールアドレス)入力用テキストボックス
    private lateinit var password: EditText//パスワード入力用テキストボックス
    private lateinit var loginButton: Button//ログインボタン
    private lateinit var createUserButton: Button//新規ユーザ作成ボタン

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //入力フォームインスタンスの生成
        setContentView(R.layout.activity_login)
        username = findViewById(R.id.input_username)
        password = findViewById(R.id.input_password)
        loginButton = findViewById(R.id.button_login)
        createUserButton = findViewById(R.id.button_create)
        //ボタンを押したときの処理
        loginButton.setOnClickListener { login(false) }//ログインホタン
        createUserButton.setOnClickListener { login(true) }//新規ユーザ作成ボタン
    }

    override fun onBackPressed() {
        //戻るボタンでメイン画面に戻れないようにする(メイン画面に戻るにはログインが必須)
        // Disable going back to the MainActivity
        moveTaskToBack(true)
    }

    private fun onLoginSuccess() {
        //ログインに成功したら、メイン画面に戻る
        // successful login ends this activity, bringing the user back to the main activity
        finish()
    }

    private fun onLoginFailed(errorMsg: String) {
        //ログインに失敗したら、ログに書き込んだ上でメッセージ表示
        Log.e(TAG(), errorMsg)
        Toast.makeText(baseContext, errorMsg, Toast.LENGTH_LONG).show()
    }

    private fun validateCredentials(): Boolean = when {
        //ユーザ名とパスワードが空欄でないことを確認
        // zero-length usernames and passwords are not valid (or secure), so prevent users from creating accounts with those client-side.
        username.text.toString().isEmpty() -> false
        password.text.toString().isEmpty() -> false
        else -> true
    }

    /**
     * ログインボタンを押したときの処理
     * @param[createUser]:trueなら新規ユーザ作成、falseなら通常のログイン
     */
    // handle user authentication (login) and account creation
    private fun login(createUser: Boolean) {
        if (!validateCredentials()) {
            onLoginFailed("Invalid username or password")
            return
        }

        //処理中はボタンを押せないようにする
        // while this operation completes, disable the buttons to login or create a new account
        createUserButton.isEnabled = false
        loginButton.isEnabled = false

        val username = this.username.text.toString()
        val password = this.password.text.toString()

        if (createUser) {//新規ユーザ作成のとき
            // ユーザ名(E-mailアドレス)＋パスワードでユーザ作成実行
            // register a user using the Realm App we created in the TaskTracker class
            app.emailPasswordAuth.registerUserAsync(username, password) {
                // re-enable the buttons after user registration completes
                createUserButton.isEnabled = true
                loginButton.isEnabled = true
                if (!it.isSuccess) {//ユーザ作成失敗時は、メッセージを表示
                    onLoginFailed("Could not register user.")
                    Log.e(TAG(), "Error: ${it.error}")
                } else {//成功時は、そのまま通常ログイン
                    Log.i(TAG(), "Successfully registered user.")
                    // when the account has been created successfully, log in to the account
                    login(false)
                }
            }
        } else {//通常ログインのとき
            val creds = Credentials.emailPassword(username, password)
            app.loginAsync(creds) {
                // re-enable the buttons after
                loginButton.isEnabled = true
                createUserButton.isEnabled = true
                if (!it.isSuccess) {//ログイン失敗時は、メッセージを表示
                    onLoginFailed(it.error.message ?: "An error occurred.")
                } else {//成功時は、メイン画面に戻る
                    onLoginSuccess()
                }
            }
        }
    }
}
```
基本的な処理の流れは下記となります
・ログインボタン：ユーザ名＆パスワード入力確認 → ログイン → 成功したらメイン画面に遷移
・新規ユーザ作成ボタン：ユーザ名＆パスワード入力確認 → ユーザ作成 → 成功したらログインボタン処理に移る


### 4-6. ログアウト処理の作成

#### ログアウトメニューの追加
ログアウトメニューのレイアウトlogout_menu.xmlを、下記手順で追加します。

app → resを右クリックし、New → Android Resource Directoryを選択します
![50_make_option_menu_layout1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/ac75d65d-1f5c-8521-4241-eb1d489b8de3.png)
Resource Typeを「menu」に変更し、OKを押します
![51_make_option_menu_layout2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/8cc4021b-e04a-81fa-3b0e-406b625f2623.png)
生成したapp → res → menuフォルダを右クリックし、New → Menu Resource Fileを選択します
![52_make_option_menu_layout3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/d179b54e-4f52-0a19-d241-013d60f74750.png)
"logout_menu"と名前を付けてOKを押します
![41_make_option_menu_layout4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/2253fb85-b577-c2f7-27ed-543f4cd14a1c.png)

生成したlogout_menu.xmlを、下記のように書き換えます

```logout_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".CounterActivity">
    <item
        android:id="@+id/action_logout"
        android:orderInCategory="100"
        android:title="@string/logout"
        android:text="@string/logout"
        app:showAsAction="always"/>
</menu>
```

#### MainActivityにログアウト処理追加
作成したログアウトメニューをメインアクティビティに追加するため、
MainActivity.ktを下記のように書き換えます

```MainActivity.kt
package com.mongodb.homeiotviewer

import android.content.Intent
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import io.realm.Realm
import io.realm.mongodb.User
import io.realm.mongodb.sync.SyncConfiguration

class MainActivity : AppCompatActivity() {
    private lateinit var realm: Realm//Realmデータベースのインスタンス
    private var user: User? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //Realmデータベースのインスタンス初期化
        realm = Realm.getDefaultInstance()
    }

    override fun onStart() {
        super.onStart()
        //ログイン中ユーザの取得
        try {
            user = app.currentUser()
        } catch (e: IllegalStateException) {
            Log.w(TAG(), e)
        }
        //ログイン中ユーザが存在しない時、ログイン画面を表示する
        if (user == null) {
            // if no user is currently logged in, start the login activity so the user can authenticate
            startActivity(Intent(this, LoginActivity::class.java))
        }
    }

    override fun onStop() {
        super.onStop()
        user.run {
            realm.close()
        }
    }

    //アクティビティ終了時の処理(realmインスタンスをClose)
    override fun onDestroy() {
        super.onDestroy()
        // if a user hasn't logged out when the activity exits, still need to explicitly close the realm
        realm.close()
    }

    //logoutメニューをMainActivity上に設置
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.logout_menu, menu)
        return true
    }

    //logoutメニューを押したときの処理(ログアウト)
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_logout -> {
                user?.logOutAsync {
                    if (it.isSuccess) {
                        // always close the realm when finished interacting to free up resources
                        realm.close()
                        user = null
                        Log.v(TAG(), "user logged out")
                        startActivity(Intent(this, LoginActivity::class.java))
                    } else {
                        Log.e(TAG(), "log out failed! Error: ${it.error}")
                    }
                }
                true
            }
            else -> {
                super.onOptionsItemSelected(item)
            }
        }
    }
}
```

#### ログイン＆ログアウト動作確認
この段階でエミュレータでソフトを実行し、
ログイン＆ログアウト処理が正常にできることを確認しましょう
（[基本的にはこちらの記事と同じ方法でログイン＆ログアウトできます]()）

### 4-7. データの取得と表示
Realmの同期とデータ取得、および取得したデータを表示
#### 表示用レイアウトの準備
デフォルトで"hello world"と表示しているTextViewを、Realmから取得したデータ表示に流用します。

activity_main.xmlに
`android:id="@+id/query_test"`
という記述を追加します（IDを記載してコードからアクセスできるようにする）

```activitiy_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/query_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

#### 同期およびクエリ処理の追加
MainActivity.ktに、を追加します
・前節で作成したTextViewをインスタンス化し、文字を追加できるようにする処理
・同期およびクエリでデータ取得する処理

TextView関係の処理は、下記となります

```kotlin
      :
    private lateinit var queryTestView: TextView//クエリ結果表示用のtextViewインスタンス
      :
    override fun onCreate(savedInstanceState: Bundle?) {
          :
        //クエリ結果表示用のtextViewインスタンス作成
        queryTestView = findViewById(R.id.query_test)
```

同期およびクエリによるデータ取得処理は、下記となります

```kotlin
      :
    override fun onStart() {
          :
        //ログイン中ユーザが存在しないとき
        if (user == null) {
              :
        }
        //ログイン中ユーザが存在するとき
        else {
            //MongoDB Realmとの同期設定
            val partitionValue: String = "Project HomeIoT"//
            val config = SyncConfiguration.Builder(user!!, "Project HomeIoT")
                .waitForInitialRemoteData()
                .build()
            //上記設定をデフォルトとして保存
            Realm.setDefaultConfiguration(config)
            //非同期バックグラウンド処理でMongoDB Realmと同期実行
            Realm.getInstanceAsync(config, object: Realm.Callback() {
                override fun onSuccess(realm: Realm) {
                    //同期したRealmインスタンスを親クラスMainActivityのインスタンスに設定
                    this@MainActivity.realm = realm
                    //クエリ操作用インスタンス作成
                    val listQuery = realm.where<SensorList>()
                    ////////以下の処理は、取得するデータ内容に応じて変える////////
                    val result = listQuery.sort("no").findAll()
                    var resultString: String = ""
                    for(device in result){
                        resultString += "${device.no},${device.sensorname},${device.place}\n"
                    }
                    //クエリ結果表示用のtextViewインスタンス
                    queryTestView.text = resultString
                }
            })
        }
    }
```

公式ドキュメントを見る限り、データ同期しつつ内容を取得・更新したい場合、
**下記のコールバック処理の中にコードを記載する事**が肝要となりそうです。
（FragmentへのAdapter処理等もこの中に記載）

```kotlin
    Realm.getInstanceAsync(config, object: Realm.Callback() {
        override fun onSuccess(realm: Realm) {
            //この中にデータの取得、更新関係の処理を全て記載
        }
    }
```
上記の変更を加えたMainActivity.ktは、下記のようになるはずです

```MainActivity.kt
package com.mongodb.homeiotviewer

import android.content.Intent
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import com.mongodb.homeiotviewer.model.Sensor
import com.mongodb.homeiotviewer.model.SensorList
import io.realm.Realm
import io.realm.mongodb.User
import io.realm.mongodb.sync.SyncConfiguration
import io.realm.kotlin.where

class MainActivity : AppCompatActivity() {
    private lateinit var realm: Realm//Realmデータベースのインスタンス
    private var user: User? = null
    private lateinit var queryTestView: TextView//クエリ結果表示用のtextViewインスタンス

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //Realmデータベースのインスタンス初期化
        realm = Realm.getDefaultInstance()

        //クエリ結果表示用のtextViewインスタンス作成
        queryTestView = findViewById(R.id.query_test)
    }

    override fun onStart() {
        super.onStart()
        //ログイン中ユーザの取得
        try {
            user = app.currentUser()
        } catch (e: IllegalStateException) {
            Log.w(TAG(), e)
        }
        //ログイン中ユーザが存在しない時、ログイン画面を表示する
        if (user == null) {
            // if no user is currently logged in, start the login activity so the user can authenticate
            startActivity(Intent(this, LoginActivity::class.java))
        }
        //ログイン中ユーザが存在するとき
        else {
            //MongoDB Realmとの同期設定
            val partitionValue: String = "Project HomeIoT"//パーティションの名前
            val config = SyncConfiguration.Builder(user!!, "Project HomeIoT")
                .waitForInitialRemoteData()
                .build()
            //上記設定をデフォルトとして保存
            Realm.setDefaultConfiguration(config)
            //非同期バックグラウンド処理でMongoDB Realmと同期実行
            Realm.getInstanceAsync(config, object: Realm.Callback() {
                override fun onSuccess(realm: Realm) {
                    //同期したRealmインスタンスを親クラスMainActivityのインスタンスに設定
                    this@MainActivity.realm = realm
                    //クエリ操作用インスタンス作成
                    val listQuery = realm.where<SensorList>()//sensor_listsコレクション
                    val sensorQuery = realm.where<Sensor>()//sensorsコレクション
                    ////////以下の処理は、取得するデータ内容に応じて変える////////
                    val result = listQuery.sort("no").findAll()
                    var resultString: String = ""
                    for(device in result){
                        resultString += "${device.no},${device.sensorname},${device.place}\n"
                    }
                    //クエリ結果をtextViewに表示
                    queryTestView.text = resultString
                }
            })
        }
    }

    override fun onStop() {
        super.onStop()
        user.run {
            realm.close()
        }
    }

    //アクティビティ終了時の処理(realmインスタンスをClose)
    override fun onDestroy() {
        super.onDestroy()
        // if a user hasn't logged out when the activity exits, still need to explicitly close the realm
        realm.close()
    }

    //logoutメニューをMainActivity上に設置
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.logout_menu, menu)
        return true
    }

    //logoutメニューを押したときの処理(ログアウト)
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_logout -> {
                user?.logOutAsync {
                    if (it.isSuccess) {
                        // always close the realm when finished interacting to free up resources
                        realm.close()
                        user = null
                        Log.v(TAG(), "user logged out")
                        startActivity(Intent(this, LoginActivity::class.java))
                    } else {
                        Log.e(TAG(), "log out failed! Error: ${it.error}")
                    }
                }
                true
            }
            else -> {
                super.onOptionsItemSelected(item)
            }
        }
    }
}
```

上記のコードを実行すると、下図のように
`sensor_listコレクションを"no"の順でソートした、"device","sensorname","place"フィールド`
が画面に表示されるはずです。
![50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/ea681f20-a1e2-c254-030e-5e920bbc29b1.png)
下記のクエリ部分の処理を書き換えることで、取得するデータ内容を変えることができます。
次章で何種類か試してみます。

```kotlin
    ////////以下の処理は、取得するデータ内容に応じて変える////////
    val result = listQuery.sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
```

## 5. クエリ操作
前章の最後で触れたクエリ処理内容を変更し、どのようなデータが取得できるか試してみます

### ソート順を降順に
ソート引数にSort.DESCENDINGを指定

```kotlin
    val result = listQuery.sort("no", Sort.DESCENDING).findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
```
![sort.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/b1f2d1dc-aaae-72e5-3ba6-66578ed7dcdb.png)

### フィルタ処理
#### 一致
.equalTo()を使用
※不一致を表す.notEqualTo()もあります

```kotlin
    val result = listQuery.equalTo("place", "indoor").sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
```
![equalto.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/89657ad0-35f9-57a2-6fa4-80bfef6124f3.png)

#### 不等号
.greaterThan()または.lessThan()を使用
※等号込みの.greaterThanOrEqualTo()、.lessThanOrEqualTo()もあります

```kotlin
    val result = listQuery.greaterThan("no", 4).sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
```
![greater.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/5bff4e86-1f85-ce39-d0a7-d37769697072.png)

#### 範囲
.between()を使用

```kotlin
    val result = listQuery.between("no", 2, 6).sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
```
![between.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/673c054f-6730-494c-3f40-949eb0f7d02e.png)

#### 配列指定
.in()を使用

```kotlin
    val result = listQuery.`in`("place", arrayOf("kitchen","outdoor")).sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
```
![in.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/ecd73775-2b6c-0194-1681-5d38b8fbe46d.png)

### 文字列操作
.beginsWith()、.contains()、.endsWith()、.like()を使用
※likeのワイルドカード指定方法は[公式ドキュメントを参照ください](https://docs.mongodb.com/realm/android/query-engine/)

```kotlin
    val result = listQuery.beginsWith("sensorname", "Omron").sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
```
![beginswith.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/c7ecd8f2-2c32-99db-bc5a-d234af2b5091.png)

### 論理演算
.and()、.not()、.or()を使用

```kotlin
    val result = listQuery.beginsWith("sensorname", "Inkbird").and().equalTo("place","indoor").sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/8adc9d54-ce70-e524-1583-41a54186a429.png)

### 統計量
以降はsensorsコレクションのデータを使用します
#### 平均、合計
.average()、.sum()を使用

```kotlin
    val result = sensorQuery.average("no01_Temperature")
    val resultString = result.toString()
```
![average.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/716c0297-41b5-7c6d-53e0-587cf341b035.png)
※センサno.01の平均温度を表示しています

#### 最大、最小
.max()、.min()を使用

```kotlin
    val result = sensorQuery.min("no01_Temperature")
    val resultString = result.toString()
```
![min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/d53da73f-8980-21c3-1cd7-c426d9ec93c9.png)
※センサno.01の最低温度を表示しています

####個数
.count()を使用

```kotlin
    val result = sensorQuery.isNull("no03_Temperature").count()
    val resultString = result.toString()
```
![nullcount.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/bde6859d-806e-7cac-98d3-5a56c834bce4.png)
※isNull()と組み合わせて、"no03_Temperature"フィールドのnull数をカウントしています

### 1要素取得
#### 最初の要素
.findFirst()を使用して、並び替えた最初の要素を取得できます

```kotlin
    val result = sensorQuery.sort("Date_Master").findFirst()
    val resultString = "${result?.Date_Master}\n" +
        "${result?.no01_Temperature}゜C\n"+
        "${result?.no01_Humidity}%\n"
```
![first.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/e10776bc-2dff-f8c8-3e80-7ab019bd8592.png)
※"Date_Master"順で最初の要素のNo01_TemperatureとNo01_Humidityを表示しています

#### 最後の要素
Sort.DESCENDINGと組み合わせて、並び替えた最後の要素を取得できます

```kotlin
    val result = sensorQuery.sort("Date_Master", Sort.DESCENDING).findFirst()
    val resultString = "${result?.Date_Master}\n" +
        "${result?.no01_Temperature}゜C\n"+
        "${result?.no01_Humidity}%\n"
```
![last.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/6896fd6e-c5d5-5f52-1305-51a4c40acfa2.png)
※"Date_Master"順で最初の要素のNo01_TemperatureとNo01_Humidityを表示しています

### Read以外のクエリ
今回はRead関係のクエリのみ紹介しましたが、
Write、Update関係のクエリも存在します。
下記の公式ドキュメントをご参照ください
https://docs.mongodb.com/realm/android/


# トラブルシューティング
私がハマったのは以下の部分です
## 「スキーマの"title"」と「データモデルクラス」の名称一致
私が一番ハマって時間を浪費してしまったのが、この部分です

MongoDB Realmでは、クラウド上のDBと、スマホアプリ上のKotlinデータモデルとの同期をとる際に、
`クラウド上のDB: スキーマの"title"`
`Kotlinデータモデル: クラスの名称`
の一致をもって、同期を実施しているようです。

ここで厄介なのが、両者の**命名規則の違い**です。
`クラウド上のDB: DBのテーブル命名規則にのっとって、snake_case（小文字アンダーバー区切り）`
`Kotlinデータモデル: Kotlinのクラス命名規則にのっとって、PascalCase（単語の最初を大文字に）`
が利用されるのが一般的です。

私は最初、例えばsensorlistコレクションでは

||||
|---|---|---|
|クラウド上のDB|コレクション名: "sensor_lists"|スキーマのtitle: "sensor_lists"|
|Kotlinデータモデル||クラス名"SensorList"|

と設定していましたが、これではスキーマのtitleとKotlinクラス名が一致していないため、
両者の同期が取れませんでした。
####対策
色々調べてみて、**両者の名称の一致**が同期の条件となることが分かったので、

||||
|---|---|---|
|クラウド上のDB|コレクション名: "sensor_lists"|**スキーマのtitle: "SensorList"**|
|Kotlinデータモデル||クラス名"SensorList"|

のように変更したところ、正常に同期できるようになりました。

## 型の変化による同期失敗
MongoDBには、Postした際に自動で型を判定してくれる便利な機能がありますが、
この機能より、スキーマを定義した際とは思いがけず型が変わってしまい、同期が失敗してしまうことがあります。

私の場合、スキーマ定義時には"no01_Temperature"フィールド(Nature Remoの気温)が
`30.4` → MongoDBでdouble型
と判定されていたのが、あるタイミングで測定温度が整数となり
`30` → MongoDBでint型
と判定されてしまい、型がスキーマと異なっているために同期に失敗していました
#### 対策
Postする前に、Pythonでfloat型にキャストするように変更しました

```python:before
'Temperature': sensorValue['Temperature'],
```

```python:after
'Temperature': float(sensorValue['Temperature']),
```

また、このようなスキーマと異なるデータが存在するかどうかの確認は
`MongoDB Realmクラウド画面のRulesタブ → 対象コレクションを選択 → Schemaタブ → VALIDATEボタン`
で実施可能です。

# さいごに
以上で、バックエンド処理の作成は完了しました。

一方で、**スマホアプリとして実用的なものとするには、フロントエンド処理を実装してUIを作成する必要があります**
フロントエンド部分は本記事の趣旨から外れるので、**以下の記事**に再び戻って解説させて頂きます
（IoTデータをグラフ表示するAndroidアプリを作成しました）

https://qiita.com/c60evaporator/items/31d9d405b9904af54e87#5-リソースファイルの作成