• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wirenboard / wb-mqtt-smartweb / 47

31 Jul 2026 10:42AM UTC coverage: 33.897% (-0.8%) from 34.703%
47

push

github

web-flow
Use exported vars instead of MAKEFLAGS for coverage options (#40)

307 of 744 branches covered (41.26%)

501 of 1478 relevant lines covered (33.9%)

10.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.35
/src/config_parser.cpp
1
#include "config_parser.h"
2

3
#include <filesystem>
4
#include <set>
5

6
#include "log.h"
7

8
#define LOG(logger) ::logger.Log() << "[config] "
9

10
namespace
11
{
12
    std::unique_ptr<ISmartWebCodec> GetCodec(const Json::Value& data)
223✔
13
    {
14
        if (data.isMember("encoding")) {
223✔
15
            auto enc = data["encoding"].asString();
223✔
16

17
            if (WBMQTT::StringStartsWith(enc, "schedule")) {
446✔
18
                throw std::runtime_error("Encoding '" + enc + "' is not supported");
12✔
19
            }
20
            if (enc == "byte")
211✔
21
                return std::make_unique<TIntCodec<int8_t, 1>>();
×
22
            if (enc == "short")
211✔
23
                return std::make_unique<TIntCodec<int16_t, 1>>();
×
24
            if (enc == "short10")
211✔
25
                return std::make_unique<TIntCodec<int16_t, 10>>();
117✔
26
            if (enc == "short100")
94✔
27
                return std::make_unique<TIntCodec<int16_t, 100>>();
×
28
            if (enc == "ushort")
94✔
29
                return std::make_unique<TIntCodec<uint16_t, 1>>();
×
30
            if (enc == "uint1K")
94✔
31
                return std::make_unique<TIntCodec<uint32_t, 1000>>();
×
32
            if (enc == "uint60K")
94✔
33
                return std::make_unique<TIntCodec<uint32_t, 60000>>();
7✔
34
            if (enc == "ubyte") {
87✔
35
                if (data.isMember("values")) {
87✔
36
                    std::map<uint8_t, std::string> values;
18✔
37
                    const auto& ar = data["values"];
18✔
38
                    for (Json::Value::const_iterator it = ar.begin(); it != ar.end(); ++it) {
252✔
39
                        values.insert({atoi(it.name().c_str()), it->asString()});
234✔
40
                    }
41
                    return std::make_unique<TEnumCodec>(values);
18✔
42
                }
18✔
43
                return std::make_unique<TIntCodec<uint8_t, 1>>();
69✔
44
            }
45
        }
223✔
46
        return std::make_unique<TIntCodec<int16_t, 10>>(); // default codec
×
47
    }
48

49
    std::shared_ptr<TSmartWebParameter> LoadParameter(const Json::Value& param,
303✔
50
                                                      const std::string& name,
51
                                                      const TSmartWebClass* programClass,
52
                                                      uint32_t orderBase)
53
    {
54
        auto p = std::make_shared<TSmartWebParameter>();
303✔
55
        p->Id = param["id"].asUInt();
303✔
56
        p->Name = name;
303✔
57
        p->Type = param.get("type", "value").asString();
303✔
58
        p->ProgramClass = programClass;
303✔
59
        p->Order = orderBase + p->Id;
303✔
60
        return p;
303✔
61
    }
×
62

63
    uint32_t LoadInputs(const Json::Value& data, TSmartWebClass* programClass)
8✔
64
    {
65
        if (!data.isMember("inputs")) {
8✔
66
            return 0;
×
67
        }
68
        uint32_t maxId = 0;
8✔
69
        const auto& ar = data["inputs"];
8✔
70
        for (Json::Value::const_iterator it = ar.begin(); it != ar.end(); ++it) {
84✔
71
            auto p = LoadParameter(*it, it.name(), programClass, 0);
38✔
72
            if (p->Type == "onOff") {
38✔
73
                p->Codec = std::make_unique<TOnOffSensorCodec>();
12✔
74
            } else {
75
                p->Codec = std::make_unique<TSensorCodec>();
26✔
76
            }
77
            LOG(WBMQTT::Debug) << "Input '" << p->Name << "' " << p->Type << " id " << p->Id;
38✔
78
            programClass->Inputs.insert({p->Id, p});
38✔
79
            maxId = std::max(maxId, p->Id);
38✔
80
        }
38✔
81
        return maxId + 1;
8✔
82
    }
83

84
    uint32_t LoadOutputs(const Json::Value& data, TSmartWebClass* programClass, uint32_t orderBase)
8✔
85
    {
86
        if (!data.isMember("outputs")) {
8✔
87
            return orderBase;
×
88
        }
89
        uint32_t maxId = 0;
8✔
90
        const auto& ar = data["outputs"];
8✔
91
        for (Json::Value::const_iterator it = ar.begin(); it != ar.end(); ++it) {
92✔
92
            auto p = LoadParameter(*it, it.name(), programClass, orderBase);
42✔
93
            if (p->Type == "PWM") {
42✔
94
                p->Codec = std::make_unique<TPwmCodec>();
18✔
95
            } else {
96
                p->Codec = std::make_unique<TOutputCodec>();
24✔
97
            }
98
            LOG(WBMQTT::Debug) << "Output '" << p->Name << "' " << p->Type << " id " << p->Id;
42✔
99
            programClass->Outputs.insert({p->Id, p});
42✔
100
            maxId = std::max(maxId, p->Id);
42✔
101
        }
42✔
102
        return orderBase + maxId + 1;
8✔
103
    }
104

105
    uint32_t LoadParameters(const Json::Value& data, TSmartWebClass* programClass, uint32_t orderBase)
8✔
106
    {
107
        if (!data.isMember("parameters")) {
8✔
108
            return orderBase;
×
109
        }
110
        uint32_t maxId = 0;
8✔
111
        const auto& ar = data["parameters"];
8✔
112
        for (Json::Value::const_iterator it = ar.begin(); it != ar.end(); ++it) {
231✔
113
            try {
114
                auto p = LoadParameter(*it, it.name(), programClass, orderBase);
223✔
115
                p->ReadOnly = false;
223✔
116
                WBMQTT::JSON::Get((*it), "readOnly", p->ReadOnly);
446✔
117
                p->Codec = GetCodec(*it);
223✔
118
                if (p->Type == "onOff") {
211✔
119
                    p->Codec = std::make_unique<TOnOffSensorCodec>();
13✔
120
                }
121
                if (p->Type == "temperature" && p->ReadOnly) {
211✔
122
                    p->Codec = std::make_unique<TSensorCodec>();
25✔
123
                }
124
                LOG(WBMQTT::Debug) << "Parameter '" << p->Name << "', " << p->Type << ", id " << p->Id << ", "
211✔
125
                                   << p->Codec->GetName() << (p->ReadOnly ? ", read only" : "");
211✔
126
                programClass->Parameters.insert({p->Id, p});
211✔
127
                maxId = std::max(maxId, p->Id);
211✔
128
            } catch (const std::exception& e) {
235✔
129
                LOG(WBMQTT::Warn) << "Parameter '" << it.name() << "' is ignored. " << e.what();
12✔
130
            }
12✔
131
        }
132
        return orderBase + maxId + 1;
8✔
133
    }
134

135
    /**
136
     * @brief Exception class thrown on open directory failure.
137
     */
138
    class TNoDirError: public std::runtime_error
139
    {
140
    public:
141
        TNoDirError(const std::string& msg): std::runtime_error(msg)
×
142
        {}
×
143
    };
144

145
    void IterateDir(const std::string& dirName, std::function<bool(const std::string&)> fn)
2✔
146
    {
147
        try {
148
            const std::filesystem::path dirPath{dirName};
2✔
149
            std::set<std::filesystem::path> sortedByPath;
2✔
150

151
            for (auto& entry: std::filesystem::directory_iterator(dirPath))
8✔
152
                sortedByPath.insert(entry.path());
8✔
153

154
            for (auto& filePath: sortedByPath) {
8✔
155
                const auto filenameStr = filePath.filename().string();
6✔
156
                if (fn(filenameStr)) {
6✔
157
                    return;
×
158
                }
159
            }
6✔
160
        } catch (std::filesystem::filesystem_error const& ex) {
2✔
161
            throw TNoDirError(ex.what());
×
162
        }
×
163
    }
164

165
    std::string IterateDirByPattern(const std::string& dirName,
2✔
166
                                    const std::string& pattern,
167
                                    std::function<bool(const std::string&)> fn)
168
    {
169
        std::string res;
2✔
170
        IterateDir(dirName, [&](const auto& name) {
2✔
171
            if (name.find(pattern) != std::string::npos) {
6✔
172
                std::string d(dirName + "/" + name);
4✔
173
                if (fn(d)) {
4✔
174
                    res = d;
×
175
                    return true;
×
176
                }
177
            }
4✔
178
            return false;
6✔
179
        });
180
        return res;
2✔
181
    }
×
182

183
    void LoadSmartWebToMqttConfig(TSmartWebToMqttConfig& config,
2✔
184
                                  const Json::Value& configJson,
185
                                  const std::string& classesDir,
186
                                  const Json::Value& classSchema,
187
                                  TDeviceClassSource source)
188
    {
189
        if (configJson.isMember("poll_interval_ms")) {
2✔
190
            config.PollInterval = std::chrono::milliseconds(configJson["poll_interval_ms"].asUInt());
2✔
191
        }
192

193
        try {
194
            IterateDirByPattern(classesDir, ".json", [classSchema, &config, source](const std::string& filePath) {
4✔
195
                try {
196
                    auto classJson = WBMQTT::JSON::Parse(filePath);
4✔
197
                    WBMQTT::JSON::Validate(classJson, classSchema);
4✔
198
                    LoadSmartWebClass(config, classJson, source);
4✔
199
                } catch (const std::exception& e) {
4✔
200
                    LOG(WBMQTT::Error) << "Failed to parse " << filePath << "\n" << e.what();
×
201
                }
×
202

203
                return false; // continue scan
4✔
204
            });
205
        } catch (std::filesystem::filesystem_error const& ex) {
×
206
            LOG(WBMQTT::Error) << "Cannot open " << classesDir << " directory: " << ex.what();
×
207
            return;
×
208
        }
×
209
    }
210

211
    void LoadTiming(TMqttToSmartWebConfig& controller, const std::string& mqtt_channel, const Json::Value& configJson)
2✔
212
    {
213
        auto& mqtt_channel_timing = controller.MqttChannelsTiming[mqtt_channel];
2✔
214
        mqtt_channel_timing.refresh_last_update_timepoint();
2✔
215
        if (configJson.isMember("value_timeout_min")) {
2✔
216
            mqtt_channel_timing.ValueTimeoutMin = TTimeIntervalMin(configJson["value_timeout_min"].asInt());
×
217
        }
218
    }
2✔
219

220
    TMqttToSmartWebConfig LoadMqttToSmartWebController(const Json::Value& configJson)
1✔
221
    {
222
        TMqttToSmartWebConfig res;
1✔
223
        res.ProgramId = configJson["controller_id"].asUInt();
1✔
224

225
        if (configJson.isMember("sensors")) {
1✔
226
            for (const auto& sensor: configJson["sensors"]) {
3✔
227
                const auto& mqtt_channel = sensor["channel"].asString();
1✔
228
                LoadTiming(res, mqtt_channel, sensor);
1✔
229
                SmartWeb::TParameterInfo parameter_info{0};
1✔
230
                parameter_info.parameter_id = SmartWeb::Controller::Parameters::SENSOR;
1✔
231
                parameter_info.program_type = SmartWeb::PT_CONTROLLER;
1✔
232
                parameter_info.index = sensor["sensor_index"].asUInt();
1✔
233

234
                LOG(WBMQTT::Info) << "Controller: " << (int)res.ProgramId << " map sensor {"
1✔
235
                                  << "parameter_index: " << (int)parameter_info.index << ", "
1✔
236
                                  << "raw " << (int)parameter_info.raw << "} to {channel: " << mqtt_channel << "};";
1✔
237

238
                if (res.ParameterMapping.count(parameter_info.raw)) {
1✔
239
                    throw std::runtime_error("Malformed JSON config: duplicate sensor");
×
240
                }
241

242
                res.ParameterMapping[parameter_info.raw].from_string(mqtt_channel);
1✔
243
                res.ParameterCount = std::max(res.ParameterCount, uint8_t(parameter_info.index + 1));
1✔
244

245
                // Sensor is also accesible as output with index = sensor_index - 1
246
                auto outputIndex = parameter_info.index - 1;
1✔
247

248
                if (res.OutputMapping[outputIndex].is_initialized()) {
1✔
249
                    throw std::runtime_error("Malformed JSON config: duplicate output " + std::to_string(outputIndex));
×
250
                }
251

252
                res.OutputMapping[outputIndex].from_string(mqtt_channel);
1✔
253
            }
1✔
254
        }
255

256
        if (configJson.isMember("parameters")) {
1✔
257
            for (const auto& parameter: configJson["parameters"]) {
3✔
258
                const auto& mqtt_channel = parameter["channel"].asString();
1✔
259
                LoadTiming(res, mqtt_channel, configJson);
1✔
260
                SmartWeb::TParameterInfo parameter_info{0};
1✔
261
                parameter_info.parameter_id = parameter["parameter_id"].asUInt();
1✔
262
                parameter_info.program_type = parameter["program_type"].asUInt();
1✔
263
                parameter_info.index = parameter["parameter_index"].asUInt();
1✔
264

265
                LOG(WBMQTT::Info) << "Controller: " << (int)res.ProgramId << " map parameter {"
1✔
266
                                  << "program_type: " << (int)parameter_info.program_type << ", "
1✔
267
                                  << "parameter_id: " << (int)parameter_info.parameter_id << ", "
1✔
268
                                  << "parameter_index: " << (int)parameter_info.index << ", "
1✔
269
                                  << "raw " << (int)parameter_info.raw << "} to {channel: " << mqtt_channel << "};";
1✔
270

271
                if (res.ParameterMapping.count(parameter_info.raw)) {
1✔
272
                    throw std::runtime_error("Malformed JSON config: duplicate parameter");
×
273
                }
274

275
                res.ParameterMapping[parameter_info.raw].from_string(mqtt_channel);
1✔
276
                res.ParameterCount = std::max(res.ParameterCount, uint8_t(parameter_info.index + 1));
1✔
277
            }
1✔
278
        }
279

280
        if (!configJson.isMember("parameters") && !configJson.isMember("sensors")) {
1✔
281
            throw std::runtime_error("Malformed JSON config: no parameter or sensor in mapping");
×
282
        }
283
        return res;
1✔
284
    }
×
285

286
    void LoadMqttToSmartWebConfig(TConfig& config, const Json::Value& configJson)
1✔
287
    {
288
        if (configJson.isMember("debug")) {
1✔
289
            config.Debug = configJson["debug"].asBool();
1✔
290
        }
291

292
        if (configJson.isMember("interface_name")) {
1✔
293
            config.InterfaceName = configJson["interface_name"].asString();
1✔
294
        }
295

296
        for (const auto& controller: configJson["controllers"]) {
2✔
297
            try {
298
                config.Controllers.push_back(LoadMqttToSmartWebController(controller));
1✔
299
            } catch (std::exception& e) {
×
300
                LOG(WBMQTT::Error) << e.what();
×
301
            }
×
302
        }
303
    }
1✔
304
}
305

306
void LoadSmartWebClass(TSmartWebToMqttConfig& config, const Json::Value& data, TDeviceClassSource source)
8✔
307
{
308
    const auto programType = data["programType"].asUInt();
8✔
309
    const auto className = data["class"].asString();
8✔
310

311
    auto classIt = config.Classes.find(programType);
8✔
312

313
    if (classIt != config.Classes.end()) {
8✔
314
        if (classIt->second->Source == source) {
1✔
315
            LOG(WBMQTT::Warn) << "Program type: " << programType << " is already defined";
×
316
            return;
×
317
        }
318

319
        // Reject changes if there is an attempt to overwrite the user class with a built-in class
320
        if (classIt->second->Source == TDeviceClassSource::USER) {
1✔
321
            return;
×
322
        }
323
    }
324

325
    auto cl = std::make_shared<TSmartWebClass>();
8✔
326
    cl->Type = programType;
8✔
327
    cl->Name = className;
8✔
328
    cl->Source = source;
8✔
329
    LOG(WBMQTT::Debug) << "Loading class '" << cl->Name << "' (program type = " << programType << ")";
8✔
330

331
    if (data.isMember("implements")) {
8✔
332
        for (const auto& parent: data["implements"]) {
16✔
333
            cl->ParentClasses.push_back(parent.asString());
8✔
334
        }
335
    }
336

337
    uint32_t orderBase = LoadInputs(data, cl.get());
8✔
338
    orderBase = LoadOutputs(data, cl.get(), orderBase);
8✔
339
    LoadParameters(data, cl.get(), orderBase);
8✔
340

341
    if (classIt != config.Classes.end()) {
8✔
342
        LOG(WBMQTT::Info) << "Overriding a built-in device class '" << className << "' in *.d/classes";
1✔
343
        classIt->second = cl;
1✔
344
    } else {
345
        config.Classes.insert({programType, cl});
7✔
346
    }
347

348
    LOG(WBMQTT::Info) << "Class '" << cl->Name << "' (program type = " << programType << ") is loaded";
8✔
349
}
8✔
350

351
void LoadConfig(TConfig& config,
1✔
352
                const std::string& configFilePath,
353
                const std::string& pathToDeviceClassDirectory,
354
                const std::string& pathToBuiltInDeviceClassDirectory,
355
                const std::string& configSchemaFileName,
356
                const std::string& classSchemaFileName)
357
{
358
    Json::Value configJson = WBMQTT::JSON::Parse(configFilePath);
1✔
359
    WBMQTT::JSON::Validate(configJson, WBMQTT::JSON::Parse(configSchemaFileName));
1✔
360

361
    LoadMqttToSmartWebConfig(config, configJson);
1✔
362

363
    Json::Value classSchema = WBMQTT::JSON::Parse(classSchemaFileName);
1✔
364
    LoadSmartWebToMqttConfig(config.SmartWebToMqtt,
1✔
365
                             configJson,
366
                             pathToBuiltInDeviceClassDirectory,
367
                             classSchema,
368
                             TDeviceClassSource::BUILTIN);
369
    LoadSmartWebToMqttConfig(config.SmartWebToMqtt,
1✔
370
                             configJson,
371
                             pathToDeviceClassDirectory,
372
                             classSchema,
373
                             TDeviceClassSource::USER);
374
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc