| @@ -24,4 +24,6 @@ cmd/presenSe/presence.db | |||||
| # Dependency directories (remove the comment below to include it) | # Dependency directories (remove the comment below to include it) | ||||
| vendor/ | vendor/ | ||||
| volumes/node-red/ | volumes/node-red/ | ||||
| main | |||||
| main | |||||
| *.sh | |||||
| @@ -13,4 +13,9 @@ | |||||
| # create topic alertBeacons | # create topic alertBeacons | ||||
| /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:29092 \ | /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:29092 \ | ||||
| --create --if-not-exists --topic alertbeacons \ | --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 | --partitions 1 --replication-factor 1 | ||||
| @@ -7,15 +7,11 @@ import ( | |||||
| "encoding/hex" | "encoding/hex" | ||||
| "encoding/json" | "encoding/json" | ||||
| "fmt" | "fmt" | ||||
| "math" | |||||
| "strconv" | |||||
| "strings" | "strings" | ||||
| "time" | |||||
| "github.com/AFASystems/presence/internal/pkg/config" | "github.com/AFASystems/presence/internal/pkg/config" | ||||
| "github.com/AFASystems/presence/internal/pkg/kafkaclient" | "github.com/AFASystems/presence/internal/pkg/kafkaclient" | ||||
| "github.com/AFASystems/presence/internal/pkg/model" | "github.com/AFASystems/presence/internal/pkg/model" | ||||
| "github.com/AFASystems/presence/internal/pkg/mqttclient" | |||||
| "github.com/segmentio/kafka-go" | "github.com/segmentio/kafka-go" | ||||
| ) | ) | ||||
| @@ -27,11 +23,11 @@ func main() { | |||||
| }, | }, | ||||
| Settings: model.Settings{ | Settings: model.Settings{ | ||||
| Settings: model.SettingsVal{ | 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{ | BeaconEvents: model.BeaconEventList{ | ||||
| @@ -54,25 +50,11 @@ func main() { | |||||
| defer alertWriter.Close() | defer alertWriter.Close() | ||||
| fmt.Println("Decoder initialized, subscribed to Kafka topics") | 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) | 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(rawReader, chRaw) | ||||
| go kafkaclient.Consume(apiReader, chApi) | go kafkaclient.Consume(apiReader, chApi) | ||||
| // go kafkaclient.Consume(latestReader, chLatest) | |||||
| // go kafkaclient.Consume(settingsReader, chSettings) | |||||
| for { | for { | ||||
| select { | select { | ||||
| @@ -81,39 +63,32 @@ func main() { | |||||
| case msg := <-chApi: | case msg := <-chApi: | ||||
| switch msg.Method { | switch msg.Method { | ||||
| case "POST": | case "POST": | ||||
| id := msg.Beacon.Beacon_id | |||||
| id := msg.Beacon.ID | |||||
| appCtx.BeaconsLookup[id] = struct{}{} | appCtx.BeaconsLookup[id] = struct{}{} | ||||
| case "DELETE": | case "DELETE": | ||||
| fmt.Println("Incoming delete message") | 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] | _, ok := ctx.BeaconsLookup[id] | ||||
| if !ok { | if !ok { | ||||
| return | return | ||||
| } | } | ||||
| err := decodeBeacon(incoming, ctx, writer) | |||||
| err := decodeBeacon(adv, ctx, writer) | |||||
| if err != nil { | if err != nil { | ||||
| fmt.Println("error in decoding") | fmt.Println("error in decoding") | ||||
| return | 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 == "" { | if beacon == "" { | ||||
| return nil // How to return error?, do I even need to return error | 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]] | ad := b[r[0]:r[1]] | ||||
| if checkIngics(ad) { | if checkIngics(ad) { | ||||
| event = parseIngicsState(ad) | event = parseIngicsState(ad) | ||||
| event.Id = id | |||||
| event.ID = id | |||||
| event.Name = id | event.Name = id | ||||
| break | break | ||||
| } else if checkEddystoneTLM(ad) { | } else if checkEddystoneTLM(ad) { | ||||
| event = parseEddystoneState(ad) | event = parseEddystoneState(ad) | ||||
| event.Id = id | |||||
| event.ID = id | |||||
| event.Name = id | event.Name = id | ||||
| break | break | ||||
| } else if checkMinewB7(ad) { | } 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] | prevEvent, ok := ctx.BeaconEvents.Beacons[id] | ||||
| ctx.BeaconEvents.Beacons[id] = event | ctx.BeaconEvents.Beacons[id] = event | ||||
| if ok && bytes.Equal(prevEvent.Hash(), event.Hash()) { | if ok && bytes.Equal(prevEvent.Hash(), event.Hash()) { | ||||
| @@ -246,45 +221,3 @@ func ParseADFast(b []byte) [][2]int { | |||||
| return res | 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 | |||||
| } | |||||
| @@ -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 | |||||
| } | |||||
| @@ -124,7 +124,7 @@ func beaconsAddHandler(writer *kafka.Writer) http.HandlerFunc { | |||||
| fmt.Println("sending POST message") | 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) | http.Error(w, "name and beacon_id cannot be blank", 400) | ||||
| return | return | ||||
| } | } | ||||
| @@ -192,7 +192,7 @@ func settingsEditHandler(writer *kafka.Writer) http.HandlerFunc { | |||||
| } | } | ||||
| func settingsCheck(settings model.SettingsVal) bool { | 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 | return false | ||||
| } | } | ||||
| @@ -30,17 +30,18 @@ func MqttHandler(writer *kafka.Writer, topicName []byte, message []byte) { | |||||
| if reading.Type == "Gateway" { | if reading.Type == "Gateway" { | ||||
| continue | 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 { | if err != nil { | ||||
| fmt.Println("Error in marshaling: ", err) | fmt.Println("Error in marshaling: ", err) | ||||
| break | |||||
| } | } | ||||
| msg := kafka.Message{ | msg := kafka.Message{ | ||||
| @@ -65,14 +66,14 @@ func MqttHandler(writer *kafka.Writer, topicName []byte, message []byte) { | |||||
| rawdata := s[4] | rawdata := s[4] | ||||
| buttonCounter := parseButtonState(rawdata) | buttonCounter := parseButtonState(rawdata) | ||||
| if buttonCounter > 0 { | if buttonCounter > 0 { | ||||
| incoming := model.Incoming_json{} | |||||
| adv := model.BeaconAdvertisement{} | |||||
| i, _ := strconv.ParseInt(s[3], 10, 64) | 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") | read_line := strings.TrimRight(string(s[5]), "\r\n") | ||||
| it, err33 := strconv.Atoi(read_line) | it, err33 := strconv.Atoi(read_line) | ||||
| @@ -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) | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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 | |||||
| @@ -7,7 +7,7 @@ import ( | |||||
| func (b BeaconEvent) Hash() []byte { | func (b BeaconEvent) Hash() []byte { | ||||
| rBatt := (b.Battery / 10) * 10 | 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 := sha256.New() | ||||
| h.Write([]byte(c)) | h.Write([]byte(c)) | ||||
| @@ -9,11 +9,13 @@ import ( | |||||
| // Settings defines configuration parameters for presence detection behavior. | // Settings defines configuration parameters for presence detection behavior. | ||||
| type SettingsVal struct { | 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 { | type Settings struct { | ||||
| @@ -21,115 +23,115 @@ type Settings struct { | |||||
| Lock sync.RWMutex | 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. | // Advertisement describes a generic beacon advertisement payload. | ||||
| type Advertisement struct { | type Advertisement struct { | ||||
| ttype string | |||||
| content string | |||||
| seen int64 | |||||
| Type string | |||||
| Content string | |||||
| Seen int64 | |||||
| } | } | ||||
| // BeaconMetric stores signal and distance data for a beacon. | // BeaconMetric stores signal and distance data for a beacon. | ||||
| type BeaconMetric struct { | type BeaconMetric struct { | ||||
| Location string | Location string | ||||
| Distance float64 | Distance float64 | ||||
| Rssi int64 | |||||
| RSSI int64 | |||||
| Timestamp int64 | Timestamp int64 | ||||
| } | } | ||||
| // Location defines a physical location and synchronization control. | // Location defines a physical location and synchronization control. | ||||
| type Location struct { | type Location struct { | ||||
| name string | |||||
| lock sync.RWMutex | |||||
| Name string | |||||
| Lock sync.RWMutex | |||||
| } | } | ||||
| // BestLocation represents the most probable location of a beacon. | // BestLocation represents the most probable location of a beacon. | ||||
| type BestLocation struct { | 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. | // HTTPLocation describes a beacon's state as served over HTTP. | ||||
| type HTTPLocation struct { | 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. | // LocationChange defines a change event for a beacon's detected location. | ||||
| type LocationChange struct { | 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. | // HAMessage represents a Home Assistant integration payload. | ||||
| type HAMessage struct { | 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. | // Beacon holds all relevant information about a tracked beacon device. | ||||
| type Beacon struct { | 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 { | type BeaconEvent struct { | ||||
| Name string | Name string | ||||
| Id string | |||||
| ID string | |||||
| Type string | Type string | ||||
| Battery uint32 | Battery uint32 | ||||
| Event int | Event int | ||||
| @@ -137,18 +139,17 @@ type BeaconEvent struct { | |||||
| // Button represents a hardware button beacon device. | // Button represents a hardware button beacon device. | ||||
| type Button struct { | 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. | // BeaconsList holds all known beacons and their synchronization lock. | ||||
| @@ -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() | |||||
| } | |||||
| @@ -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 | |||||
| } | |||||
| @@ -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) | |||||
| } | |||||
| @@ -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 | |||||
| } | |||||
| @@ -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) | |||||
| @@ -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()) | |||||
| } | |||||
| @@ -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") | |||||
| } | |||||
| } | |||||