0

node-lindaで電気をつける

nikezono君と山田くんが年末ぐらいに作ってた



  1. hubotが「電気つけて」コマンドを受けて{name:”light”, cmd:”on”, where:”delta”}をwrite
  2. たぶんRaspberry Piがこれと似たような感じで{name: “light”, cmd:”on”}をwatchしてサーボモーターを回してから、{name:”light”, cmd:”on”, where:”delta”, response:”success”}をwriteしているはず(ソースが公開されていない)
  3. hubotが{response:”success”}に反応し、「つけたと思う」をslackにmsg.send
  4. 照度センサーが付いたMacminiが{type:”sensor”, name:”light”, where:”delta”, value:数値}をwrite
  5. {type:”sensor”, name:”light”}でwatchしているherokuのスクリプトが変化を検知して{type: “hubot”, cmd: “post”, value: “〜〜で電気がつきました”}等をwrite
  6. hubot-lindaの組み込みで{type:”hubot”}に反応してslackに書き込む機能があるので、slackに「電気がつきました」等が書き込まれる

全然メンテとかしていないんだけど、2の部分みたいに勝手に誰でも接続できて拡張されちゃんと動き続けるようになってて便利

0

Node.jsでYahooから天気予報とリアルタイム降雨データを取得する

Node.jsでYahooから天気を取得するnpmを作った。

https://www.npmjs.com/package/weather-yahoo-jp

YOLP(Yahoo Open Local Platform)のリアルタイム降雨情報のAPIと、Yahoo天気の予報の取得ができる。

Nodeに日本の天気関係のnpmが無かったのと、今まで使っていたRubyのお天気系Rubygemが全て動かなくなっていたので自作した。
天気をスクレイピングして取ってくる部分がHTMLの変更により動かなくなってしまう事が多いみたいなので、CircleCIで毎日テストを走らせる事により異常にすぐ気づけるようにしてある。


現時点でv0.2.0
forecastの方はまだ多少項目追加する予定なので(気温の前日との差や降水確率など)最新情報はREADMEを見ると良い
エラーや要望はissuetwitterにどうぞ

インストール


% npm install weather-yahoo-jp


天気予報の取得

地名、もしくは天気ページのURLでgetすると天気が得られる。
import {forecast} from "weather-yahoo-jp";

forecast
.get("横浜")
.then((forecast) => {
console.log(forecast); // 取得した天気
})
.catch((err) => {
console.error(err.stack || err);
});


{
where: '神奈川県 東部(横浜)',
today: { text: '曇後雨', temperature: { high: 9, low: 4 } },
tomorrow: { text: '晴れ', temperature: { high: 8, low: 3 } },
url: 'http://weather.yahoo.co.jp/weather/jp/14/4610.html'
}

これを使ってhubot scriptも作ってみた


forecast.getはわりと適当に地名を入れても天気を返してくれる。事前に地名と天気のURLのリストを作ってforecast-url.jsonに保存してあって、この中から適当にそれっぽい地点の天気を返す。

このリストの作成にcoを使ったらdelayをいれながらゆっくりリンクをたどる処理を普通のfor文で書けたのでcoすごいと思った。
JavaSciptでディレイを入れながらゆっくり1つずつクロールするのってqueueを使うか、async.jsとかで変な書き方しないと駄目だと思ってたんだけどRubyみたいな普通な感じに書けるのでcoすごい。


@neoyokohama の天気予報にも使ってる



YOLP APIで現在の降雨状況を取得する

Yahoo Open Local PlatformのAPIに緯度経度を渡すと今そこにどれだけ雨が降ってるか取得できる。
数分後の予報も付いてくる。単位はmm/hらしい。

YOLP(地図):気象情報API – Yahoo!デベロッパーネットワーク


先にアプリケーションIDを取得する必要がある
https://e.developer.yahoo.co.jp/register

coordinatesに10個まで地名と緯度経度のペアを設定してgetWeatherすると、地名がkeyでvalueが降雨データのオブジェクトが取得できる。

試しにobservation(観測値)とforecast配列(予測値)を単純に比較して人間語で出力してみる
import {Yolp} from "weather-yahoo-jp";
var yolp = new Yolp("取得したAPPID");

var query = {
coordinates: {
東京: "139.7667157,35.6810851",
京都: "135.7605917,35.0075224",
沖縄: "128.0150716,26.5918277",
新潟: "139.0618657,37.9123509"
}
};

yolp.getWeather(query)
.then(function(data){
for(var where in data){
var w = data[where];
if(w.observation.rain > 0){
if(w.forecast[0].rain > 0){
console.log(where + "は雨が" + w.observation.rain + "降っています");
}
else{
console.log(where + "でもうすぐ雨が止みます");
}
}
else{
if(w.forecast[0].rain === 0){
console.log(where + "は雨が降っていません");
}
else{
console.log(where + "でもうすぐ雨が" + w.forecast[0].rain + "降ります");
}
}
}
})
.catch(function(err){
console.error(err.stack || err);
});

こうなる
東京でもうすぐ雨が止みます
京都でもうすぐ雨が0.25降ります
沖縄は雨が1.65降っています
新潟は雨が降っていません

YOLP APIのレスポンスはXML形式がプライマリみたいなので、JSON形式で取得するとマークアップがJSONなだけで構造がXMLっぽい超入り組んだ変なフォーマットで返ってくる。さらに複数地点の降雨データが配列で返ってきてどれがどこなのかわからない。
このままだと厳しいのでJavaScriptから使いやすいようにgetWeather関数内で直してある。
もし元データをそのまま取得したい場合はyolp.get(query)を呼べば加工前のフォーマットで取得できる。


これを使って定期的に降雨データを取得してnode-lindaにtupleで流し、それを色々な所に通知すると、hubot-lindaが反応してslackに通知が来たり、うちのMacminiが「雨が降ります」とか喋り出したり、Hueが青や赤に点滅したりする。

雨止みますという通知を俺が受信してタイミングよく買い物に行ったりできる。



0

hubot-rss-reader v0.8.1をリリースした

hubot-rss-readerをv0.8.1にアップデートした。

このアップデートでは、hubotが起動していない期間の記事を正しく配信できるようになった。Heroku無料枠で1日18時間制限がある場合などに便利。

結論から言うと、hubot-rss-reader v0.8.x以降はHubotに最初から入っているredis-brainをhubot-mongodb-brainなどのまともな実装に切り替えないと300日ぐらい後に死んでしまう可能性がある。



何が変わったか

RSSから取得した記事のうち、何が新着で何がチャットに配信済みなのか、という判定方法を変更した。


これまでの実装

新着記事を取得したらそのURLをオンメモリに持っておいて、RSSチェック毎に新しい記事かどうか判定するのに使っていた。つまり起動して最初の1回目のRSSチェックは配信済みURLリストとして保存されるだけになっていた。

なぜディスクに保存していなかったかというと、Hubotデフォルトのredis-brainの実装があまりにもひどかったから。詳しくは下に書いた
hubotのbrainが爆発した
またHubotのbrainが爆発したのでhubot-mongodb-brain作った

redis-brainがひどい

redis-brainは全データまとめてJSON.stringifyして1つkeyに巨大な文字列として保存している。しかも更新が無くても20秒毎にRedisに書き込んでいる。
サービスによって設定値が違うだろうけど、例えばRedisToGoでは1回に1.5MBを超えるとmaxmemory errorが起きて失敗する。もし毎日50byteのURLを100個保存すると1日で5Kbyteになり、300日で1.5MBを超えてしまい、死ぬ。

そもそもbrainはKVS風なAPIなのになぜRedisをKVSとして扱わないのかよくわからない。

俺は自作のhubot-mongodb-brainに切り替えているので問題ないが、ふつうのredis-brainではURLリストごときでもヤバイので、保存する事ができなかった。


v0.8.0での変更


Herokuが無料プランでは1日18時間しか動かせなくなったので、夜に6時間寝るように設定している
するとオンメモリに持っていたURLリストは消えるので、夜のうちに更新された記事は新着/既出判定できず、配信できなくなっていた。

これはかなり困る。
300日で破裂するbrainとどっちを取るかっていうとRSSがちゃんと配信されない事の方が嫌だったので、brainに保存するように変更した。


まとめ

brainを変更したほうがいい

0

Node.jsのfeedparserにドイツ語等を読ませる

ドイツ語などのヨーロッパの言語ではたまにåとかöみたいな文字が使われていて、UTF-8とかじゃなくISO-8859-1などの文字コードが使われている事があるらしい。

ドイツのspiegelという雑誌のRSSがちょうどISO-8859-1で、hubot-rss-readerで読むとところどころ文字化けするようになっていた。

こんな感じ


Node.jsでfeedを読む

feedの取得にはrequest、parseにfeedparserを使うのが多分普通なのだが、この2つはpipeでつないで使うようになっている。

で、最初は
Node.jsで文字コードの自動判別と自動変換 – Qiita
のようにjschardetで文字コードを判別して、iconvで変換してみた。でもrequest→feedparserがstreamなので少しずつchunkで送られてきて、日本語のマルチバイト文字の途中で区切りが来たりする事がある。そのchunkを1つずつjschardetにかけてしまうと全然違う文字コードが出てきたりする事があって良くない。

冷製になって考えたら、XMLなんだから頭に<?xml encoding=”ISO-8859-1″とか書いてあるのを読んでUTF-8じゃなかったら変換するstreamを作ればいい事に気づいた。

streamでXMLを食わせるとUTF-8に変換して吐き出すstream

というわけで、まずXMLのencoding attributeを読んで必要があればUTF-8に変換するstreamを返す関数を作る

charset-convert-stream.coffee
stream = require 'stream'
Iconv = require('iconv').Iconv

module.exports = ->

iconv = null

charsetConvertStream = stream.Transform()

charsetConvertStream._transform = (chunk, enc, next) ->
if m = chunk.toString().match /<\?xml[^>]* encoding=['"]([^'"]+)['"]/
charset = m[1]
if charset.toUpperCase() isnt 'UTF-8'
iconv = new Iconv charset, 'UTF-8//TRANSLIT//IGNORE'
if iconv?
@push iconv.convert(chunk)
else
@push chunk
next()

return charsetConvertStream


ドイツ語のfeedを読む

requestのon “response”からpipeでcharsetConvertStreamを通してfeedparserに繋ぐと、いい感じにUTF-8にエンコードされて出てくる。

FeedParser = require 'feedparser'
request = require 'request'
charsetConvertStream = require './charset-convert-stream'

feedparser = new FeedParser

req = request
uri: process.argv[2] or 'http://www.spiegel.de/schlagzeilen/tops/index.rss'
timeout: 10000
encoding: null # null指定でrequestが勝手にエンコードしなくなる
headers:
'User-Agent': 'test-feed-reader'

req.on 'error', (err) ->
console.error err

req.on 'response', (res) ->
if res.statusCode isnt 200
return console.error "statusCode: #{res.statusCode}"
this
.pipe charsetConvertStream()
.pipe feedparser

feedparser.on 'error', (err) ->
console.error err

feedparser.on 'data', (entry) -> # エントリーが1件ずつイベントで出てくる
console.log entry.title
console.log entry.summary or entry.description


文字化けなくなった

ここまでやって気づいたんだけど、 feedparser.meta[‘#xml’].encoding にエンコード情報が入っているのでこれを使ってtitleやdescriptionなど必要な所だけ取り出してからiconv.convertしても良かったかも・・

0

slackのreactionをhubotで通知する

slackでは個別の発言に絵文字でリアクションを送る事ができる。

これをhubotに組み込むと
https://gist.github.com/shokai/d23607d91ea7885a8df7

configに書いてあるchannelにリアクションを通知してくれるようになる

自分が見ていないchannelに関しても、リアクションがあった == だいたいのダイジェストが見れるようになるので便利。


元々はstarを付けた物を通知していたんだけど、
slackでふぁぼったのをhubotで通知する
star_addedイベントが来なくなってreaction_addedが来るようになったので書き直した。
star_addedの頃は毎回本文が通知されていたけど、たぶんそれはreadが発生するから重かったんじゃないかと思う。reaction_addedは投稿のIDしか来ない。