life-seedロゴ

NEWS

Google Apps Script (GAS) を使って Googleカレンダーのリソース予約情報を取得し、Nature Remo API を用いてエアコンを制御するシステムを構築

Nature remoとIFFFTの連携が終了されたため、Googleカレンダーで会議室を予約した場合に自動でエアコンをONにするプログラムをGoogle Apps Scriptで作成しました。

構築手順と補足

 

  1. GASプロジェクトの作成: 上記のコードを新しいGoogle Apps Scriptプロジェクトにコピー&ペーストします。
  2. アクセストークンとIDの設定:
    • NATURE_REMO_ACCESS_TOKEN を、ご自身のNature Remoアクセストークンに置き換えます。
    • https://home.nature.global/
    • ※セキュリティのため、setScriptProperty 関数を使ってスクリプトプロパティに保存することを強く推奨します。
    • RESOURCE_CALENDAR_ID を、制御したいGoogleリソースカレンダーのメールアドレス形式のIDに置き換えます。
    • NATURE_REMO_APPLIANCE_ID を特定するために、GASエディタで getRemoDeviceInfo 関数を実行し、ログに表示されるアプライアンスIDを確認して置き換えてください。
  3. GASの権限承認: 初めてスクリプトを実行する際に、Googleカレンダーへのアクセスや外部URLへの接続(UrlFetchApp)の権限承認を求められますので、承認してください。
  4. トリガーの設定:
    • GASエディタの左側メニューにある「トリガー」アイコン(時計のアイコン)をクリックします。
    • 「トリガーを追加」をクリックします。
    • 「実行する関数を選択」で controlAirConditionerBasedOnCalendar を選択します。
    • 「イベントのソースを選択」で「時間主導型」を選択します。
    • 「時間ベースのトリガーのタイプを選択」で「分タイマー」を選択し、実行頻度(例: 5分ごと、10分ごと)を設定します。
    • 「保存」をクリックします。

これで、設定された間隔でスクリプトが自動的に実行され、Googleカレンダーのリソース予約情報に基づいてエアコンが制御されるようになります。

 

applianceId:の確認は下記のとおりです。

getRemoDeviceInfo() 関数を実行する:

  • GASエディタで getRemoDeviceInfo() 関数を選択し、実行ボタン(▶︎アイコン)をクリックします。

  • 実行後、「実行ログを表示」をクリックしてログを確認します。

  • ログの中に、以下のような形式でNature Remoに登録されている家電の情報が表示されます。

    Nature Remo アプライアンス情報:
      ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx  <-- これがAppliance ID
      名前: リビングエアコン
      タイプ: AC
        エアコン情報: {"modes":{"cool":{"temp":["18","19",...],"vol":["auto","1",...],"dir":["auto","swing",...]},...}}
    ---

 

				
					/**
 * @file Google Apps Script (GAS) と Nature Remo API を連携し、
 * Googleカレンダーのリソース予約情報に基づいてエアコンを制御するサンプルスクリプト。
 * @author Gemini
 */

// --- 設定値 ---
// Nature Remoのアクセストークンをここに設定してください。
// スクリプトプロパティに保存することを強く推奨します。
// 例: ScriptProperties.getProperty('NATURE_REMO_ACCESS_TOKEN');
const NATURE_REMO_ACCESS_TOKEN = 'YOUR_NATURE_REMO_ACCESS_TOKEN'; // TODO: 実際のトークンに置き換えてください

// 複数のリソースカレンダーと対応するNature Remoデバイス、エアコン設定を定義します。
// 各オブジェクトは 'calendarId' (GoogleカレンダーのリソースID),
// 'applianceId' (Nature RemoのエアコンデバイスID),
// オプションで 'acSettings' (そのリソース固有のエアコン設定) を含みます。
const RESOURCES_CONFIG = [
  {
    calendarId: 'resource1@example.com', // TODO: 実際のリソースカレンダーIDに置き換えてください
    applianceId: 'APPLIANCE_ID_FOR_RESOURCE1', // TODO: 実際のデバイスIDに置き換えてください
    acSettings: { // このリソース固有のエアコン設定(オプション)
      operation_mode: 'cool',
      temperature: '24',
      air_volume: 'auto',
      air_direction: 'auto'
    }
  },
  {
    calendarId: 'resource2@example.com', // TODO: 別のリソースカレンダーIDに置き換えてください
    applianceId: 'APPLIANCE_ID_FOR_RESOURCE2', // TODO: 別のデバイスIDに置き換えてください
    acSettings: { // 別のリソース固有のエアコン設定(オプション)
      operation_mode: 'warm',
      temperature: '22',
      air_volume: '3',
      air_direction: 'swing'
    }
  }
  // 必要に応じて、さらにリソースを追加してください
];

// イベント開始の何分前にエアコンをオンにするか
const PRE_HEAT_MINUTES = 10;

// イベント終了の何分後にエアコンをオフにするか
const POST_COOL_MINUTES = 5;

// エアコンのデフォルト設定(冷房、24度など)
// RESOURCES_CONFIG で個別の設定が指定されていない場合に使用されます。
const AC_DEFAULT_SETTINGS = {
  operation_mode: 'cool', // 'cool', 'warm', 'dry', 'blow', 'auto'
  temperature: '24',
  air_volume: 'auto', // 'auto', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'
  air_direction: 'auto' // 'auto', 'swing', 'fixed'
};

// --- メイン関数 ---
/**
 * Googleカレンダーの複数のリソース予約情報を取得し、
 * それぞれに対応するNature Remo API を用いてエアコンを制御します。
 * この関数は、GASのトリガーによって定期的に実行されることを想定しています。
 */
function controlAirConditionerBasedOnCalendar() {
  Logger.log('controlAirConditionerBasedOnCalendar 関数が実行されました。');

  const now = new Date();
  const currentMonth = now.getMonth(); // 0-indexed (0: January, 11: December)

  // 春 (3月, 4月, 5月) と秋 (9月, 10月, 11月) は処理をスキップ
  // Months are 0-indexed: March=2, April=3, May=4, September=8, October=9, November=10
  const springMonths = [2, 3, 4]; // March, April, May
  const autumnMonths = [8, 9, 10]; // September, October, November

  if (springMonths.includes(currentMonth) || autumnMonths.includes(currentMonth)) {
    Logger.log(`現在の月 (${currentMonth + 1}月) は春または秋のため、エアコン制御処理をスキップします。`);
    return; // 処理を終了
  }

  // 定義された各リソース設定をループ処理します
  for (const resourceConfig of RESOURCES_CONFIG) {
    const { calendarId, applianceId, acSettings } = resourceConfig;
    Logger.log(`カレンダーID '${calendarId}' の処理を開始します。`);

    const calendar = CalendarApp.getCalendarById(calendarId);

    if (!calendar) {
      Logger.log(`エラー: カレンダーID '${calendarId}' が見つかりません。このリソースの処理をスキップします。`);
      continue; // 次のリソースへ
    }

    // 現在から PRE_HEAT_MINUTES 後までのイベントを検索
    // 終了後も考慮して、POST_COOL_MINUTES 前から検索範囲を開始します
    const startTime = new Date(now.getTime() - (POST_COOL_MINUTES * 60 * 1000));
    const endTime = new Date(now.getTime() + (PRE_HEAT_MINUTES * 60 * 1000));

    const events = calendar.getEvents(startTime, endTime);
    Logger.log(`${events.length} 件のイベントが '${calendarId}' から取得されました。`);

    let shouldTurnOnAC = false;
    let shouldTurnOffAC = false;

    for (const event of events) {
      const eventTitle = event.getTitle();
      const eventStart = event.getStartTime();
      const eventEnd = event.getEndTime();

      Logger.log(`  イベント: ${eventTitle}, 開始: ${eventStart}, 終了: ${eventEnd}`);

      // イベント開始時刻の PRE_HEAT_MINUTES 前になったらエアコンをオンにする
      const turnOnTime = new Date(eventStart.getTime() - (PRE_HEAT_MINUTES * 60 * 1000));
      if (now >= turnOnTime && now < eventStart) {
        Logger.log(`  イベント '${eventTitle}' の開始前 ${PRE_HEAT_MINUTES} 分です。エアコンをオンにします。`);
        shouldTurnOnAC = true;
        break; // このカレンダーのエアコンはオンにすべきなので、他のイベントは確認不要
      }

      // イベントが現在進行中であればエアコンはオンのまま
      if (now >= eventStart && now < eventEnd) {
        Logger.log(`  イベント '${eventTitle}' が現在進行中です。エアコンはオンのままにします。`);
        shouldTurnOnAC = false; // イベント進行中はオンの状態を維持
        break; // このカレンダーのエアコンはオンにすべきなので、他のイベントは確認不要
      }

      // イベント終了時刻の POST_COOL_MINUTES 後になったらエアコンをオフにする
      const turnOffTime = new Date(eventEnd.getTime() + (POST_COOL_MINUTES * 60 * 1000));
      if (now >= eventEnd && now < turnOffTime) {
        Logger.log(`  イベント '${eventTitle}' が終了しました。終了後 ${POST_COOL_MINUTES} 分です。エアコンをオフにします。`);
        shouldTurnOffAC = true;
        break; // このカレンダーのエアコンはオフにすべきなので、他のイベントは確認不要
      }
    }

    // エアコン制御の実行
    if (shouldTurnOnAC) {
      // リソース固有の設定があればそれを使用し、なければデフォルト設定を使用
      turnOnAirConditioner(applianceId, acSettings || AC_DEFAULT_SETTINGS);
    } else if (shouldTurnOffAC) {
      turnOffAirConditioner(applianceId);
    } else {
      Logger.log(`  カレンダーID '${calendarId}' のエアコン制御は現在不要です。`);
    }
  }
  Logger.log('すべてのカレンダーの処理が完了しました。');
}

/**
 * Nature Remo API を使用してエアコンをオンにします。
 * @param {string} applianceId - 制御するエアコンのデバイスID。
 * @param {object} settings - エアコンの設定(operation_mode, temperatureなど)。
 */
function turnOnAirConditioner(applianceId, settings) {
  Logger.log(`エアコン (ID: ${applianceId}) をオンにするリクエストを送信します。設定: ${JSON.stringify(settings)}`);
  const url = `https://api.nature.global/1/appliances/${applianceId}/aircon_settings`;
  const headers = {
    'Authorization': `Bearer ${NATURE_REMO_ACCESS_TOKEN}`,
    'Content-Type': 'application/x-www-form-urlencoded'
  };

  const payload = Object.keys(settings).map(key => {
    return encodeURIComponent(key) + '=' + encodeURIComponent(settings[key]);
  }).join('&');

  const options = {
    method: 'post',
    headers: headers,
    payload: payload,
    muteHttpExceptions: true // エラー時も例外を投げずにレスポンスを取得
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    const responseCode = response.getResponseCode();
    const responseText = response.getContentText();

    if (responseCode === 200) {
      Logger.log(`エアコン (ID: ${applianceId}) オンリクエストが成功しました。`);
    } else {
      Logger.log(`エアコン (ID: ${applianceId}) オンリクエストが失敗しました。ステータスコード: ${responseCode}, レスポンス: ${responseText}`);
    }
  } catch (e) {
    Logger.log(`エアコン (ID: ${applianceId}) オンリクエスト中にエラーが発生しました: ${e.message}`);
  }
}

/**
 * Nature Remo API を使用してエアコンをオフにします。
 * @param {string} applianceId - 制御するエアコンのデバイスID。
 */
function turnOffAirConditioner(applianceId) {
  Logger.log(`エアコン (ID: ${applianceId}) をオフにするリクエストを送信します。`);
  const url = `https://api.nature.global/1/appliances/${applianceId}/aircon_settings`;
  const headers = {
    'Authorization': `Bearer ${NATURE_REMO_ACCESS_TOKEN}`,
    'Content-Type': 'application/x-www-form-urlencoded'
  };

  const payload = 'button=power-off'; // エアコンをオフにするためのボタンコマンド

  const options = {
    method: 'post',
    headers: headers,
    payload: payload,
    muteHttpExceptions: true
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    const responseCode = response.getResponseCode();
    const responseText = response.getContentText();

    if (responseCode === 200) {
      Logger.log(`エアコン (ID: ${applianceId}) オフリクエストが成功しました。`);
    } else {
      Logger.log(`エアコン (ID: ${applianceId}) オフリクエストが失敗しました。ステータスコード: ${responseCode}, レスポンス: ${responseText}`);
    }
  } catch (e) {
    Logger.log(`エアコン (ID: ${applianceId}) オフリクエスト中にエラーが発生しました: ${e.message}`);
  }
}

// --- 初期設定とテスト用関数 ---

/**
 * Nature RemoのデバイスIDとアプライアンスIDを取得するための関数。
 * この関数を実行して、RESOURCES_CONFIG 内の 'applianceId' の値を特定してください。
 * 実行後、ログに表示される情報を確認してください。
 */
function getRemoDeviceInfo() {
  Logger.log('Nature Remoデバイス情報を取得します。');
  const url = 'https://api.nature.global/1/appliances';
  const headers = {
    'Authorization': `Bearer ${NATURE_REMO_ACCESS_TOKEN}`
  };

  const options = {
    method: 'get',
    headers: headers,
    muteHttpExceptions: true
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    const responseCode = response.getResponseCode();
    const responseText = response.getContentText();

    if (responseCode === 200) {
      const appliances = JSON.parse(responseText);
      Logger.log('Nature Remo アプライアンス情報:');
      appliances.forEach(appliance => {
        Logger.log(`  ID: ${appliance.id}`);
        Logger.log(`  名前: ${appliance.nickname}`);
        Logger.log(`  タイプ: ${appliance.type}`);
        if (appliance.type === 'AC') {
          Logger.log(`    エアコン情報: ${JSON.stringify(appliance.aircon.range)}`);
        }
        Logger.log('---');
      });
    } else {
      Logger.log(`デバイス情報取得リクエストが失敗しました。ステータスコード: ${responseCode}, レスポンス: ${responseText}`);
    }
  } catch (e) {
    Logger.log(`デバイス情報取得中にエラーが発生しました: ${e.message}`);
  }
}

/**
 * スクリプトプロパティにNature Remoのアクセストークンを設定する関数。
 * コードに直接トークンを書き込む代わりに、この方法を推奨します。
 * 例: setScriptProperty('NATURE_REMO_ACCESS_TOKEN', 'your_actual_token_here');
 */
function setScriptProperty(key, value) {
  PropertiesService.getScriptProperties().setProperty(key, value);
  Logger.log(`スクリプトプロパティ '${key}' が設定されました。`);
}

/**
 * スクリプトプロパティからNature Remoのアクセストークンを取得する関数。
 */
function getScriptProperty(key) {
  const value = PropertiesService.getScriptProperties().getProperty(key);
  Logger.log(`スクリプトプロパティ '${key}' の値: ${value}`);
  return value;
}