CO2 (二酸化炭素) 濃度に反応する「自動換気システム」を2通り自作してみた

CO2 (二酸化炭素) 濃度に反応する「自動換気システム」を2通り自作してみた
Page content

換気を自動化

前回の冬は、自宅の部屋内のCO2 (二酸化炭素) 濃度を低く保つことに気をつけ始めた頃で、下記の仕組みを作りました。

換気が必要な時を声が教えてくれるこの仕組みは画期的、と、自画自賛ですが最初は考えていました。しかし実際に運用してみて、“Googleさん”から「二酸化炭素濃度は現在○○ppmです。換気をしましょう!」と言われるたびに、家族の誰かが換気扇をONしたり窓の開閉を調整しなければならない点には不便を感じました。次第に面倒になって腰が重くなります。また、就眠中にも適度な換気をしたいという欲も出ます。

そこで現在は、築ウン十年の賃貸マンションの住環境 (今どきの24時間換気システムは未設置) において、換気を自動で行ってくれる仕組みとしてどのようなものが安価に作れるかを模索中です。本記事は現時点での模索の記録です。

その1) リビングでは

自宅のリビングは実際「LDK」の一部であり、キッチンと同一空間になっています。キッチンのガスコンロ設置箇所の上部には「レンジフードファン」 (ビルドイン型の換気扇) が設置されているので、この換気扇の電源ON/OFFを、リビングの二酸化炭素濃度を測定しているラズパイ4で制御して、換気を自動的に行えるようにしました。

換気扇の電源ON/OFFはスマートプラグを使って行います。ビルドイン型の配線は通常見えませんがスマートプラグをどこに配線しているかというと、換気扇上部の壁にネジ止めされている板を試しに外してみたら、内部から現れた、換気扇の電源プラグとコンセントの間の部分です (換気扇メーカーのサイトにて据付工事説明書PDFを確認してみると、電源供給がプラグ式である旨が「8.電気工事」部分に記載されていました)。取り付けたスマートプラグは、板を外さなくても直接目視したり必要時には手で操作できるように、延長コードを使って内部から外へ引っ張り出した状態にしています。

項目内容
換気扇MITSUBISHI V-316KP5
スマートプラグhyleton 312 ※公式サイトが見つからない
CO2濃度センサー・スマートプラグ制御NDIR (非分散型赤外線吸収法) 式のWinsen MH-Z19C + Raspberry Pi 4 Model B
制御ロジック・閾値cronでの定期実行 / 0~7時台: 1050ppm, 8~17時台: 850ppm, 18~19時台: 850ppm (調理時間帯の換気を止めないようON制御のみでOFFはしない), 20~23時台: 950ppm
制御スクリプトco2_fan.shswitch_tuya.py
三菱電機 (MITSUBISHI) レンジフードファン 浅形 大風量タイプ V-316KP5

三菱電機 (MITSUBISHI) レンジフードファン 浅形 大風量タイプ V-316KP5

ブランド: 三菱電機(MITSUBISHI ELECTRIC)

【国内正規代理店品】Raspberry Pi4 ModelB 4GB ラズベリーパイ4 技適対応品【RS・OKdo版】

【国内正規代理店品】Raspberry Pi4 ModelB 4GB ラズベリーパイ4 技適対応品【RS・OKdo版】

ブランド: Raspberry Pi

Nrpfell MH-Z19C IR赤外線CO2センサーモジュール二酸化炭素ガスセンサーNDIR、CO2モニター400-5Ppm UART PWM出力用

Nrpfell MH-Z19C IR赤外線CO2センサーモジュール二酸化炭素ガスセンサーNDIR、CO2モニター400-5Ppm UART PWM出力用

ブランド: Nrpfell

実際の濃度制御の様子

そろそろ冷え込んできている10月のある日の、リビングでの二酸化炭素濃度の24時間分のグラフを次の画像 (緑色の線) で示します。リビングと家族が就眠する寝室の間には空気の流れがあり、換気をしていない場合にはリビングの二酸化炭素濃度もおそらくかなり上昇しますが、自動換気システムの働きにより、濃度グラフはギザギザ状に上下しながら上限は約1000ppmに留まっていることが分かります。

追記 [2021-10-27]

その2) 仕事部屋では

仕事部屋である書斎の窓は、上部に換気用の小窓が設けられているタイプです。小窓の高さは12cmほどで、そこにお誂え向きのPC用12cmファンをはめ込みました。ファンへの電源供給をスマートプラグで行うようにして、キッチンと同様に、仕事部屋の二酸化炭素濃度を測定しているラズパイZeroで制御します。

ファンの取り付け方向 (吸気 or 排気) は、屋内へ空気を取り込む方向 (吸気) にしています。これはなぜかというと、レイアウト的に自宅の中心にあるリビングの換気扇が稼働して屋外へ排気している時には、リビングは負圧状態となり、リビングと廊下で繋がっている仕事部屋の空気はリビングへと流れていくでしょうから、仕事部屋のファンは、リビングの換気扇の働きに逆らわないようにしました。

また、ラズパイZeroには2.7インチの電子ペーパーモジュールを接続しており、温度・湿度・大気圧・二酸化炭素濃度の計測値と、算出した不快指数が常時表示されるようにしています。電子ペーパー画面右上に表示している「*」印は、二酸化炭素濃度が閾値を超えておりファンをON制御している状態を示します。

項目内容
PC用12cmファンAinex CFZ-120L (1000rpm) ※下記の写真はCFZ-120FA (1600rpm) を試しているときに撮影したものです。その後、稼働音がより小さい1000rpmのファンに切り替えました。[2021-10-21 19:55]
5V→12V昇圧ケーブルAinex CA-USB12V
スマートプラグMeross MSS110JP
CO2濃度センサー・スマートプラグ制御NDIR (非分散型赤外線吸収法) 式のWinsen MH-Z19C + Raspberry Pi Zero WH
制御ロジック・閾値cronでの定期実行 / 0~7時台: 950ppm, 8~18時台: 750ppm (在宅勤務時間帯は比較的低めに保ちたい), 19~23時台: 850ppm
制御スクリプトco2_fan.shswitch_meross.py
アイネックス OMEGA TYPHOON 120mm 標準タイプ CFZ-120FA

アイネックス OMEGA TYPHOON 120mm 標準タイプ CFZ-120FA

ブランド: アイネックス(AINEX)

アイネックス Omega Typhoon G 120mm 究極静音タイプ [ 回転数900RPM±200、ノイズ10.8dB(A) ] CFZ-120GLA

アイネックス Omega Typhoon G 120mm 究極静音タイプ [ 回転数900RPM±200、ノイズ10.8dB(A) ] CFZ-120GLA

ブランド: アイネックス(AINEX)

アイネックス ファン用 USB電源 変換ケーブル 12V 昇圧タイプ [ 1m ] CA-USB12V

アイネックス ファン用 USB電源 変換ケーブル 12V 昇圧タイプ [ 1m ] CA-USB12V

ブランド: AINEX

【Amazon Alexa認定】 Meross スマートプラグ (2個セット) WiFiスマートコンセント遠隔操作 Echo シリーズ/Googleホーム 対応 音声コントロール ハブ不要 MSS110JP

【Amazon Alexa認定】 Meross スマートプラグ (2個セット) WiFiスマートコンセント遠隔操作 Echo シリーズ/Googleホーム 対応 音声コントロール ハブ不要 MSS110JP

Meross

2.7 インチ 264X176 SPI 電子ペーパー モジュール E-inK E-Paper EPaper ディスプレイ 画面 HAT for PRI Raspberry Pi Zero W WH ラズベリーパイゼロ 3B Plus 4 for Arduino Nucleo Jetson Nano B01 2GB

2.7 インチ 264X176 SPI 電子ペーパー モジュール E-inK E-Paper EPaper ディスプレイ 画面 HAT for PRI Raspberry Pi Zero W WH ラズベリーパイゼロ 3B Plus 4 for Arduino Nucleo Jetson Nano B01 2GB

ブランド: STEMDIY

家族向けの情報ダッシュボード

家族からは「室内の現在の二酸化炭素濃度を知りたい」という要望があったので、リビングで確認できる仕掛けを考えました。具体的には例えば、自宅で測定している各種の時系列データを可視化・グラフ化しておき、リビングのテレビに映せば良さそう。子どもも一応見る可能性があるので、映し出すための操作はできるだけ簡素であることが望ましいです。

リビングのテレビにどうやって映すか

次の組み合わせで仕掛けを作りました。

  1. 次の各計測データを自宅のラズパイ2台からAmbientへ常時送信して集約します
    • 温度・湿度・大気圧・不快指数・二酸化炭素濃度 (リビングと仕事部屋の各2箇所)
    • 家全体の電力量 (Nature Remo E liteで取得)
  2. 上記データを可視化する非公開ダッシュボードをAmbientに作ります
  3. Ambientに作ったダッシュボードを、クラウドサーバAに構築したデスクトップ環境のウェブブラウザに常時表示させておきます
  4. クラウドサーバAのウェブブラウザ画面を、クラウドサーバBに構築したscreegoを用いて配信します
  5. screegoの配信URLを、リビングのLG製のテレビに内蔵されているウェブブラウザにブックマークしておきます

こうして、テレビのリモコンを操作してウェブブラウザを起動してひとつブックマークを開くことで、各計測データをグラフで一覧できるダッシュボードをテレビに素早く映すことができます。Ambientのダッシュボードは非公開を維持、テレビでの文字入力は行わない (Ambientのログイン操作はテレビでは避けたい)、テレビでは閲覧オンリー、といった私の希望を叶えるようにすると、込み入っていますが上記の仕掛けに行き着きました。

スマートエナジーハブ Nature Remo E lite

スマートエナジーハブ Nature Remo E lite

Nature Remo

感想

二酸化炭素濃度を含む自宅の環境測定に関する仕掛けを、約一年前から、ラズパイ (Raspberry Pi) を絡めてゼロからコツコツ作ってきました。その仕掛けは、今回加えた「換気の自動化」機能によって、人間に測定結果を知らせるだけではない、環境と自ら相互作用する力を初めて持ったことになります。

大げさですがこのように考えるとちょっと感慨深いですし、また新しい何かを思いついたら実装して、生活上の便利さを増してみたいと思います。

なお、今回のネタに関連する過去のブログ記事を列挙してみますと次の通りです。

そういえば、電子ペーパーモジュール (Waveshare 2.7inch E-Ink display HAT) のラズパイとの接続や、スマートメーターから電力量を読み取るNature Remo E liteの導入、screego導入の紹介記事は、未だ書けていないのでまた別の機会に……。

参考スクリプト

co2_fan.sh

引数に指定された二酸化炭素濃度の現在の値と閾値とを比較し、結果にしたがってスマートプラグを制御するスクリプトを呼び出します。

#!/bin/bash

# co2_fan.sh (ver.20211002)
# usage: $0 [CO2_NOW(ppm)] [CO2_THRESHOLD(ppm)] [{ON_only | OFF_only}]

if [[ $# -lt 2 ]]; then
	exit 1
fi
CO2_NOW=$1
if [[ ${CO2_NOW} -lt 1 ]]; then
	exit 2
fi
CO2_THRESHOLD=$2 # 閾値(ppm)
CO2_MIN=400 # 測定上の最低値(ppm)
CO2_TMP=`echo "${CO2_MIN} + (( ${CO2_THRESHOLD} - ${CO2_MIN} ) * 0.93)" | bc` # 下げたい値(ppm)
CO2_SAFE=`echo "${CO2_TMP}" | awk '{printf("%d", $1 + 0.5)}'` # 下げたい値の整数値(ppm)
FILE_STATUS='/tmp/fan_status.dat'
MODE_SWITCH=$3 # ON_only or OFF_only
FAN_KEEPSEC=300 # FILE_STATUSの有効時間(sec)

if [[ ${HOSTNAME} == 'raspi4' ]]; then
	CMD_FAN='switch_tuya.py'
elif [[ ${HOSTNAME} == 'raspi0' ]]; then
	CMD_FAN='switch_meross.py'
fi

func_switchfan() {
	if [[ "$1" == 'on' ]]; then
		${CMD_FAN} "ON"
	elif [[ "$1" == 'off' ]]; then
		${CMD_FAN} "OFF"
	fi
}
func_savestatus() {
	echo "$1" > ${FILE_STATUS}
}
func_checkstatus() {
	local FAN_STATUS="$1"
	if [[ -e ${FILE_STATUS} ]]; then
		FAN_SWITCHING=0
		local FAN_ELAPSEDSEC=$(( `date "+%s"` - `date "+%s" -r ${FILE_STATUS}` ))
		local FAN_LASTSTATUS=`cat ${FILE_STATUS}`
		if [[ ${FAN_ELAPSEDSEC} -gt ${FAN_KEEPSEC} ]]; then
			FAN_SWITCHING=1
		elif [[ ${FAN_STATUS} != ${FAN_LASTSTATUS} ]]; then
			FAN_SWITCHING=1
		fi
	else
		FAN_SWITCHING=1
	fi
}

if [[ ${CO2_NOW} -ge ${CO2_SAFE} ]]; then # 閾値以上
	if [[ ${MODE_SWITCH} != 'OFF_only' ]]; then
		func_checkstatus 'on'
		if [[ ${FAN_SWITCHING} -eq 1 ]]; then
			func_switchfan 'on'
			func_savestatus 'on'
		fi
	fi
elif [[ ${CO2_NOW} -ge ${CO2_MIN} ]]; then # 最低値以上
	if [[ ${MODE_SWITCH} != 'ON_only' ]]; then
		func_checkstatus 'off'
		if [[ ${FAN_SWITCHING} -eq 1 ]]; then
			func_switchfan 'off'
			func_savestatus 'off'
		fi
	fi
else # 何らかの異常値
	:
fi

switch_tuya.py

Tuya規格のスマートプラグをON/OFF制御します。スマートプラグのDEVICE_ID, IP_ADDRESS, LOCAL_KEYの組み合わせが識別子となります。詳細は https://github.com/jasonacox/tinytuya でご確認ください。

#!/usr/bin/env python3

# switch_tuya.py (ver.20210903)

import tinytuya # https://github.com/jasonacox/tinytuya
import sys

d = tinytuya.OutletDevice('DEVICE_ID_HERE', 'IP_ADDRESS_HERE', 'LOCAL_KEY_HERE')
d.set_version(3.1)
data = d.status()

print('Dictionary %r' % data)
switch_state = data['dps']['1']

args = sys.argv
if len(args) == 2:
	if args[1] == 'ON':
		data = d.set_status(True)
	elif args[1] == 'OFF':
		data = d.set_status(False)
	elif args[1] == 'TOGGLE':
		data = d.set_status(not switch_state)
	else:
		pass
	if data:
		print('set_status() result %r' % data)
else:
	print('Usage: %s [ON|OFF|TOGGLE]' % args[0])

switch_meross.py

Meross規格のスマートプラグをON/OFF制御します。スマートプラグの名称が識別子となります。詳細は https://github.com/albertogeniola/MerossIot でご確認ください。

#!/usr/bin/env python3

# switch_meross.py (ver.20211109)
# ref: albertogeniola/MerossIot: Simple Python library for Meross devices https://github.com/albertogeniola/MerossIot

import asyncio
import os
import sys
from meross_iot.http_api import MerossHttpClient
from meross_iot.manager import MerossManager

EMAIL = os.environ.get('MEROSS_EMAIL') or 'YOUR_MEROSS_CLOUD_EMAIL'
PASSWORD = os.environ.get('MEROSS_PASSWORD') or 'YOUR_MEROSS_CLOUD_PASSWORD'
NAME_PLUG = 'HOGEHOGE' # 制御したいスマートプラグの名称

async def main(status):
	http_api_client = await MerossHttpClient.async_from_user_password(email=EMAIL, password=PASSWORD)
	manager = MerossManager(http_client=http_api_client)
	await manager.async_init()
	await manager.async_device_discovery()
	plugs = manager.find_devices(device_type="mss110")
	if len(plugs) < 1:
		print("No MSS110 plugs found...")
	else:
		for dev in plugs:
			print(f"Device = {dev.name}")
			await dev.async_update()
			if dev.name == NAME_PLUG:
				if status == 'on':
					print(f"Turning on {dev.name}...")
					await dev.async_turn_on(channel=0)
				elif status == 'off':
					print(f"Turing off {dev.name}")
					await dev.async_turn_off(channel=0)
	manager.close()
	await http_api_client.async_logout()

if __name__ == '__main__':
	args = sys.argv
	status = ''
	if len(args) == 2:
		if args[1] == 'ON':
			status = 'on'
		elif args[1] == 'OFF':
			status = 'off'
		else:
			print('Usage: %s [ON|OFF]' % args[0])
	else:
		print('Usage: %s [ON|OFF]' % args[0])
	if status:
		loop = asyncio.get_event_loop()
		loop.run_until_complete(main(status))
		loop.close()

参考図書

エコハウスのウソ2

エコハウスのウソ2

著者: 前 真之