package api // A home charger is the wallbox a user owns, stored the way a car is: a record // of their own, listed on the Charging page, kept after the account it came from // is disconnected. It belongs to the person rather than to a car — it charges // whichever car is plugged into it, and it outlives any of them — so there is no // car relation here and no sharing: everyone sees their own chargers only. // // GET /api/home-chargers — the caller's chargers // PATCH /api/home-chargers/{id} — rename one // DELETE /api/home-chargers/{id} — forget one // // Creating one is the business of chargerproviders.go: a charger is imported // from a service the user has connected, the same move the garage makes for a // car. Nothing here writes the provider link, so a rename cannot silently break // it. import ( "encoding/json" "fmt" "net/http" "net/url" "strings" "drivervault/apiserver/internal/models" ) // homeChargerRecord is the PocketBase-facing shape of a home charger. type homeChargerRecord struct { ID string `json:"id"` Name string `json:"name"` Serial string `json:"serial"` Vendor string `json:"vendor"` Model string `json:"model"` SiteName string `json:"site_name"` PowerKw float64 `json:"power_kw"` Connector string `json:"connector"` Provider string `json:"provider"` ProviderChargerID string `json:"provider_charger_id"` Owner string `json:"owner"` Created string `json:"created"` } func (rec homeChargerRecord) toModel() models.HomeCharger { return models.HomeCharger{ ID: rec.ID, Name: rec.Name, Serial: rec.Serial, Vendor: rec.Vendor, Model: rec.Model, SiteName: rec.SiteName, PowerKw: rec.PowerKw, Connector: rec.Connector, Provider: rec.Provider, ProviderChargerID: rec.ProviderChargerID, Created: rec.Created, } } // homeChargerPayload is the write shape. The provider link is not part of it: // only the import endpoint sets that, and it adds those two fields itself. func homeChargerPayload(c models.HomeCharger) map[string]any { return map[string]any{ "name": c.Name, "serial": c.Serial, "vendor": c.Vendor, "model": c.Model, "site_name": c.SiteName, "power_kw": c.PowerKw, "connector": c.Connector, } } // listHomeChargers returns the caller's own chargers, newest first — the order // they were imported in, which is the order they were installed in often enough. func (s *Server) listHomeChargers(w http.ResponseWriter, r *http.Request) { me := s.currentUserID(r) if me == "" { writeError(w, http.StatusUnauthorized, "not authenticated") return } res, err := s.pb.List(r.Context(), colHomeChargers, url.Values{ "filter": {fmt.Sprintf("owner='%s'", me)}, "sort": {"created"}, "perPage": {"200"}, }) if err != nil { writePBError(w, err) return } var recs []homeChargerRecord if err := json.Unmarshal(res.Items, &recs); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } out := make([]models.HomeCharger, 0, len(recs)) for _, rec := range recs { out = append(out, rec.toModel()) } writeJSON(w, http.StatusOK, map[string]any{"chargers": out}) } // ownedHomeCharger fetches one charger and checks it is the caller's. It writes // the error response itself and returns ok=false when it is not. func (s *Server) ownedHomeCharger(w http.ResponseWriter, r *http.Request) (homeChargerRecord, bool) { var rec homeChargerRecord id := strings.TrimSpace(r.PathValue("id")) if id == "" { writeError(w, http.StatusBadRequest, "charger id is required") return rec, false } if err := s.pb.GetOne(r.Context(), colHomeChargers, id, &rec); err != nil { writePBError(w, err) return rec, false } // Someone else's charger is not found rather than forbidden: whether a record // exists is not this caller's business either. if rec.Owner != s.currentUserID(r) { writeError(w, http.StatusNotFound, "charger not found") return rec, false } return rec, true } // updateHomeCharger renames a charger. Only the name is editable — everything // else describes the hardware and comes from the service it was imported from. func (s *Server) updateHomeCharger(w http.ResponseWriter, r *http.Request) { rec, ok := s.ownedHomeCharger(w, r) if !ok { return } var body struct { Name string `json:"name"` } if err := decodeJSON(r, &body); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } name := strings.TrimSpace(body.Name) if name == "" { writeError(w, http.StatusBadRequest, "name is required") return } var updated homeChargerRecord if err := s.pb.Update(r.Context(), colHomeChargers, rec.ID, map[string]any{"name": name}, &updated); err != nil { writePBError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"charger": updated.toModel()}) } // deleteHomeCharger forgets a charger. The charger itself is untouched — this is // a record about it, and importing it again brings it straight back. func (s *Server) deleteHomeCharger(w http.ResponseWriter, r *http.Request) { rec, ok := s.ownedHomeCharger(w, r) if !ok { return } if err := s.pb.Delete(r.Context(), colHomeChargers, rec.ID); err != nil { writePBError(w, err) return } w.WriteHeader(http.StatusNoContent) }