3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

記事投稿キャンペーン 「2024年!初アウトプットをしよう」

NatureRemoから部屋の気温を収集してgrafanaでダッシュボード表示する

Last updated at Posted at 2024-01-28

やりたいこと

スマートホームハブであるNatureRemoを購入したところ、デバイスが取得している気温(いいものを買うと湿度や照度も)をAPIで取得できるということを知りました。
そこでPythonのNatureRemoライブラリを利用して気温を取得して、PostgreSQLにデータを保存し、grafanaでダッシュボード表示させることにしました。
nature_remo.png

GrafanaとpostgreSQLの準備

コンテナ立ち上げ

Grafanaをdocker composeを利用して立ち上げます。
以下のファイルを作成して、docker compose up -dで起動させてください。

docker-compose.yaml
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 psdocker compose logs -fなどでコンテナが立ち上がったことを確認します。

テーブル準備

psqlでpostgreSQLに接続してテーブルを作成します。-U grafanadocker-compose.yamlで指定したユーザー名を指定してください。

docker compose exec postgres psql -h localhost -U grafana

以下のDDLを実行します。

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ライブラリをインストールします。

Dockerfile
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"]
docker-compose.yaml
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.pysetting.pyファイルを作成します。
setting.pyでは3つのことを行なっています。

  1. 環境変数の取得
    DBの認証情報は、APIの情報はdocker-compose.yamlで環境変数として設定しています。これをコンテナ内のプログラムから読み込む処理をしています。(Secretなどを利用した方が望ましいでしょうが、、、)
  2. sqlalchemyのテーブル定義とエンジン作成
    PostgreSQLへの接続はsqlalchemyというライブラリを利用しています。詳しい説明は他の方に任せますが、PostgreSQLへ接続するengineとDBのテーブルにマップするPythonのクラスを作成して利用します。
  3. ロガーの設定
    ログ出力のためのロガーを作成します。
src/setting
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()をコメントアウトして下さい。

src/remo_extract.py
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 psdocker compose logs -fなどでコンテナが立ち上がったことを確認します。

Grafanaの設定

Grafanaログイン.png

usernameはadmin、Passwordもadminです。ログインするとPasswordの更新を求められると思うので更新してください。

基本的には案内に従って、設定をします。

PostgreSQLの登録

Add data source.png
PostgreSQL.png

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

connection.png

PostgreSQLのコンテナを立ち上げたサーバーでファイアウォールが有効な場合はポート5432を解放する必要があります。

sudo ufw status

などで状態を確認して解放を行なってください。

dashboardの設定

dashboard.png

Add visualizationをクリックしてデータの追加を行います。データソースとして先ほど登録したPostgreSQLを選択する。

PostgreSQL.png

Codeを選択して以下のSQLを入力します。Add queryをクリックしてBを追加します。
おそらく初めからTime seriesが設定されていると思いますが、異なった場合は設定してください。リビング寝室にはNature Remoの名前を設定してください。
スクリーンショット 2024-01-28 21.24.49.png

---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を確認することができます。

グラフ.png

エアコンをつけた瞬間と消した瞬間がよくわかります。

省略しますが、get_timeの最大をとることで現在の気温も確認できます。
グラフの寝室の気温のデータが欠損していますが、Nature Remo自体が一定間隔でしか気温を取得していないので、4分間隔の取得をおこなったところ前回分のデータが取得されたのだと思います。
ダッシュボード.png

3
0
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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?