[PYTHON] Jouez pour informer Slack des données environnementales de SensorTag à l'aide d'AWS PaaS via Raspberry Pi3

Aperçu

--Je souhaite mesurer des données environnementales chronologiques à l'aide de SensorTag --Je joue à un Raspberry Pi3, donc je veux l'utiliser --Je veux essayer des AWS PaaS - DynamoDB、Lambda、API Gateway --Je veux surveiller et utiliser Slack (je ne veux pas écrire UI / UX)

→ En conséquence, nous avons créé quelque chose qui fonctionne, donc je vais le résumer.

Vue d'ensemble

SensorTag2Slack.png La figure a été créée sur www.draw.io. Super pratique.

Envoyer des données de SensorTag vers Raspberry Pi3

J'ai utilisé le SensorTag de TI CC2650stk. Il possède de nombreux capteurs tels que l'accélération, la température, l'humidité, l'éclairement, le gyroscope, le géomagnétisme et la pression.

Alimentation SensorTag

Le SensorTag est essentiellement alimenté par batterie. Cependant, si vous déplacez beaucoup le capteur, la consommation d'énergie sera élevée. Cette fois, je veux obtenir toutes les informations du capteur dans l'ordre chronologique. Attachez Debug Board à SensorTag, connectez-vous à RaspberryPi3 via USB et connectez-vous. L'alimentation était fournie par un câble USB. Cela vous permet de mesurer pendant une longue période sans vous soucier de la batterie.

SensorTag marche / arrêt

J'ai fait référence à ce site. http://kinokotimes.com/2017/03/07/usb-control-method-by-raspberry-pi/ [BeagleBoneBlackBox_USB Power Control](http://www.si-linux.co.jp/techinfo/index.php?BeagleBoneBlackBox_USB%E9%9B%BB%E6%BA%90%E5%88%B6%E5%BE% A1)

Je collecte les données du capteur avec l'application côté Raspberry Pi. Parfois, l'application plante et redémarre. À ce moment-là, l'alimentation de SensorTag est également redémarrée au cas où. Que cela soit approprié ou non, cela rendra l'état de démarrage uniforme.

usbrefresh.sh


#! /bin/sh
hub-ctrl -h 0 -P 2 -p 0
sleep 1
hub-ctrl -h 0 -P 2 -p 1

Collection avec bluepy

J'ai fait référence à ce site. http://taku-make.blogspot.jp/2015/02/blesensortag.html http://dev.classmethod.jp/hardware/raspberrypi/sensortag-raspberry-pi-2-script/ Il existait diverses bibliothèques liées au BLE pour collecter des informations à partir d'étiquettes de capteur. bluepy a fonctionné le plus facilement, alors maintenant.

Envoyer vers AWS IoT

[Exemple de code] du kit SDK AWS (https://github.com/aws/aws-iot-device-sdk-python/blob/master/samples/basicPubSub/basicPubSub.py) a été utilisé tel quel. Licence Apache 2.0. Nous avons apporté des modifications telles que la partie à acquérir par BLE et la partie à créer JSON. Fondamentalement, je pense que le code est basé sur l'hypothèse que la communication sera effectuée en utilisant les informations d'identification X.509.

sendMQTT.py


'''
/*
 * Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
 '''

###-----------------------------------------------------------------------------

from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
import sys
import logging
import time
import argparse
from btle import UUID, Peripheral, DefaultDelegate, AssignedNumbers
import struct
import math
from sensortag import SensorTag, KeypressDelegate
import json
from datetime import datetime

###-----------------------------------------------------------------------------

# Custom MQTT message callback
def customCallback(client, userdata, message):
        print("--------------")
	print("Received  : " + message.payload)
	print("from topic: " + message.topic)
	print("--------------\n\n")

###-----------------------------------------------------------------------------

# Read in command-line parameters
parser = argparse.ArgumentParser()

### AWS!
parser.add_argument("-e", "--endpoint", action="store", required=True, dest="host", help="Your AWS IoT custom endpoint")
parser.add_argument("-r", "--rootCA", action="store", dest="rootCAPath", default="root-CA.crt", help="Root CA file path")
parser.add_argument("-c", "--cert", action="store", dest="certificatePath", default="certificate.pem.crt",help="Certificate file path")
parser.add_argument("-k", "--key", action="store", dest="privateKeyPath", default="private.pem.key",help="Private key file path")
parser.add_argument("-w", "--websocket", action="store_true", dest="useWebsocket", default=False,
                    help="Use MQTT over WebSocket")
parser.add_argument("-id", "--clientId", action="store", dest="clientId", default="Raspi_1", help="Targeted client id")
parser.add_argument("-t", "--topic", action="store", dest="topic", default="etl/room", help="Targeted topic")

### SensorTag!
parser.add_argument('-n', action='store', dest='count', default=0,
        type=int, help="Number of times to loop data")
parser.add_argument('-T',action='store', dest="sleeptime", type=float, default=5.0, help='time between polling')
parser.add_argument('-H', action='store', dest="taghost", help='MAC of BT device')
parser.add_argument('--all', action='store_true', default=True)

args = parser.parse_args()
host = args.host
rootCAPath = args.rootCAPath
certificatePath = args.certificatePath
privateKeyPath = args.privateKeyPath
useWebsocket = args.useWebsocket
clientId = args.clientId
topic = args.topic
sleeptime = args.sleeptime
deviceID = args.clientId

###=============================================================================

if args.useWebsocket and args.certificatePath and args.privateKeyPath:
	parser.error("X.509 cert authentication and WebSocket are mutual exclusive. Please pick one.")
	exit(2)

if not args.useWebsocket and (not args.certificatePath or not args.privateKeyPath):
	parser.error("Missing credentials for authentication.")
	exit(2)

###=============================================================================

# Enabling selected sensors
print('Connecting to ' + args.taghost)
tag = SensorTag(args.taghost)
if args.all:
    tag.IRtemperature.enable()
    tag.humidity.enable()
    tag.barometer.enable()
    tag.accelerometer.enable()
    tag.magnetometer.enable()
    tag.gyroscope.enable()
    tag.battery.enable()
    tag.keypress.enable()
    tag.setDelegate(KeypressDelegate())
    tag.lightmeter.enable()
    time.sleep(1.0)

###=============================================================================

# Configure logging
logger = logging.getLogger("AWSIoTPythonSDK.core")
logger.setLevel(logging.DEBUG)
streamHandler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
streamHandler.setFormatter(formatter)
logger.addHandler(streamHandler)

###=============================================================================

# Init AWSIoTMQTTClient
myAWSIoTMQTTClient = None
if useWebsocket:
	myAWSIoTMQTTClient = AWSIoTMQTTClient(clientId, useWebsocket=True)
	myAWSIoTMQTTClient.configureEndpoint(host, 443)
	myAWSIoTMQTTClient.configureCredentials(rootCAPath)
else:
	myAWSIoTMQTTClient = AWSIoTMQTTClient(clientId)
	myAWSIoTMQTTClient.configureEndpoint(host, 8883)
	myAWSIoTMQTTClient.configureCredentials(rootCAPath, privateKeyPath, certificatePath)

# AWSIoTMQTTClient connection configuration
myAWSIoTMQTTClient.configureAutoReconnectBackoffTime(1, 32, 20)
myAWSIoTMQTTClient.configureOfflinePublishQueueing(-1)  # Infinite offline Publish queueing
myAWSIoTMQTTClient.configureDrainingFrequency(0.5)  # Draining: 2 Hz
myAWSIoTMQTTClient.configureConnectDisconnectTimeout(10)  # 10 sec
myAWSIoTMQTTClient.configureMQTTOperationTimeout(5)  # 5 sec

# Connect and subscribe to AWS IoT
myAWSIoTMQTTClient.connect()
#myAWSIoTMQTTClient.subscribe(topic, 1, customCallback)
time.sleep(2)

###=============================================================================

# Publish to the same topic in a loop forever
loopCount = 0
Payload = {}
while True:
    Payload['ID'] = str(deviceID)

    ambient_temp, target_temparature = tag.IRtemperature.read()
    Payload["AmbientTemp"] = ambient_temp
    Payload["TargetTemp"] = target_temparature

    ambient_temp, rel_humidity = tag.humidity.read()
    Payload["Humidity"] = rel_humidity

    ambient_temp, pressure_millibars = tag.barometer.read()
    Payload["Barometer"] = pressure_millibars

    Acc_x, Acc_y, Acc_z = tag.accelerometer.read()
    Payload["AccX"] = Acc_x
    Payload["AccY"] = Acc_y
    Payload["AccZ"] = Acc_z

    magnet_x, magnet_y, magnet_z = tag.magnetometer.read()
    Payload["MagnetX"] = magnet_x
    Payload["MagnetY"] = magnet_y
    Payload["MagnetZ"] = magnet_z

    gyro_x, gyro_y, gyro_z = tag.gyroscope.read()
    Payload["GyroX"] = gyro_x
    Payload["GyroY"] = gyro_y
    Payload["GyroZ"] = gyro_z

    Payload["Light"] = tag.lightmeter.read()
    Payload["Batterys"] = tag.battery.read()

    Payload["Count"] = loopCount
    Payload["Datetime"] = datetime.now().strftime("%Y/%m/%d %H:%M:%S")

#    print("try to send!")
    myAWSIoTMQTTClient.publish(topic, json.dumps(Payload), 1)
#    print("end!")
    loopCount += 1
    time.sleep(sleeptime)

Surveillance de la vie et de la mort des programmes Python

Le programme ci-dessus échoue parfois. Il peut être judicieux de débuter sérieusement, C'est une tarte aux râpes et c'est Python, alors j'ai décidé qu'il tomberait. Exécutez un script comme celui ci-dessous avec cron une fois par minute. Vous voudrez peut-être quitter sendMQTT.py à chaque fois et le démarrer avec juste cron, Il est également difficile de démarrer Maikai Python- Il existe également une partie de Python qui compte les répétitions. Je sais aussi que cela fonctionne environ 10 000 fois sans tomber, donc cron essaie de surveiller la vie et la mort.

Au fait, avec crontab -e

sendSensorData.sh


#!/bin/sh

ps | grep python
if [ "$?" -eq 0 ]
then
  logger "python is exist.  exit..."
  exit 0
else

  logger "start reset sequence..."
  sudo usbrefresh.sh
  sleep 3

  cd /home/pi/deviceSDK
  python ./sendMQTT.py -T 59 -H "AA:BB:CC:DD:EE:FF" -e awsiotnoarn.iot.ap-northeast-1.amazonaws.com  >/dev/null 2>&1 & 

  cd
  exit 0
fi

Précision pour faire fonctionner chaque cycle

Il y a une partie du script ci-dessus qui dit -T 59. Cela signifie dormir pendant 59 secondes dans une boucle infinie dans le script Python. Il existe des fluctuations subtiles telles que le temps d'acquisition des données avec BLE et le temps de communication avec AWS. Il peut être possible d'utiliser un système d'exploitation en temps réel et d'insérer une interruption toutes les minutes, etc. Puisqu'il est composé de tarte à la râpe, de jessie raspbienne et de Python, il est jugé qu'il est impossible d'atteindre ce point, et il est divisible. Vous pouvez en fait obtenir des données chronologiques, mais pas exactement toutes les minutes. Les données montrent qu'il y a quelques secondes de gigue.

AWS PaaS Le côté AWS est construit sans serveur. Pour étudier.

AWS IoT J'ai fait référence à ce site. http://qiita.com/nsedo/items/c6f33c7cadea7023403f Le reste est toujours le SDK AWS. MQTT crée et lance simplement JSON, je n'ai donc pas à me soucier de l'utilisation d'AWS IoT. Il reçoit les données JSON et les place dans DynamoDB telles quelles.

DynamoDB

DB pour enregistrer les données de séries chronologiques

La clé est le nom de l'ID et les informations d'heure. J'ai entendu quelque part que cette méthode est bonne pour créer des données de séries chronologiques avec l'IoT. D'autres sont comme organiser les données acquises. Lors de l'analyse de données chronologiques, nous n'organisons que les données non structurées dans la base de données. Comme vous pouvez le voir, il y a un endroit où j'ai créé un JSON et l'ai lancé tel quel.

DB pour enregistrer l'état

La clé est uniquement le nom de l'ID. La valeur est --Data: Le dernier JSON reçu tel quel --isNight: indique s'il reconnaît ou non que la lumière est allumée est. Cette fois, nous ne traitons que des changements dans les informations de Lumière, donc nous ne conservons que cet état. Afin de rendre le côté Lambda sans état, l'état est destiné à DynamoDB.

Juste au cas où, si vous l'écrivez en JSON, ce seront de telles données. Pour la base de données de séries chronologiques, seuls les éléments de données suivants sont alignés.

room.json


{
  "Data": {
    "AccX": {
      "N": "0.915283203125"
    },
    "AccY": {
      "N": "-0.129150390625"
    },
    "AccZ": {
      "N": "0.48974609375"
    },
    "AmbientTemp": {
      "N": "31.78125"
    },
    "Barometer": {
      "N": "1006.03"
    },
    "Batterys": {
      "N": "100"
    },
    "Count": {
      "N": "261"
    },
    "Datetime": {
      "S": "2017/09/06 13:31:35"
    },
    "GyroX": {
      "N": "-4.0740966796875"
    },
    "GyroY": {
      "N": "1.85394287109375"
    },
    "GyroZ": {
      "N": "1.983642578125"
    },
    "Humidity": {
      "N": "40.95458984375"
    },
    "ID": {
      "S": "Raspi_1"
    },
    "Light": {
      "N": "57.88"
    },
    "MagnetX": {
      "N": "38.83418803418803"
    },
    "MagnetY": {
      "N": "-19.34212454212454"
    },
    "MagnetZ": {
      "N": "-17.842735042735043"
    },
    "TargetTemp": {
      "N": "23.78125"
    }
  },
  "ID": "Raspi_1",
  "isNight": "0"
}

Lambda C'est embarrassant car il y a de nombreux endroits qui ne sont pas écrits correctement. .. .. La partie d'Exception et le contenu arrivent à Slack comme un endroit où je ne peux pas arriver à une conclusion tant que je suis inquiet. Je ne sais pas quoi faire dans un tel cas. Je ne veux pas être exécuté à plusieurs reprises lorsqu'une exception se produit dans Lambda, ou je ne veux pas publier à plusieurs reprises une erreur dans Slack, alors je me demandais quoi faire, mais j'en ai fait une tâche future. Je ne connais pas non plus les spécifications de Lambda.

Construire une table de salle qui contient l'état de la pièce est également presque approprié, Clé: ID Rasppie, ici "Raspi_1" data: Toutes les données de SensorTag, Kita guy Lumière: l'état actuel est-il déterminé comme étant «activé» ou «désactivé»? C'est un dessin.

slackpost C'est là que Lambda reçoit les données du flux dynamoDB. Obtenez des informations Light de la table ETLRoom et comparez-les avec la table actuelle Il détermine si la lumière est allumée ou éteinte et l'affiche pour se détendre en cas de changement.

slackpost.py


# coding : utf-8
import json
import datetime
import requests
import boto3

LIGHT_TAG='Light'

#===============================================================================
# Slack Incomming Webhooks URL
SLACL_POST_URL = "https://hooks.slack.com/services/XXXXX/YYYYY/ZZZZZ"

# Post to Slack
def PostSlack(message, icon=':ghost:', username='ETLBot', channel="#room"):
    
    Dict_Payload = {
    "text": message,
    "username": username,
    "icon_emoji": icon,
    "channel": channel,
    }
    return requests.post(SLACL_POST_URL, data=json.dumps(Dict_Payload))

#-------------------------------------------------------------------------------
    
def Check_LightChanges(new, old, IsNight):

    Change2Night = None
    Change2Morining = None
    print("new:" , new, ", old:", old, ", IsNight:", IsNight)
    
    if (IsNight=='1') and (new > (old + 50)):
        Change2Morining = True
        IsNight = '0'
    elif (IsNight=='0') and (new < 10):
        Change2Night = True
        IsNight = '1'
    
    # Down -> UP
    if Change2Morining:
        message = ":smiley: Light is Turned On. Good Morning! :smiley:"
        icon = ":smiley:"
    # UP -> Down
    elif Change2Night:
        message = ":ghost: Light is Turned Down. Good Bye! :ghost:"
        icon = ":ghost:"
    else:
        return IsNight

    PostSlack(message, icon=icon)
    return IsNight
#-------------------------------------------------------------------------------

table = None
def update_table(data):
    ID = data['ID']['S']
    
    # Access to ETLRoom Table
    global table
    if not table:
        table = boto3.resource('dynamodb').Table('ETLRoom')
    response = table.get_item(Key={'ID': ID})
    if response:
        item = response['Item']
        #PostSlack(json.dumps(item))
        light = round(float(item['Data'][LIGHT_TAG]['N']))
        IsNight = item['IsNight']
    else:
        light = 0

    IsNight = Check_LightChanges(round(float(data[LIGHT_TAG]['N'])), light, IsNight)
    
    # Update Room Table
    response = table.put_item(
    Item={
          "ID": ID,
          "Data" : data,
          "IsNight": IsNight
        }
    )

    return 0

#-------------------------------------------------------------------------------

def lambda_handler(event, context):
    
    try:
        for record in event['Records']:
            dynamodb = record['dynamodb']
            keys = dynamodb['Keys']
            data = dynamodb['NewImage']
        
        # Keys are "ID" and "Datetime".
        id = keys['ID']['S']
        datetime = keys['Datetime']['S']
        print("ID:", id, "/ Date:", datetime)
        
        update_table(data)
    except Exception as e:
        import traceback
        message = traceback.format_exc()
        print(message)
        PostSlack('Meets Exception!\n' + message)
        raise e
        
    return 0

#===============================================================================

getroomenv

getroomenv.py


# coding : utf-8
import json
import requests
import boto3

# Slack Incomming Webhooks URL
SLACL_POST_URL = "https://hooks.slack.com/services/XXXXX/YYYYY/ZZZZZ"

#===============================================================================

def MakeStr(data, key, round_n):
    return str(round(float(data[key]['N']), round_n))

table = None
def GetRoomEnv(id, isAll):
    
    global table
    if not table:
        table = boto3.resource('dynamodb').Table('ETLRoom')

    response = table.get_item(
        Key={
            'ID': id
        }
    )
    
    data = response['Item']['Data']
    light = MakeStr(data, 'Light', 1)
    temp = MakeStr(data, 'TargetTemp', 1)
    humid = MakeStr(data, 'Humidity', 1)
    balo = MakeStr(data, 'Barometer', 1)
    time = data['Datetime']['S']
    
    message = "" \
        + "La température actuelle est" + temp + "Degré, humidité"+ humid + "Diplôme.\n" \
        + "Luminosité" + light + "En lux, la pression est"+ balo + "C'est hPa.\n" \
        + "(" + time + "Mesuré à:bar_chart:)"
        
    #S'il y a un argument ALL, s'il y a un argument, vider toutes les données
    if isAll:
        message = ""
        for d in data:
            s = str(d)
            v = data[s]
            if "N" in v:
                message += s + ":" + v["N"] + "\n"
            else:
                message += s + ":" + v["S"] + "\n"

    return message
    
#-------------------------------------------------------------------------------

# POST to Slack
def PostSlack(message):
    
    Dict_Payload = {
    "text": message,
    "username": 'NowBot',
    "icon_emoji": ":clock3:",
    "channel": '#room',
    }
    return requests.post(SLACL_POST_URL, data=json.dumps(Dict_Payload))

#-------------------------------------------------------------------------------
def lambda_handler(event, context):
    
    isAll = False
    try:
        tri1 = 'text='
        tri2 = 'trigger_word='
        body = event['body']
        tag = body[body.find(tri1)+len(tri1):body.find(tri2)-1]
        taglist = tag.split("+")
        for word in taglist:
            if "ALL" in word:
                isAll = True
    except:
        pass
    
    try:
        message = GetRoomEnv('Raspi_1', isAll)
    except Exception as e:
        import traceback
        message = traceback.format_exc()
        print(message)
        PostSlack('Meets Exception!\n' + message)
        raise e
        
    PostSlack(message)
    return 0

#===============================================================================

API Gateway

Slack Les WebHooks entrants et les WebHooks sortants ont été ajoutés aux intégrations personnalisées. Vous pouvez maintenant utiliser l'interface utilisateur. Super pratique.

Incoming WebHooks https://hooks.slack.com/services/XXXXXXX/YYYYYYY/ZZZZZZ Générez et définissez l'URL de publication sur Slack. Tout ce que vous avez à faire est de publier depuis Lambda. Vous pouvez dire s'il est activé ou désactivé. Ce qui suit est un exemple.

<img width = "556" alt = "Capture d'écran 2017-09-05 21.07.36.png " src = "https://qiita-image-store.s3.amazonaws.com/0/171122/6fd49e45-4271" -642d-f5a4-045833411c15.png ">

En regardant cela, nous pouvons voir ce qui suit.

Outgoing WebHooks Trigger Word Le mot déclencheur est «maintenant». Envoyez "maintenant" et NowBot vous indiquera l'état de la salle.

<img width = "389" alt = "Capture d'écran 2017-09-05 20.07.48.png " src = "https://qiita-image-store.s3.amazonaws.com/0/171122/94beb1e4-359a" -45f4-a6bb-01d02354fd63.png ">

L'envoi de "now ALL" videra toutes les données de la base de données. Pour le débogage.

<img width = "437" alt = "Capture d'écran 2017-09-05 20.11.08.png " src = "https://qiita-image-store.s3.amazonaws.com/0/171122/20954556-c401" -142f-e816-d6a2ade8f29b.png ">

J'ai pensé à traiter le libellé suivant "maintenant" en langage naturel (comme Amazon Polly) et à le renvoyer s'il y avait un paramètre lié (comme la température), mais ce sera la prochaine opportunité. .. ..

URL https://XXXXXX.execute-api.ap-northeast-1.amazonaws.com/prod/getroomenv Est réglé. Il a été créé avec AWS API Gateway, avec Lambda derrière. Le script getroomenv est en cours d'exécution.

Impressions

Il a fallu environ 3 jours pour se déplacer complètement Après cela, je me suis entretenu pendant environ deux jours et j'ai observé la situation. J'ai besoin d'un peu plus de Kouo pour DynamoDB et Lambda. C'était une bonne étude. Utiliser Slack pour l'interface utilisateur était une excellente réponse. Vraiment pratique.

Depuis que j'ai collecté des données de séries chronologiques, Utilisez ceci pour prendre la composante de fluctuation périodique Si vous pouvez informer Slack en cas de mouvement inhabituel, Je pense que c'est bon comme prochain développement. Indiquez s'il est normal que les gens viennent travailler le samedi. .. ..

Recommended Posts

Jouez pour informer Slack des données environnementales de SensorTag à l'aide d'AWS PaaS via Raspberry Pi3
Envoyer des données depuis Raspberry Pi à l'aide d'AWS IOT
Comment obtenir la température du thermo-hygromètre SwitchBot à l'aide de Raspberry Pi
J'ai envoyé les données de Raspberry Pi à GCP (gratuit)
Notifier LINE de la température corporelle du thermomètre BLE avec la tarte à la râpe # 1
Avertir LINE de la température corporelle du thermomètre BLE avec la tarte à la râpe n ° 2
Sortie du Raspberry Pi vers la ligne
Création d'un LINE BOT pour notifier d'autres concours AtCoder à l'aide d'AWS
J'ai essayé d'informer Slack de la mise à jour de Redmine
Lancement automatique des programmes Raspberry Pi à l'aide de Systemd
Sortie CSV des données d'impulsion avec Raspberry Pi (sortie CSV)
Connectez votre Raspberry Pi à votre smartphone en utilisant Blynk
[AWS] Migrer les données de DynamoDB vers Aurora MySQL
Mémo de la migration de la base de données de Django de SQLite3 vers MySQL sur Docker sur Raspberry Pi 4B
Essayez de diffuser des vidéos et des sites Web de Raspeye vers Chromecast ou Nest Hub à l'aide de CATT