| @@ -24,4 +24,6 @@ cmd/presenSe/presence.db | |||
| # Dependency directories (remove the comment below to include it) | |||
| vendor/ | |||
| volumes/node-red/ | |||
| main | |||
| main | |||
| *.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 | |||
| @@ -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 | |||
| } | |||
| @@ -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") | |||
| 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 | |||
| } | |||
| @@ -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) | |||
| @@ -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 { | |||
| 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)) | |||
| @@ -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. | |||
| @@ -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") | |||
| } | |||
| } | |||