Skip to content

Commit e6569ca

Browse files
committed
add raw_expression
1 parent 5ed4e4b commit e6569ca

File tree

4 files changed

+120
-58
lines changed

4 files changed

+120
-58
lines changed

Readme.md

+18-2
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,17 @@ metrics:
288288
type: counter
289289
# This setting requires an almost monotonic counter as the source. When monotonicy is enforced, the metric value is regularly written to disk. Thus, resets in the source counter can be detected and corrected by adding an offset as if the reset did not happen. The result is a true monotonic increasing time series, like an ever growing counter.
290290
force_monotonicy: true
291+
- prom_name: linky_time
292+
# The name of the metric in a MQTT JSON message
293+
mqtt_name: linky_current_date
294+
# Regular expression to only match sensors with the given name pattern
295+
sensor_name_filter: "^linky.*$"
296+
# The prometheus help text for this metric
297+
help: current unix timestamp from linky
298+
# The prometheus type for this metric. Valid values are: "gauge" and "counter"
299+
type: gauge
300+
# convert dynamic datetime string to unix timestamp
301+
raw_expression: 'date(string(raw_value), "H060102150405", "Europe/Paris").Unix()'
291302
```
292303
293304
### Environment Variables
@@ -335,13 +346,18 @@ Create a docker secret to store the password(`mqtt-credential` in the example be
335346

336347
### Expressions
337348

338-
Metric values can be derived from sensor inputs using complex expressions. Set the metric config option `expression` to the desired formular to calculate the result from the input. Here's an example which integrates all positive values over time:
339-
349+
Metric values can be derived from sensor inputs using complex expressions. Set the metric config option `raw_expression` or `expression` to the desired formular to calculate the result from the input. `raw_expression` and `expression` are mutually exclusives:
350+
* `raw_expression` is run without raw value conversion. It's `raw_expression` duty to handle the conversion. Only `raw_value` is set while `value` is always set to 0.0. Here is an example which convert datetime (format `HYYMMDDhhmmss`) to unix timestamp:
351+
```yaml
352+
raw_expression: 'date(string(raw_value), "H060102150405", "Europe/Paris").Unix()'
353+
```
354+
* `expression` is run after raw value conversion. If conversion fails, `expression` is not run. Here's an example which integrates all positive values over time:
340355
```yaml
341356
expression: "value > 0 ? last_result + value * elapsed.Seconds() : last_result"
342357
```
343358

344359
During the evaluation, the following variables are available to the expression:
360+
* `raw_value` - the raw MQTT sensor value (without any conversion)
345361
* `value` - the current sensor value (after string-value mapping, if configured)
346362
* `last_value` - the `value` during the previous expression evaluation
347363
* `last_result` - the result from the previous expression evaluation

pkg/config/config.go

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ type MetricConfig struct {
140140
Help string `yaml:"help"`
141141
ValueType string `yaml:"type"`
142142
OmitTimestamp bool `yaml:"omit_timestamp"`
143+
RawExpression string `yaml:"raw_expression"`
143144
Expression string `yaml:"expression"`
144145
ForceMonotonicy bool `yaml:"force_monotonicy"`
145146
ConstantLabels map[string]string `yaml:"const_labels"`

pkg/metrics/parser.go

+74-55
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ type dynamicState struct {
2424
// Last value that was used for evaluating the given expression
2525
LastExprValue float64 `yaml:"last_expr_value"`
2626
// Last result returned from evaluating the given expression
27+
LastExprRawValue interface{} `yaml:"last_expr_raw_value"`
28+
// Last result returned from evaluating the given expression
2729
LastExprResult float64 `yaml:"last_expr_result"`
2830
// Last result returned from evaluating the given expression
2931
LastExprTimestamp time.Time `yaml:"last_expr_timestamp"`
@@ -53,19 +55,21 @@ type Parser struct {
5355

5456
// Identifiers within the expression evaluation environment.
5557
const (
56-
env_value = "value"
57-
env_last_value = "last_value"
58-
env_last_result = "last_result"
59-
env_elapsed = "elapsed"
60-
env_now = "now"
61-
env_int = "int"
62-
env_float = "float"
63-
env_round = "round"
64-
env_ceil = "ceil"
65-
env_floor = "floor"
66-
env_abs = "abs"
67-
env_min = "min"
68-
env_max = "max"
58+
env_raw_value = "raw_value"
59+
env_value = "value"
60+
env_last_value = "last_value"
61+
env_last_raw_value = "last_raw_value"
62+
env_last_result = "last_result"
63+
env_elapsed = "elapsed"
64+
env_now = "now"
65+
env_int = "int"
66+
env_float = "float"
67+
env_round = "round"
68+
env_ceil = "ceil"
69+
env_floor = "floor"
70+
env_abs = "abs"
71+
env_min = "min"
72+
env_max = "max"
6973
)
7074

7175
var now = time.Now
@@ -112,6 +116,7 @@ func toFloat64(i interface{}) float64 {
112116
func defaultExprEnv() map[string]interface{} {
113117
return map[string]interface{}{
114118
// Variables
119+
env_raw_value: nil,
115120
env_value: 0.0,
116121
env_last_value: 0.0,
117122
env_last_result: 0.0,
@@ -165,60 +170,71 @@ func (p *Parser) parseMetric(cfg *config.MetricConfig, metricID string, value in
165170
var metricValue float64
166171
var err error
167172

168-
if boolValue, ok := value.(bool); ok {
169-
if boolValue {
170-
metricValue = 1
171-
} else {
172-
metricValue = 0
173+
if cfg.RawExpression != "" {
174+
if metricValue, err = p.evalExpressionValue(metricID, cfg.RawExpression, value, metricValue); err != nil {
175+
if cfg.ErrorValue != nil {
176+
metricValue = *cfg.ErrorValue
177+
} else {
178+
return Metric{}, err
179+
}
173180
}
174-
} else if strValue, ok := value.(string); ok {
175-
176-
// If string value mapping is defined, use that
177-
if cfg.StringValueMapping != nil {
178-
179-
floatValue, ok := cfg.StringValueMapping.Map[strValue]
180-
if ok {
181-
metricValue = floatValue
181+
} else {
182182

183-
// deprecated, replaced by ErrorValue from the upper level
184-
} else if cfg.StringValueMapping.ErrorValue != nil {
185-
metricValue = *cfg.StringValueMapping.ErrorValue
186-
} else if cfg.ErrorValue != nil {
187-
metricValue = *cfg.ErrorValue
183+
if boolValue, ok := value.(bool); ok {
184+
if boolValue {
185+
metricValue = 1
188186
} else {
189-
return Metric{}, fmt.Errorf("got unexpected string data '%s'", strValue)
187+
metricValue = 0
190188
}
189+
} else if strValue, ok := value.(string); ok {
191190

192-
} else {
191+
// If string value mapping is defined, use that
192+
if cfg.StringValueMapping != nil {
193193

194-
// otherwise try to parse float
195-
floatValue, err := strconv.ParseFloat(strValue, 64)
196-
if err != nil {
197-
if cfg.ErrorValue != nil {
194+
floatValue, ok := cfg.StringValueMapping.Map[strValue]
195+
if ok {
196+
metricValue = floatValue
197+
198+
// deprecated, replaced by ErrorValue from the upper level
199+
} else if cfg.StringValueMapping.ErrorValue != nil {
200+
metricValue = *cfg.StringValueMapping.ErrorValue
201+
} else if cfg.ErrorValue != nil {
198202
metricValue = *cfg.ErrorValue
199203
} else {
200-
return Metric{}, fmt.Errorf("got data with unexpectd type: %T ('%v') and failed to parse to float", value, value)
204+
return Metric{}, fmt.Errorf("got unexpected string data '%s'", strValue)
201205
}
206+
202207
} else {
203-
metricValue = floatValue
208+
209+
// otherwise try to parse float
210+
floatValue, err := strconv.ParseFloat(strValue, 64)
211+
if err != nil {
212+
if cfg.ErrorValue != nil {
213+
metricValue = *cfg.ErrorValue
214+
} else {
215+
return Metric{}, fmt.Errorf("got data with unexpectd type: %T ('%v') and failed to parse to float", value, value)
216+
}
217+
} else {
218+
metricValue = floatValue
219+
}
220+
204221
}
205222

223+
} else if floatValue, ok := value.(float64); ok {
224+
metricValue = floatValue
225+
} else if cfg.ErrorValue != nil {
226+
metricValue = *cfg.ErrorValue
227+
} else {
228+
return Metric{}, fmt.Errorf("got data with unexpectd type: %T ('%v')", value, value)
206229
}
207230

208-
} else if floatValue, ok := value.(float64); ok {
209-
metricValue = floatValue
210-
} else if cfg.ErrorValue != nil {
211-
metricValue = *cfg.ErrorValue
212-
} else {
213-
return Metric{}, fmt.Errorf("got data with unexpectd type: %T ('%v')", value, value)
214-
}
215-
216-
if cfg.Expression != "" {
217-
if metricValue, err = p.evalExpression(metricID, cfg.Expression, metricValue); err != nil {
218-
if cfg.ErrorValue != nil {
219-
metricValue = *cfg.ErrorValue
220-
} else {
221-
return Metric{}, err
231+
if cfg.Expression != "" {
232+
if metricValue, err = p.evalExpressionValue(metricID, cfg.Expression, value, metricValue); err != nil {
233+
if cfg.ErrorValue != nil {
234+
metricValue = *cfg.ErrorValue
235+
} else {
236+
return Metric{}, err
237+
}
222238
}
223239
}
224240
}
@@ -336,9 +352,9 @@ func (p *Parser) enforceMonotonicy(metricID string, value float64) (float64, err
336352
return value + ms.dynamic.Offset, nil
337353
}
338354

339-
// evalExpression runs the given code in the metric's environment and returns the result.
355+
// evalExpressionValue runs the given code in the metric's environment and returns the result.
340356
// In case of an error, the original value is returned.
341-
func (p *Parser) evalExpression(metricID, code string, value float64) (float64, error) {
357+
func (p *Parser) evalExpressionValue(metricID, code string, raw_value interface{}, value float64) (float64, error) {
342358
ms, err := p.getMetricState(metricID)
343359
if err != nil {
344360
return value, err
@@ -354,8 +370,10 @@ func (p *Parser) evalExpression(metricID, code string, value float64) (float64,
354370
}
355371

356372
// Update the environment
373+
ms.env[env_raw_value] = raw_value
357374
ms.env[env_value] = value
358375
ms.env[env_last_value] = ms.dynamic.LastExprValue
376+
ms.env[env_last_raw_value] = ms.dynamic.LastExprRawValue
359377
ms.env[env_last_result] = ms.dynamic.LastExprResult
360378
if ms.dynamic.LastExprTimestamp.IsZero() {
361379
ms.env[env_elapsed] = time.Duration(0)
@@ -372,6 +390,7 @@ func (p *Parser) evalExpression(metricID, code string, value float64) (float64,
372390

373391
// Update the dynamic state
374392
ms.dynamic.LastExprResult = ret
393+
ms.dynamic.LastExprRawValue = raw_value
375394
ms.dynamic.LastExprValue = value
376395
ms.dynamic.LastExprTimestamp = now()
377396

pkg/metrics/parser_test.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,32 @@ func TestParser_parseMetric(t *testing.T) {
667667
Value: 11.0, // 600 watts for 1 minute = 10 Wh
668668
},
669669
},
670+
{
671+
name: "raw expression, step 1",
672+
fields: fields{
673+
map[string][]*config.MetricConfig{
674+
"apower": {
675+
{
676+
PrometheusName: "total_energy",
677+
ValueType: "gauge",
678+
OmitTimestamp: true,
679+
RawExpression: `float(join(filter(split(string(raw_value), ""), { # matches "^[0-9\\.]$" }), ""))`,
680+
},
681+
},
682+
},
683+
},
684+
elapseNow: 3 * time.Minute,
685+
args: args{
686+
metricPath: "apower",
687+
deviceID: "shellyplus1pm-foo",
688+
value: "H42Jj.j44",
689+
},
690+
want: Metric{
691+
Description: prometheus.NewDesc("total_energy", "", []string{"sensor", "topic"}, nil),
692+
ValueType: prometheus.GaugeValue,
693+
Value: 42.44,
694+
},
695+
},
670696
}
671697
for _, tt := range tests {
672698
t.Run(tt.name, func(t *testing.T) {
@@ -797,7 +823,7 @@ func TestParser_evalExpression(t *testing.T) {
797823

798824
p := NewParser(nil, ".", stateDir)
799825
for i, value := range tt.values {
800-
got, err := p.evalExpression(id, tt.expression, value)
826+
got, err := p.evalExpressionValue(id, tt.expression, value, value)
801827
want := tt.results[i]
802828
if err != nil {
803829
t.Errorf("evaluating the %dth value '%v' failed: %v", i, value, err)

0 commit comments

Comments
 (0)