気の向くままに辿るIT/ICT/IoT
IoT・電子工作

Raspberry Pi/ESP32/MQTT/Node.js/PostgreSQLで温湿度環境モニタを自作

ホーム前へ次へ
Raspberry Piって?

Raspberry Pi/ESP32/MQTT/Node.js/PostgreSQLで温湿度環境モニタを自作

Raspberry Pi/ESP32/MQTT/Node.js/PostgreSQLで温湿度環境モニタを自作

2025/10/06

 Raspberry Pi 2Bサーバ/Debian BookwormとESP32+温湿度センサー、MQTTNode.js+JavaScriptとRDBの1つPostgreSQLで温度・湿度(・気圧)の値及びCSSによるグラフを一覧表示、モニタリングできるようにしてみた話。

PostgreSQL取得データを基にCSSでグラフ化 PCブラウザ編

 PCブラウザだと、こんな感じ。

PostgreSQL取得データを基にCSSでグラフ化 スマホ縦ブラウザ編

 スマホブラウザで縦だと、こんな感じ。

PostgreSQL取得データを基にCSSでグラフ化 スマホ横ブラウザ編

 スマホブラウザで横だと、こんな感じ。

目的

 以前から室内各所の温度や湿度の違いや天井と床との温度差、室外と室内の温度や湿度の差などは気になっており、エアコンの自動制御まではやるつもりはありませんが、エアコンをつけるかどうかの判断材料としても良いかなと。

概要

 Raspberry Pi 2Bサーバ/Debian Bookworm+MosquittoをMQTTブローカーとし、屋内・屋外各所に配置のESP32+温度・湿度(、気圧)センサー測定都度、MQTT PublishされたデータをNode.js+JavaScriptデーモンでMQTT Subscribeしつつ、RDB(PostgreSQL)に登録、別途、Node.js+JavaScriptデーモンによるWebサーバにブラウザでアクセス都度、各所最新データを1件ずつ取得、データ値及びCSSによるグラフを一覧できるようにします。

 ちなみに高/快適/低っぽく温度帯や湿度帯によってグラフ色も変化。

 Node.jsの機能的には、MQTT Subscribe、WebSocketで直接受けてブラウザに表示...ということもできました。

 が、今回の場合、Publishする温湿度センサーデバイスは1つではなく、多数あり、通信は無線、よって軽量で短時間で一方のみならず双方向に通信でき、不安定な環境でクライアントが切断、再接続といったケースでも再送できたり、メッセージ到達品質を選択できたりといった術があるMQTTを採用することに。

 また、最初にデバイスの電源を投入したタイミングや通信ラグなども想定され、同時に全てSubscribeしてブラウザで一覧表示...というわけにはいかないので各所それぞれの最新データを取得できるようにRDBMSも使うことに。

Mosquittoの準備

debian: $ sudo apt update
debian: $ sudo apt install -y mosquitto
debian: $ sudo apt install -y mosquitto-clients
debian: $

 ラズパイサーバをMQTTブローカーにするに当たり、ここでは、Mosquittoをインストールします。

 尚、mosquitto-clientsもインストールしておくと端末でSubscribeやPublishすることもでき、以後の作業を行う前でもESP32からMQTT Publishされたデータを確認することもできます。

debian: $ vi /etc/mosquitto/conf.d/base.conf
listener 1883
allow_anonymous true
debian: $

 今日時点、Debian Bookwormでは、mosquittoの設定ファイルは、MQTTに限らず、追加設定用として推奨される任意のファイル名+拡張子[.conf]を配置する/etc/mosquitto/conf.d/ディレクトリとベースとなる/etc/mosquitto/mosquitto.confがあります。

 というわけで/etc/mosquitto/conf.d/base.confという名のファイルを配置することにして通信可能なポート指定としてMQTTデフォルトポート1883と匿名で接続を許可する設定としました。

 ここではセキュリティを十分に考慮しておらず、必要ならallow_anonymousはfalseに設定、認証必須とします。

 尚、サーバ以外の他のデバイスからアクセスしたい場合、ファイアウォールが有効なら、ファイアウォール設定でもMQTTポートを通信許可しておく必要があります。

Node.jsの準備

debian: $ sudo apt update
debian: $ sudo apt install -y nodejs npm
debian: $ sudo npm install n -g
debian: $ mkdir path/to/nodejs/project
debian: $ cd path/to/nodejs/project
debian:~path/to/nodejs/project $ npm init // 対話形式を回避したい場合、-yを追記

 Node.jsを利用する際のお約束行事。

MQTT SubscribeとRDB登録デーモン

 Node.jsの準備が済んでいる前提で、今回、各所に配置のESP32+温湿度モジュールからMQTT PublishされたデータをMQTTブローカーとしたラズパイサーバでSubscribe、PostgreSQLに登録するNode.js + JavaScriptデーモンを作成していきます。

debian:~path/to/nodejs/project $ npm i node-postgres

 Node.jsでPostgreSQLを利用したいのでnode-postgresをインストールします。

 尚、iは、installでも可。

debian:~path/to/nodejs/project $ createuser -h localhost -p 5432 -U postgres -d -l -r -s user_name
debian:~path/to/nodejs/project $

 node-postgresをインストールした時点でデフォルトユーザーは、postgresですが、併せてcreateuserコマンドもインストールされており、ユーザーの追加と同時に各種権限の設定もできます。

 ここでは、ホスト名、ポート番号、createuserコマンドを実行するユーザー、DB作成権限、ログイン権限、スーパーユーザー権限を付与しています。

 ここで[-P](大文字)を指定した場合、DB接続時にパスワードが必要になります。

 尚、後述の作成済みDB接続後にSQLでCREATE USERすることでもユーザーを追加することができます。

debian:~path/to/nodejs/project $ createdb ANY_DB_NAME
debian:~path/to/nodejs/project $ psql ANY_DB_NAME
...
ANY_DB_NAME>>
(Ctrl + d)
debian:~path/to/nodejs/project $

 node-postgresをインストールした時点でcreatedbやdropdbコマンドもインストールされており、RDB名を渡すとPostgreSQL用データベースの作成、削除ができます。

 必要なら実行後、パスワードを入力します。

 また、PostgreSQL対話式クライアントpsqlコマンドもインストールされており、RDB名を渡すとRDBに接続できます。

 RDBからログアウトしたい場合は、[Ctrl]+[d]で。

debian:~path/to/nodejs/project $ psql ANY_DB_NAME
...
ANY_DB_NAME>> CREATE TABLE client_list (clientid integer PRIMARY KEY, name text, full_name text);
...
ANY_DB_NAME>> CREATE TABLE temperature_humidity (clientid integer, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, topics text, temp numeric(4,2), humi numeric(4,2), CONSTRAINT pkey PRIMARY KEY(clientid,created_at));
...
ANY_DB_NAME>> INSERT INTO client_list (clientid, name, full_name) VALUES (1, 'wa', 'washitsu'), (2, 'yo', 'yoshitsu'), (3, 'kit', 'kitchen')
(Ctrl + d)
debian:~path/to/nodejs/project $

 RDBに接続したら、CREATE、SELECT、UPDATE、INSERT、DELETEなど一連のRDB操作ができるようになります。

 ここでは、今回使用するclient_listテーブルとtemperature_humidityテーブルを作成、前者は、予め必要データをINSERTしておきます。

 client_listテーブルは、主キーがclientidで短縮名とフルネーム用のカラムがあり、temperature_humidityテーブルのclientidと紐づけるためにあります。

 temperature_humidityテーブルのプライマリキーは、clientidとcreated_atでTIMESTAMP型の後者は、データ登録(INSERT)時のタイムスタンプがデフォルトで自動登録、MQTT Topicテキストと全体4桁、小数点以下2桁の数値な温度と湿度の列から成っています。

 ちなみに気圧は省きました。

 PostgreSQLのDBとテーブルを作成したのでMQTT SubscribeしたデータをINSERTする準備は整いました。

temp_humi/(複数可な)区分/(一意な)場所

 MQTT Topicは、今回、こんな感じで運用するものとします。

 また、[(一意な)場所]は、[client_list]テーブルのカラム[name]と一致するものとします。

const hostname = "127.0.0.1"; // localhostでも可
const http_port = "9001"; // 任意のポート番号
const mqtt_port = "1883"; // MQTTデフォルトポート
// PostgreSQLアクセス用
const postgres_port = "5432"; // Postgresデフォルトポート
const user_name = "USER_NAME";
const user_pass = "USER_PASS";
const db_name = "DB_NAME";
 
const { Pool } = require('pg')
const pool = new Pool({
  user: user_name,
  host: hostname,
  database: db_name,
  password: user_pass,
  port: postgres_port,
})
 
async function postgresql_connect(cname, topic, temp, humi){
  //console.log('cname' + " " + cname + " " + topic + " " + temp + " " + humi)
  const sql_cid = "SELECT clientid FROM client_list WHERE name = $1"
  const sql_cid_val = [cname]
  const sql_ins = "INSERT INTO temperature_humidity (clientid, topics, temp, humi) VALUES ($1, $2, $3, $4)"
  const sql_count = "SELECT COUNT(*) FROM temperature_humidity"
  const sql_del ="DELETE FROM temperature_humidity"
  try {
    const result_cid = await pool.query(sql_cid, sql_cid_val)
    console.log(result_cid.rows[0].clientid)
    const values = [result_cid.rows[0].clientid, topic, temp, humi]
    const result_ins = await pool.query(sql_ins, values)
    const result_count = await pool.query(sql_count)
    count=result_count.rows[0].count
 
    if(count > 20000){
          await pool.query(sql_del)
    }
  } catch (e) {
     console.error(e)
  }
}
 
const topic = "temp_humi/#";
var mqtt_topic;
var mqtt_message_array;
 
const mqtt = require("mqtt");
const client = mqtt.connect(`mqtt://${hostname}:${mqtt_port}`);
 
client.on("connect", () => {
  client.subscribe(`${topic}`, (err) => {
  if (!err) {
   //client.publish("presence", "Hello mqtt");
  }
  });
});
 
client.on("message", (topic, message) => {
  mqtt_topic = topic;
  mqtt_message_array = message.toString().split(" ");
  mqtt_topic_array = topic.toString().split("/");
  console.log("USER : " + mqtt_topic_array[2]);
 
  console.log(Object.entries(mqtt_topic_array))
  console.log(Object.keys(mqtt_topic_array)[2])
  console.log(Object.values(mqtt_topic_array)[2])
 
  postgresql_connect(Object.values(mqtt_topic_array)[2].toString(), topic, mqtt_message_array[0], mqtt_message_array[1]);
});

 そしてプログラムは、こんな感じ。

 常にMQTTポートを監視して都度、MQTT Subscribeする内容は、前述の通り、[temp_humi/(複数可な)区分/(一意な)場所]から成る[topic]、また、温度と湿度がスペース区切りな[message]から成っており、[(一意な)場所]は、[client_list]テーブルのカラム[name]と一致する前提となっています。

 スラッシュ区切りのTopicから[(一意な)場所]、空白区切りの[message]から得た[温度]と[湿度]でPostgreSQLに接続、アクセス。

 [(一意な)場所]=client_listのnameでSELECTしたclientid、引数からtopicと[温度]と[湿度]をINSERT。

 temperature_humidityをSELECT COUNTして全数を得て、一定数を超えたらDELETE TABLE。

debian:~path/to/nodejs/project $ node mqtt_data_insert_rdb.js
debian:~path/to/nodejs/project $

 ファイル名が[mqtt_data_insert_rdb.js]なら、このようにすれば、一時起動でき、MQTT Publishがあれば、MQTT Subscribe、DBにINSERTされます。

debian:~ $ systemctl cat regist_db_mqtt_temp_humi.service
[Unit]
Description=DB Resist MQTT Temperature and Humidity Info
Wants=network-online.target
After=network-online.target
 
[Service]
Type=oneshot
ExecStart=/bin/su - USER /bin/sh -c "/home/USER/.regist_db_mqtt_temp_humi.sh"
RemainAfterExit=yes
 
[Install]
WantedBy=multi-user.target

 良さ気ならsystemdユニットファイルを作成。

debian:~ $ cat /home/USER/.regist_db_mqtt_temp_humi.sh
#!/bin/bash
 
node /home/path/to/nodejs/project/mqtt_data_insert_rdb.js &

 systemdユニットファイルで呼んでいるファイル。

 なんとなく、shellファイルが相性良さそうと思えたことがあって以来、ラッパ。

debian:~ $ sudo chmod +x /etc/systemd/system/regist_db_mqtt_temp_humi.service
debian:~ $ sudo systemctl start regist_db_mqtt_temp_humi.service
debian:~ $ sudo systemctl enable regist_db_mqtt_temp_humi.service
debian:~ $

 バッチリできたら、ユニットファイルに実行権限を付与、systemd/systemctlでstartで一時開始、enableで永続化。

ウェブサーバデーモンで最新データ取得と値とグラフをブラウザ表示

 続いてWebサーバアドレス+特定ポートにアクセスした場合、PostgreSQLに登録済みの温湿度データの内、MQTT Topicごとの最新データを各1件取得、Topic単位でデータ値とデータ値を基にした円グラフをCSSで描画するデーモンを、やはり、Node.js + JavaScriptで実装していきます。

debian:~path/to/nodejs/project $ npm i express
debian:~path/to/nodejs/project $ npm i path
debian:~path/to/nodejs/project $ npm i node-postgres

 Node.jsの準備が済んでいる前提でexpress、path、pg(node-postgres)を利用したいのでインストールします。

 expressは、Webサーバ用、pathはパスの階層構造指定用、pg(node-postgres)は、PostgreSQLアクセス用です。

 わざわざインストールしなくともNode.jsには、標準Webサーバとしてhttpというものがあるのですが、なんとなく。

 もちろん、同一プロジェクトディレクトリ内で既にインストール済みなもの(npm listやpackage.jsonで列挙されているもの)は、改めてインストールする必要はありません。

const express = require('express');
const path = require('path');
const { Client } = require('pg');
 
const app = express();
 
const client = new Client({
  host: 'localhost',    // 127.0.0.1でも可
  port: 5432,           // Postgresデフォルトポート
  user: 'USER_NAME',
  password: 'USER_PASS',
  database: 'DB_NAME',
});
 
client.connect()
  .then(() => console.log('PostgreSQLに接続しました'))
  .catch(err => console.error('接続エラー:', err));
 
app.use(express.static(path.join(__dirname, 'public')));
 
app.get('/', (req, res) => {
  client.query('SELECT DISTINCT ON (clientid) * FROM temperature_humidity ORDER BY clientid, created_at DESC')
    .then(result => {
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.write('\n\n');
      res.write('<!doctype html>\n');
      res.write('<html lang="ja-jp">\n');
      res.write('<head>\n');
      res.write('<meta charset="utf-8">\n');
      res.write('<link rel="stylesheet" href="css/style.css" type="text/css">\n');
      res.write('<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">\n');
      res.write('<link media="only screen and \(max-device-width:480px\)" href="css/responsive.css" type="text/css" rel="stylesheet">\n');
      res.write('</head>\n');
      res.write('<body>\n');
      res.write('<div class="for_win_ie"><div class="container">\n');
      res.write('<h1>温湿度モニタ</h1>');
      res.write('<div id="back">\n');
 
      res.write('<div class="float">\n');
      res.write('<div class="topic">' + result.rows[7].topics + '</div>\n');
      res.write('<span class="temp">' + result.rows[7].temp + '</span>\n');
      res.write('<span class="humi">' + result.rows[7].humi + '</span>\n');
      res.write('</div>\n');
      res.write('<div class="clear_pos"></div>\n');
 
      res.write('<div class="float">\n');
      res.write('<div class="topic">' + result.rows[0].topics + '</div>\n');
      res.write('<span class="temp">' + result.rows[0].temp + '</span>\n');
      res.write('<span class="humi">' + result.rows[0].humi + '</span>\n');
      res.write('</div>\n');
 
      res.write('<div class="float">\n');
      res.write('<div class="topic">' + result.rows[1].topics + '</div>\n');
      res.write('<span class="temp">' + result.rows[1].temp + '</span>\n');
      res.write('<span class="humi">' + result.rows[1].humi + '</span>\n');
      res.write('</div>\n');
 
      res.write('<div class="float">\n');
      res.write('<div class="topic">' + result.rows[2].topics + '</div>\n');
      res.write('<span class="temp">' + result.rows[2].temp + '</span>\n');
      res.write('<span class="humi">' + result.rows[2].humi + '</span>\n');
      res.write('</div>\n');
      res.write('<div class="clear_pos"></div>\n');
 
      res.write('<div class="float">\n');
      res.write('<div class="topic">' + result.rows[3].topics + '</div>\n');
      res.write('<span class="temp">' + result.rows[3].temp + '</span>\n');
      res.write('<span class="humi">' + result.rows[3].humi + '</span>\n');
      res.write('</div>\n');
 
      res.write('<div class="float">\n');
      res.write('<div class="topic">' + result.rows[4].topics + '</div>\n');
      res.write('<span class="temp">' + result.rows[4].temp + '</span>\n');
      res.write('<span class="humi">' + result.rows[4].humi + '</span>\n');
      res.write('</div>\n');
      res.write('<div class="clear_pos"></div>\n');
 
      res.write('<div class="float">\n');
      res.write('<div class="topic">' + result.rows[5].topics + '</div>\n');
      res.write('<span class="temp">' + result.rows[5].temp + '</span>\n');
      res.write('<span class="humi">' + result.rows[5].humi + '</span>\n');
      res.write('</div>\n');
 
      res.write('<div class="float">\n');
      res.write('<div class="topic">' + result.rows[6].topics + '</div>\n');
      res.write('<span class="temp">' + result.rows[6].temp + '</span>\n');
      res.write('<span class="humi">' + result.rows[6].humi + '</span>\n');
      res.write('</div>\n');
      res.write('<div class="clear_pos"></div>\n');
      res.write('</div>\n');
      res.write('</div></div>\n');
 
      res.write('<script>\n');
      res.write('var elem_span_temp = document.querySelectorAll(".temp");\n');
      res.write('for (var i = 0;i < elem_span_temp.length;i++) {\n');
      res.write('var percent_temp_vs_50 = Math.round(elem_span_temp[i].textContent / 50 * 100);\n');
      res.write('if (elem_span_temp[i].textContent <= 4.99) {\n');
      res.write('elem_span_temp[i].style.color = "blue" ;\n');
      res.write('elem_span_temp[i].style.backgroundImage = "radial-gradient(#fff 55%, transparent 55%), conic-gradient(#FFFFFF " + percent_temp_vs_50 + "%, #f2f2f2 " + percent_temp_vs_50 + "% 100%)" ;\n');
      res.write('} else if (elem_span_temp[i].textContent <= 15.99) {\n');
      res.write('elem_span_temp[i].style.color = "lightskyblue" ;\n');
      res.write('elem_span_temp[i].style.backgroundImage = "radial-gradient(#fff 55%, transparent 55%), conic-gradient(#87cefa " + percent_temp_vs_50 + "%, #f2f2f2 " + percent_temp_vs_50 + "% 100%)" ;\n');
      res.write('} else if (elem_span_temp[i].textContent <= 24.99) {\n');
      res.write('elem_span_temp[i].style.backgroundImage = "radial-gradient(#fff 55%, transparent 55%), conic-gradient(#98fb98 " + percent_temp_vs_50 + "%, #f2f2f2 " + percent_temp_vs_50 + "% 100%)" ;\n');
      res.write('} else if (elem_span_temp[i].textContent <= 29.99) {\n');
      res.write('elem_span_temp[i].style.color = "#cb65f0" ;\n');
      res.write('elem_span_temp[i].style.backgroundImage = "radial-gradient(#fff 55%, transparent 55%), conic-gradient(#cb65f0 " + percent_temp_vs_50 + "%, #f2f2f2 " + percent_temp_vs_50 + "% 100%)" ;\n');
      res.write('} else if (elem_span_temp[i].textContent <= 34.99) {\n');
      res.write('elem_span_temp[i].style.color = "red" ;\n');
      res.write('elem_span_temp[i].style.backgroundImage = "radial-gradient(#fff 55%, transparent 55%), conic-gradient(#FF9900 " + percent_temp_vs_50 + "%, #f2f2f2 " + percent_temp_vs_50 + "% 100%)" ;\n');
      res.write('} else if (35 <= elem_span_temp[i].textContent) {\n');
      res.write('elem_span_temp[i].style.color = "red" ;\n');
      res.write('elem_span_temp[i].style.backgroundImage = "radial-gradient(#fff 55%, transparent 55%), conic-gradient(#FF0000 " + percent_temp_vs_50 + "%, #f2f2f2 " + percent_temp_vs_50 + "% 100%)" ;\n');
      res.write('}\n');
      res.write('console.log(elem_span_temp[i].textContent + " : " + elem_span_temp[i].style.color);\n');
      res.write('}\n');
      res.write('var elem_span_humi = document.querySelectorAll(".humi");\n');
      res.write('for (var i = 0;i < elem_span_humi.length;i++) {\n');
      res.write('if (39.99 >= elem_span_humi[i].textContent) {\n');
      res.write('elem_span_humi[i].style.color = "blue" ;\n');
      res.write('elem_span_humi[i].style.backgroundImage = "radial-gradient(#fff 55%, transparent 55%), conic-gradient(#00ECFF " + elem_span_humi[i].textContent + "%, #f2f2f2 " + elem_span_humi[i].textContent + "% 100%)" ;\n');
      res.write('} else if (49.99 >= elem_span_humi[i].textContent) {\n');
      res.write('elem_span_humi[i].style.color = "dodgerblue" ;\n');
      res.write('elem_span_humi[i].style.backgroundImage = "radial-gradient(#fff 55%, transparent 55%), conic-gradient(#1E90FF " + elem_span_humi[i].textContent + "%, #f2f2f2 " + elem_span_humi[i].textContent + "% 100%)" ;\n');
      res.write('} else if (64.99 >= elem_span_humi[i].textContent) {\n');
      res.write('elem_span_humi[i].style.backgroundImage = "radial-gradient(#fff 55%, transparent 55%), conic-gradient(#98fb98 " + elem_span_humi[i].textContent + "%, #f2f2f2 " + elem_span_humi[i].textContent + "% 100%)" ;\n');
      res.write('} else if (65 <= elem_span_humi[i].textContent) {\n');
      res.write('elem_span_humi[i].style.color = "mediumblue" ;\n');
      res.write('elem_span_humi[i].style.backgroundImage = "radial-gradient(#fff 55%, transparent 55%), conic-gradient(#0000cd " + elem_span_humi[i].textContent + "%, #f2f2f2 " + elem_span_humi[i].textContent + "% 100%)" ;\n');
      res.write('}\n');
      res.write('console.log(elem_span_humi[i].textContent + " : " + elem_span_humi[i].style.color + " : " + Math.round(elem_span_humi[i].textContent));\n');
      res.write('}\n');
      res.write('</script>\n');
      res.write('</body>\n');
      res.write('</html>\n');
 
      res.end();
    })
    .catch(err => {
      console.error('クエリエラー:', err)
      res.write('データ取得失敗');
    });
});
 
app.listen(9001, () => {
  console.log('http://localhost:9001 サーバー起動中')
});

 プログラムは、こんな感じ。

 仮に9001ポートでサーバ起動、ホストはlocalhostとしていますが、サーバがIP固定してあれば、何を調べるまでもなく、LAN内からIPアドレス:9001で(Androidでなければ、mDNSを効かせておけばHOSTNAME.local:9001等でも)アクセスできます。

 ただし、ファイアウォールが有効なら、ファイアウォール設定でも前述のMQTTポートも含め、Webサーバ用の9001ポートを開放(後、マシンを再起動)しておく必要がありますが。

 当該ポートにアクセスすると[temp_humi/(複数可な)区分/(一意な)場所]から成る[topic]の内、[(一意な)場所](client_listのname)ごとに最新データを1件取得、[name]、[温度]、[湿度]の値と、それらを元にしたグラフをCSSで表示します。

 尚、昔からレイアウトには使ってくれるなよと注意喚起されていたものの、当初、楽だからと試したtableタグだとグラフをうまく収められなかったため、spanタグ、divタグのfloatの設定・解除で並べることにしました。

#back {
  background-color: lightgrey ;
}
 
.for_win_ie
{
text-align:center ;
}
 
.container
{
margin-left:auto ;
margin-right:auto ;
width: 100% ;
height:110px ;
text-align:justify ;
}
 
.temp, .humi {
  display: flex ;
  justify-content: center ;
  align-items: center ;
  width: 100px ;
  height: 80px ;
  margin: 10px ;
  border-radius: 50% ;
  background-image: radial-gradient(#fff 55%, transparent 55%), conic-gradient(#98fb98 60%, #f2f2f2 60% 100%) ;
  font-weight: 600 ;
  padding: 10px ;
  float: left ;
}
 
.temp:after
{
    content: "℃" ;
}
 
.humi:after
{
    content: "%" ;
}
 
.topic {
    width: 300px ;
    text-align: center ;
    text-decoration: underline ;
    font-weight: bold ;
    font-size: 15px ;
    color:#000000 ;
}
 
.float {
    float: left ;
}
 
.clear_pos {
    clear: both ;
}

 CSSの外部ファイル。

 Webサーバについては、expressの標準的なもの、CSSでグラフについては、Dounut PieChart/ドーナツ型の円グラフに倣いました。

 (JavaScriptの)backgroundImage(htmlではbackground-image)に画像ファイル以外を設定できるとは知りませんでした。

 グラデーション設定でなんでこうなるのか理解しきれていませんが、試してみるとbackgroundImageのconic-gradientが、グラフ値に相当する帯?部のカラー設定だったので湿度は、値そのまま、温度は、仮に50度を100%として(ブラウザのコンソールで見たら、稀に1つだけ小数点以下桁数が10桁くらいあるのもあって気分的に小数点以下を四捨五入して丸めたものを)設定。

 ハマったのは、温湿度ともに快適度を示すべく温度帯と湿度帯で色を変えるためのJavaScriptのif文での温度や湿度の範囲設定...、未だに明確には理解できていませんが、どこに起因するのか、範囲指定によっては、うまく適用されなかったり、外部CSSで設定した部分を判定に入れた途端、おかしなことになったり...ブラウザを替えても同様で、?がたくさんな謎現象に悩まされました。

debian:~path/to/nodejs/project $ node temp_humi_monitor.js
debian:~path/to/nodejs/project $

 ファイル名が[temp_humi_monitor]なら、このようにすれば、一時起動できます。

debian:~ $ systemctl cat temp_humi_monitor.service
[Unit]
Description=DB Resist MQTT Temperature and Humidity Info
Wants=network-online.target
After=network-online.target
 
[Service]
Type=oneshot
ExecStart=/bin/su - USER /bin/sh -c "/home/USER/.temp_humi_monitor.sh"
RemainAfterExit=yes
 
[Install]
WantedBy=multi-user.target

 良さ気ならsystemdユニットファイルを作成。

debian:~ $ cat /home/USER/.temp_humi_monitor.sh
#!/bin/bash
 
node /home/path/to/nodejs/project/temp_humi_monitor.js &

 systemdユニットファイルで呼んでいるファイル。

 なんとなく、shellファイルが相性良さそうと思えたことがあって以来、ラッパースクリプト。

debian:~ $ sudo chmod +x /etc/systemd/system/temp_humi_monitor.service
debian:~ $ sudo systemctl start temp_humi_monitor.service
debian:~ $ sudo systemctl enable temp_humi_monitor.service
debian:~ $

 いけると確信もてたら、ユニットファイルに実行権限を付与、systemd/systemctlでstartで一時開始、enableで永続化。

LAN内の他のデバイスからアクセスできない時は

 前述のようにファイアウォールが有効でLAN内の他デバイスからアクセスしたい場合は、MQTTサーバ、Webサーバなど、必要な通信ポートを開放(、必要なら再起動)しておく必要があります。

debian:~ $ sudo ufw allow 1883
debian:~ $ sudo ufw allow 9001
debian:~ $

 ufwなら、こんな感じで。

自作スマートホーム制御パネルに温湿度モニタを追加

ESP32自作スマートホーム制御パネルに追加した温湿度モニタメニュー

 できたところでESP32+Webサーバでブラウザ版の自作スマートホームコントロールパネルに温湿度モニタを追加しました。

ホーム前へ次へ