Nature Remo miniの室温データをinfluxdbに入れてみる

Nature Remo mini 使って寝ている間に暑すぎればクーラーON、寒すぎればOFFしています。
買ってすぐの頃は温度設定がイマイチでうまく行ってなくて暑いのにONされなかったり、夜中に寒すぎて手動でOFFしたりしてました。

そこで室温データを分析してうまくやりたいと思ってNature Remo Cloud APIを叩いて室温を取得してからGoogle Spreadsheetに書き込むというのを書いたことがありました。
ただ設定を試行錯誤しているうちにいい感じになってきたのでデータを使うことはありませんでした・・・

最近InfluxDBを触っていたのでSpreadsheetの代わりにDBへ入れたほうがいいかなと思って修正してみたらむしろ簡単になったのでこちらを記録に残しておくことにします。

前提としてMacで作業していてGoでプログラム書いてます。
(常時動かすときはLinuxで動かすつもりですけど)

室温データの取得

Nature Remo Cloud API

developer.nature.global

アクセストークンが必要です。トーク無しでAPIを叩くとUnauthorizedになります。
トークンを公開しないように気をつけましょう。

バイスの情報を取りたいので /1/devices というAPIを使ってみます。

swagger.nature.global

Goで書いてるのでまずはJSONが欲しいということでトークンを使って取得してみます。(${Token}を置き換える)

$ curl -X GET "https://api.nature.global/1/devices" -H "accept: application/json" -k --header "Authorization: Bearer ${Token}"
[{"name":"3F_Remo","id":"xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","created_at":"2018-08-06T13:12:11Z","updated_at":"2019-09-30T14:12:13Z","mac_address":"xx:xx:xx:xx:xx:xx","serial_number":"xxxxxxxxx","firmware_version":"Remo-mini/1.0.87-g8b06f0e","temperature_offset":-2,"humidity_offset":0,"users":[{"id":"xxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx","nickname":"GOZU Kenichiro","superuser":true}],"newest_events":{"te":{"val":27.2,"created_at":"2019-10-01T00:13:52Z"}}}]

こんな感じで取れました。
見ずらいので整形したものを貼っておきます。

[
    {
        "name": "3F_Remo",
        "id": "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "created_at": "2018-08-06T13:12:11Z",
        "updated_at": "2019-09-30T14:12:13Z",
        "mac_address": "xx:xx:xx:xx:xx:xx",
        "serial_number": "xxxxxxxxx",
        "firmware_version": "Remo-mini/1.0.87-g8b06f0e",
        "temperature_offset": -2,
        "humidity_offset": 0,
        "users": [
            {
                "id": "xxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx",
                "nickname": "GOZU Kenichiro",
                "superuser": true
            }
        ],
        "newest_events": {
            "te": {
                "val": 27.2,
                "created_at": "2019-10-01T00:13:52Z"
            }
        }
    }
]

newest_events.te.val が室温ですね。27.2度でした。

GoでJSON扱うために struct を書くのが鬱陶しいのでjson2goサイトでサクッと変換します。

mholt.github.io

type RemoDevices []struct {
    Name              string    `json:"name"`
    ID                string    `json:"id"`
    CreatedAt         time.Time `json:"created_at"`
    UpdatedAt         time.Time `json:"updated_at"`
    MacAddress        string    `json:"mac_address"`
    SerialNumber      string    `json:"serial_number"`
    FirmwareVersion   string    `json:"firmware_version"`
    TemperatureOffset int       `json:"temperature_offset"`
    HumidityOffset    int       `json:"humidity_offset"`
    Users             []struct {
        ID        string `json:"id"`
        Nickname  string `json:"nickname"`
        Superuser bool   `json:"superuser"`
    } `json:"users"`
    NewestEvents struct {
        Te struct {
            Val       float64   `json:"val"`
            CreatedAt time.Time `json:"created_at"`
        } `json:"te"`
    } `json:"newest_events"`
}

配列になってます。うちにはRemoは1つしかないから常に1しか返ってこないけどね。。。

あとは API 叩いてレスポンスのJSONからteを取得します。
まずは net/httpを使ってhttp clientの準備。

import (
    "fmt"
    "log"
    "net/http"
    "net/http/httputil"
)

func newRequest(m map[string]string) (*http.Request, error) {
    url := m["url"]

    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return nil, err
    }

    req.Header.Set("Content-Type", "application/json")
    req.Header.Set(m["headerKey"], m["headerValue"])

    dump, err := httputil.DumpRequestOut(req, true)
    fmt.Printf("%s", dump)
    if err != nil {
        log.Fatal("Error requesting dump")
    }

    return req, err
}

func getResponse(m map[string]string) (*http.Response, error) {
    req, err := newRequest(m)

    res, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    } else if res.StatusCode != 200 {
        return nil, fmt.Errorf("http status %d", res.StatusCode)
    }

    return res, err
}

そんでもってAPIの処理。(実際にはファイル別れてるけど便宜上まとめて書いてます)

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "time"
)

func main() {
    te := Remo("https://api.nature.global/1/devices")
}

func Remo(url string) (te float64) {
    res, err := encodeJson4Remo(url)
    if err != nil {
        log.Fatalf("http error: %v", err)
    }
    for _, r := range res {
        jst := time.FixedZone("Asia/Tokyo", 9*60*60)

        fmt.Println("Name: " + r.Name)
        fmt.Println("ID: " + r.ID)
        fmt.Printf("UpdatedAt: %s\n", r.UpdatedAt.In(jst).Format("2006/01/02 15:04:05"))
        fmt.Printf("TemperatureOffset: %d\n", r.TemperatureOffset)
        fmt.Println("User ID: " + r.Users[0].ID)
        fmt.Println("User Nickname: " + r.Users[0].Nickname)
        fmt.Printf("User Superuser: %t\n", r.Users[0].Superuser)
        fmt.Printf("Temperature val: %f\n", r.NewestEvents.Te.Val)
        te = r.NewestEvents.Te.Val
        fmt.Printf("Temperature CreatedAt: %s\n", r.NewestEvents.Te.CreatedAt.In(jst).Format("2006/01/02 15:04:05"))
    }

    return te
}

func encodeJson4Remo(url string) (RemoDevices, error) {
    token := "Bearer トークン入れてね"
    m := map[string]string{
        "url":         url,
        "headerKey":   "Authorization",
        "headerValue": token}
    res, err := getResponse(m)
    if err != nil {
        return remoDevices, err
    }

    byteArray, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return remoDevices, err
    }
    defer res.Body.Close()

    if err := json.Unmarshal(byteArray, &remoDevices); err != nil {
        log.Fatalf("Error!: %v", err)
    }
    return remoDevices, err
}

結果。

$ go run natureremo_temperature.go remo.go httpclient.go 
GET /1/devices HTTP/1.1
Host: api.nature.global
User-Agent: Go-http-client/1.1
Authorization: Bearer トークン
Content-Type: application/json
Accept-Encoding: gzip

Name: 3F_Remo
ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
UpdatedAt: 2019/09/30 23:12:13
TemperatureOffset: -2
User ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
User Nickname: GOZU Kenichiro
User Superuser: true
Temperature val: 26.000000
Temperature CreatedAt: 2019/09/30 23:37:19
26.000000
success!

室温データの登録

室温は取れたのでInfluxDBに保存します。

環境構築

brewでinfluxdb入れます。

$ brew install influxdb
Updating Homebrew...
==> Auto-updated Homebrew!
Updated 1 tap (homebrew/core).
==> New Formulae
kepubify                                                    tmuxinator
==> Updated Formulae
ammonite-repl       evince              git-secret          jenkins-lts         libomp              squashfs

==> Downloading https://homebrew.bintray.com/bottles/influxdb-1.7.7.mojave.bottle.tar.gz
==> Downloading from https://akamai.bintray.com/aa/aa1cf675fa005f5f5dff6879ca4f5a31886f6d82828037c285656df9786f59c1?__g
######################################################################## 100.0%
==> Pouring influxdb-1.7.7.mojave.bottle.tar.gz
==> Caveats
To have launchd start influxdb now and restart at login:
  brew services start influxdb
Or, if you don't want/need a background service you can just run:
  influxd -config /usr/local/etc/influxdb.conf
==> Summary
🍺  /usr/local/Cellar/influxdb/1.7.7: 9 files, 127.4MB

$ influx
Failed to connect to http://localhost:8086: Get http://localhost:8086/ping: dial tcp [::1]:8086: connect: connection refused
Please check your connection settings and ensure 'influxd' is running.

落ち着け!起動してないとbrewが教えてくれてるじゃないかw

$ influxd -config /usr/local/etc/influxdb.conf

 8888888           .d888 888                   8888888b.  888888b.
   888            d88P"  888                   888  "Y88b 888  "88b
   888            888    888                   888    888 888  .88P
   888   88888b.  888888 888 888  888 888  888 888    888 8888888K.
   888   888 "88b 888    888 888  888  Y8bd8P' 888    888 888  "Y88b
   888   888  888 888    888 888  888   X88K   888    888 888    888
   888   888  888 888    888 Y88b 888 .d8""8b. 888  .d88P 888   d88P
 8888888 888  888 888    888  "Y88888 888  888 8888888P"  8888888P"

2019-09-30T17:50:06.740730Z info    InfluxDB starting   {"log_id": "0ICpYNa0000", "version": "v1.7.7", "branch": "master", "commit": "f8fdf652f348fc9980997fe1c972e2b79ddd13b0"}
2019-09-30T17:50:06.740759Z info    Go runtime  {"log_id": "0ICpYNa0000", "version": "go1.12.6", "maxprocs": 4}
2019-09-30T17:50:06.868211Z info    Using data dir  {"log_id": "0ICpYNa0000", "service": "store", "path": "/usr/local/var/influxdb/data"}
2019-09-30T17:50:06.868314Z info    Compaction settings {"log_id": "0ICpYNa0000", "service": "store", "max_concurrent_compactions": 2, "throughput_bytes_per_second": 50331648, "throughput_bytes_per_second_burst": 50331648}
2019-09-30T17:50:06.868349Z info    Open store (start)  {"log_id": "0ICpYNa0000", "service": "store", "trace_id": "0ICpYO50000", "op_name": "tsdb_open", "op_event": "start"}
2019-09-30T17:50:06.868582Z info    Open store (end)    {"log_id": "0ICpYNa0000", "service": "store", "trace_id": "0ICpYO50000", "op_name": "tsdb_open", "op_event": "end", "op_elapsed": "0.236ms"}
2019-09-30T17:50:06.868937Z info    Registered diagnostics client   {"log_id": "0ICpYNa0000", "service": "monitor", "name": "system"}

DB作ります。

$ influx
Connected to http://localhost:8086 version v1.7.7
InfluxDB shell version: v1.7.7
> show databases
name: databases
name
----
_internal
> create database remo
> show databases
name: databases
name
----
_internal
remo

アクセス用のユーザ作っておくべきなんですがちょいと手抜きで・・・

データ登録

参考にした記事では、"github.com/influxdata/influxdb/client/v2" を使えと書いてあったがgo getでエラーになる。URLが変わっているらしい。

import (
   "github.com/influxdata/influxdb/client/v2"
)

$ go get
package github.com/influxdata/influxdb/client/v2: cannot find package "github.com/influxdata/influxdb/client/v2" in any of:
    /usr/local/Cellar/go/1.13.1/libexec/src/github.com/influxdata/influxdb/client/v2 (from $GOROOT)
    /Users/gozu/go/src/github.com/influxdata/influxdb/client/v2 (from $GOPATH)

"github.com/influxdata/influxdb1-client/v2" に修正する。

Client作って tags と fields を登録する流れ。
値は複数登録できるっぽいけどとりあえず一回起動したら一回データを登録するようにしておいた。(cronで登録すればいいし)

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/influxdata/influxdb1-client/v2"
)

func PushData(te float64) {
    MyDB := "remo"
    username := "root"
    password := "root"
    MyMeasurement := "temperature"

    c, err := client.NewHTTPClient(client.HTTPConfig{
        Addr:     "http://localhost:8086",
        Username: username,
        Password: password,
    })
    if err != nil {
        log.Fatalln("Error: ", err)
    }

    bp, err := client.NewBatchPoints(client.BatchPointsConfig{
        Database:  MyDB,
        Precision: "s",
    })
    if err != nil {
        log.Fatalln("Error: ", err)
    }

    tags := map[string]string{"remo": "3F"}
    fields := map[string]interface{}{
        "temperature": te,
    }

    pt, err := client.NewPoint(MyMeasurement, tags, fields, time.Now())
    if err != nil {
        log.Fatalln("Error: ", err)
    }

    bp.AddPoint(pt)
    err = c.Write(bp)
    if err != nil {
        log.Fatalf("Unable to write value. %v", err)
    }

    fmt.Printf("success!\n")
}

実行してみる。

$ go run remo.go remoget.go httpclient.go influxdb.go

確認。

$ influx
Connected to http://localhost:8086 version v1.7.7
InfluxDB shell version: v1.7.7
> use remo
Using database remo
> show series
key
---
temperature,remo=3F
> select * from temperature;
name: temperature
time                remo temperature
----                ---- -----------
1569869753000000000 3F   26
> precision rfc3339
> select * from temperature;
name: temperature
time                 remo temperature
----                 ---- -----------
2019-09-30T18:55:53Z 3F   26
> 

入ってるね!
それではとりあえずcronで1分に1回実行するようにしてデータ取ってみる。

$ crontab -e
*/1 * * * * /Users/gozu/go/src/github.com/gozuk16/natureremo_temperature/natureremo_temperature > /dev/null 2>&1

まあ取れてる。

> select * from temperature;
name: temperature
time                 remo temperature
----                 ---- -----------
2019-09-30T18:55:53Z 3F   26
2019-09-30T19:25:02Z 3F   26
2019-09-30T19:26:01Z 3F   26
2019-09-30T19:27:02Z 3F   26
2019-09-30T19:28:01Z 3F   26
2019-09-30T19:29:01Z 3F   26
2019-09-30T19:30:02Z 3F   26
2019-09-30T23:09:01Z 3F   26.59
2019-09-30T23:10:01Z 3F   26.59
2019-09-30T23:16:01Z 3F   26.59
2019-09-30T23:17:01Z 3F   26.59
2019-09-30T23:18:01Z 3F   26.59
2019-09-30T23:45:01Z 3F   26.59
2019-09-30T23:46:01Z 3F   27.2
2019-10-01T00:04:02Z 3F   27.79
2019-10-01T00:05:01Z 3F   27.79
2019-10-01T00:06:01Z 3F   27.79
2019-10-01T00:07:01Z 3F   27.79
2019-10-01T00:11:01Z 3F   27.79
2019-10-01T00:13:47Z 3F   27.79
2019-10-01T00:13:48Z 3F   27.79
2019-10-01T00:14:01Z 3F   27.2
2019-10-01T00:15:01Z 3F   27.2
2019-10-01T00:16:01Z 3F   27.2
2019-10-01T00:17:01Z 3F   27.2
2019-10-01T00:18:01Z 3F   27.2
2019-10-01T00:24:01Z 3F   27.2
2019-10-01T00:25:01Z 3F   27.2
2019-10-01T00:26:01Z 3F   27.2
2019-10-01T00:27:01Z 3F   27.2
2019-10-01T00:28:01Z 3F   27.2
2019-10-01T01:05:01Z 3F   27.79
2019-10-01T01:06:01Z 3F   27.79
2019-10-01T01:07:01Z 3F   27.79
2019-10-01T01:11:01Z 3F   27.79
2019-10-01T01:24:01Z 3F   27.79
2019-10-01T01:25:01Z 3F   27.79
2019-10-01T03:17:02Z 3F   29.59
2019-10-01T03:18:01Z 3F   29.59
2019-10-01T03:19:01Z 3F   29.59
2019-10-01T03:20:01Z 3F   29.59
2019-10-01T03:21:01Z 3F   29.59
2019-10-01T03:22:01Z 3F   29.59

貼り付けたデータは適当に間引いてます。

とりあえずLTするために突貫で書いたので後でまとめなおそう・・・