Poo recorder almost ported to go, needs to have perodically backup
This commit is contained in:
54
src/components/homeassistant/homeassistant.go
Normal file
54
src/components/homeassistant/homeassistant.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package homeassistant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type haMessage struct {
|
||||
Target string `json:"target"`
|
||||
Action string `json:"action"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func HandleHaMessage(w http.ResponseWriter, r *http.Request) {
|
||||
var message haMessage
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
err := decoder.Decode(&message)
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("HandleHaMessage Error decoding request body", err))
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch message.Target {
|
||||
case "poo_recorder":
|
||||
handlePooRecorderMsg(message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handlePooRecorderMsg(message haMessage) {
|
||||
switch message.Action {
|
||||
case "get_latest":
|
||||
handleGetLatestPoo()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handleGetLatestPoo() {
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 1,
|
||||
}
|
||||
port := viper.GetString("port")
|
||||
_, err := client.Get("http://localhost:" + port + "/poo/latest")
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("handleGetLatestPoo Error sending request to poo recorder", err))
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
package pooRecorder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/t-liu93/home-automation-backend/util/notion"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
type recordDetail struct {
|
||||
Status string `json:"status"`
|
||||
Latitude string `json:"latitude"`
|
||||
Longitude string `json:"longitude"`
|
||||
}
|
||||
|
||||
type pooStatusHttpSensorAttributes struct {
|
||||
LastPoo string `json:"last_poo"`
|
||||
}
|
||||
|
||||
type pooStatusHttpSensor struct {
|
||||
EntityId string `json:"entity_id"`
|
||||
State string `json:"state"`
|
||||
Attributes pooStatusHttpSensorAttributes `json:"attributes"`
|
||||
}
|
||||
|
||||
var (
|
||||
db *sql.DB
|
||||
)
|
||||
|
||||
func publishPooStatus(pooStatus pooStatusHttpSensor) {
|
||||
if viper.InConfig("pooRecorder.homeassistantIp") &&
|
||||
viper.InConfig("pooRecorder.homeassistantPort") &&
|
||||
viper.InConfig("pooRecorder.homeassistantToken") {
|
||||
homeAssistantIp := viper.GetString("pooRecorder.homeassistantIp")
|
||||
homeAssistantPort := viper.GetString("pooRecorder.homeassistantPort")
|
||||
url := fmt.Sprintf("http://%s:%s/api/states/%s", homeAssistantIp, homeAssistantPort, pooStatus.EntityId)
|
||||
payload, err := json.Marshal(pooStatus)
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("HandleRecordPoo Error marshalling poo status", err))
|
||||
return
|
||||
}
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("HandleRecordPoo Error creating request", err))
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+viper.GetString("pooRecorder.homeassistantToken"))
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 1,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("HandleRecordPoo Error sending request", err))
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
slog.Warn(fmt.Sprintln("HandleRecordPoo Unexpected response status", resp.StatusCode))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
} else {
|
||||
slog.Warn("HandleRecordPoo Home Assistant IP, port, or token not found in config file")
|
||||
}
|
||||
}
|
||||
|
||||
func storeStatus(record recordDetail, timestamp time.Time) {
|
||||
tableId := viper.GetString("pooRecorder.tableId")
|
||||
recordDate := timestamp.Format("2006-01-02")
|
||||
recordTime := timestamp.Format("15:04")
|
||||
slog.Info(fmt.Sprintln("Recording poo", record.Status, "at", record.Latitude, record.Longitude))
|
||||
go func() {
|
||||
header, err := notion.GetTableRows(tableId, 1, "")
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("HandleRecordPoo Failed to get table header", err))
|
||||
return
|
||||
}
|
||||
if len(header) == 0 {
|
||||
slog.Warn("HandleRecordPoo Table header not found")
|
||||
return
|
||||
}
|
||||
headerId := header[0].GetID()
|
||||
err = notion.WriteTableRow([]string{recordDate, recordTime, record.Status, record.Latitude + "," + record.Longitude}, tableId, headerId.String())
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("HandleRecordPoo Failed to write table row", err))
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
func migrateDb() {
|
||||
|
||||
}
|
||||
|
||||
func initDb() {
|
||||
if !viper.InConfig("pooRecorder.dbPath") {
|
||||
slog.Info("HandleRecordPoo dbPath not found in config file, using default: pooRecorder.db")
|
||||
viper.SetDefault("pooRecorder.dbPath", "pooRecorder.db")
|
||||
}
|
||||
|
||||
dbPath := viper.GetString("pooRecorder.dbPath")
|
||||
err := error(nil)
|
||||
db, err = sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
slog.Error(fmt.Sprintln("PooRecorderInit Error opening database", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
err = db.Ping()
|
||||
if err != nil {
|
||||
slog.Error(fmt.Sprintln("PooRecorderInit Error pinging database", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS poo_records (
|
||||
timestamp TEXT PRIMARY KEY,
|
||||
status TEXT,
|
||||
latitude TEXT,
|
||||
longitude TEXT)`)
|
||||
|
||||
}
|
||||
|
||||
func Init() {
|
||||
initDb()
|
||||
}
|
||||
|
||||
func HandleRecordPoo(w http.ResponseWriter, r *http.Request) {
|
||||
var record recordDetail
|
||||
if !viper.InConfig("pooRecorder.tableId") {
|
||||
slog.Warn("HandleRecordPoo Table ID not found in config file")
|
||||
http.Error(w, "Table ID not found in config file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
decorder := json.NewDecoder(r.Body)
|
||||
decorder.DisallowUnknownFields()
|
||||
err := decorder.Decode(&record)
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("HandleRecordPoo Error decoding request body", err))
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
storeStatus(record, now)
|
||||
timeAttribute := now.Format("Mon | 2006-01-02 | 15:04")
|
||||
pooStatus := pooStatusHttpSensor{
|
||||
EntityId: "sensor.test_poo_sensor",
|
||||
State: record.Status,
|
||||
Attributes: pooStatusHttpSensorAttributes{
|
||||
LastPoo: timeAttribute,
|
||||
},
|
||||
}
|
||||
publishPooStatus(pooStatus)
|
||||
}
|
||||
210
src/components/pooRecorder/pooRecorder.go
Normal file
210
src/components/pooRecorder/pooRecorder.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package pooRecorder
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"log/slog"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/t-liu93/home-automation-backend/util/homeassistantutil"
|
||||
"github.com/t-liu93/home-automation-backend/util/notion"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
var (
|
||||
db *sql.DB
|
||||
scheduler *gocron.Scheduler
|
||||
)
|
||||
|
||||
type recordDetail struct {
|
||||
Status string `json:"status"`
|
||||
Latitude string `json:"latitude"`
|
||||
Longitude string `json:"longitude"`
|
||||
}
|
||||
|
||||
type pooStatusSensorAttributes struct {
|
||||
LastPoo string `json:"last_poo"`
|
||||
}
|
||||
|
||||
type pooStatusWebhookBody struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type pooStatusDbEntry struct {
|
||||
Timestamp string
|
||||
Status string
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
}
|
||||
|
||||
func Init(mainScheduler *gocron.Scheduler) {
|
||||
initDb()
|
||||
initScheduler(mainScheduler)
|
||||
}
|
||||
|
||||
func HandleRecordPoo(w http.ResponseWriter, r *http.Request) {
|
||||
var record recordDetail
|
||||
if !viper.InConfig("pooRecorder.tableId") {
|
||||
slog.Warn("HandleRecordPoo Table ID not found in config file")
|
||||
http.Error(w, "Table ID not found in config file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
err := decoder.Decode(&record)
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("HandleRecordPoo Error decoding request body", err))
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
err = storeStatus(record, now)
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("HandleRecordPoo Error storing status", err))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
pooStatus := homeassistantutil.HttpSensor{
|
||||
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") {
|
||||
homeassistantutil.TriggerWebhook(viper.GetString("pooRecorder.webhookId"), pooStatusWebhookBody{Status: record.Status})
|
||||
} else {
|
||||
slog.Warn("HandleRecordPoo Webhook ID not found in config file")
|
||||
}
|
||||
}
|
||||
|
||||
func HandleNotifyLatestPoo(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("HandleGetLatestPoo Error getting latest poo", err))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
recordTime, err := time.Parse("2006-01-02T15:04Z07:00", latest.Timestamp)
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("HandleGetLatestPoo Error parsing timestamp", err))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
recordTime = recordTime.Local()
|
||||
pooStatus := homeassistantutil.HttpSensor{
|
||||
EntityId: "sensor.test_poo_sensor",
|
||||
State: latest.Status,
|
||||
Attributes: pooStatusSensorAttributes{
|
||||
LastPoo: recordTime.Format("Mon | 2006-01-02 | 15:04"),
|
||||
},
|
||||
}
|
||||
homeassistantutil.PublishSensor(pooStatus)
|
||||
slog.Debug(fmt.Sprintln("HandleGetLatestPoo Latest poo", pooStatus.State, "at", pooStatus.Attributes.(pooStatusSensorAttributes).LastPoo))
|
||||
}
|
||||
|
||||
func initDb() {
|
||||
if !viper.InConfig("pooRecorder.dbPath") {
|
||||
slog.Info("HandleRecordPoo dbPath not found in config file, using default: pooRecorder.db")
|
||||
viper.SetDefault("pooRecorder.dbPath", "pooRecorder.db")
|
||||
}
|
||||
|
||||
dbPath := viper.GetString("pooRecorder.dbPath")
|
||||
err := error(nil)
|
||||
db, err = sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
slog.Error(fmt.Sprintln("PooRecorderInit Error opening database", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
err = db.Ping()
|
||||
if err != nil {
|
||||
slog.Error(fmt.Sprintln("PooRecorderInit 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("PooRecorderInit Error getting db user version", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
if userVersion == 0 {
|
||||
migrateDb0To1(&userVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func migrateDb0To1(userVersion *int) {
|
||||
// this is actually create new db
|
||||
slog.Info("Creating database version 1..")
|
||||
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS poo_records (
|
||||
timestamp TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
latitude REAL NOT NULL,
|
||||
longitude REAL NOT NULL,
|
||||
PRIMARY KEY (timestamp))`)
|
||||
if err != nil {
|
||||
slog.Error(fmt.Sprintln("PooRecorderInit Error creating table", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
_, err = db.Exec(`PRAGMA user_version = 1`)
|
||||
if err != nil {
|
||||
slog.Error(fmt.Sprintln("PooRecorderInit Error setting user version to 1", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
*userVersion = 1
|
||||
}
|
||||
|
||||
func initScheduler(mainScheduler *gocron.Scheduler) {
|
||||
scheduler = mainScheduler
|
||||
_, err := (*scheduler).NewJob(gocron.CronJob("0 5 * * *", false), gocron.NewTask(
|
||||
per,
|
||||
))
|
||||
if err != nil {
|
||||
slog.Error(fmt.Sprintln("PooRecorderInit Error creating scheduled task", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func per() {
|
||||
slog.Info("PooRecorderInit Running scheduled task ")
|
||||
}
|
||||
|
||||
func storeStatus(record recordDetail, timestamp time.Time) error {
|
||||
tableId := viper.GetString("pooRecorder.tableId")
|
||||
recordDate := timestamp.Format("2006-01-02")
|
||||
recordTime := timestamp.Format("15:04")
|
||||
slog.Debug(fmt.Sprintln("Recording poo", record.Status, "at", record.Latitude, record.Longitude))
|
||||
_, err := db.Exec(`INSERT OR IGNORE INTO poo_records (timestamp, status, latitude, longitude) VALUES (?, ?, ?, ?)`,
|
||||
timestamp.UTC().Format("2006-01-02T15:04Z07:00"), record.Status, record.Latitude, record.Longitude)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
header, err := notion.GetTableRows(tableId, 1, "")
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("HandleRecordPoo Failed to get table header", err))
|
||||
return
|
||||
}
|
||||
if len(header) == 0 {
|
||||
slog.Warn("HandleRecordPoo Table header not found")
|
||||
return
|
||||
}
|
||||
headerId := header[0].GetID()
|
||||
err = notion.WriteTableRow([]string{recordDate, recordTime, record.Status, record.Latitude + "," + record.Longitude}, tableId, headerId.String())
|
||||
if err != nil {
|
||||
slog.Warn(fmt.Sprintln("HandleRecordPoo Failed to write table row", err))
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user