やりたいこと
スマートホームハブであるNatureRemoを購入したところ、デバイスが取得している気温(いいものを買うと湿度や照度も)をAPIで取得できるということを知りました。
そこでPythonのNatureRemoライブラリを利用して気温を取得して、PostgreSQLにデータを保存し、grafanaでダッシュボード表示させることにしました。
GrafanaとpostgreSQLの準備
コンテナ立ち上げ
Grafanaをdocker composeを利用して立ち上げます。
以下のファイルを作成して、docker compose up -d
で起動させてください。
version: "3.8"
services:
postgres:
image: postgres
restart: unless-stopped
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: grafana #DB設定 以下3行適宜変更
POSTGRES_USER: grafana
POSTGRES_PASSWORD: Pa55w0rd
ports:
- '5432:5432' #待受のポート番号を変更したい場合は「:」の左側を変更
grafana:
image: grafana/grafana-enterprise
container_name: grafana
restart: unless-stopped
volumes:
- 'grafana_storage:/var/lib/grafana'
ports:
- '3000:3000' #待受のポート番号を変更したい場合は「:」の左側を変更
volumes: # データの永続化
grafana_storage: {}
postgres_data: {}
docker compose ps
やdocker compose logs -f
などでコンテナが立ち上がったことを確認します。
テーブル準備
psqlでpostgreSQLに接続してテーブルを作成します。-U grafana
はdocker-compose.yaml
で指定したユーザー名を指定してください。
docker compose exec postgres psql -h localhost -U grafana
以下のDDLを実行します。
CREATE TABLE nature_remo(
id varchar(36),
get_time timestamp,
device_name varchar(100),
temp NUMERIC(3, 1)
);
テーブルが作成されていることを確認します。
\dt
----出力
List of relations
Schema | Name | Type | Owner
--------+-------------+-------+---------
public | nature_remo | table | grafana
(1 row)
---
Nature Remo
プログラムと実行コンテナの作成をしていきます。
最終定期なディレクトリ構成は以下の通りです。
.
├── Dockerfile
├── docker-compose.yaml
└── src
├── remo_extract.py
└── setting.py
access tokenの発行
Nature Remoの設定は適切に終わっている前提です。まずはAPIを取得します。
Nature Remoにアクセスをして、メールアドレスでログインをします。
Generate access token
をクリックしてaccsess tokenを発行します。コピーをして保存してください。
今回はaccess token
を使ってデータの取得のみ行いますが、登録している家電のコントロールも可能です。他人に知られないように十分に注意してください。
コンテナの作成
データを取得してPostgreSQLへinsertするプログラムを動かすコンテナを作成します。
python3のイメージをベースとして、必要なpythonライブラリをインストールします。
FROM python:3
USER root
RUN apt-get update
RUN apt-get -y install locales &&\
localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm
RUN pip install --upgrade pip
RUN pip install --upgrade nature-remo
RUN pip install --upgrade requests
RUN pip install --upgrade urllib3
RUN pip install --upgrade sqlalchemy
RUN pip install --upgrade psycopg2
RUN pip install --upgrade schedule
CMD ["python", "-u", "/root/src/remo_extract.py"]
version: '3'
services:
nature_remo:
container_name: nature_remo
build: .
environment:
- remo_token=【先ほど取得したaccess tokenに置き換え】
- db_user=grafana
- db_password=Pa55w0rd
- db_server=【postgreSQLサーバーを指定します。】 #ex)192.168.1.1:5432, localshot:5432
- db_name=grafana
- interval_time=4 #室温を取得する時間の間隔(分)
volumes:
- ./src:/root/src
プログラム
src
ディレクトリにremo_extract.py
とsetting.py
ファイルを作成します。
setting.py
では3つのことを行なっています。
- 環境変数の取得
DBの認証情報は、APIの情報はdocker-compose.yaml
で環境変数として設定しています。これをコンテナ内のプログラムから読み込む処理をしています。(Secretなどを利用した方が望ましいでしょうが、、、) - sqlalchemyのテーブル定義とエンジン作成
PostgreSQLへの接続はsqlalchemyというライブラリを利用しています。詳しい説明は他の方に任せますが、PostgreSQLへ接続するengineとDBのテーブルにマップするPythonのクラスを作成して利用します。 - ロガーの設定
ログ出力のためのロガーを作成します。
import datetime
import os
from functools import wraps
import logging
from remo import NatureRemoAPI
from sqlalchemy import Column, create_engine
from sqlalchemy.dialects.mysql import NUMERIC, TIMESTAMP, VARCHAR
from sqlalchemy.orm import declarative_base
import sys
# ハンドラーをLoggerに追加
logger.addHandler(handler)
Base = declarative_base()
#コンテナの環境変数から設定の読み取り
API_KEY: str = os.environ.get("remo_token")
DB_USER: str = os.environ.get("db_user")
DB_PASSWORD: str = os.environ.get("db_password")
DB_SERVER: str = os.environ.get("db_server")
DB_NAME: str = os.environ.get("db_name")
INTERVAL_MINUTES: str = os.environ.get("interval_time")
api = NatureRemoAPI(API_KEY)
tz_jst_name = datetime.timezone(datetime.timedelta(hours=9), name="JST")
engine = create_engine(f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_SERVER}/{DB_NAME}")
# テーブルクラスの定義
class NATURE_REMO(Base):
__tablename__ = "nature_remo"
id = Column(VARCHAR(36), primary_key=True)
get_time = Column(TIMESTAMP, primary_key=True)
device_name = Column(VARCHAR(36))
temp = Column(NUMERIC(2, 1))
# loggerの定義
logger = logging.getLogger('my_logger')
logger.setLevel(logging.DEBUG)
# 標準出力へのハンドラーを作成
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
# フォーマットを設定
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
def my_log(logger):
def decorator_fn(fn):
@wraps(fn)
def wrap_fn(*args, **kwargs):
#local_args = locals()
logger.debug(f"{fn.__name__} WAS CALLED")
return_val = fn(*args, **kwargs)
logger.debug(f"{fn.__name__} FINISHED")
return return_val
return wrap_fn
return decorator_fn
一定間隔で室温を取得してDBへinsertするプログラムです。デバッグするときに切れ目がわかりにくいので、プログラムを最初に呼び出した時にRemo extractという文字列をアスキーアートで出力するようにしています。不要の場合はself.print_logo()
をコメントアウトして下さい。
from setting import NATURE_REMO, INTERVAL_MINUTES, Base, api, engine, logger, my_log, tz_jst_name
from sqlalchemy.orm import Session
import schedule
import time
class REMO_EX:
def __init__(self) -> None:
Base.metadata.create_all(engine)
self.logo_ascii = """
.(.dWHHQ,.
WN, dM[
-Hb .mH%
.WMNNH#^ .WMMm, (TNa..dNa.dmJ. ..,.
.dgf?TN, WMHMH% ,MMHHMMNMMMl .HH#W"BQ,
.WM% ,WQ-JMe.. (NNFJMNHUHH} ,MH .MN
.7"= ,MNM!_TB= TMN.. TNggHHM=
. (m,
.dgMMMg, ., .XqR. ,Hb .... dm.
dqHY! (MN. ,Ha..mgf (WNMMMMMMM$ zm-WMB"?Mx .., +MMa, dMl..
.mmP .dgg@ TMNgY .HM ,MMHl ?'.qY?Mg, J@% Tb .(kMMMY?7
4Mb7WMMY^ .XMMb .@M MNP .gP (M# MM W#:
(Wa...... .gg@ ?H, ,H@ MM] ,@l .XMM, MN MM
_'''9"^ ?= MMl dMF ,H).WK' ?''TM, .##
-^ TMHY ?M,.... .H#
,H@
JMF
"""
self.print_logo()
@my_log(logger)
def print_logo(self) -> None:
logger.info(self.logo_ascii)
@my_log(logger)
def fetch_devices_info(self):
devices = api.get_devices()
return devices
@staticmethod
@my_log(logger)
def make_devices(device):
id = device.id
get_time = device.newest_events["te"].created_at.astimezone(tz_jst_name)
name = device.name
temp = device.newest_events["te"].val
#logger.debug(id, get_time, name, temp)
record = NATURE_REMO(id=id, get_time=get_time, device_name=name, temp=temp)
return record
@my_log(logger)
def run(self):
devices = self.fetch_devices_info()
with Session(engine) as session:
for device in devices:
record = self.make_devices(device)
session.add(record)
session.commit()
def __main__():
remo_ex = REMO_EX()
remo_ex.run()
schedule.every(INTERVAL_MINUTES).minutes.do(remo_ex.run)
while True:
schedule.run_pending()
time.sleep(1)
if __name__ == "__main__":
__main__()
コンテナの立ち上げ
docker compose -d --build
docker compose ps
やdocker compose logs -f
などでコンテナが立ち上がったことを確認します。
Grafanaの設定
usernameはadmin
、Passwordもadmin
です。ログインするとPasswordの更新を求められると思うので更新してください。
基本的には案内に従って、設定をします。
PostgreSQLの登録
PostgreSQLを選択します。
docker-compose.yaml
で設定した値を入力していきます。TLS/SSLは今回設定していないのでdisable
にしてください。
最後にSave & test
をクリックして正しく接続できることを確認してください。
docker-compose.yaml | Grafana | example |
---|---|---|
Host URL | db_server | 192.168.1.1 |
Database name | db_name | grafana |
Username | db_user | grafana |
Password | db_password | Pa55w0rd |
PostgreSQLのコンテナを立ち上げたサーバーでファイアウォールが有効な場合はポート5432
を解放する必要があります。
sudo ufw status
などで状態を確認して解放を行なってください。
dashboardの設定
Add visualization
をクリックしてデータの追加を行います。データソースとして先ほど登録したPostgreSQLを選択する。
Code
を選択して以下のSQLを入力します。Add query
をクリックしてBを追加します。
おそらく初めからTime series
が設定されていると思いますが、異なった場合は設定してください。リビング
、寝室
にはNature Remoの名前を設定してください。
---A
SELECT
$__timeGroupAlias("get_time",'5m'),
AVG(temp) AS "リビング"
FROM nature_remo
where device_name = 'リビング'
GROUP BY device_name, time
-----B
SELECT
$__timeGroupAlias("get_time",'5m'),
AVG(temp) AS "寝室"
FROM nature_remo
where device_name = '寝室'
GROUP BY device_name, time
save
をするとDashboardを確認することができます。
エアコンをつけた瞬間と消した瞬間がよくわかります。
省略しますが、get_time
の最大をとることで現在の気温も確認できます。
グラフの寝室の気温のデータが欠損していますが、Nature Remo自体が一定間隔でしか気温を取得していないので、4分間隔の取得をおこなったところ前回分のデータが取得されたのだと思います。