LoginSignup
27
39

More than 3 years have passed since last update.

Raspberry Pi 4 でスマートメータ電気使用量・CO2センサ・温湿度計をグラフ化

Last updated at Posted at 2021-03-10

Raspberry Pi 4 でスマートメータ電気使用量・CO2センサ・温湿度計をグラフ化

Raspberry Pi で、スマートメータ、CO2センサ、温度計あたりを可視化しようと思って色々購入しました。
データは ClickHouse にいれて、Grafana でグラフにしました。
これはそのセットアップの記録です。

外観と画面

IMG_6058.JPEG

ラズパイはファンレス、基盤が見えているのは二酸化炭素センサです。
写真には写ってませんが、SwictBotの温湿度計を近くにおいています。

grafana20210310c.png

画面はこんな感じです。
外出時にすぐに天気がわかるように右上は天気予報エリアです。

真ん中が電力系ですが、中央左が今の瞬間電力、真ん中が直近10分、右が1日分です。
直近10分のグラフの下には、10分間での最大・最小値の差分を増加量として表示しています。

例えばドライヤーを使い始めた後 or 使い終わった後に見ると、最大値と最小値の差がドライヤー分の電力になるので、おおよそどのくらいかがわかります。
このキャプチャはドライヤー使い終わった後なのですが、1060Wほど使ってることがわかります。

画面中央下は二酸化炭素濃度、右が温湿度です。

可視化してみてわかったこと

  • 電力
    • トイレを使った時のウォッシュレット(たぶん)の電力はかなり高いようです。800~1200Wくらいいきます。
      • ただし一瞬だけです。
    • 食洗器とか動いていると時々大きく電力を消費します。
      • 乾燥時は1分間隔で800Wくらい上下します。どうも間欠で温めているようです。
    • 今のところの家全体でのピーク消費電力は3788W。60A契約なので一応余裕があることが確認できました。(系統ごと20Aなのでそちらのほうがひっかかりやすいかも)
  • 二酸化炭素濃度
    • マンションの24時間換気システムのおかげで、換気せずにいてもだいたい800ppmくらいで維持されていました。高くなっても1000ppm超えることはほとんどありません。
    • 試しに24時間換気システムを止めると、数時間で1100ppm以上に上昇しました。換気大事ですね。
    • 窓を開放しても、520ppmくらいまでしか下がりませんでした。部屋の奥の方にセンサー置いているためか、センサーの誤差なのかは不明です(普段換気しないので、自動で400ppmに補正する機能はオフにしてます)
  • 温度・湿度(Switchbot)
    • 温度センサーの追従性能はあまりよくないようで、1時間あたり1度くらいまでしか変化しません。換気したりして一気に温度変化がおきても、記録上はゆっくり変化するようです。

購入物

Amazon

  • Raspberry Pi4 ModelB 8GB 商品リンク
  • サンディスク microSD 32GB 商品リンク
    • 64GB以上はフォーマット等があるので32GBがおすすめらしい
  • Samsung Fit Plus 128GB 300MB/S USB 3.1 Flash Drive 商品リンク
    • メインのディスク用。USBブートできるようなので、性能を考えてこちらにOSを入れることにします。
  • Smraza Raspberry Pi 4 ACアダプター 商品リンク
  • Geekworm Raspberry Pi 4B 受動冷却金属ケース&アルミメタルケース 商品リンク
    • ファン無しを選びました。家は埃が多いので、ファンだと故障率高そうかなと…
  • Amazonベーシック HDMIケーブル 0.9m (タイプAオス - マイクロタイプDオス) 商品リンク
  • Wabisabi ブレッドボード・ジャンパーワイヤー 40x20cm デュポンケーブル 3本セット 商品リンク
    • CO2センサ接続用です。はんだ付けなしでGPIOにつなげるために購入。
  • MH-Z19B ピン付き 二酸化炭素センサーモジュール、0-5000ppm、赤外線方式(NDIR) 商品リンク
    • はんだ付けなしでいけるようにピン付きタイプにしました。CO2センサの中では精度がいいタイプ&安いようです。
  • SwitchBot 温湿度計 商品リンク
    • Bluetooth経由で直接温度と湿度を取り出せる記事を見つけたので、こちらを選定。値段も手ごろですね。

chip1stop

SDカードのRaspbianセットアップ

このあたりの記事を参考にしました。

eeprom の更新は行わなくても、2020-09-03 バージョンになっていて更新不要でした。

Ubuntu USBブートのセットアップ

このあたりの記事を参考にしました。

イメージの準備は、Raspberry Pi Imager を使う方法でWindowマシンからセットアップ。

raspi-configでのメニュー構成はちょっと変わっていて、Adbancedの中にありました。
USBブートを選べば完了。

問題なく起動して、ubuntuアカウントのパスワード変更しました。
Ubuntu Server はGUIもなく、無線LANセットアップも手動なのですね。
YAMLファイル更新で自宅無線LANに繋ぎました。

手順はここが参考になりました。

初回起動時にバックグラウンドで勝手に更新がかかるようで、自動decompressスクリプト設置前に再起動しちゃったので、起動しなくなってしまいました。
再起動時に裏で更新中と出たので嫌な予感がしましたが、予想通り…

これはUSBデバイスを抜いて再度vmlinuxを作り直して解決です。
こういうときのためにもsdカードはさしたままにしておくのが便利そうです。

デスクトップ環境のセットアップ

こちらを参考にしました。

# apt-get -y install ubuntu-desktop

結構時間がかかりました。

セットアップするとGUI画面になりますが、周囲に黒枠がでます。
Raspbianのときは最初のログイン時に黒枠周りにある?ってきかれてそこで治ったのですが、そのような機能はなさそう。
ぐぐったところ、/boot/firmware/config.txt

[pi4]
disable_overscan=1

のようにpi4セクションにdisable_overscan設定を追加して再起動で治りました。

TimeZone の設定

タイムゾーンの設定をしておきます。

(状態を確認。最初はUTCになっている)
# timedatectl status
               Local time: Sun 2021-03-07 00:36:11 UTC
           Universal time: Sun 2021-03-07 00:36:11 UTC
                 RTC time: n/a
                Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no
(TimeZone一覧を表示)
# timedatectl list-timezones
(Tokyoに変更)
# timedatectl set-timezone Asia/Tokyo
(状態を確認)
# timedatectl status
               Local time: Sun 2021-03-07 09:37:28 JST
           Universal time: Sun 2021-03-07 00:37:28 UTC
                 RTC time: n/a
                Time zone: Asia/Tokyo (JST, +0900)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

ClickHouse のインストール

自分でコンパイルするのは大変そう(時間的に)なので、こちらに記載があるようにCIツールのビルド結果を拝借します。

ドキュメントでは ClickHouse build check をみるとありますが、AArch64用は ClickHouse special build check のほうにあります。ドキュメントのみのコミットだとリンクがないので、コードを修正したコミットを探す必要があります。

リンク先ページで、Compiler 欄が clang-11-aarch64 のところの、Artifacts 欄の clickhouse のリンクがバイナリになっています。

# groupadd --system clickhouse
# useradd --system -m -g clickhouse clickhouse
# mkdir /home/clickhouse/bin
# cd /home/clickhouse/bin
# wget https://clickhouse-builds.s3.yandex.net/0/ca98bb68d4c9adf8fb416fd58afe139984fc9fdb/clickhouse_special_build_check/clang-11-aarch64_relwithdebuginfo_none_bundled_unsplitted_disable_False_binary/clickhouse
(しばらくするとファイル消えると思うので、その場合上に書いた場所からリンクを見つける必要があります)
# chmod +x clickhouse

追記:
ドキュメントのこちらに、masterのビルド結果取れるURLがありました。

https://clickhouse.tech/docs/en/getting-started/install/#from-binaries-non-linux

curl -O 'https://builds.clickhouse.tech/master/aarch64/clickhouse' && chmod a+x ./clickhouse

こちらだと同じURLでできそうです。

よく使うclickhouse-clientコマンドはパスの通ったところに作っておきます。

# ln -s /home/clickhouse/bin/clickhouse /usr/bin/clickhouse-client

自動的にサービスとして起動するようにscriptを作り、データ置き場やとconfigファイルを用意します。

# su - clickhouse
$ mkdir var
$ mkdir var/log
$ mkdir var/tmp
$ mkdir var/data
$ mkdir var/access
$ mkdir var/run
$ mkdir etc

/home/clickhouse/etc/config.xml は以下のようにしました。
リポジトリ上のconfigはこちらにあるので、これを参考に作ります。

設定ファイル設定例(config.xml、user.xml)
<yandex>
    <logger>
        <level>trace</level>
        <log>/home/clickhouse/var/log/clickhouse-server.log</log>
        <errorlog>/home/clickhouse/var/log/clickhouse-server.err.log</errorlog>
        <size>100M</size>
        <count>3</count>
    </logger>
    <http_port>8123</http_port>
    <tcp_port>9000</tcp_port>
    <listen_host>127.0.0.1</listen_host>
    <max_connections>4096</max_connections>
    <keep_alive_timeout>3</keep_alive_timeout>
    <max_concurrent_queries>100</max_concurrent_queries>
    <max_server_memory_usage>0</max_server_memory_usage>
    <max_thread_pool_size>10000</max_thread_pool_size>
    <max_server_memory_usage_to_ram_ratio>0.9</max_server_memory_usage_to_ram_ratio>
    <total_memory_profiler_step>4194304</total_memory_profiler_step>
    <total_memory_tracker_sample_probability>0</total_memory_tracker_sample_probability>
    <uncompressed_cache_size>536870912</uncompressed_cache_size>
    <mark_cache_size>536870912</mark_cache_size>
    <path>/home/clickhouse/var/data/</path>
    <tmp_path>/home/clickhouse/var/tmp/</tmp_path>
    <user_directories>
        <users_xml>
            <path>users.xml</path>
        </users_xml>
        <local_directory>
            <path>/home/clickhouse/var/access/</path>
        </local_directory>
    </user_directories>
    <default_profile>default</default_profile>
    <default_database>default</default_database>
    <mlock_executable>true</mlock_executable>
</yandex>
<yandex>
    <profiles>
        <default>
        </default>
    </profiles>
    <users>
        <default>
            <password></password>
            <profile>default</profile>
        </default>
    </users>
</yandex>

/etc/systemd/system/clickhouse-server.service に起動スクリプトを用意します。

[Unit]
Description=ClickHouse Server (analytic DBMS for big data)
Requires=network-online.target
After=network-online.target

[Service]
Type=simple
User=clickhouse
Group=clickhouse
Restart=always
RestartSec=30
RuntimeDirectory=clickhouse-server
ExecStart=/home/clickhouse/bin/clickhouse server --config=/home/clickhouse/etc/config.xml --pid-file=/home/clickhouse/var/run/clickhouse-server.pid
LimitCORE=infinity
LimitNOFILE=500000
CapabilityBoundingSet=CAP_NET_ADMIN CAP_IPC_LOCK CAP_SYS_NICE

[Install]
WantedBy=multi-user.target
# systemctl daemon-reload
# systemctl enable clickhouse-server.service
# systemctl start clickhouse-server.service
# systemctl status clickhouse-server.service

これでセットアップ完了です。

無事動くかテストしてみます。

# clickhouse-client --multiline
ClickHouse client version 21.3.1.6146 (official build).
Connecting to localhost:9000 as user default.
Connected to ClickHouse server version 21.3.1 revision 54447.

ubuntu :) create table test (key String) ENGINE=MergeTree ORDER BY key;

CREATE TABLE test
(
    `key` String
)
ENGINE = MergeTree
ORDER BY key

Query id: cbb229c5-c11b-4698-8b49-f02201cc7958

Ok.

0 rows in set. Elapsed: 0.022 sec.

ubuntu :) INSERT INTO test VALUES ('test');

INSERT INTO test VALUES

Query id: 64605886-c2f7-4c32-8be6-1744b0c8f6c6

Ok.

1 rows in set. Elapsed: 0.011 sec.

ubuntu :) SELECT * FROM test;

SELECT *
FROM test

Query id: 62b504c4-68c7-4f96-a0a6-ef8947b5ede7

┌─key──┐
 test 
└──────┘

1 rows in set. Elapsed: 0.009 sec.

Prometheus/Grafanaのインストール

こちらの記事を参考にしました。

インストールを行って自動起動を設定します。
Grafanaセットアップは2つ目の記事の方を参考に設定しました。

# apt install prometheus prometheus-node-exporter

# wget -q -O - https://packages.grafana.com/gpg.key | apt-key add -
# echo "deb https://packages.grafana.com/oss/deb stable main" | tee /etc/apt/sources.list.d/grafana.list
# apt update && sudo apt install -y grafana
# systemctl enable grafana-server.service
# systemctl start grafana-server
# systemctl status grafana-server

Prometheus は http://IPアドレス:9090/ で、
Grafana は http://IPアドレス:3000/ でアクセスできるようになります。

Grafana にログインして、Prometheus のデータソースを追加。

image.png

ダッシュボードは https://grafana.com/grafana/dashboards/1860 こちらをベースに、CPU温度だけ追加しました。

image.png

image.png

続いてClickHouseのデータソースを追加します。

Installation タブを開くと説明があるのでその通りやります。

# grafana-cli plugins install vertamedia-clickhouse-datasource
installing vertamedia-clickhouse-datasource @ 2.2.3
from: https://grafana.com/api/plugins/vertamedia-clickhouse-datasource/versions/2.2.3/download
into: /var/lib/grafana/plugins

✔ Installed vertamedia-clickhouse-datasource successfully

Restart grafana after installing plugins . <service grafana-server restart>

# service grafana-server restart

これで、DataSource に ClickHouse が追加されているので、あとはそれを選択して設定を行います。

image.png

CO2センサー

こちらの記事を参考にしました。

センサーのマニュアル。

詳細情報はここが参考になりました。
レスポンスについてはマニュアルよりこちらの方が手元のものと一致する挙動でした。

ABC(24時間の最低値を400ppmに補正する機能)はオフにしました。
ずっと家ですが換気とかしないことも多いので…💦
また、レンジも5000ppmまでに設定します。

ClickHouse上に記録用のテーブルを作っておきます。

CREATE DATABASE co2;

USE co2;

CREATE TABLE co2 (
    time    DateTime DEFAULT now(),
    co2     UInt16
)
ENGINE=MergeTree
ORDER BY time
;

取得用のプログラムは以下のようなコードになりました。

プログラムコード
package main

import (
    //  "fmt"
    "io"
    "log"
    "time"

    "database/sql"
    //  "encoding/hex"

    _ "github.com/ClickHouse/clickhouse-go"
    "github.com/tarm/serial"
)

func main() {
    serialConfig := &serial.Config{Name: "/dev/ttyS0", Baud: 9600, ReadTimeout: time.Second * 5}
    ser, err := serial.OpenPort(serialConfig)
    if err != nil {
        log.Fatal(err)
    }
    defer ser.Close()

    connect, err := sql.Open("clickhouse", "tcp://127.0.0.1:9000")
    if err != nil {
        log.Fatal(err)
    }

    // 0x99や0x77もデータシートと違って応答を返す模様

    // 0x99 Sensor detection range setting -> 5000ppm(0x1388)
    send(ser, add_checksum([]byte("\xff\x01\x99\x13\x88\x00\x00\x00")))
    _ = read(ser)

    time.Sleep(1 * time.Second)
    // 0x79 ABC login on/off -> off(0x00)
    send(ser, add_checksum([]byte("\xff\x01\x79\x00\x00\x00\x00\x00")))
    _ = read(ser)

    time.Sleep(1 * time.Second)
    for {
        // 0x86 Read CO2 concentration
        send(ser, add_checksum([]byte("\xff\x01\x86\x00\x00\x00\x00\x00")))

        buf := read(ser)

        co2 := uint16(buf[2])<<8 + uint16(buf[3])

        //  fmt.Printf("co2: %d ppm\n", co2)

        tx, err := connect.Begin()
        if err != nil {
            log.Fatal(err)
        }
        stmt, err := tx.Prepare("INSERT INTO co2.co2 (co2) VALUES (?)")
        if err != nil {
            log.Fatal(err)
        }
        defer stmt.Close()

        if _, err := stmt.Exec(co2); err != nil {
            log.Fatal("Exec", err)
        }

        if err := tx.Commit(); err != nil {
            log.Fatal(err)
        }

        time.Sleep(5 * time.Second)
    }

}

func read(ser *serial.Port) []byte {
    buf := make([]byte, 9)
    if _, err := io.ReadFull(ser, buf); err != nil {
        log.Fatal(err)
    }
    //  fmt.Printf("<= %s\n", hex.EncodeToString(buf))

    return buf
}

func send(ser *serial.Port, data []byte) {
    //  fmt.Printf("=> %s\n", hex.EncodeToString(data))
    _, err := ser.Write(data)
    if err != nil {
        log.Fatal(err)
    }
}

func add_checksum(data []byte) []byte {
    var checksum byte
    for i, p := range data {
        if i == 0 {
            continue
        }
        checksum += p
    }
    checksum = 0xff - checksum + 1

    return append(data, checksum)
}

時々通信が固まったりして、最初はコードを疑って他の方の実装でためしても失敗率が高く、原因を調べたところ getty サービスと競合するようでした。

systemctl stop serial-getty@ttyS0.service
systemctl mask serial-getty@ttyS0.service

としてサービス止める&自動起動も停止したところ、安定しました。

CO2センサー取得プログラムも自動起動するように設定しました。
/etc/systemd/system/co2-server.service

[Unit]
Description=CO2 Sensor
Requires=clickhouse-server.service
After=clickhouse-server.service

[Service]
Type=simple
User=root
Group=root
Restart=always
RestartSec=30
RuntimeDirectory=co2-server
ExecStart=/home/mikage/usr/co2/co2

[Install]
WantedBy=multi-user.target
# systemctl daemon-reload
# systemctl enable co2-server.service
# systemctl start co2-server.service
# systemctl status co2-server.service

スマートメータとの通信

こちらの記事を参考にしました。

ClickHouse上に記録用のテーブルを作っておきます。

CREATE DATABASE smartmeter;

USE smartmeter;

CREATE TABLE smartmeter_w (
    time    DateTime DEFAULT now(),
    w       Int32
)
ENGINE=MergeTree
ORDER BY time
;

CREATE TABLE smartmeter_wh (
    time    DateTime DEFAULT now(),
    wh      UInt32
)
ENGINE=MergeTree
ORDER BY time
;

取得用のプログラムは以下のようなコードになりました。
安定して動くかは不明ですが、とりあえずしばらく新規に値をとれなかったら終了するようにしておきました。

プログラムコード
package main

import (
    "bufio"
    "fmt"
    "log"
    "regexp"
    "strconv"
    "strings"
    "time"

    "database/sql"

    _ "github.com/ClickHouse/clickhouse-go"
    "github.com/tarm/serial"
)

func main() {
    // 通信に失敗したら終了して再起動させる
    timer := time.AfterFunc(time.Second * 120, func() {
        log.Fatal("timeout")
    })
    timerKick := func() {
        timer.Stop()
        timer.Reset(time.Second * 120)
    }

    serialConfig := &serial.Config{Name: "/dev/ttyUSB0", Baud: 115200}
    ser, err := serial.OpenPort(serialConfig)
    if err != nil {
        log.Fatal(err)
    }


    connect, err := sql.Open("clickhouse", "tcp://127.0.0.1:9000")
    if err != nil {
        log.Fatal(err)
    }


    send(ser, []byte("SKINFO\r\n"))


    // パスワード等の情報
    id := "0000"
    pwd := "XXX"


    channel := ""
    panid := ""
    addr := ""
    mac := ""

    state := "info"
    scanner := bufio.NewScanner(ser)
    for scanner.Scan() {
        ret := scanner.Text()
//      fmt.Println("[", state, "]<= ", ret)

        if ret == "OK" {
            switch state {
                case "info":
                    state = "setpw"
                    send(ser, []byte("SKSETPWD C " + pwd + "\r\n"))
                case "setpw":
                    state = "setid"
                    send(ser, []byte("SKSETRBID " + id + "\r\n"))
                case "setid":
                    state = "scan"
                    // 電波状況悪い環境なので8にしてますが、場所が良ければ7や6でもよさそうです。
                    send(ser, []byte("SKSCAN 2 FFFFFFFF 8\r\n"))
                    timerKick()
                case "reg_channel":
                    state = "reg_panid"
                    sendmes := fmt.Sprintf("SKSREG S3 %s\r\n", panid)
                    send(ser, []byte(sendmes))
                case "reg_panid":
                    state = "skll"
                    sendmes := fmt.Sprintf("SKLL64 %s\r\n", addr)
                    send(ser, []byte(sendmes))
                case "skjoin":
                    state = "sksend"
                    message := messageE7()
                    sendmes := fmt.Sprintf("SKSENDTO 1 %s 0E1A 1 %04X %s", mac, len(message), message)
                    send(ser, []byte(sendmes))
                case "sksend":
                    state = "sksend2"
                    time.Sleep(5 * time.Second)
                    message := messageE7()
                    sendmes := fmt.Sprintf("SKSENDTO 1 %s 0E1A 1 %04X %s", mac, len(message), message)
                    send(ser, []byte(sendmes))
                case "sksend2":
                    state = "sksend"
                    message := messageE0()
                    sendmes := fmt.Sprintf("SKSENDTO 1 %s 0E1A 1 %04X %s", mac, len(message), message)
                    send(ser, []byte(sendmes))
            }
        } else if(state == "scan") {
            if m := regexp.MustCompile(`^  Channel:(.*)`).FindStringSubmatch(ret); len(m) > 0 {
                channel = m[1]
                fmt.Println("channel: ", channel)
            } else if m := regexp.MustCompile(`^  Pan ID:(.*)`).FindStringSubmatch(ret); len(m) > 0 {
                panid = m[1]
                fmt.Println("panid: ", panid)
            } else if m := regexp.MustCompile(`^  Addr:(.*)`).FindStringSubmatch(ret); len(m) > 0 {
                addr = m[1]
                fmt.Println("addr: ", addr)
            } else if strings.HasPrefix(ret, "EVENT 22 ") {
                if channel == "" {
                    log.Fatal("error: scan failed.")
                } else {
                    state = "reg_channel"
                    sendmes := fmt.Sprintf("SKSREG S2 %s\r\n", channel)
                    send(ser, []byte(sendmes))
                    timerKick()
                }
            }
        } else if(state == "skll") {
            if m := regexp.MustCompile(`^([0-9A-F]{4}:.*)`).FindStringSubmatch(ret); len(m) > 0 {
                mac = m[1]
                fmt.Println("mac: ", mac)
                state = "skjoin"
                sendmes := fmt.Sprintf("SKJOIN %s\r\n", mac)
                send(ser, []byte(sendmes))
            }
        } else if strings.HasPrefix(ret, "ERXUDP") {
            udp := strings.Split(ret, " ")
            mes := udp[8]

            // E7応答
            // 0   4   8     14    2022242628      36
            // 1081000102880105FF017201E704000004AF
            // EDH TID SEOJ  DEOJ  ESV EPC data
            //                       OPC PDC

        //  fmt.Println("mes: ", mes)

            seoj := mes[8:14]
            esv := mes[20:22]

            if seoj == "028801" && esv == "72" {
                epc := mes[24:26]
            //  fmt.Println("epc: ", epc)

                if epc == "E7" {
                    // 瞬時電力計測値
                    w, err := strconv.ParseInt(mes[28:36], 16, 32)
                    if err != nil {
                        log.Fatal(err)
                    }
                    writeE7(connect, w)
                    timerKick()
                }
                if epc == "E0" {
                    // 積算電力量計測値(正方向計測値)
                    wh, err := strconv.ParseUint(mes[28:36], 16, 32)
                    if err != nil {
                        log.Fatal(err)
                    }
                    writeE0(connect, wh)
                }
            }

        }
    }
    if scanner.Err() != nil {
        log.Fatal(err)
    }
    fmt.Println("loop exit.")

}

func writeE7(connect *sql.DB, w int64) {
    tx, err := connect.Begin()
    if err != nil {
        log.Fatal(err)
    }
    stmt, err := tx.Prepare("INSERT INTO smartmeter.smartmeter_w (w) VALUES (?)")
    if err != nil {
        log.Fatal(err)
    }
    defer stmt.Close()

    if _, err := stmt.Exec(w); err != nil {
        log.Fatal("Exec", err)
    }

    if err := tx.Commit(); err != nil {
        log.Fatal(err)
    }
}

func writeE0(connect *sql.DB, wh uint64) {
    tx, err := connect.Begin()
    if err != nil {
        log.Fatal(err)
    }
    stmt, err := tx.Prepare("INSERT INTO smartmeter.smartmeter_wh (wh) VALUES (?)")
    if err != nil {
        log.Fatal(err)
    }
    defer stmt.Close()

    if _, err := stmt.Exec(wh); err != nil {
        log.Fatal("Exec", err)
    }

    if err := tx.Commit(); err != nil {
        log.Fatal(err)
    }
}


func send(ser *serial.Port, data []byte) {
//  fmt.Print("=> ", string(data))
    _, err := ser.Write(data)
    if err != nil {
        log.Fatal(err)
    }
}

func messageE7() string {
    frame := ""
    frame += "\x10\x81\x00\x01" // EDH/TID
    frame += "\x05\xFF\x01" // SEOJ
    frame += "\x02\x88\x01" // DEOJ
    frame += "\x62" // ESV
    frame += "\x01" // OPC
    frame += "\xE7" // EPC
    frame += "\x00" // PDC(EDTのバイト数)
                    // EDCなし

    return frame
}

func messageE0() string {
    frame := ""
    frame += "\x10\x81\x00\x01" // EDH/TID
    frame += "\x05\xFF\x01" // SEOJ
    frame += "\x02\x88\x01" // DEOJ
    frame += "\x62" // ESV
    frame += "\x01" // OPC
    frame += "\xE0" // EPC
    frame += "\x00" // PDC(EDTのバイト数)
                    // EDCなし

    return frame
}


自動起動するように設定しました。
/etc/systemd/system/smartmeter-server.service

[Unit]
Description=SmartMeter
Requires=clickhouse-server.service
After=clickhouse-server.service

[Service]
Type=simple
User=root
Group=root
Restart=always
RestartSec=30
RuntimeDirectory=smartmeter-server
ExecStart=/home/mikage/usr/smartmeter/smartmeter

[Install]
WantedBy=multi-user.target
# systemctl daemon-reload
# systemctl enable smartmeter-server.service
# systemctl start smartmeter-server.service
# systemctl status smartmeter-server.service

Grafana で表示する時、積算電力については結構粒度が荒いので、30分単位で差分で出すようにしました。runningDifference で集計できます。
(ただしblock境界を超えると正しく集計できないので、正確ではないかも…)

SELECT
  t,
  runningDifference(kWh) as kWh
FROM (
  SELECT
      $timeSeries as t,
      max(wh) * 0.1 as kWh
  FROM $table
  WHERE $timeFilter
  GROUP BY t
  ORDER BY t
)

SwitchBot温湿度計

主にこちらの記事を参考にしました。

こちらはgoで書き直すの大変そうだったので、記事のスクリプトを参考にClickHouseに格納するようにだけ修正しました。

Bluetoothと、スクリプトのセットアップを行います。

# apt install pi-bluetooth
(一度リブートします)
# shutdown -r now

# apt-get install libglib2.0-dev
# apt install python3-pip
# pip3 install bluepy
(bluepy-helperの場所を確認して権限付与します)
# find /usr/local/lib -name bluepy-helper
/usr/local/lib/python3.8/dist-packages/bluepy/bluepy-helper
# setcap 'cap_net_raw,cap_net_admin+eip' /usr/local/lib/python3.8/dist-packages/bluepy/bluepy-helper

ClickHouse用にテーブルを用意します。

CREATE DATABASE switchbot;

USE switchbot;

CREATE TABLE meter (
    time         DateTime DEFAULT now(),
    battery      UInt8,
    temperature  Int16,
    humidity     UInt8
)
ENGINE=MergeTree
ORDER BY time
;

python用のClickHouseドライバをセットアップします。

# pip3 install clickhouse-driver

スクリプトはこのようになりました。
温度は小数にしたくなかったので、10倍にしています。

プログラムコード
import binascii
from bluepy.btle import Scanner, DefaultDelegate
import time
from clickhouse_driver import Client

# スマホアプリでMACアドレスを確認して記入する
macaddr = 'xx:xx:xx:xx:xx:xx'

client = Client('localhost')


class ScanDelegate(DefaultDelegate):
  def __init__(self):
    DefaultDelegate.__init__(self)

  def handleDiscovery(self, dev, isNewDev, isNewData):
    if dev.addr != macaddr: return
    for (adtype, desc, value) in dev.getScanData():
      if (adtype != 22): continue

      servicedata = binascii.unhexlify(value[4:])

      battery = servicedata[2] & 0b01111111
      temperature = (servicedata[3] & 0b00001111) + (servicedata[4] & 0b01111111) * 10
      isTemperatureAboveFreezing = servicedata[4] & 0b10000000

      if not isTemperatureAboveFreezing:
        temperature = -temperature

      humidity = servicedata[5] & 0b01111111

      client.execute(
        'INSERT INTO switchbot.meter (battery, temperature, humidity) VALUES',
        [(battery, temperature, humidity)]
      )

      exit()

scanner = Scanner().withDelegate( ScanDelegate() )
scanner.scan(10)

こちらは1分1回起動するように設定しました。

$ crontab -e
---
* * * * * python3 /home/mikage/usr/switchbot-meter/switchbot-meter.py
---

Grafana パネル

外部のHTMLをiframeで入れるのに、これが必要でした。

自宅用に外部の天気予報を拝借して一緒に表示しています。

認証なしで見れるようにする。

/etc/grafana/grafana.ini で認証なしで見れるようにしておきます。

[auth.anonymous]
enabled = true
org_role = Viewer

自動でブラウザを立ち上げる。

マウスカーソルを非表示にできるように unclutter を追加します。

sudo apt-get install unclutter

GUI の Setting メニューから、自動ログインを有効にします。

GUI の左下のアプリ一覧を開き、start で検索し自動起動の設定をします。
Add をクリックして、適当な名前で以下のようなコマンドを設定します。

URL は、GrafanaのダッシュボードのURLを記入します。
複数見せるならPlaylistですが、同じページをずっと出すだけなので直接dashboard指定する形にしました。

/usr/bin/firefox --kiosk http://localhost:3000/d/KIDee7UGz/mikages-home?orgId=1&refresh=5s

同じように unclutter も登録します。

unclutter -idle 0.1 -root

Settings の Privacy の Screen Lock でスクリーンロックを無効にします。
これで画面表示されたままになります。

まとめ

以上のような感じでセットアップして、今の所安定して動いています。
ファンなしケースですが、本体の温度も50度前後で安定しています。

消費電力の細かい変動とか、二酸化炭素濃度など今まで見れなかったものが見れるようになるのは良いですね。

データが溜まってきたら、月次でのグラフに差し替えるなどしてみようと思います。

27
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
39