package handler import ( "context" "fmt" "io/ioutil" "os" "strconv" "time" "github.com/micro/micro/v3/service/errors" "github.com/robfig/cron/v3" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "github.com/micro/micro/v3/service/config" log "github.com/micro/micro/v3/service/logger" evchargers "github.com/micro/services/evchargers/proto" ) const ( defaultDistance = int64(5000) // 5km ) var ( sphereIndexVersion = int32(3) ) type Evchargers struct { conf conf mdb *mongo.Client } type conf struct { MongoHost string `json:"mongo_host"` CaCrt string `json:"ca_crt"` OCMKey string `json:"ocm_key"` } func New() *Evchargers { val, err := config.Get("micro.evchargers") if err != nil { log.Fatalf("Failed to load config") } var conf conf if err := val.Scan(&conf); err != nil { log.Fatalf("Failed to load config") } if len(conf.MongoHost) == 0 { log.Fatalf("Missing mongodb host") } if len(conf.CaCrt) > 0 { // write the cert to file if err := ioutil.WriteFile(os.TempDir()+"/mongo.crt", []byte(conf.CaCrt), 0644); err != nil { log.Fatalf("Failed to write crt file for mongodb %s", err) } } opts := []*options.ClientOptions{options.Client().ApplyURI(conf.MongoHost)} ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() client, err := mongo.Connect(ctx, opts...) if err != nil { log.Fatalf("Failed to connect to mongo db %s", err) } ctx2, cancel2 := context.WithTimeout(context.Background(), 10*time.Second) defer cancel2() if err := client.Ping(ctx2, nil); err != nil { log.Fatalf("Failed to ping mongo db %s", err) } // make sure the indexes are set up _, err = client.Database("ocm").Collection("poi").Indexes().CreateMany(context.Background(), []mongo.IndexModel{ { // bounding box queries Keys: bson.D{{"SpatialPosition.coordinates", "2dsphere"}}, Options: &options.IndexOptions{ SphereVersion: &sphereIndexVersion, }, }, { // distance queries Keys: bson.D{{"SpatialPosition", "2dsphere"}}, Options: &options.IndexOptions{ SphereVersion: &sphereIndexVersion, }, }, { Keys: bson.M{"DateCreated": -1}, }, { Keys: bson.M{"DateLastStatusUpdate": -1}, }, { Keys: bson.M{"ID": -1}, }, }) if err != nil { log.Fatalf("Failed to craete indexes %s", err) } ev := &Evchargers{conf: conf, mdb: client} if len(conf.OCMKey) > 0 { c := cron.New() // 4am every Sunday for refresh c.AddFunc("0 4 * * 0", ev.refreshDataFromSource) c.Start() } return ev } func (e *Evchargers) Search(ctx context.Context, request *evchargers.SearchRequest, response *evchargers.SearchResponse) error { addFilter := func(filters bson.D, key, op string, in []string) bson.D { vals := bson.A{} for _, v := range in { if v == "" { continue } r, _ := strconv.Atoi(v) vals = append(vals, r) } if len(vals) == 0 { return filters } filters = append(filters, bson.E{key, bson.D{{op, vals}}}) return filters } filters := bson.D{} if request.Location != nil { distance := defaultDistance if request.Distance > 0 { distance = request.Distance } filters = append(filters, bson.E{"SpatialPosition", bson.M{"$nearSphere": bson.M{"$geometry": bson.M{ "type": "Point", "coordinates": []float64{float64(request.Location.Longitude), float64(request.Location.Latitude)}, }, "$maxDistance": distance, }, }}) } else if request.Box != nil && request.Box.BottomLeft != nil { filters = append(filters, bson.E{"SpatialPosition.coordinates", bson.M{"$geoWithin": bson.M{"$box": bson.A{ []float32{request.Box.BottomLeft.Longitude, request.Box.BottomLeft.Latitude}, []float32{request.Box.TopRight.Longitude, request.Box.TopRight.Latitude}, }}}}) } if len(request.CountryId) > 0 { i, _ := strconv.Atoi(request.CountryId) filters = append(filters, bson.E{"AddressInfo.CountryID", i}) } if len(request.ConnectionTypes) > 0 { filters = addFilter(filters, "Connections.ConnectionTypeID", "$in", request.ConnectionTypes) } if len(request.Levels) > 0 { filters = addFilter(filters, "Connections.LevelID", "$in", request.Levels) } if request.MinPower > 0 { filters = append(filters, bson.E{"Connections.PowerKW", bson.D{{"$gte", request.MinPower}}}) } if len(request.Operators) > 0 { filters = addFilter(filters, "OperatorID", "$in", request.Operators) } if len(request.UsageTypes) > 0 { filters = addFilter(filters, "UsageTypeID", "$in", request.UsageTypes) } maxLim := int64(100) max := options.FindOptions{ Limit: &maxLim, } if request.MaxResults > 0 { max.Limit = &request.MaxResults } crs, err := e.mdb.Database("ocm").Collection("poi").Find(ctx, filters, &max) if err != nil { log.Errorf("Error querying %s", err) return errors.InternalServerError("evchargers.search", "Failed to query ev chargers") } defer crs.Close(ctx) for crs.Next(ctx) { var result Poi if err := crs.Decode(&result); err != nil { log.Errorf("Error decoding result %s", err) return errors.InternalServerError("evchargers.search", "Failed to query ev chargers") } poi := &evchargers.Poi{ Id: strconv.Itoa(int(result.ID)), DataProviderId: strconv.Itoa(int(result.DataProviderID)), OperatorId: strconv.Itoa(int(result.OperatorID)), UsageTypeId: strconv.Itoa(int(result.UsageTypeID)), Address: &evchargers.Address{ Location: &evchargers.Coordinates{ Latitude: float32(result.Address.Latitude), Longitude: float32(result.Address.Longitude), }, Title: result.Address.Title, AddressLine_1: result.Address.AddressLine1, AddressLine_2: result.Address.AddressLine2, Town: result.Address.Town, StateOrProvince: result.Address.StateOrProvince, AccessComments: result.Address.AccessComments, Postcode: result.Address.Postcode, CountryId: strconv.Itoa(int(result.Address.CountryID)), LatLng: fmt.Sprintf("%v, %v", result.Address.Latitude, result.Address.Longitude), }, Connections: marshalConnections(result.Connections), NumPoints: int64(result.NumberOfPoints), Cost: result.Cost, } if true { // verbose poi.Operator = marshalOperator(result.OperatorInfo) poi.UsageType = marshalUsageType(result.UsageType) poi.Address.Country = marshalCountry(result.Address.Country) } response.Pois = append(response.Pois, poi) } return nil } func marshalCountry(in Country) *evchargers.Country { return &evchargers.Country{ Id: strconv.Itoa(int(in.ID)), Title: in.Title, IsoCode: in.ISOCode, ContinentCode: in.ContinentCode, } } func marshalConnections(in []Connection) []*evchargers.Connection { res := make([]*evchargers.Connection, len(in)) for i, v := range in { res[i] = &evchargers.Connection{ ConnectionTypeId: strconv.Itoa(int(v.TypeID)), ConnectionType: &evchargers.ConnectionType{ Id: strconv.Itoa(int(v.Type.ID)), Title: v.Type.Title, FormalName: v.Type.FormalName, IsDiscontinued: v.Type.IsDiscontinued, IsObsolete: v.Type.IsObsolete, }, Reference: v.Reference, LevelId: strconv.Itoa(int(v.LevelID)), Level: &evchargers.ChargerType{ Id: strconv.Itoa(int(v.Level.ID)), Title: v.Level.Title, Comments: v.Level.Comments, IsFastChargeCapable: v.Level.IsFastChargeCapable, }, Amps: float32(v.Amps), Voltage: float32(v.Voltage), Power: float32(v.Power), Current: strconv.Itoa(int(v.CurrentTypeID)), } } return res } func marshalDataProvider(in DataProvider) *evchargers.DataProvider { return &evchargers.DataProvider{ Id: strconv.Itoa(int(in.ID)), Title: in.Title, Website: in.WebsiteURL, Comments: in.Comments, DataProviderStatusType: marshalDataProviderStatus(in.DataProviderStatus), License: in.License, } } func marshalDataProviderStatus(in DataProviderStatus) *evchargers.DataProviderStatusType { return &evchargers.DataProviderStatusType{ Id: strconv.Itoa(int(in.ID)), Title: in.Title, IsProviderEnabled: in.IsProviderEnabled, } } func marshalOperator(in Operator) *evchargers.Operator { return &evchargers.Operator{ Id: strconv.Itoa(int(in.ID)), Title: in.Title, Website: in.WebsiteURL, Comments: in.Comments, IsPrivateIndividual: in.IsPrivateIndividual, ContactEmail: in.ContactEmail, PhonePrimary: in.PhonePrimary, PhoneSecondary: in.PhoneSecondary, FaultReportEmail: in.FaultReportEmail, } } func marshalUsageType(in UsageType) *evchargers.UsageType { return &evchargers.UsageType{ Id: strconv.Itoa(int(in.ID)), Title: in.Title, IsPayAtLocation: in.IsPayAtLocation, IsMembershipRequired: in.IsMembershipRequired, IsAccessKeyRequired: in.IsAccessKeyRequired, } } func marshalCheckinStatusType(in CheckinStatusType) *evchargers.CheckinStatusType { return &evchargers.CheckinStatusType{ Id: strconv.Itoa(int(in.ID)), Title: in.Title, IsPositive: in.IsPositive, IsAutomated: in.IsAutomatedCheckin, } } func marshalUserCommentType(in UserCommentType) *evchargers.UserCommentType { return &evchargers.UserCommentType{ Id: strconv.Itoa(int(in.ID)), Title: in.Title, } } func marshalStatusType(in StatusType) *evchargers.StatusType { return &evchargers.StatusType{ Id: strconv.Itoa(int(in.ID)), Title: in.Title, IsOperational: in.IsOperational, } } func marshalCurrentType(in CurrentType) *evchargers.CurrentType { return &evchargers.CurrentType{ Id: strconv.Itoa(int(in.ID)), Title: in.Title, Description: in.Description, } } func marshalConnectionType(in ConnectionType) *evchargers.ConnectionType { return &evchargers.ConnectionType{ Id: strconv.Itoa(int(in.ID)), Title: in.Title, FormalName: in.FormalName, IsDiscontinued: in.IsDiscontinued, IsObsolete: in.IsObsolete, } } func marshalChargerType(in ChargerType) *evchargers.ChargerType { return &evchargers.ChargerType{ Id: strconv.Itoa(int(in.ID)), Title: in.Title, Comments: in.Comments, IsFastChargeCapable: in.IsFastChargeCapable, } } func marshalSubmissionStatusType(in SubmissionStatusType) *evchargers.SubmissionStatusType { return &evchargers.SubmissionStatusType{ Id: strconv.Itoa(int(in.ID)), Title: in.Title, IsLive: in.IsLive, } } func (e *Evchargers) ReferenceData(ctx context.Context, request *evchargers.ReferenceDataRequest, response *evchargers.ReferenceDataResponse) error { res := e.mdb.Database("ocm").Collection("reference").FindOne(ctx, bson.D{}) if res.Err() != nil { log.Errorf("Error retrieving ref data %s", res.Err()) return errors.InternalServerError("evchargers.referencedata", "Error retrieving reference data") } var r ReferenceData if err := res.Decode(&r); err != nil { log.Errorf("Error decoding ref data %s", err) return errors.InternalServerError("evchargers.referencedata", "Error retrieving reference data") } dps := make([]*evchargers.DataProvider, len(r.DataProviders)) for i, dp := range r.DataProviders { dps[i] = marshalDataProvider(dp) } response.DataProviders = dps cs := make([]*evchargers.Country, len(r.Countries)) for i, c := range r.Countries { cs[i] = marshalCountry(c) } response.Countries = cs cst := make([]*evchargers.CheckinStatusType, len(r.CheckinStatusTypes)) for i, v := range r.CheckinStatusTypes { cst[i] = marshalCheckinStatusType(v) } response.CheckinStatusTypes = cst uct := make([]*evchargers.UserCommentType, len(r.UserCommentTypes)) for i, v := range r.UserCommentTypes { uct[i] = marshalUserCommentType(v) } response.UserCommentTypes = uct st := make([]*evchargers.StatusType, len(r.StatusTypes)) for i, v := range r.StatusTypes { st[i] = marshalStatusType(v) } response.StatusTypes = st ut := make([]*evchargers.UsageType, len(r.UsageTypes)) for i, v := range r.UsageTypes { ut[i] = marshalUsageType(v) } response.UsageTypes = ut ct := make([]*evchargers.CurrentType, len(r.CurrentTypes)) for i, v := range r.CurrentTypes { ct[i] = marshalCurrentType(v) } response.CurrentTypes = ct connt := make([]*evchargers.ConnectionType, len(r.ConnectionTypes)) for i, v := range r.ConnectionTypes { connt[i] = marshalConnectionType(v) } response.ConnectionTypes = connt chrgt := make([]*evchargers.ChargerType, len(r.ChargerTypes)) for i, v := range r.ChargerTypes { chrgt[i] = marshalChargerType(v) } response.ChargerTypes = chrgt ops := make([]*evchargers.Operator, len(r.Operators)) for i, v := range r.Operators { ops[i] = marshalOperator(v) } response.Operators = ops sst := make([]*evchargers.SubmissionStatusType, len(r.SubmissionStatusTypes)) for i, v := range r.SubmissionStatusTypes { sst[i] = marshalSubmissionStatusType(v) } response.SubmissionStatusTypes = sst return nil }