OpenWeatherMap

パン作りでは、発酵の過程が重要です。パン生地をこねた後は、一次発酵。成形後はさらに二次発酵。いずれも時間と気温との微妙な調整です。同じ時間でも、気温の高い夏は過発酵になったり、逆に冬では発酵時間が足りなくてパンが膨らまないなどのトラブルが発生します。

そこで、温度や湿度、その日の天気などを記録しておけば、うまくいく発酵時間を見極めることができるのではないかと思い、天候データのデータベース化を考えてみました。

色々調査してみたら、無償のOpenWeatherMapの利用が最適と考えました。頻繁なアクセスや高度な利用にはそれなりの費用がかかりますが、パン屋さんの記録用なら、2時間に1回程度の取得で充分でしょう。

データベースはMariaDBを使うことにします。

drop table if exists weathers;
create table weathers (
        measure_time timestamp default current_timestamp, -- 計測時刻
        city_id int default 1852899, -- 市コード
        cloudiness varchar(20) not null, -- 天候
        degree float default 0, -- 温度
        humidity int default 0, -- 湿度
        pressure int default 1000, -- 気圧
 primary key (measure_time)
 );

OpenWeatherMapを利用するにはアカウント作成が必要ですので、サイトメニューのpriceメニューを確認して納得したうえで行ってください。アカウント作成が正常に完了するとAPIIDの長い文字列が表示されますので、これをAPIのデータに付けて問い合わせを行います。

http://api.openweathermap.org/data/2.5/weather?id=1852899&units=metric&appid=(取得したAPI ID)

これで取得できるデータをGO言語で直接取得して、先程作成したweathersテーブルに挿入していきます。作成するプログラムはコマンドとして1回実行して終了します。これをcrontabで2時間に1回呼び出せば、計測時刻の天候、温度、湿度、気圧を記録します。市コードはお店のある佐世保市のものですが、英国のサービスでありながら、日本についてもかなり細かなエリアで指定できるようです。

PalpanのWebサイトのフレームワークはBeegoで作成してますが、今回は特にこれを利用したものではありません。管理しやすいようにプロジェクトの下にvendorフォルダを作成して、その中にweather.goを作成します。

package main

import (
        "encoding/json"
        "log"
        "net/http"
        "time"

        "fmt"
        mdl "shop/models"
)

shopプロジェクトの下のmodelsを読み込んでいますが、これはのちほど作成します。
続けて、今回必要な構造体や変数を定義します。

type weatherData struct {
        Name string    `json:"name"`
        Sky  []SkyData `json:"weather"`
        Main struct {
                Degree   float64 `json:"temp"`
                Humidity float64 `json:"humidity"`
                Pressure float64 `json:"pressure"`
        } `json:"main"`
}

type SkyData struct {
        ID      int    `json:"id"`
        Descrip string `json:"description"`
        Icon    string `json:"icon"`
}

var myClient = &http.Client{Timeout: 10 * time.Second}

空模様(SkyData、jsonではweatherで取得)でハマってしましましたが、配列として記述するのが良いようです。

これで準備が整いましたので、メインに取りかかります。

func main() {
        url := "http://api.openweathermap.org/data/2.5/weather?id=1852899&units=metric&appid=(取得したAPIID)"

        w := new(weatherData)
        err := getJson(url, &w)

        if err != nil {
                log.Println(err.Error())
                return
        }

        _, err = mdl.NewDB()

        if err != nil {
                log.Println(err.Error())
                return
        }

        weather := new(mdl.Weather)
        weather.Cloudiness = sky2Jp(w.Sky[0].ID)
        weather.Degree = w.Main.Degree
        weather.Humidity = int(w.Main.Humidity)
        weather.Pressure = int(w.Main.Pressure)

        fmt.Printf("%v", weather)

        err = mdl.AddWeather(weather)

        if err != nil {
                log.Println(err.Error())
        }
        mdl.CloseDB()
}

mainの中に書くと混乱しそうなので、jsonデータの取得やら、データベースへの書き込みは外出しにしてあります。

func getJson(url string, target interface{}) error {
        r, err := myClient.Get(url)
        if err != nil {
                log.Println(err.Error())
                return err
        }
        defer r.Body.Close()

        return json.NewDecoder(r.Body).Decode(target)
}

さらに、空模様は英語で帰ってくるので日本語に変換。かなり力技で書いてるように見えますが、Excelを使ってパパっと作ったものです。

func sky2Jp(id int) string {

        var s string = ""
        switch id {
        case 200:
                s = "雷雨"
        case 201:
                s = "強い雷雨"
        case 202:
                s = "激しい雷雨"
        case 210:
                s = "雷雨"
        case 211:
                s = "雷雨"
        case 212:
                s = "強い雷雨"
        case 221:
                s = "荒れた天気"
        case 230:
                s = "強い雷雨"
        case 231:
                s = "強い雷雨"
        case 232:
                s = "強い雷雨"
        case 300:
                s = "霧雨"
        case 301:
                s = "霧雨"
        case 302:
                s = "暴風雨"
        case 310:
                s = "暴風雨"
        case 311:
                s = "暴風雨"
        case 312:
                s = "暴風雨"
        case 313:
                s = "暴風雨"
        case 314:
                s = "暴風雨"
        case 321:
                s = "暴風雨"
        case 500:
                s = "小雨"
        case 501:
                s = "雨"
        case 502:
                s = "本降りの雨"
        case 503:
                s = "かなり激しい雨"
        case 504:
                s = "土砂降りの雨"
        case 511:
                s = "氷雨"
        case 520:
                s = "雨まじりの雪"
        case 521:
                s = "強い雨"
        case 522:
                s = "激しい雨"
        case 531:
                s = "荒れた天気"
        case 600:
                s = "小雪"
        case 601:
                s = "雪"
        case 602:
                s = "強い雪"
        case 611:
                s = "みぞれ"
        case 612:
                s = "みぞれ"
        case 615:
                s = "小雨まじりの雪"
        case 616:
                s = "雨まじりの雪"
        case 620:
                s = "雪"
        case 621:
                s = "雪"
        case 622:
                s = "激しい雪"
        case 701:
                s = "霧"
        case 711:
                s = "霞"
        case 721:
                s = "もや"
        case 731:
                s = "砂埃"
        case 741:
                s = "濃い霧"
        case 751:
                s = "砂嵐"
        case 761:
                s = "スモッグ"
        case 762:
                s = "火山灰"
        case 771:
                s = "夕立"
        case 781:
                s = "暴風"
        case 800:
                s = "快晴"
        case 801:
                s = "晴れ"
        case 802:
                s = "曇り"
        case 803:
                s = "曇り"
        case 804:
                s = "本曇り"
        }
        return s
}

天候に関する英語の微妙な表現が分からないところは適当に書いてます。

データベースへの入出力はmodelsフォルダに書いています。データベースへの接続、切断はdb.goにまとめています。

// db.go
package models

import (
        "database/sql"
        _ "github.com/go-sql-driver/mysql"
        "log"
)

type DB struct {
        *sql.DB
}

const (
        DataSource = "shop:shop@/shop?parseTime=true"
)

var Db *sql.DB
// データベース接続
func NewDB() (*DB, error) {
        db, err := sql.Open("mysql", DataSource)
        if err != nil {
                return nil, err
        }
        if err = db.Ping(); err != nil {
                return nil, err
        }

        Db = db

        return &DB{db}, nil
}
// データベース切断
func CloseDB() {
        if err := Db.Ping(); err == nil {
                Db.Close()
                log.Println("データベース切断")
        }
}

DataSource文字列は、自分のMariaDB環境に合わせてください。

Weatherモデルはmodelsの下のweather.goに記述します。

package models

import (
        _ "github.com/go-sql-driver/mysql"
        "log"
        "time"
)

type Weather struct {
        MasureTime time.Time `db:"measure_time"`
        CityId     int    `db:"city_id"`
        Cloudiness string  `db:"cloudiness"`
        Degree float64 `db:"degree"`
        Humidity int `db:"humidity"`
        Pressure int `db:"pressure"`
}

// 追加
func AddWeather(p *Weather) error {

        que := "insert into weathers ( cloudiness,degree,humidity,pressure) values (?,?,?,?)"

        _, err := Db.Exec(que, p.Cloudiness,p.Degree,p.Humidity,p.Pressure)
        if err != nil {
                log.Println(err.Error())
                return err
        }
        return nil
}
// 最新データ取得
func GetWeather() (*Weather, error) {

        que := "select * from weathers where measure_time = (select max(measure_time) from weathers)"

        p := new(Weather)

        err := Db.QueryRow(que).Scan(
                &p.MasureTime,
                &p.CityId,
                &p.Cloudiness,
                &p.Degree,
                &p.Humidity,
                &p.Pressure,
        )

        if err != nil {
                return nil, err
        }

        return p, nil
}

これを次のコマンドで実行

go run weather.go

これをMariaDBで直接参照してみると、

MariaDB [shop]> select * from weathers;
+---------------------+---------+------------+--------+----------+----------+
| measure_time        | city_id | cloudiness | degree | humidity | pressure |
+---------------------+---------+------------+--------+----------+----------+
| 2018-11-14 19:36:25 | 1852899 | 快晴       |  12.94 |       67 |     1020 |
| 2018-11-14 19:38:24 | 1852899 | 快晴       |  12.94 |       67 |     1020 |
| 2018-11-14 20:01:13 | 1852899 | 快晴       |  12.93 |       67 |     1020 |
| 2018-11-14 20:03:31 | 1852899 | 快晴       |  12.93 |       67 |     1020 |
| 2018-11-14 21:00:02 | 1852899 | 快晴       |  11.93 |       71 |     1021 |
| 2018-11-14 21:23:39 | 1852899 | 快晴       |   11.9 |       71 |     1021 |
| 2018-11-14 23:00:01 | 1852899 | 晴れ       |  14.02 |      100 |     1031 |
+---------------------+---------+------------+--------+----------+----------+
7 rows in set (0.00 sec)

予定どおり取れてます。まあ、書いている間に何回かテストしているので、結構な量出来上がってます。
ついでに、最新の1件表示。

MariaDB [shop]> select * from weathers where measure_time = (select max(measure_time) from weathers);
+---------------------+---------+------------+--------+----------+----------+
| measure_time        | city_id | cloudiness | degree | humidity | pressure |
+---------------------+---------+------------+--------+----------+----------+
| 2018-11-14 23:00:01 | 1852899 | 晴れ       |  14.02 |      100 |     1031 |
+---------------------+---------+------------+--------+----------+----------+
1 row in set (0.00 sec)

これで気象データは自動的に記録されたので、レジデータと組み合わせれば、天候による売れ筋商品が見えてくるかも。