20190728のNode.jsに関する記事は12件です。

npmの-"g"オプション

私、node.jsは初心者で、こんなことも知らなかった。調べればすぐにわかる事でも少しずつ書き留めていく。

npmの"-g"オプション=パッケージスコープ

npmを使ってパッケージをインストールする際に"-g"オプションは以下のように機能する。

  • -gを使ってインストールしたパッケージはその環境のnode.jsの機能拡張と同義という事。
  • -gを使用せずインストールしたパッケージはプログラム/プロジェクト自体の拡張パッケージになるという事。

というように、"-g"の有無により、インストールしたパッケージのスコープが変わる。
私が考える"-g"のメリデメは以下の通り。

  • メリット1:今使っている環境で他のプログラム/プロジェクトでも利用(共有)できる
  • メリット2:プログラム/プロジェクト毎のパッケージが不要になるのでディスク領域の消費を抑えることができる。
  • デメリット1:他の環境にデプロイする場合、その環境にも事前に対象パッケージをインストールしておく必要がある。(インストールステップが増える)
  • デメリット2:プロジェクト毎に異なるバージョンを利用したい場合に対応できない??(ほんと?)

私の感覚では、各プロジェクトではそれぞれ必要なパッケージをきちんと管理したほうが良いと思うので、できるだけ"-g"は利用したくないと考える。
今後どのような場面に出くわすかによって、この記事で書いた内容が覆ることがあるかもしれないが、現時点では"-g"なしインストールの方針で行く。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

レストラン検索Bot作ってみた(店カテゴリー選択、リッチメニュー、英語対応)

はじめに

何番煎じかわからないですが、位置情報からレストランを検索できるLINEBotを開発しました。なるべくお金をかけずに開発する上で工夫した点についてまとめます。

開発したもの

友達登録QRコードです。良ければ動作をお試し下さい!
ぐるなびAPIは一時利用のため、2019/11/5まで利用可能です。
QRコード.png

環境

  • AWS Lambda(Node.js 10.x)
  • Amazon API Gateway
  • Messaging API
  • ぐるなびAPI
  • Google Translate API(v3.0)

工夫①:位置情報を含めた文脈

最初の会話で送ってもらった位置情報を次の会話で使用したい場合にどうするかです。
これは、MessagingAPIのpostbackを使用しました。

位置情報のリクエストを受け取ると、そのリクエストをもとにpostbackの選択肢を作成します。

indexJa.js
async function handleLocationMessage(payload, callback) {
    try {

        var latitude = payload.message.latitude;
        var longitude = payload.message.longitude;

        const replymessage = JSON.stringify({
            replyToken: payload.replyToken,
            messages: [
                {
                    "type": "template",
                    "altText": "店のカテゴリを選んでください。",
                    "template": {
                        "type": "buttons",
                        "actions": [
                            {
                                "type": "postback",
                                "label": "和食",
                                "displayText": "和食",
                                "data": `category_l=RSFST01000&latitude=${latitude}&longitude=${longitude}`

                            },
                            {
                                "type": "postback",
                                "label": "中華",
                                "displayText": "中華",
                                "data": `category_l=RSFST14000&latitude=${latitude}&longitude=${longitude}`

                            },
                            {
                                "type": "postback",
                                "label": "洋食",
                                "displayText": "洋食",
                                "data": `category_l=RSFST13000&latitude=${latitude}&longitude=${longitude}`

                            },
                            {
                                "type": "postback",
                                "label": "アジア・エスニック料理",
                                "displayText": "アジア・エスニック料理",
                                "data": `category_l=RSFST15000&latitude=${latitude}&longitude=${longitude}`

                            }
                            ],
                            "title": "店のカテゴリ",
                            "text": "店のカテゴリを選んでください"
                    }
                }
            ]
        });

    await Line.postMessage(replymessage);

    callback(null,"");

    }
    catch (error) {
        return callback(new Error(error.message));
      }
    }

工夫②:位置情報のURIアクション

リッチメニューから直接位置情報を送れるように、URIアクションを設定しました。

Qiita用.png

richmenu_ja.json
{
  "size": {
    "width": 2500,
    "height": 1686
  },
  "selected": true,
  "name": "レストラン検索",
  "chatBarText": "メニュー",
  "areas": [
    {
      "bounds": {
        "x": 114,
        "y": 207,
        "width": 1118,
        "height": 1386
      },
      "action": {
        "type": "uri",
        "uri": "line://nv/location"
      }
    },
    {
      "bounds": {
        "x": 1421,
        "y": 561,
        "width": 1033,
        "height": 421
      },
      "action": {
        "type": "message",
        "text": "日本語に設定して"
      }
    },
    {
      "bounds": {
        "x": 1413,
        "y": 1004,
        "width": 1041,
        "height": 442
      },
      "action": {
        "type": "message",
        "text": "Please set to English"
      }
    }
  ]
}

ちなみにリッチメニューのJSONはLINE Bot Designerから簡単に作成することができました。

工夫③:言語設定

リッチメニューから言語設定をできるようにしました。
最初はDBやキャッシュを参照して...と考えましたが、「現在のリッチメニューのID」で言語の判断をしていいます。

具体的なフローは
①リッチメニューからの言語設定のリクエストを受信
③言語によって各リッチメニューを動的にセット
③位置情報のリクエストを受信すると、現在のリッチメニューIDを取得
④リッチメニューIDによって、言語それぞれの処理を実行

結構力技ですが、リッチメニューから言語設定を実現できました!

ソース

https://github.com/g-kawano/restaurantSearchBot

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

puppeteerを使ってyoutubeの新着動画を探そう

動機

良い紳士向けMMDが出ているかもしれない。
チェックしなくてはならない。

成果物

youtube_puppet(github)
※slackに関する処理にtoken情報が記載されていたため、そのjsファイルだけはpushしていない状態です。
上記をbat経由で呼び出しています。
トリガーとしてタスクスケジューラーを使用しています。
(将来的にはタスクスケジューラーは止めて別のにする予定です。)

最終的な成果物の出力イメージは以下画像
image.png

概要

簡単に成果物の概要を説明をします。
まず、実現したいこととしては「本日に投稿された紳士向けMMD動画を取得し、それをslack上に特定の時間に流したい」ということです。

・紳士向けMMD動画を取得する方法を考えます。
これはいわゆるスクレイピング技術で解決できるでしょう。
クローラーとも呼ばれます。
以下参考
ウェブスクレイピング(wiki)

紳士向けMMD動画を取得することに関しては上記で完了しました。

・slackに投稿する方法を考えます
slackに投稿することに関しては以下を参考にすると良いでしょう。
postMessage
slackAPIを使いたいが、どこでtokenを発行するのか分からない人は下記リンクを参照してください。
https://api.slack.com/apps

・特定の時間に起動させる方法を考えます
手段は色々ありますが、
最も簡単な方法だと簡易なbatを書き、それをタスクスケジューラーで呼び出すことだと思います。
以下参考
Windowsでbatファイルを自動実行したい時(タスクスケジューラの設定)

新しいのが好きな方であればfirebaseとか使っちゃえばいいと思います。
流行ってますし。

以下もうちょっと詳しく解説。

URLの取得

image.png

上記画像のように何を検索するか、対象期間はいつにするかを設定し、その際のURLを使えば望みの物が拾えると思います。
今回は「紳士向け MMD」と検索し、フィルターを使い「今日」に絞りました。

puppeteerの出番です。

puppeteerを使い、スレイピングしていきましょう。
そのためにまずF12を押してhtmlを見ていきましょう。
※各自で確認お願いします。

結果だけ言うと欲しいものとしては
document.getElementsByClassName('text-wrapper')
こんな感じで一覧が取れそうでした。
そこからさらに必要な情報を抜き取っていけば望みのものが出来そうです。
具体的にはtext-wrapper配下にある
#meta > #title-wrapper > .title-and-badge > a
のhrefがあれば十分です。
なのでpuppeteerを使ってそれを取得しましょう。

puppeteerの使い方は下記が参考になると思います。
puppeteerでの要素の取得方法

puppeteerの基礎から知りたい方は書籍を買うといいでしょう。
私は下記の書籍のみ購入し、後はネットで調べました。
Puppeteer入門 スクレイピング+Web操作自動処理プログラミング Kindle版

一度書店で立ち読みしてから購入を検討されると良いと思います。

slackAPIを使って投稿

実際のソースを貼るとtokenとか丸見えだったんです。
tokenだけ別ファイルにするとかしてpushしようと思ったんですがめんどくさくなったので
コードを貼り付けます。

const request_promise = require("request-promise");
const SLACK_TOKEN = 'とーくん';
const CHANNEL_ID = 'ちゃんねるID';
const USER_NAME = 'ぼっとのなまえ';

exports.send_message = items => {
  const messages = items.map(item => {
    return item.href;
  });

  messages.forEach(message => {
    (async () => {
      console.log(
        await request_promise({
          uri : 'https://slack.com/api/chat.postMessage',
          method : "POST",
          headers : {
              'content-type' : 'application/x-www-form-urlencoded',
              'charset' : 'utf-8'
          },
          form : {
            token: SLACK_TOKEN,
            channel: CHANNEL_ID ,
            username: USER_NAME ,
            text: message
          },
          json : true
        })
      )
    })()
  });
}

slackのメッセージ投稿については下記が参考になると思います。
NodejsでSlackにメッセージ投稿(2019年1月版)

最終的な成果

一番上の参考画像の通り、slackにURLが投稿されて、動画の一覧が出てきました。

あとがき

今回の成果物ですが、main.jsに記載されている1か所を変えれば恐らく好きな動画をスレイピング出来る物になってると思います。
12行目のawait page.goto('');のURLの部分です。
良ければ変更して使ってください。
使ってくださいと言っている割にcloneしてその部分を変更するだけで使えるものになってないのは申し訳ないなと思います。
cloneもしたくないけど紳士向けMMD動画の新着が見たい方はtwitterでDMください。
何とかするかもしれません。

結構簡単に作れたのでpuppeteer面白いなぁという感想です。
コードは結構拾ってきた部分が多いので記述的にこれで良いのか不安な部分があったりします。
変な部分があれば良かったら指摘してください。

以上です。
最後まで読んでいただいた方ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsにて為替情報を取得

中華人民元を日本円で換算したらいくらになっているのかをスクレイピングで取得する

scraping.js
import * as request from 'request';
import * as client from 'cheerio-httpcli'

export function getNowGenValue():number {
    const url = 'https://jp.investing.com/currencies/cny-jpy';
    let urlRes = client.fetchSync(url) // 受け取った値をカスタマイズしてresposeとして返すために同期処理にする
    return parseInt(urlRes.$("#last_last").text());
}

質は有料のものよりも劣るかもだけど、割と爆速で取得できる。
非同期処理にしてもいいかもだけど取得できてなかったらだるいので同期処理にした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LodashでObjectをマージする方法

VueとかVuexでObjectをlodashを使って、必要なところだけマージしたい時に何回か調べるのでメモまでに。

lodashとかをあまり知らない人からすれば同じだけれど、よく知っている人からしたら実装意図がわからなくなってしまう問題。

ただのObjectsをマージするなら
_.defaults();

prototype chainまで見るなら(Object.assignと似てる)
_.assign();

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【firestore】 Cloud Functions for FirebaseからFire Storeへデータ追加する~位置情報(GeoPoint)編~

今開発中のアプリの中でfirestoreにGeoPoint型のデータを追加する処理を書く機会があったので、忘備録も兼ねて記載。

Cloud FunctionsでFire StoreにGeoPoint型のデータ追加を行う

GeoPointクラスに関するFirebase公式ドキュメントはこちら
(日本語のリファレンスはまだないみたいですね...)

サンプルコード

今回はサンプルコードとして梅田駅の位置情報をfirestoreに追加するファンクションを書いてみます。

index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
var fireStore = admin.firestore();

  exports.insertHotelRoom = functions.region('asia-northeast1').https.onRequest((request, response) => {
    var dataRef = fireStore.collection('Stations');
    var data ={
        Address:'大阪府大阪市北区梅田1-1-1',
        Area:'梅田',
        Location:  new admin.firestore.GeoPoint(34.421740, 135.295420),
        Name: 'UMEDA STATION'
    }
    // Add a new document with a generated id.
    dataRef.add({
        data
    }).then(ref => {
        console.log('Added document with ID: ', ref.id);
    }).catch(err =>{
        response.send('ERROR HAPPENED.'+err)
    });
  })

簡単ですが、以上になります。
シンプルにnew admin.firestore.GeoPoint(緯度,経度)でGeoPointの設定ができるみたいですね。
参考になれば幸いです。

参考にした記事

本記事を書くにあたって(というよりもCloud FunctionsからFire Storeを参照する処理について)以下の記事を参考にさせていただきました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Express】セッションの値が取得できない

はじめに

セッションに保存した値を取得したいが、エラーを吐かれる。
とりあえず、下記の方法で解決したが、詳しい原因はよくわかっていない。

問題

index.js
// セッションに保存したい内容
var data = {
  title: 'HELLO',
  msg: 'hello world',
}

router.get('/', function(res, req, next) {
  req.session.data = data;
});

router.post('/', function (res, req, next) {
  function (err, result) {
    var hoge = req.session.data.msg;
  }
});

req.session.dataが定義されていないとのエラーを吐かれる。

TypeError: Cannot read property 'msg' of undefined

解決策

index.js
router.post('/', function (res, req, next) {
  var hoge = req.session.data.msg; //変更点
  function (err, result) {
    console.log(hoge);
  }
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gulp v4 で typescript + sequelize と transpile する

sequelize も typescript で記述したい

sequelize v5 で typescript もサポートしたので、まるごと typescript化してトランスパイルにチャレンジしてみました。sequelize の標準構成である config/config.json も使いつつ、models/*.tsdist/ 配下へトランスパイルして配置したいと考えました。

しかし、標準の tsc では typescript のファイルをトランスパイルしてくれますが、*.json などのファイルは指定した outDirへは配置してくれません。何事もなければ ./src 配下でファイルを作って、実行に必要なファイルを ./distへ配置するようにしたいと願ったとき、tscだけでは対処できないのでした。

gulp を使おう

そこで、タスクランナーである gulp の出番です。対象のファイルや機能毎に、個別にタスクを割り当ててバッチ処理を行えます。いわゆる make のようなものです。javascript で記述できるので、自由度も高いですね。

さて、元のディレクトリ構成はこんな感じです。migrations はマイグレーションでしか利用しないので、トランスパイルも不要ですし、sequelize-cli の吐き出すものをそのまま利用しています。実行時に必要なのは ./src/models./src/config/config.json です。

src
├── index.ts
├── config
│   └── config.json
├── migrations
│   └── 20190728*.js
├── models
│   ├── index.ts
│   ├── schema.ts
│   └── *****.ts
└── seeders

実行時の ./dist/ ではこのようなディレクトリ構成になってほしいわけです。

dist
├── config
│   └── config.json
├── models
│   ├── index.js
│   ├── schema.js
│   └── *****.js
└── index.js

gulp のタスクを実装して実行しよう

まず、必要なモジュールを入れておきます。

npm install --save gulp-cli gulp gulp-typescript typescript

gulp v4 になって、書き方もだいぶモダンになったので、それに合わせてタスクを書いてみます。
構成ファイルが複数になるのは切ないので、トランスパイルには tsconfig.json ファイルの内容を利用するようにしています。

gulpfile.js
const { src, dest, parallel, series } = require('gulp');
const ts = require('gulp-typescript');
const tsconfig = require('./tsconfig.json');

const config = {
  output: 'dist/',
  json: {
    source: 'src/**/*.json'
  }
}
const typescript = () => {
  return src(tsconfig.include)
    .pipe(ts(tsconfig.compilerOptions))
    .pipe(dest(config.output))
}

const json = () => {
  return src(config.json.source)
    .pipe(dest(config.output))
}

exports.typescript = typescript;
exports.default = series(parallel(typescript, json));

こんだけ。注意点は typescript() 内の .pipe(dest(...)) が必要なこと。

npx gulp

でどうぞ。

参考情報:
gulp-typescript

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gulp v4 で typescript + sequelize を transpile する

sequelize も typescript で記述したい

sequelize v5 で typescript もサポートしたので、まるごと typescript化してトランスパイルにチャレンジしてみました。sequelize の標準構成である config/config.json も使いつつ、models/*.tsdist/ 配下へトランスパイルして配置したいと考えました。

しかし、標準の tsc では typescript のファイルをトランスパイルしてくれますが、*.json などのファイルは指定した outDirへは配置してくれません。何事もなければ ./src 配下でファイルを作って、実行に必要なファイルを ./distへ配置するようにしたいと願ったとき、tscだけでは対処できないのでした。

gulp を使おう

そこで、タスクランナーである gulp の出番です。対象のファイルや機能毎に、個別にタスクを割り当ててバッチ処理を行えます。いわゆる make のようなものです。javascript で記述できるので、自由度も高いですね。

さて、元のディレクトリ構成はこんな感じです。migrations はマイグレーションでしか利用しないので、トランスパイルも不要ですし、sequelize-cli の吐き出すものをそのまま利用しています。実行時に必要なのは ./src/models./src/config/config.json です。

src
├── index.ts
├── config
│   └── config.json
├── migrations
│   └── 20190728*.js
├── models
│   ├── index.ts
│   ├── schema.ts
│   └── *****.ts
└── seeders

実行時の ./dist/ ではこのようなディレクトリ構成になってほしいわけです。

dist
├── config
│   └── config.json
├── models
│   ├── index.js
│   ├── schema.js
│   └── *****.js
└── index.js

gulp のタスクを実装して実行しよう

まず、必要なモジュールを入れておきます。

npm install --save gulp-cli gulp gulp-typescript typescript

gulp v4 になって、書き方もだいぶモダンになったので、それに合わせてタスクを書いてみます。
構成ファイルが複数になるのは切ないので、トランスパイルには tsconfig.json ファイルの内容を利用するようにしています。

gulpfile.js
const { src, dest, parallel, series } = require('gulp');
const ts = require('gulp-typescript');
const tsconfig = require('./tsconfig.json');

const config = {
  output: 'dist/',
  json: {
    source: 'src/**/*.json'
  }
}
const typescript = () => {
  return src(tsconfig.include)
    .pipe(ts(tsconfig.compilerOptions))
    .pipe(dest(config.output))
}

const json = () => {
  return src(config.json.source)
    .pipe(dest(config.output))
}

exports.typescript = typescript;
exports.default = series(parallel(typescript, json));

こんだけ。注意点は typescript() 内の .pipe(dest(...)) が必要なこと。

npx gulp

でどうぞ。

参考情報:
gulp-typescript

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ExpressからHeroku Postgresを使う

はじめに

HerokuでHeroku Postgresを使うアプリケーションを初めて公開する際に色々と手間取ったので、本記事に一連の流れをまとめる。
なお、アプリケーションの作成にはExpressを用いた。

環境変数を使う

環境変数を使うことで、ソースコードにべた書きしたくない情報(DB接続情報やAPIキーなど)を隠すことができる。

前提・環境

  • Heroku CLIインストール済み
  • Herokuに該当プロジェクトをあげてある
  • windows 10
  • node 10.15.3
  • heroku 7.26.2

Heroku Postgresの準備

Heroku上のアプリにHeroku Postgresを導入

  1. 「Resources > Find more add-ons > Heroku Postgres」の順に移動。
  2. 「Install Heroku Postgres」でインストール。
  3. 「Hobby Dev」という無料プランを選択。
  4. Heroku Postgresを導入するアプリケーションを選択し、「Provision addo-ons」をクリック。
  5. 導入完了

Heroku Postgresへの接続

command
heroku pg:psql -a [Heroku上のアプリ名]

データ用意

基本的なSQL文の書き方は割愛。

テーブル作成

command
CREATE TABLE Rank(
id SERIAL,
hands INTEGER NOT NULL,
name VARCHAR(10) NOT NULL,
level INTEGER NOT NULL,
PRIMARY KEY(id)
);

ダミーレコード挿入

command
INSERT INTO Rank (hands, name, level) VALUES (10, 'taro', 2);

Heroku Postgresの環境変数設定

接続情報入手

メニューから、「DATA > アプリ名 > Settings > (Database Credentialsの)View Credentials」の順に移動。
Host, Database, User, Port, Passwordなどの情報が入手できる。
注意点として、これらの情報をソースコードにべた書きしてGitHub上に上げる、などということは絶対にしないように!!
(この情報使えば、誰でもデータベースいじれてしまうので…)

環境変数設定

command
$ heroku config:set [任意のKEY]=[環境変数を指定] --app [アプリ名を指定]

# 例えば、HOST情報を環境変数に設定したい場合
$ heroku config:set ENV_HOST=xxxxxxxxxxxxxxxx --app SAMPLEAPP

上記のようにして、Host, Database, User, Port, Passwordを環境変数に設定すれば、基本的にデータベース接続に必要な情報は十分。
もちろん、ENV_HOSTなどのKEYは自由に命名して問題ない。

設定できたかの確認・削除

command
# 環境変数一覧の取得
$ heroku config:get -a [アプリ名]

# 特定の環境変数の取得
$ heroku config:get [KEY] -a [アプリ名]

# 環境変数の削除
$ heroku config:unset [KEY] -a [アプリ名]

環境変数を使う

例えば、Expressアプリの場合は下記のようにして扱う。

node-dotenvをインストール

command
npm install --save dotenv

process.env.KEYとすることで、Herokuで設定した環境変数を使えるようになる。

db/db.js
const pg = require('pg');
require('dotenv').config();

exports.pool = pg.Pool ({
  host: process.env.ENV_HOST,
  databese: process.env.ENV_DB,
  user: process.env.ENV_USER,
  port: 5432,
  password: process.env.ENV_PASSWORD,
});

一応接続できるか確認する簡単なサンプルコードも以下に。
※express-generatorで作成したアプリに手を加えています。

route/index.js
const express = require('express');
const router = express.Router();

const db = require('../db/index');

router.get('/', (req, res, next) => {
  db.pool.connect((err, client) => {
    if (err) {
      console.log(err);
    } else {
      client.query('SELECT name, hands FROM rank' (err, result) => {
        console.log(result.rows);
      });
    }
  });
  res.render('index', {
    title: 'hello express',
  });
});

ちなみに、.envファイルをプロジェクト直下に用意することで、ローカルではローカルのPostgreSQLを参照することができる。

.env
ENV_HOST = localhost
ENV_DB = [ローカルの任意のdb名]
ENV_USER = postgres
ENV_PASSWORD = [ローカルのpostgresのパスワード]

.envファイルの内容はgitに上げてはいけないので、.gitignoreファイルに下記の記述も忘れずに。

.gitignore
.env

おわりに

本記事ではは、Node.jsのフレームワークExpressで作ったアプリケーションにHeroku Postgresを導入する方法をまとめた。
また、サンプルコードは、筆者が作ったアプリから抜粋してきたものなので、そのままコピペしてもうまくいかない可能性があるのでご注意を。
間違いなどあれば、コメントにお願いします。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Grafanaで温湿度IoTをグラフ化する

せっかくGrafanaがわかってきたので、グラフ化するデータをちょっと増やそうと思います。
今回グラフ化するのは、温湿度です。2つの情報源を扱います。

  • Xiaomi Mijia温湿度計

 以前以下の記事を投稿し、定期的に室内の温度・湿度を記録しました。今回もそれを使います。
  Xiaomi Mijia 温湿度計 をIoTデバイスとして使う

  • 気象庁の天気予報情報

 これも以前、以下の記事を投稿し、天気予報情報を取得しました。今回はこのうち、最高気温・最低気温・降水確率を使います。
  LINE Beaconを自宅に住まわせる

前者は、刻々と変わる室内温度を記録することで、昼と夜の違い、人が部屋にいる場合といない場合の違いが如実に分かります。
後者は、部屋の温度と連動するのに加えて、雨雲が通り過ぎていくのがなんとなくわかります。

そんなことより、グラフを見ていただければすぐわかります。
まず前者の室内の温湿度です。

image.png

今度は、最高気温・最低気温・降水確率です。

image.png

Xiaomi Mijia温湿度計で温度と湿度を記録する

まずは記録するデータベースのテーブルを作成します。
テーブル名はとりあえず「envdata」としました。

image.png

次に温湿度の取得のスクリプトを作成します。
実際には、以下の記事の焼き直しです。
 Xiaomi Mijia 温湿度計 をIoTデバイスとして使う

以下のnpmモジュールを使っています。

  • noble
  • xiaomi-gap-parser
  • mysql
  • dotenv
index.js
var noble = require('noble');
var xiaomi = require('xiaomi-gap-parser');
var mysql = require('mysql');
require('dotenv').config();

const DB_HOST = process.env.DB_HOST || MySQLサーバのホスト名】;
const DB_PORT = process.env.DB_PORT || MySQLサーバのポート番号】;
const DB_USER = process.env.DB_USER || MySQLサーバのユーザ名】;
const UB_PASSWORD = process.env.DB_PASSWORD || MySQLサーバのパスワード】;
const DB_NAME = process.env.DB_NAME || 【データベース名】;
const DB_TABLE = process.env.DB_TABLE || 【テーブル名】;

var conn = mysql.createConnection({
    host : DB_HOST,
    port : DB_PORT,
    user : DB_USER,
    password : UB_PASSWORD,
    database : DB_NAME
});

const SCAN_DURATION = process.env.SCAN_DURATION ? parseInt(process.env.SCAN_DURATION) : 30000;
const SCAN_INTERVAL = process.env.SCAN_INTERVAL ? parseInt(process.env.SCAN_INTERVAL) : 600000;;

function wait_async(timeout){
    return new Promise((resolve, reject) =>{
        setTimeout(resolve, timeout);
    });
}

noble.on('stateChange', async function(state) {
    console.log('stateChange: ' + state);
    if (state === 'poweredOn') {
//        do{
            console.log('Start Scanning');
            noble.startScanning(["fe95"]);
            await wait_async(SCAN_DURATION);
            console.log('Stop Scanning');
            noble.stopScanning();
//            await wait_async(SCAN_INTERVAL);
            process.exit();
//        }while(true);
    } else {
        noble.stopScanning();
        process.exit();
    }
});

noble.on('discover', async (peripheral) => {
    var serviceData = peripheral.advertisement.serviceData;
    if (serviceData && serviceData.length) {
        for (var i in serviceData) {
            if( serviceData[i].uuid == 'fe95' ){
                var data = serviceData[i].data;
                var mijia = xiaomi.readServiceData(data);
                console.log(mijia);

                var now = new Date();

                var value = {
                    mac: mijia.mac,
                    tmp: mijia.event.data.tmp,
                    hum: mijia.event.data.hum,
                    created_at: now.getTime(),
                };

                noble.stopScanning();

                await insert_db(value);
            }
        }
    }
});

async function insert_db(value){
    return new Promise((resolve, reject) =>{
        conn.connect((err) => {
            if(err){
                console.error('error connecting: ' + err.stack);
                return reject(err);
            }

            if( !value.tmp || !value.hum )
                return resolve({result: 'NG'});

            try {
                var insert_str = 'INSERT INTO ' + DB_TABLE + " (type, mac, tmp, hum, created_at) VALUES ('mijia', '" + value.mac + "', " + value.tmp + ", " + value.hum + ", " + value.created_at + ");";
                console.log(insert_str);
                conn.query(insert_str);
                conn.end();
            }catch( err ){
                console.log(err);
                return reject(err);
            }

            resolve({result: 'OK'});
        });
    })
}

以下は、環境に合わせて変更してください。

【MySQLサーバのホスト名】
【MySQLサーバのポート番号】
【MySQLサーバのユーザ名】
【MySQLサーバのパスワード】
【データベース名】
【テーブル名】

あとは、シェルスクリプトを作ってCron化します。そこらへんは、以下の以前の記事と同じです。

GrafanaでQiitaのView数を眺める

#!/bin/sh

cd /home/XXXX/projects/node/cron_mijia
/home/XXXX/.nvm/versions/node/v8.12.0/bin/node index.js
> chmod uog+x index.sh

以下のコマンドでエディタを立ち上げ、

crontab -e

以下を書き込みました。要は、10分ごとに起動します。

0,10,20,30,40,50 * * * * /home/XXXX/projects/node/cron_mijia/index.sh

天気予報情報を取得する

毎度のように、データベースのテーブルを作成します。
テーブル名はとりあえず「weather」としました。

image.png

それではスクリプトで天気予報情報を取得します。
取得する都市は横浜にしています。

以下のnpmモジュールを使っています。

  • moment
  • node-fetch
  • mysql
  • dotenv
index.js
const fetch = require('node-fetch');
var mysql = require('mysql');
const moment = require('moment');
require('dotenv').config();

const DB_HOST = process.env.DB_HOST || MySQLサーバのホスト名】;
const DB_PORT = process.env.DB_PORT || MySQLサーバのポート番号】;
const DB_USER = process.env.DB_USER || MySQLサーバのユーザ名】;
const UB_PASSWORD = process.env.DB_PASSWORD || MySQLサーバのパスワード】;
const DB_NAME = process.env.DB_NAME || 【データベース名】;
const DB_TABLE = process.env.DB_TABLE || 【テーブル名】;

var conn = mysql.createConnection({
    host : DB_HOST,
    port : DB_PORT,
    user : DB_USER,
    password : UB_PASSWORD,
    database : DB_NAME
});

/* location: 13:東京、14:神奈川 */
function do_get_weather(location){
  return fetch('https://www.drk7.jp/weather/json/' + location + '.js', {
      method : 'GET'
  })
  .then((response) => {
      return response.text();
  })
  .then(text =>{
      text = text.trim();
      if( text.startsWith('drk7jpweather.callback(') )
          text = text.slice(23, -2);
      return JSON.parse(text);
  });
}

do_get_weather(14)
.then(async (json) =>{
  var now = moment();
  var info = json.pref.area['東部'].info;
  var weather = null;
  var date = null;
  for( var i = 0 ; i < info.length ; i++ ){
    date = moment(info[i].date, "YYYY/MM/DD");
    if( now.diff(date, 'days') == 0 ){
      weather = info[i];
      break;
    }
  }
  if( weather == null )
    return;

  var value = {
    fall_0: weather.rainfallchance.period[0].content,
    fall_6: weather.rainfallchance.period[1].content,
    fall_12: weather.rainfallchance.period[2].content,
    fall_18: weather.rainfallchance.period[3].content,
    tmp_high: weather.temperature.range[0].content,
    tmp_low: weather.temperature.range[1].content,
    moment: date,
    created_at: now.valueOf()
  };

  await insert_db(value);
})

async function insert_db(value){
  return new Promise((resolve, reject) =>{
      conn.connect((err) => {
          if(err){
              console.error('error connecting: ' + err.stack);
              return reject(err);
          }

          try {
            var date = value.moment;
            date.set('hour', 0);
            var insert_str = 'INSERT INTO ' + DB_TABLE + " (type, fall, created_at) VALUES ('fall', " + value.fall_0 + ", " + date.valueOf() + ");";
            console.log(insert_str);
            conn.query(insert_str);
            date.set('hour', 6);
            var insert_str = 'INSERT INTO ' + DB_TABLE + " (type, fall, created_at) VALUES ('fall', " + value.fall_6 + ", " + date.valueOf() + ");";
            console.log(insert_str);
            conn.query(insert_str);
            date.set('hour', 12);
            var insert_str = 'INSERT INTO ' + DB_TABLE + " (type, fall, created_at) VALUES ('fall', " + value.fall_12 + ", " + date.valueOf() + ");";
            console.log(insert_str);
            conn.query(insert_str);
            date.set('hour', 18);
            var insert_str = 'INSERT INTO ' + DB_TABLE + " (type, fall, created_at) VALUES ('fall', " + value.fall_18 + ", " + date.valueOf() + ");";
            console.log(insert_str);
            conn.query(insert_str);

            date.set('hour', 0);
            var insert_str = 'INSERT INTO ' + DB_TABLE + " (type, tmp_high, tmp_low, created_at) VALUES ('tmp', " + value.tmp_high + ", " + value.tmp_low + ", " + date.valueOf() + ");";
            console.log(insert_str);
            conn.query(insert_str);

            conn.end();
          }catch( err ){
              console.log(err);
              return reject(err);
          }

          resolve({result: 'OK'});
      });
  })
}

環境に合わせて以下を変更してください。

【MySQLサーバのホスト名】
【MySQLサーバのポート番号】
【MySQLサーバのユーザ名】
【MySQLサーバのパスワード】
【データベース名】
【テーブル名】

何をしているかというと、毎日深夜1時に取得することを前提に、その日の最高気温・最低気温・降水確率を取得します。
降水確率は、0~6時、6~12時、12~18時、18~24時までの4つ分がありますので、それぞれをその日の0時、6時、12時、18時のエポック時刻とともに別のrowとして記録します。そうすることで、グラフ化したときにそれが時系列に並んで表示されることになります。

毎度のように、シェルスクリプト化して、chmodして、Cronに登録します。

#!/bin/sh

cd /home/XXXX/projects/node/cron_weather
/home/XXXX/.nvm/versions/node/v8.12.0/bin/node index.js

毎日深夜1時に取得するようにしました。

0 1 * * * /home/XXXX/projects/node/cron_weather/index.sh

あとはデータが蓄積されるまで、気長に待ちましょう。

Grafanaに登録する

それではグラフを作っていきましょう。
まずはデータソースとしてMySQLを設定するのですが、以前の記事で作成したQiitaのデータベースと同じで、テーブルのみ別にしているだけであれば、それをそのまま使うことができます。

温湿度計

ダッシュボードを開くか、新規に作成します。
そして新規にPanelを作成します。
Queryにはさきほどのデータソースを選択します。FROMにはenvdataを選択します。

作成するグラフは、温度と湿度の2つです。
最初はAだけあると思いますが、それを温度にして、もう一つ追加してBとして湿度を割り当てます。Add Queryボタンを押下すれば追加できます。

image.png

温度と湿度では、単位が違うので、左側のY軸を℃、右側のY軸を%Hとしたいと思います。
表示されているグラフの左下にある2個のグラフの線のうち右側をクリックするとポップアップが表示されます。
Y-Axisタブを選択し、Use right Y-AxisをOnにします。そうするとそのグラフの線が右側に移ります。

image.png

Visualizationです。
Y軸に単位を入れたいのですが、すでにテンプレートがあるのでそれを選択します。Unitのところです。
また、Labelには日本語で入れておきます。

image.png

Generalでは、適当にTitleを決めて入力します。

image.png

最後に保存して完成です。

天気予報情報

天気予報情報もほぼ同じです。
Queryにはさきほどのデータソースを選択し、FROMにはweatherを選択します。
降水確率・最高気温・最低気温の3つのグラフなので、それぞれA、B、Cを割り当てました。

image.png

最高気温と最低気温を右側に移動しました。
降水確率のグラフをFillにして、斜め線ではなく階段状にしています。それぞれ、Line:fill、Staircase line:trueの部分です。
Unit、Labelを指定します。

image.png

タイトルを決めます。

image.png

保存して完成です!

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Alexa APL, 第2回 画面非対応Echoへの対応

はじめに

画面付きAlexaへの画面出力対応のために、Alexa APLを学び始めました。
その理解がとても難しく、わからないことが多いため、備忘のためにまとめておくことにしました。
今回は、画面非対応Echoへの対応方法をまとめます。
基本的に、ソースコードはAlexa APL関連でまとめているソースコードに続いていきますので、読んでいただける方は、つじつまが合わない箇所は、「参考」に記載する過去の記事も読んでいただいたほうが良いかと思います。

今回実施する内容

APLで記載した画面付きデバイスに対応したスキルを、画面非対応デバイスでエラーが発生しないように実装する。

環境

OS:Windows 10 JP
Alexaスキル言語:Node.js
Editor:Visual Studio Code
APLバージョン:1.0, 1.1

参考

Alexa ハローAPL、Alexaスキルの画面への対応
第1回のAlexa APLの記事です。タイトル通り、ハローAPLを表示させるだけのAPLです。

APLトレーニングシリーズ第1回: 初めての Alexa Presentation Language (APL)
AmazonのAlexa Blogsに画面非対応Echoへの対応の記載がありました。

用語

APL

Alexa Presentation Language
amazonの画面つきのAlexaの画面表示用の言語。
JSONを使用した記載方法です。
インターネットのホームページはHTMLとCSSで作成しますが、AlexaはAPLで作成するということです。

APLオーサリングツール

APL作成を視覚的に見ながらAPLのJSONファイルを作成するツール。
サンプルテンプレートも準備されており、その中から選択していくだけで、だいたいの画面は作成できる。

前提条件

前提条件はとくにないといえばないですが、本まとめを読むにあたり、以下がわかっていることが前提です。
・alexa developer consoleのアカウントがある
・Alexaスキルを開発したことがある
・JSONの記載方法を知っている
・「Alexa ハローAPL、Alexaスキルの画面への対応」の記事をみている

画面非対応Echoへの対応

「ハローAPL」を画面非対応Echoで実施してみる

前回の「Alexa ハローAPL、Alexaスキルの画面への対応」で作成した「ハローAPL」を表示するだけのスキルを、画面付きEcho(私の場合、Echo Show 5)で実行すると、画面に「ハローAPL」が表示されます。

このスキルを画面非対応Echo(私の場合、Echo Dot)で実行すると、「スキルがリクエストに正しく応答できませんでした」とAlexaに言われます。

また、iphoneのAmazon Alexaアプリで実行すると、以下のようなエラーが表示され、やはり、「スキルがリクエストに正しく応答できませんでした」とAlexaに言われます。

Invalid Directive
Request Identifier:null

The device does not support Alexa.Presentation.APL directives

なるほど、このままではスキル公開したとしても、審査に落ちてしまいますね。
画面非対応Echoの場合にエラーが発生しないようにしないといけないですね。

画面非対応の識別関数の追加

スキルのソースコードに画面非対応Echoなのか画面対応Echoなのか判断する処理が必要になってきます。
その内容は、参考に記載したAPLトレーニングシリーズ第1回: 初めての Alexa Presentation Language (APL)にそのまま載っていました。

判断するために関数を準備して、それによって、responseBuilderを変更するということですね。
その判断処理の関数は以下です。

// util.js へ以下を追加
module.exports.isAPLSupported = function isAPLSupported(request) {
 return request
   && request.context
   && request.context.System
   && request.context.System.device
   && request.context.System.device.supportedInterfaces
   && request.context.System.device.supportedInterfaces['Alexa.Presentation.APL'];
}

この関数を使って、例えば次のように、APLドキュメントの追加をコントロールします。

const Util = require('util.js');

...
   handle(handlerInput) {
       ...
       const builder = handlerInput.responseBuilder
           .speak(speechText)
           .reprompt(speechText);
       if (Util.isAPLSupported(handlerInput.requestEnvelope)) {
           ...
           builder.addDirective({
               type : 'Alexa.Presentation.APL.RenderDocument',
               ...
           });
       }
       return builder.getResponse();
   }
};

isAPLSupported関数は、ほかのスキルでも使用できるため、モジュール化してるわけですね。モジュール化しない場合は、index.jsにそのまま記載すればよいと思います。
私は、記載の通り、Util.jsを作成して試してみました。うまくいきそうです。
でも、index.jsに少しだけ手を加えました。

index.js
const Alexa = require('ask-sdk');
const Util = require('./org_modules/util.js');

const LaunchRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
  },
  handle(handlerInput) {
    const builder = handlerInput.responseBuilder
      .speak("画面表示へようこそ")
      .reprompt("画面表示へようこそ")
      .withStandardCard("画面", "画面表示へようこそ");

    if(Util.isAPLSupported(handlerInput.requestEnvelope)) {  
      builder.addDirective({
        type : 'Alexa.Presentation.APL.RenderDocument',
        version: '1.1',
        token: "token",
        document: require('./helloAPL.json'),
        datasources: {
          "hello": {
            "text": "ハローAPL"
          }
        }
      });
    } 
    return builder.getResponse();
  },
};

違いは大したところではないですが、モジュールの読み込みは、私の場合、「org_modules」というフォルダに置いているため、パスは違います。
builderに、「.withStandardCard("画面", "画面表示へようこそ");」を一文加えました。
画面非対応Echoへの対応であるため、不要な一文ではありますが、iphoneのAlexaアプリから実行すると、画面に何も表示されないため、気持ち悪いなと思い、つけておきました。

おわりに

今回は、画面非対応Echoへの対応方法を記載しました。
といっても、Amazon Blogsに書いている内容の転載であり、目新しいことはないです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む