diff --git a/pkg/tsdb/testdatasource/sims/engine.go b/pkg/tsdb/testdatasource/sims/engine.go index f2e0ba33500..fa18df54bc6 100644 --- a/pkg/tsdb/testdatasource/sims/engine.go +++ b/pkg/tsdb/testdatasource/sims/engine.go @@ -66,6 +66,7 @@ func NewSimulationEngine() (*SimulationEngine, error) { initializers := []simulationInitializer{ newFlightSimInfo, newSinewaveInfo, + newTankSimInfo, } for _, init := range initializers { @@ -155,7 +156,10 @@ func (s *SimulationEngine) QueryData(ctx context.Context, req *backend.QueryData maxPoints := q.MaxDataPoints * 2 for i := int64(0); i < maxPoints && timeWalkerMs < to; i++ { t := time.UnixMilli(timeWalkerMs).UTC() - appendFrameRow(frame, sim.GetValues(t)) + vals := sim.GetValues(t) + if vals != nil { // nil is returned when you ask for an invalid time + appendFrameRow(frame, vals) + } timeWalkerMs += stepMillis } } diff --git a/pkg/tsdb/testdatasource/sims/tank.go b/pkg/tsdb/testdatasource/sims/tank.go new file mode 100644 index 00000000000..ce8c1626374 --- /dev/null +++ b/pkg/tsdb/testdatasource/sims/tank.go @@ -0,0 +1,152 @@ +package sims + +import ( + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +type tankSim struct { + key simulationKey + cfg tankConfig + state tankState +} + +var ( + _ Simulation = (*tankSim)(nil) +) + +type tankConfig struct { + TankCapacity float64 `json:"tankCapacity"` // size (in gallons) of the tank + FillRate float64 `json:"fillRate"` // fillRate + DrainRate float64 `json:"drainRate"` // how fast it will drain (when valve is open) + DrainOpen bool `json:"drainOpen"` + PumpOn bool `json:"pumpOn"` +} + +type tankState struct { + Time time.Time + FillPercent float64 // 0-1 +} + +func (s *tankSim) GetState() simulationState { + return simulationState{ + Key: s.key, + Config: s.cfg, + } +} + +func (s *tankSim) SetConfig(vals map[string]interface{}) error { + return updateConfigObjectFromJSON(&s.cfg, vals) +} + +func (s *tankSim) NewFrame(size int) *data.Frame { + frame := data.NewFrameOfFieldTypes("", size, + data.FieldTypeTime, // time + data.FieldTypeFloat64, // value + data.FieldTypeFloat64, // value + data.FieldTypeBool, + data.FieldTypeBool, + ) + + var min data.ConfFloat64 = 0.0 + var max data.ConfFloat64 = 1.0 + + frame.Fields[0].Name = "time" + frame.Fields[1].Name = "percentFull" + frame.Fields[1].Config = &data.FieldConfig{ + Unit: "percentunit", + Min: &min, + Max: &max, + } + + frame.Fields[2].Name = "tankVolume" + frame.Fields[2].Config = &data.FieldConfig{ + Unit: "m3", // cubic meters + } + frame.Fields[3].Name = "pumpOn" + frame.Fields[4].Name = "drainOpen" + return frame +} + +func (s *tankSim) GetValues(t time.Time) map[string]interface{} { + if t.Before(s.state.Time) { + return nil // can not look backwards! + } + if t.After(s.state.Time) { + elapsed := t.Sub(s.state.Time).Seconds() + v := s.state.FillPercent * s.cfg.TankCapacity + if s.cfg.PumpOn { + v += (elapsed * s.cfg.FillRate) + } + if s.cfg.DrainOpen { + v -= (elapsed * s.cfg.DrainRate) + } + + p := v / s.cfg.TankCapacity + if p > 1 { + p = 1 + } else if p < 0 { + p = 0 + } + + s.state.FillPercent = p + s.state.Time = t + } + + return map[string]interface{}{ + "time": s.state.Time, + "percentFull": s.state.FillPercent, + "tankVolume": s.state.FillPercent * s.cfg.TankCapacity, + "pumpOn": s.cfg.PumpOn, + "drainOpen": s.cfg.DrainOpen, + } +} + +func (s *tankSim) Close() error { + return nil +} + +func newTankSimInfo() simulationInfo { + tc := tankConfig{ + TankCapacity: 100.0, + FillRate: 1.0, // gallons/min? + DrainRate: 0.5, // `json:"drainRate,omitempty"` // how fast it will drain (when valve is open) + DrainOpen: false, + PumpOn: true, + } + ts := tankState{ + Time: time.Now(), + FillPercent: .6, + } + + df := data.NewFrame("") + df.Fields = append(df.Fields, data.NewField("tankCapacity", nil, []float64{tc.TankCapacity}).SetConfig(&data.FieldConfig{ + Unit: "m3", + })) + df.Fields = append(df.Fields, data.NewField("fillRate", nil, []float64{tc.FillRate}).SetConfig(&data.FieldConfig{ + Unit: "cms", + })) + df.Fields = append(df.Fields, data.NewField("drainRate", nil, []float64{tc.DrainRate}).SetConfig(&data.FieldConfig{ + Unit: "cms", + })) + df.Fields = append(df.Fields, data.NewField("drainOpen", nil, []bool{tc.DrainOpen})) + df.Fields = append(df.Fields, data.NewField("pumpOn", nil, []bool{tc.PumpOn})) + + return simulationInfo{ + Type: "tank", + Name: "Tank", + Description: "Fill and drain a water tank", + SetupFields: df, + OnlyForward: false, + create: func(cfg simulationState) (Simulation, error) { + s := &tankSim{ + key: cfg.Key, + cfg: tc, + state: ts, + } + err := updateConfigObjectFromJSON(&s.cfg, cfg.Config) // override any fields + return s, err + }, + } +} diff --git a/public/app/plugins/datasource/testdata/components/SimulationQueryEditor.tsx b/public/app/plugins/datasource/testdata/components/SimulationQueryEditor.tsx index 3b566970220..6016af7b874 100644 --- a/public/app/plugins/datasource/testdata/components/SimulationQueryEditor.tsx +++ b/public/app/plugins/datasource/testdata/components/SimulationQueryEditor.tsx @@ -10,6 +10,7 @@ export const SimulationQueryEditor = ({ onChange, query }: EditorProps) => { const options = [ { label: 'Flight', value: 'flight' }, { label: 'Sine', value: 'sine' }, + { label: 'Tank', value: 'tank' }, ]; const onUpdateKey = (key: typeof simQuery.key) => {