Google Photos Library APIへの移行

今まで、おうちで撮った写真はFlashAir W-04やPQI Air Cardによって、おうちのGentooサーバーを経由してPicasa Webにアップロードされていました。

gentoolinux.hatenablog.comgentoolinux.hatenablog.com

gentoolinux.hatenablog.com


しかし、2019年1月の3連休中に、突如アップロードされなくなってしまいました。
当初は2段階認証を導入したせいだと思っていたのですが、アップロード後に返ってくるjsonの応答を見ると、

The Picasa API is deprecated. See https://developers.google.com/picasa-web/ for more details and the migration guide.

ざっくり言うと、「Picasa APIはもう使えないよ! 詳しくはこのURLとマイグレーションガイドを見てね。」って言っています。
で、このキーワードでググると、Picasa APIからPhotos Library APIに移行するそうな。
あー面倒・・・。
Googleさんはすぐにいままでのことをヤメたり仕様を変えたりと、変わり身が早い企業ですよね。

で、FlashAirやPQI Air Cardから画像をサーバーに引っ張ってくるところなどは変更せず、今までPicasaにアップロードしていた"upload_images_in_dir.sh"のスクリプトを中心に変更していきます。

参考にしたのはコチラのサイト
qiita.com


ひとつひとつ丁寧に解説していただいております。大変助かります。

基本的に、今までのOAuth2の認証情報は変更せず、Photos Library APIを有効化し、スコープをPhotos Libraryにします。

Googleデベロッパーコンソールにアクセスします。
console.developers.google.com


ログインするとこんな感じだと思います。

f:id:naoyukinagano:20190202183134p:plain
Google developer console

左の「ライブラリ」をクリックします。

f:id:naoyukinagano:20190202183253p:plain
ライブラリリス

APIライブラリが膨大にあるため絞り込みます。
検索窓に"photos"とでも入れます。

f:id:naoyukinagano:20190202183351p:plain
photosキーワードで絞り込まれた状態

このPhotos Library APIをクリックします。

f:id:naoyukinagano:20190202183455p:plain
Photos Library APIの説明

"有効にする"をクリックします。

f:id:naoyukinagano:20190202183544p:plain
Photos Library APIを有効化

認証情報を作成することを勧めていますが、これはPicasa APIの時に作った認証情報が流用できるのでやりません。

(実は、最初わからなくて途中まで新しい認証情報を作りかけたのですが、最後の最後で「似たような既存の認証情報があります」って出てきました。そこで作るのをやめました。紛らわしいわ。)

さて、このPhotos Library APIのスコープを有効化して、新しいリフレッシュトークンを準備します。
ここからGentoo Linuxsshでアクセスして作業します。
まずは、Photos Libraryを読み書きする権限を有するスコープ、"https://www.googleapis.com/auth/photoslibrary"の認証情報を取得します。

認証情報をブラウザでも取得できるように、URLを生成するbashスクリプトを活用します。

#!/bin/sh

#デベロッパーコンソールの認証情報に記載されているクライアントID
CLIENT_ID="yourClientID.apps.googleusercontent.com"

# ブラウザでAuthorization_Codeを取得できるおまじない
REDIRECT_URI="urn:ietf:wg:oauth:2.0:oob"

# Photos Library APIのスコープ情報
SCOPE="https://www.googleapis.com/auth/photoslibrary"

# URLに整形して出力
echo "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=$SCOPE&access_type=offline"

これを実行します。

./get_authorization_code.sh

そうすると、次のような文字列が出てきます。

https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=yourClientID.apps.googleusercontent.com&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/photoslibrary&access_type=offline

これをブラウザにコピペすると、ログイン後に認証情報が出ます。

f:id:naoyukinagano:20190202185511p:plain
まずはログイン画面

f:id:naoyukinagano:20190202201343p:plain
アカウントへのリクエストとスコープ情報が出ます

許可します。

f:id:naoyukinagano:20190202185700p:plain
さらっと認証情報が表示されます


さて、この認証情報を使って、リフレッシュトークンを取得します。
これも過去のスクリプトを使い回しましょう。
"get_access_token_and_refresh_token.sh"というスクリプトを作成します。

#/bin/bash

#デベロッパーコンソールの認証情報に記載されているクライアントID
CLIENT_ID="yourClientID.apps.googleusercontent.com"

#デベロッパーコンソールの認証情報に記載されているクライアントシークレット
CLIENT_SECRET="yourClientSecret"

#先ほどのブラウザに表示されていた認証コード
AUTHORIZATION_CODE="NewAuthorizationCodeFromBrowser"

# HTTPサーバを起動しなくてもトークンを取得できるおまじない
REDIRECT_URI="urn:ietf:wg:oauth:2.0:oob"

# 実際にリフレッシュトークンを取得
curl --data "code=${AUTHORIZATION_CODE}" \
 --data "client_id=${CLIENT_ID}" \
 --data "client_secret=${CLIENT_SECRET}" \
 --data "redirect_uri=${REDIRECT_URI}" \
 --data "grant_type=authorization_code" \
 --data "access_type=offline" \
 https://www.googleapis.com/oauth2/v4/token

これを実行すると、json形式でアクセストークンとリフレッシュトークンが出力されます。

./get_access_token_and_refresh_token.sh

出力例

{
  "access_token" => "NewAccessToken",
  "token_type" => "Bearer",
  "expires_in" => 3600,
  "refresh_token" : "RefreshTokenIsPermanent"
}

Photos Library APIではこのリフレッシュトークンを使っていきます。
で、Photos Library APIを使ってアルバムにアップロードする場合、Photos Library APIを使って作成したアルバムじゃないと、アップロード出来ません。

アルバム作成のAPI解説
developers.google.com

まずは、アルバムを作成するスクリプトとして、create_album.shというスクリプトを作りました。

#/bin/bash

CLIENT_ID="yourClientID.apps.googleusercontent.com"
CLIENT_SECRET="yourClientSecret"
REFRESH_TOKEN="RefreshTokenAcquiredEarlier"

#アクセストークン取得
curl -s --data "refresh_token=${REFRESH_TOKEN}" --data "client_id=${CLIENT_ID}" --data "client_secret=${CLIENT_SECRET}" --data "grant_type=refresh_token" https://www.googleapis.com/oauth2/v4/token > token.JSON
ACCESS_TOKEN=`cat token.JSON | jq -r '.access_token'`
echo "Access token is " ${ACCESS_TOKEN}

# アルバム名を指定
ALBUMNAME="NewAlbum"
ENDPOINT="https://photoslibrary.googleapis.com/v1/albums"

#アルバムを作成してAlbumIdを取得
ALBUMID=$(curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" \
         -H "Content-type: application/json" \
         -d '{ "album": { "title":"${ALBUMNAME}" } }' ${ENDPOINT})

echo ${ALBUMID}
echo ${ALBUMID} >> albumid.txt

これを実行します。

./create_album.sh

すると、json形式でアルバム名とアルバムIDが表示されます。
念のため、albumid.txtというファイルにも追記していくことにしました。

出力例

{ "id": "NewAlbumIDisLongLongString",
 "title": "${ALBUMNAME}",
 "productUrl": "https://photos.google.com/lr/album/NewAlbumURLisAlsoLongLongString",
 "isWriteable": true }

あらら、なぜかアルバムタイトルが${ALBUMNAME}になっちゃった。
でも、これはGoogleフォトアプリやサイト上で直せます。
大事なのはidです。

ちなみに、ここは余談ですが、既存のアルバムIDとアルバム名を取得するスクリプトget_albumid.shも作ってしまいました。

#/bin/bash

CLIENT_ID="yourClientID.apps.googleusercontent.com"
CLIENT_SECRET="yourClientSecret"
REFRESH_TOKEN="RefreshTokenAcquiredEarlier"

#アクセストークン取得
curl -s --data "refresh_token=${REFRESH_TOKEN}" \
    --data "client_id=${CLIENT_ID}" \
    --data "client_secret=${CLIENT_SECRET}" \
    --data "grant_type=refresh_token" \
    https://www.googleapis.com/oauth2/v4/token > token.JSON

ACCESS_TOKEN=`cat token.JSON | jq -r '.access_token'`
echo "Access token is " ${ACCESS_TOKEN}

ENDPOINT="https://photoslibrary.googleapis.com/v1/albums?pageSize=50"


ALBUMLIST=$(curl -s -H "Authorization: Bearer $ACCESS_TOKEN" ${ENDPOINT})
echo ${ALBUMLIST} | jq -c '.albums[] | [.id, .title]'

jqを使ってアルバムIDとアルバム名を1行に出力するので、grepを使って目的のアルバムIDを取得できます。
が、先ほども書きましたが、Photos Library APIでアップロードできるアルバム先は、Photos Library APIで作成したアルバムだけです。
あんまり役にたたないですが、一応・・・。


アルバム等のリスト取得の解説
developers.google.com


さて、新たなPhotos Library APIは、アルバムに写真をアップロードするのに、2段階の手順を踏みます。

  1. 写真をアップロードしupload tokenを取得
  2. upload tokenを使ってアップロード先のアルバムを指定

アップロード先のURLはhttps://photoslibrary.googleapis.com/v1/uploadsになり、ここに必要なデータをポストします。
いままでのPicasa APIはURLに認証情報などを埋め込んでいましたが、今回からはヘッダーに入れるようになっています。

developers.google.com


curlを使ってfile.JPGという写真をアップロードするなら次のようなイメージになります。

curl -s -X POST \
       -H "Authorization: Bearer yourAccessToken" \
       -H "Content-type: application/octet-stream" \
       -H "X-Goog-Upload-File-Name: $(basename file.JPG)" \
       -H "X-Goog-Upload-Protocol: raw" \
       --upload-file "file.JPG" \
       https://photoslibrary.googleapis.com/v1/uploads

アップロードに成功すると、upload tokenが返ってきます。そのupload tokenとアルバムIDを指定して、アルバムに移動させます。
移動を指示するURLは"https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate"です。

curl -H "Content-type: application/json" \
       -H "Authorization: Bearer  yourAccessToken" \
       -d '{"albumId": "'NewAlbumIDisLongLongString'", "newMediaItems":[{"simpleMediaItem":{"uploadToken": "'UploadTokenIsAlsoLongLongStoring'"}}]}' \
       -X POST "https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate"

新しく生まれ変わったupload_images_in_dir.shは次の通りです。
これは、指定したディレクトリ上にある全ての.JPG/.jpg/.bmp/.pngGoogle Photosにアップロードします。

#/bin/bash

CLIENT_ID="yourClientID.apps.googleusercontent.com"
CLIENT_SECRET="yourClientSecret"
REFRESH_TOKEN="RefreshTokenAcquiredEarlier"
ALBUM_ID="NewAlbumIDisLongLongString"

#アクセストークン取得
curl -s --data "refresh_token=${REFRESH_TOKEN}" \
 --data "client_id=${CLIENT_ID}" --data "client_secret=${CLIENT_SECRET}" \
 --data "grant_type=refresh_token" https://www.googleapis.com/oauth2/v4/token > token.JSON

ACCESS_TOKEN=`cat token.JSON | jq -r '.access_token'`
#echo "Access token is " ${ACCESS_TOKEN}

ENDPOINT="https://photoslibrary.googleapis.com/v1/uploads"
MEDIAITEMS="https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate"

DIR="./" # 任意のディレクトリを指定

IFS_BAK=${IFS}
IFS="
"
FILES=`ls -1 ${DIR}`
for FILE in ${FILES}
do
  if [ "${FILE}" != "" ]; then
    TYPE="UNKNOWN"
    case "${FILE}" in
      *\.bmp) TYPE="image/bmp";;
      *\.JPG | *\.jpg) TYPE="image/jpeg";;
      *\.png) TYPE="image/png";;
      *) TYPE="UNKNOWN";;
    esac
#    echo "$TYPE $FILE $LENGTH"
    if [ "${TYPE}" = "UNKNOWN" ]; then
      continue;
    fi
     echo "Uploading to Google Photos " ${FILE}
     UPLOAD_TOKEN=$(curl -s -X POST \
       -H "Authorization: Bearer ${ACCESS_TOKEN}" \
       -H "Content-type: application/octet-stream" \
       -H "X-Goog-Upload-File-Name: $(basename $FILE)" \
       -H "X-Goog-Upload-Protocol: raw" \
       --upload-file "${FILE}" \
       ${ENDPOINT})

#     echo "UPLOAD_TOKEN: ${UPLOAD_TOKEN} "
     echo "Move Alubums " ${FILE}
     curl -H "Content-type: application/json" \
          -H "Authorization: Bearer ${ACCESS_TOKEN}" \
          -d '{"albumId": "'${ALBUM_ID}'", "newMediaItems":[{"simpleMediaItem":{"uploadToken": "'${UPLOAD_TOKEN}'"}}]}' \
          -s -o /dev/null \
          -X POST "${MEDIAITEMS}"

    sleep 1
  fi
done
IFS=$IFS_BAK

あ、もちろん、"yourClientID.apps.googleusercontent.com"、"yourClientSecret"、"RefreshTokenAcquiredEarlier"、"NewAlbumIDisLongLongString"などは、あなたがお持ちの認証情報に書き換えないと動きませんよ。

FlashAir(W-04)の画像をGoogle Photos(Picasa Web)のアルバムにアップロードするスクリプトを改善 その2

以前、FlashAir(W-04)の画像をサーバー側のスクリプトを使って一時的にダウンロード、リサイズ、Google Photosにアップロードというのを自動化する方法を実施しました。

gentoolinux.hatenablog.com

だいぶ快適になりましたが、FlashAirの無線LANは電波の送受信が弱く、WebDAVとしても非力なため、転送が中断されることもしばしばありました。
転送が中断されると、写真の半分がグレーになった写真がアップされてしまいます。
また、中断中はサーバースクリプトが長くダンマリするため、カメラの電源OFF/ONしてFlashAirを復旧しても、Google Photosへのアップロードが始まらないということがあったわけです。

この辺を直してみました。
まず、FlashAirからの転送が中断するとダンマリになる原因は、wgetのデフォルトの振る舞いにありました。
wgetを使ってFlashAirからhttpで写真を転送します。

#!/bin/bash

#初期変数一覧

#Flashairのアドレス
FLASHAIR_IP="http://192.168.x.161"

#DCIMフォルダの名称
DCIM_FOLDER="CANON"

#DCIMフォルダのURLを生成
WGET_PRE_URL=${FLASHAIR_IP}"/DCIM/"

#(中略)
  echo "Flashair - Get file "${JPG_LIST[JPG_LOOP]}
     /usr/bin/wget -q -N --no-host-directories --no-directories ${FLASHAIR_IP}${JPG_LIST[JPG_LOOP]}

このとき、wgetタイムアウトは900秒、リトライ回数は20回です。
つまり、15分以上ダンマリを決め込むわけです。いくら気長な私でも待てません。
タイムアウトを30秒、リトライを5回にすると、約1分くらいで処理をあきらめます。

#!/bin/bash
echo "Flashair - Get file "${JPG_LIST[JPG_LOOP]}
/usr/bin/wget -q --tries=5 --timeout=30 --no-host-directories --no-directories ${FLASHAIR_IP}${JPG_LIST[JPG_LOOP]}

ついでに、既存ファイルの上書き禁止の-Nオプションを廃止しました。

さて、続いてこの中断したファイルを消すことと、正しく転送されたファイルリスト「flashair_done.txt」に追記する処理をスキップしなければなりません。

まず、wgetの終了ステイタスコードを理解します。

  • 0 No problems occurred.(正常終了)
  • 1 Generic error code.(一般的なエラー)
  • 2 Parse error—for instance, when parsing command-line options, the ‘.wgetrc’ or ‘.netrc’...(パースエラー、.wgetrcや.netrcの記述が間違っている)
  • 3 File I/O error.(ファイル入出力エラー)
  • 4 Network failure.(ネットワークの異常)
  • 5 SSL verification failure.(SSL認証エラー)
  • 6 Username/password authentication failure.(ユーザー/パスワード認証エラー)
  • 7 Protocol errors.(プロトコルエラー)
  • 8 Server issued an error response. (サーバーがエラーを応答)

FlashAirの異常終了はほぼほぼ4ですが、まあ正常終了の0以外はファイル削除することにします。
シェルスクリプトの中で直前のコマンドの終了ステイタスコードは「$?」に代入されます。しかも、次に何らかのコマンドが実行されると消えてしまいますので、何らかの変数に代入しておきます。
0なら正常終了なので「flashair_done.txt」に追記します。

#!/bin/bash
#wgetによるファイルの取得
   echo "Flashair - Get file "${JPG_LIST[JPG_LOOP]}
    /usr/bin/wget -q --tries=5 --timeout=30 --no-host-directories --no-directories ${FLASHAIR_IP}${JPG_LIST[JPG_LOOP]}

   #wgetのステイタスを確認
   WGET_STATUS=$?
   echo "Flashair - wget exit status is "$WGET_STATUS
      if [ $WGET_STATUS -eq 0 ]; then

     #wgetが正常終了ならflashair_done.txtにファイル名を追加
     echo "Flashair - Output to flashair_done.txt "${JPG_LIST[JPG_LOOP]}
     echo ${JPG_LIST[JPG_LOOP]} >> flashair_done.txt

つづいて、else以下にwgetの終了ステイタスコードが0以外、つまりwget異常終了時の振る舞いとして、ダウンロード途中で終わった現在のファイルを削除します。
が、${JPG_LIST[JPG_LOOP]}には、/DCIM/100CANON/1NA0001.JPGのように、ディレクトリ名まで代入されています。
しかし、中途半端にダウンロードしているファイルは、スクリプトが実行されているフォルダの直下に1NA0001.JPGとして保存されています。"/DCIM/100CANON/"は文字列置換で削除する必要があります。
sedを使って削除です。
ちょうど良いことに、${DCIM_LIST[DCIM_LOOP]}にこの"/DCIM/100CANON"(配列になっており、100だけじゃなく101だったり102もあったりします)が代入されていますので、これと/の1文字を削除です。

#!/bin/bash
DEL_JPEG=(`echo ${JPG_LIST[JPG_LOOP]} | sed -e "s@${DCIM_LIST[DCIM_LOOP]}@@" -e "s@/@@"`)
rm $DEL_JPEG
DEL_JPEG=

ずーっとsed -e "s/${DCIM_LIST[DCIM_LOOP]}//"と記載していて、sedシンタックスエラーで悩んでいたのですが、変数に"/"が入っているため、sedがそれを変数としてではなく、セパレーターとして認識してしまっていたため、セパレーターを@にすることで解決しました。

では、リトライを調整して完成した改善後のスクリプト全文はコチラです↓
(192.168.x.161のIPアドレスは、FlashAirのIPアドレスに置き換えてください。)

#!/bin/bash

#初期変数一覧

#Flashairのアドレス
FLASHAIR_IP="http://192.168.x.161"

#DCIMフォルダの名称
DCIM_FOLDER="EOS7D"


#現在のディレクトリに移動
cd `dirname $0`

#プロセスIDを保存
echo $$ > pid_flashair.txt

#DCIMフォルダのURLを生成
WGET_PRE_URL=${FLASHAIR_IP}"/DCIM/"

echo "Starting Get Photos from Flashair"

while :
do

#Flashairを死活監視
ALIVE=$(/usr/bin/wget -nv --spider --timeout 60 -t 1 ${FLASHAIR_IP} 2>&1 | grep -c '200 OK')

  #もし死活監視で生きていたら
  if [ $ALIVE -eq 1 ]; then

    #配列を初期化
    echo "Flashair - Format Arrys."
    DCIM_LIST=()
    JPG_LIST=()
    FLASHAIR_DONE=()
    WGET_STATUS=

    #DCIM配下のディレクトリを配列に代入
    echo "Flashair - get under DCIM directory list."
    DCIM_LIST=(`/usr/bin/wget ${WGET_PRE_URL} -q -O - | grep 'wlansd.push({"r_uri"' | sed -e 's/wlansd.push({"r_uri":"//' -e 's/", "fname":"/\//' -e 's/", "fsize".*//' | grep ${DCIM_FOLDER}`)

    #フォルダの数だけループ処理を実施
    echo "Flashair - Starting loop under DCIM directory."
    DCIM_LOOP=${#DCIM_LIST[@]}
    DCIM_LOOP=$((DCIM_LOOP - 1))
    while [ $DCIM_LOOP -ge 0 ];
    do

      #フォルダの中のファイルを配列に代入
      echo "Flashair - Get file list under "${DCIM_LIST[DCIM_LOOP]}
      JPG_LIST=(`/usr/bin/wget ${FLASHAIR_IP}${DCIM_LIST[${DCIM_LOOP}]} -q -O - | grep 'wlansd.push({"r_uri"' | sed -e 's/wlansd.push({"r_uri":"//' -e 's/", "fname":"/\//' -e 's/", "fsize".*//'`)

      #ファイルの数だけループ
      echo "Flashair - Starting loop under "${DCIM_LIST[DCIM_LOOP]}
      JPG_LOOP=${#JPG_LIST[@]}
      JPG_LOOP=$((JPG_LOOP - 1))
      while [ $JPG_LOOP -ge 0 ];
      do

      #アップ済みのファイルかどうかをチェック
      echo "Flashair - Check FLASHAIR_DONE "${JPG_LIST[JPG_LOOP]}
      if ! `cat flashair_done.txt | grep -q "${JPG_LIST[JPG_LOOP]}"` ; then

      #アップ済みになかった場合の処理
      #wgetによるファイルの取得
      echo "Flashair - Get file "${JPG_LIST[JPG_LOOP]}
      /usr/bin/wget -q --tries=5 --timeout=30 --no-host-directories --no-directories ${FLASHAIR_IP}${JPG_LIST[JPG_LOOP]}

      #wgetのステイタスを確認
      WGET_STATUS=$?
      echo "Flashair - wget exit status is "$WGET_STATUS
        if [ $WGET_STATUS -eq 0 ]; then

        #wgetが正常終了ならflashair_done.txtにファイル名を追加
        echo "Flashair - Output to flashair_done.txt "${JPG_LIST[JPG_LOOP]}
        echo ${JPG_LIST[JPG_LOOP]} >> flashair_done.txt

        #wgetが異常終了ならダウンロード途中のファイルを削除
        else
        DEL_JPEG=(`echo ${JPG_LIST[JPG_LOOP]} | sed -e "s@${DCIM_LIST[DCIM_LOOP]}@@" -e "s@/@@"`)
        echo "Flashair - Download Uncomplete. Delete File "$DEL_JPEG
        rm $DEL_JPEG
        DEL_JPEG=
        fi

      fi

      JPG_LOOP=$((JPG_LOOP - 1))
      done


    DCIM_LOOP=$((DCIM_LOOP - 1))

    #ダウンロードした全てのJPGを2048pxにリサイズ
    echo "Flashair - Resize Images"
    /usr/bin/mogrify -resize 2048x2048 -quality 100 *.JPG

    #ディレクトリ上にあるJPGファイルをpicasaweb APIでアップロード
    echo "Flashair - Uploading Images"
    ./upload_images_in_dir.sh

    #ディレクトリ上にあるJPGファイルを日付ディレクトリで整理
    echo "Flashair - Move Images to YMD directory "
    /usr/bin/exiftool -q '-Directory < CreateDate' -d %Y%m%d *.JPG

    done

  fi

  sleep 10
done

これで、スクリプトの再起動はしばらくやらなくてよさそう。

marantz NA7004 ネットワークオーディオプレイヤーの修理

愛用していたネットワークオーディオプレイヤー、マランツNA7004が、突如お亡くなりになりました。

f:id:naoyukinagano:20180901171509j:plain

電源部が壊れたようで、電源スイッチ上部のランプが点灯していません。

このところGoogle Home miniばかりを使っていたので、2~3週間ほど使っていなかったため、機嫌を損ねたのでしょうか? んなわけない。

メーカーに修理に出すと、1~2万円程度かかるそうです。

 

というわけで、早速フタを開けて中身を見てみましょう。

片サイド3本のネジと裏面2本、合計8本のネジを外すと天板ケースを外せます。

f:id:naoyukinagano:20180901171635j:plain

 

中を見ると、ぎっしり部品がありますね。

でも、最近(2013年製)のオーディオなので、配線はすっきりしています。

f:id:naoyukinagano:20180901171727j:plain

中央から左後方に大きな面積を占める基板が、電源とデジタル処理のようです。

基板の左1/3くらいが電源の整流回路基板のようです。

その上に小さく載っている基板はネットワーク基板です。

右側の集積度が低いのが、アナログ(D/A変換)のようですね。

電源が入らないので、主に左側を重点的に見ていきます。

 

すると、ネットワーク基板の下に、怪しいコンデンサを発見。

f:id:naoyukinagano:20180901172113j:plain

この、緑色のコンデンサ、上部がふくれあがっています。

どうも、このコンデンサ、Koshin(東佳電子)という中国の会社のものでして、パイオニアサウンドバーでも電源不良で問題になっているそうです。

 

とりあえず、交換してみましょう。

基板を外していきます。

f:id:naoyukinagano:20180901172350j:plain

外せない(外しにくい)コネクタ2本以外は全て外し、先にNW基板を外します。

写真取り忘れましたが、NW基板は裏面端子側の2つのネジだけで止まっていて、後はプラスチックの支柱を外すだけです。

基板はコネクタを裏面側板に取り付けるネジや基板固定ネジを外します。

これも写真取り忘れましたが、基板固定ネジは長さがバラバラ、材質もバラバラなので気をつけます。

 

f:id:naoyukinagano:20180901172727j:plain

容疑が掛けられたコンデンサが見えてきました。

 

基板を裏返します。

f:id:naoyukinagano:20180901172857j:plain

前面パネルのコントロール部とのケーブルが抜けないので、気をつけながら前面パネル側に倒します。

 

この部分の半田を溶かしてコンデンサを抜きますが、コレが至難の業です。

f:id:naoyukinagano:20180901173054j:plain

 

コンデンサを外す作業は両手を使うので写真が撮れません。

なので、こんな「やってる感」を出すヤラセ画像で。

f:id:naoyukinagano:20180901173255j:plain

 

外したコンデンサです。

f:id:naoyukinagano:20180901173323j:plain

10V 1000μFのようですね。

 

f:id:naoyukinagano:20180901173352j:plain

逆サイドを見ると、105℃とあり、高温度品です。そんなに温度高くねぇだろ!って、ツッコみたくなります。確か、105℃の電解コンデンサは105℃で4000時間前後の耐久性、10℃下がる毎に倍になるはずなので、まぁ、常時通電だと寿命ということでしょうか?

 

で、これに相当するコンデンサを入手します。

できれば日本製と行きたいところです。

奮発して、ニチコンのオーディオグレードを入手。

16V 1000μF 105℃です。

f:id:naoyukinagano:20180901173808j:plain

ちょっと背が高いですが、大丈夫でしょう。

 

取り付けてみました。

f:id:naoyukinagano:20180901173853j:plain

相変わらずコゲが多い下手な半田です。

クラックしないことを祈ります。

 

こんな感じで、これよりも背が高い部品がまだまだあり、ネットワーク基板とは干渉しないようです。

f:id:naoyukinagano:20180901174104j:plain

 

では、基板を元の位置に固定してネジを締め、コネクタを元に戻します。

f:id:naoyukinagano:20180901174204j:plain

あ、ネットワーク基板へのケーブルの経路が逆だった。

直します。

 

で、天板を戻す前に、おそるおそるコンセントを差します。

f:id:naoyukinagano:20180901174437j:plain

お! スタンバイランプが光った!

 

電源ボタンを押してみると。

f:id:naoyukinagano:20180901174535j:plain

復活!

電解コンデンサという小さな部品ひとつで、電源が入らなくなるものなんですね。

200円くらいで直せました。

HomeassistantをGoogle Assistant (Google Home)からコントロールする

いままでHomebridgeをハブとしてiPadのSiriで電灯スイッチのボイスコントロールが出来ていました。が、Siriは精度が悪すぎました。

「Hey,Siri.寝室の電気を消して」
「すみません。寝室の天気がわかりません。」

「Hey、Siri!」
「・・・」
「Hey,Siri!」
「聞こえてますよ。」

上はほんの一例。
「行っている意味がわかりません。」はしょっちゅうです。

やっぱり音声認識Googleでやりたい。
HomeassistantをGoogle assistantでコントロール出来るようにしました。

まずは、Homeassistantのコンフィグを設定します。
私の環境では、
/home/homeassistant/.homeassistant/configuration.yaml
にあります。
このファイルの最終行に"cloud"アドオンの設定を書きます。

cloud:

これだけ。
で、Homeassistantを再起動します。

うちの環境ではGentooのお作法に従い、/etc/local.d/に起動スクリプトと停止スクリプトを書いています。

/etc/local.d/homeassistant.start

#!/bin/sh
sudo -u homeassistant -H /home/homeassistant/bin/hass --pid-file /home/homeassistant/hass.pid  --daemon > /var/log/home-assistant.log 2>&1

/etc/local.d/homeassistant.stop

#!/bin/sh
kill $(cat "/home/homeassistant/hass.pid") 2>&1

このスクリプトで停止と起動をしています。

/etc/local.d/homeassistant.stop
/etc/local.d/homeassistant.start

echoコマンドを設定していないので、物静かに停止と起動をしています。

で、https://192.168.0.x:8123(IPアドレスは各自の環境に読み替えてください)にアクセスし、「設定」をクリックすると、"Home Assistant Cloud"という項目が増えています。これをクリックします。
f:id:naoyukinagano:20180621212647p:plain

サインインするページが現れますが、まずは先に最下部の「Create Account」でHomeassistant Cloudのアカウントを作ります。
f:id:naoyukinagano:20180621213934p:plain

メールアドレスを入力し、パスワードを設定し、「CREATE ACCOUNT」をクリックします。
f:id:naoyukinagano:20180621214201p:plain

すると、メールが飛んでくるので、メール本文の「Confirm Email」をクリックします。
f:id:naoyukinagano:20180621214301p:plain

この画面で完了です。
f:id:naoyukinagano:20180621214508p:plain

Homeassistantの設定画面に戻り、メールアドレスとパスワードを入力して「SIGN IN」します。
f:id:naoyukinagano:20180621220546p:plain

下記のような画面でHomeassistant側は完了です。
f:id:naoyukinagano:20180621220627p:plain
このアカウントは2018年7月22日が有効期限のようです。というのも、Homeassistant Cloudは現在は無料ですが、今後はコミュニティーサポートとして有料化するようです。

次にAndroidスマホからの設定です。
「アシスタント」アプリから設定します。右上の青い○に皿の絵のようななんともいえないアイコンをタップします。
f:id:naoyukinagano:20180621223337p:plain

使い方・ヒントが表示されますが、右上のメニューアイコン「︙」をタップします。
f:id:naoyukinagano:20180621223429p:plain

メニューの中から「設定」をタップします。
f:id:naoyukinagano:20180621223718p:plain

スクロールして機能カテゴリーにある「スマートホーム」をタップします。
f:id:naoyukinagano:20180621223751p:plain

スマートホームのデバイスを追加するため、右下の「+」をタップします。
(すみません。画像がデバイス追加後の画像になっちゃってます。)
f:id:naoyukinagano:20180621223842p:plain

スクロールして「hass.io」を探してタップします。
f:id:naoyukinagano:20180621224030p:plain

先ほどHomeassistant Cloudで設定したアカウントとパスワードを入力し、Sign inをタップします。
f:id:naoyukinagano:20180621224115p:plain

サインインすると、「GoogleがHome Assistantにリンクしてあなたのデバイスをコントロールできるようにして良いですか?」と訊かれるので、「Allow Access」をタップして許可します。
f:id:naoyukinagano:20180621224209p:plain

これで、HomeassistantのデバイスGoogle Assistant(Google Home)でコントロール出来ます。
「タップして部屋を割り当て」とでてますが、このページからやるよりも、部屋の設定画面から設定した方が楽です。
f:id:naoyukinagano:20180621224412p:plain

より正確にコントロールできるように、ニックネームを設定すると良いです。
というのも、Homeassistant上で"Living Snowball"と設定している照明があるのですが、「スノーボールを消して」と言ってもWeb検索の結果が出るため、googleアシスタント上では「スノーボール」とカタカナのニックネームをつけています。

Googleのスマートウォッチ Wear OSからもコントロール出来るようになりました!

Homeassistant on Gentooのコンフィグ

だいぶ時間が経ちましたが、Homebridgeのコンフィグを紹介します。
私のHomebridgeはGentoo Linux上で動いているためhass.ioではありません。
(hass.ioのほうが、アドオンのインストールが簡単なのですが、Linux用がなさそうで・・・)

インストール時にhomebridgeというユーザーを作成し、chroot環境でpythonが動くようにしましたが、configはrootで読み書き可能ですので、suやchrootは不要です。

コンフィグのありかは、
/home/ユーザ名/.homeassistant/にある、"configuration.yaml"です。
うちの場合は、/home/homeassistant/.homeassistant/configuration.yamlです。
初めて見ました、yaml

では、うちの場合のconfiguration.yamlを紹介します。

homeassistant:
  # Name of the location where Home Assistant is running
  name: Sample_home
  # Location required to calculate the time the sun rises and sets
  latitude: 42.0000
  longitude: 141.0000
  # Impacts weather/sunrise data (altitude above sea level in meters)
  elevation: 20
  # metric for Metric, imperial for Imperial
  unit_system: metric
  # Pick yours from here: http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
  time_zone: Asia/Tokyo
  # Customization file
  customize: !include customize.yaml

まず、"homeassistant:"のセクションです。(各セクションは"section:"で始まります。)
最初にname:でhomeassistantに名称をつけましょう。
latitude:は緯度、longitude:経度、elevation:は高さですので、自宅の位置を調べて世界測地系で入力します。
unit_system:は単位です。メートルなのでmetricを。
time_zoneはその通りタイムゾーンですので、日本ならAsia/Tokyoを。
customize:で、このファイル以外に設定ファイルを読み込みます。ここではデフォルトのまま!include customize.yamlにしていますが、そもそもcustomize.yamlは空のフィアルです。

# Show links to resources in log and frontend
introduction:

# Enables the frontend
frontend:

# Enables configuration UI
config:

この3セクションはなにもいじっていません。

http:
  # Secrets are defined in the file secrets.yaml
  # api_password: !secret http_password
  # Uncomment this if you are using SSL/TLS, running in Docker container, etc.
  # base_url: example.duckdns.org:8123
  api_password: angou
  ssl_certificate: /etc/ssl/csr/rapidssl.crt
  ssl_key: /home/homeassistant/.homeassistant/private.key

http:では、homeassistantのwebページについて定義します。
とはいえ、base_urlを定義していません。
api_password:に、webアクセスの際のパスワードを設定します。生値なのがちょっと気になります。
ssl_certificate:にSSLの中間証明書を、ssl_key:にSSLのサーバー用秘密鍵を指定します。
が、通常、秘密鍵はrootしか読めないようにしなければなりません。しかし、このプロセスはrootではなくhomeassistantユーザーで起動します。なので、秘密鍵をhomeassistantで読めるようにします。
ちょっとセキュリティが下がるような気がします。
/etcにある秘密鍵をユーザーhomeassistantが読めるようにchmodするか、コピーしてchownするかのどちらかです。

これで、https://192.168.0.x:8123で、ブラウザ上にhomeassistantが表示されるようになります。
ルーターでIPポートフォワードを設定していれば、https://yourhostname:8123でインターネット上からもアクセスが可能になります。

# Checks for available updates
# Note: This component will send some information about your system to
# the developers to assist with development of Home Assistant.
# For more information, please see:
# https://home-assistant.io/blog/2016/10/25/explaining-the-updater/
updater:
  # Optional, allows Home Assistant developers to focus on popular components.
  # include_used_components: true

# Discover some devices automatically
discovery:

# Allows you to issue voice commands from the frontend in enabled browsers
conversation:

# Enables support for tracking state changes over time
#history:

# View all events in a logbook
#logbook:

# Enables a map showing the location of tracked devices
map:

# Track the sun
sun:

# Weather prediction
sensor:
  - platform: yr

このあたりもデフォルトのままです。
updater:はhomeassistantにアップデートがあれば、画面上で知らせてくれます。
discovery:はhomeassistantが見つけることが可能なデバイスを見つけます。私の家ではDLNAオーディオデバイスを見つけてくれています。
conversation:は使っていませんが、対応ブラウザからのボイスコマンドを受け付けるかどうからしいのですが、不明です。
map:はその名の通り地図のサポートです。このため、最初の緯度経度の設定が必要なんです。と、言ってもほぼ使いませんが・・・。
sun:は今の地域が日中かどうかを表示してくれます。日の出日の入りもわかります。
sensor: - platform: yr は天気予報を表示してくれます。ノルウェー気象局の情報なので、アテになるかどうか・・・。

ここからは、実際のデバイスの定義をしていきます。
まずはセンサー系

#sensor:
  - platform: rest
    resource: http://192.168.0.xxx/json?tasknr=1
    name: Outside Temp
    value_template: '{{value_json.temperature}}'
    unit_of_measurement: "C"

  - platform: rest
    resource: http://192.168.0.xxx/json?tasknr=1
    name: Outside Hum
    value_template: '{{value_json.humidity}}'
    unit_of_measurement: "%"

これは、sonoffを改造して作った温度湿度センサーの情報を取得します。
gentoolinux.hatenablog.com
このとき作ったセンサーですね。
EspEasy化されたsonoffはhttpサーバーでjson形式の情報を提供しているので、httpの情報を読み取るAPIプラットフォームであるRESTを使うので、- platform: restを指定します。
resource:はjsonを提供するURLを指定。
name:はそのままセンサーの名称を。
value_template:はjson形式であることを指定し、jsonの左辺を指定します。シングルコーテーションや{{}}が特殊ですが、気にせずそのまま指定しましょう。他の形式はよくわかりません。
unit_of_measurement:は、数字の後に付く単位を指定しましょう。
これで、homeassistantの上部にセンサーの情報が表示されるようになります。
f:id:naoyukinagano:20180616212904p:plain

では、重要な電灯スイッチ系です。
以前Raspberry Pi ZeroにWebioPiをインストールしていました。
gentoolinux.hatenablog.com
gentoolinux.hatenablog.com
ですので、linuxのコマンドを駆使してスイッチのON/OFFや状態把握をします。

switch:
  - platform: command_line
    switches:
      snowball:
       command_on: "/usr/bin/curl -X POST http://192.168.0.xxx:8000/GPIO/26/value/0"
       command_off: "/usr/bin/curl -X POST http://192.168.0.xxx:8000/GPIO/26/value/1"
       command_state: "/usr/bin/curl -X GET http://192.168.0.xxx:8000/GPIO/26/value"
       value_template: '{{ value == "0" }}'
       friendly_name: Living SnowBall

まずスイッチ系はswitch:というセクションでまとめます。

  • platform: command_lineで、linuxのコマンドを使うことを指定します。

snowball:は自分で決めたひとつのスイッチのセクションです。他の名前でもかまいません。私は類スポールセンのスノーボールという照明をコントロールしたくて、このようなセクション名にしています。
command_on:は、スイッチをONにするコマンドです。
WebioPiに対してhttpのGetメソッドを使って、GPIOの状態を変更しますので、コマンドとしてはcurlを使います。(wgetの方が使い慣れているならwgetでもOK)
curlのオプション-Xはメソッドの指定です。WebioPiはPOSTでGPIOの状態を変更するので、-XでPOSTを指定します。
その後、URLを指定しますが、GPIO26番を0(Low)にする。という意味です。
同様に、command_off:のコマンドをcurlを使って記述します。
command_state:は、現在のON/OFF状態を取得するコマンドを記述します。WebioPiはGETで状態を取得するので、GPIO26番の状態をGETメソッドで取得します。
WebioPiはjsonではなく、単純にhttpで0か1だけでGPIOのLow/Hiの状態を返します。
うちのリレーはGPIOがLOW(valueが0)でONなので、value_template: '{{ value == "0" }}'を指定します。
もしHiでリレーオンなら、1を指定しましょう。
friendly_name: は表示する名称です。スイッチの場所と名称を記載するとわかりやすいでしょう。

次に、sonoffで作ったスイッチを設定します。うちでは寝室の電灯スイッチがこれです。
gentoolinux.hatenablog.com
gentoolinux.hatenablog.com

      bedroom:
       command_on: "/usr/bin/curl -X GET http://192.168.0.xxx/control?cmd=event,PowerOn"
       command_off: "/usr/bin/curl -X GET http://192.168.0.xxx/control?cmd=event,PowerOff"
       command_state: "/usr/bin/curl -X GET http://192.168.0.xxx/control?cmd=status,gpio,12"
       value_template: '{{ value_json.state == 1 }}'
       friendly_name: Bedroom Light

これは- platform: command_lineのswitchs:セクションの続きです。
EspEasy化されたsonoffもhttpでリレーを制御しますので、そのままcommand_lineプラットフォームを使います。
寝室にあるのでbedroom:セクションですがここは他とかぶらないければ何でもOKです。
command_on:はWebioPiと同様にcurlで指定しますが、EspEasyのrulesで指定している、PowerOnイベントを指定します。
command_off:も同様。
command_state: の中のvalue_template:はONの状態のjsonを定義するので、value_template: '{{ value_json.state == 1 }}'とします。
JSONで左辺がstateとなっているものを読み、1の場合がONということです。

続いて、command_lineではないプラットフォームの例です。
broadlink製のWiFi赤外線リモコンがありますので、その指定です。
同様にスイッチなので、switch:の続きとして指定していきます。

  - platform: broadlink
    host: 192.168.0.xxx
    mac: '34:AA:34:AA:00:AA'
    timeout: 10
    switches:
      roomba:
        friendly_name: "Roomba"
        command_on: 'JgBaAGIgIWEfYR9hXyEgYSBhHwACqWEhIGEgYR9hXyEgYSBhHwACqGIhIGEfYh9hXyEgYR9hIAACqGEiIGEfYSBhXyEgYR9hIAACsGIgIF8iXyFfYSEgYSBgIAANBQAAAAAAAAAAAAAAAAAA'
        command_off: 'JgBaAGIgIWAgYSBgIGFgICFgYAACcGIgIWAgYSBgIGFgICFgYAACaGIgIWAgYSBgIGFfICFhXwACaWIgIWAgYR9hIGFfICFhXwACaWIgIGEgYR9hIGBgICFgYAANBQAAAAAAAAAAAAAAAAAA'

switch:セクションの続きに- platform: broadlinkを指定します。
host:でIPアドレスを指定します。
mac:でシングルコーテーションでMACアドレスを指定します。
timeout:でコマンドを受け付けられなくてあきらめるタイムアウト秒数を指定します。
switches:でスイッチ群の指定が始まることを定義します。
friendly_name:で表示される名称を指定します。
command_on:でONの赤外線データをシングルコーテーションで指定します。中はbase64エンコードで記述します。

これで、各スイッチが表示されるようになります。
f:id:naoyukinagano:20180616222334p:plain

Gentooのglibcが"too old: GNU ld"と言われてアップデート出来ない

gccをアップデートしたらglibcもアップデートしたい。
しかし、何度やってもemergeできない。
configureの途中で、

too old: GNU ld

と言われました。
ldが古いので、ldをバージョンアップします。
ldはbinutilsに入っているので、

# emerge -u binutils

でアップデートしますが、それでもemerge -u glibcはtoo old: GNU ldと言い続けます。

どうしてアップデート出来ないのかと思ったら、configがあるんですね。

# binutils-config -l
 [1] x86_64-pc-linux-gnu-2.24 *
 [2] x86_64-pc-linux-gnu-2.28
 [3] x86_64-pc-linux-gnu-2.30

おやおや、古いままですね。新しいものにセットし直します。

# binutils-config 3

 * Switching to x86_64-pc-linux-gnu-2.30 ...
(中略)
 * Please remember to run:

 *   # . /etc/profile

最後の

 # . /etc/profile

を忘れず実施すると、emerge -u glibcも通るようになりました。

VFS not found でkernel panic。grub.cfgの設定

gentooのemergeシステムは、コンパイルの難しさや依存関係を自動的にやってくれて、しかも、自分の環境に合った効率的な実行ファイルをビルドする、素晴らしいシステムですが、中核となるカーネルだけは、ほぼ自分でmakeする必要があります。
そして、make installしたカーネルを、grub2に登録し、再起動して新しいカーネルで起動させますね。

私はたまたま早々に64bit EFI環境に移行したため、grub2-9999の頃からの長いおつきあいです。
あの頃はドキュメントが少なすぎて大変でした。

さて、しばらくぶりにカーネルアップデートして再起動すると、kernel panicに遭遇します。

そして、毎回のことなのに、作業を忘れます。
kernel panicの内容は

kernel panic – not syncing: VFS: Unable to mount root fs

ルートファイルシステムがマウントできません。

/boot/grub/grub.cfgを見ると、今までブートできていたのは、

linux   /kernel-4.9.34-gentoo root=/dev/sda3 ro  

ですが、起動できなかったのは、

/vmlinuz-4.9.95-gentoo root=2270e722-6fa3-4ebf-a3e0-8225cd61f291 ro  

vmlinuzは名前の変更忘れなので関係ないとして、root=がデバイス名称なのかUUIDなのかの違いで、UUIDでは起動しないようです。
決して"kernelの.configにFS_VFS=yを記入し忘れたから"ではありません。
kernelの.configにVFSの項目はありませんでした。

で、手動で/boot/gurb/grub.cfgの内容を書き換えても良いのですが、そうすると次回のカーネルアップデートの時にまた忘れます。
そこで、grub-mkconfig(人によってはgrub2-mkconfig)の時に、自動的にroot=/dev/sda3を記入するように設定します。

/etc/default/grub

GRUB_DISABLE_LINUX_UUID=true
GRUB_DEVICE="/dev/sda3"

設定のテンプレートに"GRUB_DEVICE="の項目はありませんので、ご自身で追加を。

これで、grub-mkconfig時に"root=/dev/sda3"が設定されます。