diff --git a/.gitignore b/.gitignore index 15a7b5e..2492246 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ cmd/presenSe/presence.db # Dependency directories (remove the comment below to include it) vendor/ volumes/node-red/ -main \ No newline at end of file +main + +*.sh \ No newline at end of file diff --git a/build/init-scripts/create_topic.sh b/build/init-scripts/create_topic.sh index e5d57ca..8a14d50 100755 --- a/build/init-scripts/create_topic.sh +++ b/build/init-scripts/create_topic.sh @@ -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 \ No newline at end of file diff --git a/cmd/decoder/main.go b/cmd/decoder/main.go index adbf001..97f01d2 100644 --- a/cmd/decoder/main.go +++ b/cmd/decoder/main.go @@ -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 -} diff --git a/cmd/location/main.go b/cmd/location/main.go new file mode 100644 index 0000000..1be59a4 --- /dev/null +++ b/cmd/location/main.go @@ -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 +} diff --git a/cmd/server/main.go b/cmd/server/main.go index e1d03a0..cb2482a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -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 } diff --git a/internal/pkg/bridge/mqtthandler/mqtthandler.go b/internal/pkg/bridge/mqtthandler/mqtthandler.go index f939caa..8a524ee 100644 --- a/internal/pkg/bridge/mqtthandler/mqtthandler.go +++ b/internal/pkg/bridge/mqtthandler/mqtthandler.go @@ -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) diff --git a/internal/pkg/httpserver/server.go b/internal/pkg/httpserver/server.go deleted file mode 100644 index 39b5df1..0000000 --- a/internal/pkg/httpserver/server.go +++ /dev/null @@ -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) - } - } - } -} diff --git a/internal/pkg/httpserver/server.md b/internal/pkg/httpserver/server.md deleted file mode 100644 index d245c76..0000000 --- a/internal/pkg/httpserver/server.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/internal/pkg/model/typeMethods.go b/internal/pkg/model/typeMethods.go index 716a7b0..2c7f97c 100644 --- a/internal/pkg/model/typeMethods.go +++ b/internal/pkg/model/typeMethods.go @@ -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)) diff --git a/internal/pkg/model/types.go b/internal/pkg/model/types.go index aaad9e1..803f7d0 100644 --- a/internal/pkg/model/types.go +++ b/internal/pkg/model/types.go @@ -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. diff --git a/internal/pkg/mqttclient/beacon.go b/internal/pkg/mqttclient/beacon.go deleted file mode 100644 index 3f69614..0000000 --- a/internal/pkg/mqttclient/beacon.go +++ /dev/null @@ -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() -} diff --git a/internal/pkg/mqttclient/fillter.go b/internal/pkg/mqttclient/fillter.go deleted file mode 100644 index f6d3039..0000000 --- a/internal/pkg/mqttclient/fillter.go +++ /dev/null @@ -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 -} diff --git a/internal/pkg/mqttclient/location.go b/internal/pkg/mqttclient/location.go deleted file mode 100644 index 7101aa0..0000000 --- a/internal/pkg/mqttclient/location.go +++ /dev/null @@ -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) -} diff --git a/internal/pkg/mqttclient/processor.go b/internal/pkg/mqttclient/processor.go deleted file mode 100644 index eedb5f1..0000000 --- a/internal/pkg/mqttclient/processor.go +++ /dev/null @@ -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 -} diff --git a/test/README.md b/test/README.md deleted file mode 100644 index cdcf65f..0000000 --- a/test/README.md +++ /dev/null @@ -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) - - diff --git a/test/httpserver_test/httpserver_test.go b/test/httpserver_test/httpserver_test.go deleted file mode 100644 index 79445c3..0000000 --- a/test/httpserver_test/httpserver_test.go +++ /dev/null @@ -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()) -} diff --git a/test/mqtt_test/mqtt_test.go b/test/mqtt_test/mqtt_test.go deleted file mode 100644 index 4603e09..0000000 --- a/test/mqtt_test/mqtt_test.go +++ /dev/null @@ -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") - } -} diff --git a/test/node-red-integration-tests/apitest.json b/test/node-red-integration-tests/apitest.json deleted file mode 100644 index 992c355..0000000 --- a/test/node-red-integration-tests/apitest.json +++ /dev/null @@ -1,900 +0,0 @@ -[ - { - "id": "59310844a3cdc638", - "type": "tab", - "label": "Flow 1", - "disabled": false, - "info": "", - "env": [] - }, - { - "id": "de9de6edefcc7ee6", - "type": "group", - "z": "59310844a3cdc638", - "style": { - "stroke": "#999999", - "stroke-opacity": "1", - "fill": "none", - "fill-opacity": "1", - "label": true, - "label-position": "nw", - "color": "#a4a4a4" - }, - "nodes": [ - "883490d684612591", - "3a198c85c047a80d", - "522c9262dc81a34f", - "2f19e204e95d32b2", - "8a39a884d333343d", - "bfc307bbffd86501", - "368acb55a1372809", - "06586c5ac291f999", - "d8d3d6ee351c3fce", - "b03a9a99c8897f49", - "48eb1b4c5a48a874", - "3b832f5f2a705f87", - "1817099dc85d0c73", - "3b4d5dca9051f727", - "213a0de12ca7b387", - "9095389f88755788", - "668a57a11d34fab9", - "9fe67862caf1033d", - "c52f0e6e0e08f1ca", - "35d94840a12ca741", - "2475de26f85f96e5", - "50c9899be46f95be" - ], - "x": 74, - "y": 39, - "w": 892, - "h": 482 - }, - { - "id": "f03dded513b83b0d", - "type": "group", - "z": "59310844a3cdc638", - "style": { - "stroke": "#999999", - "stroke-opacity": "1", - "fill": "none", - "fill-opacity": "1", - "label": true, - "label-position": "nw", - "color": "#a4a4a4" - }, - "nodes": [ - "af59b1d57bce5a71", - "1a1c5a74d2fc1538", - "d846952c928cabfd", - "e8f1a6bb67aa191e", - "082bd00a6d8f4bc2", - "04d408c7b56c5004", - "9d98036262ab3bdc", - "39131f3755e4535d", - "8284044fa3903be9", - "09e2fb2b15f80b1d", - "4321534186191e5c", - "29f7ce4208592cc7" - ], - "x": 74, - "y": 539, - "w": 892, - "h": 282 - }, - { - "id": "cd2e897dda200635", - "type": "group", - "z": "59310844a3cdc638", - "style": { - "stroke": "#999999", - "stroke-opacity": "1", - "fill": "none", - "fill-opacity": "1", - "label": true, - "label-position": "nw", - "color": "#a4a4a4" - }, - "nodes": [ - "0390244642827293", - "b573b41eeb6a3021", - "f367b6f3d40ee8c4", - "6eee04a8f67e01eb" - ], - "x": 74, - "y": 839, - "w": 712, - "h": 142 - }, - { - "id": "883490d684612591", - "type": "inject", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "", - "props": [ - { - "p": "payload" - }, - { - "p": "topic", - "vt": "str" - } - ], - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": 0.1, - "topic": "", - "payload": "", - "payloadType": "date", - "x": 180, - "y": 180, - "wires": [ - [ - "522c9262dc81a34f" - ] - ] - }, - { - "id": "3a198c85c047a80d", - "type": "comment", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "Server components test | BEACONS LIST", - "info": "", - "x": 260, - "y": 80, - "wires": [] - }, - { - "id": "522c9262dc81a34f", - "type": "function", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "GET beacons", - "func": "msg.url = \"http://presense-server:1902/api/beacons\";\nmsg.method = \"GET\";\nmsg.payload = \"\";\nreturn msg;", - "outputs": 1, - "timeout": 0, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 360, - "y": 180, - "wires": [ - [ - "2f19e204e95d32b2" - ] - ] - }, - { - "id": "2f19e204e95d32b2", - "type": "http request", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "", - "method": "use", - "ret": "txt", - "paytoqs": "ignore", - "url": "", - "tls": "", - "persist": false, - "proxy": "", - "insecureHTTPParser": false, - "authType": "", - "senderr": false, - "headers": [], - "x": 550, - "y": 180, - "wires": [ - [ - "3b832f5f2a705f87" - ] - ] - }, - { - "id": "8a39a884d333343d", - "type": "comment", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "GET beacons list", - "info": "", - "x": 180, - "y": 140, - "wires": [] - }, - { - "id": "bfc307bbffd86501", - "type": "inject", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "", - "props": [ - { - "p": "payload" - }, - { - "p": "topic", - "vt": "str" - } - ], - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": 0.1, - "topic": "", - "payload": "", - "payloadType": "date", - "x": 180, - "y": 280, - "wires": [ - [ - "368acb55a1372809" - ] - ] - }, - { - "id": "368acb55a1372809", - "type": "function", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "POST beacons", - "func": "msg.url = \"http://presense-server:1902/api/beacons\";\nmsg.method = \"POST\";\nmsg.payload = {\n \"timestamp\": \"2025-07-24T15:00:00.141Z\", \n \"mac\": \"C3000057B9DA\", \n \"rssi\": -66, \n \"rawData\": \"0201060303AAFE1116AAFE20000C392500000601EA01192890\",\n \"Name\": \"Beacon1\",\n \"Beacon_id\": \"1\"\n};\nreturn msg;", - "outputs": 1, - "timeout": 0, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 360, - "y": 280, - "wires": [ - [ - "06586c5ac291f999" - ] - ] - }, - { - "id": "06586c5ac291f999", - "type": "http request", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "", - "method": "use", - "ret": "txt", - "paytoqs": "ignore", - "url": "", - "tls": "", - "persist": false, - "proxy": "", - "insecureHTTPParser": false, - "authType": "", - "senderr": false, - "headers": [], - "x": 550, - "y": 280, - "wires": [ - [ - "48eb1b4c5a48a874" - ] - ] - }, - { - "id": "d8d3d6ee351c3fce", - "type": "comment", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "POST beacons list", - "info": "", - "x": 190, - "y": 240, - "wires": [] - }, - { - "id": "b03a9a99c8897f49", - "type": "debug", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "debug 1", - "active": true, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "false", - "statusVal": "", - "statusType": "auto", - "x": 860, - "y": 180, - "wires": [] - }, - { - "id": "48eb1b4c5a48a874", - "type": "debug", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "debug 2", - "active": true, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "false", - "statusVal": "", - "statusType": "auto", - "x": 860, - "y": 280, - "wires": [] - }, - { - "id": "3b832f5f2a705f87", - "type": "json", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "", - "property": "payload", - "action": "", - "pretty": false, - "x": 710, - "y": 180, - "wires": [ - [ - "b03a9a99c8897f49" - ] - ] - }, - { - "id": "1817099dc85d0c73", - "type": "inject", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "", - "props": [ - { - "p": "payload" - }, - { - "p": "topic", - "vt": "str" - } - ], - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": 0.1, - "topic": "", - "payload": "", - "payloadType": "date", - "x": 180, - "y": 380, - "wires": [ - [ - "3b4d5dca9051f727" - ] - ] - }, - { - "id": "3b4d5dca9051f727", - "type": "function", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "PUT beacons", - "func": "msg.url = \"http://presense-server:1902/api/beacons\";\nmsg.method = \"PUT\";\nmsg.payload = {\n \"timestamp\": \"2025-07-24T15:00:00.141Z\",\n \"mac\": \"C3000056GSZW\",\n \"rssi\": -76,\n \"rawData\": \"0201060303AAFE1116AAFE20000C392500000601EA01192890\",\n \"Name\": \"Beacon2\",\n \"Beacon_id\": \"2\"\n};\nreturn msg;", - "outputs": 1, - "timeout": 0, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 360, - "y": 380, - "wires": [ - [ - "213a0de12ca7b387" - ] - ] - }, - { - "id": "213a0de12ca7b387", - "type": "http request", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "", - "method": "use", - "ret": "txt", - "paytoqs": "ignore", - "url": "", - "tls": "", - "persist": false, - "proxy": "", - "insecureHTTPParser": false, - "authType": "", - "senderr": false, - "headers": [], - "x": 550, - "y": 380, - "wires": [ - [ - "668a57a11d34fab9" - ] - ] - }, - { - "id": "9095389f88755788", - "type": "comment", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "PUT beacons list", - "info": "", - "x": 180, - "y": 340, - "wires": [] - }, - { - "id": "668a57a11d34fab9", - "type": "debug", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "debug 4", - "active": true, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "false", - "statusVal": "", - "statusType": "auto", - "x": 860, - "y": 380, - "wires": [] - }, - { - "id": "9fe67862caf1033d", - "type": "inject", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "", - "props": [ - { - "p": "payload" - }, - { - "p": "topic", - "vt": "str" - } - ], - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": 0.1, - "topic": "", - "payload": "", - "payloadType": "date", - "x": 180, - "y": 480, - "wires": [ - [ - "c52f0e6e0e08f1ca" - ] - ] - }, - { - "id": "c52f0e6e0e08f1ca", - "type": "function", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "DELETE beacons", - "func": "msg.url = \"http://presense-server:1902/api/beacons/2\";\nmsg.method = \"DELETE\";\nmsg.payload = \"\";\nreturn msg;", - "outputs": 1, - "timeout": 0, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 370, - "y": 480, - "wires": [ - [ - "35d94840a12ca741" - ] - ] - }, - { - "id": "35d94840a12ca741", - "type": "http request", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "", - "method": "use", - "ret": "txt", - "paytoqs": "ignore", - "url": "", - "tls": "", - "persist": false, - "proxy": "", - "insecureHTTPParser": false, - "authType": "", - "senderr": false, - "headers": [], - "x": 550, - "y": 480, - "wires": [ - [ - "50c9899be46f95be" - ] - ] - }, - { - "id": "2475de26f85f96e5", - "type": "comment", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "DELETE beacons list", - "info": "", - "x": 200, - "y": 440, - "wires": [] - }, - { - "id": "50c9899be46f95be", - "type": "debug", - "z": "59310844a3cdc638", - "g": "de9de6edefcc7ee6", - "name": "debug 3", - "active": true, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "false", - "statusVal": "", - "statusType": "auto", - "x": 860, - "y": 480, - "wires": [] - }, - { - "id": "af59b1d57bce5a71", - "type": "inject", - "z": "59310844a3cdc638", - "g": "f03dded513b83b0d", - "name": "", - "props": [ - { - "p": "payload" - }, - { - "p": "topic", - "vt": "str" - } - ], - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": 0.1, - "topic": "", - "payload": "", - "payloadType": "date", - "x": 180, - "y": 680, - "wires": [ - [ - "d846952c928cabfd" - ] - ] - }, - { - "id": "1a1c5a74d2fc1538", - "type": "comment", - "z": "59310844a3cdc638", - "g": "f03dded513b83b0d", - "name": "Server components test | SETTINGS", - "info": "", - "x": 240, - "y": 580, - "wires": [] - }, - { - "id": "d846952c928cabfd", - "type": "function", - "z": "59310844a3cdc638", - "g": "f03dded513b83b0d", - "name": "POST settings", - "func": "msg.url = \"http://presense-server:1902/api/settings\";\nmsg.method = \"POST\";\nmsg.payload = {\n \"location_confidence\": 10,\n \"last_seen_threshold\": 30,\n \"beacon_metrics_size\": 5,\n \"ha_send_interval\": 60,\n \"ha_send_changes_only\": true\n};\nreturn msg;", - "outputs": 1, - "timeout": 0, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 360, - "y": 680, - "wires": [ - [ - "e8f1a6bb67aa191e" - ] - ] - }, - { - "id": "e8f1a6bb67aa191e", - "type": "http request", - "z": "59310844a3cdc638", - "g": "f03dded513b83b0d", - "name": "", - "method": "use", - "ret": "txt", - "paytoqs": "ignore", - "url": "", - "tls": "", - "persist": false, - "proxy": "", - "insecureHTTPParser": false, - "authType": "", - "senderr": false, - "headers": [], - "x": 550, - "y": 680, - "wires": [ - [ - "04d408c7b56c5004" - ] - ] - }, - { - "id": "082bd00a6d8f4bc2", - "type": "comment", - "z": "59310844a3cdc638", - "g": "f03dded513b83b0d", - "name": "POST settings", - "info": "", - "x": 170, - "y": 640, - "wires": [] - }, - { - "id": "04d408c7b56c5004", - "type": "debug", - "z": "59310844a3cdc638", - "g": "f03dded513b83b0d", - "name": "debug 5", - "active": true, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "false", - "statusVal": "", - "statusType": "auto", - "x": 860, - "y": 680, - "wires": [] - }, - { - "id": "9d98036262ab3bdc", - "type": "inject", - "z": "59310844a3cdc638", - "g": "f03dded513b83b0d", - "name": "", - "props": [ - { - "p": "payload" - }, - { - "p": "topic", - "vt": "str" - } - ], - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": 0.1, - "topic": "", - "payload": "", - "payloadType": "date", - "x": 180, - "y": 780, - "wires": [ - [ - "39131f3755e4535d" - ] - ] - }, - { - "id": "39131f3755e4535d", - "type": "function", - "z": "59310844a3cdc638", - "g": "f03dded513b83b0d", - "name": "GET settings", - "func": "msg.url = \"http://presense-server:1902/api/settings\";\nmsg.method = \"GET\";\nmsg.payload = \"\";\nreturn msg;", - "outputs": 1, - "timeout": 0, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 350, - "y": 780, - "wires": [ - [ - "8284044fa3903be9" - ] - ] - }, - { - "id": "8284044fa3903be9", - "type": "http request", - "z": "59310844a3cdc638", - "g": "f03dded513b83b0d", - "name": "", - "method": "use", - "ret": "txt", - "paytoqs": "ignore", - "url": "", - "tls": "", - "persist": false, - "proxy": "", - "insecureHTTPParser": false, - "authType": "", - "senderr": false, - "headers": [], - "x": 550, - "y": 780, - "wires": [ - [ - "29f7ce4208592cc7" - ] - ] - }, - { - "id": "09e2fb2b15f80b1d", - "type": "comment", - "z": "59310844a3cdc638", - "g": "f03dded513b83b0d", - "name": "GET settings", - "info": "", - "x": 170, - "y": 740, - "wires": [] - }, - { - "id": "4321534186191e5c", - "type": "debug", - "z": "59310844a3cdc638", - "g": "f03dded513b83b0d", - "name": "debug 6", - "active": true, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "false", - "statusVal": "", - "statusType": "auto", - "x": 860, - "y": 780, - "wires": [] - }, - { - "id": "29f7ce4208592cc7", - "type": "json", - "z": "59310844a3cdc638", - "g": "f03dded513b83b0d", - "name": "", - "property": "payload", - "action": "", - "pretty": false, - "x": 710, - "y": 780, - "wires": [ - [ - "4321534186191e5c" - ] - ] - }, - { - "id": "0390244642827293", - "type": "inject", - "z": "59310844a3cdc638", - "g": "cd2e897dda200635", - "name": "", - "props": [ - { - "p": "payload" - }, - { - "p": "topic", - "vt": "str" - } - ], - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": 0.1, - "topic": "", - "payload": "", - "payloadType": "date", - "x": 180, - "y": 940, - "wires": [ - [ - "f367b6f3d40ee8c4" - ] - ] - }, - { - "id": "b573b41eeb6a3021", - "type": "comment", - "z": "59310844a3cdc638", - "g": "cd2e897dda200635", - "name": "MQTT test", - "info": "", - "x": 160, - "y": 880, - "wires": [] - }, - { - "id": "f367b6f3d40ee8c4", - "type": "function", - "z": "59310844a3cdc638", - "g": "cd2e897dda200635", - "name": "prepare msg and topic", - "func": "msg.payload = [\n { \"timestamp\": \"2025-07-24T15:00:00.161Z\", \"type\": \"Gateway\", \"mac\": \"AC233FC1DCCB\", \"nums\": 56 },\n { \"timestamp\": \"2025-07-24T15:00:00.141Z\", \"mac\": \"C3000057B9DA\", \"rssi\": -66, \"rawData\": \"0201060303AAFE1116AAFE20000C392500000601EA01192890\" },\n { \"timestamp\": \"2025-07-24T15:00:00.180Z\", \"mac\": \"2EEF56E34CF7\", \"rssi\": -52, \"rawData\": \"1EFF06000109202231FC772C59F6DD39CBE3F46A46C69105FC424C6705B6D4\" },\n { \"timestamp\": \"2025-07-24T15:00:00.200Z\", \"mac\": \"C3000057B9F5\", \"rssi\": -55, \"rawData\": \"0201060303E1FF1216E1FFA103640007FFFE0100F5B9570000C3\" },\n { \"timestamp\": \"2025-07-24T15:00:00.286Z\", \"mac\": \"2EEF56E34CF7\", \"rssi\": -64, \"rawData\": \"1EFF06000109202231FC772C59F6DD39CBE3F46A46C69105FC424C6705B6D4\" },\n { \"timestamp\": \"2025-07-24T15:00:00.293Z\", \"mac\": \"C3000057B9DD\", \"rssi\": -77, \"rawData\": \"0201060303E1FF1216E1FFA1036400050002010ADDB9570000C3\" },\n { \"timestamp\": \"2025-07-24T15:00:00.312Z\", \"mac\": \"C3000057B9DD\", \"rssi\": -77, \"rawData\": \"0201060303AAFE1516AAFE00E800112233445566778899000000000011\" },\n { \"timestamp\": \"2025-07-24T15:00:00.332Z\", \"mac\": \"C3000057B9F6\", \"rssi\": -75, \"rawData\": \"0201060303AAFE0C16AAFE10E8016D696E657700\" },\n { \"timestamp\": \"2025-07-24T15:00:00.333Z\", \"mac\": \"C300003B1E21\", \"rssi\": -72, \"rawData\": \"0201060303E1FF1116E1FFA10848211E3B0000C34D57433031\" },\n { \"timestamp\": \"2025-07-24T15:00:00.337Z\", \"mac\": \"0C063F7162D6\", \"rssi\": -59, \"rawData\": \"1EFF0600010F2022DD24BF3F2AB0BED78AE0CF7151E580A9C3562C5C16425D\" },\n { \"timestamp\": \"2025-07-24T15:00:00.338Z\", \"mac\": \"C3000057B9DC\", \"rssi\": -70, \"rawData\": \"0201060303E1FF1216E1FFA10364000700F80000DCB9570000C3\" },\n { \"timestamp\": \"2025-07-24T15:00:00.361Z\", \"mac\": \"C3000057B9DC\", \"rssi\": -79, \"rawData\": \"0201060303E1FF0E16E1FFA10864DCB9570000C34237\" },\n { \"timestamp\": \"2025-07-24T15:00:00.380Z\", \"mac\": \"D54E908B7972\", \"rssi\": -76, \"rawData\": \"020106020A001216ABFE40000A0BD50001D54E908B7972300B\" },\n { \"timestamp\": \"2025-07-24T15:00:00.387Z\", \"mac\": \"C3000057B9DC\", \"rssi\": -70, \"rawData\": \"0201060303AAFE1516AAFE00E800112233445566778899000000000010\" },\n { \"timestamp\": \"2025-07-24T15:00:00.392Z\", \"mac\": \"2EEF56E34CF7\", \"rssi\": -53, \"rawData\": \"1EFF06000109202231FC772C59F6DD39CBE3F46A46C69105FC424C6705B6D4\" },\n { \"timestamp\": \"2025-07-24T15:00:00.393Z\", \"mac\": \"E017085443A7\", \"rssi\": -68, \"rawData\": \"0201060C16E0FE2001000000000100000A094D4B20427574746F6E\" },\n { \"timestamp\": \"2025-07-24T15:00:00.401Z\", \"mac\": \"C3000057B9F6\", \"rssi\": -71, \"rawData\": \"0201060303E1FF1216E1FFA10364FFEF00F8000AF6B9570000C3\" },\n { \"timestamp\": \"2025-07-24T15:00:00.409Z\", \"mac\": \"C3000057B9DB\", \"rssi\": -66, \"rawData\": \"0201060303AAFE1516AAFE00E800112233445566778899000000000009\" },\n { \"timestamp\": \"2025-07-24T15:00:00.411Z\", \"mac\": \"C3000057B9F4\", \"rssi\": -79, \"rawData\": \"0201060303F1FF1716E2C56DB5DFFB48D2B060D0F5A71096E000000000EC64\" },\n { \"timestamp\": \"2025-07-24T15:00:00.436Z\", \"mac\": \"0C063F7162D6\", \"rssi\": -75, \"rawData\": \"1EFF0600010F2022DD24BF3F2AB0BED78AE0CF7151E580A9C3562C5C16425D\" },\n { \"timestamp\": \"2025-07-24T15:00:00.449Z\", \"mac\": \"C3000057B9D7\", \"rssi\": -69, \"rawData\": \"0201060303AAFE1116AAFE20000C1B1F0000060D6B011929E4\" },\n { \"timestamp\": \"2025-07-24T15:00:00.494Z\", \"mac\": \"2EEF56E34CF7\", \"rssi\": -53, \"rawData\": \"1EFF06000109202231FC772C59F6DD39CBE3F46A46C69105FC424C6705B6D4\" },\n { \"timestamp\": \"2025-07-24T15:00:00.522Z\", \"mac\": \"CD738844D504\", \"rssi\": -63, \"rawData\": \"07FF4C0012020003\" },\n { \"timestamp\": \"2025-07-24T15:00:00.522Z\", \"mac\": \"C3000057B9F9\", \"rssi\": -80, \"rawData\": \"0201060303AAFE0C16AAFE10E8016D696E657700\" },\n { \"timestamp\": \"2025-07-24T15:00:00.545Z\", \"mac\": \"0C063F7162D6\", \"rssi\": -69, \"rawData\": \"1EFF0600010F2022DD24BF3F2AB0BED78AE0CF7151E580A9C3562C5C16425D\" },\n { \"timestamp\": \"2025-07-24T15:00:00.573Z\", \"mac\": \"C3000057B9D3\", \"rssi\": -70, \"rawData\": \"0201060303E1FF1216E1FFA10364FFECFFFEFF06D3B9570000C3\" },\n { \"timestamp\": \"2025-07-24T15:00:00.588Z\", \"mac\": \"C3000057B9E8\", \"rssi\": -73, \"rawData\": \"0201060303AAFE0C16AAFE10E8016D696E657700\" },\n { \"timestamp\": \"2025-07-24T15:00:00.596Z\", \"mac\": \"2EEF56E34CF7\", \"rssi\": -54, \"rawData\": \"1EFF06000109202231FC772C59F6DD39CBE3F46A46C69105FC424C6705B6D4\" },\n { \"timestamp\": \"2025-07-24T15:00:00.645Z\", \"mac\": \"C3000057B9D7\", \"rssi\": -70, \"rawData\": \"0201060303AAFE0C16AAFE10E8016D696E657700\" },\n { \"timestamp\": \"2025-07-24T15:00:00.647Z\", \"mac\": \"0C063F7162D6\", \"rssi\": -58, \"rawData\": \"1EFF0600010F2022DD24BF3F2AB0BED78AE0CF7151E580A9C3562C5C16425D\" },\n { \"timestamp\": \"2025-07-24T15:00:00.704Z\", \"mac\": \"6E9836F89346\", \"rssi\": -68, \"rawData\": \"02011A020A070BFF4C0010063E1E8C2885FC\" },\n { \"timestamp\": \"2025-07-24T15:00:00.705Z\", \"mac\": \"2EEF56E34CF7\", \"rssi\": -52, \"rawData\": \"1EFF06000109202231FC772C59F6DD39CBE3F46A46C69105FC424C6705B6D4\" },\n { \"timestamp\": \"2025-07-24T15:00:00.707Z\", \"mac\": \"C300003947E2\", \"rssi\": -53, \"rawData\": \"0201060303E1FF1216E1FFA10364000700F50000E247390000C3\" },\n { \"timestamp\": \"2025-07-24T15:00:00.713Z\", \"mac\": \"C300003947C4\", \"rssi\": -81, \"rawData\": \"0201060303E1FF1216E1FFA10364001700FAFFFEC447390000C3\" },\n { \"timestamp\": \"2025-07-24T15:00:00.727Z\", \"mac\": \"C3000057B9D7\", \"rssi\": -69, \"rawData\": \"0201060303E1FF1216E1FFA10364000A0105FFFBD7B9570000C3\" }, \n { \"timestamp\": \"2025-07-24T15:00:00.752Z\", \"mac\": \"0C063F7162D6\", \"rssi\": -71, \"rawData\": \"1EFF0600010F2022DD24BF3F2AB0BED78AE0CF7151E580A9C3562C5C16425D\" },\n { \"timestamp\": \"2025-07-24T15:00:00.811Z\", \"mac\": \"2EEF56E34CF7\", \"rssi\": -54, \"rawData\": \"1EFF06000109202231FC772C59F6DD39CBE3F46A46C69105FC424C6705B6D4\" },\n { \"timestamp\": \"2025-07-24T15:00:00.828Z\", \"mac\": \"C3000057B9D4\", \"rssi\": -81, \"rawData\": \"0201060303AAFE1116AAFE20000C1B1B00002C441A01192BB0\" }, \n { \"timestamp\": \"2025-07-24T15:00:00.830Z\", \"mac\": \"C3000057B9DC\", \"rssi\": -70, \"rawData\": \"0201060303AAFE1116AAFE20000C3022000005FBF1011926E2\" }, \n { \"timestamp\": \"2025-07-24T15:00:00.832Z\", \"mac\": \"C3000057B9F4\", \"rssi\": -60, \"rawData\": \"0201060303E1FF1216E1FFA10364000001000005F4B9570000C3\" }, \n { \"timestamp\": \"2025-07-24T15:00:00.847Z\", \"mac\": \"C3000057B9E5\", \"rssi\": -83, \"rawData\": \"0201060303AAFE1116AAFE20000BEB1B000006262A01192084\" }, \n { \"timestamp\": \"2025-07-24T15:00:00.855Z\", \"mac\": \"0C063F7162D6\", \"rssi\": -70, \"rawData\": \"1EFF0600010F2022DD24BF3F2AB0BED78AE0CF7151E580A9C3562C5C16425D\" }, \n { \"timestamp\": \"2025-07-24T15:00:00.882Z\", \"mac\": \"C3000057B9D6\", \"rssi\": -68, \"rawData\": \"0201060303AAFE1516AAFE00E800112233445566778899000000000004\" }, \n { \"timestamp\": \"2025-07-24T15:00:00.906Z\", \"mac\": \"C3000057B9F5\", \"rssi\": -57, \"rawData\": \"0201060303AAFE1516AAFE00E800112233445566778899000000000035\" },\n { \"timestamp\": \"2025-07-24T15:00:00.920Z\", \"mac\": \"2EEF56E34CF7\", \"rssi\": -54, \"rawData\": \"1EFF06000109202231FC772C59F6DD39CBE3F46A46C69105FC424C6705B6D4\" }, \n { \"timestamp\": \"2025-07-24T15:00:00.931Z\", \"mac\": \"C3000057B9F5\", \"rssi\": -67, \"rawData\": \"0201060303AAFE0C16AAFE10E8016D696E657700\" }, \n { \"timestamp\": \"2025-07-24T15:00:00.963Z\", \"mac\": \"0C063F7162D6\", \"rssi\": -58, \"rawData\": \"1EFF0600010F2022DD24BF3F2AB0BED78AE0CF7151E580A9C3562C5C16425D\" }, \n { \"timestamp\": \"2025-07-24T15:00:00.975Z\", \"mac\": \"6E9836F89346\", \"rssi\": -63, \"rawData\": \"02011A020A070BFF4C0010063E1E8C2885FC\" },\n { \"timestamp\": \"2025-07-24T15:00:00.998Z\", \"mac\": \"C300003947E2\", \"rssi\": -71, \"rawData\": \"0201060303AAFE1116AAFE20000BEB1D00009704B20DB7A57C\" },\n { \"timestamp\": \"2025-07-24T15:00:01.027Z\", \"mac\": \"2EEF56E34CF7\", \"rssi\": -52, \"rawData\": \"1EFF06000109202231FC772C59F6DD39CBE3F46A46C69105FC424C6705B6D4\" }, \n { \"timestamp\": \"2025-07-24T15:00:01.055Z\", \"mac\": \"C3000057B9EA\", \"rssi\": -82, \"rawData\": \"0201060303AAFE0C16AAFE10E8016D696E657700\" }, \n { \"timestamp\": \"2025-07-24T15:00:01.056Z\", \"mac\": \"F045AEE31DB4\", \"rssi\": -71, \"rawData\": \"0201060C16E0FE2001000000000100000A094D4B20427574746F6E\" }, \n { \"timestamp\": \"2025-07-24T15:00:01.070Z\", \"mac\": \"C3000057B9D4\", \"rssi\": -82, \"rawData\": \"0201060303E1FF1216E1FFA10364000000F50002D4B9570000C3\" }, \n { \"timestamp\": \"2025-07-24T15:00:01.079Z\", \"mac\": \"C3000057B9E8\", \"rssi\": -70, \"rawData\": \"0201060303E1FF1216E1FFA10364FFFB00FAFFECE8B9570000C3\" }, \n { \"timestamp\": \"2025-07-24T15:00:01.089Z\", \"mac\": \"C3000057B9F4\", \"rssi\": -61, \"rawData\": \"0201060303AAFE1116AAFE20000C181C000005FD3901191896\" }, \n { \"timestamp\": \"2025-07-24T15:00:01.116Z\", \"mac\": \"C3000057B9DB\", \"rssi\": -72, \"rawData\": \"0201060303E1FF1216E1FFA103640002FFF4010ADBB9570000C3\" }, \n { \"timestamp\": \"2025-07-24T15:00:01.136Z\", \"mac\": \"2EEF56E34CF7\", \"rssi\": -54, \"rawData\": \"1EFF06000109202231FC772C59F6DD39CBE3F46A46C69105FC424C6705B6D4\" }\n];\n\nmsg.topic = \"publish_out/ac233fc1dccb\";\n\nreturn msg;", - "outputs": 1, - "timeout": 0, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 380, - "y": 940, - "wires": [ - [ - "6eee04a8f67e01eb" - ] - ] - }, - { - "id": "6eee04a8f67e01eb", - "type": "mqtt out", - "z": "59310844a3cdc638", - "g": "cd2e897dda200635", - "name": "", - "topic": "", - "qos": "", - "retain": "", - "respTopic": "", - "contentType": "", - "userProps": "", - "correl": "", - "expiry": "", - "broker": "109b7c239250b941", - "x": 710, - "y": 940, - "wires": [] - }, - { - "id": "109b7c239250b941", - "type": "mqtt-broker", - "name": "", - "broker": "emqx", - "port": 1883, - "clientid": "", - "autoConnect": true, - "usetls": false, - "protocolVersion": 4, - "keepalive": 60, - "cleansession": true, - "autoUnsubscribe": true, - "birthTopic": "", - "birthQos": "0", - "birthRetain": "false", - "birthPayload": "", - "birthMsg": {}, - "closeTopic": "", - "closeQos": "0", - "closeRetain": "false", - "closePayload": "", - "closeMsg": {}, - "willTopic": "", - "willQos": "0", - "willRetain": "false", - "willPayload": "", - "willMsg": {}, - "userProps": "", - "sessionExpiry": "" - } -] \ No newline at end of file