Help us understand the problem. What is going on with this article?

Google Homeに声をかけずに天気とGoogleカレンダーの予定を言わせる

More than 1 year has passed since last update.

Google Homeに声をかけずに天気を言わせるを拡張して
Google Homeに声をかけずに天気とGoogleカレンダーの予定を言わせるようにしてみました。

やりたいこと

  • 声をかけずに天気とGoogleカレンダーの予定を言わせたい
  • 会社の予定と個人の予定をまとめて教えて欲しい
  • 朝が早い予定がある場合は警告して欲しい

Google APIを使用するためのClient ID&Client Secretを取得する

こちら等を参考にGoogle Developer ConsoleにアクセスしてGoogle APIを使用するための
Client IDとClient Secretを作成する。
作成したついでにリダイレクトURLに http://localhost を登録しておく。
この作業を会社のアカウントと個人のアカウント両方で行う。

Google APIのリフレッシュトークンを取得する

こちら等を参考にGoogle APIで使用するアクセストークンを取得するためにリフレッシュトークンを取得する。
こちらも会社のアカウントと個人のアカウント両方のリフレッシュトークンを用意。

天気&スケジュールつぶやきスクリプト作成

必要なパッケージのインストール

package.json
{
  "name": "google-home",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "google-home-notifier": "^1.2.0",
    "googleapis": "^28.1.0",
    "sync-request": "^6.0.0"
  }
}
$ npm install

OAuthで必要な情報を記述したjson

google-api-info.json
{
  "own": {
    "client_id": "${個人のアカウントのClient ID}",
    "client_secret": "${個人のアカウントのClient Secret}",
    "redirect_url": "http://localhost",
    "refresh_token": "${個人のアカウントのリフレッシュトークン}"
  },
  "company": {
    "client_id": "${会社のアカウントのClient ID}",
    "client_secret": "${会社のアカウントのClient Secret}",
    "redirect_url": "http://localhost",
    "refresh_token": "${会社のアカウントのリフレッシュトークン}"
  }
}

天気情報取得スクリプト

weather.js
'use strict';

const request = require('sync-request');
const darkSkyUrl = 'https://api.darksky.net/forecast/${Secert Key}/${緯度},${経度}?lang=ja&units=si';

const options = {
  headers: {'Content-Type': 'application/json'}
};

const getDate = (unixTime) => {
    const date = unixTime ? new Date(unixTime*1000) : new Date();
    return `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()}`;
};

const getTodayData = hourly => {
  const today = getDate();
  const todayData = hourly.filter(hourlyData => {
    const date = getDate(hourlyData.time);
    return today === date;
  });
  const temps = todayData.map(data => { return data.temperature; });
  const precipProbabilities = todayData.map(data => { return data.precipProbability; });
  const maxTemp = Math.max.apply(null, temps);
  const minTemp = Math.min.apply(null, temps);
  const precipProbability = Math.max.apply(null, precipProbabilities);
  return {
    maxTemp: Math.round(maxTemp),
    minTemp: Math.round(minTemp),
    precipProbability: Math.round(precipProbability * 100)
  };
};

const createMessage = body => {
  const currently = body.currently;
  const weather = currently.summary;

  const todayData = getTodayData(body.hourly.data);
  const maxTemp = todayData.maxTemp;
  const minTemp = todayData.minTemp;
  const precipProbability = todayData.precipProbability;

  const message = `今日の天気は${weather}、最高気温${maxTemp}度、最低気温${minTemp}度、降水確率${precipProbability}%です。`;
  return message;
};

exports.fetchWeatherInfo = () => {
  const response = request('GET', darkSkyUrl, options);
  const body = JSON.parse(response.getBody('utf-8'));
  return createMessage(body);
};

カレンダー情報取得スクリプト

google-calendar.js
'use strict';

const API_INFO = require("./google-api-info.json")
const { google } = require('googleapis');
const OAuth2 = google.auth.OAuth2;

const today = () => {
  const date = new Date();
  return `${date.getFullYear()}-${('0'+(date.getMonth()+1)).slice(-2)}-${('0'+date.getDate()).slice(-2)}`;
};

const isToday = (date) => {
  const regexp = new RegExp(`^${today()}`);
  return regexp.test(date);
};

const time = (dateString) => {
  const date = new Date(dateString);
  return `${date.getHours()}:${('0'+date.getMinutes()).slice(-2)}`;
};

const getTodaySchedules = (scheduleData) => {
  const schedules = scheduleData.filter((schedule) => {
    return schedule.start.date && isToday(schedule.start.date);
  }).map((schedule) => {
    return schedule.summary;
  });
  return schedules;
};

const getHourlySchedules = (scheduleData) => {
  const regexp = new RegExp(`^${today()}`);
  const schedules = scheduleData.filter((schedule) => {
    return schedule.start.dateTime && isToday(schedule.start.dateTime);
  }).map((schedule) => {
    return { time: time(schedule.start.dateTime), summary: schedule.summary };
  });
  return schedules;
};

const mergeSchedules = (mySchedules, companySchedules) => {
  const todaySchedule = mySchedules.todaySchedule.concat(companySchedules.todaySchedule);
  const hourlySchedule = mySchedules.hourlySchedule.concat(companySchedules.hourlySchedule).sort((a,b) => {
    if (Number(a.time) < Number(b.time)) return - 1;
    if (Number(a.time) > Number(b.time)) return 1;
    return 0;
  });
  return { todaySchedule: todaySchedule, hourlySchedule: hourlySchedule };
};

const createMessage = (scheduleInfo) => {
  let message = '今日の予定は、';
  let isEarly = false;
  const todaySchedule = scheduleInfo.todaySchedule;
  const hourlySchedule = scheduleInfo.hourlySchedule;
  if (!todaySchedule.length && !hourlySchedule.length) {
    return '本日登録されている予定はありません。';
  }
  if (todaySchedule.length) {
    message += `${todaySchedule.join('と、')}、`;
  }
  if (hourlySchedule.length) {
    const hourly = hourlySchedule.map(schedule => {
      return `${schedule.time}から${schedule.summary}`;
    }).join('');
    message += `${hourly}、`;
    isEarly = !!(hourlySchedule.filter(schedule => {
      return Number(schedule.time.replace(':', '')) < 1030;
    }).length);
  }
  message += 'です。';
  if (isEarly) message += '朝一の予定があるので、早めに起きてください。';
  return message;
};

const fetchTokens = async (apiInfo) => {
  const oauth2Client = new OAuth2(
    apiInfo.client_id, apiInfo.client_secret, apiInfo.redirect_url
  );
  oauth2Client.setCredentials({ refresh_token: apiInfo.refresh_token });

  const oauthPromise = new Promise((resolve, reject) => {
    oauth2Client.refreshAccessToken((err, response) => {
      if (err) return reject(err);
      resolve(response);
    });
  });
  const tokens = await oauthPromise;
  oauth2Client.credentials = tokens;
  return oauth2Client;
};

const fetchEvents = async (apiInfo) => {
  const auth = await fetchTokens(apiInfo);
  const today = new Date(new Date().setHours(0, 0, 0, 0));
  const timeMin = today.toISOString();
  const calendar = google.calendar({ version: 'v3', auth: auth});
  const calendarPromise = new Promise((resolve, reject) => {
    calendar.events.list({
      calendarId: 'primary',
      timeMin: timeMin,
      maxResults: 20,
      singleEvents: true,
      orderBy: 'startTime'
    }, (err, res) => {
      if (err) return reject(err);
      resolve(res);
    });
  });
  const response = await calendarPromise;
  const scheduleData = response.data.items;
  return {
    todaySchedule: getTodaySchedules(scheduleData),
    hourlySchedule: getHourlySchedules(scheduleData)
  };
};

const fetchSchedules = async () => {
  const mySchedules = await fetchEvents(API_INFO.own);
  const companySchedules = await fetchEvents(API_INFO.company);
  const schedule = mergeSchedules(mySchedules, companySchedules);
  return createMessage(schedule);
};

exports.fetchMySchedules = () => {
  return fetchSchedules();
};

Google Homeつぶやきスクリプト

notifier.js
'use strict';

const googlehome = require('google-home-notifier');
const googleCalendar = require('./google-calendar.js');
const weather = require('./weather.js');

const createMessage = async () => {
  const weatherInfo = weather.fetchWeatherInfo();
  const scheduleInfo = await googleCalendar.fetchMySchedules();
  const message = `おはようございます。${weatherInfo}${scheduleInfo}`;
  return message;
};

const notify = async (message) => {
  const text = await message;
  googlehome.device("Google-Home", 'ja');
  googlehome.notify(text, res => {
    console.log(text);
  });
};

notify(createMessage());

実行

$ node notifier.js

補足

文字数が多いとGoogleHomeがつぶやいてくれなかった。。
あと突然スクリプトが動かなくなって調べてみたら原因はこれだったので
node_modules/google_tts を最新にして解決した。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした