Androidのメモとか

ポキオの日記です。今日も遅延してない。

Sonos Roamことはじめ

ろーーーむ!

ポキオ Sonos Roam

Sonos Roamとは

オーディオメーカーSonosから発売されたポータブルスピーカーというやつです。

www.sonos.com

以前参加したSonosのイベントを発端に、だんだんSonosにハマってきてしまったわけですね。はい。

relativelayout.hatenablog.com

今回のRoamは最近日本で発売されたばかりなんですが…。

www.bcnretail.com

ヨドバシさんで先行販売らしく…。

ポキオ Sonos Roam

ほう…、オンラインストアでも買えるのね…。

ポキオ Sonos Roam

で、気づいたら自宅にあった、というわけです。

ひとまずUnboxing

箱に布製の「とって」みたいなものが付いてるんですよね。

ポキオ Sonos Roam

波平さんみたいで可愛いですよね(そこじゃない)。

ポキオ Sonos Roam

箱の中はこんな感じ。毎度おなじみの不織布で包まれています。

ポキオ Sonos Roam

不織布の中には、この色白の子。

ポキオ Sonos Roam

裏側にはType-Cの充電ポートと、電源ボタンっぽいやつ。

ポキオ Sonos Roam

操作系はすべてスピーカーの上側(?)にまとまっています。ボタン類は他のSonos製品と同じような感じで、UXも同じような感じなので、マニュアルを読まずに使い始められるのがいいですね。

ポータブルスピーカーを家の中で?

Roamはポータブルスピーカーであって、Wi−FiだけでなくBTにも対応しているのが特異的な部分であることは承知をしていますが、ひとまず家の中で使ってみます。あくまでもSonosのスピーカーであることには変わりないので、Wi−Fi接続をして他のSonosスピーカーと連携することもできるのがすごいところです。

ポキオ Sonos Roam

こちらは弊家のリビングの間取りと、既存のSonosスピーカーたち。普段は3つのOneを連携させて音楽を聞いています。(訳あって、PlaybarはTVと接続し、独立して使っています)

ポキオ Sonos Roam

まずはWFH中の音楽鑑賞。手元にRoamを置いて、3つのOneとRoamを連携させて音楽を聞いてみました。

ポキオ Sonos Roam

もともとWFHしている場所が部屋の端によっているため、最適なリスニングポジションではなかったものの、手元にRoamを置いたことで、

  • Roamのおかげで音楽、とくにボーカルの解像度が上がったような感じ
  • Oneが3つあることによる包まれてる感じ

上の2つがうまい塩梅で両立しているような環境となりました。また、コンパクトなRoamのおかげで、机の上に置いても邪魔にならない感じが非常に良いです。

ポキオ Sonos Roam

次に同様にOne 3つと連携させたまま、Roamをキッチンに持っていってみます。というのも、間取りの関係で、リビングの3つのOneの音楽がキッチンでは聞きづらいと常々感じていたものの、キッチンのためだけにOneを増設するのも微妙かなと思っていたわけです。

ポキオ Sonos Roam

Roamはバッテリー内蔵で、先述の通りコンパクト。しかも縦置きもできるので、我が家の狭いキッチンでも置き場に困りません。そして、コンパクトなのにパワフルな音を出せるので、意外と音がうるさい料理中もしっかり音楽に没頭(料理も頑張ってますが)できました。リビングに行っても、キッチンに戻っても、シームレスに音楽が聞ける体験は、本当に素晴らしいですねぇ。そして、必要なときだけ持っていけるのは、ポータブルスピーカーならではのメリットですね。

というわけで

  • My New Gear : Sonos Roam
  • ポータブルスピーカーだけど、まだ家の中でしか使ってないよ
  • でも、家の中でも機動性を活かして、いろいろ使えるすごいやつ
  • 既存のSonosシステムと連携ができる
  • コンパクトだけどいい音

サマるとこんな感じでしょうか。屋外でのレビューはまた今度!

娘の夏休みの宿題のデイリー進捗を可視化してみた

見える化

ポキオ Arduino Todo カンバン Adafruit Neopixel

意外と多いタスク

小1の娘は初めての長い夏休みを堪能しつつ、初めての夏休みの宿題をこなしています。意外と学校の宿題も多く大変そうだなぁと思いつつ、親がいちいち「あれやった?」みたいなポーリングによる進捗確認は工数だけがかかる作業なのであまりしたくありません。

そこで、デイリーでこなすべき宿題の進捗を可視化できるガジェットを作ってみました。

Arduinoで作っていきます

イメージとしては光り物で、タスクが一覧できる物理的なカンバンを作って、タスクごとに光らせるんですが、タスクが終わったらボタンを押して光っている色を変える感じ。

ポキオ Arduino Todo カンバン Adafruit Neopixel

とりあえず家に転がってたArduino Mega互換ボードに、NeopixelのLEDとスイッチを接続。スイッチはプルアップとして使うので、スイッチはGPIOとGNDにつなげます。Neopixelは信号線を1つでも良かったのですが、無心で作ってたので、複数信号線を作ってしまいました…(笑)

ポキオ Arduino Todo カンバン Adafruit Neopixel

さらに、3DプリンタでLEDを覆うようなパーツを作って・・・。

ポキオ Arduino Todo カンバン Adafruit Neopixel

これらをすべてOSBボードに打ち付けて、できました。

動作の様子はこちら

起動直後はすべてのタスクが赤く光ります。

ポキオ Arduino Todo カンバン Adafruit Neopixel

タスクを終えて、ボタンを押すと赤→青になります。

ポキオ Arduino Todo カンバン Adafruit Neopixel

全部のタスクが終わると、白色点滅。

全クリ時に、おめでとう感を出していくスタイルです。

導入してみてどうだった?

普段はWFHな私ですが、仕事中も娘の進捗が見えて非常によかったです。また、娘も達成感を味わえるようで、進んでArduinoの電源を入れて宿題に取り組んでました。

とりあえず、大成功?

コードはこんな感じ

#define使えよ…(笑)

#include <Adafruit_NeoPixel.h>

int b1 = 0;
int b2 = 15;
int b3 = 17;
int b4 = 19;
int b5 = 21;

int l1 = 12;
int l2 = 10;
int l3 = 5;
int l4 = 8;
int l5 = 7;

Adafruit_NeoPixel pixels1 = Adafruit_NeoPixel(2, l1, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel pixels2 = Adafruit_NeoPixel(2, l2, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel pixels3 = Adafruit_NeoPixel(2, l3, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel pixels4 = Adafruit_NeoPixel(2, l4, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel pixels5 = Adafruit_NeoPixel(2, l5, NEO_GRB + NEO_KHZ800);

bool stat1 = false;
bool stat2 = false;
bool stat3 = false;
bool stat4 = false;
bool stat5 = false;

void setup() {
  Serial.begin(9600);

  pinMode(b1, INPUT_PULLUP);
  pinMode(b2, INPUT_PULLUP);
  pinMode(b3, INPUT_PULLUP);
  pinMode(b4, INPUT_PULLUP);
  pinMode(b5, INPUT_PULLUP);

  pixels1.begin();
  pixels2.begin();
  pixels3.begin();
  pixels4.begin();
  pixels5.begin();

  pixels1.setBrightness(64);
  pixels2.setBrightness(64);
  pixels3.setBrightness(64);
  pixels4.setBrightness(64);
  pixels5.setBrightness(64);
}

void loop() {
  stat1 = stat1 || (digitalRead(b1) == LOW);
  stat2 = stat2 || (digitalRead(b2) == LOW);
  stat3 = stat3 || (digitalRead(b3) == LOW);
  stat4 = stat4 || (digitalRead(b4) == LOW);
  stat5 = stat5 || (digitalRead(b5) == LOW);

  if (stat1) {
    pixels1.setPixelColor(0, pixels1.Color(0, 0, 255));
    pixels1.setPixelColor(1, pixels1.Color(0, 0, 255));
  } else {
    pixels1.setPixelColor(0, pixels1.Color(255, 0, 0));
    pixels1.setPixelColor(1, pixels1.Color(255, 0, 0));
  }

  if (stat2) {
    pixels2.setPixelColor(0, pixels1.Color(0, 0, 255));
    pixels2.setPixelColor(1, pixels1.Color(0, 0, 255));
  } else {
    pixels2.setPixelColor(0, pixels1.Color(255, 0, 0));
    pixels2.setPixelColor(1, pixels1.Color(255, 0, 0));
  }

  if (stat3) {
    pixels3.setPixelColor(0, pixels1.Color(0, 0, 255));
    pixels3.setPixelColor(1, pixels1.Color(0, 0, 255));
  } else {
    pixels3.setPixelColor(0, pixels1.Color(255, 0, 0));
    pixels3.setPixelColor(1, pixels1.Color(255, 0, 0));
  }

  if (stat4) {
    pixels4.setPixelColor(0, pixels1.Color(0, 0, 255));
    pixels4.setPixelColor(1, pixels1.Color(0, 0, 255));
  } else {
    pixels4.setPixelColor(0, pixels1.Color(255, 0, 0));
    pixels4.setPixelColor(1, pixels1.Color(255, 0, 0));
  }

  if (stat5) {
    pixels5.setPixelColor(0, pixels1.Color(0, 0, 255));
    pixels5.setPixelColor(1, pixels1.Color(0, 0, 255));
  } else {
    pixels5.setPixelColor(0, pixels1.Color(255, 0, 0));
    pixels5.setPixelColor(1, pixels1.Color(255, 0, 0));
  }

  pixels1.show();
  pixels2.show();
  pixels3.show();
  pixels4.show();
  pixels5.show();

  if (stat1 && stat2 && stat3 && stat4 && stat5) {
    congrats();
  }
}

void congrats() {
  for (int i = 0; i < 100; i++) {
    setColor(255, 255, 255);
    delay(500);
    setColor(0, 0, 0);
    delay(500);
  }

  setColor(255, 255, 255);
  while (1) {}
}

void setColor(int r, int g, int b) {
  pixels1.setPixelColor(0, pixels1.Color(r, g, b));
  pixels1.setPixelColor(1, pixels1.Color(r, g, b));
  pixels2.setPixelColor(0, pixels1.Color(r, g, b));
  pixels2.setPixelColor(1, pixels1.Color(r, g, b));
  pixels3.setPixelColor(0, pixels1.Color(r, g, b));
  pixels3.setPixelColor(1, pixels1.Color(r, g, b));
  pixels4.setPixelColor(0, pixels1.Color(r, g, b));
  pixels4.setPixelColor(1, pixels1.Color(r, g, b));
  pixels5.setPixelColor(0, pixels1.Color(r, g, b));
  pixels5.setPixelColor(1, pixels1.Color(r, g, b));

  pixels1.show();
  pixels2.show();
  pixels3.show();
  pixels4.show();
  pixels5.show();
}

Android 12でBluetoothは位置情報権限の呪縛から解かれる

久々にAndroidの話。

ポキオ Android 12 BLUETOOTH_SCAN BLUETOOTH_CONNECT permission

tl:dr

  • いままではBluetoothの利用には位置情報の権限が必要でした
  • Android 12からBluetooth専用の権限が新設され、位置情報の権限は不要になります

developer.android.com

忌まわしき位置情報の権限

Androidの話なので、iOSユーザーの方はポカーンって感じかもしれないですが。

ポキオ Android 12 BLUETOOTH_SCAN BLUETOOTH_CONNECT permission

Androidはユーザーのプライバシーを守るために、Android 6からRuntime Permissionsを実装し、アプリがスマホの一部のリソースを使う前に、アプリがユーザーに対して権限の付与を尋ねるという機能が入りました。

たとえば、地図アプリや、アプリでどこかのスポットにチェックインしたりするときなど、アプリがスマホの位置情報をアクセスする際は、予めユーザーに対して位置情報へのアクセスを許可してもらう必要があり、許可がおりて初めて位置情報にアクセスできるようになります。

ただし、ここでポイントなのは、Bluetooth(BLE)やWi−Fiのスキャンをする際も位置情報アクセスの権限取得が何故か必要ということです。Android側のロジックとしては、BLEビーコンやWi−Fiのアクセスポイントのスキャン結果とそれぞれの電波強度から位置情報が推測できるため、そのような制約を設けていると思われるのですが、アプリを使っているユーザーからは、そんな内部の都合なんて理解できるはずがありません。

結果として、アプリとしては位置情報がほしいわけでもないのにBLEやWi− Fiを使う際は、ユーザーに対して位置情報へのアクセス権限を求めざるを得ず、ユーザーにとってみれば、なぜ位置情報へのアクセスを求められているのかがわからず、変に怪しまれるということが多々あったわけです。

ようやくAndroid 12で仕様が変わります

この状況が2021年秋にリリースされるAndroid 12で大きくかわります。

ポキオ Android 12 BLUETOOTH_SCAN BLUETOOTH_CONNECT permission

Android 12では、新たにBluetooth利用のための権限が2つ新設されます。

これにより、今までAndroidManifest.xmlで、

 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

のように宣言していた位置情報アクセスの権限でしたが、BLEのスキャンと接続をするだけであれば、上記の権限は不要で、

<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
                 android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

というように、専用の権限利用の宣言だけをすれば良くなりました。ユーザーの見た目的にもBLE利用の際に求められる権限は、

Allow [an app] to find, connect to, and determine relative position of nearby devices.

のような、わかりやすい理由で権限を求められるようになります。

戦いはこれで終わりなのか?

というわけで、新しいOSでは、BLE利用と位置情報利用のための権限が分かれたことで、ユーザーフレンドリーな仕様に一歩近づいたわけです。

ポキオ Android 12 BLUETOOTH_SCAN BLUETOOTH_CONNECT permission

ただし、これはあくまでもAndroid 12のデバイスで動作するアプリの話であって、依然として大多数を占めるAndroid 11以下のデバイスでは関係ない話です。

なので、アプリデベロッパーとしてはぬか喜びすることなく、当面の間はAndroid 12で増えた権限のハンドリングをしつつ、Android 11以下のデバイスを考慮した設計が必要になりそうです。悲しいかな、デベロッパーの諸兄姉は目をつぶっていてもif文を無限にコーディングできるスキルを持っているはずなので、容易い話かと思いますが。

一方でユーザーも、急にアプリが位置情報の権限を要求してきても焦らずに、許可・不許可をする必要があります。もしかしたら、Bluetoothを使いたいだけなのに位置情報の権限を要求しているのかもしれません。(まぁ、そういうときは、アプリはその旨をユーザーに伝えるべきですが)位置情報に限らずそのような権限はあとからでも不許可に倒すことも可能なので、脳死で許可・不許可をせずに、冷静に考えて決めてほしいなと願っています。

でも、あんまり不許可にしないでね!もしかしたらバグるかもしれないから!(笑)

Androidアプリ開発 74のアンチパターン

Androidアプリ開発 74のアンチパターン

  • 作者:深見 浩和
  • 発売日: 2017/09/27
  • メディア: 単行本(ソフトカバー)

テレワーク用の椅子(5脚目)

ついに5脚目。

ポキオ カスケード Cascade Mountain Tech Compact High Back Aluminum Chair

テレワーク用の椅子の紆余曲折

いままで色々試してきました。

relativelayout.hatenablog.com

結局、最後に買った「ポンコタン ハイバックチェア」が一番しっくり来ていました。

なんですが、毎日座ってたせいで、ファブリックの縫い目がほつれ始めてきていました。

コストコ怪しいハイバックチェアが・・・

で、コストコでなにやらコスパの良さそうなハイバックチェアが売っていました。

ポキオ カスケード Cascade Mountain Tech Compact High Back Aluminum Chair

すぐにカートに入れちゃう癖やめたいところですが、なんと約3500円。

ポキオ カスケード Cascade Mountain Tech Compact High Back Aluminum Chair

きっと本国のメリケン人が乗っても壊れないんだろうなとか思うと、耐久性も期待できそうだったので早速購入。

Unboxingと組み立て

ポンコタンのハイバックチェアと同じように収納バッグに入ってて、非常にコンパクト。

ポキオ カスケード Cascade Mountain Tech Compact High Back Aluminum Chair

で、ひとまず開封。骨組みを組み立てるところまでは良かったが・・・。

ポキオ カスケード Cascade Mountain Tech Compact High Back Aluminum Chair

ファブリックの座面を骨組みに取り付けるのが本当に大変。未使用でファブリックが伸びてないのか、なかなか骨組みに入らず10分ほど格闘。最後はなんとか入りましたが。

ポキオ カスケード Cascade Mountain Tech Compact High Back Aluminum Chair

ちなみに、足はこんなタイプ。地面が砂地などでも潜り込まないような構造になっていますが、ポンコタンと異なり、この足は取り外しができないようです。

ポキオ カスケード Cascade Mountain Tech Compact High Back Aluminum Chair

で、完成。

座り心地は?

まず、ファーストインプレッションとして、

(ポンコタンのハイバックチェアと比べると…)

  • 背もたれは低めで、ハイバックというかミッドバック。
  • 左右方向のホールド感は低め。
  • 前後方向のホールド感は低めで、浅め。
  • 座面はちょっとだけ高め。
  • 大きめのヘッドレストがついてて快適。
  • 背もたれ上部に、左右方向に伸びる骨が入ってて安定感がある。

僕も割とお尻が大きい方ですが、それでも余裕があるので、ポンコタンより欧米寄りというのがわかります。とまぁ、色々差異はあるものの、なかなかの座り心地。耐久性の評価はこれからですが、すぐに壊れてもコストコなので返品しちゃえばいいしね。そう考えても3500円というのはコスパ高いですね。

ポキオ カスケード Cascade Mountain Tech Compact High Back Aluminum Chair

というわけで、カスケードマウンテンテックのハイバックチェアでした。次こそはHelinox買うん・・・だ・・・!

バチ抜けをNode-REDで地図に可視化する

つりはっく。

ポキオ Node-RED WorldMap バチ抜け

バチ抜けとは

釣り用語の一つです。

pokio-ringyo.hatenablog.com

pokio-ringyo.hatenablog.com

バチとはイソメやゴカイなどの環形動物のことを指し、バチ抜けとは春先の大潮の夜に海底に潜んでいるバチが一気に浮いてくる現象です。抜けたバチは種々の魚にとってはごちそうであって、バチを求めて魚が寄ってくるというわけです。バチ抜けは地域によって発生する時期がまちまちで、更に魚が寄ってくるかもわからない自然のイベントとなっているわけで、いつどのへんでバチ抜けが起こったのかを知りたいのは釣り人の性みたいなもの。

ポキオ輪業商会 横浜 メバリング シーバス プラッキング

今回は、Twitterで位置情報とともにつぶやかれている情報をもとに、バチ抜けなどの情報を地図上にマッピングしてみようと思います。

使うのはNode-RED

今回もNode-REDで実装していきます。フローはこんな感じ。

ポキオ Node-RED WorldMap バチ抜け

ポイントとなるノードはこちら。

Twitterノード

これでTwitter上の情報をほぼリアルタイムにスキャンして、指定したハッシュタグのツイートを引っ張ってきます。実際にツイートが見つかると、おそらくTwitter本家のTweet Objectの形式でデータが取得できます。

developer.twitter.com

ただ、仕様が複雑(笑)今回は、次のFunctionノードでテキトーに位置情報をパースします。

Functionノード

ここで、Twitterノードから取得したデータから位置情報をパースします。

if(msg.tweet && msg.tweet.place && msg.tweet.place.bounding_box && msg.tweet.place.bounding_box.coordinates){
    var longitude = 0;
    var latitude = 0;
    var array = msg.tweet.place.bounding_box.coordinates[0];
    var tweet = msg.payload.replace(/\r?\n/g,"");
    var link = 'https://twitter.com/' + msg.tweet.user.screen_name + '/status/' + msg.tweet.id_str;

    array.forEach(value => {
        longitude += value[0];
        latitude += value[1];
    });
    
    longitude /= array.length;
    latitude /= array.length;
    
    msg = {};
    msg.has_location = true;
    msg.payload = {
        "lat" : latitude,
        "lon" : longitude,
        "name" : tweet,
        "weblink" : link,
    };
}
return msg;

tweet.place.bounding_box.coordinates内の座標の平均値をとって、それを緯度経度として採用しています。後段の処理でCSVファイルに保存するため、Tweet本文は改行を削除してから保存しています。

CSVノード・Fileノード

前段の処理で、位置情報がパースできたTweetだけをCSVとして一旦保存しておきます。

WorldMapノード

今回の要。

flows.nodered.org

node-red-contrib-web-worldmapをインストールして、地図を表示できるようになります。フロー中にこのWorldMapノードがあると、特定のURLにアクセスすると地図が表示されるようになります。このノードに対して緯度経度などをくべると、地図上にピンが表示されるようになります。今回は、WorldMapにアクセスがあったときに発火するイベントを契機に、CSVを読み込み、そこに書かれた位置情報をすべてWorldMapにマッピングします。

これをつかって地図表示させてみました

こんな感じ。

ポキオ Node-RED WorldMap バチ抜け

まだまだTwitterの監視をし始めたばかりなのでピンはすくなめですが、ちゃんと可視化できています。

ちょっとずつピンが増えてくると楽しいですねぇ。

フローはこちら

[
    {
        "id": "add2a0f2.b28d2",
        "type": "twitter in",
        "z": "52e43921.021f08",
        "twitter": "",
        "tags": "バチ抜け,シーバス,#バチ抜け,#シーバス",
        "user": "false",
        "name": "バチ抜けツイート",
        "inputs": 0,
        "x": 130,
        "y": 300,
        "wires": [
            [
                "643d563.f2d9fa8",
                "1e92e179.29a17f"
            ]
        ]
    },
    {
        "id": "efe29703.79f498",
        "type": "comment",
        "z": "52e43921.021f08",
        "name": "ツイート監視",
        "info": "",
        "x": 110,
        "y": 260,
        "wires": []
    },
    {
        "id": "643d563.f2d9fa8",
        "type": "function",
        "z": "52e43921.021f08",
        "name": "位置情報パース",
        "func": "if(msg.tweet && msg.tweet.place && msg.tweet.place.bounding_box && msg.tweet.place.bounding_box.coordinates){\n    var longitude = 0;\n    var latitude = 0;\n    var array = msg.tweet.place.bounding_box.coordinates[0];\n    var tweet = msg.payload.replace(/\\r?\\n/g,\"\");\n    var link = 'https://twitter.com/' + msg.tweet.user.screen_name + '/status/' + msg.tweet.id_str;\n\n    array.forEach(value => {\n        longitude += value[0];\n        latitude += value[1];\n    });\n    \n    longitude /= array.length;\n    latitude /= array.length;\n    \n    msg = {};\n    msg.has_location = true;\n    msg.payload = {\n        \"lat\" : latitude,\n        \"lon\" : longitude,\n        \"name\" : tweet,\n        \"weblink\" : link,\n    };\n}\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 340,
        "y": 300,
        "wires": [
            [
                "8649b0d9.a5f98"
            ]
        ]
    },
    {
        "id": "dddeedd9.5a74b",
        "type": "worldmap",
        "z": "52e43921.021f08",
        "name": "",
        "lat": "",
        "lon": "",
        "zoom": "",
        "layer": "",
        "cluster": "",
        "maxage": "",
        "usermenu": "show",
        "layers": "show",
        "panit": "false",
        "panlock": "false",
        "zoomlock": "false",
        "hiderightclick": "false",
        "coords": "none",
        "showgrid": "false",
        "allowFileDrop": "false",
        "path": "/worldmap",
        "x": 1060,
        "y": 460,
        "wires": []
    },
    {
        "id": "8649b0d9.a5f98",
        "type": "switch",
        "z": "52e43921.021f08",
        "name": "位置情報があるときだけ動作",
        "property": "has_location",
        "propertyType": "msg",
        "rules": [
            {
                "t": "true"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 1,
        "x": 580,
        "y": 300,
        "wires": [
            [
                "6d46063b.e4bce8"
            ]
        ]
    },
    {
        "id": "433fb432.0e49fc",
        "type": "function",
        "z": "52e43921.021f08",
        "name": "ズーム",
        "func": "msg.payload = {\n    command : {\n        lat : msg.payload.lat,\n        lon : msg.payload.lon,\n        zoom : 6,\n    }\n}\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 930,
        "y": 480,
        "wires": [
            [
                "dddeedd9.5a74b"
            ]
        ]
    },
    {
        "id": "75d4e561.cf315c",
        "type": "worldmap in",
        "z": "52e43921.021f08",
        "name": "",
        "path": "/worldmap",
        "events": "all",
        "x": 100,
        "y": 460,
        "wires": [
            [
                "493cf6af.c9d1d8"
            ]
        ]
    },
    {
        "id": "493cf6af.c9d1d8",
        "type": "switch",
        "z": "52e43921.021f08",
        "name": "",
        "property": "payload.action",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "connected",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 1,
        "x": 250,
        "y": 460,
        "wires": [
            [
                "6381668a.b712c8"
            ]
        ]
    },
    {
        "id": "6d46063b.e4bce8",
        "type": "csv",
        "z": "52e43921.021f08",
        "name": "CSVに変換",
        "sep": ",",
        "hdrin": "",
        "hdrout": "",
        "multi": "one",
        "ret": "\\n",
        "temp": "lat,lon,name,weblink",
        "skip": "0",
        "strings": false,
        "x": 810,
        "y": 300,
        "wires": [
            [
                "96084250.d8c06",
                "23921cd8.841d44"
            ]
        ]
    },
    {
        "id": "96084250.d8c06",
        "type": "file",
        "z": "52e43921.021f08",
        "name": "history.csvに保存",
        "filename": "history.csv",
        "appendNewline": true,
        "createDir": false,
        "overwriteFile": "false",
        "encoding": "none",
        "x": 1010,
        "y": 300,
        "wires": [
            []
        ]
    },
    {
        "id": "23921cd8.841d44",
        "type": "debug",
        "z": "52e43921.021f08",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "x": 970,
        "y": 340,
        "wires": []
    },
    {
        "id": "3b137fe3.26fa",
        "type": "csv",
        "z": "52e43921.021f08",
        "name": "",
        "sep": ",",
        "hdrin": "",
        "hdrout": "",
        "multi": "one",
        "ret": "\\n",
        "temp": "lat,lon,name,weblink",
        "skip": "0",
        "strings": true,
        "x": 630,
        "y": 460,
        "wires": [
            [
                "5786ae6a.d8e1"
            ]
        ]
    },
    {
        "id": "6381668a.b712c8",
        "type": "file in",
        "z": "52e43921.021f08",
        "name": "history.csvを読み込み",
        "filename": "history.csv",
        "format": "utf8",
        "chunk": false,
        "sendError": false,
        "encoding": "none",
        "x": 440,
        "y": 460,
        "wires": [
            [
                "3b137fe3.26fa"
            ]
        ]
    },
    {
        "id": "5786ae6a.d8e1",
        "type": "function",
        "z": "52e43921.021f08",
        "name": "weblink作成",
        "func": "const weblink = {\n    \"name\" : \"Twitter\",\n    \"url\" : msg.payload.weblink,\n    \"target\":\"_new\"\n}\n\nmsg.payload.weblink = weblink;\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 790,
        "y": 460,
        "wires": [
            [
                "dddeedd9.5a74b",
                "433fb432.0e49fc"
            ]
        ]
    },
    {
        "id": "cddaa0e7.caea7",
        "type": "comment",
        "z": "52e43921.021f08",
        "name": "Mapにアクセスがあったとき",
        "info": "",
        "x": 160,
        "y": 420,
        "wires": []
    },
    {
        "id": "1e92e179.29a17f",
        "type": "debug",
        "z": "52e43921.021f08",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "x": 310,
        "y": 340,
        "wires": []
    }
]

Node-REDをつかってGoogle Home Miniに時報を喋らせる

テレワークに便利。

ポキオ Google Home Mini 時報 Node-RED

tl;dr

  • Google Homeの「ルーティーン」を使ってもできる
  • 今回はNode-REDから時報を喋らせる
  • 定期的にタスクを実行して、喋らせたい文字列をGoogle Home MiniにCastするだけ

テレワークのタイムマネジメント

いうほどできてないんですが。ついうっかりミーティングの存在を忘れたりしがちなので、以前から1時間おきにGoogle Home Miniに時報を喋らせてました。

support.google.com

ルーティーンという機能をつかって、設定した時間に「OK, google. 今何時?」という問いを内部的に投げて、その答えをGoogle Home Miniから発話させることで、時報として活用していました。ただ、最近設定した時間の±1分くらいに発火する謎の病にかかってしまい、代替手段を探していました。で、今回はその方法としてNode-REDからGoogle Home Miniに対して時報を喋らせてみようと思います。

フローは至ってシンプル

最初にフローから。

ポキオ Google Home Mini 時報 Node-RED

こんな感じ。

ポキオ Google Home Mini 時報 Node-RED

まずはInjectionノードで定期実行をトリガー。「おいおい、真夜中に発火させてどうした?」みたいなこと思うかもしれませんが、Node-REDが動作しているRaspberry PiタイムゾーンがUKのままなので、運用でカバーしています・・・(笑)良い子は真似しないでね。

次のFunctionノードで発話させるメッセージを作成しています。こちらでもタイムゾーンをゴニョゴニョしてます。

const date = new Date(Date.now() + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000));
msg.message = "現在の時刻は、だいたい" + date.getHours() + "時です。";
return msg;

そして、実際の発話はnode-red-contrib-castノードで実行しています。

flows.nodered.org

文字通り、Castを行うノードですが、対象のIPアドレスGoogle Home MiniのIPアドレスにすると、msg.messageのStringを発話してくれます。

実際のフローはこちら

[
    {
        "id": "3e94927e.7c6d7e",
        "type": "cast-to-client",
        "z": "36b47ac9.340b66",
        "name": "",
        "url": "",
        "contentType": "",
        "message": "",
        "language": "ja",
        "ip": "192.168.1.31",
        "port": "",
        "volume": "50",
        "x": 630,
        "y": 120,
        "wires": [
            []
        ]
    },
    {
        "id": "fdae59a3.98caa8",
        "type": "inject",
        "z": "36b47ac9.340b66",
        "name": "22:00-00:00",
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "repeat": "",
        "crontab": "0 22-23 * * 1,2,3,4,5",
        "once": false,
        "onceDelay": 0.1,
        "x": 140,
        "y": 60,
        "wires": [
            [
                "88c5f7d4.a36758",
                "b569a844.e50c58"
            ]
        ]
    },
    {
        "id": "88c5f7d4.a36758",
        "type": "debug",
        "z": "36b47ac9.340b66",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "x": 370,
        "y": 60,
        "wires": []
    },
    {
        "id": "b569a844.e50c58",
        "type": "function",
        "z": "36b47ac9.340b66",
        "name": "メッセージ作成",
        "func": "const date = new Date(Date.now() + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000));\nmsg.message = \"現在の時刻は、だいたい\" + date.getHours() + \"時です。\";\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 380,
        "y": 120,
        "wires": [
            [
                "3e94927e.7c6d7e"
            ]
        ]
    },
    {
        "id": "a93920ed.ddbc3",
        "type": "inject",
        "z": "36b47ac9.340b66",
        "name": "00:00-10:00",
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "repeat": "",
        "crontab": "0 0-9 * * 1,2,3,4,5",
        "once": false,
        "onceDelay": 0.1,
        "x": 140,
        "y": 120,
        "wires": [
            [
                "88c5f7d4.a36758",
                "b569a844.e50c58"
            ]
        ]
    }
]

カードが不正利用されました

流石にビビった。

ポキオ LINE PAY カード 不正利用 Amazon 500円

きっかけ

ある日、LINE PAYカード(JCBプリペイドカード)での利用通知がアプリに届いたんですが。

ポキオ LINE PAY カード 不正利用 Amazon 500円

「Amazon 500円」

いや、何も買った覚えはないし、Amazonは別のカードを紐付けているので、そもそもLINE PAYカードにAmazonから請求が来るのはおかしいと思い、とりあえず不正利用の可能性が高まりました。

まずはLINE PAYカードのカスタマーサービスに連絡

すると、こんな返信が。

【1】請求内容の確認について

恐れ入りますが、Amazonからの請求内容については、注文履歴をご確認ください。

なお250円、500円、4,900円の請求の場合、Amazonプライム会費の可能性があります。

ほうほう。少なくとも自分のアカウントを確認しても、請求先は違うので、これはなさそう。

【2】問題が解決できない場合

Amazonカスタマーサービスにご相談いただいた上で、覚えがないまたは、購入内容が確認できない場合は、弊社にて対象取引の取消手続きを承ることが可能です。​

弊社での取消手続きには、事前にお客さま自身で以下2点をご対応いただく必要があります。

・請求が発生したLINE Pay カードの解約操作

・最寄りの警察署へ被害内容の相談

どうやらLINE PAYカード側でトランザクションのキャンセルを行うには、いろいろ手順を踏まないとダメそうということがわかりました。結構面倒なんですね。

次にAmazonカスタマーサービスに連絡

出来事をAmazonカスタマーサービスに連絡したところ、その場で私のアカウントで発生した請求ではないと言うことが判明。引き続き、専門のチームで調査を進めた結果、翌日にAmazonから連絡があり、

  • 私以外のアカウントで利用されたことが確認できた
  • 私が承認した決済でないことから、そのアカウントに対して適正な対処をした(BANってこと?)
  • 500円に関しては返金予定

とのことでした。Amazonのフットワークの軽さに感動。

ということで、LINE PAYカードを解約することなく、返金が行われることになり、一旦解決。

なぜ不正利用されてしまったのか問題

起こってしまったことはしょうがないとして、なぜ不正利用されてしまったのかは気になるところ。

スキミングや物理的にカード番号・CVVが漏れた

多分ないはず。リアル店舗でLINE PAYカードを使うことはまずないし、カードを持ち歩こともないので、この線は可能性が低いと思っている。

ネットで利用した際にカード番号・CVVが漏れた

ここ数年の利用を遡ってみると、ほとんどがLINEアプリ上でのLINE PAYの決済で、JCBプリペイドカードとしての利用はあまりありませんでした。ただ、ちょっと怪しいのが、

  • 某自転車通販サイト(台湾)
  • 某総合ECサイト(中国)

このあたりは、Paypalが使えなかったため、LINE PAYカードで決済をしていました。あまり信じたくないですが、こういうところから漏れたんですかねぇ…。

総当り攻撃でカード番号・CVVが漏れた

そうでないと願っています。

不正利用されることは防げないという前提で対策をする

まぁ、犯人探しは想像の域を出ないので、今後はちゃんと対策をしていきたいです。

不要なカードは停止

クレジットカード・プリペイドカードは便利なんですが、それはセキュリティホールでもあるということがわかったので、使わないものは停止をしようと思います。LINE PAYカードなどは、アプリから一時的にカードを停止したりできるので、そういう機能も使っていこうと思います。

利用時に通知を受けられるようにする

今回も、問題発覚の要因になったのは利用通知でした。メインのカードも通知が飛ぶように設定をしました。

ポキオ LINE PAY カード 不正利用 Amazon 500円

www.smbc-card.com

カードの補償内容を確認

不正利用時にカード会社がどのくらい補償してくれるかは、会社によってまちまちです。LINE PAYカードの場合は、

(3) 1事故(一事由または同一原因による一連の事由により発生した損害をいいます。)あたりの補償限度額は、原則、10万円とします。ただし、前二号で定める補償対象となる損害の額が10万円を超過する場合は、利用者のご利用状況や警察当局による捜査結果等を踏まえ、補償限度額の引き上げを個別に検討します。

https://terms2.line.me/linepay_JP_Money_new_TermsofUse?lang=ja

となっており、10万円くらいは補償の対象っぽいです。カードによっては補償がなかったりすることもあるので、確認が必要ですね。

ヤバそうなサイトは捨てカードで決済

今回も、Paypalが使えない怪しげなサイトだったのでプリペイドカードで決済をしましたが、今後もそういうサイトを利用する際はメインのカードではなく、利用できる額が少額になっているプリペイドカードでの決済が安心かもしれませんね。

というわけで

不正利用なんて他人事だとおもってましたが、まさか自分の身にも降りかかると思ってもいなかったです。今後も気をつけて使っていきたい所存。

「Androidのメモとか」は、Amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、Amazonアソシエイト・プログラムの参加者です。

このブログは個人的なメモ書きであったり、考えを書く場所であります。執筆者の所属する団体や企業のコメントや意向とは無関係であります。また、このブログは必ずしも正しいことが書かれているとは限らず、誤字脱字や意図せず誤った情報を載せる場合がありえます。それが原因で読者が不利益を被ったとしても、執筆者はいかなる責任も負いません。ありがとうございます。