はじめに
ポケモン30周年だし、ポケモン目覚まし時計を作ろうと思った。
なぜ目覚ましなのかというと、普段はスマホのアラームを使っているんだけど、アラームは曜日で設定できるんだけど、祝日も早い時間に起こしてくるので、困っていた。
目覚まし時計アプリも軽く見てみたけど、同様だった。
だから、自分でカスタマイズできる目覚まし時計を作ろうと思った。
完成したものが実際に動いている様子はXへ投稿したので、良ければ見ていただければと思います。
ハードウェア側
まずは機材を用意しないといけない。
使用する機材は以下。
- ラズパイ
- 今回はRaspberry Pi Zero Wを使用。
- micro usb (32G byte)
- 家にあったもの
- 棚に設置するコンセント
- ニトリで購入(棚に目覚まし時計を置くと、コードが棚から出てきて煩わしいので購入した)
- スピーカー
- ラズパイの周辺機器セット(必要ならば)
- ラズパイとスピーカーを接続させるための変換器
- type BとUSBの接続ケーブル
- 家にあったもの
僕が購入したAmazonのリンク先も参考に載せておきます👇
●Raspberry Pi Zero W
●棚に設置するコンセント(こちらはニトリで購入)
●スピーカー
●ラズパイの周辺機器セット(必要であれば)
こちらは無くても実装はできましたが、あっても損ではないと思います。特に画面に接続して操作したい場合は重宝する。
●ラズパイとスピーカーを接続させるための変換器
セッティングする
osを入れる。
osの入れ方はググると結構出てくるので、そちらを参考にしてほしい。
osを入れるときは、Raspberry Pi Zeroの項目とRaspberry Pi Zero 2Wの項目があるので、間違えないように注意。
今回の場合だとRaspberry Pi Zero Wを使用するのでRaspberry Pi ZeroのOSを選択する。
osを入れる時にwifiの設定などをしておくと後で楽だと思う。

セッティング自体は結構単純で、スピーカを接続するだけ。
スピーカーの真ん中にテラリウムピカチュウを置くと以下のようになり、完成する。

プログラムを組む
普段ラズパイの中でプログラミングや作業をするときはvs codeでssh接続をする。
しかし、今回使用するRaspberry Pi Zero Wはvs codeからsshで接続できないようで、vs code上でプログラムのコーディングができない。
仕方が無いので、ローカルPCでコーディングをしたのち、scpでファイル群をUploadする流れで対応する。
その他、困ったところとしてはclaude cliも入れることができないことだ。
デバッグの時間の短縮はありがたいので、claude cliが入れられないのは困った。
これも、基本はローカルPCで相談する形になる。
python
pythonのコードは以下のようになる。
#!/usr/bin/env python3
"""CLI alarm clock for Raspberry Pi Zero (mpg123 + USB speaker).
Distinguishes weekday / weekend / holiday(JP) alarm times, and plays a
different sound per day of the week.
"""
import argparse
import json
import os
import signal
import subprocess
import sys
import time
from datetime import datetime
import jpholiday
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
PID_FILE = "/tmp/alarm_clock.pid"
DEFAULT_SOUND = os.path.join(BASE_DIR, "sounds", "alarm.mp3")
DAY_NAMES = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
CATEGORIES = ["weekday", "weekend", "holiday"]
DEFAULT_CONFIG = {
"schedules": {
"weekday": {"time": "07:00", "enabled": True},
"weekend": {"time": "09:00", "enabled": True},
"holiday": {"time": "09:00", "enabled": True},
},
"sounds": {day: DEFAULT_SOUND for day in DAY_NAMES},
}
def load_config():
if not os.path.exists(CONFIG_FILE):
return json.loads(json.dumps(DEFAULT_CONFIG))
with open(CONFIG_FILE, "r") as f:
config = json.load(f)
# backfill any missing keys (safe for older config files)
for cat in CATEGORIES:
config["schedules"].setdefault(cat, DEFAULT_CONFIG["schedules"][cat])
for day in DAY_NAMES:
config["sounds"].setdefault(day, DEFAULT_SOUND)
return config
def save_config(config):
with open(CONFIG_FILE, "w") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
def category_for_date(d):
if jpholiday.is_holiday(d):
return "holiday"
return "weekday" if d.weekday() < 5 else "weekend"
def cmd_set(args):
if args.category not in CATEGORIES:
print(f"category must be one of {CATEGORIES}")
sys.exit(1)
try:
datetime.strptime(args.time, "%H:%M")
except ValueError:
print("time must be HH:MM")
sys.exit(1)
config = load_config()
config["schedules"][args.category]["time"] = args.time
save_config(config)
print(f"{args.category} alarm set to {args.time}")
def cmd_toggle(args, enabled):
if args.category not in CATEGORIES:
print(f"category must be one of {CATEGORIES}")
sys.exit(1)
config = load_config()
config["schedules"][args.category]["enabled"] = enabled
save_config(config)
print(f"{args.category} alarm {'enabled' if enabled else 'disabled'}")
def cmd_sound(args):
if args.day not in DAY_NAMES:
print(f"day must be one of {DAY_NAMES}")
sys.exit(1)
config = load_config()
config["sounds"][args.day] = args.path
save_config(config)
print(f"{args.day} sound set to {args.path}")
def cmd_show(args):
config = load_config()
print("schedules:")
for cat in CATEGORIES:
s = config["schedules"][cat]
status = "ON" if s["enabled"] else "off"
print(f" {cat:8s} {s['time']} [{status}]")
print("sounds:")
for day in DAY_NAMES:
print(f" {day}: {config['sounds'][day]}")
today = datetime.now().date()
print(f"today ({today}) category: {category_for_date(today)}")
def cmd_stop(args):
if not os.path.exists(PID_FILE):
print("no alarm currently sounding")
return
with open(PID_FILE) as f:
pid = int(f.read().strip())
try:
os.killpg(os.getpgid(pid), signal.SIGTERM)
print("alarm stopped")
except ProcessLookupError:
print("alarm process already gone")
os.remove(PID_FILE)
def play_sound(sound_path):
proc = subprocess.Popen(
["mpg123", "--loop", "-1", sound_path],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
preexec_fn=os.setsid,
)
with open(PID_FILE, "w") as f:
f.write(str(proc.pid))
return proc
def cmd_run(args):
print("alarm loop started, checking every 20s. Ctrl+C to quit.")
fired_this_minute = set()
while True:
now = datetime.now()
current_time = now.strftime("%H:%M")
minute_key = now.strftime("%Y%m%d%H%M")
category = category_for_date(now.date())
day_name = DAY_NAMES[now.weekday()]
config = load_config()
schedule = config["schedules"][category]
if schedule["enabled"] and schedule["time"] == current_time:
key = (category, minute_key)
if key not in fired_this_minute:
fired_this_minute.add(key)
sound = config["sounds"][day_name]
print(f"[{now}] firing {category} alarm, playing {sound}")
play_sound(sound)
fired_this_minute = {k for k in fired_this_minute if k[1] == minute_key}
time.sleep(20)
def main():
parser = argparse.ArgumentParser(description="Raspberry Pi alarm clock")
sub = parser.add_subparsers(dest="command", required=True)
p_set = sub.add_parser("set", help="set alarm time for a category")
p_set.add_argument("category", choices=CATEGORIES)
p_set.add_argument("time", help="HH:MM 24h format")
p_set.set_defaults(func=cmd_set)
p_enable = sub.add_parser("enable", help="enable a category's alarm")
p_enable.add_argument("category", choices=CATEGORIES)
p_enable.set_defaults(func=lambda args: cmd_toggle(args, True))
p_disable = sub.add_parser("disable", help="disable a category's alarm")
p_disable.add_argument("category", choices=CATEGORIES)
p_disable.set_defaults(func=lambda args: cmd_toggle(args, False))
p_sound = sub.add_parser("sound", help="set the sound file for a day of week")
p_sound.add_argument("day", choices=DAY_NAMES)
p_sound.add_argument("path", help="path to mp3")
p_sound.set_defaults(func=cmd_sound)
p_show = sub.add_parser("show", help="show current config")
p_show.set_defaults(func=cmd_show)
p_stop = sub.add_parser("stop", help="stop a currently sounding alarm")
p_stop.set_defaults(func=cmd_stop)
p_run = sub.add_parser("run", help="run the alarm checking loop (foreground)")
p_run.set_defaults(func=cmd_run)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()大まかな機能としては、以下である。
- 平日・土日・祝日でアラーム時刻を変えられる
- 曜日ごとに再生する音楽を変えられる
主に使われる関数を幾つか紹介する。
load_config() 関数
設定ファイル(config.json)を読み込む関数。
save_config() 関数
configファイルを保存する関数。
例えば
$ alarm_clock.py set weekday 06:30と実行すると、
{
"weekday":{
"time":"06:30"
}
}が保存される。
category_for_date() 関数
ある日付が平日、土日、祝日のどれか判定する。
例えば2026/1/1なら「holiday」になる。
cmd_sound() 関数
例えば、
$ alarm_clock.py sound mon music.mp3を実行すると、月曜日だけmusic.mp3にできる。
play_sound() 関数
ここで実際に音を鳴らしている。
mpg123 --loop -1 alarm.mp3をsubprocessを使って実行している。
最後に
次回は実際に動かしてみる。
