JSONで管理する「家族向け定時アナウンスシステム」─ 祝日API連動・音楽+音声合わせ技

Google Home

「平日の朝、子供部屋に向けて自動で起こしてほしい」「家族の誕生日に Google Home からお祝いメッセージを流したい」——そういった定時アナウンスをひとつのシステムで管理する仕組みを自作しました。
すべてのアナウンスルールは JSON ファイルで定義するため、プログラムを触らずに追加・変更ができます。また祝日 API と連動して「祝日はお知らせしない」といった制御も可能です。BGM(音楽ファイル)を流したあとに音声メッセージを続ける「音楽+音声合わせ技」にも対応しています。

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

我が家では毎朝の子供の起床が一苦労です。何度呼んでも起きてこないため、子供部屋の Google Home から自動で時刻アナウンスを流す仕組みを作ることにしました。
さらに家族の誕生日にサプライズで Google Home がお祝いを言ってくれる機能も追加したところ、家族に好評でした。
はじめはコードに直書きしていたアナウンスルールも、増えてくるたびに管理が煩雑になるため、JSON ファイルで一元管理する設計にリファクタリングしました。
祝日は平日と同じように通知してしまうと「今日は休みなのに!」となるので、祝日 API を使って自動で通知を止める仕組みも組み込んでいます。

Google Home を家族みんなのアシスタントにしたい!設定は JSON で簡単に管理できるように!

実現したいこと

  • アナウンスルール(時刻・曜日・メッセージ・音楽ファイル・場所)を JSON ファイルで一元管理する
  • 祝日 API と連動し、祝日には通知しないルールを設定できる
    • 年末年始(大晦日・三が日など)もカスタムで祝日扱いにできる
  • BGM(音楽ファイル)を再生してから、続けて音声メッセージを発話する「合わせ技」ができる
  • メッセージ文中に現在の年・月・日・時・分を自動で埋め込んだり、計算式で年齢を算出したりできる
  • リビング・子供部屋・Nest Hub など、場所ごとに異なる Google Home に発話させられる
  • Raspberry Pi 上で systemd サービスとして常時稼働する

この記事でわかること

  • JSON ファイルでアナウンスルールを定義・管理する方法
  • 祝日 API(holidays-jp)から祝日一覧を取得し、年末年始なども祝日扱いにする方法
  • fnmatch のワイルドカードで「毎年〇月〇日」「平日の毎分」などの日時パターンにマッチさせる方法
  • pydub で音楽ファイルの再生秒数を取得して、音楽→音声の順に正確なタイミングで流す方法
  • メッセージ文中の変数(年・月・日など)を動的に置換し、eval() で計算式を展開する方法
  • systemd サービスとして登録し Raspberry Pi 起動時から自動稼働させる方法

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

ハードウェア
  • ミニPC(Ubuntu) or Raspberry Pi(例:Raspberry Pi 5)
  • Google Home (Nest)(リビング・子供部屋・Nest Hub など複数台でも可)
ソフトウェア/サービス
  • Python
    • requests
    • pydub(音楽ファイルの再生時間取得に使用)
    • fnmatch(標準ライブラリ:ワイルドカード日時マッチング)
    • datetime / time(標準ライブラリ)
  • Google Home 発話ライブラリ(自作:googlehome.py)
    • 詳細は別記事を参照
  • 祝日 API:holidays-jp.github.io(無料・登録不要)
  • systemd(常駐サービス化)

完成イメージ

JSON ファイルにルールを追加するだけで、様々なアナウンスが実現できます。実際に我が家で動いているルールの例を紹介します。

  • 毎朝7時20分〜(平日のみ):子供部屋の Google Home から BGM が流れ、続けて「7時20分になりました。早く起きてください。」と音声アナウンス
  • 7時30分以降〜9時台まで(平日のみ):1分ごとに「〇時〇分になりました」と子供部屋にアナウンス(祝日はスキップ)
  • 毎晩21時(毎日):リビングの Google Home が「9時になりました」とアナウンス(祝日も通知)
  • 家族の誕生日(毎年):0時ちょうどにリビングで「〇〇さん!〇〇歳のお誕生日おめでとうございます!」とアナウンス(年齢は自動計算)

システムの仕組み

プログラムは約0.9秒ごとにループして現在時刻を取得し、messages.json に定義された全ルールと照合します。
日時パターンのマッチングには fnmatch のワイルドカードを使用しているため、「毎年5月27日」「平日の毎分」のような柔軟なパターンが設定できます。

  1. 祝日データの取得:起動時に祝日 API から当年前後の祝日一覧を取得し、年末年始もカスタム追加する
  2. メインループ:0.9秒ごとに現在日時と全ルールを照合する
  3. 条件チェック:日時パターン・曜日・祝日フラグの3条件がすべて合致した場合にアナウンスを実行する
  4. アナウンス実行:音楽ファイルがあれば先に再生 → 再生時間だけ待機 → 音声メッセージを発話する

実装のポイント

fnmatch によるワイルドカード日時マッチング

現在日時を文字列(YYYY/MM/DD HH:MM:SS)に変換し、JSON の datetime パターンと fnmatch でマッチングします。
* が任意の文字列に対応するため、下記のような直感的なパターンが使えます。

  • */05/21 00:00:00 → 毎年5月21日の0時0分0秒
  • *07:20:00 → 任意の日付の7時20分0秒
  • *08:*:00 → 任意の日付の8時台の毎分0秒(1分ごとに発火)

シンプルなパターン設計
cron 構文より直感的で、ファイルを見ればいつ鳴るかがひと目でわかる。日付・時刻の一部だけ固定して残りをワイルドカードにする柔軟な設計が可能

同一パターンが複数秒にマッチする可能性がある
ループ間隔が0.9秒のため、*07:20:00 のように秒まで指定していれば1回だけ発火するが、ワイルドカードが広いパターンでは意図せず複数回発火する可能性があるため注意が必要

祝日 API + 年末年始のカスタム対応

holidays-jp.github.io の API は登録不要・無料で利用でき、日本の祝日一覧を JSON 形式で返してくれます。
ただし年末年始(12/29〜1/3)は法定祝日ではないため、API には含まれません。そこでプログラム側で手動追加しています。

毎年自動で祝日が更新される
API から毎回取得するため、来年以降の祝日定義もメンテナンス不要

音楽ファイル → 音声メッセージの合わせ技

BGM を流してから音声メッセージを続けるには、BGM の再生時間ぴったりだけ待機する必要があります。
pydub を使って音楽ファイルの再生秒数を取得し、time.sleep(audio_play_seconds + 0.1) で待機してから発話します。

BGM が終わった直後に音声が始まる自然な演出
ハードコードの待機秒数ではなく、音楽ファイルの実際の長さをプログラムが取得して待機するため、音楽ファイルを入れ替えても自動的に対応できる

メッセージ文中の動的変数・計算式

メッセージ文中の {YEAR}{MONTH}{HOUR} などを現在日時の値に置換したあと、残った {式} 形式の文字列を eval() で計算します。
これにより誕生日の年齢計算({{YEAR}-1990}35 など)が自動で行えます。

eval() は JSON ファイルに書いた任意の式を実行します。JSON は自分だけが編集できる環境で使うことを前提とし、外部から入力を受け取る仕組みには使わないようにしてください。

事前準備

必要なライブラリのインストール

pip install requests pydub

pydub で mp3 ファイルを扱うには ffmpeg が必要です。

sudo apt install ffmpeg

音楽ファイルの準備

Google Home に再生させる BGM ファイル(mp3 形式)を、Nginx の音声ファイル配信ディレクトリに配置します。
ファイルが不要な場合は JSON の audiofilenull にすればスキップされます。

Google Home 発話ライブラリの準備

このプログラムでは自作の googlehome.py ライブラリを使用して Google Home に発話させます。ライブラリの準備については別記事を参照してください。

実装方法

下記のフォルダ構成でプログラムと設定ファイルを配置します。

smarthome/
└── core/
    └── notify/
        └── time_rule/
            ├── config/
            │   └── messages.json       # アナウンスルール定義ファイル
            └── time_rule_notice.py     # メインプログラム(常駐サービス)

アナウンスルール設定ファイルを作成

各アナウンスルールを JSON オブジェクトの配列で定義します。各フィールドの意味は後述のコード解説を参照してください。

[
    {"datetime":"*/05/21 00:00:00","weekdays":"1234567","message":"<名前>さん!{{YEAR}-<生まれ年>}歳のお誕生日おめでとうございます!","audiofile":null,"notify_on_holidays":true,"place":"LIVING"},
    {"datetime":"*/11/23 00:00:00","weekdays":"1234567","message":"<名前>さん!{{YEAR}-<生まれ年>}歳のお誕生日おめでとうございます!","audiofile":null,"notify_on_holidays":true,"place":"LIVING"},
    {"datetime":"*07:20:00","weekdays":"23456","message":"{HOUR}時{MINUTE}分になりました。早く起きてください。","audiofile":"ohayo_music.mp3","notify_on_holidays":false,"place":"CHILD_ROOM"},
    {"datetime":"*07:30:00","weekdays":"23456","message":"{HOUR}時{MINUTE}分になりました。","audiofile":null,"notify_on_holidays":false,"place":"LIVING;CHILD_ROOM"},
    {"datetime":"*08:*:00","weekdays":"23456","message":"{HOUR}時{MINUTE}分になりました。","audiofile":null,"notify_on_holidays":false,"place":"CHILD_ROOM"},
    {"datetime":"*21:00:00","weekdays":"1234567","message":"9時になりました。","audiofile":null,"notify_on_holidays":true,"place":null}
]

各フィールドの説明は下記の通りです。

  • datetime:発火する日時パターン。* はワイルドカード。形式は YYYY/MM/DD HH:MM:SS
  • weekdays:発火する曜日(1=日・2=月・3=火・4=水・5=木・6=金・7=土)を文字列で列挙
  • message:発話するメッセージ。{YEAR}{MONTH}{DAY}{HOUR}{MINUTE} が使用可能。null なら発話しない
  • audiofile:先に再生する BGM ファイル名。null なら BGM なし
  • notify_on_holidaystrue なら祝日でも通知、false なら祝日はスキップ
  • place:発話する場所。LIVINGCHILD_ROOMNEST_HUB; 区切りで複数指定可能。null はリビングのみ

メインプログラムを作成

# -*- coding: utf-8 -*-
import sys
import os
import requests
import json
import time
import datetime
import fnmatch
import googlehome
from pydub import AudioSegment

# Google Home の初期設定
google_notify_living   = googlehome.Notify(googlehome.GoogleHome.LIVING)
google_notify_childroom = googlehome.Notify(googlehome.GoogleHome.CHILD_ROOM, volume_gain_db=16)
google_notify_nesthub  = googlehome.Notify(googlehome.GoogleHome.NEST_HUB)

# アナウンスルールを読み込む
with open('config/messages.json', 'r') as f:
    messages = json.load(f)

# 音声ファイルディレクトリ
AUDIO_FILE_DIR_PATH = '/home/<username>/projects/smarthome/webserver/audio/'

# 祝日 API から祝日一覧を取得
response_holidays = requests.get('https://holidays-jp.github.io/api/v1/date.json')
holidays = response_holidays.json()

# 年末年始(法定祝日外)を手動で追加
year = datetime.datetime.now().year
for y in [year - 1, year, year + 1]:
    holidays[f'{y}-01-02'] = '三が日'
    holidays[f'{y}-01-03'] = '三が日'
    holidays[f'{y}-12-29'] = '年末休暇'
    holidays[f'{y}-12-30'] = '年末休暇'
    holidays[f'{y}-12-31'] = '大晦日'

def is_holiday(date_str_yyyymmdd: str) -> str | None:
    """指定した日付が祝日なら祝日名を返す。祝日でなければ None を返す。"""
    for holiday_date, holiday_name in holidays.items():
        if holiday_date.replace('-', '/') == date_str_yyyymmdd:
            return holiday_name
    return None

def replace_variables(message: str) -> str:
    """メッセージ内の {YEAR} などの変数を現在日時の値に置換する。"""
    now = datetime.datetime.now()
    message = message.replace('{YEAR}',   now.strftime('%Y'))
    message = message.replace('{MONTH}',  now.strftime('%-m'))
    message = message.replace('{DAY}',    now.strftime('%-d'))
    message = message.replace('{HOUR}',   now.strftime('%-H'))
    message = message.replace('{MINUTE}', now.strftime('%-M'))
    message = message.replace('{SECOND}', now.strftime('%-S'))
    # {} に囲まれた計算式を eval で展開(例:{2026-1980} → 46)
    while '{' in message and '}' in message:
        l = message.find('{')
        r = message.find('}')
        formula = message[l+1:r]
        message = message[:l] + str(eval(formula)) + message[r+1:]
    return message

def speak(message: str, place: str | None):
    """指定した場所の Google Home に発話させる。"""
    if place is None or 'LIVING' in place:
        google_notify_living.speak(message)
    if place is not None and 'CHILD_ROOM' in place:
        google_notify_childroom.speak(message)
    if place is not None and 'NEST_HUB' in place:
        google_notify_nesthub.speak(message)

def play_audio(audiofile: str, place: str | None):
    """指定した場所の Google Home で BGM を再生する。"""
    if place is None or 'LIVING' in place:
        google_notify_living.play_audio(audiofile)
    if place is not None and 'CHILD_ROOM' in place:
        google_notify_childroom.play_audio(audiofile)
    if place is not None and 'NEST_HUB' in place:
        google_notify_nesthub.play_audio(audiofile)

# --- メインループ ---
while True:
    time.sleep(0.9)  # 約0.9秒ごとにチェック

    now = datetime.datetime.now()
    now_str = now.strftime('%Y/%m/%d %H:%M:%S')

    # 曜日を変換(1=日・2=月・3=火・4=水・5=木・6=金・7=土)
    weekday = now.weekday()  # Python: 月=0, ..., 日=6
    weekday = 1 if weekday == 6 else weekday + 2

    for message in messages:
        # 日時パターンと曜日をチェック
        if not fnmatch.fnmatch(now_str, message['datetime']):
            continue
        if str(weekday) not in message['weekdays']:
            continue

        # 祝日チェック
        if not message['notify_on_holidays']:
            holiday_name = is_holiday(now_str[:10])
            if holiday_name is not None:
                print(f'祝日({holiday_name})のためスキップ')
                continue

        # BGM を再生(audiofile が指定されている場合)
        if message['audiofile'] is not None:
            audio = AudioSegment.from_file(AUDIO_FILE_DIR_PATH + message['audiofile'])
            audio_seconds = len(audio) / 1000
            play_audio(message['audiofile'], message['place'])

            # メッセージもある場合は BGM が終わるまで待機
            if message['message'] is not None:
                time.sleep(audio_seconds + 0.1)

        # 音声メッセージを発話
        if message['message'] is not None:
            speaking_message = replace_variables(message['message'])
            speak(speaking_message, message['place'])
            time.sleep(0.1)  # 連続発話防止

systemd サービスとして登録する

[Unit]
Description=Regular Notice
After=network.target

[Service]
ExecStart=/home/<username>/Projects/venv/bin/python /home/<username>/Projects/smarthome/core/notify/time_rule/time_rule_notice.py
Restart=always
RestartSec=60
Type=simple
User=<username>

[Install]
WantedBy=multi-user.target
sudo systemctl enable smh-time-rule-notice.service
sudo systemctl start smh-time-rule-notice.service
sudo systemctl status smh-time-rule-notice.service

コードの解説

fnmatch によるワイルドカード日時マッチング

現在日時を YYYY/MM/DD HH:MM:SS 形式の文字列に変換し、JSON の datetime パターンと fnmatch.fnmatch() でマッチさせます。
* が「0文字以上の任意の文字列」に対応するため、cron のような複雑な構文なしに直感的なパターンが書けます。

now_str = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
# 例:"2026/05/21 00:00:00"

# パターン "*/05/21 00:00:00" → 毎年5月21日0時0分0秒にマッチ
# パターン "*07:20:00"        → 任意の日付の7時20分0秒にマッチ
# パターン "*08:*:00"         → 任意の日付の8時台の毎分0秒にマッチ
if fnmatch.fnmatch(now_str, message['datetime']):
    ...

曜日の変換ロジック

Python の weekday() は月曜を 0、日曜を 6 で返します。JSON の weekdays フィールドでは「1=日・2=月・…・7=土」と定義しているため、下記の変換をしています。

weekday = now.weekday()          # 月=0, 火=1, ..., 土=5, 日=6
weekday = 1 if weekday == 6 else weekday + 2
# 月=2, 火=3, 水=4, 木=5, 金=6, 土=7, 日=1

誕生日メッセージの年齢自動計算

{YEAR} が現在の西暦に置換されたあと、{YEAR-<生まれ年>} のような式が eval() で計算されます。
JSON に生まれ年を書いておくだけで、毎年正しい年齢が自動でアナウンスされます。

{"message": "<名前>さん!{{YEAR}-<生まれ年>}歳のお誕生日おめでとうございます!"}

メッセージ処理の流れ:

  1. {YEAR}2026 に置換
  2. {2026-<生まれ年>}eval("2026-<生まれ年>")36 などに置換
  3. 結果:<名前>さん!36歳のお誕生日おめでとうございます!

音楽ファイル再生時間の取得と待機

BGM の再生秒数を pydub で取得し、その秒数だけ待機してから音声メッセージを流します。
音楽ファイルを入れ替えてもコードの変更は不要です。

audio = AudioSegment.from_file(AUDIO_FILE_DIR_PATH + message['audiofile'])
audio_seconds = len(audio) / 1000  # ミリ秒 → 秒

play_audio(message['audiofile'], message['place'])

# BGM 終了後に音声メッセージを発話する場合は待機
if message['message'] is not None:
    time.sleep(audio_seconds + 0.1)

動作確認

サービス起動後、ログでアナウンスの実行状況を確認します。

sudo journalctl -u smh-time-rule-notice.service -f

テスト時は messages.jsondatetime パターンを現在時刻の直近に設定して動作を確認するのが便利です。
例えば現在が 2026/05/24 15:30:00 なら、下記のように設定すれば今すぐ動作確認できます。

{"datetime":"*15:31:00","weekdays":"1234567","message":"テストです。{HOUR}時{MINUTE}分です。","audiofile":null,"notify_on_holidays":true,"place":null}

設定変更後はサービスを再起動しないと反映されません。sudo systemctl restart smh-time-rule-notice.service を実行してください。また祝日データはサービス起動時に1回だけ取得するため、年をまたぐ場合はサービスを再起動して祝日データを更新してください。

まとめ

この記事では、JSON ファイルでアナウンスルールを一元管理する「家族向け定時アナウンスシステム」を実装しました。

  • fnmatch のワイルドカードで「毎年〇月〇日」「平日の毎分」などの柔軟な日時パターンを設定
  • 祝日 API + 年末年始カスタム追加で「祝日は通知しない」を自動制御
  • pydub で BGM の再生秒数を取得し、音楽→音声の自然な合わせ技を実現
  • メッセージ内の変数・計算式で誕生日の年齢を自動算出
  • JSON を編集するだけでルールを追加・変更できるのでコードに触れる必要がない

一度セットアップすれば毎年の誕生日も平日朝の起こしアナウンスも全自動で動き続けます。JSON にルールを追加するだけで簡単に機能を拡張できるので、ぜひお試しください。

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