Amazon Echo + rasberry pi + Nature Remoで家電操作

前回電気を良い感じに点灯するscriptを書いたので、これをraspberry-piから起動する設定を実施。

以下作業メモ

1. Node-RED Alexa Home Skill Bridgeの登録

Node-RED Alexa Home Skill Bridgeにアクセスし、Registerのリンクをクリックする f:id:yFujita:20180821225712p:plain

下記画面の項目を入力し、「Register」ボタンをクリック https://alexa-node-red.bm.hardill.me.uk/f:id:yFujita:20180821225802p:plain

「Device」→「Add Device」ボタンをクリック
f:id:yFujita:20180821230024p:plain

バイスを登録。
とりあえずは電源のON/OFFを実行したいので以下の設定
f:id:yFujita:20180821231011p:plain

Node-RED Alexa Home Skill Bridgeの設定は以上。


2. AlexaスキルにNode-RED追加

iphoneのAlexaアプリより以下の手順を実行
* スキルで「Node-RED」検索
* 「有効にする」ボタンをクリック
* 1で作成したアカウント・パスワードを入力し「Authorize」ボタンを押す。
* 正常にリンクされました のメッセージを確認
* 「端末の検出」ボタンをクリック
* 1で作成した「Light」というデバイスが存在することを確認

以上でNode-Redスキルの設定は完了


3. raspberry-piにNode-Redをインストール

インストール方法はこのRunning on Raspberry Piを参考に実施。

何もしない状態でも確かにNode-Redは入っている模様 f:id:yFujita:20180821232618p:plain

念の為Upgradingを実行
一般ユーザで下記コマンドを実行

$ update-nodejs-and-nodered

正常終了することドキドキ待つこと5分あまり、問題なく終了した模様
f:id:yFujita:20180821233551p:plain

下記コマンドを実行し、Node-Red自動起動の設定を実施

$ sudo systemctl enable nodered.service

下記コマンドでNode-Redの起動

$ node-red-start

ブラウザからhttp://raspberry-piのIP:1880に接続すると以下画面が起動   f:id:yFujita:20180821234642p:plain

Node-RED Alexa Home Skill Bridgeと連携するためのプラグインをインストールする。
* 右上のメニューを開き、「パレットの管理」→「ノードの追加」を選択する。
* 「node-red-contrib-alexa-home-skill」で検索し、「ノードを追加」ボタンをクリックする
f:id:yFujita:20180821235226p:plain * 左下のパレットに「Alexa」が追加されたことを確認

以上で、raspberry-piへのNode-Redインストールは完了。


4. Node-Redでフローを作成する

パレット上で「Alexa Home」をドロップ
f:id:yFujita:20180821235909p:plain

「Alexa Home」をダブルクリックすると編集メニューが出てくるので、Accountの鉛筆ボタンをクリック

f:id:yFujita:20180822000022p:plain

ログイン画面が出てくるので、1で作ったアカウントでログイン f:id:yFujita:20180822000059p:plain

ログイン完了するとDevice欄に「Light」が表示される f:id:yFujita:20180822000249p:plain

パレットからアイテムをドラッグ&ドロップしてフローを作成する。
完成図は以下の通り
f:id:yFujita:20180826223159p:plain

各ノードの設定は以下
まずはswitchノード
f:id:yFujita:20180826223305p:plain

プロパティのmsg.commandにAlexaからON/OFFした場合の値を設定
詳細は公式Docを参照のこと

次はONノード
f:id:yFujita:20180826223834p:plain

ONノードの場合はmsg.payloadにonを設定している。
この値が次のノードであるexecノードで実行されるshell scriptの引数になる。
(OFFノードは設定値がoffにしてあるだけなので、画面割愛)

最後のexecノードでは実行するshell scriptを指定する
f:id:yFujita:20180826224047p:plain

ここのノードで実行されるscriptは以下

# pyenv実行
. ~/py36/bin/activate

case $1 in
  on)
    python ~/py_project/nature_remo/light/light_on.py;;
  off)
    python ~/py_project/nature_remo/light/light_off.py;;
esac

pyenvでpython実行環境を設定した後、引数に合わせたpython scriptを実行するのみ。
python scriptについては前回記事 で作成したものを呼んでいる。

これにてフロー作成は完了。
Node-Red画面右上の「デプロイ」ボタンをクリックして設定の反映を行う。

これにて全ての設定が完了。

Nature RemoのAPIを叩く

Nature Remo miniを買ったので、APIを叩いてみる

以下作業手順。

1. アクセストークンの取得

ここにアクセスし、nature remoアカウントでログインする。
ログインするとリクエスト画面になるので、許可するボタンをクリック f:id:yFujita:20180820220200p:plain


Access tokens」画面になるので、「Generate Access Tokens」ボタンをクリック
f:id:yFujita:20180820220738p:plain


するとアクセストークンが表示されるので、これを控える

f:id:yFujita:20180820221825p:plain

2. APIを叩く

2.1 デバイス一覧の取得

APIページを参考にAPIを叩いてみる。
まずは登録した機器の一覧を出力。
$tokenに1で取得したアクセストークンを設定した上で、以下コマンドを実行

$ curl https://api.nature.global/1/appliances -H "Authorization: Bearer $token" | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2546  100  2546    0     0   3512      0 --:--:-- --:--:-- --:--:--  3511
[
  {
    "id": "XXXXXXXX-2518-4bd8-XXXX-c571XXXXXXXX",
    "device": {
      "name": "RemoLiving",
      "id": "d9171fca-XXXX-4647-XXXX-XXXXXXXXXXXX",
      "created_at": "2018-08-19T01:41:11Z",
      "updated_at": "2018-08-19T08:55:37Z",
      "firmware_version": "Remo-mini/1.0.87-g8b06f0e",
      "temperature_offset": 0,
      "humidity_offset": 0
    },
    "model": null,
    "nickname": "電気",
    "image": "ico_light",
    "type": "IR",
    "settings": null,
    "aircon": null,
    "signals": [
      {
        "id": "c9d7c6b6-bb22-4b57-8ecc-04683096caee",
        "name": "オフ",
        "image": "ico_io"
      },
      {
        "id": "b67d7329-f5ac-45ca-a87e-468c5210e1a9",
        "name": "全光",
        "image": "ico_io"
      },
      {
        "id": "fa7459bc-5b0c-4d36-bb2c-ab6f59290d66",
        "name": "光色(青)",
        "image": "ico_color_blue"
      },
      {
        "id": "2482b859-5d6e-48f4-9900-93172606e578",
        "name": "光色(オレンジ)",
        "image": "ico_color_yellow"
      },
      {
        "id": "c46b8bfe-5654-41a0-aaac-76bacce73f1e",
        "name": "明るさ(UP)",
        "image": "ico_arrow_top"
      },
      {
        "id": "071c83e9-2070-49ca-8511-c7edec74ed92",
        "name": "明るさ(down)",
        "image": "ico_arrow_bottom"
      }
    ]
  },
  (以下略)
]

とズラズラっと登録したデバイス情報が取得できた。

2.2 家電の電源ON/OFF

先程取得したdevice情報の中でsignalsのidを使用し電源のON/OFFを試す。
以下コマンド(これでTVの電源ON/OFFができるはず)

$ curl -X POST https://api.nature.global/1/signals/{signalsのid}/send -H "Authorization: Bearer $token

テレビがつけることに成功。



3. scriptから電気の明るさを変える

我が家ではリモコンから電気をONにすると明るすぎるため、毎回リモコンで明るさを調整していた。 (壁のスイッチをON/OFFだと明るさ設定は保たれるので、なにかしらのやりようはある気がしているが、、) 毎回リモコンから実行するのが面倒なため、pythonからapiを複数回叩いて電気の明るさを良い感じに変えるscriptを書いた

import requests
import json
from time import sleep

# APIを呼んで情報取得
headers = {
    'accept': 'application/json',
    'Authorization': 'Bearer "アクセストークン"',
}

signal_id = "1a4a72e8-fb56-45b5-9fd8-19a81be8b6de"

# 前光でつける
signal_id = "b67d7329-f5ac-45ca-a87e-468c5210e1a9"
response = requests.post("https://api.nature.global/1/signals/" + signal_id + "/send",headers=headers)

# 光色を暖色にするを7回実行
signal_id = "2482b859-5d6e-48f4-9900-93172606e578"
for i in range(7):
    response = requests.post("https://api.nature.global/1/signals/" + signal_id + "/send",headers=headers)
    sleep(1)

# 明るさdownを3回実行
signal_id = "071c83e9-2070-49ca-8511-c7edec74ed92"
for i in range(3):
    response = requests.post("https://api.nature.global/1/signals/" + signal_id + "/send",headers=headers)
    sleep(1)

次はこのscriptをAmazon Echoから実行できるように設定する

flask tutorialのDBにMongoDBを使う

flaskを勉強中。
flaskのtutorialを写経するだけではなーと思い、
tutorialではDBにsqlite3 を使っているところ、MongoDBを使ってみた。

github.com

以下備忘兼ねて、ハマったところなどメモ

PyMongoでCollectionをJoinするとき

tutorialのblog.py 36行目にて以下SQLでJoinしている箇所

    post = get_db().execute(
        'SELECT p.id, title, body, created, author_id, username'
        ' FROM post p JOIN user u ON p.author_id = u.id'
        ' WHERE p.id = ?',
        (id,)
    ).fetchone()

MongoDBでCollectonのjoinのやり方が分からず結構苦労した。
MongoDBでJoin相当の処理を実行する場合はaggregateメソッドを使用して同等の処理ができるらしい。

参考は以下

qiita.com

$lookup (aggregation) — MongoDB Manual

pymongoのAggregation Examplesを見ても$lookupなどの記載はなかったが、 ここのCollectionに関しての記載を読む感じ、行けそうだったので試してみたところ問題なく実行できた。
コードは以下

posts = db.post.aggregate([
    {"$lookup":
        {
            "from":"user",
            "localField":"author_id",
            "foreignField":"_id",
            "as":"userInfos"
        }
    },
    {"$unwind":"$userInfos"},
    {"$sort": { "updated" : -1 }},
    {"$project":
        {
            "id": "$_id",
            "title":"$title",
            "body": "$body",
            "updated": "$updated",
            "author_id": "$author_id",
            "username":"$userInfos.username",
        }
    }
    ])

取得した後、listに格納し直した上でrender_templateで変数を渡しているのだけど、
もっとスマートなやり方があるような気がする。。

pymongoでfind_oneメソッドにて_idでfilterする場合は

ObjectIdでなきゃ駄目
それ以上でも以下でもないが、、
以下はNGで

g.user = get_db().user.find_one({'_id': user_id})

以下はOK

g.user = get_db().user.find_one({'_id': ObjectId(user_id)})

Scrapy 備忘メモ

Scrapy メモ

実行方法

  • プロジェクトでのscrapy実行時は
> scrapy crawl "name(Spiderの名前)"

各種メソッド

  • css selector
    Response.css()メソッドの戻り値はSelectroListオブジェクトを取得
    例は以下
# class topicsのリンクを抜き出す
response.css('ul.topics a::attr("href")')

SelectorListオブジェクトのメソッド

  • extract()
    ノードの一覧を文字列のlistとして取得する

  • extract_first()
    ノードの一覧の最初の要素を文字列として取得

  • re(regex)
    ノードの一覧のうち、引数に指定した正規表現regex)にマッチする部分のみを文字列のlistとして取得する

  • re_first(regex)
    ノードの一覧のうち、引数に指定した正規表現regex)にマッチする最初の部分を文字列として取得する

  • css(query)
    ノードの一覧の要素に対して、引数に指定したCSSセレクター(query)にマッチするノードの一覧をSelectorListとして取得する

  • xpath(query)
    ノードの一覧の要素に対して、引数に指定したXPath(query)にマッチするノードの一覧をSelectorListとして取得する

Scrapy Shellにて(?)

  • shelp()
    Scrapy Shellのヘルプ表示

  • fetch(request_or_url)
    引数でしていたRequestオブジェクトまたはURLのページを新しく取得し、requestやresponseなどの変数を置き換える

  • view(response) 引数でしていたResponseオブジェクトをブラウザーで表示する

細かい使い方

  • HTML要素内のテキストのみを取得した場合は、::text疑似セレクターでテキストノードを取得してからextract()を適用するとよし
# title 要素からextract()すると、タグを含む文字列が得られる。
 >>> response.css(' title'). extract() 
['<title >「 あかつき」 軌道 修正に成功 | 2016/ 4/ 8( 金) 20: 56 - Yahoo! ニュース </ title >']

>>> response.css(' title:: text') # テキストノードを取得 する。
 [<Selector xpath =' descendant-or-self:: title/ text()' data ='「 あかつき」 軌道 修正 に 成功 | 2016/ 4/ 8( 金) 20: 56 - Yaho - Yahoo!ニュース'>] 

# テキスト ノード から extract() する と、 タグ を 含ま ない 文字列 が 得 られる。
>>>response.css('title::text').extract()
['「あかつき」軌道修正に成功|2016/4/8(金)20:56-Yahoo!ニュース']

#extract_first()を使うとlistではなく文字列が得られる。
>>>response.css('title::text').extract_first()
'「あかつき」軌道修正に成功|2016/4/8(金)20:56-Yahoo!ニュース'
  • 抜き出したい対象(p要素内など)にbr要素やa要素などが含まれてるときに、すべてのテキストを抽出したい CSSセレクターの::textの代わりに、XPathstring()関数を使って要素の子孫のすべてのテキストを取得できる
>>>response.css('.hbody').xpath('string()').extract_first()
'\u3000宇宙航空研究開発機構(JAXA)は8日、金星を回る探査機「あかつき」の軌道修正に成功したと発表した。昨年12月に周回軌道に投入されたが、そのままでは約2年後に日陰に入る時間が長くなり、観測を継続できない恐れがあった。軌道修正で観測期間を5年以上に延長できた。(時事通信)'

errbotからgoogle-calendar-apiを叩いて予定の一覧を取得する

はじめに

以下を参考にさせて頂いた

Python Quickstart  |  Calendar API  |  Google Developers

1. OAuth認証 JSONファイルのダウンロード

  • Google APIsにアクセスして「認証情報」を選択し、「OAuth同意画面」タブにて[「メールアドレス」、「ユーザに表示されるサービス名」を入力し、「保存」ボタンをクリック
  • 「認証情報」タブを選択し、「認証情報を作成」ボタンをクリックし、「OAuth クライアントID」を選択する
  • 「クライアントIDの作成」画面にて、「その他」を選択し、名前項目に適当な名前(今回はerrbotとした)を設定し、「作成」ボタンをクリックする
  • 「OAuth クライアント」画面が出力されるので、「OK」ボタンをクリック
  • 「認証情報」画面より、今回作成したkeyのfile_download (Download JSON)ボタンをクリックし、ファイルを保存する。ファイル名はclient_secret.jsonとする

2. Google Client Libraryのインストール

下記コマンドを実行する

$ pip install --upgrade google-api-python-client

3. errbot pluginを作成

pluginのフォルダ構成は以下。 また、client_secret.jsonをrootフォルダに配置する。

.
└── client_secret.json
└── plugins
    ├── GoogleCalendar
         ├── get_gcal.plug
         └── get_gcal.py

pluginファイルの作成

[get_gcal.plug]

[Core]
Name = Get_Gcal
Module = get_gcal

[Python]
Version = 2+

[Documentation]
Description = Example "get my GoogleCalendar" plugin

[get_gcal.py]

from __future__ import print_function
import httplib2
import os

from apiclient import discovery
from oauth2client import client
from oauth2client import tools
from oauth2client.file import Storage

import datetime

from errbot import BotPlugin, botcmd

try:
    import argparse
    flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args()
except ImportError:
    flags = None

# If modifying these scopes, delete your previouly saved credenrials
# at ~/.credentials/calendar-python-quickstart.json
SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
CLIENT_SECRET_FILE = 'client_secret.json'
APPLICATION_NAME = 'errbot'


def get_credentials():
    """Gets valid user credentials from Storage

    If nothing has been stored, or if the stored credenrials are invalid
    the OAuth2 flow is completed to obtain the new credentials.

    Returns:
        Credenrials, the obtained credential.
    """
    home_dir = os.path.expanduser('~')
    credential_dir = os.path.join(home_dir, '.credenrials')
    if not os.path.exists(credential_dir):
        os.makedirs(credential_dir)
    credential_path = os.path.join(credential_dir, 'calendar-python-quickstart.json')

    store = Storage(credential_path)
    credentials = store.get()
    if not credentials or credentials.invalid:
        flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
        flow.user_agent = APPLICATION_NAME
        if flags:
            credentials = tools.run_flow(flow, store, flags)
        else: # Need only for compatibolity with Python 2.6
            credentials = tools.run(flow, store)
        print('Storing credentials to ' + credential_path)
    return credentials


class get_gcal(BotPlugin):
    @botcmd
    def getGcal(self, msg, args):
        """Shows basic usage of the Google Calendar API

        Creates a Google Calendar API service object and outputs a list of next
        10 events on the user's a calendar
        """
        credentials = get_credentials()
        http = credentials.authorize(httplib2.Http())
        service = discovery.build('calendar', 'v3', http=http)

        now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
        eventsResult = service.events().list(
            # 下記calendaridは参照したいgoogle calendar idを設定
            calendarId='XXXXX', timeMin=now, maxResults=10, singleEvents=True,
            orderBy='startTime').execute()
        events = eventsResult.get('items', [])

        if not events:
            return 'No upcoming events found.'
        for event in events:
            #start = event['start'].get('dateime', event['start'].get('date'))
            try:
                event['start']['date']
            except KeyError:
                start = "{} {}".format(event['start']['dateTime'][:10], event['start']['dateTime'][11:16])
            else:
                start = event['start']['date']

            yield "{}: {}".format(start, event['summary'])

実行すると以下の認証画面が起動するので、googleアカウントでログインする

4. errbotの実行

slackで!getGcalを実行すると、Rasberry pi上で以下の画面が起動する。

f:id:yFujita:20180329230040p:plain

google accountでログインを実行すると、slack上にカレンダー情報がpostされる

f:id:yFujita:20180331104616p:plain

Mac上のAtomからRasberry Pi上のファイルを編集する

Atomパッケージのインストール

以下をインストール ・ftp-remote-edit

Atomのパッケージ→FTP Remote→Edit Service

  • パスワード入力

  • 下記画面が出力されるのでそれぞれ入力。 f:id:yFujita:20180324155206p:plain

Atomのパッケージ→FTP Remote→Toggleすると右ペインにリモートでのツリーが表示される!

f:id:yFujita:20180324160031p:plain

試しにファイルを作成してみる f:id:yFujita:20180324220436p:plain

ファイル作成できた f:id:yFujita:20180324220830p:plain

Rasberry Pi 3のセットアップ

Rasberry-piを購入したので、セットアップ
以下のサイトを参考にさせてもらいました

karaage.hatenadiary.jp

1.ディスクの作成

1.1 Raspbianのダウンロード

下記から「RASPBIAN STRETCH WITH DESKTOP」のダウンロード。
自分が実施したタイミングではRelease-dateは2018-03-13だった

www.raspberrypi.org

1.2 ディスクのフォーマット

SDメモリカードフォーマッターを利用して ディスクのフォーマットを実行。
操作は起動後、以下の画面で「フォーマット」ボタンをクリックするだけ。

f:id:yFujita:20180321220839p:plain

1.3 イメージの書き込み

イメージの書き込み前にzipファイルを解凍する必要がある。
Raspbianページに記載のあるThe Unarchiverを使用して、2018-03-13-raspbian-stretch.zipファイルの解凍を実行

書き込み対象であるSDファイルのデバイスファイルを確認する

$ df -h
~~
/dev/disk2s1    15Gi  2.4Mi   15Gi     1%     0      0  100%   /Volumes/NO NAME

対象は確認できたので、アンマウントした後ddコマンドで書き込みを実行
書き込み完了したら取り出し

$ diskutil umountDisk /dev/disk2
$ sudo dd if=~/Downloads/2018-03-13-raspbian-stretch.img of=/dev/disk2 bs=1m conv=sync
$ diskutil eject /dev/disk2

書き込み完了した後Rasberry piを起動したところ、稲妻マークが出て画面が落ちる。   ぐぐってみるとRasberry pi 3は2.5A推奨ってことでA足りてない模様。
自転車で秋葉原にいってきて、電源タップと電源コードを買ってくる。。。

2. OS設定

2.1 WIFI設定

GUIで設定するのみ

2.2 パッケージアップデート

パッケージアップデート。
またデフォルトで日本語フォントが入っていないとのことだったので、
日本語フォントインストール

$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo apt-get install fonts-vlgothic

2.3 Raspi-configセットアップ

やったことは

  • [Systemsタブ]:host名、パスワード変更
  • [Interfacesタブ]: ssh, CameraをEnableに変更
  • [Localisation]タブ: Localを日本、 charcter setをUTF-8に変更
  • [Localisation]タブ: timezone, keyboardをJapanに変更 設定完了後、リブート

2.4 日本語設定

漢字変換ソフトインストール

$ sudo apt-get update
$ sudo apt-get install ibus-mozc

2.5 ipアドレス固定化設定

/etc/dhcpcd.confの以下の値を設定

  • interface eth0
  • static ip_address
  • static routers
  • static domain_name_servers

2.6 swapfileサイズ変更

/etc/dphys-swapfileを変更

# 変更前
CONF_SWAPSIZE=100

# 変更後
CONF_SWAPSIZE=1024

変更後にrebootし、以下コマンドで変更されていることを確認

$ free -m


3 各種ソフトインストール

3.1 vncserverインストール

$ sudo apt-get update
$ sudo apt-get install tightvncserver
$ vncserver

パスワード設定が終わったらMacから接続してみて問題ないことを確認。
vncサーバの自動起動設定をkaraageさんのスクリプトから設定。

$ git clone https://github.com/karaage0703/raspberry-pi-setup.git
$ cd raspberry-pi-setup/dotfiles
$ chmod 755 vncboot
$ sudo mv vncboot /etc/init.d/vncboot
$ sudo update-rc.d vncboot defaults

3.2 vsftpdインストール

ftp経由でファイル転送を実行したいので、vsftpdをインストール

$ sudo apt-get install vsftpd

書き込みを有効にするため/etc/vsftpd.conf の更新

write_enable=YES

サービス起動設定

$ sudo systemctl enable vsftpd

3.3 pyenv, virtualenv導入

GitHub - pyenv/pyenv: Simple Python version management

$ git clone https://github.com/pyenv/pyenv.git ~/.pyenv
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
$ eval "$(pyenv init -)"
$ pyenv install 3.6.1

OpenSSLのエラーが出たので、インストールしておく

$ sudo apt-get install libssh-dev
$ pyenv install 3.6.1
$ pyenv global 3.6.1

続いてvirtualenv導入

$ pip3 install virtualenv

問題なく終了した!