mqtt-scenario-sim 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startSimulator = startSimulator;
4
+ const config_1 = require("./config");
5
+ const sensors_1 = require("./sensors");
6
+ const scenarios_1 = require("./scenarios");
7
+ const effects_1 = require("./effects");
8
+ const logger_1 = require("./logger");
9
+ function startSimulator(config, client, encode) {
10
+ const timers = [];
11
+ const revertTimers = new Map();
12
+ const publishListeners = new Set();
13
+ const entries = new Map();
14
+ for (const source of config.sources) {
15
+ const key = JSON.stringify(source.labels);
16
+ const topic = (0, config_1.resolveTopicTemplate)(source.topic, source.labels);
17
+ const states = source.metrics.map((m) => (0, sensors_1.createSensorState)({ ...m, intervalMs: m.intervalMs ?? config.publishIntervalMs }));
18
+ entries.set(key, {
19
+ config: source,
20
+ states,
21
+ effectConfigs: source.effects ?? [],
22
+ effectState: {},
23
+ scenario: { active: 'normal', ticks: new Array(source.metrics.length).fill(0) },
24
+ topic,
25
+ });
26
+ // Subscribe to inbound commands on the reverse topic: topic + "/cmd"
27
+ const cmdTopic = `${topic}/cmd`;
28
+ client.subscribe(cmdTopic, (err) => {
29
+ if (err)
30
+ logger_1.logger.error(`[sim] subscribe error ${cmdTopic}:`, err);
31
+ });
32
+ }
33
+ // Handle inbound MQTT command messages
34
+ client.on('message', (topic, payload) => {
35
+ if (!topic.endsWith('/cmd'))
36
+ return;
37
+ const baseTopic = topic.slice(0, -'/cmd'.length);
38
+ let raw;
39
+ try {
40
+ raw = JSON.parse(payload.toString());
41
+ }
42
+ catch {
43
+ return;
44
+ }
45
+ for (const entry of entries.values()) {
46
+ if (entry.topic === baseTopic) {
47
+ const cmd = (0, effects_1.parseEffectCommand)(raw);
48
+ if (cmd) {
49
+ entry.effectState = (0, effects_1.applyEffect)(entry.effectState, cmd.effect, cmd.state);
50
+ logger_1.logger.info(`[effect] ${baseTopic} "${cmd.effect}" → ${cmd.state}`);
51
+ }
52
+ return;
53
+ }
54
+ }
55
+ });
56
+ // Schedule publish intervals for each metric in each source
57
+ for (const entry of entries.values()) {
58
+ for (let i = 0; i < entry.states.length; i++) {
59
+ const state = entry.states[i];
60
+ const metricCfg = entry.config.metrics[i];
61
+ const intervalMs = state.config.intervalMs;
62
+ const timer = setInterval(() => publishMetric(entry, i, metricCfg, state, client, encode, publishListeners), intervalMs);
63
+ timers.push(timer);
64
+ }
65
+ }
66
+ const sourceCount = entries.size;
67
+ const metricCount = [...entries.values()].reduce((n, e) => n + e.states.length, 0);
68
+ logger_1.logger.info(`[sim] started ${metricCount} metric(s) across ${sourceCount} source(s) | scenario: normal`);
69
+ function applyScenario(id, sourceKey, durationSeconds) {
70
+ const targets = sourceKey ? [sourceKey] : [...entries.keys()];
71
+ for (const key of targets) {
72
+ const entry = entries.get(key);
73
+ if (!entry)
74
+ continue;
75
+ const sc = entry.scenario;
76
+ if (id === sc.active && !durationSeconds)
77
+ continue;
78
+ sc.active = id;
79
+ sc.ticks.fill(0);
80
+ const existing = revertTimers.get(key);
81
+ if (existing) {
82
+ clearTimeout(existing);
83
+ revertTimers.delete(key);
84
+ }
85
+ logger_1.logger.info(`\n[scenario:${entry.topic}] → ${id}`);
86
+ logger_1.logger.info(` ${scenarios_1.SCENARIO_DETAIL[id]}`);
87
+ if (durationSeconds && durationSeconds > 0) {
88
+ logger_1.logger.info(` Auto-reverts in ${durationSeconds}s\n`);
89
+ const t = setTimeout(() => {
90
+ revertTimers.delete(key);
91
+ applyScenario('normal', key);
92
+ logger_1.logger.info(`[scenario:${entry.topic}] auto-reverted to normal\n`);
93
+ }, durationSeconds * 1000);
94
+ revertTimers.set(key, t);
95
+ }
96
+ else {
97
+ logger_1.logger.info('');
98
+ }
99
+ }
100
+ }
101
+ return {
102
+ stop() {
103
+ for (const t of timers)
104
+ clearInterval(t);
105
+ for (const t of revertTimers.values())
106
+ clearTimeout(t);
107
+ logger_1.logger.info('[sim] stopped');
108
+ },
109
+ setScenario(id, sourceKey, durationSeconds) {
110
+ applyScenario(id, sourceKey, durationSeconds);
111
+ },
112
+ getScenario(sourceKey) {
113
+ if (sourceKey)
114
+ return entries.get(sourceKey)?.scenario.active ?? 'normal';
115
+ const actives = [...entries.values()].map((e) => e.scenario.active);
116
+ return actives.every((a) => a === actives[0]) ? (actives[0] ?? 'normal') : 'normal';
117
+ },
118
+ getState() {
119
+ const firstScenario = [...entries.values()][0]?.scenario.active ?? 'normal';
120
+ const metrics = [];
121
+ for (const entry of entries.values()) {
122
+ for (let i = 0; i < entry.states.length; i++) {
123
+ const s = entry.states[i];
124
+ metrics.push({
125
+ labels: entry.config.labels,
126
+ metric: entry.config.metrics[i].name,
127
+ units: entry.config.metrics[i].units,
128
+ value: s.lastValue,
129
+ lastPublishedAt: s.lastPublishedAt,
130
+ });
131
+ }
132
+ }
133
+ return { scenario: firstScenario, metrics };
134
+ },
135
+ getEffectStates() {
136
+ const out = {};
137
+ for (const entry of entries.values()) {
138
+ out[entry.topic] = entry.effectState;
139
+ }
140
+ return out;
141
+ },
142
+ onPublish(listener) {
143
+ publishListeners.add(listener);
144
+ return () => publishListeners.delete(listener);
145
+ },
146
+ };
147
+ }
148
+ async function publishMetric(entry, metricIndex, metricCfg, state, client, encode, listeners) {
149
+ const span = state.resolved.max - state.resolved.min;
150
+ const targetBias = (0, effects_1.computeBias)(entry.effectConfigs, entry.effectState, metricCfg.name, span);
151
+ (0, sensors_1.advanceBias)(state, targetBias);
152
+ const sc = entry.scenario;
153
+ const tick = sc.ticks[metricIndex] ?? 0;
154
+ const override = (0, scenarios_1.scenarioValue)(sc.active, state.resolved, tick, Math.random());
155
+ let value;
156
+ if (override !== null) {
157
+ value = Math.round((override + state.currentBias) * 100) / 100;
158
+ }
159
+ else {
160
+ value = (0, sensors_1.nextValue)(state, state.currentBias);
161
+ }
162
+ if (sc.active !== 'normal') {
163
+ sc.ticks[metricIndex] = tick + 1;
164
+ }
165
+ state.lastValue = value;
166
+ state.lastPublishedAt = Date.now();
167
+ try {
168
+ const buf = await encode({
169
+ labels: entry.config.labels,
170
+ metric: metricCfg.name,
171
+ value,
172
+ units: metricCfg.units,
173
+ timestamp: state.lastPublishedAt,
174
+ });
175
+ await client.publishAsync(entry.topic, buf, { qos: 0 });
176
+ if (listeners.size > 0) {
177
+ const event = {
178
+ labels: entry.config.labels,
179
+ topic: entry.topic,
180
+ metric: metricCfg.name,
181
+ units: metricCfg.units,
182
+ value,
183
+ scenario: sc.active,
184
+ timestamp: state.lastPublishedAt,
185
+ };
186
+ for (const fn of listeners)
187
+ fn(event);
188
+ }
189
+ const scenarioTag = sc.active !== 'normal' ? ` [${sc.active}]` : '';
190
+ const biasTag = state.currentBias !== 0
191
+ ? ` (bias ${state.currentBias > 0 ? '+' : ''}${state.currentBias.toFixed(2)})`
192
+ : '';
193
+ logger_1.logger.debug(`[${metricCfg.name}] ${value.toFixed(2)} ${metricCfg.units} → ${entry.topic}${scenarioTag}${biasTag}`);
194
+ }
195
+ catch (err) {
196
+ logger_1.logger.error(`[sim] publish error for ${metricCfg.name}:`, err);
197
+ }
198
+ }
199
+ //# sourceMappingURL=simulator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simulator.js","sourceRoot":"","sources":["../src/simulator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ mqtt:
2
+ host: localhost
3
+ port: 1883
4
+
5
+ publishIntervalMs: 5000
6
+
7
+ encoding:
8
+ type: protobuf
9
+ protoFile: ./my.proto # path to your .proto file
10
+ messageType: myapp.SensorReading
11
+ # fieldMap: remap internal Reading fields to your proto field names (optional)
12
+ # If your proto field names match the defaults below, fieldMap is not needed.
13
+ #
14
+ # Internal fields available for mapping:
15
+ # labels.* — any label key (e.g. labels.room → room)
16
+ # metric — metric name string
17
+ # value — float reading
18
+ # units — units string
19
+ # timestamp — epoch ms
20
+ #
21
+ fieldMap:
22
+ metric: sensor_name
23
+ value: reading_value
24
+ units: reading_units
25
+ timestamp: recorded_at
26
+
27
+ sources:
28
+ - labels:
29
+ building: hq
30
+ floor: "3"
31
+ zone: east
32
+ topic: "{building}/{floor}/{zone}/data"
33
+ metrics:
34
+ - name: temperature
35
+ units: "°C"
36
+ mode: sinusoidal
37
+ range: { low: 18, high: 26 }
38
+ - name: co2
39
+ units: ppm
40
+ mode: drift
41
+ range: { low: 400, high: 1000 }
42
+ effects:
43
+ - name: hvac
44
+ effects:
45
+ temperature: -0.30
46
+ co2: -0.25
@@ -0,0 +1,139 @@
1
+ mqtt:
2
+ host: localhost
3
+ port: 1883
4
+
5
+ publishIntervalMs: 5000
6
+
7
+ encoding:
8
+ type: json
9
+
10
+ sources:
11
+ - labels:
12
+ org: demo-org
13
+ device: esp32-sim-1
14
+ variety: cherry-tomato
15
+ topic: "{org}/{device}/metrics"
16
+ metrics:
17
+ - name: temperature
18
+ units: "°C"
19
+ mode: sinusoidal
20
+ range: { low: 20, high: 30 }
21
+ periodSeconds: 7200
22
+ - name: humidity
23
+ units: "%"
24
+ mode: sinusoidal
25
+ range: { low: 60, high: 80 }
26
+ periodSeconds: 7200
27
+ - name: co2
28
+ units: ppm
29
+ mode: drift
30
+ range: { low: 400, high: 1200 }
31
+ - name: light
32
+ units: lux
33
+ mode: sinusoidal
34
+ range: { low: 0, high: 50000 }
35
+ periodSeconds: 86400
36
+ - name: nitrogen
37
+ units: "mg/L"
38
+ mode: normal
39
+ range: { low: 150, high: 250 }
40
+ - name: phosphorus
41
+ units: "mg/L"
42
+ mode: normal
43
+ range: { low: 30, high: 60 }
44
+ - name: potassium
45
+ units: "mg/L"
46
+ mode: normal
47
+ range: { low: 200, high: 400 }
48
+ - name: waterLevel
49
+ units: "%"
50
+ mode: drift
51
+ range: { low: 20, high: 95 }
52
+ - name: flowRate
53
+ units: "L/min"
54
+ mode: normal
55
+ range: { low: 0.5, high: 3.0 }
56
+ effects:
57
+ - name: fan
58
+ effects:
59
+ temperature: -0.40
60
+ humidity: -0.30
61
+ co2: -0.40
62
+ - name: heater
63
+ effects:
64
+ temperature: +0.30
65
+ - name: solenoid
66
+ effects:
67
+ humidity: +0.35
68
+ - name: lights
69
+ effects:
70
+ light: +0.80
71
+ - name: pump
72
+ effects:
73
+ waterLevel: +0.35
74
+ flowRate: +0.50
75
+
76
+ - labels:
77
+ org: demo-org
78
+ device: esp32-sim-2
79
+ variety: butterhead-lettuce
80
+ topic: "{org}/{device}/metrics"
81
+ metrics:
82
+ - name: temperature
83
+ units: "°C"
84
+ mode: sinusoidal
85
+ range: { low: 18, high: 24 }
86
+ periodSeconds: 7200
87
+ - name: humidity
88
+ units: "%"
89
+ mode: sinusoidal
90
+ range: { low: 55, high: 75 }
91
+ periodSeconds: 7200
92
+ - name: co2
93
+ units: ppm
94
+ mode: drift
95
+ range: { low: 400, high: 1000 }
96
+ - name: light
97
+ units: lux
98
+ mode: sinusoidal
99
+ range: { low: 0, high: 40000 }
100
+ periodSeconds: 86400
101
+ - name: nitrogen
102
+ units: "mg/L"
103
+ mode: normal
104
+ range: { low: 100, high: 200 }
105
+ - name: phosphorus
106
+ units: "mg/L"
107
+ mode: normal
108
+ range: { low: 20, high: 50 }
109
+ - name: potassium
110
+ units: "mg/L"
111
+ mode: normal
112
+ range: { low: 150, high: 300 }
113
+ - name: waterLevel
114
+ units: "%"
115
+ mode: drift
116
+ range: { low: 20, high: 95 }
117
+ - name: flowRate
118
+ units: "L/min"
119
+ mode: normal
120
+ range: { low: 0.3, high: 2.0 }
121
+ effects:
122
+ - name: fan
123
+ effects:
124
+ temperature: -0.40
125
+ humidity: -0.30
126
+ co2: -0.40
127
+ - name: heater
128
+ effects:
129
+ temperature: +0.30
130
+ - name: solenoid
131
+ effects:
132
+ humidity: +0.35
133
+ - name: lights
134
+ effects:
135
+ light: +0.80
136
+ - name: pump
137
+ effects:
138
+ waterLevel: +0.35
139
+ flowRate: +0.50
@@ -0,0 +1,19 @@
1
+ mqtt:
2
+ host: localhost
3
+ port: 1883
4
+
5
+ publishIntervalMs: 5000
6
+
7
+ # encoding defaults to JSON — no configuration needed
8
+ encoding:
9
+ type: json
10
+
11
+ sources:
12
+ - labels:
13
+ id: sensor-1
14
+ topic: "{id}/metrics"
15
+ metrics:
16
+ - name: temperature
17
+ units: "°C"
18
+ mode: sinusoidal
19
+ range: { low: 18, high: 28 }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "mqtt-scenario-sim",
3
+ "version": "1.0.0",
4
+ "description": "Configurable MQTT sensor simulator with built-in test scenarios. Publish synthetic sensor data over MQTT — as JSON or protobuf — without real hardware.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "mqtt-scenario-sim": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "examples/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "ts-node src/cli.ts",
19
+ "test": "c8 mocha",
20
+ "test:ci": "c8 --reporter=lcov --reporter=text mocha",
21
+ "lint": "eslint src test"
22
+ },
23
+ "keywords": [
24
+ "mqtt",
25
+ "iot",
26
+ "sensor",
27
+ "simulator",
28
+ "testing",
29
+ "mock",
30
+ "scenario",
31
+ "protobuf"
32
+ ],
33
+ "license": "MIT",
34
+ "author": "dislev",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/dislev/mqtt-scenario-sim.git"
38
+ },
39
+ "homepage": "https://github.com/dislev/mqtt-scenario-sim#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/dislev/mqtt-scenario-sim/issues"
42
+ },
43
+ "engines": {
44
+ "node": ">=22.0.0"
45
+ },
46
+ "dependencies": {
47
+ "express": "^5.2.1",
48
+ "js-yaml": "^4.1.0",
49
+ "mqtt": "^5.15.1",
50
+ "protobufjs": "^8.3.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/express": "^5.0.0",
54
+ "@types/js-yaml": "^4.0.9",
55
+ "@types/mocha": "^10.0.10",
56
+ "@types/node": "^22.0.0",
57
+ "c8": "^10.1.3",
58
+ "chai": "^5.2.0",
59
+ "eslint": "^9.0.0",
60
+ "mocha": "^12.0.0-beta-9.4",
61
+ "ts-node": "^10.9.2",
62
+ "typescript": "^5.8.3",
63
+ "typescript-eslint": "^8.59.3"
64
+ }
65
+ }