464 lines
9.9 KiB
Go
464 lines
9.9 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/charger"
|
|
"github.com/evcc-io/evcc/meter"
|
|
"github.com/evcc-io/evcc/util/config"
|
|
"github.com/evcc-io/evcc/util/templates"
|
|
"github.com/evcc-io/evcc/vehicle"
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
const (
|
|
// typeTemplate is the updatable configuration type
|
|
typeTemplate = "template"
|
|
|
|
// masked indicates a masked config parameter value
|
|
masked = "***"
|
|
)
|
|
|
|
// templatesHandler returns the list of templates by class
|
|
func templatesHandler(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
|
|
class, err := templates.ClassString(vars["class"])
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
lang := r.URL.Query().Get("lang")
|
|
templates.EncoderLanguage(lang)
|
|
|
|
if name := r.URL.Query().Get("name"); name != "" {
|
|
res, err := templates.ByName(class, name)
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
jsonResult(w, res)
|
|
return
|
|
}
|
|
|
|
jsonResult(w, templates.ByClass(class))
|
|
}
|
|
|
|
// productsHandler returns the list of products by class
|
|
func productsHandler(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
|
|
class, err := templates.ClassString(vars["class"])
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
tmpl := templates.ByClass(class)
|
|
lang := r.URL.Query().Get("lang")
|
|
|
|
res := make(products, 0)
|
|
for _, t := range tmpl {
|
|
for _, p := range t.Products {
|
|
res = append(res, product{
|
|
Name: p.Title(lang),
|
|
Template: t.TemplateDefinition.Template,
|
|
Group: t.Group,
|
|
})
|
|
}
|
|
}
|
|
|
|
slices.SortFunc(res, func(a, b product) int {
|
|
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
|
|
})
|
|
|
|
jsonResult(w, res)
|
|
}
|
|
|
|
func deviceConfigMap[T any](class templates.Class, dev config.Device[T]) (map[string]any, error) {
|
|
conf := dev.Config()
|
|
|
|
dc := map[string]any{
|
|
"name": conf.Name,
|
|
"type": conf.Type,
|
|
}
|
|
|
|
if configurable, ok := dev.(config.ConfigurableDevice[T]); ok {
|
|
// from database
|
|
params, err := sanitizeMasked(class, conf.Other)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dc["id"] = configurable.ID()
|
|
dc["config"] = params
|
|
} else if title := conf.Other["title"]; title != nil {
|
|
// from yaml- add title only
|
|
if s, ok := title.(string); ok {
|
|
dc["config"] = map[string]any{"title": s}
|
|
}
|
|
}
|
|
|
|
return dc, nil
|
|
}
|
|
|
|
func deviceConfig[T any](class templates.Class, id int, h config.Handler[T]) (map[string]any, error) {
|
|
dev, err := h.ByName(config.NameForID(id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return deviceConfigMap(class, dev)
|
|
}
|
|
|
|
// deviceHandler returns a device configuration by class
|
|
func deviceHandler(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
|
|
class, err := templates.ClassString(vars["class"])
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
id, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
var res map[string]any
|
|
|
|
switch class {
|
|
case templates.Meter:
|
|
res, err = deviceConfig(class, id, config.Meters())
|
|
|
|
case templates.Charger:
|
|
res, err = deviceConfig(class, id, config.Chargers())
|
|
|
|
case templates.Vehicle:
|
|
res, err = deviceConfig(class, id, config.Vehicles())
|
|
}
|
|
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
jsonResult(w, res)
|
|
}
|
|
|
|
func devicesConfig[T any](class templates.Class, h config.Handler[T]) ([]map[string]any, error) {
|
|
var res []map[string]any
|
|
|
|
for _, dev := range h.Devices() {
|
|
dc, err := deviceConfigMap(class, dev)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res = append(res, dc)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// devicesHandler returns a device configurations by class
|
|
func devicesHandler(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
|
|
class, err := templates.ClassString(vars["class"])
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
var res []map[string]any
|
|
|
|
switch class {
|
|
case templates.Meter:
|
|
res, err = devicesConfig(class, config.Meters())
|
|
|
|
case templates.Charger:
|
|
res, err = devicesConfig(class, config.Chargers())
|
|
|
|
case templates.Vehicle:
|
|
res, err = devicesConfig(class, config.Vehicles())
|
|
}
|
|
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
jsonResult(w, res)
|
|
}
|
|
|
|
func newDevice[T any](class templates.Class, req map[string]any, newFromConf func(string, map[string]any) (T, error), h config.Handler[T]) (*config.Config, error) {
|
|
instance, err := newFromConf(typeTemplate, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conf, err := config.AddConfig(class, typeTemplate, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &conf, h.Add(config.NewConfigurableDevice[T](conf, instance))
|
|
}
|
|
|
|
// newDeviceHandler creates a new device by class
|
|
func newDeviceHandler(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
|
|
class, err := templates.ClassString(vars["class"])
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
var req map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
delete(req, "type")
|
|
|
|
var conf *config.Config
|
|
|
|
switch class {
|
|
case templates.Charger:
|
|
conf, err = newDevice(class, req, charger.NewFromConfig, config.Chargers())
|
|
|
|
case templates.Meter:
|
|
conf, err = newDevice(class, req, meter.NewFromConfig, config.Meters())
|
|
|
|
case templates.Vehicle:
|
|
conf, err = newDevice(class, req, vehicle.NewFromConfig, config.Vehicles())
|
|
}
|
|
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
res := struct {
|
|
ID int `json:"id"`
|
|
}{
|
|
ID: conf.ID,
|
|
}
|
|
|
|
jsonResult(w, res)
|
|
}
|
|
|
|
func updateDevice[T any](id int, class templates.Class, conf map[string]any, newFromConf func(string, map[string]any) (T, error), h config.Handler[T]) error {
|
|
dev, instance, err := deviceInstanceFromMergedConfig(id, class, conf, newFromConf, h)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
configurable, ok := dev.(config.ConfigurableDevice[T])
|
|
if !ok {
|
|
return errors.New("not configurable")
|
|
}
|
|
|
|
return configurable.Update(conf, instance)
|
|
}
|
|
|
|
// updateDeviceHandler updates database device's configuration by class
|
|
func updateDeviceHandler(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
|
|
class, err := templates.ClassString(vars["class"])
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
id, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
var req map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
delete(req, "type")
|
|
|
|
switch class {
|
|
case templates.Charger:
|
|
err = updateDevice(id, class, req, charger.NewFromConfig, config.Chargers())
|
|
|
|
case templates.Meter:
|
|
err = updateDevice(id, class, req, meter.NewFromConfig, config.Meters())
|
|
|
|
case templates.Vehicle:
|
|
err = updateDevice(id, class, req, vehicle.NewFromConfig, config.Vehicles())
|
|
}
|
|
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
res := struct {
|
|
ID int `json:"id"`
|
|
}{
|
|
ID: id,
|
|
}
|
|
|
|
jsonResult(w, res)
|
|
}
|
|
|
|
func deleteDevice[T any](id int, h config.Handler[T]) error {
|
|
name := config.NameForID(id)
|
|
|
|
dev, err := h.ByName(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
configurable, ok := dev.(config.ConfigurableDevice[T])
|
|
if !ok {
|
|
return errors.New("not configurable")
|
|
}
|
|
|
|
if err := configurable.Delete(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return h.Delete(name)
|
|
}
|
|
|
|
// deleteDeviceHandler deletes a device from database by class
|
|
func deleteDeviceHandler(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
|
|
class, err := templates.ClassString(vars["class"])
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
id, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
switch class {
|
|
case templates.Charger:
|
|
err = deleteDevice(id, config.Chargers())
|
|
|
|
case templates.Meter:
|
|
err = deleteDevice(id, config.Meters())
|
|
|
|
case templates.Vehicle:
|
|
err = deleteDevice(id, config.Vehicles())
|
|
}
|
|
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
res := struct {
|
|
ID int `json:"id"`
|
|
}{
|
|
ID: id,
|
|
}
|
|
|
|
jsonResult(w, res)
|
|
}
|
|
|
|
func testDevice[T any](id int, class templates.Class, conf map[string]any, newFromConf func(string, map[string]any) (T, error), h config.Handler[T]) (T, error) {
|
|
if id == 0 {
|
|
return newFromConf(typeTemplate, conf)
|
|
}
|
|
|
|
_, instance, err := deviceInstanceFromMergedConfig(id, class, conf, newFromConf, h)
|
|
|
|
return instance, err
|
|
}
|
|
|
|
// testHandler tests a configuration by class
|
|
func testHandler(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
|
|
class, err := templates.ClassString(vars["class"])
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
var id int
|
|
if vars["id"] != "" {
|
|
// test existing device with updated config
|
|
id, err = strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
var req map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
delete(req, "type")
|
|
|
|
var instance any
|
|
|
|
switch class {
|
|
case templates.Charger:
|
|
instance, err = testDevice(id, class, req, charger.NewFromConfig, config.Chargers())
|
|
|
|
case templates.Meter:
|
|
instance, err = testDevice(id, class, req, meter.NewFromConfig, config.Meters())
|
|
|
|
case templates.Vehicle:
|
|
instance, err = testDevice(id, class, req, vehicle.NewFromConfig, config.Vehicles())
|
|
}
|
|
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
type result = struct {
|
|
Value any `json:"value"`
|
|
Error error `json:"error"`
|
|
}
|
|
|
|
res := make(map[string]result)
|
|
|
|
if dev, ok := instance.(api.Meter); ok {
|
|
val, err := dev.CurrentPower()
|
|
res["CurrentPower"] = result{val, err}
|
|
}
|
|
|
|
if dev, ok := instance.(api.MeterEnergy); ok {
|
|
val, err := dev.TotalEnergy()
|
|
res["TotalEnergy"] = result{val, err}
|
|
}
|
|
|
|
if dev, ok := instance.(api.Battery); ok {
|
|
val, err := dev.Soc()
|
|
res["Soc"] = result{val, err}
|
|
}
|
|
|
|
jsonResult(w, res)
|
|
}
|