ESP-01で部屋の湿度を制御する

#はじめに
 ストレージに使用し始めた部屋が風通しが悪く、カビが生えそうで怖いのです。そこで、湿度をモニターし一定以上になったらエアコンをONして湿度を下げる装置をESP-01(ESP8266)で作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/812e1bfa-aa6d-21c8-f0d9-db1b98fd73c1.png)

#構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/94887d08-9ad3-39d3-bb42-e1e53274723a.png)



 湿度の検出には、温度、湿度、気圧を測定可能なBME280というモジュールを使用します。同時に温度などの状態をOLEDで表示します。また、5分に一度Ambientへデータを送信しますので、PC、スマホから現在のデータ、過去からの推移をグラフとしてモニターすることが可能です。
湿度が57%を越えた場合は、赤外LEDを操作しエアコンを1時間ONし湿度を低下させます。
 全体のコントロールはESP-01(ESP8266)で行なっています。

#必要なもの
##ESP-01
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/0c01efc2-6608-5289-bb85-d90ee3faf69e.png)

ESP8266を搭載したボードであればどれでも構いません。**日本では、ESP-01は技適の認証がありませんので違法となる可能性があります。**

##赤外LED部
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/5f69ae47-b79d-975a-4801-1fa7be9e548d.png)

SD-AR512C9という940nmのIR LEDを使用しました。60mAが定格のため、3.3Vでは( 3.3 - 1.2 )/0.060 = 35ohm、35Ωの電流制限抵抗で定格となります。手持ちの関係で62Ωを使用しています。
トランジスタには2SC1815を使用しました。

##I2Cチェーン
BME280と0.96インチOLEDはI2C接続ですので、電源2本、クロック、データの計4本でバス接続することが可能です。
###BME280
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/ac1439a6-6bae-4fd4-c051-e8c49e474fea.png)
BME280とBMP280の二つがあります。BM **E**は温度、湿度、気圧を測定可能で、BM **P**は温度と気圧です。BM **E**が必要です。

###0.96インチOLED
Ambientにアップロードするので無くても良いですが、ちょっと部屋に入って温度湿度を確認するのに便利です。消費電力が増えるので、電池駆動には適していません。電池駆動を考える場合はOLEDを外し、ESP8266をディープスリープモードを活用することになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/0fb7da78-2e97-bb04-9c16-a14ab0cf5a50.png)

本作品では温度、湿度、気圧、現在時刻、エアコンの状態/OFFする時間、カビの危険度をアイコンで表示します。
表示は下記にしたがいます。
// < 65 : GOOD
// 65 - 74 : 注意
// 75 < : 危険

##電源
ESP-01の電源は3.3Vです。レギュレータにはHT7333を使用しました。


##その他
ケース、PCB、コンデンサ、配線材、半田、コテ等が必要となります。
ケースは50*58*24MMのアルミケースを使用しています。アルミならハンドドリルとヤスリでなんとかなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/7e8d9599-e348-fb0e-d638-e8f9b1383bcc.png)

#回路
##回路図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/428a9b2a-657b-4b07-a56f-44d696d83d63.png)

##見た目
###表
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/6a939c55-ca8f-d963-66e4-d17022b74494.png)
右上の欠けはDCジャックの干渉除けです。

###裏
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/66baf09e-e60c-6c15-3730-caa870a2191f.png)

###結合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/87c38967-161d-e9f7-7954-7596905bba07.png)

#ソフトウェア
##ソース

```KABI.cpp
///////////////////////////////////////////////////////////////////////
// 2020/07/31
// ESP-01 & BME280 & OLED & Ambient & Clock & Aircon Control
///////////////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////////////
// OLED SSD1306
// 1  VCC
// 2  GND
// 3  SCL  -- Clk to I2C SCL (GPIO 2)
// 4  SDA  -- Data to I2C SDA (GPIO 0)
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
// BME280 (BMP280と同じ)
// 1  VCC
// 2  GND
// 3  SCL  -- Clk to I2C SCL (GPIO 2)
// 4  SDA  -- Data to I2C SDA (GPIO 0)
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
// KABI Waring
// < 55 : Kabi cannot grow
// < 65 : OK
// 65 - 74 : Kabi grows within few months
// 75 - 89 : Kabi grows faster
// > 90    : Within few days
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
// IR LED
//             VCC
//              |
//             LED (SD-AR512C9 940nm V1.2-1.3, 40mA)
//              |
//              & 60ohm
//              |
// 1  RX  -www--K (2SC1815)
//         470  |
//             GND
///////////////////////////////////////////////////////////////////////

#include <Wire.h>
#include <ESP8266WiFi.h>
#include <time.h>     // for time(), localtime()

#include "BME280_MOD-1022.h"    // https://github.com/embeddedadventures/BME280
#include <SSD1306.h>
#include <SSD1306Wire.h>
#include "Ambient.h"

#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <ir_Mitsubishi.h>

#define PERIOD 300        // Ambientに送信するタイミング
#define AIRCON_BUFFER 5   // エアコンをONする移動平均値 (PERIOD x AIRCON_BUFFER) 5なら30分の移動平均で判断する
#define AIRCON_ON_THR 56  // 56%を越えたらエアコンON
#define AIRCNO_ON_TIME  3600000L  // mill sec
#define RX    3

unsigned long runtime = 0L;
float temperature = 0.0;
float humidity    = 0.0;
float pressure    = 0.0;
char time_text[32];     // current time
char off_time_Text[32]; // Aircon off time
uint8_t AirconStatus = 0x01;  // bit 0:OFF, bit 1:ON, bit 2:OFF->ON(trigger), bit 3:ON->OFF(trigger)
int8_t HumiDataBuff[AIRCON_BUFFER];
unsigned long HumiBuffPos = 0;
unsigned long aircon_on_timing = 0L;

// Create a display object
SSD1306  display(0x3c, 0, 2, GEOMETRY_128_64); //0x3d for the Adafruit 1.3" OLED, 0x3C being the usual address of the OLED

// WIFI Setting
const char* ssid = "SET YOUR WiFi SSID";        // ***CHANGE HERE*** ここにWiFiのSSIDを入力
const char* password = "SET YOUR WiFi PASS";    // ***CHANGE HERE*** ここにWiFiのPASSを入力

// Ambient setting
unsigned int channelId =  00000;                // ***CHANGE HERE*** Ambient Channel IDを入力
const char* writeKey = "00ff11ee22dd33cc";      // ***CHANGE HERE*** Ambinet Write IDを入力 

WiFiClient client;
Ambient ambient;

// Remote controller
const uint16_t kIrLed = RX;  // ESP8266 GPIO pin to use.
IRMitsubishiAC ac(kIrLed);  // Set the GPIO used for sending messages.


// NTP設定
#define TIMEZONE_SGT  (3600 * 8)  // 日本標準時は、UTC（世界標準時）よりも９時間進んでいる。
#define DAYLIGHTOFFSET_SGT  (0)   // 日本は、サマータイム（夏時間）はない。
#define NTP_SERVER1   "ntp.nict.jpntp.nict.jp"  // NTPサーバー
#define NTP_SERVER2   "ntp.jst.mfeed.ad.jp"   // NTPサーバー


void drawTemp( float temp);
void drawHumidity( float hum);
void drawPressure( float pre);
void drawKabiWarn( float hum);
void UpdateDisplay( bool bWithUpload);
void drawUploading();
void AirconOn(void) ;
void AirconOff(void);
void printState();
bool HuimidityControl( uint8_t* p_sts );
void AirconControl( uint8_t* p_sts);
int8_t GetHumiAverage();      // HumiDataBuffの平均値を入手
void SetHumiData(float humi); // リングバッファに設定
bool AirconOnTimeover(); // エアコンがONした時間が、OFFにする時間を過ぎたかどうか
void SetTimeText();   // 現在時刻をセット time_textにセットされる
void SetAConOffTimeText(bool OnOff);  // エアコンOFF時間のセット off_time_Textにセットされる
void readBME280();

const uint8_t Hum_Normal[] PROGMEM = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x7F, 0x00,
  0x00, 0xFE, 0xFF, 0x00, 0x80, 0x1F, 0xF0, 0x03, 0xC0, 0x07, 0xC0, 0x07,
  0xE0, 0x03, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x1C,
  0x78, 0x00, 0xE0, 0x3C, 0x38, 0x00, 0xF0, 0x38, 0x38, 0x00, 0x78, 0x38,
  0x1C, 0x00, 0x3C, 0x70, 0x1C, 0x00, 0x1E, 0x70, 0x1C, 0x00, 0x0F, 0x70,
  0x1C, 0x87, 0x07, 0x70, 0x1C, 0x8F, 0x07, 0x70, 0x1C, 0xCF, 0x03, 0x70,
  0x1C, 0xFE, 0x01, 0x70, 0x38, 0xFE, 0x00, 0x38, 0x38, 0xFC, 0x00, 0x38,
  0x78, 0x7C, 0x00, 0x3C, 0x70, 0x18, 0x00, 0x1C, 0xF0, 0x00, 0x00, 0x1E,
  0xE0, 0x01, 0x00, 0x0F, 0xC0, 0x07, 0xC0, 0x07, 0x80, 0x1F, 0xF0, 0x03,
  0x00, 0xFF, 0xFF, 0x01, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0xE0, 0x0F, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

const uint8_t Hum_Worn[] PROGMEM = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00,
  0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00,
  0x00, 0xF0, 0x0F, 0x00, 0x00, 0x70, 0x0E, 0x00, 0x00, 0x78, 0x1E, 0x00,
  0x00, 0x38, 0x1C, 0x00, 0x00, 0x3C, 0x3C, 0x00, 0x00, 0x1C, 0x38, 0x00,
  0x00, 0x9E, 0x79, 0x00, 0x00, 0xCE, 0x73, 0x00, 0x00, 0xEF, 0xF7, 0x00,
  0x00, 0xE7, 0xE7, 0x00, 0x80, 0xE7, 0xE7, 0x01, 0x80, 0xE3, 0xC7, 0x01,
  0xC0, 0xC3, 0xC3, 0x03, 0xC0, 0xC1, 0x83, 0x03, 0xE0, 0xC1, 0x83, 0x07,
  0xE0, 0x80, 0x01, 0x07, 0xF0, 0x80, 0x01, 0x0F, 0x70, 0x00, 0x00, 0x0E,
  0x78, 0xC0, 0x03, 0x1E, 0x38, 0xC0, 0x03, 0x1C, 0x3C, 0xC0, 0x03, 0x3C,
  0x1C, 0x00, 0x00, 0x38, 0xFE, 0xFF, 0xFF, 0x7F, 0xFE, 0xFF, 0xFF, 0x7F,
  0xFC, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00,
};

const uint8_t Hum_Danger[] PROGMEM = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x7F, 0x00,
  0x00, 0xFE, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x03, 0xC0, 0xF8, 0xFF, 0x07,
  0xE0, 0xF0, 0x3F, 0x0E, 0xE0, 0xF0, 0x1F, 0x1E, 0xF0, 0xE1, 0x0F, 0x1E,
  0xF8, 0xC3, 0x07, 0x3F, 0xF8, 0x87, 0x83, 0x3F, 0xF8, 0x0F, 0xC1, 0x3F,
  0xFC, 0x1F, 0xE0, 0x7F, 0xFC, 0x3F, 0xF0, 0x7F, 0xFC, 0x3F, 0xF8, 0x7F,
  0xFC, 0x1F, 0xF0, 0x7F, 0xFC, 0x0F, 0xE0, 0x7F, 0xFC, 0x07, 0xC3, 0x7F,
  0xFC, 0x83, 0x87, 0x7F, 0xF8, 0xC1, 0x0F, 0x3F, 0xF8, 0xE0, 0x1F, 0x3E,
  0x78, 0xF0, 0x3F, 0x3C, 0x70, 0xF8, 0x3F, 0x1C, 0x70, 0xFC, 0x7F, 0x1C,
  0xE0, 0xFF, 0xFF, 0x0F, 0xC0, 0xFF, 0xFF, 0x07, 0x80, 0xFF, 0xFF, 0x03,
  0x00, 0xFF, 0xFF, 0x01, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0xE0, 0x0F, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

const uint8_t Uploading[] PROGMEM = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x7C, 0x00,
  0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x01,
  0x00, 0x00, 0xFF, 0x03, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0x03,
  0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFD, 0x07,
  0x00, 0xFE, 0xF8, 0x07, 0x00, 0x7E, 0xF0, 0x0F, 0x00, 0x3F, 0xE0, 0x1F,
  0xE0, 0x1F, 0xC0, 0x1F, 0xF8, 0x0F, 0x80, 0x3F, 0xFC, 0x07, 0x00, 0x7F,
  0xFC, 0x07, 0x00, 0x7F, 0xFC, 0x07, 0x00, 0xFF, 0xFE, 0x3F, 0xE0, 0xFF,
  0xFE, 0x3F, 0xE0, 0xFF, 0xFE, 0x3F, 0xE0, 0xFF, 0xFC, 0xFF, 0xFF, 0x7F,
  0xF8, 0x3F, 0xE0, 0x3F, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

/////////////////////////////////////////////////////////////////////////
// SETUP
/////////////////////////////////////////////////////////////////////////
void setup() {
  Serial.begin(9600);

  pinMode(RX, OUTPUT);  // IO3

  Wire.pins(0, 2);  // Start the OLED with GPIO 0 and 2 on ESP-01
  Wire.begin(0, 2); // 0=sda, 2=scl

  // Displayを初期化
  display.init();
  display.flipScreenVertically();
  display.setFont(ArialMT_Plain_10);
  display.setBrightness( 16 ); // 0: low brightness, 254 : high

  // BME280を初期化
  display.drawString(0,  2, "BME initializing."); display.display();
  BME280.readCompensationParams();

  // オーバーサンプリングの回数を設定
  BME280.writeOversamplingTemperature(os1x);
  BME280.writeOversamplingHumidity(os1x);
  BME280.writeOversamplingPressure(os1x);
  Serial.println("BME280 start.");
  readBME280(); // read from BME280 and set to data for initial value of HumiDataBuff[]
  readBME280(); // 1回目は変な値が読み込まれるためもう一度読む
  display.drawString(0,  2, "BME initializing...done"); display.display();

  // wifi初期化
  display.drawString(0, 12, "WiFi connecting."); display.display();
  WiFi.begin(ssid, password);
  Serial.println("WiFi start.");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  display.drawString(0, 12, "WiFi connecting...done"); display.display();

  // チャネルIDとライトキーを指定してAmbientの初期化
  display.drawString(0, 22, "Ambient connecting."); display.display();
  ambient.begin(channelId, writeKey, &client);
  Serial.println("Ambient start.");
  display.drawString(0, 22, "Ambient connecting...done"); display.display();

  // NTP同期
  display.drawString(0, 32, "NTP syncing."); display.display();
  configTime( TIMEZONE_SGT, DAYLIGHTOFFSET_SGT, NTP_SERVER1, NTP_SERVER2 );

  int i = 0;
  time_t timeNow = time(NULL);
  while ( ((timeNow / 100) * 100) == TIMEZONE_SGT )
  {
    timeNow = time(NULL);
    Serial.println(timeNow);
    delay(1000);
    i++;
    if (i > 30) // 偶然起動タイミングが8:00だった場合は、強制的にぬける
      break;
  }
  display.drawString(0, 32, "NTP syncing...done");
  display.display();
  Serial.println("NTP READY");


  // AC start
  ac.begin();
  memset( HumiDataBuff, (int8_t)(humidity), sizeof(HumiDataBuff));  // Buffer clear -> fill by current humiditiy
  AirconStatus = 0x01; // Aircon Sts clear : 0x01 = OFF
  HumiBuffPos = 0;  // Humi 移動平均buff clear
  SetAConOffTimeText(false);  // AirconOFF text reset

  Serial.println("Setup finish");

  // time set
  runtime = millis();
}

/////////////////////////////////////////////////////////////////////////
// MAIN
/////////////////////////////////////////////////////////////////////////
void loop() {

  if (WiFi.status() == WL_CONNECTED) //Check WiFi connection status
  {
    SetTimeText();  // time_text[]に設定される
  }
  
  ///////////////////////////////////////////////
  // read from BME280
  readBME280();

  unsigned long millsec = millis();
  if ( millsec < runtime ) // タイマーが一周した
  {
    //millsec = (ULONG_MAX - runtime) + millis(); // これではループ2回目に対応できない
    runtime = 0; // やっつけ処理
  }
  //////////////////////////////////////////////////
  // データを更新
  if ( (millsec - runtime) > (PERIOD * 1000) ) // OLEDの更新+Ambientに送る
  {
    //////////////////////////////////////
    // AIRCON設定つける？
    bool aircon ;
    SetHumiData( humidity );  // Humiをバッファにセット
    // Check humidity
    aircon = HuimidityControl( &AirconStatus );  // Aircon control need to update

    //////////////////////////////////////
    // 画面更新
    UpdateDisplay(true );

    //////////////////////////////////////
    // 操作
    UploadToAmbient();   // data upload to Ambient
    if ( aircon ) // エアコンの制御必要？
      AirconControl( &AirconStatus);

    runtime = millsec;
  }
  else
  {
    UpdateDisplay(false);                     // OLEDの更新
  }

  // 読み取った値をシリアルにプリント
  Serial.print("Temperature: ");
  Serial.println(temperature);
  Serial.print("Humidity: ");
  Serial.println(humidity);
  Serial.print("Pressure: ");
  Serial.println(pressure);

  delay(10000);
}

///////////////////////////////////////////////////////////////////////
// AMIBIENT FUNCTION
///////////////////////////////////////////////////////////////////////

///////////////////////////////////////////
// AMBIENTにアップロード
void UploadToAmbient()
{
  // Ambientに連携
  ambient.set(1, temperature);
  ambient.set(2, humidity);
  ambient.set(3, pressure);
  // uint8_t AirconStatus = 0;  // bit 0:OFF, bit 1:ON, bit 2:OFF->ON(trigger), bit 3:ON->OFF(trigger)
  ambient.set(4, AirconStatus );
  ambient.set(5, GetHumiAverage());
  ambient.send();
}

///////////////////////////////////////////////////////////////////////
// Display functions
///////////////////////////////////////////////////////////////////////

///////////////////////////////////////////////
// bWithUpload : true : Display uploading mark
void UpdateDisplay( bool bWithUpload)
{
  display.clear();

  drawTemp( temperature );
  drawHumidity( humidity );  // 少数部は無しとする
  drawPressure( pressure ); //おまけ表示
  drawKabiWarn( humidity) ; // test
  drawTime( time_text );
  drawOffTime( off_time_Text );
  if ( bWithUpload )
    drawUploading();
  display.display();
}

///////////////////////////////////////////////
// draw current time
void drawTime( char* time_text )
{
  display.setFont(ArialMT_Plain_10);
  display.drawString ( 0, 40 , "Now" );
  display.setFont( Arimo_Regular_16 );
  display.drawString( 0, 42 + 1/*pressure*/ + 5 /* gap to above*/ , time_text );
}


///////////////////////////////////////////////
// draw current time
void drawOffTime( char* time_text )
{
  display.setFont(ArialMT_Plain_10);
  display.drawString ( 43, 40 , "Off" );
  display.setFont( Arimo_Regular_16 );
  display.drawString( 43, 42 + 1/*pressure*/ + 5 /* gap to above*/ , time_text );
}

///////////////////////////////////////////////
// draw Temperatute
void drawTemp( float temp)
{
  char text[32];
  display.setFont(Lato_Heavy_30);
  sprintf(text, "%2d", (int)(temp + 0.05) ); // (int)で小数点以下切り捨て。23.96等の場合に備えて0.05を加えておく
  display.drawString( 0, 0 , text);
  display.setFont(Arimo_Regular_16);
  sprintf(text, ".%d", (int)((temp + 0.05) * 10.) % 10 ); // 0.01桁を四捨五入する

  display.drawString( 36, 15 , text);

  display.drawCircle( 51 + 2, 16, 1);
  display.drawString( 50, 15, " C");
  display.setFont(Arimo_Regular_16);
}
///////////////////////////////////////////////
// draw Humidity
void drawHumidity( float hum)
{
  char text[32];
  sprintf(text, "%2d", (int)(hum + 0.5)  ); // 整数部.小数部は四捨五入
  display.setFont(Lato_Heavy_30);
  display.drawString( 75, 0 , text);
  display.setFont(Arimo_Regular_16);
  display.drawString( 128 - 16, 15, "%");
  display.setFont(Lato_Heavy_30);
}
///////////////////////////////////////////////
// draw Pressure
void drawPressure( float pre)
{
  char text[32];

  sprintf(text, "%d%s", (int)pre, "hPa" ); // 整数部.小数部は無し
  display.setFont(ArialMT_Plain_10);
  display.drawString( 0, 31 , text);
  //  display.setFont(Arimo_Regular_16);
  //  display.drawString( 64 + 10 + 2, 32+10, " hPa");
  display.setFont(Lato_Heavy_30);
}
///////////////////////////////////////////////
// draw かび警告アイコン
// < 55 : Kabi cannot grow
// < 65 : OK
// 65 - 74 : Kabi grows within few months
// 75 - 89 : Kabi grows faster
// > 90    : Within few days
void drawKabiWarn( float hum)
{
  display.setFont(ArialMT_Plain_10);
  display.drawString( 73, 33, "Kabi");
  const uint8_t* picture = Hum_Normal;  // Hum_Normal, Hum_Worn, Hum_Danger
  if ( hum < 65.0 )
    picture = Hum_Normal;
  else if ( hum < 75.0 )
    picture = Hum_Worn;
  else
    picture = Hum_Danger;

  display.drawXbm(95, 33, 32, 32, picture);
}
///////////////////////////////////////////////
// draw Ambientにアップロード中アイコン
void drawUploading()
{
  display.drawXbm(1, 33, 32, 32, Uploading);
}
/////////////////////////////////////
// 時間表示文字列設定
void SetTimeText()
{
  // 曜日文字列配列
  //static const char *pszWDay[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
  //static const char *pszMonth[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

  // 現在時刻の取得
  time_t timeNow = time(NULL);
  struct tm* tmNow = localtime(&timeNow);

  // 時刻
  sprintf( time_text, "%02d:%02d",
           tmNow->tm_hour,
           tmNow->tm_min
         );
}

///////////////////////////////////////////////////////////////////////
// BME280 functions
///////////////////////////////////////////////////////////////////////
void readBME280()
{
  // BME280を1度だけ測定を行うモードに設定し計測が終わるまで待機
  BME280.writeMode(smForced);
  while (BME280.isMeasuring()) {
    delay(1);
  }

  // BME280から測定値を読み取る
  BME280.readMeasurements();
  temperature = BME280.getTemperature();
  humidity = BME280.getHumidity();
  pressure = BME280.getPressure();

}

////////////////////////////////////////////////
// エアコンOFF時間のセット off_time_Textにセットされる
// OnOff : true : ON
//       : false: OFF
void SetAConOffTimeText(bool OnOff)
{
  if( OnOff == false ) // OFF時の表示
  {
    sprintf( off_time_Text, "%s", "OFF");
    return;
  }

  // エアコンOFF時間のセット off_time_Textにセットされる
  time_t timeNow = time(NULL);
  struct tm* tmNow = localtime(&timeNow);

  int8_t off_hr = tmNow->tm_hour;
  int8_t off_min= tmNow->tm_min;

  // エアコンON間隔の計算
  int8_t add_hr = (int8_t)((AIRCNO_ON_TIME / 3600000L));
  int8_t add_min= (int8_t)((AIRCNO_ON_TIME % 3600000L)/(60L*000L));

  off_min += add_min;
  if( off_min > 60 )
  {
    add_hr ++;
    off_min %= 60;
  }
  off_hr += add_hr;
  if( off_hr >= 24 )
    off_hr -= 24;
  
  // OFF 時刻
  sprintf( off_time_Text, "%02d:%02d", off_hr, off_min);
}


/////////////////////////////////////////////////////////////////////////
// エアコン制御
/////////////////////////////////////////////////////////////////////////

/////////////////////////////////////
// AIRCON
// ON button
void AirconOn(void) {
  Serial.println("Sending IR command to A/C ...");
  ac.on();
  ac.setFan(1);
  ac.setMode(kMitsubishiAcCool);
  ac.setTemp(24);
  ac.setVane(kMitsubishiAcVaneAuto);
  ac.send();
  printState();
  aircon_on_timing = millis();  // ONした時間をセット
  SetAConOffTimeText(true);   // OFF時間を設定
  AirconStatus = 0x02 ; // bit 0:OFF, bit 1:ON, bit 2:OFF->ON(trigger), bit 3:ON->OFF(trigger)

}

/////////////////////////////////////
// AIRCON
// OFF button
void AirconOff(void) {

  Serial.println("Sending IR command to A/C ...");

  ac.off();
  ac.send();
  printState();
  SetAConOffTimeText(false);   // OFF時間を消す
  AirconStatus = 0x01 ; // bit 0:OFF, bit 1:ON, bit 2:OFF->ON(trigger), bit 3:ON->OFF(trigger)

}
/////////////////////////////////////
// AIRCON
// 状態出力(Serial)
void printState() {
  // Display the settings.
  Serial.println("Mitsubishi A/C remote is in the following state:");
  Serial.printf("  %s\n", ac.toString().c_str());
  // Display the encoded IR sequence.
  unsigned char* ir_code = ac.getRaw();
  Serial.print("IR Code: 0x");
  for (uint8_t i = 0; i < kMitsubishiACStateLength; i++)
    Serial.printf("%02X", ir_code[i]);
  Serial.println();
}
///////////////////////////////////////////////
// HumiDataBuffの平均値を入手
int8_t GetHumiAverage()
{
  int16_t average = 0;
  for ( int i = 0; i < AIRCON_BUFFER; i++)
    average += HumiDataBuff[i];
  average /= AIRCON_BUFFER ;
  return (int8_t) average;
}
///////////////////////////////////////////////
// HumiDataBuffにデータをセット
void SetHumiData(float humi)
{
  HumiDataBuff[ HumiBuffPos ] = (int8_t)humi;
  HumiBuffPos++;
  if ( HumiBuffPos >= AIRCON_BUFFER ) // reset ring buff pos
    HumiBuffPos = 0;
}
///////////////////////////////////////////////
// エアコンの制御を判断する
// true  : need to control aircon
// false : no need
//
// ON  : 30分の移動平均が65%を越えたら
// OFF : ONして1時間経過したら
bool HuimidityControl(uint8_t* p_sts )
{
  bool required = false;
  // #define AIRCON_BUFFER 5   // エアコンをONする移動平均値 (PERIOD x AIRCON_BUFFER) 5なら30分の移動平均で判断する
  // uint8_t AirconStatus = 0;  // bit 0:OFF, bit 1:ON, bit 2:OFF->ON(trigger), bit 3:ON->OFF(trigger)
  // int8_t HumiDataBuff[AIRCON_BUFFER];
  // #define AIRCON_ON_THR 65  // 65%を越えたらエアコンON

  int8_t avarage = GetHumiAverage();
  if ( (avarage > AIRCON_ON_THR) && (*p_sts & 0x01) ) // 湿度が閾値を越えた && OFFしている
  {
      *p_sts |= 0x04 ;  // bit 2:OFF->ON(trigger)
      required = true;
  }
  else if ( (*p_sts & 0x02) && AirconOnTimeover() ) // ONしている && 1時間経過した
  {
    *p_sts |= 0x08 ;  // bit 3:ON->OFF(trigger)
    required = true;
  }
  return required;
}
////////////////////////////////////////
// ONしてから1時間経過した？
bool AirconOnTimeover()
{
  unsigned long lasp = millis();
  if ( lasp < aircon_on_timing ) // 1周した
  {
    //lasp = (ULONG_MAX - aircon_on_timing) + millis();
    aircon_on_timing = 0; // やっつけ処理
  }
  // #define AIRCNO_ON_TIME  3600000L  // mill sec
  return ( (lasp - aircon_on_timing) > AIRCNO_ON_TIME ) ? true : false;
}

////////////////////////////////////////////////
//uuint8_t AirconStatus = 0;  // bit 0:OFF, bit 1:ON, bit 2:OFF->ON(trigger), bit 3:ON->OFF(trigger)
void AirconControl( uint8_t* p_sts )
{
  if ( *p_sts & 0x04 ) // ON trigger
  {
    AirconOn();
    // *p_sts &= ~0x04 ;  // Trigger OFF
    *p_sts = 0x02 ; // ON , OFF->ON(trigger) OFF
  }
  if ( *p_sts & 0x08 ) // OFF trigger
  {
    AirconOff();
    // *p_sts &= ~0x08 ;  // Trigger OFF
    *p_sts = 0x01 ; // OFF & Trigger OFF(0x08)
  }
}

```

##ライブラリを幾つか使用しています。
includeを見て必要なものをインストールしてください。Headerファイルで検索すると出てきます。

##仕様説明

画面更新は10秒毎、Ambientへは5分毎にアップロードしています。エアコンを制御するための情報は5回分の移動平均を使用し突発的な異常値を無視できるように配慮しています。
エアコンのON-OFF制御は、移動平均が57%になったON, 1時間経過したらOFFにしています。ここは実際の環境に合せて変更ください。

下記で基本設定を変更可能です。

```
#define PERIOD 300        // Ambientに送信するタイミング
#define AIRCON_BUFFER 5   // エアコンをONする移動平均値 (PERIOD x AIRCON_BUFFER) 5なら30分の移動平均で判断する
#define AIRCON_ON_THR 56  // 56%を越えたらエアコンON
#define AIRCNO_ON_TIME  3600000L  // mill sec
```

##エアコンの能力
エアコン制御の部分をコメントアウトして、どのような温度変化になるか確認したあと、エアコンを動作させどれだけ湿度が下るか、どれだけ湿度を維持できるかを確認し、パラメータを設定してください。

当方の環境(ドア締切)では、1時間ONしたあと回復までに12時間は必要でした。1日か2日に一時間で十分かと思うので、一日のON制限回数を付けても良いかと思っています。


#動作
温度推移がAmbinetで確認できますので安心できます。エアコン制御のフラグや移動平均の情報も載っていますので、解析にも活用可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/4bc54ebf-ba8a-bae6-6f76-6b5c81617ea1.png)


#おわりに
おもった以上に良く動作してくれました。これでカビフリーになれれば。

エアコンOFF後に熱交換器乾燥のための空運転や、空気循環のための空運転機能なども組込むのも良いかと思っています。