文章程式碼顯示

2018年1月30日 星期二

NodeMCU 教學 - 09:使用 WeMos D1 mini NodeMCU 網路爬蟲 抓取 JSON 天氣、空氣品質 並結合手機 BLYNK 與 Line 通知

我們最終總結一下所有,得到以下功能
  1.  WeMos D1 mini 爬氣象局天氣資料(氣溫、風速、濕度)
  2. 爬環保署的空氣品質資料( AQI 、 PM2.5 、 PM10)
  3. 利用天氣資料計算出體感溫度
  4. 使用 BLYNK 作為手機端的顯示介面(上述資料)
  5. 使用 IFTTT to Line 在每天的某一個可設定時間發送目前的氣溫、體感溫度、空氣品質 AQI 給使用者( 小時設定區間限定 : 01~23;分鐘設定區間限定 : 03~56 )
  6. 新增若資料抓取不完整或失敗,就不更新至 BLYNK
  7. 針對環保署空品這個不穩定的資料來源進行特別的處理
7.1 修改 get_AQI() 的寫法,增加等待數據時間最長為 300 sec
7.2 增加 flag ,並用一個 30 mins 的 timer 對此 flag 重置
7.3 當 flag 為 1 時,每 330秒就會觸發 330 secs timer ,在這 timer 中觸發 get_AQI()。如此一來整個收 AQI 的邏輯變成 "若在 30 分鐘內沒有收到 AQI data ,則不斷的嘗試觸發 get_AQI() " 

結合整個 WeMos D1 mini NodeMCU + JSON + BLYNK + IFTTT to Line 的以下系列文章


程式碼放在文末,因為太長了。我懶得將一些可以合併起來的地方進行合併,就把前面幾章的那些全部兜起來做成這個最終應用。






#define BLYNK_PRINT Serial
#include "ESP8266WiFi.h"
#include "ArduinoJson.h"
#include "BlynkSimpleEsp8266.h"
#include "SimpleTimer.h"
#include "WidgetRTC.h"

bool get_Weather(); 
bool showWeather(char *json);
bool get_AQI(); 
bool showAQI(char *json);
void send_event(const char *event);
void showTime(); 


// ===== Wifi setup =====
const char *ssid = "你的wifi名稱";
const char *pass = "你的wifi密碼";

// =====  Weather. setup =====
const char *privateKey = "你的識別碼";
const char *dataid = "O-A0003-001";
const char *locationName = "新竹"; //改這個地方至你要的地區
const char *elementName = "TEMP,HUMD,WDSD";//擷取溫度、濕度、風速
const char *host = "opendata.cwb.gov.tw";
static char respBuf[2048];

// =====  AQI. setup =====
const char *token = "你的識別碼";
const char *sourceid = "355000000I-000259";
const char *_limit = "1"; //limit不要動
const char *_offset = "49"; //改這個地方至你要的地區
const char *AQIhost = "opendata.epa.gov.tw";
static char AQIrespBuf[2048];
volatile int flag = 0; //在30分鐘內是否有成功抓取到 API 資料

// ==== BLYNK ====
char auth[] = "你的識別碼";
volatile int AQI;
volatile int PM2_5;
volatile int PM10;
volatile float temp;
volatile float perceived_temp; // 體感溫度
SimpleTimer timer; 
WidgetRTC rtc;

// ===== IFTTT setup =====
const char *IFTTTprivateKey = "你的識別碼";
const char *event = "你的事件名稱";
const char *IFTTThost = "maker.ifttt.com";
const int Notify_hour = 7; // 早上七點
const int Notify_min = 3; // 三分

// ===== Hardware setup =====
const int ledPin = D4;        // the number of the LED pin

/* =====================*/
// ==== 330 secs timer ====
/* =====================*/
void timer_330000(){
  if ( !flag ){
    if( get_AQI() ){
      Blynk.connect();
      Serial.println("Send AQI. data to BLYNK !!");
      Serial.println(AQI);
      Serial.println(PM2_5);
      Serial.println(PM10);
      Blynk.virtualWrite(V3, AQI); 
      Blynk.virtualWrite(V4, PM2_5); 
      Blynk.virtualWrite(V5, PM10); 
      Serial.println("===== get_AQI() DONE ! ===== ");
      delay(1000);
    }
  }
}

/* =====================*/
// ===== 60secs timer ====
/* =====================*/
void timer_60000(){
  showTime(); // 顯示目前時間於串列通訊埠
  static int event_flag = 0;

  if( (hour() == (Notify_hour - 1)) ) event_flag = 1;
  
  if( (hour() == Notify_hour) && (minute() >= (Notify_min - 3) ) && (minute() <= (Notify_min + 3) ) && event_flag )
  {
    event_flag = 0;
    send_event(event);
  }
}

/* =====================*/
// ==== 20 mins timer ====
/* =====================*/
void timer_1200000(){
  
  if( get_Weather() ){  
  Blynk.connect();
  Serial.println("Send Temperature data to BLYNK !!");  
  Blynk.virtualWrite(V1, temp);
  Blynk.virtualWrite(V2, perceived_temp);
  Serial.println("===== get_Weather() DONE ! ===== ");
  delay(1000);
  }  
}

/* =====================*/
// ==== 30 mins timer ====
/* =====================*/
void timer_1800000(){
  flag = 0;
}

/* =====================*/
// ===== setup ====
/* =====================*/
void setup() 
{
  pinMode(ledPin, OUTPUT);
  Serial.begin(115200);
  delay(10);
  digitalWrite(ledPin,HIGH);
  Blynk.connectWiFi(ssid, pass); //連線至 Wi-Fi
  Blynk.config(auth); //輸入Auth,顯示 BLYNK on Arduino 歡迎訊息
  Blynk.connect(); // 連線至 BLYNK ,成功後會顯示 Ready
  rtc.begin();

  attachInterrupt(digitalPinToInterrupt(buttonPin), buttonISR, FALLING);

  timer.setInterval(1200000L, timer_1200000); //for get_weather(), 20 mins timer
  timer.setInterval(60000L, timer_60000); // for time, 1 mins timer
  timer.setInterval(1800000L, timer_1800000); // for get_AQI(), 30 mins timer
  timer.setInterval(330000L, timer_330000); // for get_AQI(), 330 secs timer

  digitalWrite(ledPin,LOW);
  delay(1000);
}

/* =====================*/
// ===== loop ====
/* =====================*/
void loop()
{
  Blynk.run();
  timer.run();  
  delay(20);
}

/* =====================*/
// ===== get_Weather ====
/* =====================*/
bool get_Weather()
{
  digitalWrite(ledPin, HIGH);
  Serial.println("\n============================================= ");
  Serial.print("===== Connecting to ");
  Serial.print(host);
  Serial.println(" =====");
  Serial.println("============================================= ");
  
  WiFiClient client;
  
  const int Port = 80;
  if (!client.connect(host, Port)) {
    Serial.println("Connection failed");
    return false;
  }
  else Serial.println("Connection success");
  
  // Create a URI for the request
  String url = "/api/v1/rest/datastore/";
  url += dataid;
  url += "?Authorization=";
  url += privateKey;
  url += "&locationName=";
  url += locationName;
  url += "&elementName=";
  url += elementName;
  
  Serial.print("Requesting URL: ");
  Serial.println(url);
  
  // This will send the request to the server
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" + 
               "Connection: close\r\n\r\n");
  client.flush();

  int respLen = 0;
  bool skip_headers = true;
  while (client.connected() || client.available()) {
    if (skip_headers) {
      String aLine = client.readStringUntil('\n');
      if (aLine.length() <= 1) {
        skip_headers = false;
      }
    }
    else {
      int bytesIn;
      bytesIn = client.read((uint8_t *)&respBuf[respLen], sizeof(respBuf) - respLen);
      Serial.print(F("bytesIn ")); Serial.println(bytesIn);
      if (bytesIn > 0) {
        respLen += bytesIn;
        if (respLen > sizeof(respBuf)) respLen = sizeof(respBuf);
      }
      else if (bytesIn < 0) {
        Serial.print(F("read error "));
        Serial.println(bytesIn);
      }
    }
    delay(1);
  }

  Serial.println();
  Serial.println("===== Closing connection =====");
  Serial.println();
  client.stop();
  
  if (respLen >= sizeof(respBuf)) {
    Serial.print(F("respBuf overflow "));
    Serial.println(respLen);
    return false;
  }

  if(showWeather(respBuf)) { 
    Serial.println("Get Temp. data completed");
    digitalWrite(ledPin, LOW);
    return true;
   }
   else{ 
    Serial.println("Get Temp. data FAILD(JSON)");
    digitalWrite(ledPin, LOW);
    return false;
   }  
}

/* =====================*/
// ===== showWeather ====
/* =====================*/
bool showWeather(char *json){  
  StaticJsonBuffer<2048> jsonBuffer;

  // 前處理
  char *jsonstart = strchr(json, '{');
  Serial.println("jsonstart");
  Serial.println(jsonstart);
  Serial.println("");
  if (jsonstart == NULL) {
    Serial.println(F("JSON data missing"));
    return false;
  }
  json = jsonstart;

  // Parse(解析) JSON
  JsonObject& root = jsonBuffer.parseObject(json);  
  if (!root.success()) {
    Serial.println(F("jsonBuffer.parseObject() failed"));
    return false;
  }

  // Extract weather info from parsed JSON
  float wdsd = root["records"]["location"][0]["weatherElement"][0]["elementValue"];
  float _temp = root["records"]["location"][0]["weatherElement"][1]["elementValue"];  
  float humd = root["records"]["location"][0]["weatherElement"][2]["elementValue"];
    
  float _perceived_temp = 1.04*_temp + 0.2*humd*6.1*exp( (17.27*_temp)/(237.7+_temp) ) - 0.65*wdsd -2.7;
  
  Serial.print("Temp. = ");
  Serial.println(_temp);
  Serial.print("Wdsd. = ");
  Serial.println(wdsd);
  Serial.print("Humd. = ");
  Serial.println(humd);
  Serial.print("Perceived Temp. = ");
  Serial.println(_perceived_temp);

  temp = _temp;  // 用一個全域變數供 get_Weather 調用
  perceived_temp = _perceived_temp;
  
  return true;
}

/* =====================*/
// ===== get_AQI ====
/* =====================*/
bool get_AQI()
{
  digitalWrite(ledPin, HIGH);
  Serial.println("\n============================================= ");
  Serial.print("===== Connecting to ");
  Serial.print(AQIhost);
  Serial.println(" =====");
  Serial.println("============================================= ");
  
  WiFiClient client;
  
  const int Port = 80;
  if (!client.connect(AQIhost, Port)) {
    Serial.println("Connection failed");
    return false ; //若連線失敗直接離開 get_AQI()
  }
  else Serial.println("Connection success");
  
  // ====== Create a URI for the request =====
  String url = "/webapi/api/rest/datastore/";
  url += sourceid;
  url += "/?format=json&limit=";
  url += _limit;
  url += "&offset=";
  url += _offset;
  url += "&sort=County&token=";
  url += token;
  
  Serial.print("Requesting URL: ");
  Serial.println(url);
  
  // This will send the request to the server
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + AQIhost + "\r\n" + 
               "Connection: close\r\n\r\n");
  client.flush();

  int AQIrespLen = 0;
  bool skip_headers = true;
  unsigned long last_time = millis();
  flag = 1;
  
  Serial.println("Waiting for response");
  while (client.connected() || client.available()) {
    if (skip_headers) {
      String aLine = client.readStringUntil('\n');
      if (aLine.length() <= 1) skip_headers = false;
    }
      else {
        int bytesIn;
        bytesIn = client.read((uint8_t *)&AQIrespBuf[AQIrespLen], sizeof(AQIrespBuf) - AQIrespLen);
          if (bytesIn > 0) {
            AQIrespLen += bytesIn;
            if (AQIrespLen > sizeof(AQIrespBuf)) AQIrespLen = sizeof(AQIrespBuf);
          }
          else if (bytesIn < 0){
            Serial.print(F("read error "));
            Serial.println(bytesIn);
          }
      }//end else
  delay(1);
    if( ( millis() - last_time ) > 300000 ) {
      Serial.println("[ERROR] TIME OUT (300sec)");
      client.stop();
      flag = 0;
      return false;
      }
  } // end while
  
  Serial.println("===== Closing connection =====");
  client.stop();
  
  if (AQIrespLen >= sizeof(AQIrespBuf)) {
    Serial.print(F("AQIrespBuf overflow "));
    Serial.println(AQIrespLen);
    flag = 0;
    return false;
  }
  
  AQIrespBuf[AQIrespLen++] = '\0';
  Serial.print(F("AQIrespLen "));
  Serial.println(AQIrespLen);
  Serial.print("Use ");
  Serial.print((millis() - last_time)/1000);
  Serial.println(" sec");

  if(showAQI(AQIrespBuf)) { 
    Serial.println("Get AQI. data SUCCESS");
    digitalWrite(ledPin, LOW);
    flag = 1; //成功擷取到 AQI 資料
    Serial.print("flag = ");
    Serial.println(flag);
    return true;
  }
    else{
      Serial.println("Get AQI. data FAILED ");
      digitalWrite(ledPin, LOW);
      flag = 0;
      return false;    
    }
}

/* =====================*/
// ===== showAQI ====
/* =====================*/
bool showAQI(char *json){
  StaticJsonBuffer<2048> jsonBuffer;
  
  Serial.println("Show AQI. data");
  
  char *jsonstart = strchr(json, '{');
  Serial.println("jsonstart");
  Serial.print(jsonstart);
  if (jsonstart == NULL) {
    Serial.println(F("JSON data missing"));
    return false;
  }
  json = jsonstart;

  JsonObject& root = jsonBuffer.parseObject(json);
  if (!root.success()) {
    Serial.println(F("jsonBuffer.parseObject() failed"));
    return false;
  }

  int _AQI = root["result"]["records"][0]["AQI"];
  int _PM2_5 = root["result"]["records"][0]["PM2.5"];
  int _PM10 = root["result"]["records"][0]["PM10"];
  
  Serial.print("AQI = ");
  Serial.println(_AQI);
  Serial.print("APM2.5 = ");
  Serial.println(_PM2_5);    
  Serial.print("PM10 = ");
  Serial.println(_PM10);    

  if ( ( _AQI > 0 ) && ( _PM2_5 > 0 ) && ( _PM10 > 0 ) ) {
    AQI = _AQI;
    PM2_5 = _PM2_5;
    PM10 = _PM10;
    Serial.println("AQI Update");
    return true;
  }
  else{
    AQI = AQI;
    PM2_5 = PM2_5;
    PM10 = PM10;
    Serial.println("AQI Maintain");
    return false;
  }
}
/* =====================*/
// ===== send_event ====
/* =====================*/
void send_event(const char *event)
{
  digitalWrite(ledPin, HIGH);
  Serial.print("Connecting to ");
  Serial.println(IFTTThost);
  WiFiClient client;
  
  const int httpPort = 80;
  if (!client.connect(IFTTThost, httpPort)) {
    Serial.println("Connection failed");
    return;
  }
  else Serial.println("Connection successful");
  
  // Create a URI for the request
  String url = "/trigger/";
  url += event;
  url += "/with/key/";
  url += IFTTTprivateKey;
  url += "?";
  url += "value1=";
  url += temp;
  url += "&";
  url += "value2=";
  url += perceived_temp;
  url += "&";
  url += "value3=";
  url += AQI;

  Serial.print("\nSend Temperature = ");
  Serial.println(temp);
  Serial.print("Send Perceived Temperature = ");
  Serial.println(perceived_temp);
  Serial.print("AQI = ");
  Serial.println(AQI);
  
  Serial.print("Requesting URL: ");
  Serial.println(url);  
  // This will send the request to the server
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + IFTTThost + "\r\n" + 
               "Connection: close\r\n\r\n");

  // Read all the lines of the reply from server and print them to Serial,
  // the connection will close when the server has sent all the data.
  while( client.connected() )
  {
    if(client.available()){
      String line = client.readStringUntil('\r');
      Serial.print(line);
    }
    else {
      delay(50); // No data yet, wait a bit
    }
  }
  
  Serial.println();
  Serial.println("===== Closing connection =====");
  client.stop();
  digitalWrite(ledPin, LOW);
}

/* =====================*/
// ===== showTime ====
/* =====================*/
void showTime() {
    String t="";
    
    byte h = hour();
      if (h < 10) {t.concat('0');}
      t.concat(h);
      t.concat(':');
      
    byte m = minute();
      if (m < 10) {t.concat('0');}
      t.concat(m);
      t.concat(':');
      
    byte s = second();    
      if (s < 10) {t.concat('0');}
      t.concat(s);
      
   Serial.print("Current Time -> ");
   Serial.println(t);
}


其中 goodMorning 是我在 IFTTT 上面設定的事件名稱,將 value1, value2, value3 都用上了
欲更改成你要的地點,請看下方兩篇

NodeMCU 教學 - 06:使用 WeMos D1 mini NodeMCU 網路爬蟲 抓取 JSON 天氣資訊

NodeMCU 教學 - 08:使用 WeMos D1 mini NodeMCU 網路爬蟲 抓取 JSON 空氣品質 並結合手機 BLYNK

BLYNK 方面的設置,請看

《進階※應用篇》寫程式Arduino教學 - 03:手機與 Arduino 的連結 BLYNK 應用簡述

NodeMCU 教學 - 02:WeMos D1 mini NodeMCU 與 BLYNK

IFTTT Line 通知方面,請看

NodeMCU 教學 - 03:WeMos D1 mini (NodeMCU) 與 Line 的連結

↓↓↓ 連結到部落格方針與索引 ↓↓↓

Blog 使用方針與索引