make the smart speaker smarter 4

08/19

script for me

さて、今回は、何かをトリガーに
しゃべるって仕組みを作ってみる。
トリガーは以下を用意する予定。

  1. When I Want(時間で)
  2. If She Felt Temp/Humidity(温湿度センサから)
  3. When She Saw(カメラから)
  4. If She Felt Someone(人感センサから)

今回は1番目のやつを実装する。

say something when I want her to tell me

しゃべってほしい時間にしゃべる仕組みが
1番簡単そうなので小手調べ。
どんな風にスケジュールを管理しようかな?
例えばGoogleカレンダみたいなクラウドから
持ってくるか。そうなると、例えば目覚ましとかの
ルーティンワークも登録する必要がある。
むしろ予定はこっちから聞くことが多いから
雑多でありながら忘れがちなものを
教えてくれたほうが気が利くってもんだ。

  • ○月○日○時です。天気は○です。おはようございます。
  • もう○時です、寝る時間ですよ。
  • 家を出る時間まであと○分です。
  • お誕生日おめでとうございます。
  • 今日は○さんのお誕生日です。

みたいなの。平日と休日も理解して
繰り返してくれる。
本当は誰かの誕生日の○週間前の週末ぐらいに
教えてくれるのが親切なんだろうな。
これは後でやろう。

大まかな作りとしては
rpi+2s.jsというメインのモジュールを作って
サブモジュールとしてScheduleCaller.jsなんてのを
用意して、ここで実装する。

write down the recipe

ScheduleCaller.jsを書いてみる。
今回はJSON形式のファイルを解析して
喋らせようかなと思う。構造はこんな感じ。

[
	{"no": no,
	"name": schedule name,
	"freq": executed frequency,
	"date": executed date,
	"time": executed time,
	"message": what she speaks
	},

	{"no": no,
	.
	.
	.
	},
	
	.
	.
	.
]

こんな感じにした。
noとnameは今は特に意味はなし。
freqは次の値から選べる。

  • m : 毎時間同分に実行
  • h : 同時刻に実行
  • d : 同日同時刻に実行
  • w : 同曜日同時刻に実行

dateはfreqによって入れる値が変わる。
freqがmとhの場合はemptyでOK。
dはMM/DD形式の日付。
wは曜日の英語表記を英小文字3文字で。
(例:monとかsatとか)

timeは常に時刻を入れる。HH24:MI形式で。
ただしfreqがmの場合は分の部分しか評価しない。

messageは喋らせたい言葉を入れる。

これをschedule.jsonってファイルに保存する。

どのくらいのタイミングでチェックするかは
まだ決めてないけど、予定時刻の
30分以内か1分以内であればトリガーを
発動するようにしたい。

さてやっとモジュールを書いてみる。

const fs = require('fs');
const now = require('date-utils');
const schedJSON = './schedule.json';

const chkWeekly = 'w';
const chkDaily = 'd';
const chkHouly = 'h';
const chkMinutely = 'm';

const endTimeInterval1 = 1000 * 60 * 30; // 1800000msec = 30min
const endTimeInterval2 = 1000 * 60;      // 60000msec = 1min

class ScheduleCaller {

    constructor(){
        this.now = new Date();
        this.load();
    }

    load(){
        var promise=Promise.resolve();
        promise
            .then(this.parseJSON())
            .then(this.loadJSON())
            .catch(this.onRejected);
    }

    parseJSON(){
        this.rs = JSON.parse('{"result": false, "message": "", "weather": "" }');
    }

    loadJSON(){
        this.sc = JSON.parse(fs.readFileSync(schedJSON, 'utf8'));
    }

    onRejected(err){
        console.log(err);
    }

    isInTime(validTime, validDate, checkType) {
        var isCorrect=false;
        var validtime=new Date('1990/01/01 '+validTime);
        switch(checkType){
        case chkDaily:
            var validtime=new Date('1990/'+validDate+' '+validTime);
            var nexttime=new Date(validtime.getTime() + endTimeInterval1);
            if(this.now.toFormat("MM/DD")==validtime.toFormat("MM/DD")){
                if((this.now.toFormat("HH24:MI")>=validtime.toFormat("HH24:MI"))&&
                    nexttime.toFormat("HH24:MI")>=this.now.toFormat("HH24:MI")){
                    if(!isCorrect)isCorrect=!isCorrect;
                }
            }
            break;
        case chkWeekly:
            var day = new Date().getDay();
            var weekday = 'sunmontuewedthufrisat'.substring((day*3),(day*3)+3);
            var nexttime=new Date(validtime.getTime() + endTimeInterval1);
            if(weekday==validDate.toLowerCase()){
                if((this.now.toFormat("HH24:MI")>=validtime.toFormat("HH24:MI"))&&
                    nexttime.toFormat("HH24:MI")>=this.now.toFormat("HH24:MI")){
                    if(!isCorrect)isCorrect=!isCorrect;
                }
            }
            break;
        case chkHouly:
            var nexttime=new Date(validtime.getTime() + endTimeInterval2);
            if(this.now.toFormat("HH24")==validtime.toFormat("HH24")){
                if((this.now.toFormat("MI")>=validtime.toFormat("MI"))&&
                    nexttime.toFormat("MI")>=this.now.toFormat("MI")){
                    if(!isCorrect)isCorrect=!isCorrect;
                }
            }
            break;
        case chkMinutely:
            var nexttime=new Date(validtime.getTime() + endTimeInterval2);
            if((this.now.toFormat("MI")>=validtime.toFormat("MI"))&&
                nexttime.toFormat("MI")>=this.now.toFormat("MI")){
                if(!isCorrect)isCorrect=!isCorrect;
            }
            break;
        }
        return isCorrect;
    }

    hasTimeCome(){
        var message='';
        var needSpeak=false;
        for(var i=0;i<this.sc.length;i++){
            if(this.isInTime(this.sc[i].time, this.sc[i].date, this.sc[i].freq)){
                message+=" "+this.sc[i].message;
                if(!needSpeak)needSpeak=!needSpeak;
            }
        }
        if(needSpeak){
            this.rs.result=true;
            this.rs.message=message;
        }else{
            this.rs.result=false;
            this.rs.message=message;
        }
        return this.rs;
    }
}
module.exports=ScheduleCaller;

まだまだ改造の余地があるが、ひとまず先に進みたいので
この辺にしておく。

動作としては、JSONファイルに従って動くこととして
もし同時刻の予定が重なった場合も考えて
喋らせる言葉を直列につなぐようにした。

喋らせる言葉には変数を入れられるようにして
$HOURとか$WEATHERを入れておくと
勝手に変換されるようにする。
そのうち$FORTUNEとか$TEMPみたいなのも
追加してみたい。
$WEATHERで天気を変換したいのでTenkiモジュールを作ってみる。
天気の情報をOpen Weather Map APIを使うことにする。
取得する情報の同期処理をかけたいので
sync-requestモジュールを使っている。これは大変便利。

さて書いてみる。

const request=require('sync-request');
const mycity="Sapporo-shi,JP";
const units='metric';
const openweatherapi="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

class Tenki {

    constructor(){
        this.URL = 'http://api.openweathermap.org/data/2.5/weather?q='+ mycity +'&units='+ units +'&appid='+ openweatherapi;
        this.res;
        this.fetch();
        this.localize();
    }

    fetch(){
        var response=request('GET', this.URL);
        var body=JSON.parse(response.body);
        this.res=body;
    }

    localize(){
        var result='';
        if(this.res){
            for(var i=0;i<this.res.weather.length;i++){
                switch(this.res.weather[i].main){
                case 'Rain':
                    this.res.weather[i].wamei='雨';
                    break;
                case 'Mist':
                    this.res.weather[i].wamei='霧';
                    break;
                case 'Clear':
                    this.res.weather[i].wamei='晴';
                    break;
                case 'Clouds':
                    this.res.weather[i].wamei='曇';
                    break;
                default:
                    this.res.weather[i].wamei='不明';
                    break;
                }
            }
        }else{
	        console.error(err);
        }
    }

    getWeather(){
        return this.res;
    }
}
module.exports=Tenki;

天気の和名翻訳を自前で作っているのだが
まだ間に合ってない。
ま、これで

const tenki=require('./Tenki');
var tk=new tenki();
var w=tk.getWeather();

console.log(w.weather[0].wamei);

こんな感じで取得できる。

んで、スケジュール管理と天気の変換を行う
メインのモジュールを書いてみる。
どんどんやっつけになってきたかな?

const googleSpeak = require('./GoogleSpeak');
const scheduleCaller = require('./ScheduleCaller');
const Tenki = require('./Tenki');

class Rpi2s {

    constructor() {
        
    }

    howling(){
        this.isHowling=false;
        this.ret;
        this.sc = new scheduleCaller();
        this.fc = new Tenki();

        this.ret=this.sc.hasTimeCome();
        if(this.ret.result){
            this.message=this.changeVariables(this.ret.message);
            if(!this.isHowling)this.isHowling=!this.isHowling;
        }else{
            console.log(this.ret);
        }

        if(this.isHowling){
            this.bowwow();
        }
    }

    bowwow(){
        var gs=new googleSpeak();
        gs.meow(this.message);
    }

    changeVariables(txt){
        var message=txt;
        var forecast=this.fc.getWeather();
        return message
            .replace(/\$MONTH/i, new Date().toFormat("M"))
            .replace(/\$DAY/i, new Date().toFormat("D"))
            .replace(/\$HOUR/i, new Date().toFormat("H"))
            .replace(/\$MINUTE/i, new Date().toFormat("MM"))
            .replace(/\$SECOND/i, new Date().toFormat("SS"))
            .replace(/\$WEATHER/i, forecast.weather[0].wamei);
    }

}
module.exports=Rpi2s;

まだトリガーが一つしかないので寂しいが
これから数珠つなぎにいろんなトリガーを
くっつけていってrπ+2sを賢くしてみたい。

const Rpi2s=require('./Rpi+2s');
var rpi2s=new Rpi2s();
rpi2s.howling();

このファイルをrpi2s.jsみたいなファイル名で保存して
foreverに渡せばいいのかな?
ちょっとまだ良くわかってないから調べてみよう。

例えば、こんなJSONファイルを用意する。

[
    {"no": "1",
    "name": "good morning",
    "freq": "h",
    "date": "",
    "time": "07:00",
    "message": "$MONTH月$DAY日$HOUR時です。天気は$WEATHERです。おはようございます。"}
]

ってファイルを作ってみる。
rpi2s.jsを実行してみると

8がつ19にち7じです。 てんきはあめです。 おはようございます。

なんて朝の情報番組のキャスターみたいなことを言ってくれる。

Next

さて次は、やっとラズパイっぽいことを
してみようかな?
センサをトリガーにして
なんかしゃべらせるってやつ。


コメント: