[PYTHON] Réalisez une maison super IoT en acquérant des données de capteur dans la maison avec Raspberry Pi

introduction

La gestion des données par la méthode supercity est un sujet brûlant dans les rues, En tant que secte d'intérieur, surfons sur la vague du temps à la maison en face de la ville! J'ai pensé, ** Nous avons créé un mécanisme pour afficher une grande quantité de données de capteurs sur un tableau de bord en temps réel **.

** À partir de la conclusion, je pense que nous avons construit un système capable de visualiser les données des capteurs de manière stable **.

DashBoard.png

Je voudrais essayer de créer un article sans un saut d'explication afin que même les débutants puissent le comprendre facilement. ** Je vous serais reconnaissant si vous pouviez signaler des sauts ou des erreurs! ** **

Les choses nécessaires

** ・ RaspberryPi ** (Pi3 Model B est utilisé cette fois) ** ・ Environnement d'exécution Python ** (Python 3.7.3 prédéfini est utilisé cette fois)

Liste des capteurs utilisés

Type de sac de capteur d'environnement Omron (2JCIE-BL01)Type USB du capteur d'environnement Omron (2JCIE-BU01) ・ [Inkbird IBS-TH1 mini](https://www.amazon.co.jp/Inkbird-%E3%83%96%E3%83%AB%E3%83%BC%E3%83%88%E3% 82% A5% E3% 83% BC% E3% 82% B9-% E6% B8% A9% E6% B9% BF% E5% BA% A6% E3% 82% BB% E3% 83% B3% E3% 82 % B5% E3% 83% BC-Android-IBS-TH1 / dp / B07D74CQLJ) ・ [Inkbird IBS-TH1](https://www.amazon.co.jp/Inkbird-IBS-TH1-Bluetooth-%E5%A4%96%E9%83%A8%E3%83%97%E3%83 % AD% E3% 83% BC% E3% 83% 96% E4% BB% 98% E3% 81% 8D-% E3% 82% B9% E3% 83% 9E% E3% 83% 9B% E3% 81% A8% E9% 80% A3% E6% 8E% A5% E3% 81% A7% E3% 81% 8D / dp / B07D75H734 / ref = pd_aw_sbs_201_3 / 356-7389076-5979439? 4e16-8524-c984893af9bd & pd_rd_w = Ip0MD & pd_rd_wg = lcDuD & pf_rd_p = bff3a3a6-0f6e-4187-bd60-25e75d4c1c8f & pf_rd_r = 939W116VQ9B4AY ・ Thermo-hygromètre SwitchBotNature Remo

Configuration du système

Schéma de configuration global

La structure générale est celle illustrée dans la figure ci-dessous. Les flèches représentent le flux de données. センサ値取得システム.png

Fonctionnalité

Exécution périodique

Le fonctionnement à long terme étant difficile avec l'exécution manuelle de scripts, nous avons créé un mécanisme d'exécution périodique automatique. Le script sensor_to_spreadsheet.py qui contrôle l'ensemble est périodiquement exécuté par le cron de Raspberry Pi pour accumuler des données.

Communication sans fil entre le capteur et le Raspberry Pi

Fondamentalement, il communique avec Bluetooth Low Energy (BLE). BLE est une norme qui devient la norme de facto pour les appareils IoT à domicile, et les notes sont résumées dans cet article d'une manière facile à comprendre. [Détails ci-dessous](https://qiita.com/c60evaporator/items/283d0569eba58830f86e#%E5%90%84%E7%A8%AE%E3%82%A8%E3%83%A9%E3%83%BC % E5% AF% BE% E5% BF% 9C), mais la stabilisation de la BLE était le plus grand obstacle à ce développement. De plus, seul Nature Remo est connecté par WiFi au lieu de BLE.

Destination de stockage des données

La destination de stockage des données est divisée en deux systèmes: le stockage CSV local et Google Spreadsheets. CSV: utilisé ultérieurement pour l'analyse Feuilles de calcul Google: utilisées pour la visualisation sur le portail de données (je voulais visualiser sur le cloud) Je l'ai divisé en supposant qu'il sera utilisé, mais je pense qu'il est redondant, alors je pense qu'il est normal de l'unifier en un seul système. Lors de l'unification, je pense qu'il est idéal de pouvoir enregistrer dans une base de données accessible sur le cloud.

procédure

Le système ci-dessus a été construit selon la procédure suivante ** ① Réglage initial du capteur et contrôle de fonctionnement ** ** ② Créer une classe d'acquisition de données de capteur ** ** ③ Créer le script principal ** ** ④ Accédez à l'API GAS de Python pour écrire des données dans la feuille de calcul ** ** ⑤ Exécution périodique du script ** ** ⑥ Visualisation par Google Data Portal **

Les détails seront expliqués dans le chapitre suivant

Détails

① Réglage initial du capteur et contrôle de fonctionnement

Depuis que j'ai créé l'article individuellement, veuillez effectuer le réglage initial et le contrôle de fonctionnement pour chaque capteur.

Type de sac de capteur d'environnement Omron (2JCIE-BL01)Type USB du capteur d'environnement Omron (2JCIE-BU01)Inkbird IBS-TH1 miniInkbird IBS-TH1Thermo-hygromètre SwitchBotNature Remo

② Créer une classe d'acquisition de données de capteur

Créez une classe Python pour obtenir des données de capteur pour chaque type de capteur. (Inkbird IBS-TH1 et IBS-TH1 mini sont acquis dans la même classe. ")

De plus, les modules (fichiers .py) sont organisés par fabricant. Ci-dessous, nous expliquerons chaque module (par fabricant) dans des sections distinctes.

Capteur environnemental Omron

Créez une classe d'acquisition de données pour Omron 2JCIE-BL01 & 2JCIE-BU01. Le contenu est généralement [Type de sac](https://qiita.com/c60evaporator/items/ed2ffde4c87001111c12#%E3%82%BB%E3%83%B3%E3%82%B5%E6%B8%AC%E5%AE%9A% E5% 80% A4% E5% 8F% 96% E5% BE% 97% E7% 94% A8% E3% 82% AF% E3% 83% A9% E3% 82% B9% E3% 82% 92% E4% BD% 9C% E6% 88% 90) [Type USB](https://qiita.com/c60evaporator/items/ced01c241031c6c9c379#%E3%82%BB%E3%83%B3%E3%82%B5%E5%80%A4%E5%8F%96% E5% BE% 97% E3% 82% B9% E3% 82% AF% E3% 83% AA% E3% 83% 97% E3% 83% 88% E3% 81% AE% E4% BD% 9C% E6% 88% 90) Est une combinaison de

** ・ Méthode d'appel de l'extérieur Type de BAG: ** Créez une instance de la classe OmniBroadcastScanDelegate et

Nom de l'instance.scan(timeout)

Vous pouvez obtenir les données du capteur avec. Pour l'argument «timeout», spécifiez la valeur de timeout pour l'acquisition de données en secondes (environ 3 à 5 secondes semblent être bonnes).

** Type USB: ** Vous pouvez obtenir les données du capteur par la méthode suivante

GetOmronConnectModeData().get_env_usb_data(macaddr)

Pour l'argument "macaddr", spécifiez l'adresse Mac du capteur.

** - Script réel **

omron_env.py


# coding: UTF-8
from bluepy import btle
import struct

#Délégué d'acquisition de données de diffusion (type BAG)
class OmronBroadcastScanDelegate(btle.DefaultDelegate):
    #constructeur
    def __init__(self):
        btle.DefaultDelegate.__init__(self)
        #Variables pour conserver les données du capteur
        self.sensorValue = None

    #Gestionnaire de scan
    def handleDiscovery(self, dev, isNewDev, isNewData):  
        #Lorsqu'un nouvel appareil est trouvé
        if isNewDev or isNewData:  
            #Extraire les données publicitaires
            for (adtype, desc, value) in dev.getScanData():  
                #Effectuer la récupération des données lorsque le capteur d'environnement
                if desc == 'Manufacturer' and value[0:4] == 'd502':
                    #Retirez le type de capteur (EP ou IM)
                    sensorType = dev.scanData[dev.SHORT_LOCAL_NAME].decode(encoding='utf-8')
                    #Extraction des données du capteur pendant EP
                    if sensorType == 'EP':
                        self._decodeSensorData_EP(value)
                    #Extraction des données du capteur pendant la messagerie instantanée
                    if sensorType == 'IM':
                        self._decodeSensorData_IM(value)

    #Extraire les données du capteur et les convertir au format dict (en mode EP)
    def _decodeSensorData_EP(self, valueStr):
        #Données du capteur de la chaîne de caractères(6ème et suivants caractères)Sortir et convertir uniquement en binaire
        valueBinary = bytes.fromhex(valueStr[6:])
        #Convertir les données binaires du capteur en nombre entier Taple
        (temp, humid, light, uv, press, noise, discomf, wbgt, rfu, batt) = struct.unpack('<hhhhhhhhhB', valueBinary)
        #Stocker dans le type de dict après la conversion d'unité
        self.sensorValue = {
            'SensorType': 'Omron_BAG_EP',
            'Temperature': temp / 100,
            'Humidity': humid / 100,
            'Light': light,
            'UV': uv / 100,
            'Pressure': press / 10,
            'Noise': noise / 100,
            'Discomfort': discomf / 100,
            'WBGT': wbgt / 100,
            'BatteryVoltage': (batt + 100) / 100
        }
    
    #Extraire les données du capteur et les convertir au format dict (en mode IM)
    def _decodeSensorData_IM(self, valueStr):
        #Données du capteur de la chaîne de caractères(6ème et suivants caractères)Sortir et convertir uniquement en binaire
        valueBinary = bytes.fromhex(valueStr[6:])
        #Convertir les données binaires du capteur en nombre entier Taple
        (temp, humid, light, uv, press, noise, accelX, accelY, accelZ, batt) = struct.unpack('<hhhhhhhhhB', valueBinary)
        #Stocker dans le type de dict après la conversion d'unité
        self.sensorValue = {
            'SensorType': 'Omron_BAG_IM',
            'Temperature': temp / 100,
            'Humidity': humid / 100,
            'Light': light,
            'UV': uv / 100,
            'Pressure': press / 10,
            'Noise': noise / 100,
            'AccelerationX': accelX / 10,
            'AccelerationY': accelY / 10,
            'AccelerationZ': accelZ / 10,
            'BatteryVoltage': (batt + 100) / 100
        }

#Classe d'acquisition de données en mode connexion (type USB)
class GetOmronConnectModeData():
    def get_env_usb_data(self, macaddr):
        peripheral = btle.Peripheral(macaddr, addrType=btle.ADDR_TYPE_RANDOM)
        characteristic = peripheral.readCharacteristic(0x0059)
        return self._decodeSensorData_EP(characteristic)

    def _decodeSensorData_EP(self, valueBinary):
        (seq, temp, humid, light, press, noise, eTVOC, eCO2) = struct.unpack('<Bhhhlhhh', valueBinary)
        sensorValue = {
                'SensorType': 'Omron_USB_EP',
                'Temperature': temp / 100,
                'Humidity': humid / 100,
                'Light': light,
                'Pressure': press / 1000,
                'Noise': noise / 100,
                'eTVOC': eTVOC,
                'eCO2': eCO2
            }
        return sensorValue

Inkbird IBS-TH1 & IBS-TH1 mini Créez une classe d'acquisition de données pour Inkbird IBS-TH1 et IBS-TH1 mini. Le contenu est généralement IBS-TH1 IBS-TH1 mini Est une combinaison de

** ・ Méthode d'appel de l'extérieur ** Inkbird_IBSTH1 et Inkbird_IBSTH1mini, les deux peuvent acquérir des données de capteur par la méthode suivante

GetIBSTH1Data().get_ibsth1_data(macaddr, sensortype)

Pour l'argument "macaddr", spécifiez l'adresse Mac du capteur, et pour "sensortype", spécifiez'Inkbird_IBSTH1mini'ou'Inkbird_IBSTH1 'en fonction du type de capteur.

** - Script réel **

inkbird_ibsth1.py


from bluepy import btle
import struct

#Inkbird IBS-Classe d'acquisition de données TH1
class GetIBSTH1Data():
    def get_ibsth1_data(self, macaddr, sensortype):
        #Connectez-vous à l'appareil
        peripheral = btle.Peripheral(macaddr)
        #IBS-Lorsque TH1 mini
        if sensortype == 'Inkbird_IBSTH1mini':
            characteristic = peripheral.readCharacteristic(0x002d)
            return self._decodeSensorData_mini(characteristic)
        #IBS-Au TH1
        elif sensortype == 'Inkbird_IBSTH1':
            characteristic = peripheral.readCharacteristic(0x28)
            return self._decodeSensorData(characteristic)
        else:
            return None
    
    #IBS-TH1 mini
    def _decodeSensorData_mini(self, valueBinary):
        (temp, humid, unknown1, unknown2, unknown3) = struct.unpack('<hhBBB', valueBinary)
        sensorValue = {
                'SensorType': 'Inkbird_IBSTH1mini',
                'Temperature': temp / 100,
                'Humidity': humid / 100,
                'unknown1': unknown1,
                'unknown2': unknown2,
                'unknown3': unknown3,
            }
        return sensorValue

    #IBS-TH1
    def _decodeSensorData(self, valueBinary):
        (temp, humid, unknown1, unknown2, unknown3) = struct.unpack('<hhBBB', valueBinary)
        sensorValue = {
                'SensorType': 'Inkbird_IBSTH1',
                'Temperature': temp / 100,
                'Humidity': humid / 100,
                'unknown1': unknown1,
                'unknown2': unknown2,
                'unknown3': unknown3,
            }
        return sensorValue

Thermo-hygromètre SwitchBot

Créez une classe d'acquisition de données pour le thermo-hygromètre SwitchBot. Le contenu est [ici](https://qiita.com/c60evaporator/items/7c3156a6bbb7c6c59052#%E3%82%BB%E3%83%B3%E3%82%B5%E5%80%A4%E5%8F%96 % E5% BE% 97% E3% 82% B9% E3% 82% AF% E3% 83% AA% E3% 83% 97% E3% 83% 88% E3% 81% AE% E4% BD% 9C% E6 Identique à% 88% 90)

** ・ Méthode d'appel de l'extérieur ** Créer une instance de la classe SwitchbotScanDelegate

Nom de l'instance.scan(timeout)

Vous pouvez obtenir les données du capteur avec. Pour l'argument «timeout», spécifiez la valeur de timeout pour l'acquisition de données en secondes (environ 5 secondes semblent être bonnes).

** - Script réel **

switchbot.py


from bluepy import btle
import struct

#Délégué d'acquisition de données de diffusion
class SwitchbotScanDelegate(btle.DefaultDelegate):
    #constructeur
    def __init__(self, macaddr):
        btle.DefaultDelegate.__init__(self)
        #Variables pour conserver les données du capteur
        self.sensorValue = None
        self.macaddr = macaddr

    #Gestionnaire de scan
    def handleDiscovery(self, dev, isNewDev, isNewData):
        #Si vous trouvez un appareil avec l'adresse Mac cible
        if dev.addr == self.macaddr:
            #Extraire les données publicitaires
            for (adtype, desc, value) in dev.getScanData():  
                #Effectuer la récupération des données lorsque le capteur d'environnement
                if desc == '16b Service Data':
                    #Extraire les données du capteur
                    self._decodeSensorData(value)

    #Extraire les données du capteur et les convertir au format dict
    def _decodeSensorData(self, valueStr):
        #Données du capteur de la chaîne de caractères(4e et suivants caractères)Sortir et convertir uniquement en binaire
        valueBinary = bytes.fromhex(valueStr[4:])
        #Convertir les données binaires du capteur en valeur numérique
        batt = valueBinary[2] & 0b01111111
        isTemperatureAboveFreezing = valueBinary[4] & 0b10000000
        temp = ( valueBinary[3] & 0b00001111 ) / 10 + ( valueBinary[4] & 0b01111111 )
        if not isTemperatureAboveFreezing:
            temp = -temp
        humid = valueBinary[5] & 0b01111111
        #Stocker dans le type de dict
        self.sensorValue = {
            'SensorType': 'SwitchBot',
            'Temperature': temp,
            'Humidity': humid,
            'BatteryVoltage': batt
        }

Nature Remo En plus des données du capteur, les données du climatiseur sont également acquises via la télécommande.

** ・ Méthode d'appel de l'extérieur Acquisition des données du capteur: ** Vous pouvez obtenir les données du capteur par la méthode suivante

get_sensor_data(Token, API_URL)

L'argument «Token» est le jeton d'accès Remo (référence), et «API_URL» est «https://api.nature.global/» (corrigé). Valeur)

** Acquisition des données du climatiseur **: Vous pouvez obtenir les données du capteur par la méthode suivante

get_aircon_data(Token, API_URL)

Les arguments sont les mêmes que lors de l'acquisition des données du capteur

** - Script réel **

remo.py


import json
import requests
import glob
import pandas as pd

#Classe d'acquisition de données 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_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._decodeAirconData(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

    #Extraire les données du capteur et les convertir au format dict
    def _decodeSensorData(self, rjson):
        for device in rjson:
            #Sélectionnez Remo (pour éviter de sélectionner Remo E par erreur)
            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

    #Retirez les données du climatiseur et convertissez-les au format dict
    def _decodeAirconData(self, rjson):
        for appliance in rjson:
            if appliance['type'] == 'AC':
                Value = {
                    'TempSetting': appliance['settings']['temp'],
                    'Mode': appliance['settings']['mode'],
                    'AirVolume': appliance['settings']['vol'],
                    'AirDirection': appliance['settings']['dir'],
                    'Power': appliance['settings']['button']
                }
                break
        return Value

③ Créer un script principal

Structure générale

Appelez la classe (2) et créez un script principal pour acquérir les données du capteur. La structure est divisée en 5 types de méthodes et de structures suivants

** 1) Méthode pour exécuter la classe ② ** getdata_omron_bag (device): obtenir la valeur du capteur du type de BAG du capteur d'environnement Omron getdata_omron_usb (périphérique): Capteur d'environnement Omron Acquisition de valeur de capteur de type USB getdata_ibsth1 (périphérique): obtenir la valeur du capteur de Inkbird IBS-TH1 et IBS-TH1 mini getdata_switchbot_thermo (appareil): obtenir la valeur du capteur du thermo-hygromètre SwitchBot getdata_remo (appareil, csvpath): obtenir la valeur du capteur Nature Remo et les données du climatiseur La signification de l'argument est la suivante périphérique: spécifiez la ligne lue depuis DeviceList.csv. csvpath: chemin enregistré CSV (le capteur humain de Remo doit être comparé aux données antérieures enregistrées CSV)

** 2) Méthode de sortie CSV ** output_csv (data, csvpath): génère les données de valeur mesurée pour chaque capteur au format CSV. La signification de l'argument est la suivante data: Données acquises du capteur par la méthode de 1 csvpath: chemin de sauvegarde CSV

** 3) Méthode de sortie SpreadSheet ** output_spreadsheet (all_values_dict): affiche les données de mesure de tous les capteurs dans la feuille de calcul. La signification de l'argument est la suivante all_values_dict: Données de tous les capteurs au format dict (en supposant la structure ci-dessous)

all_values_dict = {
Nom du capteur 1:{
Nom de colonne 1:Valeur 1,
Nom de colonne 2:Valeur 2,
          :
    },
Nom du capteur 2:{
Nom de colonne 1:Valeur 1,
Nom de colonne 2:Valeur 2,
          :
    },
    :
}

** 4) Méthode de redémarrage de l'adaptateur Bluetooth ** restart_hci0 (devicename): C'est la méthode de contre-mesure "Bug 2" décrite en bas. La signification de l'argument est la suivante devicename: nom du périphérique (utilisé pour la sortie du journal)

** 5) Partie de traitement principale if name == After'main ': Exécute une série de processus comme indiqué dans le flux ci-dessous. maincode.png

fichier de configuration

À mesure que le nombre de capteurs augmentait et que je sentais qu'il y avait une limite à la gestion avec le code, j'ai créé les deux types de fichiers de paramètres suivants. ** ・ DeviceList.csv: Décrivez les informations nécessaires pour chaque capteur **

DeviceList.csv


DeviceName,SensorType,MacAddress,Timeout,Retry,Offset_Temp,Offset_Humid,API_URL,Token
SwitchBot_Thermo1,SwitchBot_Thermo,[Adresse Mac SwitchBot],4,3,0,0,,
Inkbird_IBSTH1_Mini1,Inkbird_IBSTH1mini,[IBS-Adresse mini Mac TH1],0,2,0,0,,
Inkbird_IBSTH1_1,Inkbird_IBSTH1,[IBS-Adresse Mac TH1],0,2,0,0,,
Remo1,Nature_Remo,,0,2,0,0,https://api.nature.global/,[Jeton d'accès Nature Remo]
Omron_USB1,Omron_USB_EP,[Adresse Mac de type USB Omron],0,2,0,0,,
Omron_BAG1,Omron_BAG_EP,[Adresse Mac de type Omron BAG],3,2,0,0,,

La signification des colonnes est la suivante DeviceName: gère les noms de périphériques et identifie lorsqu'il y a plusieurs capteurs du même type SensorType: Le type de capteur. Séparez les classes d'acquisition de données de capteur à exécuter en fonction de cette valeur MacAddress: adresse MAC du capteur Timeout: valeur du délai au moment de la recherche (uniquement pour les capteurs en mode diffusion) Réessayer: nombre maximal de tentatives [Détails](https://qiita.com/c60evaporator/items/283d0569eba58830f86e#%E4%B8%8D%E5%85%B7%E5%90%881peripheral%E3%81%AE%E5 % 88% 9D% E6% 9C% 9F% E5% 8C% 96% E6% 99% 82% E3% 81% AB% E3% 82% A8% E3% 83% A9% E3% 83% BC% E3% 81 % 8C% E5% 87% BA% E3% 82% 8B) Offset_Temp: valeur de décalage de température (actuellement inutilisée) Offset_Humid: valeur de décalage d'humidité (actuellement inutilisée) API_URL: URL de l'API (utilisée uniquement pour Nature Remo) Jeton: jeton d'accès (utilisé uniquement pour Nature Remo)

** ・ config.ini: Spécifiez le répertoire de sortie CSV et journal **

config.ini


[Path]
CSVOutput = /share/Data/Sensor
LogOutput = /share/Log/Sensor

Si les deux sont affichés dans le dossier partagé créé par samba, il est pratique d'accéder depuis l'extérieur du Raspberry Pi.

Script réel

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 datetime import datetime, timedelta
import os
import csv
import configparser
import pandas as pd
import requests
import logging
import subprocess

#Variables globales
global masterdate

######Acquisition de valeurs pour capteur d'environnement OMRON (type BAG)######
def getdata_omron_bag(device):
    #Appareil maximum lorsqu'aucune valeur n'est disponible.Réessayer de répéter l'analyse
    for i in range(device.Retry):
        #omron_Définit le délégué d'acquisition de la valeur du capteur d'env pour qu'il s'exécute au moment du scan
        scanner = btle.Scanner().withDelegate(OmronBroadcastScanDelegate())
        #Scannez pour obtenir la valeur du capteur
        try:
            scanner.scan(device.Timeout)
        #Si vous obtenez une erreur lors de l'analyse, redémarrez l'adaptateur Bluetooth
        except:
            restart_hci0(device.DeviceName)
        #Terminer la boucle lorsque la valeur peut être obtenue
        if scanner.delegate.sensorValue is not None:
            break
        #Si la valeur ne peut pas être obtenue, écrivez-la dans le journal
        else:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}, timeout{device.Timeout}]')
    
    #Si la valeur peut être obtenue, stockez les données POST dans dict
    if scanner.delegate.sensorValue is not None:
        #Données à POST
        data = {        
            'DeviceName': device.DeviceName,        
            'Date_Master': str(masterdate),
            'Date': str(datetime.today()),
            'Temperature': str(scanner.delegate.sensorValue['Temperature']),
            'Humidity': str(scanner.delegate.sensorValue['Humidity']),
            'Light': str(scanner.delegate.sensorValue['Light']),
            'UV': str(scanner.delegate.sensorValue['UV']),
            'Pressure': str(scanner.delegate.sensorValue['Pressure']),
            'Noise': str(scanner.delegate.sensorValue['Noise']),
            'BatteryVoltage': str(scanner.delegate.sensorValue['BatteryVoltage'])
        }
        return data
    #Si la valeur n'a pas pu être obtenue, enregistrez la sortie et redémarrez l'adaptateur Bluetooth
    else:
        logging.error(f'cannot get data [date{str(masterdate)}, device{device.DeviceName}, timeout{device.Timeout}]')
        restart_hci0(device.DeviceName)
        return None

######Acquisition de données du capteur d'environnement OMRON (type USB)######
def getdata_omron_usb(device):
    #Appareil maximum lorsqu'aucune valeur n'est disponible.Réessayer de répéter l'analyse
    for i in range(device.Retry):
        try:
            sensorValue = GetOmronConnectModeData().get_env_usb_data(device.MacAddress)
        #Sortie de journal si une erreur se produit
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}]')
            sensorValue = None
            continue
        else:
            break
    
    #Si la valeur peut être obtenue, stockez les données POST dans dict
    if sensorValue is not None:
        #Données à POST
        data = {        
            'DeviceName': device.DeviceName,        
            'Date_Master': str(masterdate),
            'Date': str(datetime.today()),
            'Temperature': str(sensorValue['Temperature']),
            'Humidity': str(sensorValue['Humidity']),
            'Light': str(sensorValue['Light']),
            'Pressure': str(sensorValue['Pressure']),
            'Noise': str(sensorValue['Noise']),
            'eTVOC': str(sensorValue['eTVOC']),
            'eCO2': str(sensorValue['eCO2'])
        }
        return data
    #Si la valeur n'a pas pu être obtenue, enregistrez la sortie et redémarrez l'adaptateur 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-Acquisition de données TH1######
def getdata_ibsth1(device):
    #Appareil maximum lorsqu'aucune valeur n'est disponible.Réessayer de répéter l'analyse
    for i in range(device.Retry):
        try:
            sensorValue = GetIBSTH1Data().get_ibsth1_data(device.MacAddress, device.SensorType)
        #Sortie de journal si une erreur se produit
        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:
        #Données à POST
        data = {        
            'DeviceName': device.DeviceName,        
            'Date_Master': str(masterdate),
            'Date': str(datetime.today()),
            'Temperature': str(sensorValue['Temperature']),
            'Humidity': str(sensorValue['Humidity']),
        }
        return data
    #Si la valeur n'a pas pu être obtenue, enregistrez la sortie et redémarrez l'adaptateur Bluetooth
    else:
        logging.error(f'cannot get data [loop{str(device.Retry)}, date{str(masterdate)}, device{device.DeviceName}]')
        restart_hci0(device.DeviceName)
        return None

######Acquisition des données du thermo-hygromètre SwitchBot######
def getdata_switchbot_thermo(device):
    #Appareil maximum lorsqu'aucune valeur n'est disponible.Réessayer de répéter l'analyse
    for i in range(device.Retry):
        #Définir le délégué d'acquisition de la valeur du capteur Switchbot
        scanner = btle.Scanner().withDelegate(SwitchbotScanDelegate(str.lower(device.MacAddress)))
        #Scannez pour obtenir la valeur du capteur
        try:
            scanner.scan(device.Timeout)
        #Si vous obtenez une erreur lors de l'analyse, redémarrez l'adaptateur Bluetooth
        except:
            restart_hci0(device.DeviceName)
        #Terminer la boucle lorsque la valeur peut être obtenue
        if scanner.delegate.sensorValue is not None:
            break
        #Si la valeur ne peut pas être obtenue, écrivez-la dans le journal
        else:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}, timeout{device.Timeout}]')
    
    #Si la valeur peut être obtenue, stockez les données POST dans dict
    if scanner.delegate.sensorValue is not None:
        #Données à POST
        data = {        
            'DeviceName': device.DeviceName,
            'Date_Master': str(masterdate),
            'Date': str(datetime.today()),
            'Temperature': str(scanner.delegate.sensorValue['Temperature']),
            'Humidity': str(scanner.delegate.sensorValue['Humidity']),
            'BatteryVoltage': str(scanner.delegate.sensorValue['BatteryVoltage'])
        }
        return data
    #Sinon, sortez le journal et redémarrez l'adaptateur 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

######Acquisition de données Nature Remo######
def getdata_remo(device, csvpath):
    #Appareil maximum lorsque la valeur des données du capteur n'est pas disponible.Réessayer de répéter l'analyse
    for i in range(device.Retry):
        try:
            sensorValue = GetRemoData().get_sensor_data(device.Token, device.API_URL)
        #Sortie de journal si une erreur se produit
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}, sensor]')
            sensorValue = None
            continue
        else:
            break
    #Appareil maximum lorsque la valeur des données du climatiseur n'est pas disponible.Réessayer de répéter l'analyse
    for i in range(device.Retry):
        try:
            airconValue = GetRemoData().get_aircon_data(device.Token, device.API_URL)
        #Sortie de journal si une erreur se produit
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}, aircon]')
            sensorValue = None
            continue
        else:
            break
        
    #Si la valeur peut être obtenue, stockez les données POST dans dict
    if sensorValue is not None:
        #Données du capteur
        data = {        
            'DeviceName': device.DeviceName,        
            'Date_Master': str(masterdate),
            'Date': str(datetime.today()),
            'Temperature': str(sensorValue['Temperature']),
            'Humidity': str(sensorValue['Humidity']),
            'Light': str(sensorValue['Light']),
            'Human_last': str(sensorValue['Human_last']),
            'HumanMotion': GetRemoData().calc_human_motion(sensorValue['Human_last'], f'{csvpath}/{device.DeviceName}')
        }
        #Données du climatiseur
        if airconValue is not None:
            data['TempSetting'] = airconValue['TempSetting']
            data['AirconMode'] = airconValue['Mode']
            data['AirVolume'] = airconValue['AirVolume']
            data['AirDirection'] = airconValue['AirDirection']
            data['AirconPower'] = airconValue['Power']
        return data
    #S'il ne peut pas être obtenu, enregistrez la sortie (comme c'est via WiFi, l'adaptateur Bluetooth ne sera pas redémarré)
    else:
        logging.error(f'cannot get data [loop{str(device.Retry)}, date{str(masterdate)}, device{device.DeviceName}]')
        return None

######Sortie CSV des données######
def output_csv(data, csvpath):
    dvname = data['DeviceName']
    monthstr = masterdate.strftime('%Y%m')
    #Nom du dossier de destination de sortie
    outdir = f'{csvpath}/{dvname}/{masterdate.year}'
    #Lorsque le dossier de destination de sortie n'existe pas, créez-en un nouveau
    os.makedirs(outdir, exist_ok=True)
    #Chemin du fichier de sortie
    outpath = f'{outdir}/{dvname}_{monthstr}.csv'
    
    #Créer un nouveau fichier de sortie lorsqu'il n'existe pas
    if not os.path.exists(outpath):        
        with open(outpath, 'w') as f:
            writer = csv.DictWriter(f, data.keys())
            writer.writeheader()
            writer.writerow(data)
    #Ajouter une ligne lorsque le fichier de sortie existe
    else:
        with open(outpath, 'a') as f:
            writer = csv.DictWriter(f, data.keys())
            writer.writerow(data)

######Processus de téléchargement sur la feuille de calcul Google######
def output_spreadsheet(all_values_dict):
    #URL de l'API
    url = 'URL de l'API GAS répertoriée ici'
    #POST des données vers l'API
    response = requests.post(url, json=all_values_dict)
    print(response.text)

######Redémarrage de l'adaptateur Bluetooth######
def restart_hci0(devicename):
    passwd = 'Entrez le mot de passe 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}]')


######Principale######
if __name__ == '__main__':    
    #Obtenir l'heure de début
    startdate = datetime.today()
    #Arrondissez l'heure de début en minutes
    masterdate = startdate.replace(second=0, microsecond=0)   
    if startdate.second >= 30:
        masterdate += timedelta(minutes=1)

    #Lire le fichier de configuration et la liste des appareils
    cfg = configparser.ConfigParser()
    cfg.read('./config.ini', encoding='utf-8')
    df_devicelist = pd.read_csv('./DeviceList.csv')
    #Nombre total de capteurs et acquisition de données réussie
    sensor_num = len(df_devicelist)
    success_num = 0

    #Initialisation du journal
    logname = f"/sensorlog_{str(masterdate.strftime('%y%m%d'))}.log"
    logging.basicConfig(filename=cfg['Path']['LogOutput'] + logname, level=logging.INFO)

    #Dict pour conserver toutes les données acquises
    all_values_dict = None

    ######Acquisition de données pour chaque appareil######
    for device in df_devicelist.itertuples():
        #Type de sac du capteur d'environnement Omron (connexion BroadCast)
        if device.SensorType in ['Omron_BAG_EP','Omron_BAG_IM']:
            data = getdata_omron_bag(device)
        #Type USB du capteur d'environnement Omron (connexion en mode 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)
        #Thermo-hygromètre SwitchBot
        elif device.SensorType == 'SwitchBot_Thermo':
            data = getdata_switchbot_thermo(device)
        #remo
        elif device.SensorType == 'Nature_Remo':
            data = getdata_remo(device, cfg['Path']['CSVOutput'])
        #Autre que ceux ci-dessus
        else:
            data = None        

        #Lorsque des données existent, ajoutez-les à Dict pour contenir toutes les données et générer le CSV
        if data is not None:
            #all_values_Créer un nouveau dictionnaire lorsque dict est None
            if all_values_dict is None:
                all_values_dict = {data['DeviceName']: data}
            #all_values_Ajouter au dictionnaire existant lorsque dict n'est pas None
            else:
                all_values_dict[data['DeviceName']] = data

            #Sortie CSV
            output_csv(data, cfg['Path']['CSVOutput'])
            #Numéro de réussite plus
            success_num+=1

    ######Processus de téléchargement sur la feuille de calcul Google######
    output_spreadsheet(all_values_dict)

    #Sortie du journal de la fin du traitement
    logging.info(f'[masterdate{str(masterdate)} startdate{str(startdate)} enddate{str(datetime.today())} success{str(success_num)}/{str(sensor_num)}]')

④ Accédez à l'API GAS de Python pour écrire des données dans la feuille de calcul

Écrivez les données de Raspberry Pi dans une feuille de calcul Google.

Du côté RaspberryPi, utilisez la classe d'exécution d'API de Python pour publier des données et Du côté de la feuille de calcul, les données ci-dessus sont reçues à l'aide d'un script Google Apps Script.

Traitement des données POST du côté Python (côté RaspberryPi)

Il semble que seule la fonction nommée doPost puisse être utilisée pour publier l'API, vous devez donc publier toutes les données de Python vers GAS en même temps. Pour cela, j'ai fait un script à publier au format JSON.

sensors_to_spreadsheet.Partie de py


######Processus de téléchargement sur la feuille de calcul Google######
def output_spreadsheet(all_values_dict):
    #URL de l'API
    url = 'URL de l'API GAS répertoriée ici'
    #POST des données vers l'API
    response = requests.post(url, json=all_values_dict)
    print(response.text)

Le fait est que la valeur passée après "requests.post (url, json =" est dict. (Ne convertissez pas la chaîne de caractères avec json.dumps (). C'est un point addictif de la passer sans la convertir en json même si le nom est json = ...)

Créer une feuille de calcul de destination

Accédez à Google Spreadsheets et accédez à Créez une feuille de calcul comme indiqué ci-dessous. sensor_spreadsheet.png

Le nom de la feuille est "SensorData". Dans la section suivante, nous allons créer un script qui envoie des données à tous les capteurs à la fois sur cette feuille.

Création de scripts GAS (réception des données et processus d'écriture de tableur)

Sélectionnez "Outils" -> "Éditeur de script" sur la feuille de calcul Créez un script GAS comme celui ci-dessous

postSensorData.gs


var spreadsheetId = '******'//← Entrez l'ID de la feuille de calcul

//Une méthode qui reçoit les données publiées et les écrit dans une feuille de calcul
function doPost(e){
  //Obtenir des informations sur la feuille de calcul (instance de feuille, liste de noms d'en-tête, nombre de lignes)
  var sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName('SensorData');
  var headerList = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
  var lastRow = sheet.getLastRow();

  //Convertir les données reçues en JSON
  var json = JSON.parse(e.postData.getDataAsString());

  //Tenez le nombre d'appareils
  var devCnt = 0;
  for(var device in json) {
    //Ajouter 1 appareil
    devCnt++;
    //Scanner toutes les mesures du capteur
    for(var colName in json[device]){
      //Le nom de la colonne est"DeviceName"Ignorez la valeur lorsque"device"(Acquis comme)
      if(colName == "DeviceName"){
      //Le nom de la colonne est"Date_Master"Quand, ignorez la valeur sauf pour le premier appareil (évitez la description en double)
      }else if(colName == "Date_Master" && devCnt >= 2){
      //Autre que ce qui précède, écrivez des données dans la feuille de calcul
      }else{        
        headerList = addData(sheet, lastRow, headerList, device, colName, json[device][colName]);
      }
    }
  }
  //Renvoie le succès à l'exécuteur d'API
  return ContentService.createTextOutput(JSON.stringify({content:"post ok", endtime:new Date()})).setMimeType(ContentService.MimeType.JSON);
}

//Écrire des données dans une feuille de calcul
function addData(sheet, lastRow, headerList, deviceName, colName, cell){
  //Le nom de la colonne est Date_Lorsque Master, écrivez des données et écrivez des données divisées uniquement en partie de date
  if (colName == 'Date_Master'){
    sheet.getRange(lastRow + 1, 1).setValue(cell);
    sheet.getRange(lastRow + 1, 2).setValue(cell.substring(0, 10));
  //Le nom de la colonne est Date_Quand autre que Master
  }else{
    //Nom de la colonne (combine le nom de l'appareil et le nom de la colonne d'origine)
    var newColName = deviceName + "_" + colName;
    //Analyser les noms d'en-tête existants
    for (i=0; i<headerList.length; i++) {
      //Écrire les données du capteur dans cette colonne s'il existe un nom d'en-tête existant égal au nom de la colonne
      if(newColName == headerList[i]){
        sheet.getRange(lastRow + 1, i + 1).setValue(cell);
        break;
      }
      //Si le nom de la colonne n'est pas inclus dans le nom de l'en-tête existant, ajoutez un nouvel en-tête et écrivez les données du capteur
      if(i == headerList.length - 1){
        sheet.getRange(1, i + 2).setValue(newColName);
        sheet.getRange(lastRow + 1, i + 2).setValue(cell);
        headerList.push(newColName);
      }
    }
  }
  return headerList
}

** * ID de la feuille de calcul ** L'URL de la feuille de calcul est "https://docs.google.com/spreadsheets/d/AAAAAA/edit#gid=0" Si c'est le cas, 「AAAAAA」 La partie de est l'ID de la feuille de calcul.

Sortie de l'API de script GAS

[Ici](https://qiita.com/c60evaporator/items/ed2ffde4c87001111c12#python%E3%81%8B%E3%82%89gas%E3%81%AEapi%E3%82%92%E5%8F%A9% E3% 81% 84% E3% 81% A6% E3% 82% B9% E3% 83% 97% E3% 83% AC% E3% 83% 83% E3% 83% 89% E3% 82% B7% E3% 83% BC% E3% 83% 88% E3% 81% AB% E3% 83% 87% E3% 83% BC% E3% 82% BF% E6% 9B% B8% E3% 81% 8D% E8% BE% Veuillez publier le script GAS en tant qu'API en vous référant à BC% E3% 81% BF). Si vous collez l'URL qui apparaît à la fin dans la partie suivante du script principal sensor_to_spreadsheet.py,

sensors_to_spreadsheet.Partie de py


def output_spreadsheet(all_values_dict):
    #URL de l'API
    url = 'URL de l'API GAS répertoriée ici'

Le script GAS est appelé lors de l'exécution de Sensor_to_spreadsheet.py La valeur est automatiquement écrite dans la feuille de calcul (l'en-tête est également ajouté automatiquement comme indiqué dans la section suivante).

Description de la feuille de calcul de sortie

Comme le montre la figure ci-dessous Nom de l'en-tête: [Nom de l'appareil_ signification de la valeur] → Ajouté automatiquement s'il n'existe pas Données: ajoutez une ligne à la fin de la valeur mesurée correspondant à l'en-tête ci-dessus. Nous avons créé un script GAS afin que les en-têtes puissent être générés et que les données puissent être écrites automatiquement. spreadsheet_sensor.png

⑤ Exécution périodique du script

C'est un problème d'exécuter le script à chaque fois pour obtenir la valeur du capteur, donc Automatisez en utilisant le package périodique "cron". Cela ressemble à un planificateur de tâches Windows.

Activer cron

Il peut être désactivé par défaut, alors activez-le en vous référant à here.

** ・ Point de contrôle 1: Vérifiez rsyslog.conf ** Cela ne fonctionnera pas si "cron" dans /etc/rsyslog.conf est commenté. Dans mon cas, il a été commenté comme ci-dessous, donc

###############
#### RULES ####
###############

#
# First some standard log files.  Log by facility.
#
auth,authpriv.*                 /var/log/auth.log
*.*;auth,authpriv.none          -/var/log/syslog
#cron.*                         /var/log/cron.log
daemon.*                        -/var/log/daemon.log

Après avoir décommenté comme ci-dessous,

cron.*                         /var/log/cron.log

Redémarrez rsyslog avec la commande suivante.

sudo /etc/init.d/rsyslog restart

** ・ Point de contrôle 2: Modifier le niveau de journal ** Dans / etc / default / cron, spécifiez les éléments à décrire dans le journal lorsque cron est exécuté. Par défaut, il semble que le journal ne soit pas sorti comme indiqué ci-dessous, donc

# For quick reference, the currently available log levels are:
#   0   no logging (errors are logged regardless)
#   1   log start of jobs
#   2   log end of jobs
#   4   log jobs with exit status != 0
#   8   log the process identifier of child process (in all logs)
#
#EXTRA_OPTS=""

Supprimer les commentaires de EXTRA_OPTS comme indiqué ci-dessous

EXTRA_OPTS='-L 15'

ça ira. Cela signifie que toutes les sorties sont 1 + 2 + 4 + 8 = 15.

Redémarrez cron avec la commande suivante.

sudo /etc/init.d/cron restart

Il réussit si cron.log est généré dans / var / log. (Veuillez cocher ici si vous voulez voir l'historique d'exécution de cron)

Planifier l'exécution avec cron

Enregistrer l'exécution périodique avec cron

** - Modifier crontab ** Ouvrez crontab avec la commande suivante

crontab -e

Lorsqu'on vous demande quel éditeur ouvrir, sélectionnez celui que vous aimez (nano recommandé pour les débutants)

# Edit this file to introduce tasks to be run by cron.
# 
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
: Diverses choses continuent

Il y a divers commentaires écrits comme ci-dessus, À la toute fin, décrivez le timing et la commande que vous souhaitez exécuter. Le format de synchronisation est [ici](http://tech.junkpot.net/archives/721/crontab-%E3%81%A7%E7%B4%B0%E3%81%8B%E3%81%84%E3 % 82% B9% E3% 82% B1% E3% 82% B8% E3% 83% A5% E3% 83% BC% E3% 83% AB% E3% 81% AE% E8% A8% AD% E5% AE Veuillez vous référer à% 9A.html)

Cette fois, puisqu'elle est exécutée toutes les 5 minutes, le contenu suivant est décrit.

*/5 * * * * [Chemin complet de Python] [sensors_to_spreadSheet.chemin complet de py] >/dev/null 2>&1

** ・ Démarrer cron ** Démarrez cron avec la commande suivante

sudo /etc/init.d/cron start

Attendez un peu ci-dessus et si la feuille de calcul est mise à jour toutes les 5 minutes, vous avez terminé!

⑥ Visualisation par Google Data Portal

Google Data Portal est un tableau de bord qui peut être modifié et affiché sur le cloud. Dans cette section, nous expliquerons comment créer la partie graphique de transition de température du tableau de bord au début.

Créer un nouveau rapport

1_dataportal.png ** Spécifiez une feuille de calcul Google comme destination de la connexion de données ** 2_connect_spreadsheet.png ** Si vous êtes invité à approuver, appuyez sur le bouton d'approbation ** 3_allow_spreadsheet.png ** Il vous sera demandé la feuille de référence, alors spécifiez la feuille de calcul créée en ④ ** 4_add_spreadsheet.png ** Appuyez sur "Ajouter au rapport" ** 5_confirm_spreadsheet.png ** Renommer le rapport ** 6_rename.png

Créer un graphique de moyenne quotidienne

Créez un graphique pour voir les changements à long terme

** Cliquez sur Ressources → Gérer les sources de données ajoutées ** 7.png ** Cliquez sur "Modifier" pour la source de données cible ** 8.png ** Changez la date et l'heure dans un format reconnaissable ** (Le portail de données a un format strict de reconnaissance de la date et de l'heure) Si vous souhaitez obtenir les statistiques correctes telles que la moyenne: AAAAMMJJ Si vous souhaitez afficher la valeur mesurée pour chaque ligne: AAAAMMJJhhmm 9.png ** Modification du graphique de série chronologique ** 10.png ** Spécifiez la dimension (axe horizontal = Date_Master_Day) et l'indice (axe vertical = température des différents capteurs) ** 11.png ** Changer les statistiques de température du total à la moyenne ** 12.png ** Modifier pour masquer les valeurs manquantes ** 13.png

Créer un graphique pour chaque mesure (toutes les 5 minutes)

Créez un graphique pour voir les changements à court terme

** Ajouter un graphique de série chronologique ** 15.png ** Spécifiez la dimension (axe horizontal = Date_Master_Day) et l'indice (axe vertical = température des différents capteurs) ** 16.png ** Changer les statistiques de température du total à la moyenne ** 17_.png ** Modifier les valeurs manquantes pour terminer linéairement ** 18.png Ceci complète le graphique de transition de température!

Pour compléter le tableau de bord

À ce stade, seule la partie du tableau de bord d'ouverture entourée par le cadre rouge ci-dessous est terminée. DashBoard_part.png

En plus de ce qui précède, il est nécessaire de faire un «graphique de transition d'humidité» et un «graphique circulaire» pour compléter. Je vais l'omettre car ce sera long si je vous explique tout.

Le graphique de transition d'humidité ne modifie que "l'indice" du graphique de transition de température de la température à l'humidité, donc je pense qu'il peut être créé sans colmatage particulier.

Comment créer un graphe circulaire est [ici](https://qiita.com/t-chi/items/01b9a9b98fbccef880c3#%E6%B9%BF%E5%BA%A6%E3%81%AE%E5%86% 86% E3% 82% B0% E3% 83% A9% E3% 83% 95) se résume de manière simple à comprendre.

Je suis désolé d'avoir fait un vol à la cuisine pendant 3 minutes en disant "Je ne sauterai pas!" Au début. Si vous faites de votre mieux pour compléter le graphique et organiser la mise en page, vous pouvez créer le tableau de bord suivant. DashBoard.png

** C'est tout! !! ** ** Il y a peut-être des gens qui veulent se précipiter dans "Où est le supermarché!" Sans aucune pièce étrange. Personnellement, j'ai été impressionné par la combinaison de gadgets à bas prix et de services gratuits.

Après la cuisson, l'humidité monte et la température dessine une belle courbe ascendante avec les saisons, et il y a de nouvelles découvertes, donc je pense que la finition ne se lassera pas de regarder!

Correspondance avec diverses erreurs

Comme pour tout système, le fonctionnement continu révèle une grande variété de problèmes qui n'ont pas été remarqués en un seul coup. Dans ce cas, j'ai été particulièrement agacé par ** l'instabilité de la réception Bluetooth **.

Après avoir traité chaque problème individuellement, il se reflète dans divers scripts. Avant les contre-mesures, d'innombrables arrêts et valeurs manquantes se sont produits, mais avec les contre-mesures suivantes, bien que les valeurs manquantes apparaissent rarement, il a été amélioré à ** un niveau où un fonctionnement continu peut être effectué sans presque aucune maintenance **!

Bug 1: une erreur se produit lors de l'initialisation du périphérique

Phénomène qui se produit

inkbird_ibsth1.Partie de py


    def get_ibsth1_data(self, macaddr, sensortype):
        #Connectez-vous à l'appareil
        peripheral = btle.Peripheral(macaddr)
        #IBS-Lorsque TH1 mini
        if sensortype == 'Inkbird_IBSTH1mini':
            characteristic = peripheral.readCharacteristic(0x002d)

au dessus de peripheral = btle.Peripheral(macaddr) Au fait

BTLEException: failed to connect to peripheral addr type: public

Une erreur apparaît ou n'apparaît pas (il est gênant qu'il n'y ait pas de reproductibilité)

Faire face

Si vous réexécutez, cela fonctionne souvent comme si de rien n'était, donc L'appelant prend l'exception avec try except et la réexécute jusqu'à un certain nombre de fois.

sensors_to_spreadsheet.py getdata_Fait partie de la méthode ibsth1


def getdata_ibsth1(device):
    #Appareil maximum lorsqu'aucune valeur n'est disponible.Réessayer de répéter l'analyse
    for i in range(device.Retry):
        try:
            sensorValue = GetIBSTH1Data().get_ibsth1_data(device.MacAddress, device.SensorType)
        #Sortie de journal si une erreur se produit
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}]')
            sensorValue = None
            continue
        else:
            break

Le nombre de réexécutions est spécifié pour chaque périphérique dans la colonne "Réessayer" du fichier de paramètres "DeviceList.csv".

Bug 2: Erreur lors de la numérisation en mode diffusion

Phénomène qui se produit

getdata_switchbot_Une partie de la méthode thermique


        #Scannez pour obtenir la valeur du capteur
        try:
            scanner.scan(device.Timeout)
        #Si vous obtenez une erreur lors de l'analyse, redémarrez l'adaptateur Bluetooth
        except:
            restart_hci0(masterdate, device.DeviceName)

Si vous répétez le scanner.scan () ci-dessus, il sera exécuté environ 20 fois.

bluepy.btle.BTLEManagementError: Failed to execute management command 'scan' (code: 11, error: Rejected)

Une erreur se produit et quel que soit le nombre de réexécutions, vous ne pourrez pas acquérir de données en raison d'une erreur. Parfois cela arrive 10 fois, parfois cela arrive 34 fois, et il n'y a pas de reproductibilité du nombre de fois, ce qui est très gênant. (La cause est inconnue, mais il semble qu'un comportement de type section soit accumulé)

Faire face

Reportez-vous à here et utilisez la commande suivante (redémarrez l'adaptateur Bluetooth)

sudo hciconfig hci0 down
sudo hciconfig hci0 up

J'ai trouvé qu'il récupère de l'erreur lorsque j'exécute, alors référez-vous à ici

python


def restart_hci0(masterdate, devicename):
    passwd = 'Entrer le mot de passe'
    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}]')

J'ai fait une méthode appelée.

cette,

getdata_switchbot_Une partie de la méthode thermique


        #Scannez pour obtenir la valeur du capteur
        try:
            scanner.scan(device.Timeout)
        #Si vous obtenez une erreur lors de l'analyse, redémarrez l'adaptateur Bluetooth
        except:
            restart_hci0(masterdate, device.DeviceName)

C'est un mécanisme qui peut être exécuté lorsqu'une erreur se produit et redémarré et récupéré automatiquement.

La commande ci-dessus semble être efficace pour améliorer le bogue 1, j'ai donc essayé de l'exécuter même si la valeur ne pouvait pas être obtenue dans le nombre de tentatives. ** Redémarrer en cas de problème ** est un chapeau polyvalent!

Bug 3: lorsque la valeur ne peut pas être obtenue, la cause ne peut pas être suivie.

Phénomène qui se produit

Même si vous répétez la ré-exécution pour le bogue 1, vous ne pourrez peut-être pas obtenir la valeur dans le nombre de tentatives.

En outre, en mode de diffusion comme SwitchBot, les erreurs liées aux périphériques ne se produisent pas, mais le bogue 2 se produit, la valeur n'est pas renvoyée dans le délai d'expiration et la valeur peut ne pas être acquise dans le nombre de tentatives.

Si vous ne faites rien, vous ne vous rendrez pas compte que vous ne pouvez pas obtenir la valeur, et si vous ne pouvez pas obtenir la valeur tout le temps en raison d'un échec, vous ne pouvez pas suivre quand elle a échoué.

approche

Enregistrez la valeur n'a pas pu être obtenue et affichez les 4 modèles de journaux suivants en vous référant à ici afin que la cause puisse être retracée plus tard. Je l'ai fait. ** Motif 1: journal de la partie de sortie lorsque le défaut 1 se produit **

sensors_to_spreadsheet.py getdata_omron_Fait partie de la méthode USB


        #Sortie de journal si une erreur se produit
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}]')

** Motif 2: journal de la partie de sortie lorsque le défaut 2 se produit **

sensors_to_spreadsheet.redémarrage py_Fait partie de la méthode hci0


    logging.error(f'restart bluetooth adapter [date{str(masterdate)}, device{devicename}]')

** Modèle 3: journal de la partie de sortie lorsque la valeur n'a pas pu être acquise même après une nouvelle tentative le nombre de fois spécifié **

sensors_to_spreadsheet.py getdata_omron_Fait partie de la méthode USB


    #Si la valeur n'a pas pu être obtenue, enregistrez la sortie et redémarrez l'adaptateur Bluetooth
    else:
        logging.error(f'cannot get data [loop{str(device.Retry)}, date{str(masterdate)}, device{device.DeviceName}]')

** Modèle 4: journal de la partie sortie lorsqu'une série de traitements est terminée **

sensors_to_spreadsheet.La fin du code principal de py


    #Sortie du journal de la fin du traitement
    logging.info(f'[masterdate{str(masterdate)} startdate{str(startdate)} enddate{str(datetime.today())} success{str(success_num)}/{str(sensor_num)}]')

La destination de sortie du journal est spécifiée dans "Sortie du journal" du fichier de paramètres "config.ini".

Ce que j'ai ressenti en l'essayant

Difficulté de fonctionnement stable à long terme

Il était plus difficile d'assurer un fonctionnement stable à long terme que d'acquérir des données. En particulier, l'environnement de la réception Bluetooth est instable et les erreurs qui ne sont pas reproductibles sont impitoyablement déchirantes (rires) Le processus try-except est très utile pour résoudre le problème ci-dessus, mais si vous en abusez, vous ne remarquerez pas le problème, vous devez donc l'utiliser avec prudence.

Importance de l'extensibilité

En répertoriant la gestion des appareils dans un fichier de configuration et en automatisant la génération de colonnes de feuille de calcul Il est devenu beaucoup plus facile de réagir lors de l'augmentation du nombre de capteurs. Les exigences d'évolutivité sont souvent repoussées, mais j'ai réalisé que ce ne serait pas une douleur plus tard si vous y réfléchissiez dès les premiers stades de la conception!

Racine profonde des problèmes de sécurité

Si vous essayez d'assurer la sécurité comme le masquage des mots de passe, la mise en œuvre et le fonctionnement seront compliqués, donc J'ai trouvé difficile d'équilibrer la relation de compromis avec la commodité. Il y a également un problème de confidentialité, car les données des capteurs peuvent indiquer dans une certaine mesure l'absence de personnes et ce qu'elles font. (C'est exactement la partie qui pose également problème dans la loi Super City)

Impressions pour chaque capteur

Signe de batterie déchargée

L'Inkbird IBS-TH1 mini a montré beaucoup de mou, la batterie s'épuisant en moins d'un mois. Environ 2 jours avant l'épuisement de la batterie, des valeurs anormales (température = -46,2 ° C, humidité = 0%) comme la ligne rose de la figure ci-dessous ont commencé à se produire fréquemment. InkbirdMini_異常値.png

S'il s'agit d'un signe reproductible d'une batterie déchargée, il s'agit d'une information très utile car elle peut être remplacée avant qu'elle ne soit complètement morte pour éviter les valeurs manquantes. Même si la batterie s'épuise la prochaine fois, j'observerai attentivement la situation juste avant.

Au fait, le niveau de batterie restant obtenu avec l'application Inkbird était d'environ 40% restant à ce stade, j'ai donc trouvé qu'il n'était pas du tout fiable (rires).

J'aimerais utiliser les connaissances ci-dessus pour un développement futur!

Recommended Posts

Réalisez une maison super IoT en acquérant des données de capteur dans la maison avec Raspberry Pi
Un mémo pour utiliser simplement le capteur d'éclairement TSL2561 avec Raspberry Pi 2
Utilisez le capteur d'éclairement numérique TSL2561 avec Raspberry Pi
Vérifiez! Obtenez les données du capteur via Bluetooth avec Raspberry Pi ~ Préparation
Notez ce que vous voulez faire à l'avenir avec Razpai
Traitement du langage naturel (données originales) avec Word2Vec développé par des chercheurs Google américains
Contrôlez le moteur avec un pilote de moteur en utilisant python sur Raspberry Pi 3!
Raspberry Pi --1 --Première fois (Connectez un capteur de température pour afficher la température)
Utiliser une webcam avec Raspberry Pi
Consigner périodiquement les valeurs des capteurs d'environnement Omron avec Raspberry Pi
Construction d'un environnement distribué avec la série Raspberry PI (Partie 1: Résumé de la disponibilité des clients sans disque par modèle)
L'histoire d'un capteur de stationnement en 10 minutes avec le kit de démarrage GrovePi +
Un mémo organisé en renommant les noms de fichiers dans le dossier avec python
Acquérir la valeur du capteur de Grove Pi + avec Raspberry Pi et la stocker dans Kintone
Enregistrement des valeurs du capteur d'environnement Omron avec Raspberry Pi (type USB)
Créer un environnement Tensorflow avec Raspberry Pi [2020]
Utiliser le capteur Grove avec Raspberry Pi
Capteur humain amélioré fabriqué avec Raspberry Pi
Utiliser le capteur de mouvement PIR avec Raspberry Pi
Faire une minuterie de lavage-séchage avec Raspberry Pi
Faites fonctionner l'oscilloscope avec le Raspberry Pi
Créez un compteur de voiture avec Raspberry Pi
Fabriquez un thermomètre avec Raspberry Pi et rendez-le visible sur le navigateur Partie 3
J'ai tweeté l'éclairement de la pièce avec Raspberry Pi, Arduino et un capteur optique
[Pour les débutants] J'ai fait un capteur humain avec Raspberry Pi et notifié LINE!
Utilisation du capteur de température numérique à 1 fil DS18B20 avec Raspberry Pi de Python
[Python] Récupérez les fichiers dans le dossier avec Python
Supprimer des données dans un modèle avec Redis Cluster
Envoyer des données depuis Raspberry Pi à l'aide d'AWS IOT
Sortie CSV des données d'impulsion avec Raspberry Pi (sortie CSV)
Observez le groupe de météores Futago avec RaspberryPi4
Modifiez et déboguez le code dans Raspberry Pi avec la fonction de connexion SSH de VSCode
[Ne pas se référer au 04.02.17] Afficher le capteur de température dans un graphique en temps réel avec rasberry pi 3
[Raspberry Pi] Lorsque le capteur humain le détecte, stockez l'horodatage dans la base de données Firebase Realtime.
Créez un capteur de couleur à l'aide d'une tarte à la râpe et d'une caméra
Entraînez les données MNIST avec PyTorch en utilisant un réseau neuronal
IoT facile pour démarrer avec Raspeye et MESH
Visualisons la pièce avec tarte aux râpes, partie 1
Essayez d'utiliser le capteur de température (LM75B) avec Raspeye.
Prenez la valeur du thermo-hygromètre SwitchBot avec Raspberry Pi
Changer les valeurs du thermo-hygromètre Bot avec Raspberry Pi
Un mémorandum lors de la réalisation d'une caméra de surveillance avec Raspeye
J'ai créé une classe pour obtenir le résultat de l'analyse par MeCab dans ndarray avec python
C'était génial d'éditer le fichier Python dans Raspberry Pi avec la fonction à distance d'Atom