はじめに
自宅で運用しているRaspberryPiの死活監視システムを構築したいと思い、GUIを含めたシステムを簡単に構築が出来そうなAzure IoT Centralを使用してみようと思います。
#####やりたいこと
- リソース情報をダッシュボードに表示
- CPU・メモリ使用率 等のリソース増加の通知
- デバイスのシステムダウン(ラズパイからの通信無し) の通知
環境
- Raspberry Pi 4 Model B
- Python : 3.7.3
- azure-iot-device : 2.7.1 <pipパッケージ>
- sysstat : 11.5.2 <debパッケージ>
インストール
$ pip3 install azure-iot-device
$ sudo apt install sysstat
システム構成
ラズパイからCPU・メモリ 等のデバイスリソース情報をAzure IoT Centralに送信して、ダッシュボードにリアルタイムでリソース情報を表示します。また、リソース情報が一定条件を満たすとメールを送信します。
ダッシュボード
ラズパイから送信された、CPU使用率、メモリ使用率、ディスク使用率を数値とグラフで表示しています。変動が多いCPUとメモリのみをグラフ表示しています。
数値を表示しているパネルの色は、数値によって変更できるようになっていて、「0~70は青、70~90は黄、90~100は赤」みたいな感じで、ぱっと見で状態が分かるようになっています。
通知メール
「CPU使用率:90%以上」or「メモリ使用率:85%」の状態を検知すると以下のメールが送られ、デバイスのリソース使用率増加をすぐに知ることが出来ます。
ラズパイの設計
送信リソース情報
デバイスのリソース情報をjson形式に整形してAzure IoT Centralに送信します。
※ コア毎のCPU使用率を送信していますが、今回は「All」のみを使用してます。
{
"CpuUsage": {
"All": 25.63,
"Core0": 58.16,
"Core1": 16.32,
"Core2": 11.34,
"Core3": 16.67
},
"MemoryUsage": 51.70,
"DiskUsage": 92,
"MsgId": 7
}
Pythonプログラム
Azure IoT Centralの「SCOPE_ID」,「DEVICE_ID」,「DEVICE_KEY」が環境変数に設定されていることが前提条件のプログラムとなっています。main.py
では通信コネクション及び、取得したリソース情報の定期送信を行っています。
import os
import sys
import json
import asyncio
from systemMoniter import SystemMoniter
from deviceProvisioningService import Device
from azure.iot.device.aio import IoTHubDeviceClient
def message_received_handler(message):
# ログ出力のみ
print("the data in the message received was ")
print(message.data)
print("custom properties are")
print(message.custom_properties)
async def main():
scopeID = None
deviceId = None
key = None
# 環境変数の取得
scopeID = os.getenv('SCOPE_ID')
deviceId = os.getenv('DEVICE_ID')
key = os.getenv('DEVICE_KEY')
if scopeID is None or deviceId is None or key is None:
sys.exit(1)
# Azureとの通信に必要な「connection string」の取得
dps = Device(scopeID, deviceId, key)
conn_str = await dps.connection_string
# コネクション確立
device_client = IoTHubDeviceClient.create_from_connection_string(conn_str)
await device_client.connect()
# 受信ハンドラの設定、Azureからコマンドを受信するとこの関数が呼び出される
device_client.on_message_received = message_received_handler
systemMoniter = SystemMoniter()
while True:
try:
# リソース情報取得
telemetry = await systemMoniter.getSystemStatus()
if telemetry is not None:
# ログ出力、json形式に変換し送信
print(telemetry)
data = json.dumps(telemetry)
await device_client.send_message(data)
await asyncio.sleep(10)
except:
print("Unexpected error:", sys.exc_info()[0])
# コネクション切断
await device_client.disconnect()
if __name__ == "__main__":
asyncio.run(main())
# If using Python 3.6 or below, use the following code instead of asyncio.run(main()):
# loop = asyncio.get_event_loop()
# loop.run_until_complete(main())
# loop.close()
import asyncio
from azure.iot.device.aio import ProvisioningDeviceClient
class Device():
def __init__(self, scope, device_id, key):
self.scope = scope
self.device_id = device_id
self.key = key
async def __register_device(self):
provisioning_device_client = ProvisioningDeviceClient.create_from_symmetric_key(
provisioning_host='global.azure-devices-provisioning.net',
registration_id=self.device_id,
id_scope=self.scope,
symmetric_key=self.key,
)
return await provisioning_device_client.register()
@property
async def connection_string(self):
results = await asyncio.gather(self.__register_device())
registration_result = results[0]
# build the connection string
conn_str = 'HostName=' + registration_result.registration_state.assigned_hub + \
';DeviceId=' + self.device_id + \
';SharedAccessKey=' + self.key
return conn_str
import subprocess
class SystemMoniter():
def __init__(self) -> None:
self.msgId = 0
async def getSystemStatus(self) -> dict:
telemetry = {}
self.msgId += 1
telemetry.update(self._cpuUpuUsage())
telemetry.update(self._memoryUsage())
telemetry.update(self._diskUsage())
telemetry["MsgId"] = self.msgId
return telemetry
def _cpuUsage(self) -> dict:
ret = {}
try:
usages = {}
cmd = R"LC_ALL=C sar -P ALL 1 1 | grep Average"
cpuUsage = subprocess.check_output(cmd, shell=True).decode('utf-8')
list = cpuUsage.splitlines()[1:]
for num in range(0, len(list)):
value = list[num].split()
if(num == 0): property = "All"
else: property = R"Core" + str(num-1)
# cpuUsage = %user + %nice + %system + %iowait + %steal
usages[property] = float(value[2])+float(value[3])+float(value[4])+float(value[5])+float(value[6])
ret["CpuUsage"] = usages
except:
print("Failed get the cpu usages.")
ret = {}
return ret
def _memoryUsage(self) -> dict:
ret = {}
try:
cmd = R"free -k | grep Mem:"
cpuUsage = subprocess.check_output(cmd, shell=True).decode('utf-8')
list = cpuUsage.split()
ret["MemoryUsage"] = (float(list[2])*100)/float(list[1])
except:
print("Failed get the memory usage.")
ret = {}
return ret
def _diskUsage(self) -> dict:
ret = {}
try:
cmd = R"df | grep /dev/root | awk '{print $5}' | sed 's/%//'"
diskUsage = subprocess.check_output(cmd, shell=True).decode('utf-8')
ret["DiskUsage"] = float(diskUsage)
except:
print("Failed get the disk usage.")
ret = {}
return ret
Azure IoT Central の設計
デバイステンプレート
ラズパイから送信するjsonデータに合わせてパラメータを設定します。
規則(アクション)
以下のように条件を設定してメールを送るようにしています。
パラメータの数値によって条件を付けることが出来るが、「受信できなかった」等の条件付けは無く、デバイスからデータが送信されていることが前提の条件付けになっている。その為、やりたかった「デバイスのシステムダウン(ラズパイからの通信無し) の通知」は現状できなさそう。
最後に
今回はAzure IoT Centralを使用して死活監視システムを構築しました。想像していたより簡単に作ることが出来たため、かなり驚いています。これぐらいのシステムなら構想から構築まで1~2日あれば実現できると思います。
気になる点としては、ダッシュボードや規則(アクション)についてはカスタマイズ性がないため、複雑なGUIや規則は設計できませんでした。複雑なことがしたいなら自前で作る必要があります。PoCレベルでさっと作る分ならかなり使えるサービスだと感じました。
やりたかった「デバイスのシステムダウン(ラズパイからの通信無し) の通知」が出来なかったのは残念でしたが、今後のアップデートに期待したいと思います。
参考
以下の情報を参考にさせていただきました。
Raspberry-Pi-Python-Environment-Monitor-with-the-Pimoroni-Enviro-Air-Quality-PMS5003-Sensor