Ver a proveniência

feat: locations algorithm, chore: remove unused files

chore/restructure-decoder
Blaz Smehov há 1 semana
ascendente
cometimento
4b872a5cc0
18 ficheiros alterados com 378 adições e 2067 eliminações
  1. +3
    -1
      .gitignore
  2. +5
    -0
      build/init-scripts/create_topic.sh
  3. +16
    -83
      cmd/decoder/main.go
  4. +248
    -0
      cmd/location/main.go
  5. +2
    -2
      cmd/server/main.go
  6. +15
    -14
      internal/pkg/bridge/mqtthandler/mqtthandler.go
  7. +0
    -371
      internal/pkg/httpserver/server.go
  8. +0
    -3
      internal/pkg/httpserver/server.md
  9. +1
    -1
      internal/pkg/model/typeMethods.go
  10. +88
    -87
      internal/pkg/model/types.go
  11. +0
    -128
      internal/pkg/mqttclient/beacon.go
  12. +0
    -35
      internal/pkg/mqttclient/fillter.go
  13. +0
    -165
      internal/pkg/mqttclient/location.go
  14. +0
    -62
      internal/pkg/mqttclient/processor.go
  15. +0
    -9
      test/README.md
  16. +0
    -160
      test/httpserver_test/httpserver_test.go
  17. +0
    -46
      test/mqtt_test/mqtt_test.go
  18. +0
    -900
      test/node-red-integration-tests/apitest.json

+ 3
- 1
.gitignore Ver ficheiro

@@ -24,4 +24,6 @@ cmd/presenSe/presence.db
# Dependency directories (remove the comment below to include it)
vendor/
volumes/node-red/
main
main

*.sh

+ 5
- 0
build/init-scripts/create_topic.sh Ver ficheiro

@@ -13,4 +13,9 @@
# create topic alertBeacons
/opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:29092 \
--create --if-not-exists --topic alertbeacons \
--partitions 1 --replication-factor 1

# create topic alertBeacons
/opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:29092 \
--create --if-not-exists --topic locevents \
--partitions 1 --replication-factor 1

+ 16
- 83
cmd/decoder/main.go Ver ficheiro

@@ -7,15 +7,11 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
"time"

"github.com/AFASystems/presence/internal/pkg/config"
"github.com/AFASystems/presence/internal/pkg/kafkaclient"
"github.com/AFASystems/presence/internal/pkg/model"
"github.com/AFASystems/presence/internal/pkg/mqttclient"
"github.com/segmentio/kafka-go"
)

@@ -27,11 +23,11 @@ func main() {
},
Settings: model.Settings{
Settings: model.SettingsVal{
Location_confidence: 4,
Last_seen_threshold: 15,
Beacon_metrics_size: 30,
HA_send_interval: 5,
HA_send_changes_only: false,
LocationConfidence: 4,
LastSeenThreshold: 15,
BeaconMetricSize: 30,
HASendInterval: 5,
HASendChangesOnly: false,
},
},
BeaconEvents: model.BeaconEventList{
@@ -54,25 +50,11 @@ func main() {
defer alertWriter.Close()

fmt.Println("Decoder initialized, subscribed to Kafka topics")

// // Kafka reader for latest list updates
// latestReader := kafkaclient.KafkaReader(cfg.KafkaURL, "latestbeacons", "gid-latest")
// defer latestReader.Close()

// // Kafka reader for settings updates
// settingsReader := kafkaclient.KafkaReader(cfg.KafkaURL, "settings", "gid-settings")
// defer settingsReader.Close()

// declare channel for collecting Kafka messages
chRaw := make(chan model.Incoming_json, 2000)
chRaw := make(chan model.BeaconAdvertisement, 2000)
chApi := make(chan model.ApiUpdate, 2000)
// chLatest := make(chan model.Incoming_json, 2000)
// chSettings := make(chan model.SettingsVal, 10)

go kafkaclient.Consume(rawReader, chRaw)
go kafkaclient.Consume(apiReader, chApi)
// go kafkaclient.Consume(latestReader, chLatest)
// go kafkaclient.Consume(settingsReader, chSettings)

for {
select {
@@ -81,39 +63,32 @@ func main() {
case msg := <-chApi:
switch msg.Method {
case "POST":
id := msg.Beacon.Beacon_id
id := msg.Beacon.ID
appCtx.BeaconsLookup[id] = struct{}{}
case "DELETE":
fmt.Println("Incoming delete message")
}
// case msg := <-chLatest:
// fmt.Println("latest msg: ", msg)
// case msg := <-chSettings:
// appCtx.Settings.Lock.Lock()
// appCtx.Settings.Settings = msg
// fmt.Println("settings channel: ", msg)
// appCtx.Settings.Lock.Unlock()
}
}
}

func processIncoming(incoming model.Incoming_json, ctx *model.AppContext, writer *kafka.Writer) {
id := mqttclient.GetBeaconID(incoming)
func processIncoming(adv model.BeaconAdvertisement, ctx *model.AppContext, writer *kafka.Writer) {
id := adv.MAC
_, ok := ctx.BeaconsLookup[id]
if !ok {
return
}

err := decodeBeacon(incoming, ctx, writer)
err := decodeBeacon(adv, ctx, writer)
if err != nil {
fmt.Println("error in decoding")
return
}
}

func decodeBeacon(incoming model.Incoming_json, ctx *model.AppContext, writer *kafka.Writer) error {
beacon := strings.TrimSpace(incoming.Data)
id := mqttclient.GetBeaconID(incoming)
func decodeBeacon(adv model.BeaconAdvertisement, ctx *model.AppContext, writer *kafka.Writer) error {
beacon := strings.TrimSpace(adv.Data)
id := adv.MAC
if beacon == "" {
return nil // How to return error?, do I even need to return error
}
@@ -137,12 +112,12 @@ func decodeBeacon(incoming model.Incoming_json, ctx *model.AppContext, writer *k
ad := b[r[0]:r[1]]
if checkIngics(ad) {
event = parseIngicsState(ad)
event.Id = id
event.ID = id
event.Name = id
break
} else if checkEddystoneTLM(ad) {
event = parseEddystoneState(ad)
event.Id = id
event.ID = id
event.Name = id
break
} else if checkMinewB7(ad) {
@@ -151,7 +126,7 @@ func decodeBeacon(incoming model.Incoming_json, ctx *model.AppContext, writer *k
}
}

if event.Id != "" {
if event.ID != "" {
prevEvent, ok := ctx.BeaconEvents.Beacons[id]
ctx.BeaconEvents.Beacons[id] = event
if ok && bytes.Equal(prevEvent.Hash(), event.Hash()) {
@@ -246,45 +221,3 @@ func ParseADFast(b []byte) [][2]int {

return res
}

func getBeaconDistance(incoming model.Incoming_json) float64 {
rssi := incoming.RSSI
power := incoming.TX_power
distance := 100.0

ratio := float64(rssi) * (1.0 / float64(twos_comp(power)))
if ratio < 1.0 {
distance = math.Pow(ratio, 10)
} else {
distance = (0.89976)*math.Pow(ratio, 7.7095) + 0.111
}
return distance
}

func updateBeacon(beacon *model.Beacon, incoming model.Incoming_json) {
now := time.Now().Unix()

beacon.Incoming_JSON = incoming
beacon.Last_seen = now
beacon.Beacon_type = incoming.Beacon_type
beacon.HB_ButtonCounter = incoming.HB_ButtonCounter
beacon.HB_Battery = incoming.HB_Battery
beacon.HB_RandomNonce = incoming.HB_RandomNonce
beacon.HB_ButtonMode = incoming.HB_ButtonMode

if beacon.Beacon_metrics == nil {
beacon.Beacon_metrics = make([]model.BeaconMetric, 10)
}

metric := model.BeaconMetric{}
metric.Distance = getBeaconDistance(incoming)
metric.Timestamp = now
metric.Rssi = int64(incoming.RSSI)
metric.Location = incoming.Hostname
beacon.Beacon_metrics = append(beacon.Beacon_metrics, metric)
}

func twos_comp(inp string) int64 {
i, _ := strconv.ParseInt("0x"+inp, 0, 64)
return i - 256
}

+ 248
- 0
cmd/location/main.go Ver ficheiro

@@ -0,0 +1,248 @@
package main

import (
"context"
"encoding/json"
"fmt"
"math"
"strconv"
"time"

"github.com/AFASystems/presence/internal/pkg/config"
"github.com/AFASystems/presence/internal/pkg/kafkaclient"
"github.com/AFASystems/presence/internal/pkg/model"
"github.com/segmentio/kafka-go"
)

func main() {
// Load global context to init beacons and latest list
appCtx := model.AppContext{
Settings: model.Settings{
Settings: model.SettingsVal{
LocationConfidence: 4,
LastSeenThreshold: 15,
BeaconMetricSize: 30,
HASendInterval: 5,
HASendChangesOnly: false,
RSSIEnforceThreshold: true,
RSSIMinThreshold: 100,
},
},
BeaconsLookup: make(map[string]struct{}),
LatestList: model.LatestBeaconsList{
LatestList: make(map[string]model.Beacon),
},
}

cfg := config.Load()

// Kafka reader for Raw MQTT beacons
rawReader := kafkaclient.KafkaReader(cfg.KafkaURL, "rawbeacons", "gid-raw-loc")
defer rawReader.Close()

// Kafka reader for API server updates
apiReader := kafkaclient.KafkaReader(cfg.KafkaURL, "apibeacons", "gid-api-loc")
defer apiReader.Close()

writer := kafkaclient.KafkaWriter(cfg.KafkaURL, "locevents")
defer writer.Close()

fmt.Println("Locations algorithm initialized, subscribed to Kafka topics")

locTicker := time.NewTicker(1 * time.Second)
defer locTicker.Stop()

chRaw := make(chan model.BeaconAdvertisement, 2000)
chApi := make(chan model.ApiUpdate, 2000)

go kafkaclient.Consume(rawReader, chRaw)
go kafkaclient.Consume(apiReader, chApi)

for {
select {
case <-locTicker.C:
getLikelyLocations(&appCtx, writer)
case msg := <-chRaw:
assignBeaconToList(msg, &appCtx)
case msg := <-chApi:
switch msg.Method {
case "POST":
id := msg.Beacon.ID
appCtx.BeaconsLookup[id] = struct{}{}
case "DELETE":
fmt.Println("Incoming delete message")
}
}
}
}

func getLikelyLocations(ctx *model.AppContext, writer *kafka.Writer) {
ctx.Beacons.Lock.Lock()
beacons := ctx.Beacons.Beacons

for _, beacon := range beacons {

// Shrinking the model because other properties have nothing to do with the location
r := model.HTTPLocation{
Method: "Standard",
Distance: 999,
Name: beacon.Name,
ID: beacon.ID,
Location: "",
LastSeen: 999,
}

mSize := len(beacon.BeaconMetrics)

if (int64(time.Now().Unix()) - (beacon.BeaconMetrics[mSize-1].Timestamp)) > ctx.Settings.Settings.LastSeenThreshold {
continue
}

locList := make(map[string]float64)
seenW := 1.5
rssiW := 0.75
for _, metric := range beacon.BeaconMetrics {
res := seenW + (rssiW * (1.0 - (float64(metric.RSSI) / -100.0)))
locList[metric.Location] += res
}

bestLocName := ""
maxScore := 0.0
for locName, score := range locList {
if score > maxScore {
maxScore = score
bestLocName = locName
}
}

bestLocation := model.BestLocation{
Name: bestLocName,
Distance: beacon.BeaconMetrics[mSize-1].Distance,
LastSeen: beacon.BeaconMetrics[mSize-1].Timestamp,
}

if bestLocName == beacon.PreviousLocation {
beacon.LocationConfidence++
} else {
beacon.LocationConfidence = 0
}

r.Distance = bestLocation.Distance
r.Location = bestLocName
r.LastSeen = bestLocation.LastSeen

if beacon.LocationConfidence == ctx.Settings.Settings.LocationConfidence && beacon.PreviousConfidentLocation != bestLocName {
beacon.LocationConfidence = 0

// Who do I need this if I am sending entire structure anyways? who knows
js, err := json.Marshal(model.LocationChange{
Method: "LocationChange",
BeaconRef: beacon,
Name: beacon.Name,
PreviousLocation: beacon.PreviousConfidentLocation,
NewLocation: bestLocName,
Timestamp: time.Now().Unix(),
})

if err != nil {
beacon.PreviousConfidentLocation = bestLocName
beacon.PreviousLocation = bestLocName
ctx.Beacons.Beacons[beacon.ID] = beacon
continue
}

msg := kafka.Message{
Value: js,
}

err = writer.WriteMessages(context.Background(), msg)
if err != nil {
fmt.Println("Error in sending Kafka message")
}
}

beacon.PreviousLocation = bestLocName
ctx.Beacons.Beacons[beacon.ID] = beacon

js, err := json.Marshal(r)
if err != nil {
continue
}

msg := kafka.Message{
Value: js,
}

err = writer.WriteMessages(context.Background(), msg)
if err != nil {
fmt.Println("Error in sending Kafka message")
}
}
}

func assignBeaconToList(adv model.BeaconAdvertisement, ctx *model.AppContext) {
id := adv.MAC
_, ok := ctx.BeaconsLookup[id]

now := time.Now().Unix()

if !ok {
// handle removing from the list somewhere else, probably at the point where this is being used which is nowhere in the original code
ctx.LatestList.Lock.Lock()
ctx.LatestList.LatestList[id] = model.Beacon{ID: id, BeaconType: adv.BeaconType, LastSeen: now, IncomingJSON: adv, BeaconLocation: adv.Hostname, Distance: getBeaconDistance(adv)}
ctx.LatestList.Lock.Unlock()
return
}

if ctx.Settings.Settings.RSSIEnforceThreshold && (int64(adv.RSSI) < ctx.Settings.Settings.RSSIMinThreshold) {
return
}

ctx.Beacons.Lock.Lock()
beacon := ctx.Beacons.Beacons[id]

beacon.IncomingJSON = adv
beacon.LastSeen = now
beacon.BeaconType = adv.BeaconType
beacon.HSButtonCounter = adv.HSButtonCounter
beacon.HSBattery = adv.HSBatteryLevel
beacon.HSRandomNonce = adv.HSRandomNonce
beacon.HSButtonMode = adv.HSButtonMode

if beacon.BeaconMetrics == nil {
beacon.BeaconMetrics = make([]model.BeaconMetric, 0, ctx.Settings.Settings.BeaconMetricSize)
}

metric := model.BeaconMetric{
Distance: getBeaconDistance(adv),
Timestamp: now,
RSSI: int64(adv.RSSI),
Location: adv.Hostname,
}

if len(beacon.BeaconMetrics) >= ctx.Settings.Settings.BeaconMetricSize {
copy(beacon.BeaconMetrics, beacon.BeaconMetrics[1:])
beacon.BeaconMetrics[ctx.Settings.Settings.BeaconMetricSize-1] = metric
} else {
beacon.BeaconMetrics = append(beacon.BeaconMetrics, metric)
}

ctx.Beacons.Beacons[id] = beacon
ctx.Beacons.Lock.Unlock()
}

func getBeaconDistance(adv model.BeaconAdvertisement) float64 {
ratio := float64(adv.RSSI) * (1.0 / float64(twosComp(adv.TXPower)))
distance := 100.0
if ratio < 1.0 {
distance = math.Pow(ratio, 10)
} else {
distance = (0.89976)*math.Pow(ratio, 7.7095) + 0.111
}
return distance
}

func twosComp(inp string) int64 {
i, _ := strconv.ParseInt("0x"+inp, 0, 64)
return i - 256
}

+ 2
- 2
cmd/server/main.go Ver ficheiro

@@ -124,7 +124,7 @@ func beaconsAddHandler(writer *kafka.Writer) http.HandlerFunc {

fmt.Println("sending POST message")

if (len(strings.TrimSpace(inBeacon.Name)) == 0) || (len(strings.TrimSpace(inBeacon.Beacon_id)) == 0) {
if (len(strings.TrimSpace(inBeacon.Name)) == 0) || (len(strings.TrimSpace(inBeacon.ID)) == 0) {
http.Error(w, "name and beacon_id cannot be blank", 400)
return
}
@@ -192,7 +192,7 @@ func settingsEditHandler(writer *kafka.Writer) http.HandlerFunc {
}

func settingsCheck(settings model.SettingsVal) bool {
if settings.Location_confidence <= 0 || settings.Last_seen_threshold <= 0 || settings.HA_send_interval <= 0 {
if settings.LocationConfidence <= 0 || settings.LastSeenThreshold <= 0 || settings.HASendInterval <= 0 {
return false
}



+ 15
- 14
internal/pkg/bridge/mqtthandler/mqtthandler.go Ver ficheiro

@@ -30,17 +30,18 @@ func MqttHandler(writer *kafka.Writer, topicName []byte, message []byte) {
if reading.Type == "Gateway" {
continue
}
incoming := model.Incoming_json{
Hostname: hostname,
MAC: reading.MAC,
RSSI: int64(reading.RSSI),
Data: reading.RawData,
HB_ButtonCounter: parseButtonState(reading.RawData),
adv := model.BeaconAdvertisement{
Hostname: hostname,
MAC: reading.MAC,
RSSI: int64(reading.RSSI),
Data: reading.RawData,
HSButtonCounter: parseButtonState(reading.RawData),
}

encodedMsg, err := json.Marshal(incoming)
encodedMsg, err := json.Marshal(adv)
if err != nil {
fmt.Println("Error in marshaling: ", err)
break
}

msg := kafka.Message{
@@ -65,14 +66,14 @@ func MqttHandler(writer *kafka.Writer, topicName []byte, message []byte) {
rawdata := s[4]
buttonCounter := parseButtonState(rawdata)
if buttonCounter > 0 {
incoming := model.Incoming_json{}
adv := model.BeaconAdvertisement{}
i, _ := strconv.ParseInt(s[3], 10, 64)
incoming.Hostname = hostname
incoming.Beacon_type = "hb_button"
incoming.MAC = s[1]
incoming.RSSI = i
incoming.Data = rawdata
incoming.HB_ButtonCounter = buttonCounter
adv.Hostname = hostname
adv.BeaconType = "hb_button"
adv.MAC = s[1]
adv.RSSI = i
adv.Data = rawdata
adv.HSButtonCounter = buttonCounter

read_line := strings.TrimRight(string(s[5]), "\r\n")
it, err33 := strconv.Atoi(read_line)


+ 0
- 371
internal/pkg/httpserver/server.go Ver ficheiro

@@ -1,371 +0,0 @@
package httpserver

import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"

"github.com/AFASystems/presence/internal/pkg/model"
"github.com/AFASystems/presence/internal/pkg/persistence"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
)

var (
upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
)

const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = (pongWait * 9) / 10
beaconPeriod = 2 * time.Second
)

// Init store in main or anywhere else and pass it to all initializer functions
// called in main, then with controllers or handlers use wrapper that takes entire store
// allocates only the properties that need to be passed into the controller

func StartHTTPServer(addr string, ctx *model.AppContext) {
headersOk := handlers.AllowedHeaders([]string{"X-Requested-With"})
originsOk := handlers.AllowedOrigins([]string{"*"})
methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"})

// Set up HTTP server
r := mux.NewRouter()
r.HandleFunc("/api/results", resultsHandler(&ctx.HTTPResults))

r.HandleFunc("/api/beacons/{beacon_id}", BeaconsDeleteHandler(&ctx.Beacons, ctx.ButtonsList)).Methods("DELETE")
r.HandleFunc("/api/beacons", BeaconsListHandler(&ctx.Beacons)).Methods("GET")
r.HandleFunc("/api/beacons", BeaconsAddHandler(&ctx.Beacons)).Methods("POST") //since beacons are hashmap, just have put and post be same thing. it'll either add or modify that entry
r.HandleFunc("/api/beacons", BeaconsAddHandler(&ctx.Beacons)).Methods("PUT")

r.HandleFunc("/api/latest-beacons", latestBeaconsListHandler(&ctx.LatestList)).Methods("GET")

r.HandleFunc("/api/settings", SettingsListHandler(&ctx.Settings)).Methods("GET")
r.HandleFunc("/api/settings", SettingsEditHandler(&ctx.Settings)).Methods("POST")

r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(http.Dir("static_html/js/"))))
r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir("static_html/css/"))))
r.PathPrefix("/img/").Handler(http.StripPrefix("/img/", http.FileServer(http.Dir("static_html/img/"))))
r.PathPrefix("/").Handler(http.FileServer(http.Dir("static_html/")))

http.Handle("/", r)

mxWS := mux.NewRouter()
mxWS.HandleFunc("/ws/api/beacons", serveWs(&ctx.HTTPResults))
mxWS.HandleFunc("/ws/api/beacons/latest", serveLatestBeaconsWs(&ctx.LatestList))
mxWS.HandleFunc("/ws/broadcast", handleConnections(ctx.Clients, &ctx.Broadcast))
http.Handle("/ws/", mxWS)

go handleMessages(ctx.Clients, &ctx.Broadcast)

http.ListenAndServe(addr, handlers.CORS(originsOk, headersOk, methodsOk)(r))

}

func resultsHandler(httpResults *model.HTTPResultsList) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
httpResults.HTTPResultsLock.Lock()
defer httpResults.HTTPResultsLock.Unlock()
js, err := json.Marshal(httpResults.HTTPResults)

if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Write(js)
}
}

func BeaconsListHandler(beacons *model.BeaconsList) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
beacons.Lock.RLock()
js, err := json.Marshal(beacons.Beacons)
beacons.Lock.RUnlock()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Write(js)
}
}

func BeaconsAddHandler(beacons *model.BeaconsList) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var inBeacon model.Beacon
err := decoder.Decode(&inBeacon)

if err != nil {
http.Error(w, err.Error(), 400)
return
}

if (len(strings.TrimSpace(inBeacon.Name)) == 0) || (len(strings.TrimSpace(inBeacon.Beacon_id)) == 0) {
http.Error(w, "name and beacon_id cannot be blank", 400)
return
}

beacons.Beacons[inBeacon.Beacon_id] = inBeacon

err = persistence.PersistBeacons(beacons)

if err != nil {
http.Error(w, "trouble persisting beacons list, create bucket", 500)
return
}

w.Write([]byte("ok"))
}
}

func BeaconsDeleteHandler(beacons *model.BeaconsList, buttonsList map[string]model.Button) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
fmt.Println("route param: ", vars)
beaconId := vars["beacon_id"]
_, ok := beacons.Beacons[beaconId]
if !ok {
http.Error(w, "no beacon with the specified id", 400) // change the status code
return
}
delete(beacons.Beacons, beaconId)

_, ok = buttonsList[beaconId]
if ok {
delete(buttonsList, beaconId)
}

err := persistence.PersistBeacons(beacons)
if err != nil {
http.Error(w, "trouble persisting beacons list, create bucket", 500)
return
}

w.Write([]byte("ok"))
}
}

func latestBeaconsListHandler(latestList *model.LatestBeaconsList) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
latestList.Lock.RLock()
var la = make([]model.Beacon, 0)
for _, b := range latestList.LatestList {
la = append(la, b)
}
latestList.Lock.RUnlock()
js, err := json.Marshal(la)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Write(js)
}
}

func SettingsListHandler(settings *model.Settings) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
js, err := json.Marshal(settings)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Write(js)
}
}

func SettingsEditHandler(settings *model.Settings) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var inSettings model.Settings
err := decoder.Decode(&inSettings)
if err != nil {
http.Error(w, err.Error(), 400)
return
}

//make sure values are > 0
if (inSettings.Location_confidence <= 0) ||
(inSettings.Last_seen_threshold <= 0) ||
(inSettings.HA_send_interval <= 0) {
http.Error(w, "values must be greater than 0", 400)
return
}

*settings = inSettings

err = persistence.PersistSettings(settings)
if err != nil {
http.Error(w, "trouble persisting settings, create bucket", 500)
return
}

w.Write([]byte("ok"))
}
}

func reader(ws *websocket.Conn) {
defer ws.Close()
ws.SetReadLimit(512)
ws.SetReadDeadline(time.Now().Add(pongWait))
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, _, err := ws.ReadMessage()
if err != nil {
break
}
}
}

func writer(ws *websocket.Conn, httpResult *model.HTTPResultsList) {
pingTicker := time.NewTicker(pingPeriod)
beaconTicker := time.NewTicker(beaconPeriod)
defer func() {
pingTicker.Stop()
beaconTicker.Stop()
ws.Close()
}()
for {
select {
case <-beaconTicker.C:
httpResult.HTTPResultsLock.Lock()
defer httpResult.HTTPResultsLock.Unlock()
js, err := json.Marshal(httpResult.HTTPResults)

if err != nil {
js = []byte("error")
}

ws.SetWriteDeadline(time.Now().Add(writeWait))
if err := ws.WriteMessage(websocket.TextMessage, js); err != nil {
return
}
case <-pingTicker.C:
ws.SetWriteDeadline(time.Now().Add(writeWait))
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
return
}
}
}
}

func serveWs(httpResult *model.HTTPResultsList) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
log.Println(err)
}
return
}

go writer(ws, httpResult)
reader(ws)
}
}

func latestBeaconWriter(ws *websocket.Conn, latestBeaconsList map[string]model.Beacon, lock *sync.RWMutex) {
pingTicker := time.NewTicker(pingPeriod)
beaconTicker := time.NewTicker(beaconPeriod)
defer func() {
pingTicker.Stop()
beaconTicker.Stop()
ws.Close()
}()
for {
select {
case <-beaconTicker.C:

lock.RLock()
var la = make([]model.Beacon, 0)
for _, b := range latestBeaconsList {
la = append(la, b)
}
lock.RUnlock()
js, err := json.Marshal(la)

if err != nil {
js = []byte("error")
}

ws.SetWriteDeadline(time.Now().Add(writeWait))
if err := ws.WriteMessage(websocket.TextMessage, js); err != nil {
return
}
case <-pingTicker.C:
ws.SetWriteDeadline(time.Now().Add(writeWait))
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
return
}
}
}
}

func serveLatestBeaconsWs(latestList *model.LatestBeaconsList) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
log.Println(err)
}
return
}

go latestBeaconWriter(ws, latestList.LatestList, &latestList.Lock)
reader(ws)
}
}

func handleConnections(clients map[*websocket.Conn]bool, broadcast *chan model.Message) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Fatal(err)
}

defer ws.Close()

clients[ws] = true

for {
var msg model.Message
err := ws.ReadJSON(&msg)
if err != nil {
log.Printf("error: %v", err)
delete(clients, ws)
break
}
*broadcast <- msg
}
}
}

func handleMessages(clients map[*websocket.Conn]bool, broadcast *chan model.Message) {
for {
msg := <-*broadcast
for client := range clients {
err := client.WriteJSON(msg)
if err != nil {
log.Printf("error: %v", err)
client.Close()
delete(clients, client)
}
}
}
}

+ 0
- 3
internal/pkg/httpserver/server.md Ver ficheiro

@@ -1,3 +0,0 @@
# Server

TODO: refactor to structure: router -> controller -> service, possibly use swagger or any other package to define structure of the API server

+ 1
- 1
internal/pkg/model/typeMethods.go Ver ficheiro

@@ -7,7 +7,7 @@ import (

func (b BeaconEvent) Hash() []byte {
rBatt := (b.Battery / 10) * 10
c := fmt.Sprintf("%d%d%s%s%s", rBatt, b.Event, b.Id, b.Name, b.Type)
c := fmt.Sprintf("%d%d%s%s%s", rBatt, b.Event, b.ID, b.Name, b.Type)
h := sha256.New()
h.Write([]byte(c))



+ 88
- 87
internal/pkg/model/types.go Ver ficheiro

@@ -9,11 +9,13 @@ import (

// Settings defines configuration parameters for presence detection behavior.
type SettingsVal struct {
Location_confidence int64 `json:"location_confidence"`
Last_seen_threshold int64 `json:"last_seen_threshold"`
Beacon_metrics_size int `json:"beacon_metrics_size"`
HA_send_interval int64 `json:"ha_send_interval"`
HA_send_changes_only bool `json:"ha_send_changes_only"`
LocationConfidence int64 `json:"location_confidence"`
LastSeenThreshold int64 `json:"last_seen_threshold"`
BeaconMetricSize int `json:"beacon_metrics_size"`
HASendInterval int64 `json:"ha_send_interval"`
HASendChangesOnly bool `json:"ha_send_changes_only"`
RSSIMinThreshold int64 `json:"rssi_min_threshold"`
RSSIEnforceThreshold bool `json:"enforce_rssi_threshold"`
}

type Settings struct {
@@ -21,115 +23,115 @@ type Settings struct {
Lock sync.RWMutex
}

// Incoming_json represents the JSON payload received from beacon messages.
type Incoming_json struct {
Hostname string `json:"hostname"`
MAC string `json:"mac"`
RSSI int64 `json:"rssi"`
Is_scan_response string `json:"is_scan_response"`
Ttype string `json:"type"`
Data string `json:"data"`
Beacon_type string `json:"beacon_type"`
UUID string `json:"uuid"`
Major string `json:"major"`
Minor string `json:"minor"`
TX_power string `json:"tx_power"`
Namespace string `json:"namespace"`
Instance_id string `json:"instance_id"`
// button stuff
HB_ButtonCounter int64 `json:"hb_button_counter"`
HB_ButtonCounter_Prev int64 `json:"hb_button_counter_prev"`
HB_Battery int64 `json:"hb_button_battery"`
HB_RandomNonce string `json:"hb_button_random"`
HB_ButtonMode string `json:"hb_button_mode"`
// BeaconAdvertisement represents the JSON payload received from beacon advertisements.
type BeaconAdvertisement struct {
Hostname string `json:"hostname"`
MAC string `json:"mac"`
RSSI int64 `json:"rssi"`
ScanResponse string `json:"is_scan_response"`
Type string `json:"type"`
Data string `json:"data"`
BeaconType string `json:"beacon_type"`
UUID string `json:"uuid"`
Major string `json:"major"`
Minor string `json:"minor"`
TXPower string `json:"tx_power"`
NamespaceID string `json:"namespace"`
InstanceID string `json:"instance_id"`
HSButtonCounter int64 `json:"hb_button_counter"`
HSButtonPrev int64 `json:"hb_button_counter_prev"`
HSBatteryLevel int64 `json:"hb_button_battery"`
HSRandomNonce string `json:"hb_button_random"`
HSButtonMode string `json:"hb_button_mode"`
}

// Advertisement describes a generic beacon advertisement payload.
type Advertisement struct {
ttype string
content string
seen int64
Type string
Content string
Seen int64
}

// BeaconMetric stores signal and distance data for a beacon.
type BeaconMetric struct {
Location string
Distance float64
Rssi int64
RSSI int64
Timestamp int64
}

// Location defines a physical location and synchronization control.
type Location struct {
name string
lock sync.RWMutex
Name string
Lock sync.RWMutex
}

// BestLocation represents the most probable location of a beacon.
type BestLocation struct {
Distance float64
Name string
Last_seen int64
Distance float64
Name string
LastSeen int64
}

// HTTPLocation describes a beacon's state as served over HTTP.
type HTTPLocation struct {
Previous_confident_location string `json:"previous_confident_location"`
Distance float64 `json:"distance"`
Name string `json:"name"`
Beacon_name string `json:"beacon_name"`
Beacon_id string `json:"beacon_id"`
Beacon_type string `json:"beacon_type"`
HB_Battery int64 `json:"hb_button_battery"`
HB_ButtonMode string `json:"hb_button_mode"`
HB_ButtonCounter int64 `json:"hb_button_counter"`
Location string `json:"location"`
Last_seen int64 `json:"last_seen"`
Method string `json:"method"`
PreviousConfidentLocation string `json:"previous_confident_location"`
Distance float64 `json:"distance"`
Name string `json:"name"`
BeaconName string `json:"beacon_name"`
ID string `json:"id"`
BeaconType string `json:"beacon_type"`
HSBattery int64 `json:"hs_battery"`
HSButtonMode string `json:"hs_button_mode"`
HSButtonCounter int64 `json:"hs_button_counter"`
Location string `json:"location"`
LastSeen int64 `json:"last_seen"`
}

// LocationChange defines a change event for a beacon's detected location.
type LocationChange struct {
Beacon_ref Beacon `json:"beacon_info"`
Name string `json:"name"`
Beacon_name string `json:"beacon_name"`
Previous_location string `json:"previous_location"`
New_location string `json:"new_location"`
Timestamp int64 `json:"timestamp"`
Method string `json:"method"`
BeaconRef Beacon `json:"beacon_info"`
Name string `json:"name"`
BeaconName string `json:"beacon_name"`
PreviousLocation string `json:"previous_location"`
NewLocation string `json:"new_location"`
Timestamp int64 `json:"timestamp"`
}

// HAMessage represents a Home Assistant integration payload.
type HAMessage struct {
Beacon_id string `json:"id"`
Beacon_name string `json:"name"`
Distance float64 `json:"distance"`
ID string `json:"id"`
BeaconName string `json:"name"`
Distance float64 `json:"distance"`
}

// Beacon holds all relevant information about a tracked beacon device.
type Beacon struct {
Name string `json:"name"`
Beacon_id string `json:"beacon_id"`
Beacon_type string `json:"beacon_type"`
Beacon_location string `json:"beacon_location"`
Last_seen int64 `json:"last_seen"`
Incoming_JSON Incoming_json `json:"incoming_json"`
Distance float64 `json:"distance"`
Previous_location string
Previous_confident_location string
Expired_location string
Location_confidence int64
Location_history []string
Beacon_metrics []BeaconMetric

HB_ButtonCounter int64 `json:"hb_button_counter"`
HB_ButtonCounter_Prev int64 `json:"hb_button_counter_prev"`
HB_Battery int64 `json:"hb_button_battery"`
HB_RandomNonce string `json:"hb_button_random"`
HB_ButtonMode string `json:"hb_button_mode"`
Name string `json:"name"`
ID string `json:"beacon_id"`
BeaconType string `json:"beacon_type"`
BeaconLocation string `json:"beacon_location"`
LastSeen int64 `json:"last_seen"`
IncomingJSON BeaconAdvertisement `json:"incoming_json"`
Distance float64 `json:"distance"`
PreviousLocation string
PreviousConfidentLocation string
ExpiredLocation string
LocationConfidence int64
LocationHistory []string
BeaconMetrics []BeaconMetric
HSButtonCounter int64 `json:"hs_button_counter"`
HSButtonPrev int64 `json:"hs_button_counter_prev"`
HSBattery int64 `json:"hs_button_battery"`
HSRandomNonce string `json:"hs_button_random"`
HSButtonMode string `json:"hs_button_mode"`
}

type BeaconEvent struct {
Name string
Id string
ID string
Type string
Battery uint32
Event int
@@ -137,18 +139,17 @@ type BeaconEvent struct {

// Button represents a hardware button beacon device.
type Button struct {
Name string `json:"name"`
Button_id string `json:"button_id"`
Button_type string `json:"button_type"`
Button_location string `json:"button_location"`
Incoming_JSON Incoming_json `json:"incoming_json"`
Distance float64 `json:"distance"`
Last_seen int64 `json:"last_seen"`

HB_ButtonCounter int64 `json:"hb_button_counter"`
HB_Battery int64 `json:"hb_button_battery"`
HB_RandomNonce string `json:"hb_button_random"`
HB_ButtonMode string `json:"hb_button_mode"`
Name string `json:"name"`
ButtonID string `json:"button_id"`
ButtonType string `json:"button_type"`
ButtonLocation string `json:"button_location"`
IncomingJSON BeaconAdvertisement `json:"incoming_json"`
Distance float64 `json:"distance"`
LastSeen int64 `json:"last_seen"`
HSButtonCounter int64 `json:"hs_button_counter"`
HSBattery int64 `json:"hs_button_battery"`
HSRandomNonce string `json:"hs_button_random"`
HSButtonMode string `json:"hs_button_mode"`
}

// BeaconsList holds all known beacons and their synchronization lock.


+ 0
- 128
internal/pkg/mqttclient/beacon.go Ver ficheiro

@@ -1,128 +0,0 @@
package mqttclient

import (
"bytes"
"encoding/json"
"fmt"
"log"
"math"
"os/exec"
"strconv"

"github.com/AFASystems/presence/internal/pkg/model"
"github.com/yosssi/gmq/mqtt"
"github.com/yosssi/gmq/mqtt/client"
)

func GetBeaconID(incoming model.Incoming_json) string {
unique_id := fmt.Sprintf("%s", incoming.MAC)
return unique_id
}

func updateLatestList(incoming model.Incoming_json, now int64, latestList *model.LatestBeaconsList) {
latestList.Lock.Lock()
defer latestList.Lock.Unlock()

b := model.Beacon{
Beacon_id: GetBeaconID(incoming),
Beacon_type: incoming.Beacon_type,
Last_seen: now,
Incoming_JSON: incoming,
Beacon_location: incoming.Hostname,
Distance: getBeaconDistance(incoming),
}

latestList.LatestList[b.Beacon_id] = b

for id, v := range latestList.LatestList {
if now-v.Last_seen > 10 {
delete(latestList.LatestList, id)
}
}
}

func updateBeaconData(beacon *model.Beacon, incoming model.Incoming_json, now int64, cl *client.Client, settings *model.SettingsVal) {
beacon.Incoming_JSON = incoming
beacon.Last_seen = now
beacon.Beacon_type = incoming.Beacon_type
beacon.HB_ButtonCounter = incoming.HB_ButtonCounter
beacon.HB_Battery = incoming.HB_Battery
beacon.HB_RandomNonce = incoming.HB_RandomNonce
beacon.HB_ButtonMode = incoming.HB_ButtonMode

m := model.BeaconMetric{
Distance: getBeaconDistance(incoming),
Timestamp: now,
Rssi: int64(incoming.RSSI),
Location: incoming.Hostname,
}

beacon.Beacon_metrics = append(beacon.Beacon_metrics, m)
if len(beacon.Beacon_metrics) > settings.Beacon_metrics_size {
beacon.Beacon_metrics = beacon.Beacon_metrics[1:]
}

if beacon.HB_ButtonCounter_Prev != beacon.HB_ButtonCounter {
beacon.HB_ButtonCounter_Prev = incoming.HB_ButtonCounter
sendButtonPressed(*beacon, cl)
}
}

func sendButtonPressed(beacon model.Beacon, cl *client.Client) {
btn_msg, err := json.Marshal(beacon)
if err != nil {
panic(err)
}

err = cl.Publish(&client.PublishOptions{
QoS: mqtt.QoS1,
TopicName: []byte("afa-systems/presence/button/" + beacon.Beacon_id),
Message: btn_msg,
})
if err != nil {
panic(err)
}
s := fmt.Sprintf("/usr/bin/php /usr/local/presence/alarm_handler.php --idt=%s --idr=%s --st=%d", beacon.Beacon_id, beacon.Incoming_JSON.Hostname, beacon.HB_ButtonCounter)
err, out, errout := Shellout(s)
if err != nil {
log.Printf("error: %v\n", err)
}
fmt.Println("--- stdout ---")
fmt.Println(out)
fmt.Println("--- stderr ---")
fmt.Println(errout)
}

func getBeaconDistance(incoming model.Incoming_json) float64 {
distance := 1000.0
distance = getiBeaconDistance(incoming.RSSI, incoming.TX_power)

return distance
}

func getiBeaconDistance(rssi int64, power string) float64 {
ratio := float64(rssi) * (1.0 / float64(twos_comp(power)))
distance := 100.0
if ratio < 1.0 {
distance = math.Pow(ratio, 10)
} else {
distance = (0.89976)*math.Pow(ratio, 7.7095) + 0.111
}
return distance
}

func twos_comp(inp string) int64 {
i, _ := strconv.ParseInt("0x"+inp, 0, 64)

return i - 256
}

func Shellout(command string) (error, string, string) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command("bash", "-c", command)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
return err, stdout.String(), stderr.String()
}

+ 0
- 35
internal/pkg/mqttclient/fillter.go Ver ficheiro

@@ -1,35 +0,0 @@
package mqttclient

import (
"fmt"
"strconv"
"strings"

"github.com/AFASystems/presence/internal/pkg/model"
)

func IncomingBeaconFilter(incoming model.Incoming_json) model.Incoming_json {
out_json := incoming
if incoming.Beacon_type == "hb_button" {
raw_data := incoming.Data
hb_button_prefix_str := fmt.Sprintf("02010612FF5900")
if strings.HasPrefix(raw_data, hb_button_prefix_str) {
out_json.Namespace = "ddddeeeeeeffff5544ff"
counter_str := fmt.Sprintf("0x%s", raw_data[22:24])
counter, _ := strconv.ParseInt(counter_str, 0, 64)
out_json.HB_ButtonCounter = counter

battery_str := fmt.Sprintf("0x%s%s", raw_data[20:22], raw_data[18:20])

battery, _ := strconv.ParseInt(battery_str, 0, 64)
out_json.HB_Battery = battery

out_json.TX_power = fmt.Sprintf("0x%s", "4")

out_json.Beacon_type = "hb_button"
out_json.HB_ButtonMode = "presence_button"
}
}

return out_json
}

+ 0
- 165
internal/pkg/mqttclient/location.go Ver ficheiro

@@ -1,165 +0,0 @@
package mqttclient

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

"github.com/AFASystems/presence/internal/pkg/model"
"github.com/AFASystems/presence/internal/pkg/persistence"
"github.com/yosssi/gmq/mqtt"
"github.com/yosssi/gmq/mqtt/client"
)

func getLikelyLocations(settings *model.SettingsVal, ctx *model.AppContext, cl *client.Client) {
ctx.HTTPResults.HTTPResultsLock.Lock()
defer ctx.HTTPResults.HTTPResultsLock.Unlock()
ctx.HTTPResults.HTTPResults = model.HTTPLocationsList{Beacons: []model.HTTPLocation{}}

shouldPersist := false

for id, beacon := range ctx.Beacons.Beacons {
if len(beacon.Beacon_metrics) == 0 {
continue
}

if isExpired(&beacon, settings) {
handleExpiredBeacon(&beacon, cl, ctx)
continue
}

best := calculateBestLocation(&beacon)
updateBeaconState(&beacon, best, settings, ctx, cl)

appendHTTPResult(ctx, beacon, best)
ctx.Beacons.Beacons[id] = beacon
shouldPersist = true
}

if shouldPersist {
persistence.PersistBeacons(&ctx.Beacons)
}
}

func isExpired(b *model.Beacon, s *model.SettingsVal) bool {
return time.Now().Unix()-b.Beacon_metrics[len(b.Beacon_metrics)-1].Timestamp > s.Last_seen_threshold
}

func handleExpiredBeacon(b *model.Beacon, cl *client.Client, ctx *model.AppContext) {
if b.Expired_location == "expired" {
return
}
b.Expired_location = "expired"
msg := model.Message{
Email: b.Previous_confident_location,
Username: b.Name,
Message: "expired",
}
data, _ := json.Marshal(msg)
log.Println(string(data))
ctx.Broadcast <- msg
}

func calculateBestLocation(b *model.Beacon) model.BestLocation {
locScores := map[string]float64{}
for _, m := range b.Beacon_metrics {
score := 1.5 + 0.75*(1.0-(float64(m.Rssi)/-100.0))
locScores[m.Location] += score
}
bestName, bestScore := "", 0.0
for name, score := range locScores {
if score > bestScore {
bestName, bestScore = name, score
}
}
last := b.Beacon_metrics[len(b.Beacon_metrics)-1]
return model.BestLocation{Name: bestName, Distance: last.Distance, Last_seen: last.Timestamp}
}

func updateBeaconState(b *model.Beacon, best model.BestLocation, s *model.SettingsVal, ctx *model.AppContext, cl *client.Client) {
updateLocationHistory(b, best.Name)
updateConfidence(b, best.Name, s)

if locationChanged(b, best, s) {
publishLocationChange(b, best, cl)
b.Location_confidence = 0
b.Previous_confident_location = best.Name
}
}

func updateLocationHistory(b *model.Beacon, loc string) {
b.Location_history = append(b.Location_history, loc)
if len(b.Location_history) > 10 {
b.Location_history = b.Location_history[1:]
}
}

func updateConfidence(b *model.Beacon, loc string, s *model.SettingsVal) {
counts := map[string]int{}
for _, l := range b.Location_history {
counts[l]++
}

maxCount, mostCommon := 0, ""
for l, c := range counts {
if c > maxCount {
maxCount, mostCommon = c, l
}
}

if maxCount >= 7 {
if mostCommon == b.Previous_confident_location {
b.Location_confidence++
} else {
b.Location_confidence = 1
b.Previous_confident_location = mostCommon
}
}
}

func locationChanged(b *model.Beacon, best model.BestLocation, s *model.SettingsVal) bool {
return (b.Location_confidence == s.Location_confidence &&
b.Previous_confident_location != best.Name) ||
b.Expired_location == "expired"
}

func publishLocationChange(b *model.Beacon, best model.BestLocation, cl *client.Client) {
location := best.Name
if b.Expired_location == "expired" {
location = "expired"
}

js, err := json.Marshal(model.LocationChange{
Beacon_ref: *b,
Name: b.Name,
Previous_location: b.Previous_confident_location,
New_location: location,
Timestamp: time.Now().Unix(),
})
if err != nil {
return
}

err = cl.Publish(&client.PublishOptions{
QoS: mqtt.QoS1,
TopicName: []byte("afa-systems/presence/changes"),
Message: js,
})
if err != nil {
log.Printf("mqtt publish error: %v", err)
}
}

func appendHTTPResult(ctx *model.AppContext, b model.Beacon, best model.BestLocation) {
ctx.HTTPResults.HTTPResultsLock.Lock()
defer ctx.HTTPResults.HTTPResultsLock.Unlock()

r := model.HTTPLocation{
Name: b.Name,
Beacon_id: b.Beacon_id,
Location: best.Name,
Distance: best.Distance,
Last_seen: best.Last_seen,
}
ctx.HTTPResults.HTTPResults.Beacons = append(ctx.HTTPResults.HTTPResults.Beacons, r)
}

+ 0
- 62
internal/pkg/mqttclient/processor.go Ver ficheiro

@@ -1,62 +0,0 @@
package mqttclient

import (
"fmt"
"log"
"time"

"github.com/AFASystems/presence/internal/pkg/model"
"github.com/AFASystems/presence/internal/pkg/persistence"
"github.com/boltdb/bolt"
"github.com/yosssi/gmq/mqtt/client"
)

func IncomingMQTTProcessor(updateInterval time.Duration, cl *client.Client, db *bolt.DB, ctx *model.AppContext) chan<- model.Incoming_json {
ch := make(chan model.Incoming_json, 2000)
persistence.CreateBucketIfNotExists(db)

ticker := time.NewTicker(updateInterval)
go runProcessor(ticker, cl, ch, ctx)

return ch
}

func runProcessor(ticker *time.Ticker, cl *client.Client, ch <-chan model.Incoming_json, ctx *model.AppContext) {
for {
select {
case <-ticker.C:
getLikelyLocations(&ctx.Settings.Settings, ctx, cl)
case incoming := <-ch:
ProcessIncoming(incoming, cl, ctx)
}
}
}

func ProcessIncoming(incoming model.Incoming_json, cl *client.Client, ctx *model.AppContext) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()

incoming = IncomingBeaconFilter(incoming)
id := GetBeaconID(incoming)
now := time.Now().Unix()

beacons := &ctx.Beacons

beacons.Lock.Lock()
defer beacons.Lock.Unlock()

latestList := &ctx.LatestList
settings := &ctx.Settings.Settings

beacon, ok := beacons.Beacons[id]
if !ok {
updateLatestList(incoming, now, latestList)
return
}
fmt.Println("updating beacon data")
updateBeaconData(&beacon, incoming, now, cl, settings)
beacons.Beacons[beacon.Beacon_id] = beacon
}

+ 0
- 9
test/README.md Ver ficheiro

@@ -1,9 +0,0 @@
# `/test`

Additional external test apps and test data. Feel free to structure the `/test` directory anyway you want. For bigger projects it makes sense to have a data subdirectory. For example, you can have `/test/data` or `/test/testdata` if you need Go to ignore what's in that directory. Note that Go will also ignore directories or files that begin with "." or "_", so you have more flexibility in terms of how you name your test data directory.

Examples:

* https://github.com/openshift/origin/tree/master/test (test data is in the `/testdata` subdirectory)



+ 0
- 160
test/httpserver_test/httpserver_test.go Ver ficheiro

@@ -1,160 +0,0 @@
package httpservertest_test

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"sync"
"testing"

"github.com/AFASystems/presence/internal/pkg/httpserver"
"github.com/AFASystems/presence/internal/pkg/model"
"github.com/boltdb/bolt"
"github.com/gorilla/mux"
)

// Functions beaconsAddHandler, beaconsListHandler, beaconsDeleteHandler
func TestBeaconCRUD(t *testing.T) {
tmpfile, _ := os.CreateTemp("", "testdb-*.db")
defer os.Remove(tmpfile.Name())

db, err := bolt.Open(tmpfile.Name(), 0600, nil)
if err != nil {
t.Fatal(err)
}
model.Db = db

ctx := model.AppContext{
Beacons: model.BeaconsList{
Beacons: make(map[string]model.Beacon),
Lock: sync.RWMutex{},
},
ButtonsList: make(map[string]model.Button),
}

b := model.Beacon{Name: "B1", Beacon_id: "1"}
body, err := json.Marshal(b)

if err != nil {
t.Fatal(err)
}

req := httptest.NewRequest("POST", "/api/beacons", bytes.NewReader(body))
w := httptest.NewRecorder()

httpserver.BeaconsAddHandler(&ctx.Beacons)(w, req)

if w.Code != http.StatusOK {
t.Fatalf("create failed: %d", w.Code)
}

fmt.Println("--------------------------------------------------------------")

req = httptest.NewRequest("GET", "/api/beacons", nil)
w = httptest.NewRecorder()

httpserver.BeaconsListHandler(&ctx.Beacons)(w, req)

fmt.Println("Status:", w.Code)
fmt.Println("Body:", w.Body.String())

fmt.Println("--------------------------------------------------------------")

newB := model.Beacon{Name: "B2", Beacon_id: "2"}
newBody, err := json.Marshal(newB)

if err != nil {
t.Fatal(err)
}

req = httptest.NewRequest("PUT", "/api/beacons", bytes.NewReader(newBody))
w = httptest.NewRecorder()

httpserver.BeaconsAddHandler(&ctx.Beacons)(w, req)

if w.Code != http.StatusOK {
t.Fatalf("create failed: %d", w.Code)
}

req = httptest.NewRequest("GET", "/api/beacons", nil)
w = httptest.NewRecorder()

httpserver.BeaconsListHandler(&ctx.Beacons)(w, req)

fmt.Println("Status:", w.Code)
fmt.Println("Body:", w.Body.String())

fmt.Println("--------------------------------------------------------------")

req = httptest.NewRequest("DELETE", "/api/beacons/1", nil)
req = mux.SetURLVars(req, map[string]string{"beacon_id": "1"})

w = httptest.NewRecorder()

httpserver.BeaconsDeleteHandler(&ctx.Beacons, ctx.ButtonsList)(w, req)

fmt.Println("Status: ", w.Code)

fmt.Println("--------------------------------------------------------------")

req = httptest.NewRequest("GET", "/api/beacons", nil)
w = httptest.NewRecorder()

httpserver.BeaconsListHandler(&ctx.Beacons)(w, req)

fmt.Println("Status:", w.Code)
fmt.Println("Body:", w.Body.String())

fmt.Println("--------------------------------------------------------------")
}

func TestSettingsCRUD(t *testing.T) {
tmpfile, _ := os.CreateTemp("", "testdb-*.db")
defer os.Remove(tmpfile.Name())

db, err := bolt.Open(tmpfile.Name(), 0600, nil)
if err != nil {
t.Fatal(err)
}
model.Db = db

ctx := model.AppContext{
Settings: model.Settings{},
}

settings := model.Settings{
Location_confidence: 10,
Last_seen_threshold: 10,
Beacon_metrics_size: 10,
HA_send_interval: 10,
HA_send_changes_only: true,
}

body, err := json.Marshal(settings)
if err != nil {
t.Fatal(err)
}

req := httptest.NewRequest("POST", "/api/settings", bytes.NewReader(body))
w := httptest.NewRecorder()

httpserver.SettingsEditHandler(&ctx.Settings)(w, req)

fmt.Println("status: ", w.Code)
if w.Code != http.StatusOK {
t.Fatalf("create failed: %d", w.Code)
}

fmt.Println("--------------------------------------------------------------")

req = httptest.NewRequest("GET", "/api/settings", nil)
w = httptest.NewRecorder()

httpserver.SettingsListHandler(&ctx.Settings)(w, req)

fmt.Println("Status:", w.Code)
fmt.Println("Body:", w.Body.String())
}

+ 0
- 46
test/mqtt_test/mqtt_test.go Ver ficheiro

@@ -1,46 +0,0 @@
package mqtt_test

import (
"os"
"testing"
"time"

"github.com/AFASystems/presence/internal/pkg/model"
"github.com/AFASystems/presence/internal/pkg/mqttclient"
"github.com/AFASystems/presence/internal/pkg/persistence"
"github.com/boltdb/bolt"
)

func TestIncomingMQTTProcessor(t *testing.T) {
ctx := &model.AppContext{
Beacons: model.BeaconsList{Beacons: make(map[string]model.Beacon)},
Settings: model.Settings{
Last_seen_threshold: 10,
Location_confidence: 3,
},
}

tmpfile, _ := os.CreateTemp("", "testdb-*.db")
defer os.Remove(tmpfile.Name())

db, err := bolt.Open(tmpfile.Name(), 0600, nil)
if err != nil {
t.Fatal(err)
}
model.Db = db

persistence.LoadState(model.Db, ctx)

ch := mqttclient.IncomingMQTTProcessor(20*time.Millisecond, nil, model.Db, ctx)
msg := model.Incoming_json{MAC: "15:02:31", Hostname: "testHost", RSSI: -55}
ch <- msg

time.Sleep(100 * time.Millisecond)

ctx.Beacons.Lock.RLock()
defer ctx.Beacons.Lock.RUnlock()

if len(ctx.LatestList.LatestList) == 0 {
t.Fatal("latest list map to update")
}
}

+ 0
- 900
test/node-red-integration-tests/apitest.json
A apresentação das diferenças no ficheiro foi suprimida por ser demasiado grande
Ver ficheiro


Carregando…
Cancelar
Guardar