From 41d558488e345fb7a9329a5dae0838de651f41f5 Mon Sep 17 00:00:00 2001 From: Dustin Pianalto Date: Fri, 25 Mar 2022 13:21:44 -0800 Subject: [PATCH] Working Server --- .github/workflows/main.yml | 60 +++++++++++ Dockerfile | 20 ++++ LICENSE | 2 +- README.md | 3 +- ambient.go | 147 +++++++++++++++++++++++++ cmd/weather/main.go | 36 +++++++ deployment.yml | 125 +++++++++++++++++++++ go.mod | 16 +++ go.sum | 18 ++++ internal/mqtt/connect.go | 46 ++++++++ internal/mqtt/event.go | 70 ++++++++++++ internal/postgres/database.go | 31 ++++++ internal/postgres/event.go | 173 ++++++++++++++++++++++++++++++ internal/postgres/sql/initial.sql | 136 +++++++++++++++++++++++ pkg/endpoints/ambient.go | 46 ++++++++ pkg/services/services.go | 48 +++++++++ 16 files changed, 975 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 Dockerfile create mode 100644 ambient.go create mode 100644 cmd/weather/main.go create mode 100644 deployment.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/mqtt/connect.go create mode 100644 internal/mqtt/event.go create mode 100644 internal/postgres/database.go create mode 100644 internal/postgres/event.go create mode 100644 internal/postgres/sql/initial.sql create mode 100644 pkg/endpoints/ambient.go create mode 100644 pkg/services/services.go diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..c334d5c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,60 @@ +name: CI + +# Controls when the action will run. Triggers the workflow on push to master or development +# with a tag like v1.0.0 or v1.0.0-dev +on: + push: + tags: + - v[0-9]+.[0-9]+.[0-9]+ + - v[0-9]+.[0-9]+.[0-9]+-[a-zA-Z]+ + +jobs: + build: + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Get Version + id: get_version + uses: battila7/get-version-action@v2.0.0 + + - name: install buildx + id: buildx + uses: crazy-max/ghaction-docker-buildx@v1 + with: + version: latest + + - name: Docker Login + # You may pin to the exact commit or the version. + # uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + uses: docker/login-action@v1.10.0 + with: + registry: ${{ secrets.DR_URL }} + # Username used to log against the Docker registry + username: ${{ secrets.DH_USERNAME }} + # Password or personal access token used to log against the Docker registry + password: ${{ secrets.DH_PASSWORD }} + # Log out from the Docker registry at the end of a job + logout: true + + - name: Docker Build & Push + env: + IMAGE_TAG: ${{ steps.get_version.outputs.version-without-v }} + run: | + docker buildx build --push \ + --tag ${{ secrets.DR_URL }}/weather:$IMAGE_TAG \ + --platform linux/amd64,linux/arm/v7,linux/arm64 . + - name: Update deployment file + run: TAG=${{ steps.get_version.outputs.version-without-v }} && sed -i 's||${{ secrets.DR_URL }}/weather:'${TAG}'|' $GITHUB_WORKSPACE/deployment.yml + + - uses: azure/k8s-set-context@v1 + with: + method: kubeconfig + kubeconfig: ${{ secrets.KUBE_CONFIG }} + id: setcontext + + - name: Deploy to Kubernetes + run: kubectl apply -f $GITHUB_WORKSPACE/deployment.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d4e087d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.17-alpine as dev + +WORKDIR /go/src/weather +COPY ./go.mod . +COPY ./go.sum . + +RUN go mod download + +COPY . . +RUN go install ./... + +CMD [ "go", "run", "cmd/weather/main.go"] + +from alpine + +WORKDIR /bin + +COPY --from=dev /go/bin/weather ./weather + +CMD [ "weather" ] diff --git a/LICENSE b/LICENSE index c4a876d..a15eb3f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Dusty.P +Copyright (c) 2022 Dusty.P Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6a453ae..8709a5b 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ -# weather \ No newline at end of file +# weather +Golang server to bridge common Weather stations to MQTT and a database diff --git a/ambient.go b/ambient.go new file mode 100644 index 0000000..d56aae8 --- /dev/null +++ b/ambient.go @@ -0,0 +1,147 @@ +package weather + +import "time" + +type AmbientService interface { + Event(uint64) (*AmbientEntry, error) + AddEvent(*AmbientEntry) (*AmbientEntry, error) + UpdateEvent(*AmbientEntry) error + DeleteEvent(*AmbientEntry) error +} + +type AmbientEntry struct { + ID uint64 + MAC string `schema:"MAC"` + DateUTC time.Time `schema:"dateutc"` + WindDir int `schema:"winddir"` + WindSpeedMPH float32 `schema:"windspeedmph"` + WindGustMPH float32 `schema:"windgustmph"` + WindGustDir int `schema:"windgustdir"` + MaxDailyGust float32 `schema:"maxdailygust"` + WindSpdMPH_Avg2m float32 `schema:"windspdmph_avg2m"` + WindDir_Avg2m int `schema:"winddir_avg2m"` + WindSpdMPH_Avg10m float32 `schema:"windspdmph_avg10m"` + WindDir_Avg10m int `schema:"winddir_avg10m"` + WindGustMPH_Interval int `schema:"windgustmph_interval"` + Humidity int `schema:"humidity"` + Humidity1 int `schema:"humidity1"` + Humidity2 int `schema:"humidity2"` + Humidity3 int `schema:"humidity3"` + Humidity4 int `schema:"humidity4"` + Humidity5 int `schema:"humidity5"` + Humidity6 int `schema:"humidity6"` + Humidity7 int `schema:"humidity7"` + Humidity8 int `schema:"humidity8"` + Humidity9 int `schema:"humidity9"` + Humidity10 int `schema:"humidity10"` + HumidityIn int `schema:"humidityin"` + TempF float32 `schema:"tempf"` + Temp1F float32 `schema:"temp1f"` + Temp2F float32 `schema:"temp2f"` + Temp3F float32 `schema:"temp3f"` + Temp4F float32 `schema:"temp4f"` + Temp5F float32 `schema:"temp5f"` + Temp6F float32 `schema:"temp6f"` + Temp7F float32 `schema:"temp7f"` + Temp8F float32 `schema:"temp8f"` + Temp9F float32 `schema:"temp9f"` + Temp10F float32 `schema:"temp10f"` + TempInF float32 `schema:"tempinf"` + HourlyRainIn float32 `schema:"hourlyrainin"` + DailyRainIn float32 `schema:"dailyrainin"` + Last24HourRainIn float32 `schema:"24hourrainin"` + WeeklyRainIn float32 `schema:"weeklyrainin"` + MonthlyRainIn float32 `schema:"monthlyrainin"` + YearlyRainIn float32 `schema:"yearlyrainin"` + EventRainIn float32 `schema:"eventrainin"` + TotalRainIn float32 `schema:"totalrainin"` + BaromRelIn float32 `schema:"baromrelin"` + BaromAbsIn float32 `schema:"baromabsin"` + UV int `schema:"uv"` + SolarRadiation float32 `schema:"solarradiation"` + CO2 int `schema:"co2"` + PM25 int `schema:"pm25"` + PM25_24H float32 `schema:"pm25_24h"` + PM25_In int `schema:"pm25_in"` + PM25_In_24H float32 `schema:"pm25_in_24h"` + PM10_In int `schema:"pm10_in"` + PM10_In_24H float32 `schema:"pm10_in_24h"` + CO2_In int `schema:"co2_in"` + CO2_In_24H float32 `schema:"co2_in_24h"` + PM_In_Temp float32 `schema:"pm_in_temp"` + PM_in_Humidity int `schema:"pm_in_humidity"` + Relay1 bool `schema:"relay1"` + Relay2 bool `schema:"relay2"` + Relay3 bool `schema:"relay3"` + Relay4 bool `schema:"relay4"` + Relay5 bool `schema:"relay5"` + Relay6 bool `schema:"relay6"` + Relay7 bool `schema:"relay7"` + Relay8 bool `schema:"relay8"` + Relay9 bool `schema:"relay9"` + Relay10 bool `schema:"relay10"` + SoilTemp1 float32 `schema:"soiltemp1"` + SoilTemp2 float32 `schema:"soiltemp2"` + SoilTemp3 float32 `schema:"soiltemp3"` + SoilTemp4 float32 `schema:"soiltemp4"` + SoilTemp5 float32 `schema:"soiltemp5"` + SoilTemp6 float32 `schema:"soiltemp6"` + SoilTemp7 float32 `schema:"soiltemp7"` + SoilTemp8 float32 `schema:"soiltemp8"` + SoilTemp9 float32 `schema:"soiltemp9"` + SoilTemp10 float32 `schema:"soiltemp10"` + SoilHum1 int `schema:"soilhum1"` + SoilHum2 int `schema:"soilhum2"` + SoilHum3 int `schema:"soilhum3"` + SoilHum4 int `schema:"soilhum4"` + SoilHum5 int `schema:"soilhum5"` + SoilHum6 int `schema:"soilhum6"` + SoilHum7 int `schema:"soilhum7"` + SoilHum8 int `schema:"soilhum8"` + SoilHum9 int `schema:"soilhum9"` + SoilHum10 int `schema:"soilhum10"` + Leak1 bool `schema:"leak1"` + Leak2 bool `schema:"leak2"` + Leak3 bool `schema:"leak3"` + Leak4 bool `schema:"leak4"` + Lightning_Time time.Time `schema:"lightning_time"` + Lightning_Day int `schema:"lightning_day"` + Lightning_Distance float32 `schema:"lightning_distance"` + BattOut bool `schema:"battout"` + BattIn bool `schema:"battin"` + Batt1 bool `schema:"batt1"` + Batt2 bool `schema:"batt2"` + Batt3 bool `schema:"batt3"` + Batt4 bool `schema:"batt4"` + Batt5 bool `schema:"batt5"` + Batt6 bool `schema:"batt6"` + Batt7 bool `schema:"batt7"` + Batt8 bool `schema:"batt8"` + Batt9 bool `schema:"batt9"` + Batt10 bool `schema:"batt10"` + BattR1 bool `schema:"battr1"` + BattR2 bool `schema:"battr2"` + BattR3 bool `schema:"battr3"` + BattR4 bool `schema:"battr4"` + BattR5 bool `schema:"battr5"` + BattR6 bool `schema:"battr6"` + BattR7 bool `schema:"battr7"` + BattR8 bool `schema:"battr8"` + BattR9 bool `schema:"battr9"` + BattR10 bool `schema:"battr10"` + Batt_25 bool `schema:"batt_25"` + Batt_25In bool `schema:"batt_25in"` + BatLeak1 bool `schema:"batleak1"` + BatLeak2 bool `schema:"batleak2"` + BatLeak3 bool `schema:"batleak3"` + BatLeak4 bool `schema:"batleak4"` + Batt_Lightning bool `schema:"batt_lightning"` + BattSM1 bool `schema:"battsm1"` + BattSM2 bool `schema:"battsm2"` + BattSM3 bool `schema:"battsm3"` + BattSM4 bool `schema:"battsm4"` + BattRain bool `schema:"battrain"` + BattCO2 bool `schema:"batt_co2"` + StationType string `schema:"stationtype"` + PASSKEY string `schema:"passkey"` +} diff --git a/cmd/weather/main.go b/cmd/weather/main.go new file mode 100644 index 0000000..a20277e --- /dev/null +++ b/cmd/weather/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "log" + "net/http" + "os" + "strconv" + + "github.com/gorilla/mux" + + "github.com/dustinpianalto/weather/internal/mqtt" + "github.com/dustinpianalto/weather/internal/postgres" + "github.com/dustinpianalto/weather/pkg/endpoints" + "github.com/dustinpianalto/weather/pkg/services" +) + +func main() { + router := mux.NewRouter() + router.HandleFunc("/data/report", endpoints.AmbientHandler) + router.HandleFunc("/data/report/", endpoints.AmbientHandler) + router.HandleFunc("/", endpoints.AmbientHandler) + + dbConnString := os.Getenv("DATABASE_URL") + postgres.ConnectDatabase(dbConnString) + host := os.Getenv("MQTT_HOST") + port, err := strconv.Atoi(os.Getenv("MQTT_PORT")) + if err != nil { + log.Fatal(err) + } + user := os.Getenv("MQTT_USER") + pass := os.Getenv("MQTT_PASSWORD") + mqtt.CreateMQTTClient(host, user, pass, port) + services.InitServices() + + log.Fatal(http.ListenAndServe(":8080", router)) +} diff --git a/deployment.yml b/deployment.yml new file mode 100644 index 0000000..d528f00 --- /dev/null +++ b/deployment.yml @@ -0,0 +1,125 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: weather + namespace: weather + labels: + app: weather +spec: + replicas: 1 + selector: + matchLabels: + app: weather + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + minReadySeconds: 120 + template: + metadata: + labels: + app: weather + spec: + containers: + - name: pgbouncer + image: timoha/pgbouncer:1.15.0 + resources: + requests: + memory: "256Mi" + cpu: "0.5" + limits: + memory: "512Mi" + cpu: "1" + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: weather + key: pgbouncer_url + - name: SERVER_TLS_SSLMODE + valueFrom: + secretKeyRef: + name: weather + key: pgbouncer_ssl + - name: AUTH_TYPE + valueFrom: + secretKeyRef: + name: weather + key: pgbouncer_auth + ports: + - containerPort: 5432 + - name: weather + image: + resources: + requests: + memory: "256Mi" + cpu: "0.5" + limits: + memory: "512Mi" + cpu: "1" + ports: + - containerPort: 8080 + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: weather + key: database_url + - name: MQTT_HOST + valueFrom: + secretKeyRef: + name: weather + key: mqtt_host + - name: MQTT_PASSWORD + valueFrom: + secretKeyRef: + name: weather + key: mqtt_password + - name: MQTT_USER + valueFrom: + secretKeyRef: + name: weather + key: mqtt_user + - name: MQTT_PORT + valueFrom: + secretKeyRef: + name: weather + key: mqtt_port + imagePullSecrets: + - name: registry-1 +--- +apiVersion: v1 +kind: Service +metadata: + name: weather-service + namespace: weather + labels: + app: weather +spec: + selector: + app: weather + ports: + - protocol: TCP + name: http + port: 8080 + targetPort: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: weather + namespace: weather +spec: + rules: + - host: weather.djpianalto.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: weather-service + port: + number: 8080 + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ceda837 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/dustinpianalto/weather + +go 1.18 + +require ( + github.com/dustinpianalto/errors v0.1.0 + github.com/eclipse/paho.mqtt.golang v1.3.5 + github.com/gorilla/mux v1.8.0 + github.com/gorilla/schema v1.2.0 + github.com/lib/pq v1.10.4 +) + +require ( + github.com/gorilla/websocket v1.4.2 // indirect + golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c244e84 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/dustinpianalto/errors v0.1.0 h1:1wG0dhtE1w0u3+QRrQnR6vRO3QrVGpqIEP/0X83i4f4= +github.com/dustinpianalto/errors v0.1.0/go.mod h1:Fo865gGhrM1eyVIp5H5U8kQkZtFc/daiU3QBpUCd+B4= +github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y= +github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/mqtt/connect.go b/internal/mqtt/connect.go new file mode 100644 index 0000000..8cb0895 --- /dev/null +++ b/internal/mqtt/connect.go @@ -0,0 +1,46 @@ +package mqtt + +import ( + "fmt" + "log" + + "github.com/dustinpianalto/errors" + "github.com/dustinpianalto/weather" + mqtt "github.com/eclipse/paho.mqtt.golang" +) + +var ( + AmbientService weather.AmbientService +) + +func CreateMQTTClient(host, username, password string, port int) { + opts := mqtt.NewClientOptions() + opts.AddBroker(fmt.Sprintf("tcp://%s:%d", host, port)) + opts.SetClientID("weather_bridge") + opts.SetUsername(username) + opts.SetPassword(password) + opts.OnConnect = connectHandler + opts.OnConnectionLost = connectionLostHandler + client := mqtt.NewClient(opts) + log.Println("mqtt client created") + token := client.Connect() + if token.Wait() && token.Error() != nil { + log.Println(token.Error()) + return + } + initServices(&client) + log.Println("mqtt client initalized") +} + +func connectHandler(client mqtt.Client) { + log.Println("MQTT Connected") +} + +func connectionLostHandler(client mqtt.Client, err error) { + const method string = "mqtt/connectionLostHandler" + log.Println(errors.E(method, errors.Internal, "MQTT Connection Lost", err)) +} + +func initServices(client *mqtt.Client) { + AmbientService = eventService{client: client} +} diff --git a/internal/mqtt/event.go b/internal/mqtt/event.go new file mode 100644 index 0000000..f9d3915 --- /dev/null +++ b/internal/mqtt/event.go @@ -0,0 +1,70 @@ +package mqtt + +import ( + "fmt" + "reflect" + "strconv" + "time" + + "github.com/dustinpianalto/weather" + mqtt "github.com/eclipse/paho.mqtt.golang" +) + +type eventService struct { + client *mqtt.Client +} + +func (e eventService) Event(i uint64) (*weather.AmbientEntry, error) { + return nil, nil +} + +func (e eventService) AddEvent(event *weather.AmbientEntry) (*weather.AmbientEntry, error) { + m := eventToMap(event) + for k, v := range m { + (*e.client).Publish(fmt.Sprintf("weather/WS-2920C/%s", k), 0, false, v) + } + return event, nil +} + +func (e eventService) UpdateEvent(event *weather.AmbientEntry) error { + return nil +} + +func (e eventService) DeleteEvent(event *weather.AmbientEntry) error { + return nil +} + +func eventToMap(event *weather.AmbientEntry) map[string]string { + m := make(map[string]string) + iVal := reflect.ValueOf(event).Elem() + typ := iVal.Type() + for i := 0; i < iVal.NumField(); i++ { + f := iVal.Field(i) + var v string + switch f.Interface().(type) { + case int, int8, int16, int32, int64: + v = strconv.FormatInt(f.Int(), 10) + case uint, uint8, uint16, uint32, uint64: + v = strconv.FormatUint(f.Uint(), 10) + case float32: + v = strconv.FormatFloat(f.Float(), 'f', 4, 32) + case float64: + v = strconv.FormatFloat(f.Float(), 'f', 4, 64) + case []byte: + v = string(f.Bytes()) + case string: + v = f.String() + case bool: + if f.Bool() { + v = "true" + } else { + v = "false" + } + case time.Time: + v = strconv.FormatInt(f.Interface().(time.Time).Unix(), 10) + } + m[typ.Field(i).Name] = v + + } + return m +} diff --git a/internal/postgres/database.go b/internal/postgres/database.go new file mode 100644 index 0000000..ef97698 --- /dev/null +++ b/internal/postgres/database.go @@ -0,0 +1,31 @@ +package postgres + +import ( + "database/sql" + "fmt" + "log" + + "github.com/dustinpianalto/weather" + _ "github.com/lib/pq" +) + +var ( + EventService weather.AmbientService +) + +func ConnectDatabase(dbConnString string) { + db, err := sql.Open("postgres", dbConnString) + if err != nil { + panic(fmt.Sprintf("Can't connect to the database. %v", err)) + } + db.SetMaxOpenConns(75) // The RDS instance has a max of 75 open connections + db.SetMaxIdleConns(5) + db.SetConnMaxLifetime(300) + log.Println("Database Connected") + initServices(db) + log.Println("Postgres Database Initialized") +} + +func initServices(db *sql.DB) { + EventService = eventService{db: db} +} diff --git a/internal/postgres/event.go b/internal/postgres/event.go new file mode 100644 index 0000000..323a02a --- /dev/null +++ b/internal/postgres/event.go @@ -0,0 +1,173 @@ +package postgres + +import ( + "database/sql" + + "github.com/dustinpianalto/weather" +) + +type eventService struct { + db *sql.DB +} + +func (e eventService) Event(i uint64) (*weather.AmbientEntry, error) { + queryString := `SELECT + mac, dateutc, winddir, windspeedmph, windgustmph, windgustdir, maxdailygust, + windspdmph_avg2m, winddir_avg2m, windspdmph_avg10m, winddir_avg10m, windgustmph_interval, + humidity, humidity1, humidity2, humidity3, humidity4, humidity5, humidity6, humidity7, + humidity8, humidity9, humidity10, humidityin, tempf, temp1f, temp2f, temp3f, temp4f, temp5f, + temp6f, temp7f, temp8f, temp9f, temp10f, tempinf, hourlyrainin, dailyrainin, last24hourrainin, + weeklyrainin, monthlyrainin, yearlyrainin, eventrainin, totalrainin, baromrelin, baromabsin, + uv, solarradiation, co2, pm25, pm25_24h, pm25_in, pm25_in_24h, pm10_in, pm10_in_24h, co2_in, + co2_in_24h, pm_in_temp, pm_in_humidity, relay1, relay2, relay3, relay4, relay5, relay6, + relay7, relay8, relay9, relay10, soiltemp1, soiltemp2, soiltemp3, soiltemp4, soiltemp5, + soiltemp6, soiltemp7, soiltemp8, soiltemp9, soiltemp10, soilhum1, soilhum2, soilhum3, soilhum4, + soilhum5, soilhum6, soilhum7, soilhum8, soilhum9, soilhum10, leak1, leak2, leak3, leak4, + lightning_time, lightning_day, lightning_distance, battout, battin, batt1, batt2, batt3, batt4, + batt5, batt6, batt7, batt8, batt9, batt10, battr1, battr2, battr3, battr4, battr5, battr6, + battr7, battr8, battr9, battr10, batt_25, batt_25in, batleak1, batleak2, batleak3, batleak4, + batt_lightning, battsm1, battsm2, battsm3, battsm4, battrain, batt_co2, stationtype, passkey + FROM events WHERE id = $1` + event := weather.AmbientEntry{ + ID: i, + } + err := e.db.QueryRow(queryString, i).Scan( + &event.MAC, &event.DateUTC, &event.WindDir, &event.WindSpeedMPH, &event.WindGustMPH, + &event.WindGustDir, &event.MaxDailyGust, &event.WindSpdMPH_Avg2m, &event.WindDir_Avg2m, + &event.WindSpdMPH_Avg10m, &event.WindDir_Avg10m, &event.WindGustMPH_Interval, &event.Humidity, + &event.Humidity1, &event.Humidity2, &event.Humidity3, &event.Humidity4, &event.Humidity5, + &event.Humidity6, &event.Humidity7, &event.Humidity8, &event.Humidity9, &event.Humidity10, + &event.HumidityIn, &event.TempF, &event.Temp1F, &event.Temp2F, &event.Temp3F, &event.Temp4F, + &event.Temp5F, &event.Temp6F, &event.Temp7F, &event.Temp8F, &event.Temp9F, &event.Temp10F, + &event.TempInF, &event.HourlyRainIn, &event.DailyRainIn, &event.Last24HourRainIn, + &event.WeeklyRainIn, &event.MonthlyRainIn, &event.YearlyRainIn, &event.EventRainIn, + &event.TotalRainIn, &event.BaromRelIn, &event.BaromAbsIn, &event.UV, &event.SolarRadiation, + &event.CO2, &event.PM25, &event.PM25_24H, &event.PM25_In, &event.PM25_In_24H, &event.PM10_In, + &event.PM10_In_24H, &event.CO2_In, &event.CO2_In_24H, &event.PM_In_Temp, &event.PM_in_Humidity, + &event.Relay1, &event.Relay2, &event.Relay3, &event.Relay4, &event.Relay5, &event.Relay6, + &event.Relay7, &event.Relay8, &event.Relay9, &event.Relay10, &event.SoilTemp1, &event.SoilTemp2, + &event.SoilTemp3, &event.SoilTemp4, &event.SoilTemp5, &event.SoilTemp6, &event.SoilTemp7, + &event.SoilTemp8, &event.SoilTemp9, &event.SoilTemp10, &event.SoilHum1, &event.SoilHum2, + &event.SoilHum3, &event.SoilHum4, &event.SoilHum5, &event.SoilHum6, &event.SoilHum7, + &event.SoilHum8, &event.SoilHum9, &event.SoilHum10, &event.Leak1, &event.Leak2, &event.Leak3, + &event.Leak4, &event.Lightning_Time, &event.Lightning_Day, &event.Lightning_Distance, + &event.BattOut, &event.BattIn, &event.Batt1, &event.Batt2, &event.Batt3, &event.Batt4, + &event.Batt5, &event.Batt6, &event.Batt7, &event.Batt8, &event.Batt9, &event.Batt10, + &event.BattR1, &event.BattR2, &event.BattR3, &event.BattR4, &event.BattR5, &event.BattR6, + &event.BattR7, &event.BattR8, &event.BattR9, &event.BattR10, &event.Batt_25, &event.Batt_25In, + &event.BatLeak1, &event.BatLeak2, &event.BatLeak3, &event.BatLeak4, &event.Batt_Lightning, + &event.BattSM1, &event.BattSM2, &event.BattSM3, &event.BattSM4, &event.BattRain, &event.BattCO2, + &event.StationType, &event.PASSKEY) + if err != nil { + return nil, err + } + return &event, nil +} + +func (e eventService) AddEvent(event *weather.AmbientEntry) (*weather.AmbientEntry, error) { + queryString := `INSERT INTO events + (mac, dateutc, winddir, windspeedmph, windgustmph, windgustdir, maxdailygust, + windspdmph_avg2m, winddir_avg2m, windspdmph_avg10m, winddir_avg10m, windgustmph_interval, + humidity, humidity1, humidity2, humidity3, humidity4, humidity5, humidity6, humidity7, + humidity8, humidity9, humidity10, humidityin, tempf, temp1f, temp2f, temp3f, temp4f, + temp5f, temp6f, temp7f, temp8f, temp9f, temp10f, tempinf, hourlyrainin, dailyrainin, + last24hourrainin, weeklyrainin, monthlyrainin, yearlyrainin, eventrainin, totalrainin, baromrelin, + baromabsin, uv, solarradiation, co2, pm25, pm25_24h, pm25_in, pm25_in_24h, pm10_in, pm10_in_24h, + co2_in, co2_in_24h, pm_in_temp, pm_in_humidity, relay1, relay2, relay3, relay4, relay5, relay6, + relay7, relay8, relay9, relay10, soiltemp1, soiltemp2, soiltemp3, soiltemp4, soiltemp5, soiltemp6, + soiltemp7, soiltemp8, soiltemp9, soiltemp10, soilhum1, soilhum2, soilhum3, soilhum4, soilhum5, + soilhum6, soilhum7, soilhum8, soilhum9, soilhum10, leak1, leak2, leak3, leak4, lightning_time, + lightning_day, lightning_distance, battout, battin, batt1, batt2, batt3, batt4, batt5, batt6, + batt7, batt8, batt9, batt10, battr1, battr2, battr3, battr4, battr5, battr6, battr7, battr8, + battr9, battr10, batt_25, batt_25in, batleak1, batleak2, batleak3, batleak4, batt_lightning, + battsm1, battsm2, battsm3, battsm4, battrain, batt_co2, stationtype, passkey) + VALUES + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, + $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, + $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53, $54, $55, $56, $57, $58, $59, + $60, $61, $62, $63, $64, $65, $66, $67, $68, $69, $70, $71, $72, $73, $74, $75, $76, $77, $78, + $79, $80, $81, $82, $83, $84, $85, $86, $87, $88, $89, $90, $91, $92, $93, $94, $95, $96, $97, + $98, $99, $100, $101, $102, $103, $104, $105, $106, $107, $108, $109, $110, $111, $112, $113, + $114, $115, $116, $117, $118, $119, $120, $121, $122, $123, $124, $125, $126, $127, $128, $129, + $130, $131, $132, $133) + RETURNING id` + err := e.db.QueryRow(queryString, event.MAC, event.DateUTC, event.WindDir, event.WindSpeedMPH, + event.WindGustMPH, event.WindGustDir, event.MaxDailyGust, event.WindSpdMPH_Avg2m, event.WindDir_Avg2m, + event.WindSpdMPH_Avg10m, event.WindDir_Avg10m, event.WindGustMPH_Interval, event.Humidity, + event.Humidity1, event.Humidity2, event.Humidity3, event.Humidity4, event.Humidity5, event.Humidity6, + event.Humidity7, event.Humidity8, event.Humidity9, event.Humidity10, event.HumidityIn, event.TempF, + event.Temp1F, event.Temp2F, event.Temp3F, event.Temp4F, event.Temp5F, event.Temp6F, event.Temp7F, + event.Temp8F, event.Temp9F, event.Temp10F, event.TempInF, event.HourlyRainIn, event.DailyRainIn, + event.Last24HourRainIn, event.WeeklyRainIn, event.MonthlyRainIn, event.YearlyRainIn, event.EventRainIn, + event.TotalRainIn, event.BaromRelIn, event.BaromAbsIn, event.UV, event.SolarRadiation, event.CO2, + event.PM25, event.PM25_24H, event.PM25_In, event.PM25_In_24H, event.PM10_In, event.PM10_In_24H, + event.CO2_In, event.CO2_In_24H, event.PM_In_Temp, event.PM_in_Humidity, event.Relay1, event.Relay2, + event.Relay3, event.Relay4, event.Relay5, event.Relay6, event.Relay7, event.Relay8, event.Relay9, + event.Relay10, event.SoilTemp1, event.SoilTemp2, event.SoilTemp3, event.SoilTemp4, event.SoilTemp5, + event.SoilTemp6, event.SoilTemp7, event.SoilTemp8, event.SoilTemp9, event.SoilTemp10, event.SoilHum1, + event.SoilHum2, event.SoilHum3, event.SoilHum4, event.SoilHum5, event.SoilHum6, event.SoilHum7, + event.SoilHum8, event.SoilHum9, event.SoilHum10, event.Leak1, event.Leak2, event.Leak3, event.Leak4, + event.Lightning_Time, event.Lightning_Day, event.Lightning_Distance, event.BattOut, event.BattIn, + event.Batt1, event.Batt2, event.Batt3, event.Batt4, event.Batt5, event.Batt6, event.Batt7, event.Batt8, + event.Batt9, event.Batt10, event.BattR1, event.BattR2, event.BattR3, event.BattR4, event.BattR5, + event.BattR6, event.BattR7, event.BattR8, event.BattR9, event.BattR10, event.Batt_25, event.Batt_25In, + event.BatLeak1, event.BatLeak2, event.BatLeak3, event.BatLeak4, event.Batt_Lightning, event.BattSM1, + event.BattSM2, event.BattSM3, event.BattSM4, event.BattRain, event.BattCO2, event.StationType, + event.PASSKEY).Scan(&event.ID) + return event, err +} + +func (e eventService) UpdateEvent(event *weather.AmbientEntry) error { + queryString := `UPDATE events SET + mac = $2, dateutc = $3, winddir = $4, windspeedmph = $5, windgustmph = $6, windgustdir = $7, + maxdailygust = $8, windspdmph_avg2m = $9, winddir_avg2m = $10, windspdmph_avg10m = $11, winddir_avg10m = $12, + windgustmph_interval = $13, humidity = $14, humidity1 = $15, humidity2 = $16, humidity3 = $17, humidity4 = $18, + humidity5 = $19, humidity6 = $20, humidity7 = $21, humidity8 = $22, humidity9 = $23, humidity10 = $24, + humidityin = $25, tempf = $26, temp1f = $27, temp2f = $28, temp3f = $29, temp4f = $30, temp5f = $31, + temp6f = $32, temp7f = $33, temp8f = $34, temp9f = $35, temp10f = $36, tempinf = $37, hourlyrainin = $38, + dailyrainin = $39, last24hourrainin = $40, weeklyrainin = $41, monthlyrainin = $42, yearlyrainin = $43, + eventrainin = $44, totalrainin = $45, baromrelin = $46, baromabsin = $47, uv = $48, solarradiation = $49, + co2 = $50, pm25 = $51, pm25_24h = $52, pm25_in = $53, pm25_in_24h = $54, pm10_in = $55, pm10_in_24h = $56, + co2_in = $57, co2_in_24h = $58, pm_in_temp = $59, pm_in_humidity = $60, relay1 = $61, relay2 = $62, + relay3 = $63, relay4 = $64, relay5 = $65, relay6 = $66, relay7 = $67, relay8 = $68, relay9 = $69, + relay10 = $70, soiltemp1 = $71, soiltemp2 = $72, soiltemp3 = $73, soiltemp4 = $74, soiltemp5 = $75, + soiltemp6 = $76, soiltemp7 = $77, soiltemp8 = $78, soiltemp9 = $79, soiltemp10 = $80, soilhum1 = $81, + soilhum2 = $82, soilhum3 = $83, soilhum4 = $84, soilhum5 = $85, soilhum6 = $86, soilhum7 = $87, + soilhum8 = $88, soilhum9 = $89, soilhum10 = $90, leak1 = $91, leak2 = $92, leak3 = $93, leak4 = $94, + lightning_time = $95, lightning_day = $96, lightning_distance = $97, battout = $98, battin = $99, + batt1 = $100, batt2 = $101, batt3 = $102, batt4 = $103, batt5 = $104, batt6 = $105, batt7 = $106, + batt8 = $107, batt9 = $108, batt10 = $109, battr1 = $110, battr2 = $111, battr3 = $112, battr4 = $113, + battr5 = $114, battr6 = $115, battr7 = $116, battr8 = $117, battr9 = $118, battr10 = $119, batt_25 = $120, + batt_25in = $121, batleak1 = $122, batleak2 = $123, batleak3 = $124, batleak4 = $125, batt_lightning = $126, + battsm1 = $127, battsm2 = $128, battsm3 = $129, battsm4 = $130, battrain = $131, batt_co2 = $132, + stationtype = $133, passkey = $134, WHERE id = $1` + _, err := e.db.Exec(queryString, event.ID, event.MAC, event.DateUTC, event.WindDir, event.WindSpeedMPH, + event.WindGustMPH, event.WindGustDir, event.MaxDailyGust, event.WindSpdMPH_Avg2m, event.WindDir_Avg2m, + event.WindSpdMPH_Avg10m, event.WindDir_Avg10m, event.WindGustMPH_Interval, event.Humidity, event.Humidity1, + event.Humidity2, event.Humidity3, event.Humidity4, event.Humidity5, event.Humidity6, event.Humidity7, + event.Humidity8, event.Humidity9, event.Humidity10, event.HumidityIn, event.TempF, event.Temp1F, + event.Temp2F, event.Temp3F, event.Temp4F, event.Temp5F, event.Temp6F, event.Temp7F, event.Temp8F, + event.Temp9F, event.Temp10F, event.TempInF, event.HourlyRainIn, event.DailyRainIn, event.Last24HourRainIn, + event.WeeklyRainIn, event.MonthlyRainIn, event.YearlyRainIn, event.EventRainIn, event.TotalRainIn, + event.BaromRelIn, event.BaromAbsIn, event.UV, event.SolarRadiation, event.CO2, event.PM25, event.PM25_24H, + event.PM25_In, event.PM25_In_24H, event.PM10_In, event.PM10_In_24H, event.CO2_In, event.CO2_In_24H, + event.PM_In_Temp, event.PM_in_Humidity, event.Relay1, event.Relay2, event.Relay3, event.Relay4, event.Relay5, + event.Relay6, event.Relay7, event.Relay8, event.Relay9, event.Relay10, event.SoilTemp1, event.SoilTemp2, + event.SoilTemp3, event.SoilTemp4, event.SoilTemp5, event.SoilTemp6, event.SoilTemp7, event.SoilTemp8, + event.SoilTemp9, event.SoilTemp10, event.SoilHum1, event.SoilHum2, event.SoilHum3, event.SoilHum4, + event.SoilHum5, event.SoilHum6, event.SoilHum7, event.SoilHum8, event.SoilHum9, event.SoilHum10, + event.Leak1, event.Leak2, event.Leak3, event.Leak4, event.Lightning_Time, event.Lightning_Day, + event.Lightning_Distance, event.BattOut, event.BattIn, event.Batt1, event.Batt2, event.Batt3, event.Batt4, + event.Batt5, event.Batt6, event.Batt7, event.Batt8, event.Batt9, event.Batt10, event.BattR1, event.BattR2, + event.BattR3, event.BattR4, event.BattR5, event.BattR6, event.BattR7, event.BattR8, event.BattR9, + event.BattR10, event.Batt_25, event.Batt_25In, event.BatLeak1, event.BatLeak2, event.BatLeak3, event.BatLeak4, + event.Batt_Lightning, event.BattSM1, event.BattSM2, event.BattSM3, event.BattSM4, event.BattRain, + event.BattCO2, event.StationType, event.PASSKEY) + return err +} + +func (e eventService) DeleteEvent(event *weather.AmbientEntry) error { + queryString := "DELETE FROM events WHERE id = $1" + _, err := e.db.Exec(queryString, event.ID) + return err +} diff --git a/internal/postgres/sql/initial.sql b/internal/postgres/sql/initial.sql new file mode 100644 index 0000000..7eb9e27 --- /dev/null +++ b/internal/postgres/sql/initial.sql @@ -0,0 +1,136 @@ +CREATE TABLE IF NOT EXISTS events ( + id BIGSERIAL PRIMARY KEY, + mac char(17) not null, + dateutc timestamp not null, + winddir integer, + windspeedmph float, + windgustmph float, + windgustdir integer, + maxdailygust float, + windspdmph_avg2m float, + winddir_avg2m integer, + windspdmph_avg10m float, + winddir_avg10m integer, + windgustmph_interval integer, + humidity integer, + humidity1 integer, + humidity2 integer, + humidity3 integer, + humidity4 integer, + humidity5 integer, + humidity6 integer, + humidity7 integer, + humidity8 integer, + humidity9 integer, + humidity10 integer, + humidityin integer, + tempf float, + temp1f float, + temp2f float, + temp3f float, + temp4f float, + temp5f float, + temp6f float, + temp7f float, + temp8f float, + temp9f float, + temp10f float, + tempinf float, + hourlyrainin float, + dailyrainin float, + last24hourrainin float, + weeklyrainin float, + monthlyrainin float, + yearlyrainin float, + eventrainin float, + totalrainin float, + baromrelin float, + baromabsin float, + uv integer, + solarradiation float, + co2 integer, + pm25 integer, + pm25_24h float, + pm25_in integer, + pm25_in_24h float, + pm10_in integer, + pm10_in_24h float, + co2_in integer, + co2_in_24h float, + pm_in_temp float, + pm_in_humidity integer, + relay1 boolean, + relay2 boolean, + relay3 boolean, + relay4 boolean, + relay5 boolean, + relay6 boolean, + relay7 boolean, + relay8 boolean, + relay9 boolean, + relay10 boolean, + soiltemp1 float, + soiltemp2 float, + soiltemp3 float, + soiltemp4 float, + soiltemp5 float, + soiltemp6 float, + soiltemp7 float, + soiltemp8 float, + soiltemp9 float, + soiltemp10 float, + soilhum1 integer, + soilhum2 integer, + soilhum3 integer, + soilhum4 integer, + soilhum5 integer, + soilhum6 integer, + soilhum7 integer, + soilhum8 integer, + soilhum9 integer, + soilhum10 integer, + leak1 boolean, + leak2 boolean, + leak3 boolean, + leak4 boolean, + lightning_time timestamp, + lightning_day integer, + lightning_distance float, + battout boolean, + battin boolean, + batt1 boolean, + batt2 boolean, + batt3 boolean, + batt4 boolean, + batt5 boolean, + batt6 boolean, + batt7 boolean, + batt8 boolean, + batt9 boolean, + batt10 boolean, + battr1 boolean, + battr2 boolean, + battr3 boolean, + battr4 boolean, + battr5 boolean, + battr6 boolean, + battr7 boolean, + battr8 boolean, + battr9 boolean, + battr10 boolean, + batt_25 boolean, + batt_25in boolean, + batleak1 boolean, + batleak2 boolean, + batleak3 boolean, + batleak4 boolean, + batt_lightning boolean, + battsm1 boolean, + battsm2 boolean, + battsm3 boolean, + battsm4 boolean, + battrain boolean, + batt_co2 boolean, + stationtype varchar(30), + passkey char(17) +); \ No newline at end of file diff --git a/pkg/endpoints/ambient.go b/pkg/endpoints/ambient.go new file mode 100644 index 0000000..d3c1aad --- /dev/null +++ b/pkg/endpoints/ambient.go @@ -0,0 +1,46 @@ +package endpoints + +import ( + "log" + "net/http" + "reflect" + "strconv" + "time" + + "github.com/dustinpianalto/errors" + "github.com/dustinpianalto/weather" + "github.com/dustinpianalto/weather/pkg/services" + "github.com/gorilla/schema" +) + +func AmbientHandler(w http.ResponseWriter, r *http.Request) { + const method errors.Method = "endpoints/AmbientHandler" + entry := &weather.AmbientEntry{} + decoder := schema.NewDecoder() + decoder.RegisterConverter(time.Time{}, timeConverter) + if err := decoder.Decode(entry, r.URL.Query()); err != nil { + log.Println(errors.E(method, errors.Malformed, "error decoding AmbientEntry", err)) + return + } + if entry.MAC == "" && entry.PASSKEY != "" { + entry.MAC = entry.PASSKEY + } + entry, err := services.EventService.AddEvent(entry) + if err != nil { + log.Println(errors.E(method, errors.Internal, "error adding entry to database", err)) + return + } + log.Printf("%#v\n\n", entry) + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write([]byte{}) +} + +func timeConverter(value string) reflect.Value { + if v, err := time.Parse("2006-01-02 15:04:05", value); err == nil { + return reflect.ValueOf(v) + } else if i, err := strconv.ParseInt(value, 10, 64); err == nil { + return reflect.ValueOf(time.Unix(i, 0)) + } + return reflect.Value{} +} diff --git a/pkg/services/services.go b/pkg/services/services.go new file mode 100644 index 0000000..184ca29 --- /dev/null +++ b/pkg/services/services.go @@ -0,0 +1,48 @@ +package services + +import ( + "log" + + "github.com/dustinpianalto/weather" + "github.com/dustinpianalto/weather/internal/mqtt" + "github.com/dustinpianalto/weather/internal/postgres" +) + +var EventService weather.AmbientService + +func InitServices() { + EventService = eventService{ + postgesService: postgres.EventService, + mqttService: mqtt.AmbientService, + } + log.Println("Services Initialized") +} + +type eventService struct { + postgesService weather.AmbientService + mqttService weather.AmbientService +} + +func (e eventService) Event(i uint64) (*weather.AmbientEntry, error) { + return e.postgesService.Event(i) +} + +func (e eventService) AddEvent(event *weather.AmbientEntry) (*weather.AmbientEntry, error) { + event, err := e.postgesService.AddEvent(event) + if err != nil { + log.Println(err) + } + event, err = e.mqttService.AddEvent(event) + if err != nil { + log.Println(err) + } + return event, nil +} + +func (e eventService) UpdateEvent(event *weather.AmbientEntry) error { + return e.postgesService.UpdateEvent(event) +} + +func (e eventService) DeleteEvent(event *weather.AmbientEntry) error { + return e.postgesService.UpdateEvent(event) +}