Google Homeで緊急地震速報!テレビより数秒早く家族に知らせるスマートホーム防災システムの作り方

未分類

地震大国・日本に暮らす私たちにとって、緊急地震速報はまさに命綱です。テレビをつけていれば放送で知らせてくれますが、就寝中や料理中など、スマートフォンが手元にない場面では頼りになりません。
本記事では、P2P地震情報のWebSocket APIとRaspberry Pi + Pythonを組み合わせて、Google Homeから緊急地震速報を音声通知するシステムの実装方法を解説します。うまく実装すると、テレビの緊急地震速報より数秒早く知らせることができます。

なぜこの機能を作ったのか?

我が家ではリビングと子供部屋にGoogle Homeを設置してスマートホーム化を進めています。各種通知もGoogle Home経由で受け取れるように整備してきましたが、ふと「緊急地震速報もGoogle Homeから流せたら安心だな」と思ったのが始まりです。
テレビをつけていない時間帯でも、スマートフォンが手元になくても、家のどこにいても緊急地震速報を受け取れるシステムがあれば、家族の安全確保につながると考えました。

スマホが手元になくても、いち早く地震を知って家族を守りたい!

実現したいこと

  • 緊急地震速報(EEW)を受信したらGoogle Homeから即座に音声で通知する
  • テレビの緊急地震速報より数秒早く通知する
  • 自宅からの震源距離・マグニチュード・予測最大震度をもとに通知条件をカスタマイズできる
  • 同一地震イベントの重複通知を防ぐ
  • 確定情報が届いたタイミングで追加通知する

この記事でわかること

  • P2P地震情報WebSocket APIを使ってEEWをリアルタイムで受信する方法
  • Google Homeへ緊急地震速報を音声通知するPythonプログラムの実装方法
  • テレビより速く通知するための3つの実装上の工夫
  • 距離・マグニチュード・震度を組み合わせた通知条件(conditions.json)の設定方法

必要な準備と用意するもの

ハードウェア
  • Raspberry Pi(例:Raspberry Pi 4 / 5)
  • Google Home / Google Nest(複数台可)
ソフトウェア/サービス
  • Python 3.x
    • websockets(pip install websockets)
    • asyncio(標準ライブラリ)
  • Node.js
    • google-home-notifier(Google Homeへの通知用)
  • P2P地震情報 WebSocket API(無料・登録不要)
    • エンドポイント:wss://api.p2pquake.net/v2/ws
  • 緊急地震速報チャイム音 MP3ファイル(「緊急地震速報 MP3」でウェブ検索して入手)

完成イメージ

完成すると、地震発生から下記の流れでGoogle Homeから自動で音声が流れます。

  1. Raspberry PiがP2P地震情報WebSocketに常時接続し、EEWを待ち受ける
  2. 緊急地震速報(コード 554 / 556)を受信する
  3. 震源座標から自宅までの距離を計算し、通知条件(距離・M・震度)と照合する
  4. 条件に合致したら、まず事前準備した「緊急地震速報チャイム」のMP3音源をGoogle Homeで即時再生する
  5. チャイムの再生中に続く詳細情報(震源地・マグニチュード・深さ)をTTSで読み上げる
  6. 確定情報(isFinal)が届いたら、確定内容を再度Google Homeが読み上げる

システムの仕組み

P2P地震情報 WebSocket APIとは

P2P地震情報(p2pquake.net)は、全国の有志が設置した地震計ネットワークのデータと気象庁の緊急地震速報を集約し、リアルタイムで配信している無料サービスです。WebSocket APIに常時接続しておくと、EEWを含む各種地震情報がプッシュ配信されます。登録不要・無料で利用できるため、個人のスマートホーム開発にも最適です。

受信するEEWには2種類のコードがあります。
コード 556:緊急地震速報(予報)— 震源推定から自動発表される最初の速報
コード 554:緊急地震速報(警報)— 特定地域に強い揺れが予測される場合に発表
本実装では両方のコードを受信・処理します。

なぜテレビの緊急地震速報より早いのか

テレビの緊急地震速報は、気象庁のデータが放送局のシステムを経由して放送電波に乗るまでに、わずかながらタイムラグが発生します。P2P地震情報のWebSocket APIはこの中継を省いてデータを直接受け取るため、より早く通知できます。
実際の運用ではテレビの緊急地震速報より数秒早くGoogle Homeから通知が流れることが確認できています。手元にスマートフォンがない状況でも、家中のGoogle Homeが鳴るため見逃しがありません。

実装のポイント

「できる限り早く、確実に通知する」ためにこだわった3つの工夫を紹介します。

① Google Homeを常にスタンバイ状態にして優先度で割り込み通知

自作のgooglehomeライブラリでは、priority パラメータで通知の緊急度を指定できます。緊急地震速報は priority=10(最高優先度)を指定しているため、音楽再生中や他の通知の再生中でも割り込んで緊急地震速報が再生されます。また、Google Homeは電源を入れたままスタンバイ状態を維持しているため、通知を受け取った瞬間に即座に音が出ます。

最高優先度で確実に割り込み
他のプログラムが音楽や通知を再生中でも、緊急地震速報を優先して割り込み再生できるため通知を見逃しません。

② 第一報はTTSを使わず事前準備した音源を即時再生

TTS(テキスト読み上げ)はテキストを音声ファイルに変換してから再生するため、発話開始まで数秒かかります。
そこで、第一報として事前に用意しておいた「緊急地震速報チャイム」のMP3音源を即時再生し、その再生中(約20秒)に続くTTS発話(震源地・マグニチュード・深さ)を生成させることで、通知開始を最速にしています。

「緊急地震速報 MP3」でウェブ検索すると、緊急地震速報のチャイム音を無料配布しているサイトがいくつか見つかります。ダウンロードしたファイルを quake_alert.mp3 にリネームし、Raspberry Pi上のウェブサーバーの音声フォルダに設置してください。

TTS生成の待ち時間をゼロに
音源をあらかじめ用意しておくことで、EEW受信の瞬間にチャイムを鳴らすことができます。

③ マグニチュード OR 震度のOR条件で通知の取りこぼしを防ぐ

EEWの第一報(初回速報)では予測最大震度が不明(-1)の場合があります。震度だけを通知条件にしていると、第一報で通知漏れが起きる可能性があります。
そこで、通知判定を「マグニチュードが閾値以上 OR 予測最大震度が閾値以上」のOR判定にしています。震度が不明な初回速報でもマグニチュードで確実にキャッチし、震度が確定した2報目以降は震度でも判定します。

将来的には「震源の深さ」も通知条件に加えることを検討しています。浅い地震は同じマグニチュードでも揺れが大きくなる傾向があるため、より精度の高い条件設定が可能になります。ただし条件の組み合わせが複雑になるため、実装するかどうかは引き続き検討中です。

第一報でも通知漏れなし
震度が未確定の初回速報でも、マグニチュードで確実に通知をキャッチできます。

事前準備

websocketsライブラリのインストール

P2P地震情報WebSocket APIへの接続に websockets ライブラリを使用します。

pip install websockets

緊急地震速報チャイム音(MP3)の準備

「緊急地震速報 MP3」でウェブ検索し、チャイム音の音源ファイルをダウンロードします。
ダウンロードしたファイルを quake_alert.mp3 にリネームし、Raspberry Pi上のウェブサーバーの音声ファイル配置フォルダに設置してください。

Google HomeからMP3ファイルを再生するには、Raspberry Pi上でウェブサーバーが稼働しており、音声ファイルにURLでアクセスできる状態にしておく必要があります。ウェブサーバーの実装は別記事を参照してください。

実装方法

通知条件設定ファイル(conditions.json)

自宅の位置情報と通知条件を conditions.json で管理します。条件は「自宅からの震源距離(km)」ごとに「最小マグニチュード」と「最小震度スケール値」を設定します。距離内でどちらか一方を超えれば通知します。

{
    "home": {
        "name": "XX市XX区",
        "latitude": 35.xxx,
        "longitude": 139.xxx
    },
    "notify_conditions": [
        {"distance_km":  50, "min_magnitude": 5.0, "min_intensity": 40},
        {"distance_km": 100, "min_magnitude": 5.5, "min_intensity": 45},
        {"distance_km": 200, "min_magnitude": 6.5, "min_intensity": 45},
        {"distance_km": 300, "min_magnitude": 7.0, "min_intensity": 50}
    ]
}

min_intensity に設定する値は、P2P地震情報APIの maxScale フィールドの値に対応しています。下表を参考に設定してください。

震度min_intensity の値
震度110
震度220
震度330
震度440
震度5弱45
震度5強50
震度6弱55
震度6強60
震度770

設定例の読み方
例えば {"distance_km": 100, "min_magnitude": 5.5, "min_intensity": 45} は、「震源が自宅から100km以内で、かつ M5.5以上 または 予測震度5弱(スケール値45)以上のときに通知する」という意味です。

メインプログラム(earth_quake_alert.py)

# -*- coding: utf-8 -*-
__group__ = '安心安全'
__description__ = '緊急地震速報'
# ------ 全Pythonプログラム共通の宣言 ----------------------------
from pathlib import Path
import sys
PROJECT_ROOT = Path(__file__).resolve()
while PROJECT_ROOT.name != "smarthome":
    if PROJECT_ROOT.parent == PROJECT_ROOT:
        raise RuntimeError("smarthome project root not found")
    PROJECT_ROOT = PROJECT_ROOT.parent
sys.path.insert(0, str(PROJECT_ROOT))
from libs.bootstrap import Bootstrap
bootstrap = Bootstrap(__file__)
settings = bootstrap.settings
# ------------------------------------------------------------
import asyncio
import json
import math
import websockets
from influx.v2 import client as influxclient
from influx.v2 import writer as influxwriter
import googlehome
import smarthomelog

# P2P地震情報 WebSocket エンドポイント
P2PQUAKE_WS_URL = 'wss://api.p2pquake.net/v2/ws'

# 受信するEEWコード
CODE_EEW_FORECAST = 556  # 緊急地震速報(予報)
CODE_EEW_WARNING  = 554  # 緊急地震速報(警報)

# 緊急地震速報チャイム音ファイル名
QUAKE_ALERT_AUDIO_FILE = 'quake_alert.mp3'

# WebSocket切断後の再接続待機時間(秒)
RECONNECT_WAIT_SEC = 5

# 通知条件をJSONから読み込む
config = bootstrap.load_local_settings('config/conditions.json')


def _haversine_km(lat1, lon1, lat2, lon2):
    """2点間の大円距離をkmで返す(Haversine公式)"""
    R = 6371
    dlat = math.radians(lat2 - lat1)
    dlon = math.radians(lon2 - lon1)
    a = (math.sin(dlat / 2) ** 2
         + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2))
         * math.sin(dlon / 2) ** 2)
    return 2 * R * math.asin(math.sqrt(a))


# P2P地震情報APIのmaxScale値 → 震度文字列の対応表
_SCALE_LABEL = {-1: '不明', 10: '1', 20: '2', 30: '3', 40: '4',
                45: '5弱', 50: '5強', 55: '6弱', 60: '6強', 70: '7'}

def _scale_str(max_scale):
    return _SCALE_LABEL.get(max_scale, f'不明({max_scale})')


def _should_notify(dist_km, magnitude, max_scale, conditions):
    """距離内で マグニチュード OR 震度 のいずれかが閾値を超えたら通知"""
    for cond in conditions:
        if dist_km <= cond['distance_km']:
            if magnitude >= cond['min_magnitude']:
                return True
            if max_scale != -1 and max_scale >= cond['min_intensity']:
                return True
    return False


async def _process_eew(data, notified_events, config, google_living, google_child, influx, success_log):
    event_id   = data.get('issue', {}).get('eventId', '')
    is_final   = data.get('isFinal', False)
    cancelled  = data.get('cancelled', False)
    earthquake = data.get('earthquake') or {}
    hypocenter = earthquake.get('hypocenter') or {}

    # キャンセルされたEEWはログだけ残して終了
    if cancelled:
        if event_id in notified_events:
            influx.insert_execution_log(__file__, f'EEW キャンセル: eventId={event_id}')
        return

    magnitude   = hypocenter.get('magnitude', 0.0)
    depth       = hypocenter.get('depth', -1)
    region_name = hypocenter.get('name', '不明')
    latitude    = hypocenter.get('latitude', -200)
    longitude   = hypocenter.get('longitude', -200)
    max_scale   = earthquake.get('maxScale', -1)
    if max_scale is None:
        max_scale = -1
    depth_str   = f'{depth}km' if depth >= 0 else '不明'

    # 震源座標が不明な報は処理しない
    if latitude <= -100 or longitude <= -100:
        return

    home    = config['home']
    dist_km = _haversine_km(home['latitude'], home['longitude'], latitude, longitude)
    is_match = _should_notify(dist_km, magnitude, max_scale, config['notify_conditions'])

    log_prefix = (
        f'eventId:{event_id}-isFinal:{is_final}'
        f'-震源地:{region_name}-M:{magnitude}-深さ:{depth_str}'
        f'-最大震度:{_scale_str(max_scale)}-震源距離:{dist_km:.0f}km-通知:{is_match}'
    )

    # 通知条件に合致し、かつ同一eventIdで初回受信の場合に速報を通知
    if is_match and event_id not in notified_events:
        # まず緊急地震速報チャイム音を即時再生(TTSより高速)
        google_living.play_audio(QUAKE_ALERT_AUDIO_FILE)
        google_child.play_audio(QUAKE_ALERT_AUDIO_FILE)
        # 再生が終わるまで待機してから詳細をTTSで読み上げ
        await asyncio.sleep(20)
        message = (
            f'緊急地震速報です。'
            f'{home["name"]}から約{dist_km:.0f}kmの地点で大きな地震が発生しました。'
            f'身の安全を確保してください。'
            f'震源地は{region_name}、マグニチュードは{magnitude}、震源の深さは{depth_str}です。'
        )
        google_living.speak(message)
        google_child.speak(message)
        influx.insert_execution_log(__file__, log_prefix)
        influx.insert_execution_log(__file__, message)
        notified_events[event_id] = {'final_sent': False}
    else:
        influx.insert_execution_log(__file__, log_prefix)

    # 通知済みのeventIdでisFinalがTrueになった場合、確定情報を追加通知
    if event_id in notified_events and not notified_events[event_id]['final_sent'] and is_final:
        message = (
            f'先ほどの地震の情報が確定しました。'
            f'震源地は{region_name}、マグニチュードは{magnitude}、震源の深さは{depth_str}です。'
        )
        influx.insert_execution_log(__file__, message)
        google_living.speak(message)
        notified_events[event_id]['final_sent'] = True

    success_log.write('処理が正常に終了しました。', __file__)


async def _listen_eew(google_living, google_child, influx, success_log, app_err_log):
    # 同一地震イベントの重複通知を防ぐための管理辞書 {eventId: {'final_sent': bool}}
    notified_events = {}

    while True:
        try:
            async with websockets.connect(P2PQUAKE_WS_URL) as ws:
                influx.insert_execution_log(__file__, 'P2P地震情報WebSocket接続完了')
                async for raw_message in ws:
                    try:
                        data = json.loads(raw_message)
                        code = data.get('code')
                        if code in (CODE_EEW_FORECAST, CODE_EEW_WARNING):
                            await _process_eew(
                                data, notified_events, config,
                                google_living, google_child,
                                influx, success_log
                            )
                    except Exception:
                        app_err_log.write_error(sys.exc_info())

        except Exception as e:
            influx.insert_execution_log(
                __file__,
                f'WebSocket接続エラー({e})。{RECONNECT_WAIT_SEC}秒後に再接続します。'
            )

        await asyncio.sleep(RECONNECT_WAIT_SEC)


try:
    influx_client = influxclient
    influx = influxwriter.Writer()
    success_log = smarthomelog.Log(__file__, smarthomelog.LogType.SUCCESS)
    app_err_log = smarthomelog.Log(__file__, smarthomelog.LogType.APPLICATION_ERROR)

    influx.insert_execution_log(__file__, 'Start Earth Quake Alert!')

    # Google Homeを最高優先度(priority=10)で初期化
    google_living = googlehome.Notify(googlehome.GoogleHome.LIVING, priority=10)
    google_child  = googlehome.Notify(googlehome.GoogleHome.CHILD_ROOM, priority=10)

    # P2P地震情報WebSocketをリッスン(無限ループ・自動再接続)
    asyncio.run(_listen_eew(google_living, google_child, influx, success_log, app_err_log))

except Exception as e:
    log = smarthomelog.Log(__file__, smarthomelog.LogType.SYSTEM_ERROR)
    log.write_error(sys.exc_info())

プログラム中で Bootstrapgooglehomesmarthomeloginflux は自作ライブラリです。これらを使用している箇所は、環境に合わせて書き換えるか、コメントアウトしてください。

コードの解説

_haversine_km — 自宅からの震源距離を計算

Haversine(ハバーサイン)公式を使って、緯度・経度から2点間の大円距離をkmで計算します。conditions.json に設定した自宅の緯度経度と、P2P地震情報APIから取得した震源の緯度経度を渡します。

_should_notify — 通知条件のOR判定

条件リストをループし、震源が自宅から distance_km 以内であれば次の判定に進みます。マグニチュードが閾値以上、または予測最大震度(maxScale)が閾値以上のどちらかを満たせば True を返します。max_scale == -1(不明)の場合は震度条件をスキップするため、初回速報でも取りこぼしが起きません。

_process_eew — EEWの処理フロー

受信したEEWデータから震源情報を取り出し、通知条件を判定します。通知条件に合致し、かつ同一 eventId の初回受信時のみ音声通知を発報します(重複防止)。
isFinalTrue になったタイミングで確定情報を追加通知します。キャンセルされたEEWはログを残して処理を終了します。

_listen_eew — WebSocket常時接続と自動再接続

WebSocketへの常時接続と自動再接続を担います。接続が切れた場合は RECONNECT_WAIT_SEC(5秒)待機後に自動で再接続するため、Raspberry Piが動いている限り監視が継続されます。

動作確認

手動実行でWebSocket接続を確認

まずは手動で実行し、WebSocket接続が確立されることを確認します。

python earth_quake_alert.py

実行後にログへ「P2P地震情報WebSocket接続完了」が記録されれば接続成功です。

systemdで自動起動する

Raspberry Piの起動時に自動でスクリプトが立ち上がるよう、systemdのサービスとして登録します。

[Unit]
Description=Earth Quake Alert
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/<username>/projects/smarthome/core/alert/earth_quake/earth_quake_alert.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable earth-quake-alert
sudo systemctl start earth-quake-alert

まとめ

P2P地震情報のWebSocket APIとRaspberry Pi + Pythonを組み合わせることで、Google Homeから緊急地震速報を音声通知するシステムを実装しました。

  • テレビの緊急地震速報より数秒早く通知 — 放送局経由のタイムラグを省いた直接受信
  • 手元にスマートフォンがなくても家中のGoogle Homeが鳴る — 就寝中・料理中も安心
  • 通知条件をJSON1ファイルで柔軟にカスタマイズ可能 — 無駄な通知を抑えつつ必要な通知を確実に受け取れる
  • Raspberry Piが動いている限り24時間自動監視 — 人手は一切不要

通知条件の conditions.json は自宅の地域や好みに合わせて調整してください。今後は「震源の深さ」も条件に加えることで、さらに精度の高い通知システムに進化させていく予定です。

タイトルとURLをコピーしました