144
168

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

3GPI とラズパイで家庭の消費電力量をグラフ表示する

Last updated at Posted at 2016-07-31

家庭の分電盤にセンサーを取り付けて、こんな感じのグラフを作ります。

メイン.gif

回路ごとの消費電力量も。

回路ごと.gif

はじめに

自宅の消費電力量を把握したいと思いました。不要な待機電力があれば削減し、無駄に電気を消費する機器があれば、低消費電力製品へ入れ替えたいです。スマートメーターにすれば総消費電力量の推移はわかりますが、分電盤から分岐した回路毎の消費量はわかりません。各回路毎の消費電力量をグラフ表示したいと思います。

電力単価は、我が家の2015年の実績から、25円/kWh と決めうちしています。

スペック

  • 分電盤に電流センサーを設置する
  • 総消費電力量と分岐回路の消費電力量をモニターする
  • ラズパイの電源は壁のコンセントから受ける
  • 単相3線、単相2線に対応する
  • 電流計測のみで消費電力量を推計する(電圧は計測しない)
  • 最大電流は 60A
  • 3G 通信させる
  • 300秒毎にログサーバーへ送信
  • ブラウザで推移グラフを見たい
  • 変な波形に対応したい。SSR、スイッチング電源、低力率の機器とか
  • 50, 60Hz 対応
  • 電子工作や加工は最低限
  • プログラム停止時にシステムを再起動する

消費電力量の計算方法

消費電力量 = 電圧 × 電流 × 時間
電圧 = 100 V に決めうち
電流 = 計測値の二乗平均平方根を使う
時間 = 各時刻の計測値を積算する

電流の計測方法

計測方法は、電流センサーの出力を平滑回路や整流回路を介さずにADコンバーターに接続しました。十分なサンプリング速度があれば、波形が読み取れることを期待しています。

サンプリング速度は、半サイクルあたり 5 サンプル程度あれば良いと思いました。電源周波数を、50Hz と 60Hz に対応するためには、毎秒 600 サンプル程度は欲しいと思います。

設計

システムの全体像(開発初期のイメージ)

構成図.png

ハードウェア設計

機器を購入します。合計 57,390円でした。
電源、USBケーブル、ユニバーサル基盤、ピンヘッダ、ピンソケット、ケーブル類は、自宅の余剰品を使いました。

必要なもの 買ったもの お店 金額
3G通信モジュール 3GPI スイッチサイエンス 29,800円
電源管理モジュール slee-Pi スイッチサイエンス 11,800円
ラズパイ Raspberry Pi 2 Model B スイッチサイエンス 5,940円
WiFiドングル GW-USNANO2A スイッチサイエンス 1,350 円
電流センサー 4個 SR-3702-150N/14Z 秋月電子 3,920円
ADコンバーター 1個 ADC1015モジュール 秋月電子 1,280円
金属皮膜抵抗 4本 100Ω 100本いり 秋月電子 300円
通信用SIM So-net 0 sim ソネット 3,000円

モジュールパーツの組み合わせなので難しい回路設計や調整は不要です。電流電圧変換ボードは、ユニバーサル基盤で作ります。

ソフトウェア

ライブラリが豊富なので、なるべく Python で統一します。Python は初めて使うので楽しみです。Python のバージョンは、3 にしました。

データ管理と表示用のサーバーは、さくらレンタルサーバースタンダードを使います。データ管理ライブラリは、グラフ出力機能をもった RRDtool にしてみます。私が借りているサーバーでは、rrdtool-fix という名前でインストールされていました。

どんどん作っちゃいます

  1. 電流センサー
  2. ケーブルコネクタ
  3. ADコンバーター
  4. ラズパイ
  5. 3GPI
  6. 計測値の送受信プログラム
  7. グラフ表示プログラム

電流センサー

LANケーブルに半田付けして、分電盤に設置します。

2016_07_17_00845.JPG
メイン回路に設置した様子

分電盤は、赤、黒、白の3本線で受電しています。
4つのセンサーを以下のように設置しました。

  • センサー0:主幹 黒線
  • センサー1:主幹 赤線
  • センサー2:リビング照明、リビングコンセント(黒系)
  • センサー3:冷蔵庫、ホームベーカリー(赤系)

他社製品ですが似ているセンサーのスペックシート(URD社)によると、抵抗10Ωで電圧変換すると、60A が 0.2V に変換されます。換算関数は、「電流 = 電圧 * 300」です。これでは低電流は測れませんが、1 - 60 A は歪みなく測れそうです。抵抗100Ωで電圧変換すると、60A が 2.0V に変換されます。換算関数は、「電流 = 電圧 * 30」です。0.1 A 程度から測れそうです。

迷うところですが、ピークは多少歪んでもよしとして、低電流が読める100Ωにしました。

実際に使用するパーツですが、誤差 ±1% の抵抗を購入しました。テスターで抵抗値を計測して、100 Ω に近い同じ抵抗値の4個を使いました。

ADコンバーター

回路を設計して作ります。

電力量モニター回路図.png

作ってみました。VDDは 3.3V としました。

ボード.JPG

ADコンバーターモジュールは、電源電圧 2-5Vで、I2C 速度は 10kHz から 400kHz だけじゃなく、3.4MHz もいけちゃいます。I2C Addressは、0x48 から 0x4B までを ADDR ピンの接続先で変更できます。なんと、アンプを内蔵していて、マイナス電位も計測でき、マイコンでゲインを制御できます。以下の6レンジ。

  • ±0.256 V(1bit あたり 125 uV)
  • ±0.512 V(1bit あたり 250 uV)
  • ±1.024 V(1bit あたり 500 uV)
  • ±2.048 V(1bit あたり 1 mV)
  • ±4.096 V(1bit あたり 2 mV)
  • ±6.144 V(1bit あたり 3 mV)

PGAで ±2.048V レンジ(x2)を使うと、電流センサーで読み取れる電流は ±61.44 A です。±2.048V レンジでやってみます。マイナス入力が可能なんて便利すぎです。最小分解能は、1 mV なので、電流に換算すると、30 mA です。消費電力は、100V × 30 mA = 3 W です。

ラズパイ

計測目的なので、GUIは不要です。ワタシはコンソール中毒なので、ネットワーク経由でセットアップします。ラズパイは初めて触るので楽しみです。

インストール

余っていたSDカード(16GB microSDHC Class4)にRaspbian Jessie Lite (2016-05-27) を焼きました。製作したADコンバーターボードと、LANケーブルを接続して、DHCP アドレスを推測して SSH ログインするだけ。簡単すぎて拍子抜けしました。以下、インストール方法です。

  • raspi-config で諸々設定
    パーティション拡張、パスワード設定、ロケール(ja_JP.EUC-JP、ja_JP.UTF-8 を追加、ja_JP.UTF-8をメインに)、タイムゾーン、ホスト名、GPUメモリ割り当て(16MB)、I2C有効化をそれぞれ設定します。

  • 無線LAN の設定

  • その他、コマンドで設定
    以下のコマンドを入力します。

# 無線LANのパワーマネージメントをOFFにする
sudo sh -c "echo options 8192cu rtw_power_mgnt=0 rtw_enusbss=1 rtw_ips_mode=1 > /etc/modprobe.d/8192cu.conf"

# SSH のタイムアウトを延長
sudo sh -c "echo ClientAliveInterval 60 >> /etc/ssh/sshd_config"
sudo sh -c "echo ClientAliveCountMax 3 >> /etc/ssh/sshd_config"

# パッケージをアップデート
sudo apt-get update
sudo apt-get upgrade

# i2c-tools をインストール
sudo apt-get install i2c-tools

# i2c のクロックレートを上げる
sudo sh -c "echo dtparam=i2c_baudrate=300000 >> /boot/config.txt"

# i2c のチェック(0x48 に反応があれば ADS1015 が反応します)
i2cdetect -y 1 

# ADS1015 のライブラリをインストール
sudo apt-get install git build-essential python-dev
git clone https://github.com/adafruit/Adafruit_Python_ADS1x15.git
cd Adafruit_Python_ADS1x15
sudo python setup.py install
sudo shutdown -h now

3GPI のインストール

Raspberry Pi が止まったら電源を抜きます。
Raspberry Pi に 3GPI を接続して、3GPI 付属の電源を接続します。

3GPI の詳細情報は、ここGitHubにあります。
3gpi2.jpg

3GPIのインストール
sudo bash -c 'echo "deb http://mechatrax.github.io/3gpi ./" > /etc/apt/sources.list.d/3gpi.list'
sudo apt-get update
sudo apt-get install 3gpi-archive-keyring
sudo apt-get install 3gpi-utils 3gpi-network-manager
sudo reboot

APN を設定します。SIMは、so-net の 0sim を使いました。使っていない APN 情報は削除するのが良いそうです。

APN設定
sudo nmcli con add type gsm ifname "*" con-name so-net apn so-net.jp user nuro password nuro
sudo nmcli c delete gsm-3gpi-iij
sudo nmcli c delete gsm-3gpi-soracom
sudo reboot

たったこれだけで、3G 接続できてしましました。感動ものです。
ピングが遅いのも 3G 接続の証拠です。うれしい!

3G回線経由
$ ping google.com -c 4
PING google.com (172.217.25.238) 56(84) bytes of data.
64 bytes from nrt12s14-in-f14.1e100.net (172.217.25.238): icmp_seq=1 ttl=50 time=377 ms
64 bytes from nrt12s14-in-f14.1e100.net (172.217.25.238): icmp_seq=2 ttl=50 time=386 ms
64 bytes from nrt12s14-in-f14.1e100.net (172.217.25.238): icmp_seq=3 ttl=50 time=366 ms
64 bytes from nrt12s14-in-f14.1e100.net (172.217.25.238): icmp_seq=4 ttl=50 time=404 ms

--- google.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3003ms
rtt min/avg/max/mdev = 366.570/383.838/404.300/13.866 ms
光回線経由
$ ping google.com
PING google.com (172.217.25.238) 56(84) bytes of data.
64 bytes from nrt12s14-in-f14.1e100.net (172.217.25.238): icmp_seq=1 ttl=53 time=8.01 ms
64 bytes from nrt12s14-in-f14.1e100.net (172.217.25.238): icmp_seq=2 ttl=53 time=9.52 ms
64 bytes from nrt12s14-in-f14.1e100.net (172.217.25.238): icmp_seq=3 ttl=53 time=9.50 ms
64 bytes from nrt12s14-in-f14.1e100.net (172.217.25.238): icmp_seq=4 ttl=53 time=9.66 ms
64 bytes from nrt12s14-in-f14.1e100.net (172.217.25.238): icmp_seq=5 ttl=53 time=9.47 ms
^C
--- google.com ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4005ms
rtt min/avg/max/mdev = 8.017/9.238/9.668/0.614 ms

計測と送信プログラム

通信量の目安を計算します。このプログラムでは、1分毎の消費電力量を送信します。ひとつのメッセージは、タイムスタンプと4チャンネル分の消費電力をアスキーテキストで表して、メッセージのサイズは50バイト程度です。5分間の情報を貯めてから、bz2 圧縮して送信します。半分程度に圧縮できるので、実際に送信している量は、120バイト程なので、3.2 bps 程度です。

TCP/IP、HTTP、SSL の各オーバーヘッドを考えると、実際に通信させてみないとなんともいえません。

so-net の 0sim は、毎月500Mbyteまでの通信料が無料です。毎秒に換算すると、1493 bps です。

通信量が多いようであれば、送信間隔を伸ばして対応します。

ラズパイに 3GPI とADコンバーターボードを指し、プログラムを書きます。

sudo mkdir -p /opt/whmonitor/bin/
sudo vi /opt/whmonitor/bin/whmonitor.py
sudo chmod +x /opt/whmonitor/bin/whmonitor.py
/opt/whmonitor/bin/whmonitor.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

import time
import math
from collections import deque
import copy
import sys
import datetime
import urllib
import urllib2
import bz2
import Adafruit_ADS1x15
import RPi.GPIO as GPIO


# 電力計測の関連定数
MEASURE_INTERVAL = 60
CHANNEL_CHANGE_INTERVAL = 10
CONVERSION_CONSTANT = 30 # 100 ohm
VOLTAGE = 100
TO_KILOWATT = 0.001

# ハートビート関連定数 ( slee-Pi 用)
FLIP_INTERVAL = 500
HEARTBEAT_GPIO = 5

# ADC1015 関連定数
I2C_BUSNUM = 1
ADS1015_I2C_BASE_ADDRESS = 0x48
SENSORS = 4
PGA_GAIN = 2
SAMPLING_RATE = 3300

# データ送信関連定数
MINIMUM_UPLOAD_QUEUE_LENGTH = 5
UPLOAD_URL = "https://[データ受信サーバーのホスト名]/whupdate.cgi"
BASICAUTH_ID = "[基本認証のID]"
BASICAUTH_PASS = "[基本認証のパスワード]"
COMPRESSION_LEVEL = 9



class Recorder:
    """このクラスは、データを Web サーバーへアップロードします。
    内部にキューを持ち、record() メソッドで受け取ったリストを保持します。
    キューが十分な長さになると、データを Web サーバーへ送信します。
    送信に失敗した場合は、次の記録タイミングで送信を試みます。
    """

    def __init__(self, url, ba_id, ba_pass, minimum_upload_queue_length = 1):
        self.data_queue = deque()

        self.url = url
        self.ba_id = ba_id
        self.ba_pass = ba_pass
        self.compression_level = COMPRESSION_LEVEL
        self.minimum_upload_queue_length = minimum_upload_queue_length


    def record(self, data):
        self.data_queue.append(self.__build_message(data))
        tmp_queue=copy.deepcopy(self.data_queue)

        try:
            if self.minimum_upload_queue_length <= len(tmp_queue) :
                self.__send_queue(tmp_queue)
                for dummy in tmp_queue:
                    self.data_queue.popleft()
        except:
            print("=== データを送信できませんでした。 ===")
            d=datetime.datetime.today()
            print d.strftime("%Y-%m-%d %H:%M:%S"),'\n'


    def __send_queue(self, queue):
        send_string = ""
        for data in queue:
            send_string += " " + data
        response=self.__send_string(send_string)


    def __send_string(self, message):

        pswd_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
        pswd_mgr.add_password(None, self.url, self.ba_id, self.ba_pass)
        opener = urllib2.build_opener(urllib2.HTTPSHandler(),
            urllib2.HTTPBasicAuthHandler(pswd_mgr))
        urllib2.install_opener(opener)

        request = urllib2.Request(self.url)
        request.add_data(bz2.compress(message,self.compression_level))

        response = urllib2.urlopen(request)
        return response.read()


    def __build_message(self, data):
        message = str(int(time.time()))
        for value in data:
            message += ":" + str(value)
        return message


class Knocker:
    """このクラスは、GPIO の出力電圧を変化させます。
    flip() メソッドが呼ばれる毎に、指定の GPIO ピンの出力をフリップします。
    """

    def __init__(self, port):
        self.port = port
        GPIO.setwarnings(False)
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(port,GPIO.OUT)


    def flip(self):
        GPIO.output(self.port,(GPIO.input(self.port)+1)%2)



class IntervalTimer(object):
    """このクラスは、全速力で busyloop() メソッドを実行します。
    また、コンストラクタで指定した秒毎に longloop() メソッドを実行します
    """

    def __init__(self, interval = 1):
        self.interval = interval

    def busyloop(self):
        pass

    def longloop(self):
        print time.time()-self.start

    def loop(self):
        self.start=int(time.time())
        prev=self.start
        while True:
            while time.time()-prev < self.interval:
                self.busyloop()
            prev = int(time.time())
            self.longloop()


class Sampler(IntervalTimer):

    """このクラスは、メインクラスです。
    AD コンバーターの管理、データ送信、ハートビートの管理をします。
    """

    def __init__(self,interval):
        super(Sampler, self).__init__(interval)
        self.knocker = Knocker(HEARTBEAT_GPIO)
        self.recorder = Recorder(UPLOAD_URL, BASICAUTH_ID, BASICAUTH_PASS, \
            MINIMUM_UPLOAD_QUEUE_LENGTH)
        self.adc = Adafruit_ADS1x15.ADS1015(\
            address=ADS1015_I2C_BASE_ADDRESS,busnum=I2C_BUSNUM)
        self.reset_valiables()


    def reset_valiables(self):
        self.sensor = 0
        self.samples = 0
        self.sample_buffer = [0]*SENSORS
        self.sample_length = [0]*SENSORS
        self.watt = [0]*SENSORS


    def busyloop(self):
        if self.samples%CHANNEL_CHANGE_INTERVAL == 0 :
            self.sensor=(self.samples/CHANNEL_CHANGE_INTERVAL)%SENSORS
            self.adc.start_adc(self.sensor, gain=PGA_GAIN, \
                data_rate=SAMPLING_RATE)
            time.sleep(2.0/SAMPLING_RATE)

        current=self.adc.get_last_result()*CONVERSION_CONSTANT

        self.sample_buffer[self.sensor] += current * current
        self.sample_length[self.sensor] += 1
        if self.samples%FLIP_INTERVAL == 0:
            self.knocker.flip()
        self.samples += 1



    def longloop(self):
        for self.sensor in range(SENSORS):
            if self.sample_length[self.sensor] == 0:
                self.watt[self.sensor]=0
            else:
                self.watt[self.sensor]=\
                    math.sqrt(self.sample_buffer[self.sensor]\
                    /self.sample_length[self.sensor])*VOLTAGE*TO_KILOWATT

        self.recorder.record(self.watt)
        self.reset_valiables()



sampler = Sampler(MEASURE_INTERVAL)
sampler.loop()

サービス登録

サービス登録情報を作成します。

sudo vi /etc/systemd/system/whmonitor.service

内容はこうしました。プログラムがストップしても自動的に再起動してくれます。

/etc/systemd/system/whmonitor.service
[Unit]
Description = watt hour monitor

[Service]
ExecStart = /opt/whmonitor/bin/whmonitor.py
Restart = always
Type = simple

[Install]
WantedBy = multi-user.target

サービスを有効にし、実行します。

sudo systemctl enable whmonitor
sudo systemctl start whmonitor

slee-Pi のセットアップ

slee-Pi は、sleepi-firmware をインストールすることで OS の死活監視を行ってくれます。標準のファームウェアでは、OS が GPIO5 に HEARTBEAT を送り、slee-Pi が監視しているようです。HEARTBEAT が一定時間確認できないと、Raspberry Pi 停止させたり再起動させたりします。

アプリケーションの死活監視をしたいので、標準ファームウェアはキャンセルしてしまいます。アプリケーションのループ内で、GPIO5 ピンの ON/OFF を行うことで、slee-Pi に死活監視してもらいます。

sudo bash -c 'echo "deb http://mechatrax.github.io/sleepi ./" > /etc/apt/sources.list.d/sleepi.list'
sudo apt-get update
sudo apt-get install sleepi-archive-keyring
sudo apt-get update
sudo apt-get install sleepi-firmware
sudo shutdown -h now
  1. Raspberry Pi (3GPI) の電源を抜く
  2. slee-Pi、3GPI、ADコンバーターボード を接続する
  3. slee-Pi に電源を接続する
  4. slee-Pi のブートボタンを押す
# モニタープログラムを停止
sudo systemctl stop whmonitor.service
# i2c のクロックを下げる
sudo rmmod i2c_bcm2708 
sudo modprobe i2c_bcm2708 baudrate=100000
# ハートビートがなくなってからシャットダウンするまでのタイムアウト秒数を確認する
printf "%d\n" `i2cget -y 1 0x69 6 b`
# タイムアウトを120秒に設定
i2cset -y 1 0x69 6 120 
# 動作モードを「再起動有効、無応答時電源再投入」に設定
i2cset -y 1 0x69 5 1
# i2c のクロックを上げる
sudo rmmod i2c_bcm2708
sudo modprobe i2c_bcm2708 baudrate=300000
# 消費電力量モニタープログラムを開始
sudo systemctl start whmonitor.service
# sleepi-firmware をアンインストール
sudo apt-get remove sleepi-firmware
# reboot
sudo reboot

データベースと受信プログラム

さくらインターネットのレンタルサーバ「スタンダード」で受信します。

データベース作成

create.sh
#!/bin/sh

/usr/local/bin/rrdtool-fix create  \
        /データベースの保存ディレクトリ/watt.rrd --step 60 \
        DS:sensor0:GAUGE:120:0:1000000 \
        DS:sensor1:GAUGE:120:0:1000000 \
        DS:sensor2:GAUGE:120:0:1000000 \
        DS:sensor3:GAUGE:120:0:1000000 \
        RRA:AVERAGE:0.5:1:1054080 \
        RRA:MAX:0.5:1:1054080 \
        RRA:MIN:0.5:1:1054080 \
        RRA:LAST:0.5:1:1054080

受信プログラム

whupdate.cgi
#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import cgi
import cgitb
import os
import sys
import subprocess
import shlex
import bz2
import re

cgitb.enable()

if os.environ['REQUEST_METHOD'] != "POST":
    print 'invalid access'
    sys.exit()

rrd="/データベースの保存ディレクトリ/watt.rrd"
lines = bz2.decompress(sys.stdin.read())
if re.match("^[0-9 :\.]+$",lines):
    cmd='/usr/local/bin/rrdtool-fix update {rrd} {param}'.format(rrd=rrd,param=lines)
    ret=subprocess.check_output(shlex.split(cmd))

print ('Content-type: text/html; charset=UTF-8')
print ("\r\n\r\n")
print

グラフ表示

IPAフォントをダウンロードします。IPAゴシック(Pゴシックじゃないほう)がおすすめです。フォントが等幅なので縦のラインが崩れにくいです。ipag.ttf をグラフ表示プログラムと同じディレクトリに保存します。

graph_total.cgi
#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import subprocess
import os
import shlex
import cgi
import config
import sys
import re

os.environ["RRD_DEFAULT_FONT"]="/フォント保存ディレクトリ/ipag.ttf"
os.environ['LANG'] = 'ja_JP.UTF-8'

title=""
end="now"
start="now-12hour"
width=800
height=200


form=cgi.FieldStorage()
strnum=re.compile("^[0-9a-zA-Z\-]+$")

if form.has_key("title") and strnum.match(form["title"].value):
        title=" "+form["title"].value
if form.has_key("end") and strnum.match(form["end"].value):
    end=form["end"].value
if form.has_key("start") and strnum.match(form["start"].value):
        start=form["start"].value
if form.has_key("width") and form["width"].value.isdigit():
        width=form["width"].value
if form.has_key("height") and form["height"].value.isdigit():
        height=form["height"].value
if form.has_key("graph_id") and form["graph_id"].value.isdigit():
        graph_id=int(form["graph_id"].value)

cmd_src='/usr/local/bin/rrdtool-fix graph - \
            --start {start} --end {end} \
            --width {width} --height {height} \
            --title "電気料金{title}" \
            --font "DEFAULT:9:" \
            --font "TITLE:12:" \
            --font "LEGEND:12:" \
            --lower-limit 0 --step 60 --slope-mode \
            --vertical-label "ワット" \
            DEF:s0_avg={rrd}:sensor0:AVERAGE \
            DEF:s1_avg={rrd}:sensor1:AVERAGE \
            DEF:s0_max={rrd}:sensor0:MAX \
            DEF:s1_max={rrd}:sensor1:MAX \
            DEF:s0_min={rrd}:sensor0:MIN \
            DEF:s1_min={rrd}:sensor1:MIN \
            CDEF:main_avg=s0_avg,s1_avg,+ \
            CDEF:main_max=s0_max,s1_max,+ \
            CDEF:main_min=s0_min,s1_min,+ \
            CDEF:main_yps=main_avg,3600,1000,*,/,25,* \
            VDEF:main_yen=main_yps,TOTAL \
            LINE1:main_avg#00ff00ff:"" \
            AREA:main_avg#00ff0033:"" \
            COMMENT:" \\n" \
            GPRINT:main_yen:"       電気料金%7.1lf 円" '

rrd="/データベース保存ディレクトリ/watt.rrd"
cmd=cmd_src\
    .format(rrd=rrd,width=width,height=height,start=start,end=end,title=title)
output=subprocess.check_output(shlex.split(cmd))

print ('Content-type: image/gif')
print
print output

graph_para.cgi
#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import subprocess
import os
import shlex
import cgi
import config
import sys
import re

os.environ["RRD_DEFAULT_FONT"]="/フォントを保存したディレクトリ/ipag.ttf"
os.environ['LANG'] = 'ja_JP.UTF-8'

title=""
end="now"
start="now-12hour"
width=800
height=200
graph_id=0


form=cgi.FieldStorage()
strnum=re.compile("^[0-9a-zA-Z\-]+$")

if form.has_key("title") and strnum.match(form["title"].value):
        title=" "+form["title"].value
if form.has_key("end") and strnum.match(form["end"].value):
    end=form["end"].value
if form.has_key("start") and strnum.match(form["start"].value):
        start=form["start"].value
if form.has_key("width") and form["width"].value.isdigit():
        width=form["width"].value
if form.has_key("height") and form["height"].value.isdigit():
        height=form["height"].value
if form.has_key("graph_id") and form["graph_id"].value.isdigit():
        graph_id=int(form["graph_id"].value)

cmd_src='/usr/local/bin/rrdtool-fix graph - \
            --start {start} --end {end} \
            --width {width} --height {height} \
            --title "回路ごとの消費電力量{title}" \
            --font "DEFAULT:9:" \
            --font "TITLE:12:" \
            --font "LEGEND:10:" \
            --lower-limit 0 --step 60 --slope-mode \
            --vertical-label "ワット" \
            DEF:s0_avg={rrd}:sensor0:AVERAGE \
            DEF:s1_avg={rrd}:sensor1:AVERAGE \
            DEF:s2_avg={rrd}:sensor2:AVERAGE \
            DEF:s3_avg={rrd}:sensor3:AVERAGE \
            DEF:s0_max={rrd}:sensor0:MAX \
            DEF:s1_max={rrd}:sensor1:MAX \
            DEF:s2_max={rrd}:sensor2:MAX \
            DEF:s3_max={rrd}:sensor3:MAX \
            DEF:s0_min={rrd}:sensor0:MIN \
            DEF:s1_min={rrd}:sensor1:MIN \
            DEF:s2_min={rrd}:sensor2:MIN \
            DEF:s3_min={rrd}:sensor3:MIN \
            CDEF:total_avg=s0_avg,s1_avg,+ \
            CDEF:total_max=s0_max,s1_max,+ \
            CDEF:total_min=s0_min,s1_min,+ \
            CDEF:total_yps=total_avg,3600,1000,*,/,25,* \
            VDEF:total_yen=total_yps,TOTAL \
            CDEF:others_avg=s0_avg,s1_avg,+,s2_avg,s3_avg,+,- \
            CDEF:others_min=s0_min,s1_min,+,s2_min,s3_min,+,- \
            CDEF:others_max=s0_max,s1_max,+,s2_max,s3_max,+,- \
            CDEF:s2_yps=s2_avg,3600,1000,*,/,25,* \
            VDEF:s2_yen=s2_yps,TOTAL \
            CDEF:s3_yps=s3_avg,3600,1000,*,/,25,* \
            VDEF:s3_yen=s3_yps,TOTAL \
            CDEF:others_yps=others_avg,3600,1000,*,/,25,* \
            VDEF:others_yen=others_yps,TOTAL \
            COMMENT:"__________\t_cost___ _cur_ _min_ _max_ _ave_"\
            COMMENT:"\\n" \
            LINE1:total_avg#00ff0077:"トータル\t\g" \
            GPRINT:total_yen:"%7.1lf\g"  \
            GPRINT:total_avg:LAST:" %5.0lf\g"  \
            GPRINT:total_min:MIN:" %5.0lf\g"  \
            GPRINT:total_max:MAX:" %5.0lf\g"  \
            GPRINT:total_avg:AVERAGE:" %5.0lf\g"  \
            COMMENT:"\\n" \
            LINE1:s2_avg#00aa88:"リビング\t\g" \
            GPRINT:s2_yen:"%7.1lf\g"  \
            GPRINT:s2_avg:LAST:" %5.0lf\g"  \
            GPRINT:s2_min:MIN:" %5.0lf\g"  \
            GPRINT:s2_max:MAX:" %5.0lf\g"  \
            GPRINT:s2_avg:AVERAGE:" %5.0lf\g"  \
            COMMENT:"\\n" \
            LINE1:s3_avg#8800aa:"キッチン\t\g" \
            GPRINT:s3_yen:"%7.1lf\g"  \
            GPRINT:s3_avg:LAST:" %5.0lf\g" \
            GPRINT:s3_min:MIN:" %5.0lf\g" \
            GPRINT:s3_max:MAX:" %5.0lf\g" \
            GPRINT:s3_avg:AVERAGE:" %5.0lf\g" \
            COMMENT:"\\n" \
            LINE1:others_avg#aa8800:"その他  \t\g" \
            GPRINT:others_yen:"%7.1lf\g"  \
            GPRINT:others_avg:LAST:" %5.0lf\g"  \
            GPRINT:others_min:MIN:" %5.0lf\g"  \
            GPRINT:others_max:MAX:" %5.0lf\g"  \
            GPRINT:others_avg:AVERAGE:" %5.0lf"  \
            '

rrd="/データベース保存ディレクトリ/watt.rrd"
cmd=cmd_src\
    .format(rrd=rrd,width=width,height=height,start=start,end=end,title=title)
output=subprocess.check_output(shlex.split(cmd))


print ('Content-type: image/gif')
print
print output

閲覧

5分ほどしてから以下のURLにアクセスしてグラフを確認します。

http://[公開サーバー]/[ディレクトリ]/graph_total.cgi
http://[公開サーバー]/[ディレクトリ]/graph_para.cgi

完成

ブラウザでグラフが表示できました。これで待機電力や大食い家電をあぶりだせます。

graph_total.gif

graph_para.gif

まとめ

全体の構成をおさらいします。

データの流れ

データの流れは2系統に分かれます。ここでは、データ収集系統とデータ表示系統と表記しました。

データ収集系統

  1. 電流センサーと抵抗で電流を電圧へ変換
  2. ADS1015 で増幅しサンプリング
  3. ラズパイでサンプリングデータを取得(I2C バスと ADS1x15ライブラリを利用)
  4. 演算
  5. 3GPI 経由で、データ管理サーバーの CGI へ送信
  6. CGI が受信し、RRDtool のラウンドロビンデータベースへ保存

データ表示系統

  1. ブラウザが CGI へグラフ表示要求
  2. CGI が RRDtool へグラフを生成させてブラウザへ送信
  3. ブラウザで表示

プログラムと設定

開発したプログラム、設定ファイル、必要な外部ファイルをまとめます。

ラズパイ側

  • 計測プログラム whmonitor.py
  • サービス設定ファイル /etc/systemd/system/whmonitor.service

サーバー側

  • RRDtool データベース生成プログラム create.sh
  • データ受信プログラム whupdate.cgi
  • グラフ表示プログラム graph_total.cgi
  • グラフ表示プログラム graph_para.cgi
  • フォントファイル ipag.ttf

感想

無事に消費電力量が計測できるようになりました。3G 通信ができて、アプリケーションの動作監視ができて、消費電力量が測れて、グラフ表示できました。消費電力量計測は、実用的でハードウェアの用意が楽なので、お勧めの電子工作です。ただし、分電盤での作業や、センサー周りの絶縁には気をつけて下さい。

作業中の感電、設置機器による漏電、短絡による火災など、どれも死亡につながるリスクがあります。分電盤への電流センサー取り付けに対して、免許が必要な電気工事に該当するかは私には判断できません。第二種電気工事士の勉強をしましたが、解釈しだいで必要とも不要とも読み取れます。どちらにしてもリスクは把握しておいたほうが良いので、第二種電気工事士に相当する知識を持っていると安心です。SHARP の Web サイトでは、「お客様自信で取り付けが可能な」例を示しています。→単回路CTセンサーユニットは、自分で設置できますか?

計測結果について

データを取り始めると、ピーク電力が大きい機器が気になりました。一回あたりの電気料金を計算します。電力単価は、2015年の実績から 1 kWh あたり 25 円としています。

|使用機器|平均消費電力[w]|平均利用時間[h]|消費電力量[kWh]|電気料金[円]|
|:--|--:|--:|--:|--:|--:|
|電子レンジ|1500|0.033|0.050|1.3|
|浴室乾燥機|800|4.0|3.2|80|
|布団乾燥機|600|2.0|1.2|30|
|諸々待機電力|80|24|1.2|30|

計測してから2週間経つと、一日当たりの大まかな料金が見えてきました。

|品目|平均消費電力[w]|平均利用時間[h]|消費電力量[kWh]|電気料金[円]|
|:--|--:|--:|--:|--:|--:|
|トータル|250|24|6.0|150|
|待機電力|50|18|0.9|23|

これによると、15% が待機電力でした。待機電力にカウントした機器は、電子レンジ、炊飯器、ヨーグルトメーカー、ホームベーカリー、テレビ、WiFi ルータ、WiFi リピーター、PC用スピーカー、外付けHDD、ノートPC(電源OFF)、デスクトップPC(電源OFF)、PC用モニター、プリンターです。リストアップすると結構ありますが、時々しか使わないものも含めて、コンセントは接続しっぱなしにしていました。使わないときにコンセントを抜くというのを徹底すると、電気料金が15%下がります。2015年の年間電気料金は、60,000円だったので、51,000円に減らせたかもしれません。

電気料金を下げるためには、待機電力対策が有効だと感じました。

消費電力データについて

電力の増減パターンを見ていると、何時に何をしたのかが大よそわかるようになりました。エアコンの利用。電子レンジの利用。気温による冷蔵庫の負荷。炊飯器の利用。照明点灯。起床時刻、就寝時刻。夜間のトイレ利用などなど。スマートメーターの普及により、電力会社が電力利用推移を取得できるようになります。各家庭の電力利用状況を分析することで、家庭で何が起こっているのかを把握できると思います。特に電子レンジのパターンは特徴的です。何時何分に使ったとか、一日に何回使ったという情報を得ることができます。家電メーカーには垂涎のデータかもしれません。

稼動しているラズパイは、毎秒1600回ほどサンプリングできているようです。電源周波数を 60Hz だとしても、1サイクルあたり26回サンプリングできます。何とか波形分析ができるレベルだと思うので、消費している機器が、モーター等の負荷か、インバーターなのか、ヒーター等かという分析もできるかもしれません。

開発について

初めてラズパイを使いましたが、わりと順調に動いてくれています。また、情報が多くてありがたいです。先人に感謝します。連続稼動させても心配ありませんでした。Python も情報が多く学習しやすかったです。ライブラリも多く、人気がある理由を感じました。Python のお作法に則ったプログラミングはできていないと思いますので、ちと恥ずかしいです。

開発時間トータルで40から50時間程度です。ハードウェア開発が部品選定、設計、製造で5時間ほどでした。ADS1015 のおかげで、ハードウェア開発にかかる時間は非常に短かったのが印象的です。回路の規模が小さかったので、最初から半田付けしましたが、ブレッドボードで組んだらもっと短時間で開発できると思います。

ソフトウェア開発に時間がかかった印象です。特に RRDtool の使い方にハマりました。凝ったことをしようとすると、情報が少なく試行錯誤が必要でした。特に VDEF 構文の使い方を調べるのに時間がかかった印象があります。3GPI は、何も考えなくても動いてくれて、非常に便利です。NetworkManager の使い方を知っていると、バックアップ回線の設定などより高度な利用ができると思いました。slee-Pi は、本来は OS の稼動監視をする機器ですが、アプリケーションの稼動監視を行いたかったので、メーカーのドライバーは使いませんでした。アプリケーションのループ内でハートビートを生成しているので、アプリケーションが停止すると自動的にラズパイを再起動してくれます。ラズパイの watchdog 機能も使えますが、外部から強制再起動をしてくれるのは安心です。

注意事項

分電盤にセンサーを取り付けるのは、作業中の感電、設置機器による漏電、短絡による火災など、死亡につながるリスクがあります。実施の可否を含めて十分注意してご判断ください。

144
168
11

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
144
168

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?