Ported location recorder
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/t-liu93/home-automation-backend/components/homeassistant"
|
"github.com/t-liu93/home-automation-backend/components/homeassistant"
|
||||||
|
"github.com/t-liu93/home-automation-backend/components/locationRecorder"
|
||||||
"github.com/t-liu93/home-automation-backend/components/pooRecorder"
|
"github.com/t-liu93/home-automation-backend/components/pooRecorder"
|
||||||
"github.com/t-liu93/home-automation-backend/util/notion"
|
"github.com/t-liu93/home-automation-backend/util/notion"
|
||||||
)
|
)
|
||||||
@@ -45,6 +46,8 @@ func initUtil() {
|
|||||||
func initComponent() {
|
func initComponent() {
|
||||||
// init pooRecorder
|
// init pooRecorder
|
||||||
pooRecorder.Init(&scheduler)
|
pooRecorder.Init(&scheduler)
|
||||||
|
// init location recorder
|
||||||
|
locationRecorder.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
func serve(cmd *cobra.Command, args []string) {
|
func serve(cmd *cobra.Command, args []string) {
|
||||||
@@ -100,6 +103,8 @@ func serve(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
router.HandleFunc("/homeassistant/publish", homeassistant.HandleHaMessage).Methods("POST")
|
router.HandleFunc("/homeassistant/publish", homeassistant.HandleHaMessage).Methods("POST")
|
||||||
|
|
||||||
|
router.HandleFunc("/location/record", locationRecorder.HandleRecordLocation).Methods("POST")
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: ":" + port,
|
Addr: ":" + port,
|
||||||
Handler: router,
|
Handler: router,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@@ -30,6 +31,8 @@ func HandleHaMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
switch message.Target {
|
switch message.Target {
|
||||||
case "poo_recorder":
|
case "poo_recorder":
|
||||||
handlePooRecorderMsg(message)
|
handlePooRecorderMsg(message)
|
||||||
|
case "location_recorder":
|
||||||
|
handleLocationRecorderMsg(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -42,6 +45,27 @@ func handlePooRecorderMsg(message haMessage) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleLocationRecorderMsg(message haMessage) {
|
||||||
|
if message.Action == "record" {
|
||||||
|
port := viper.GetString("port")
|
||||||
|
req, err := http.NewRequest("POST", "http://localhost:"+port+"/location/record", strings.NewReader(strings.ReplaceAll(message.Content, "'", "\"")))
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("handleLocationRecorderMsg Error creating request to location recorder", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Second * 1,
|
||||||
|
}
|
||||||
|
_, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("handleLocationRecorderMsg Error sending request to location recorder", err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slog.Warn(fmt.Sprintln("handleLocationRecorderMsg Unknown action", message.Action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func handleGetLatestPoo() {
|
func handleGetLatestPoo() {
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: time.Second * 1,
|
Timeout: time.Second * 1,
|
||||||
|
|||||||
194
src/components/locationRecorder/locationRecorder.go
Normal file
194
src/components/locationRecorder/locationRecorder.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package locationRecorder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
db *sql.DB
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
currentDBVersion = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Location struct {
|
||||||
|
Person string `json:"person"`
|
||||||
|
DateTime string `json:"datetime"`
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
Altitude sql.NullFloat64 `json:"altitude,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocationContent struct {
|
||||||
|
Person string `json:"person"`
|
||||||
|
Latitude string `json:"latitude"`
|
||||||
|
Longitude string `json:"longitude"`
|
||||||
|
Altitude string `json:"altitude,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
initDb()
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleRecordLocation(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var location LocationContent
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
err := decoder.Decode(&location)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleRecordLocation Error decoding request body", err))
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
latiF64, _ := strconv.ParseFloat(location.Latitude, 64)
|
||||||
|
longiF64, _ := strconv.ParseFloat(location.Longitude, 64)
|
||||||
|
altiF64, _ := strconv.ParseFloat(location.Altitude, 64)
|
||||||
|
InsertLocationNow(location.Person, latiF64, longiF64, altiF64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertLocation(person string, datetime time.Time, latitude float64, longitude float64, altitude float64) {
|
||||||
|
_, err := db.Exec(`INSERT OR IGNORE INTO location (person, datetime, latitude, longitude, altitude) VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
person, datetime.UTC().Format(time.RFC3339), latitude, longitude, altitude)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorder.InsertLocation Error inserting location", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertLocationNow(person string, latitude float64, longitude float64, altitude float64) {
|
||||||
|
InsertLocation(person, time.Now(), latitude, longitude, altitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDb() {
|
||||||
|
if !viper.InConfig("locationRecorder.dbPath") {
|
||||||
|
slog.Info("LocationRecorderInit dbPath not found in config file, using default: location_recorder.db")
|
||||||
|
viper.SetDefault("locationRecorder.dbPath", "location_recorder.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := viper.GetString("locationRecorder.dbPath")
|
||||||
|
err := error(nil)
|
||||||
|
db, err = sql.Open("sqlite", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit Error opening database", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = db.Ping()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit Error pinging database", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
migrateDb()
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateDb() {
|
||||||
|
var userVersion int
|
||||||
|
err := db.QueryRow("PRAGMA user_version").Scan(&userVersion)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit Error getting db user version", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if userVersion == 0 {
|
||||||
|
migrateDb0To1(&userVersion)
|
||||||
|
}
|
||||||
|
if userVersion == 1 {
|
||||||
|
migrateDb1To2(&userVersion)
|
||||||
|
}
|
||||||
|
if userVersion != currentDBVersion {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit Error unsupported database version", userVersion))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateDb0To1(userVersion *int) {
|
||||||
|
// this is actually create new db
|
||||||
|
slog.Info("Creating location recorder database version 1..")
|
||||||
|
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS location (
|
||||||
|
person TEXT NOT NULL,
|
||||||
|
datetime TEXT NOT NULL,
|
||||||
|
latitude REAL NOT NULL,
|
||||||
|
longitude REAL NOT NULL,
|
||||||
|
altitude REAL,
|
||||||
|
PRIMARY KEY (person, datetime))`)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit DB0To1 Error creating table", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
_, err = db.Exec(`PRAGMA user_version = 1`)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit DB0To1 Error setting user version to 1", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
*userVersion = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateDb1To2(userVersion *int) {
|
||||||
|
// this will change the datetime format into Real RFC3339
|
||||||
|
slog.Info("Migrating location recorder database version 1 to 2..")
|
||||||
|
dbTx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit DB1To2 Error beginning transaction", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fail := func(err error, step string) {
|
||||||
|
slog.Error(fmt.Sprintf("LocationRecorderInit DB1To2 Error %s: %s", step, err))
|
||||||
|
dbTx.Rollback()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
_, err = dbTx.Exec(`ALTER TABLE location RENAME TO location_old`)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "renaming table")
|
||||||
|
}
|
||||||
|
_, err = dbTx.Exec(`CREATE TABLE IF NOT EXISTS location (
|
||||||
|
person TEXT NOT NULL,
|
||||||
|
datetime TEXT NOT NULL,
|
||||||
|
latitude REAL NOT NULL,
|
||||||
|
longitude REAL NOT NULL,
|
||||||
|
altitude REAL,
|
||||||
|
PRIMARY KEY (person, datetime))`)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "creating new table")
|
||||||
|
}
|
||||||
|
row, err := dbTx.Query(`SELECT person, datetime, latitude, longitude, altitude FROM location_old`)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "selecting from old table")
|
||||||
|
}
|
||||||
|
defer row.Close()
|
||||||
|
for row.Next() {
|
||||||
|
var location Location
|
||||||
|
err = row.Scan(&location.Person, &location.DateTime, &location.Latitude, &location.Longitude, &location.Altitude)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "scanning row")
|
||||||
|
}
|
||||||
|
dateTime, err := time.Parse("2006-01-02T15:04:05-0700", location.DateTime)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "parsing datetime")
|
||||||
|
}
|
||||||
|
_, err = dbTx.Exec(`INSERT INTO location (person, datetime, latitude, longitude, altitude) VALUES (?, ?, ?, ?, ?)`, location.Person, dateTime.UTC().Format(time.RFC3339), location.Latitude, location.Longitude, location.Altitude)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "inserting new row")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dbTx.Exec(`DROP TABLE location_old`)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "dropping old table")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dbTx.Exec(`PRAGMA user_version = 2`)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit Error setting user version to 2", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
dbTx.Commit()
|
||||||
|
*userVersion = 2
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ type recordDetail struct {
|
|||||||
|
|
||||||
type pooStatusSensorAttributes struct {
|
type pooStatusSensorAttributes struct {
|
||||||
LastPoo string `json:"last_poo"`
|
LastPoo string `json:"last_poo"`
|
||||||
|
FriendlyName string `json:"friendly_name,"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type pooStatusWebhookBody struct {
|
type pooStatusWebhookBody struct {
|
||||||
@@ -49,6 +50,8 @@ type pooStatusDbEntry struct {
|
|||||||
func Init(mainScheduler *gocron.Scheduler) {
|
func Init(mainScheduler *gocron.Scheduler) {
|
||||||
initDb()
|
initDb()
|
||||||
initScheduler(mainScheduler)
|
initScheduler(mainScheduler)
|
||||||
|
notionDbSync()
|
||||||
|
publishLatestPooSensor()
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleRecordPoo(w http.ResponseWriter, r *http.Request) {
|
func HandleRecordPoo(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -73,14 +76,7 @@ func HandleRecordPoo(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pooStatus := homeassistantutil.HttpSensor{
|
publishLatestPooSensor()
|
||||||
EntityId: "sensor.test_poo_sensor",
|
|
||||||
State: record.Status,
|
|
||||||
Attributes: pooStatusSensorAttributes{
|
|
||||||
LastPoo: now.Format("Mon | 2006-01-02 | 15:04"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
homeassistantutil.PublishSensor(pooStatus)
|
|
||||||
if viper.InConfig("pooRecorder.webhookId") {
|
if viper.InConfig("pooRecorder.webhookId") {
|
||||||
homeassistantutil.TriggerWebhook(viper.GetString("pooRecorder.webhookId"), pooStatusWebhookBody{Status: record.Status})
|
homeassistantutil.TriggerWebhook(viper.GetString("pooRecorder.webhookId"), pooStatusWebhookBody{Status: record.Status})
|
||||||
} else {
|
} else {
|
||||||
@@ -89,34 +85,47 @@ func HandleRecordPoo(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HandleNotifyLatestPoo(w http.ResponseWriter, r *http.Request) {
|
func HandleNotifyLatestPoo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := publishLatestPooSensor()
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleNotifyLatestPoo Error publishing latest poo", err))
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Debug(fmt.Sprintln("HandleGetLatestPoo Latest poo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func publishLatestPooSensor() error {
|
||||||
var latest pooStatusDbEntry
|
var latest pooStatusDbEntry
|
||||||
err := db.QueryRow(`SELECT timestamp, status, latitude, longitude FROM poo_records ORDER BY timestamp DESC LIMIT 1`).Scan(&latest.Timestamp, &latest.Status, &latest.Latitude, &latest.Longitude)
|
err := db.QueryRow(`SELECT timestamp, status, latitude, longitude FROM poo_records ORDER BY timestamp DESC LIMIT 1`).Scan(&latest.Timestamp, &latest.Status, &latest.Latitude, &latest.Longitude)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn(fmt.Sprintln("HandleGetLatestPoo Error getting latest poo", err))
|
slog.Warn(fmt.Sprintln("HandleGetLatestPoo Error getting latest poo", err))
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
recordTime, err := time.Parse("2006-01-02T15:04Z07:00", latest.Timestamp)
|
recordTime, err := time.Parse("2006-01-02T15:04Z07:00", latest.Timestamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn(fmt.Sprintln("HandleGetLatestPoo Error parsing timestamp", err))
|
slog.Warn(fmt.Sprintln("HandleGetLatestPoo Error parsing timestamp", err))
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
viper.SetDefault("pooRecorder.sensorEntityName", "sensor.test_poo_status")
|
||||||
|
viper.SetDefault("pooRecorder.sensorFriendlyName", "Poo Status")
|
||||||
|
sensorEntityName := viper.GetString("pooRecorder.sensorEntityName")
|
||||||
|
sensorFriendlyName := viper.GetString("pooRecorder.sensorFriendlyName")
|
||||||
recordTime = recordTime.Local()
|
recordTime = recordTime.Local()
|
||||||
pooStatus := homeassistantutil.HttpSensor{
|
pooStatus := homeassistantutil.HttpSensor{
|
||||||
EntityId: "sensor.test_poo_sensor",
|
EntityId: sensorEntityName,
|
||||||
State: latest.Status,
|
State: latest.Status,
|
||||||
Attributes: pooStatusSensorAttributes{
|
Attributes: pooStatusSensorAttributes{
|
||||||
LastPoo: recordTime.Format("Mon | 2006-01-02 | 15:04"),
|
LastPoo: recordTime.Format("Mon | 2006-01-02 | 15:04"),
|
||||||
|
FriendlyName: sensorFriendlyName,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
homeassistantutil.PublishSensor(pooStatus)
|
homeassistantutil.PublishSensor(pooStatus)
|
||||||
slog.Debug(fmt.Sprintln("HandleGetLatestPoo Latest poo", pooStatus.State, "at", pooStatus.Attributes.(pooStatusSensorAttributes).LastPoo))
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDb() {
|
func initDb() {
|
||||||
if !viper.InConfig("pooRecorder.dbPath") {
|
if !viper.InConfig("pooRecorder.dbPath") {
|
||||||
slog.Info("HandleRecordPoo dbPath not found in config file, using default: pooRecorder.db")
|
slog.Info("PooRecorderInit dbPath not found in config file, using default: pooRecorder.db")
|
||||||
viper.SetDefault("pooRecorder.dbPath", "pooRecorder.db")
|
viper.SetDefault("pooRecorder.dbPath", "pooRecorder.db")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user