Skip to content

Commit 4eb5c02

Browse files
committed
add raw_expression
1 parent 6bd07d1 commit 4eb5c02

File tree

4 files changed

+124
-58
lines changed

4 files changed

+124
-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

+5
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"`
@@ -243,6 +244,10 @@ func LoadConfig(configFile string, logger *zap.Logger) (Config, error) {
243244
}
244245
logger.Warn("string_value_mapping.error_value is deprecated: please use error_value at the metric level.", zap.String("prometheusName", m.PrometheusName), zap.String("MQTTName", m.MQTTName))
245246
}
247+
248+
if m.Expression != "" && m.RawExpression != "" {
249+
return Config{}, fmt.Errorf("metric %s/%s: expression and raw_expression are mutually exclusive.", m.MQTTName, m.PrometheusName)
250+
}
246251
}
247252
if forcesMonotonicy {
248253
if err := os.MkdirAll(cfg.Cache.StateDir, 0755); err != nil {

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
@@ -124,6 +128,7 @@ func toFloat64(i interface{}) float64 {
124128
func defaultExprEnv() map[string]interface{} {
125129
return map[string]interface{}{
126130
// Variables
131+
env_raw_value: nil,
127132
env_value: 0.0,
128133
env_last_value: 0.0,
129134
env_last_result: 0.0,
@@ -177,60 +182,71 @@ func (p *Parser) parseMetric(cfg *config.MetricConfig, metricID string, value in
177182
var metricValue float64
178183
var err error
179184

180-
if boolValue, ok := value.(bool); ok {
181-
if boolValue {
182-
metricValue = 1
183-
} else {
184-
metricValue = 0
185+
if cfg.RawExpression != "" {
186+
if metricValue, err = p.evalExpressionValue(metricID, cfg.RawExpression, value, metricValue); err != nil {
187+
if cfg.ErrorValue != nil {
188+
metricValue = *cfg.ErrorValue
189+
} else {
190+
return Metric{}, err
191+
}
185192
}
186-
} else if strValue, ok := value.(string); ok {
187-
188-
// If string value mapping is defined, use that
189-
if cfg.StringValueMapping != nil {
190-
191-
floatValue, ok := cfg.StringValueMapping.Map[strValue]
192-
if ok {
193-
metricValue = floatValue
193+
} else {
194194

195-
// deprecated, replaced by ErrorValue from the upper level
196-
} else if cfg.StringValueMapping.ErrorValue != nil {
197-
metricValue = *cfg.StringValueMapping.ErrorValue
198-
} else if cfg.ErrorValue != nil {
199-
metricValue = *cfg.ErrorValue
195+
if boolValue, ok := value.(bool); ok {
196+
if boolValue {
197+
metricValue = 1
200198
} else {
201-
return Metric{}, fmt.Errorf("got unexpected string data '%s'", strValue)
199+
metricValue = 0
202200
}
201+
} else if strValue, ok := value.(string); ok {
203202

204-
} else {
203+
// If string value mapping is defined, use that
204+
if cfg.StringValueMapping != nil {
205205

206-
// otherwise try to parse float
207-
floatValue, err := strconv.ParseFloat(strValue, 64)
208-
if err != nil {
209-
if cfg.ErrorValue != nil {
206+
floatValue, ok := cfg.StringValueMapping.Map[strValue]
207+
if ok {
208+
metricValue = floatValue
209+
210+
// deprecated, replaced by ErrorValue from the upper level
211+
} else if cfg.StringValueMapping.ErrorValue != nil {
212+
metricValue = *cfg.StringValueMapping.ErrorValue
213+
} else if cfg.ErrorValue != nil {
210214
metricValue = *cfg.ErrorValue
211215
} else {
212-
return Metric{}, fmt.Errorf("got data with unexpectd type: %T ('%v') and failed to parse to float", value, value)
216+
return Metric{}, fmt.Errorf("got unexpected string data '%s'", strValue)
213217
}
218+
214219
} else {
215-
metricValue = floatValue
220+
221+
// otherwise try to parse float
222+
floatValue, err := strconv.ParseFloat(strValue, 64)
223+
if err != nil {
224+
if cfg.ErrorValue != nil {
225+
metricValue = *cfg.ErrorValue
226+
} else {
227+
return Metric{}, fmt.Errorf("got data with unexpectd type: %T ('%v') and failed to parse to float", value, value)
228+
}
229+
} else {
230+
metricValue = floatValue
231+
}
232+
216233
}
217234

235+
} else if floatValue, ok := value.(float64); ok {
236+
metricValue = floatValue
237+
} else if cfg.ErrorValue != nil {
238+
metricValue = *cfg.ErrorValue
239+
} else {
240+
return Metric{}, fmt.Errorf("got data with unexpectd type: %T ('%v')", value, value)
218241
}
219242

220-
} else if floatValue, ok := value.(float64); ok {
221-
metricValue = floatValue
222-
} else if cfg.ErrorValue != nil {
223-
metricValue = *cfg.ErrorValue
224-
} else {
225-
return Metric{}, fmt.Errorf("got data with unexpectd type: %T ('%v')", value, value)
226-
}
227-
228-
if cfg.Expression != "" {
229-
if metricValue, err = p.evalExpression(metricID, cfg.Expression, metricValue); err != nil {
230-
if cfg.ErrorValue != nil {
231-
metricValue = *cfg.ErrorValue
232-
} else {
233-
return Metric{}, err
243+
if cfg.Expression != "" {
244+
if metricValue, err = p.evalExpressionValue(metricID, cfg.Expression, value, metricValue); err != nil {
245+
if cfg.ErrorValue != nil {
246+
metricValue = *cfg.ErrorValue
247+
} else {
248+
return Metric{}, err
249+
}
234250
}
235251
}
236252
}
@@ -348,9 +364,9 @@ func (p *Parser) enforceMonotonicy(metricID string, value float64) (float64, err
348364
return value + ms.dynamic.Offset, nil
349365
}
350366

351-
// evalExpression runs the given code in the metric's environment and returns the result.
367+
// evalExpressionValue runs the given code in the metric's environment and returns the result.
352368
// In case of an error, the original value is returned.
353-
func (p *Parser) evalExpression(metricID, code string, value float64) (float64, error) {
369+
func (p *Parser) evalExpressionValue(metricID, code string, raw_value interface{}, value float64) (float64, error) {
354370
ms, err := p.getMetricState(metricID)
355371
if err != nil {
356372
return value, err
@@ -366,8 +382,10 @@ func (p *Parser) evalExpression(metricID, code string, value float64) (float64,
366382
}
367383

368384
// Update the environment
385+
ms.env[env_raw_value] = raw_value
369386
ms.env[env_value] = value
370387
ms.env[env_last_value] = ms.dynamic.LastExprValue
388+
ms.env[env_last_raw_value] = ms.dynamic.LastExprRawValue
371389
ms.env[env_last_result] = ms.dynamic.LastExprResult
372390
if ms.dynamic.LastExprTimestamp.IsZero() {
373391
ms.env[env_elapsed] = time.Duration(0)
@@ -384,6 +402,7 @@ func (p *Parser) evalExpression(metricID, code string, value float64) (float64,
384402

385403
// Update the dynamic state
386404
ms.dynamic.LastExprResult = ret
405+
ms.dynamic.LastExprRawValue = raw_value
387406
ms.dynamic.LastExprValue = value
388407
ms.dynamic.LastExprTimestamp = now()
389408

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)