うちのGoogle Homeをサンタさんが来るのが待ち遠しい子供のようにしてみました

Googleが毎年やっているSanta Trackerを使ってサンタさんが今どこでプレゼントを配っているかを逐次報告してくれるようにしてみました。

https://github.com/usk81/til/tree/master/go/santa-stalker

# Santa TrackerのAPI

- [自分の現在地](https://santa-api.appspot.com/info?client=web&language=en&fingerprint=&routeOffset=0&streamOffset=0)
- [サンタの目的地一覧](https://firebasestorage.googleapis.com/v0/b/santa-tracker-firebase.appspot.com/o/route%2Fsanta_en.json?alt=media&2019b)
  - URLの`2019`を変えればその年のデータになるようです
  - `en`を`ja`に変えれば地名を英語から日本語に変更できます

# 色々諦めたこと

- 緯度経度と距離から近くにいるよとかしたかった
  - 近くの定義が難しかったのでやめました
- 住んでいるエリアにサンタさんが来たら、`santa claus is comin to town`をSpotifyでながす
  - Google Assistant APIの実装が必要そうなので1日では無理だったのでやめました

# コード

```go:main.go
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/ikasamah/homecast"
)

const (
	// 自分の住んでいる場所に一番近いエリア
	myID     = "tokyo"
	santaURL = "https://firebasestorage.googleapis.com/v0/b/santa-tracker-firebase.appspot.com/o/route%2Fsanta_en.json?alt=media&2019b"
)

var jst *time.Location

func init() {
	var err error
	jst, err = time.LoadLocation("Asia/Tokyo")
	if err != nil {
		jst = time.FixedZone("Asia/Tokyo", 9*60*60)
	}
}

func main() {
	tk, err := GetTracker(santaURL)
	if err != nil {
		panic(err)
	}

	fmt.Println("Start stalking")
	for {
		r := tk.Current(time.Now())
		// なんかも同じメッセージを喋り続けられるとうざいので、ステータスが変わるまでキャッシュする 
		// キャッシュされてない場合のみメッセージを話させる
		if !r.IsCache {
			if r.Destination.ID == myID {
				// サンタさん キタ━━━(ﾟ∀ﾟ)━━━!! 
				googlehome("Santa Claus is coming to town!! Santa Claus is coming to town!! Santa Claus is coming to town!!")
				// 自分の街についたら通知をやめる
				break
			} else {
				if r.Status == statusDeliver {
					// 今サンタさんがプレゼントを配っている場所を通知
					googlehome(fmt.Sprintf("Santa Claus is in %s, %s\n", r.Destination.City, r.Destination.Region))
				}
			}
		}
		// ループを繰り返す感覚が短すぎるのでちょっとスリープ
		time.Sleep(5 * time.Second)
	}
	fmt.Println("finish")
}

// Google Homeデバイスに任意のメッセージ話させる
func googlehome(msg string) {
	fmt.Println(msg)
	ctx := context.Background()
	devices := homecast.LookupAndConnect(ctx)
	for _, device := range devices {
		device.Speak(ctx, msg, "en")
	}
}
```

```go:tracker.go
package main

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

const (
	statusTakeOff = "Takeoff"
	statusMove    = "Move"
	statusDeliver = "Deliver"
	statusFinsish = "Finsish"
)

type APIResponse struct {
	Status       string        `json:"status"`
	Language     string        `json:"language"`
	TimeOffset   int           `json:"timeOffset"`
	Fingerprint  string        `json:"fingerprint"`
	Destinations []Destination `json:"destinations"`
}

type Destination struct {
	ID                string   `json:"id"`
	Arrival           int64    `json:"arrival"`
	Departure         int64    `json:"departure"`
	Population        int      `json:"population"`
	PresentsDelivered int      `json:"presentsDelivered"`
	City              string   `json:"city"`
	Region            string   `json:"region"`
	Location          Location `json:"location"`
}

type Location struct {
	Lat float64 `json:"lat"`
	Lng float64 `json:"lng"`
}

type Tracker struct {
	Destinations []Destination
	Cache        Cache
}

type Cache struct {
	ExpiredAt   int64
	Status      string
	Destination Destination
}

type Result struct {
	IsCache     bool
	Status      string
	Destination Destination
}

func GetTracker(uri string) (result *Tracker, err error) {
	resp, err := http.Get(uri)
	if err != nil {
		return
	}
	defer resp.Body.Close()

	var r APIResponse
	if err = json.NewDecoder(resp.Body).Decode(&r); err != nil {
		return
	}

	return &Tracker{
		Destinations: r.Destinations,
	}, nil
}

func (tk *Tracker) SetCache(dest Destination, status string, expiredAt int64) {
	tk.Cache = Cache{
		Destination: dest,
		Status:      status,
		ExpiredAt:   expiredAt,
	}
}

func (tk *Tracker) Current(tt time.Time) Result {
	ts := tt.Unix() * 1000

	c := tk.Cache
	if c.Status != "" && c.ExpiredAt >= ts {
		return Result{
			Status:      c.Status,
			Destination: c.Destination,
			IsCache:     true,
		}
	}

	home := tk.Destinations[0]
	if home.Departure > ts {
		return Result{
			Status:      statusTakeOff,
			Destination: home,
		}
	}

	landing := tk.Destinations[len(tk.Destinations)-1]
	if landing.Arrival < ts {
		return Result{
			Status:      statusFinsish,
			Destination: landing,
		}
	}

	ds := tk.Destinations[1:]
	for _, dest := range ds {
		if dest.Arrival > ts {
			tk.SetCache(dest, statusMove, dest.Arrival-1)
			return Result{
				Status:      statusMove,
				Destination: dest,
			}
		} else if dest.Arrival <= ts && dest.Departure >= ts {
			tk.SetCache(home, statusDeliver, dest.Departure)
			return Result{
				Status:      statusDeliver,
				Destination: dest,
			}
		}
	}
	return Result{
		Status:      statusFinsish,
		Destination: landing,
	}
}
```

テストをしている時点でサンタさんが日本に来てしまったので、やってみたい人は来年以降楽しんでください。