143 lines
3.5 KiB
Go
143 lines
3.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/evcc-io/evcc/plugin"
|
|
"github.com/evcc-io/evcc/server/service"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/modbus"
|
|
"github.com/fatih/structs"
|
|
"github.com/spf13/cast"
|
|
)
|
|
|
|
// Simple cache for service responses
|
|
type cacheEntry struct {
|
|
value string
|
|
timestamp time.Time
|
|
}
|
|
|
|
var (
|
|
cache = make(map[string]cacheEntry)
|
|
mu sync.RWMutex
|
|
cacheTTL = 1 * time.Minute // Cache for 1 minute
|
|
)
|
|
|
|
// Query combines modbus settings, register config, and additional parameters
|
|
type Query struct {
|
|
modbus.Settings `mapstructure:",squash"`
|
|
modbus.Register `mapstructure:",squash"`
|
|
Scale float64 // scaling factor
|
|
ResultType string // type cast (int, float, string)
|
|
}
|
|
|
|
func init() {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /read", modbusRead)
|
|
|
|
service.Register("modbus", mux)
|
|
}
|
|
|
|
// modbusRead reads a parameter value from a device based on URL parameters
|
|
// Returns single value as array (for UI compatibility)
|
|
func modbusRead(w http.ResponseWriter, req *http.Request) {
|
|
// Convert URL query parameters to map for decoding
|
|
cc := make(map[string]any)
|
|
for k := range req.URL.Query() {
|
|
cc[k] = req.URL.Query().Get(k)
|
|
}
|
|
|
|
// Decode query parameters into Query struct using mapstructure
|
|
query := Query{
|
|
Scale: 1.0,
|
|
}
|
|
|
|
if err := util.DecodeOther(cc, &query); err != nil {
|
|
jsonError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
// Validate required parameters
|
|
if (query.URI == "" && query.Device == "") || cc["address"] == nil {
|
|
jsonError(w, http.StatusBadRequest, fmt.Errorf("uri or device and address parameters are required"))
|
|
return
|
|
}
|
|
|
|
// Cache per exact request: distinct id/encoding/scale/resulttype must not share a converted value
|
|
cacheKey := req.URL.Query().Encode()
|
|
|
|
// Check cache first
|
|
mu.RLock()
|
|
if entry, ok := cache[cacheKey]; ok && time.Since(entry.timestamp) < cacheTTL {
|
|
mu.RUnlock()
|
|
jsonWrite(w, []string{cast.ToString(entry.value)})
|
|
return
|
|
}
|
|
mu.RUnlock()
|
|
|
|
// Read value from modbus using plugin
|
|
// Use background context so connection isn't tied to HTTP request lifecycle
|
|
value, err := readRegisterValue(context.TODO(), query)
|
|
if err != nil {
|
|
jsonError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
// Convert to string
|
|
res := toString(value, query.ResultType)
|
|
|
|
// Store in cache
|
|
mu.Lock()
|
|
cache[cacheKey] = cacheEntry{
|
|
value: res,
|
|
timestamp: time.Now(),
|
|
}
|
|
mu.Unlock()
|
|
|
|
jsonWrite(w, []string{res})
|
|
}
|
|
|
|
// readRegisterValue reads a modbus register value by reusing the modbus plugin
|
|
func readRegisterValue(ctx context.Context, query Query) (res any, err error) {
|
|
// Convert Settings to map (plugin expects Settings fields at top level)
|
|
cfg := structs.Map(query.Settings)
|
|
|
|
// Plugin expects Register as nested object, not flattened
|
|
cfg["register"] = query.Register
|
|
cfg["scale"] = query.Scale
|
|
|
|
p, err := plugin.NewModbusFromConfig(ctx, cfg)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to create modbus plugin: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("read failed: %v", r)
|
|
}
|
|
}()
|
|
|
|
// Choose getter based on encoding type
|
|
encoding := strings.ToLower(query.Encoding)
|
|
|
|
// String encodings need special handling
|
|
if encoding == "string" || encoding == "bytes" {
|
|
g, err := p.(plugin.StringGetter).StringGetter()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return g()
|
|
}
|
|
|
|
// For all numeric encodings (int*, float*, bool*), use FloatGetter
|
|
g, err := p.(plugin.FloatGetter).FloatGetter()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return g()
|
|
}
|