NASを使った家庭内Dashboard構築

3/23/2026

AI Gadget Python

連休を使って ゲーム作成(前回)の続きを進めようと思っていたが、色々思いついたことをAIへ相談しているうちに家庭内Dashboardを作成することにした。最初は家計簿情報の有効活用として支出状況を共有しようとした際にNASが使えないか調べたところ、使用しているSynology製品のWeb機能を使って思ったよりも簡単に構築することが出来た。

ChatGPTで生成
(今回の作業アシスタントGeminiが画像生成だけ調子が悪かったため、この画像だけChatGPT使用)

使用したツール・環境

  • Excel
  • Python
  • Synology DS218Play(NAS)
  • SwitchBot(温度計)
家計簿データは元々エクセルで管理しており、今回はDashboard掲載用画像を保存するためにVBAを追加した。PythonのスクリプトをNASで定期的に実行して温度を取得

Excelダッシュボード作成

家計簿用のデータは日常的に支出情報を入力しているExcelファイル内で画像として作成し、NASへの保存をVBAで自動的に行う仕様とした。Excelファイルを保存すると、画像生成と保存を自動的に実行し、NAS側ではHTMLで画像を表示させるだけのお手軽な仕組み。

Excel Dashboard

家計簿データをExcelのグラフ&テキストボックスの組み合わせで作成。Power BIの使用やJavaScript等でもっと上手くWebページに表示させる方法もありそうだが、ひとまずDashboardの形を手っ取り早く作ることを優先。

Excelのグラフとテキストボックスのみで作成

Dashboardの画像化

作成したDashboardが含まれるセルを選択した状態にして、通常はセル番号が表示されている「名前ボックス」に名称を入力。今回は支出ダッシュボードということで「ExpDashboard」という名称にした。VBAが画像を生成する際に場所を指定するために使用する。

セル番号の代わりにこの名称で範囲指定できる
VBAは以下の4点。保存された際に自動的に実行される内容と、画像保存はExpDashboard領域と年間集計のExpDash_Year領域の保存、保存実行中のステータスバーのクリア用関数を作成。
あくまでExcel内の領域を保存するため、Excelのズーム設定を保存するときだけ強制的に100%に設定して画像サイズが毎回同じになるように工夫。

ThisWorkbookへ記述:

Private Sub Workbook_AfterSave(ByVal Success As Boolean)
    If Not Success Then Exit Sub
    
    Dim ws As Worksheet: Set ws = ThisWorkbook.Sheets("Dashboard")
    Dim currentSheet As Worksheet: Set currentSheet = ActiveSheet ' 今のシートを記憶
    Dim currentYear As String: currentYear = Format(Date, "yyyy")
    
    ipaddr = "xxx.xxx.xxx.xxx"
    
    ' --- 開始の合図 ---
    Application.DisplayStatusBar = True
    Application.StatusBar = " NASポータル更新中...(クエリ更新・画像出力)"
    
    ' --- 1. 実行判定 ---
    ' ファイル名に「現在の年」が含まれていないなら終了
    If InStr(ThisWorkbook.Name, currentYear) = 0 Then Exit Sub
    
    ' Z2ストッパー(ボタン実行時)なら、合図を消して終了
    If ws.Range("Z2").Value <> "" Then
        ws.Range("Z2").ClearContents
        Exit Sub
    End If

    ' --- 1. クエリ更新 ---
    ' クエリのバックグラウンド更新はOFFにしておくこと
    Application.StatusBar = "データを最新に更新しています..."
    On Error Resume Next
    ThisWorkbook.RefreshAll
    DoEvents ' 描画を一度許容してステータスバーを反映
    On Error GoTo 0
    
    ' --- 2. ズーム制御とシート切り替え ---
    Dim currentZoom As Integer
    Application.ScreenUpdating = False
    ' Dashboardシートを一瞬だけアクティブにしてズーム設定
    ws.Activate
    currentZoom = ActiveWindow.Zoom ' Dashboardの元のズームを記憶
    ActiveWindow.Zoom = 100         ' Dashboardを100%に
    
    ' --- 3. 画像出力 ---
    Application.StatusBar = "NASへ画像を転送しています..."
    ' 日付セット
    ws.Range("B1").Value = DateSerial(Year(Date), Month(Date), 1)
    
    ' 最新月(ExpDashboard.jpg)の出力
    Dim mainPath As String
    mainPath = "\\" & ipaddr & "\web\ExpDashboard.jpg"
    ExportRangeToJpg ws.Range("ExpDashboard"), mainPath
    
    ' 年間集計(Year_2026.jpgなど)の自動出力
    ' ファイル名に基づいた年間アーカイブもついでに更新する
    Dim yearlyPath As String
    yearlyPath = "\\" & ipaddr & "\web\archive\" & currentYear & "_Total.jpg"
    ExportRangeToJpg ws.Range("ExpDash_Year"), yearlyPath

    ' --- 4. 後片付け ---
    ActiveWindow.Zoom = currentZoom ' Dashboardのズームを戻す
    currentSheet.Activate           ' 元々作業していたシートに戻る
    
    ' --- 完了の合図 ---
    Application.StatusBar = "NASポータルの更新が完了しました!"
    Application.ScreenUpdating = True
    
    ' 3秒後にステータスバーをクリアする
    Application.OnTime Now + TimeValue("00:00:03"), "ClearStatusBar"
End Sub


' --- 汎用出力関数(コードをスッキリさせるため分離) ---
Sub ExportRangeToJpg(targetRange As Range, fullPath As String)
    Dim chartObj As ChartObject
    targetRange.CopyPicture Appearance:=xlScreen, Format:=xlPicture
    DoEvents
    Set chartObj = targetRange.Worksheet.ChartObjects.Add(0, 0, targetRange.Width, targetRange.Height)
    With chartObj
        .Select
        .Chart.Paste
        Dim startTime As Double: startTime = Timer
        Do While Timer < startTime + 0.3: DoEvents: Loop
        .Chart.Export fileName:=fullPath, FilterName:="JPG"
        .Delete
    End With
End Sub

標準モジュール.Module1: 月間ダッシュボード保存用関数

Sub save_monthly()
    Dim ws As Worksheet: Set ws = ThisWorkbook.Sheets("Dashboard")
    Dim targetRange As Range: Set targetRange = ws.Range("ExpDashboard")
    Dim targetDate As Date
    Dim fileName As String
    Dim fullPath As String
    Dim currentZoom As Integer
    Dim chartObj As ChartObject

    ' 1. 保存する年月をB1セル等から取得(なければ入力させる)
    On Error Resume Next
    targetDate = ws.Range("B1").Value
    On Error GoTo 0

    If targetDate = 0 Or Not IsDate(targetDate) Then
        Dim res As String
        res = InputBox("保存する年月を入力してください(例:2026/2)", "月次アーカイブ保存")
        If res = "" Or Not IsDate(res) Then Exit Sub
        targetDate = CDate(res)
    End If

    ' 2. 保存先パスの作成
    fileName = Format(targetDate, "yyyy_mm") & ".jpg"
    fullPath = "\\xxx.xxx.xxx.xxx\web\archive\" & fileName

    ' 3. 【重要】自動保存(ThisWorkbook側)をブロックする合図を書き込む
    ws.Range("Z2").Value = "STOP"

    ' 4. 画像の書き出し処理
    Application.ScreenUpdating = False
    
    ' ズームを100%に固定
    currentZoom = ActiveWindow.Zoom
    ActiveWindow.Zoom = 100

    targetRange.CopyPicture Appearance:=xlScreen, Format:=xlPicture
    DoEvents
    
    Set chartObj = ws.ChartObjects.Add(0, 0, targetRange.Width, targetRange.Height)
    With chartObj
        .Select
        .Chart.Paste
        
        Dim startTime As Double: startTime = Timer
        Do While Timer < startTime + 0.5: DoEvents: Loop
        
        .Chart.Export fileName:=fullPath, FilterName:="JPG"
        .Delete
    End With
    
    ' ズームを元に戻す
    ActiveWindow.Zoom = currentZoom
    
    ' 5. Excel自体を上書き保存(これによりZ2ストッパーが作動して最新版更新がスルーされる)
    ThisWorkbook.Save
    
    Application.ScreenUpdating = True
    MsgBox fileName & " としてアーカイブ保存が完了しました!"
End Sub

標準モジュール.Module2: 年間集計ダッシュボード保存用関数

Sub save_year()
    Dim ws As Worksheet: Set ws = ThisWorkbook.Sheets("Dashboard")
    Dim targetRange As Range: Set targetRange = ws.Range("ExpDash_Year") ' 年間用範囲
    Dim targetDate As Date
    Dim fileName As String
    Dim fullPath As String
    Dim currentZoom As Integer
    Dim chartObj As ChartObject

    ' 1. 保存する「年」をB1セル等から取得(なければ入力させる)
    On Error Resume Next
    targetDate = ws.Range("B1").Value
    On Error GoTo 0

    If targetDate = 0 Or Not IsDate(targetDate) Then
        Dim res As String
        res = InputBox("対象の年を入力してください(例:2025)", "年間アーカイブ保存")
        ' 数字のみ(2025)と入力された場合への対策
        If IsNumeric(res) And Len(res) = 4 Then res = res & "/1/1"
        
        If res = "" Or Not IsDate(res) Then Exit Sub
        targetDate = CDate(res)
    End If

    ' 2. 保存先パスの作成 (例: 2026_Total.jpg)
    fileName = Format(targetDate, "yyyy") & "_Total.jpg"
    fullPath = "\\xxx.xxx.xxx.xxx\web\archive\" & fileName

    ' 3. 【重要】自動保存(ThisWorkbook側)をブロックする合図を書き込む
    ws.Range("Z2").Value = "ARCHIVING"

    ' 4. 画像の書き出し処理
    Application.ScreenUpdating = False
    
    ' ズームを100%に固定
    currentZoom = ActiveWindow.Zoom
    ActiveWindow.Zoom = 100

    ' 年間用範囲をコピー
    targetRange.CopyPicture Appearance:=xlScreen, Format:=xlPicture
    DoEvents
    
    ' グラフを年間範囲のサイズで作成
    Set chartObj = ws.ChartObjects.Add(0, 0, targetRange.Width, targetRange.Height)
    With chartObj
        .Select
        .Chart.Paste
        
        Dim startTime As Double: startTime = Timer
        Do While Timer < startTime + 0.5: DoEvents: Loop
        
        .Chart.Export fileName:=fullPath, FilterName:="JPG"
        .Delete
    End With
    
    ' ズームを元に戻す
    ActiveWindow.Zoom = currentZoom
    
    ' 5. Excel自体を上書き保存(Z2ストッパー作動)
    ThisWorkbook.Save
    
    Application.ScreenUpdating = True
    MsgBox fileName & " として年間アーカイブの保存が完了しました!"
End Sub
A

標準モジュール.Module3: ステータスバーのクリア関数

Sub ClearStatusBar()
    Application.StatusBar = False ' ステータスバーをデフォルト(準備完了)に戻す
End Sub


当月画像:    ExpDashboard.jpg
先月までの月別:    archive\yyyy_mm.jpg
昨年までの年間集計: archive\yyyy_Total.jpg

Dashboard用の画像作成のためか以前よりExcelファイル動作が重くなった印象。入力するBookと画像処理用Bookに分けることや、そもそもDashboard作成を別のツールにするなど今後の課題。

最終的には使用する必要はなくなったが、Excelのカメラ機能を使うと指定範囲のライブ画像を作成することも可能。画像ではあるものの、保存するタイミングでの指定範囲の状態が反映される。

Synologyの設定

パッケージセンターからWeb Stationをインストールする。
NAS内に『web』というフォルダが作成され、index.htmlやweb_imagesというフォルダが作成されていればひとまずOK。
エラーメッセージ的ではあるがまだセットアップ出来ていないよ的な内容

ページ作成

画像で作成したDashboardを並べて表示する。今年の各月分と、過去の年間集計(月別とほぼ同じ内容だが1年間の集計と月平均支出)を並べたレイアウト。
カード表示になっておりスマホ画面でもきれいに表示される

expdashboard.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我が家の家計簿ポータル</title>
    <style>
        body { font-family: sans-serif; text-align: center; background: #f4f4f4; margin: 0; padding: 20px; }
        .card { background: white; margin: 20px auto; padding: 15px; border-radius: 10px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); max-width: 1000px; }
        .img-link { display: inline-block; transition: opacity 0.2s; text-decoration: none; }
        .img-link:hover { opacity: 0.8; }
        img { max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px; }
        h2 { color: #333; font-size: 1.2rem; }
        h1 { font-size: 1.5rem; }
        
        /* アーカイブ共通:横スクロールレイアウト */
        .scroll-container { 
            display: flex; 
            overflow-x: auto; 
            padding-bottom: 15px; 
            gap: 15px; 
            justify-content: flex-start;
            scrollbar-width: thin; /* Firefox用 */
        }
        /* スクロールバーのデザイン(Chrome/Edge/Safari) */
        .scroll-container::-webkit-scrollbar { height: 8px; }
        .scroll-container::-webkit-scrollbar-thumb { background: #ccc; border-radius: 10px; }

        .archive-item { flex: 0 0 auto; }
        
        /* 月次画像の幅 */
        .monthly-img { width: 300px; }
        
        /* 年間画像の幅(少し大きめ) */
        .yearly-img { width: 400px; }

        .label { font-size: 0.85rem; color: #555; margin-top: 5px; font-weight: bold; }
    </style>
    <meta http-equiv="Cache-Control" content="no-cache">
</head>
<body>
    <h1>🏠 支出管理ダッシュボード</h1>

    <div class="card">
        <h2>📊 最新の状況(今月・先月)</h2>
        <a href="ExpDashboard.jpg" target="_blank" class="img-link">
            <img src="ExpDashboard.jpg" alt="最新家計簿">
        </a>
        <p style="font-size: 0.8rem; color: #666;">(画像クリックで拡大)</p>
    </div>

    <div class="card">
        <h2>🗓️ 先月までの実績(月次)</h2>
        <div class="scroll-container">
            <a href="archive/2026_01.jpg" target="_blank" class="archive-item"><img src="archive/2026_01.jpg" class="monthly-img" onerror="this.parentElement.style.display='none';"><div class="label">2026/01</div></a>
            <a href="archive/2026_02.jpg" target="_blank" class="archive-item"><img src="archive/2026_02.jpg" class="monthly-img" onerror="this.parentElement.style.display='none';"><div class="label">2026/02</div></a>
            <a href="archive/2026_03.jpg" target="_blank" class="archive-item"><img src="archive/2026_03.jpg" class="monthly-img" onerror="this.parentElement.style.display='none';"><div class="label">2026/03</div></a>
            <a href="archive/2026_04.jpg" target="_blank" class="archive-item"><img src="archive/2026_04.jpg" class="monthly-img" onerror="this.parentElement.style.display='none';"><div class="label">2026/04</div></a>
            <a href="archive/2026_05.jpg" target="_blank" class="archive-item"><img src="archive/2026_05.jpg" class="monthly-img" onerror="this.parentElement.style.display='none';"><div class="label">2026/05</div></a>
            <a href="archive/2026_06.jpg" target="_blank" class="archive-item"><img src="archive/2026_06.jpg" class="monthly-img" onerror="this.parentElement.style.display='none';"><div class="label">2026/06</div></a>
            <a href="archive/2026_07.jpg" target="_blank" class="archive-item"><img src="archive/2026_07.jpg" class="monthly-img" onerror="this.parentElement.style.display='none';"><div class="label">2026/07</div></a>
            <a href="archive/2026_08.jpg" target="_blank" class="archive-item"><img src="archive/2026_08.jpg" class="monthly-img" onerror="this.parentElement.style.display='none';"><div class="label">2026/08</div></a>
            <a href="archive/2026_09.jpg" target="_blank" class="archive-item"><img src="archive/2026_09.jpg" class="monthly-img" onerror="this.parentElement.style.display='none';"><div class="label">2026/09</div></a>
            <a href="archive/2026_10.jpg" target="_blank" class="archive-item"><img src="archive/2026_10.jpg" class="monthly-img" onerror="this.parentElement.style.display='none';"><div class="label">2026/10</div></a>
            <a href="archive/2026_11.jpg" target="_blank" class="archive-item"><img src="archive/2026_11.jpg" class="monthly-img" onerror="this.parentElement.style.display='none';"><div class="label">2026/11</div></a>
            <a href="archive/2026_12.jpg" target="_blank" class="archive-item"><img src="archive/2026_12.jpg" class="monthly-img" onerror="this.parentElement.style.display='none';"><div class="label">2026/12</div></a>
        </div>
    </div>

    <div class="card">
        <h2>🏆 過去の年間集計</h2>
        <div class="scroll-container">
            <a href="archive/2026_Total.jpg" target="_blank" class="archive-item">
                <img src="archive/2026_Total.jpg" class="yearly-img" onerror="this.parentElement.style.display='none';">
                <div class="label">2026年度(経過)</div>
            </a>
            <a href="archive/2025_Total.jpg" target="_blank" class="archive-item">
                <img src="archive/2025_Total.jpg" class="yearly-img" onerror="this.parentElement.style.display='none';">
                <div class="label">2025年度 合計</div>
            </a>
            <a href="archive/2024_Total.jpg" target="_blank" class="archive-item">
                <img src="archive/2024_Total.jpg" class="yearly-img" onerror="this.parentElement.style.display='none';">
                <div class="label">2024年度 合計</div>
            </a>
        </div>
    </div>

    <p style="color: #888; margin-top: 30px;">※自動更新</p>
</body>
</html>

SwitchBotの温度取得

トークン取得

SwitchBotからデータを取得するため、トークンとクライアントシークレットという情報が必要。隠しコマンド的なやり方で、SwitchBotアプリで取得可能。

SwitchBotアプリ > 設定 > 基本データ画面のアプリバージョンを10回連打すると、『開発者向けオプション』メニュー登場

必要に応じてトークンのリセットもこの画面で可能

Synology側準備

Pythonスクリプトで上記のトークンを使用してSwitchBotから情報を取得する。このPythonスクリプトをSynology(NAS)ないで定期的に実行して温度情報を更新する。そのため、まずはSynology内にPythonをインストールする。

パッケージセンターよりPythonを検索(今回はPython 3.9)
Pythonのインストールだけだとたりず、pipとrequestsを追加する必要あり。以下のコードをSynology内のコントロールセンター > タスクスケジューラを使って実行する。タスクスケジューラは名前の通り定期的に実行する機能だが、コマンドを1度だけ実行するときにも使用できる。

PIP/Requestsのインストール

# pip を導入
  /usr/bin/python3 -m ensurepip
  /usr/bin/python3 -m pip install --upgrade pip
  # requestsのインストール
  /usr/bin/python3 -m pip install requests

温度・湿度取得

PythonスクリプトでSwitchBotのサーバーへアクセスし、戻ってきた値を.jsonファイルに記述して、Synology内のwebフォルダへ保管。これをHTMLから読み取って表示させる。
まずはじめに、取得したいSwitchBotデバイスの「Device ID」を確認する。このコマンドだけならNAS上でなくローカル環境でOK。

Switchbot_get_devices.py

import time
import hashlib
import hmac
import base64
import requests
import json

token = 'xxx'
secret = 'xxx'

def get_devices():
    t = str(int(round(time.time() * 1000)))
    nonce = ""
    string_to_sign = '{}{}{}'.format(token, t, nonce)
    string_to_sign = bytes(string_to_sign, 'utf-8')
    secret_bytes = bytes(secret, 'utf-8')
    sign = base64.b64encode(hmac.new(secret_bytes, msg=string_to_sign, digestmod=hashlib.sha256).digest())

    headers = {
        "Authorization": token,
        "sign": str(sign, 'utf-8'),
        "t": t,
        "nonce": nonce,
        "Content-Type": "application/json; charset=utf8"
    }

    response = requests.get("https://api.switch-bot.com/v1.1/devices", headers=headers)
    print(json.dumps(response.json(), indent=4, ensure_ascii=False))

get_devices()
上手く実行できると、デバイス情報が表示される。
  • deviceName: SwitchBotアプリで付けた名前(例:「リビング温湿度計」)
  • deviceType:  防水温湿度計など
  • deviceId:   ABC123456789 のような英数字
上記のdeviceIDで指定した機器から温度と湿度情報を取得できる。

Switchbot_get_devices.py

import time
import hashlib
import hmac
import base64
import requests
import json
import os

# --- 設定 ---
TOKEN = 'xxx'
SECRET = 'xxx'
DEVICES = {
    "outside": "Dxxxxxxxxxxx9",
    "living": "Bxxxxxxxxxxx3",
"bedroom": "ExxxxxxxxxxxD"
} # 保存先パス(Synologyの絶対パスを指定) SAVE_PATH = '/volume1/web/switchbot_temp.json' # Local # SAVE_PATH = 'switchbot_temp.json' def get_status(device_id, headers): url = f"https://api.switch-bot.com/v1.1/devices/{device_id}/status" try: res = requests.get(url, headers=headers) data = res.json() if data.get("statusCode") == 100: return data.get("body") except: return None return None def update_all_status(): t = str(int(round(time.time() * 1000))) nonce = "" string_to_sign = f'{TOKEN}{t}{nonce}'.encode('utf-8') sign = base64.b64encode(hmac.new(SECRET.encode('utf-8'), msg=string_to_sign, digestmod=hashlib.sha256).digest()) headers = { "Authorization": TOKEN, "sign": str(sign, 'utf-8'), "t": t, "nonce": nonce, "Content-Type": "application/json; charset=utf8" } results = { "last_update": time.strftime("%Y/%m/%d %H:%M:%S"), "devices": {} } for name, dev_id in DEVICES.items(): status = get_status(dev_id, headers) if status: results["devices"][name] = { "temp": status.get("temperature"), "humidity": status.get("humidity") } with open(SAVE_PATH, 'w', encoding='utf-8') as f: json.dump(results, f, indent=4) if __name__ == "__main__": update_all_status()
エラーなく.jsonが更新されていればOK。Synology上で実行する際は、タスクスケジューラで実行するが、エラー時の確認はタスクスケジューラ内の操作 > 結果を表示 > 「標準出力/エラー」から確認出来る。
このスクリプトをタスクスケジューラで定期実行すれば、まずは.jsonに定期的に温湿度情報が保存されていく。

トップページ

今回は取得した温度と湿度はトップページに表示させた。

Blog投稿作成段階のトップページ

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Home Portal</title>
    <style>
        :root {
            --bg-color: #f0f2f5;
            --card-bg: #ffffff;
            --accent-blue: #007bff;
            --text-main: #1c1e21;
            --text-sub: #606770;
            --out-color: #28a745; /* 外気温用:緑 */
            --liv-color: #fd7e14; /* リビング用:オレンジ */
            --bed-color: #6f42c1; /* 寝室用:紫 */
        }

        body { font-family: "Helvetica Neue", Arial, "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif; text-align: center; background: var(--bg-color); margin: 0; padding: 40px 20px; }
        .container { max-width: 650px; margin: 0 auto; }
        h1 { color: var(--text-main); margin-bottom: 25px; }

        /* --- 温度モニターエリア --- */
        .sensor-panel { display: flex; justify-content: space-between; gap: 10px; margin-bottom: 30px; }
        .sensor-card { background: white; flex: 1; padding: 15px 5px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); border-top: 4px solid #ccc; }
        .sensor-card.outside { border-top-color: var(--out-color); }
        .sensor-card.living { border-top-color: var(--liv-color); }
        .sensor-card.bedroom { border-top-color: var(--bed-color); }
        
        .s-label { font-size: 0.75rem; color: var(--text-sub); font-weight: bold; margin-bottom: 5px; }
        .s-val { font-size: 1.4rem; font-weight: bold; color: var(--text-main); }
        .s-unit { font-size: 0.8rem; margin-left: 2px; }
        .s-hum { font-size: 0.9rem; color: var(--accent-blue); margin-top: 3px; display: block; }

        /* --- メニューカード --- */
        .menu-card { background: white; display: block; text-decoration: none; margin-bottom: 15px; padding: 25px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); transition: transform 0.2s, box-shadow 0.2s; }
        .menu-card:hover { transform: translateY(-3px); box-shadow: 0 6px 16px rgba(0,0,0,0.12); }
        .icon { font-size: 2rem; margin-bottom: 8px; }
        .title { font-size: 1.2rem; font-weight: bold; color: var(--accent-blue); }
        .desc { font-size: 0.85rem; color: var(--text-sub); margin-top: 6px; }
        .admin { background: #f8f9fa; margin-top: 20px; }
        .admin .title { color: #4b4f56; }

        #update-time { color: #90949c; font-size: 0.7rem; margin-top: -20px; margin-bottom: 30px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>🏠 Home Server Portal</h1>

        <div class="sensor-panel">
            <div class="sensor-card outside">
                <div class="s-label">🌳 外気温</div>
                <div class="s-val"><span id="out-t">--</span><span class="s-unit">°C</span></div>
                <div class="s-hum">💧 <span id="out-h">--</span>%</div>
            </div>
            <div class="sensor-card living">
                <div class="s-label">🏠 リビング</div>
                <div class="s-val"><span id="liv-t">--</span><span class="s-unit">°C</span></div>
                <div class="s-hum">💧 <span id="liv-h">--</span>%</div>
            </div>
            <div class="sensor-card bedroom">
                <div class="s-label">🛏️ 寝室</div>
                <div class="s-val"><span id="bed-t">--</span><span class="s-unit">°C</span></div>
                <div class="s-hum">💧 <span id="bed-h">--</span>%</div>
            </div>
        </div>
        <div id="update-time">Loading...</div>

        <a href="expdashboard.html" class="menu-card">
            <div class="icon">📊</div>
            <div class="title">支出管理ダッシュボード</div>
            <div class="desc">今月・先月の家計状況・電気代予測を確認します</div>
        </a>

        <a href="http://xxx.xxx.xxx.xxx:5000" class="menu-card admin">
            <div class="icon">⚙️</div>
            <div class="title">NAS システム管理</div>
            <div class="desc">Synology DSM (管理者専用)</div>
        </a>

        <p style="color: #90949c; font-size: 0.8rem; margin-top: 40px;">Private Network Only</p>
    </div>

    <script>
        async function fetchSensors() {
            try {
                // ファイル名はご指定の switchbot_temp.json
                const res = await fetch('switchbot_temp.json?t=' + new Date().getTime());
                const d = await res.json();
                
                // 各要素への流し込み
                document.getElementById('out-t').innerText = d.devices.outside.temp;
                document.getElementById('out-h').innerText = d.devices.outside.humidity;
                document.getElementById('liv-t').innerText = d.devices.living.temp;
                document.getElementById('liv-h').innerText = d.devices.living.humidity;
                document.getElementById('bed-t').innerText = d.devices.bedroom.temp;
                document.getElementById('bed-h').innerText = d.devices.bedroom.humidity;
                
                document.getElementById('update-time').innerText = "Sensor Last Update: " + d.last_update;
            } catch (e) {
                console.error("JSON Read Error", e);
                document.getElementById('update-time').innerText = "Sensor Error";
            }
        }

        fetchSensors();
        setInterval(fetchSensors, 300000); // 5分おきに画面更新
    </script>
</body>
</html>

今回もAIアシスタントをフル活用して、初期コード生成とデザイン案作成はおまかせしたおかげで、ここまでおそらく累計8時間程度で作成できた。今回のBlog投稿は自分で文章を書いているため時間を要した。プログラム作成も文章作成も、自分の頭で考える時間が減りすぎないように注意したい。

ラベル

Outdoor (23) 3D Printer (13) Raspberry Pi (11) Learning (10) Game (8) Ubuntu (8) Movie (7) Blog (6) Pico (6) AI (5) FreeCAD (5) Python (5) Unity (5) MSFS (4) Gadget (2)

人気の投稿

QooQ