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

wirenboard / wb-mqtt-serial / 2

29 Dec 2025 12:28PM UTC coverage: 76.817% (+4.0%) from 72.836%
2

Pull #1045

github

54aa0c
pgasheev
up changelog
Pull Request #1045: Fix firmware version in WB-M1W2 template

6873 of 9161 branches covered (75.02%)

12966 of 16879 relevant lines covered (76.82%)

1651.61 hits per line

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

76.34
/src/templates_map.cpp
1
#include "templates_map.h"
2

3
#include <filesystem>
4

5
#include "expression_evaluator.h"
6
#include "file_utils.h"
7
#include "json_common.h"
8
#include "log.h"
9
#include "serial_config.h"
10

11
#define LOG(logger) ::logger.Log() << "[templates] "
12

13
using namespace std;
14
using namespace WBMQTT::JSON;
15

16
namespace
17
{
18
    bool EndsWith(const string& str, const string& suffix)
2,054✔
19
    {
20
        return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
2,054✔
21
    }
22

23
    void CheckNesting(const Json::Value& root, size_t nestingLevel, TSubDevicesTemplateMap& templates)
30✔
24
    {
25
        if (nestingLevel > 5) {
30✔
26
            throw TConfigParserException(
×
27
                "Too deep subdevices nesting. This could be caused by cyclic subdevice dependencies");
×
28
        }
29
        for (const auto& ch: root["device"]["channels"]) {
52✔
30
            if (ch.isMember("device_type")) {
22✔
31
                CheckNesting(templates.GetTemplate(ch["device_type"].asString()).Schema, nestingLevel + 1, templates);
12✔
32
            }
33
            if (ch.isMember("oneOf")) {
22✔
34
                for (const auto& subdeviceType: ch["oneOf"]) {
14✔
35
                    CheckNesting(templates.GetTemplate(subdeviceType.asString()).Schema, nestingLevel + 1, templates);
10✔
36
                }
37
            }
38
        }
39
    }
30✔
40

41
    void ValidateConditionAndAddDependencies(Json::Value& node, Expressions::TExpressionsCache& exprCache)
49,498✔
42
    {
43
        if (node.isMember("condition")) {
49,498✔
44
            auto condition = node["condition"].asString();
46,040✔
45
            auto itExpr = exprCache.find(condition);
23,020✔
46
            if (itExpr == exprCache.end()) {
23,020✔
47
                Expressions::TParser parser;
11,186✔
48
                parser.Parse(condition);
11,186✔
49
                itExpr = exprCache.emplace(condition, parser.Parse(condition)).first;
11,180✔
50
            }
51
            const auto dependencies = Expressions::GetDependencies(itExpr->second.get());
46,028✔
52
            if (!dependencies.empty()) {
23,014✔
53
                Json::Value dependenciesArray(Json::arrayValue);
46,028✔
54
                for (const auto& dep: dependencies) {
55,750✔
55
                    dependenciesArray.append(dep);
32,736✔
56
                }
57
                node["dependencies"] = dependenciesArray;
23,014✔
58
            }
59
        }
60
    }
49,492✔
61

62
    std::string GetNodeName(const Json::Value& node, const std::string& name)
6✔
63
    {
64
        const std::vector<std::string> keys = {"name", "title"};
36✔
65
        for (const auto& key: keys) {
10✔
66
            if (node.isMember(key)) {
10✔
67
                return node[key].asString();
6✔
68
            }
69
        }
70
        return name;
×
71
    }
72

73
    void ValidateConditionsAndAddDependencies(Json::Value& deviceTemplate)
486✔
74
    {
75
        Expressions::TExpressionsCache exprCache;
972✔
76
        std::vector<std::string> sections = {"channels", "setup", "parameters"};
3,402✔
77
        for (const auto& section: sections) {
1,932✔
78
            if (deviceTemplate.isMember(section)) {
1,452✔
79
                Json::Value& sectionNodes = deviceTemplate[section];
836✔
80
                for (auto it = sectionNodes.begin(); it != sectionNodes.end(); ++it) {
50,328✔
81
                    try {
82
                        ValidateConditionAndAddDependencies(*it, exprCache);
49,498✔
83
                    } catch (const runtime_error& e) {
12✔
84
                        throw runtime_error("Failed to parse condition in " + section + "[" +
18✔
85
                                            GetNodeName(*it, it.name()) + "]: " + e.what());
24✔
86
                    }
87
                }
88
            }
89
        }
90
    }
480✔
91

92
    bool CheckParameterProperty(std::unordered_map<std::string, Json::Value>& map,
18,940✔
93
                                const Json::Value& parameter,
94
                                const std::string& propertyName,
95
                                std::string& error)
96
    {
97
        std::string id = parameter["id"].asString();
37,880✔
98
        Json::Value value = parameter[propertyName];
37,880✔
99
        auto it = map.find(id);
18,940✔
100
        if (it != map.end() && it->second != value) {
18,940✔
101
            error = "Parameter \"" + id + "\" has several declarations with different \"" + propertyName +
4✔
102
                    "\" values (" + (it->second.isNull() ? "[null]" : it->second.asString()) + " and " +
4✔
103
                    (value.isNull() ? "[null]" : value.asString()) + "). ";
2✔
104
            return false;
2✔
105
        }
106
        map[id] = value;
18,938✔
107
        return true;
18,938✔
108
    }
109

110
    void ValidateParameterProperties(const Json::Value& parameters)
480✔
111
    {
112
        if (!parameters.isArray()) {
480✔
113
            return;
368✔
114
        }
115
        std::unordered_map<std::string, Json::Value> writeAddressMap;
224✔
116
        std::unordered_map<std::string, Json::Value> addressMap;
224✔
117
        std::unordered_map<std::string, Json::Value> fwVersionMap;
224✔
118
        std::string error;
224✔
119
        for (const auto& parameter: parameters) {
6,424✔
120
            if (!CheckParameterProperty(writeAddressMap, parameter, SerialConfig::WRITE_ADDRESS_PROPERTY_NAME, error) ||
18,942✔
121
                !CheckParameterProperty(addressMap, parameter, SerialConfig::ADDRESS_PROPERTY_NAME, error) ||
25,254✔
122
                !CheckParameterProperty(fwVersionMap, parameter, SerialConfig::FW_VERSION_PROPERTY_NAME, error))
12,626✔
123
            {
124
                break;
2✔
125
            }
126
        }
127
        if (!error.empty()) {
112✔
128
            throw std::runtime_error(
2✔
129
                error + "All parameter declarations with the same id must have the same addresses and FW versions.");
4✔
130
        }
131
    }
132

133
    void TemplateUpdatedWarning(PDeviceTemplate deviceTemplate, const std::string& path)
×
134
    {
135
        LOG(Warn) << "Existing template data for device type '" << deviceTemplate->Type << "' (from file "
×
136
                  << deviceTemplate->GetFilePath() << ") replaced with contents of file " << path;
×
137
    }
138

139
    void ConvertParametersObjectToArray(Json::Value& deviceTemplate)
470✔
140
    {
141
        auto& device = deviceTemplate["device"];
470✔
142
        if (device.isMember("parameters") && device["parameters"].isObject()) {
470✔
143
            Json::Value parametersArray(Json::arrayValue);
256✔
144
            for (auto it = device["parameters"].begin(); it != device["parameters"].end(); ++it) {
7,310✔
145
                (*it)["id"] = it.key().asString();
7,182✔
146
                parametersArray.append((*it));
7,182✔
147
            }
148
            device["parameters"] = std::move(parametersArray);
128✔
149
        }
150
    }
470✔
151
}
152

153
//=============================================================================
154
//                                TTemplateMap
155
//=============================================================================
156
TTemplateMap::TTemplateMap(const Json::Value& templateSchema): Validator(new WBMQTT::JSON::TValidator(templateSchema))
42✔
157
{}
42✔
158

159
PDeviceTemplate TTemplateMap::MakeTemplateFromJson(const Json::Value& data, const std::string& filePath)
1,782✔
160
{
161
    std::string deviceType = data["device_type"].asString();
3,564✔
162
    auto deviceTemplate = std::make_shared<TDeviceTemplate>(deviceType,
163
                                                            data["device"].get("protocol", "modbus").asString(),
3,564✔
164
                                                            Validator,
1,782✔
165
                                                            filePath);
1,782✔
166
    deviceTemplate->SetTitle(GetTranslations(data.get("title", "").asString(), data["device"]));
1,782✔
167
    deviceTemplate->SetGroup(data.get("group", "").asString());
1,782✔
168
    if (data.get("deprecated", false).asBool()) {
1,782✔
169
        deviceTemplate->SetDeprecated();
182✔
170
    }
171
    if (data["device"].isMember("subdevices")) {
1,782✔
172
        deviceTemplate->SetWithSubdevices();
92✔
173
    }
174
    if (data.isMember("hw")) {
1,782✔
175
        std::vector<TDeviceTemplateHardware> hws;
548✔
176
        for (const auto& hwItem: data["hw"]) {
590✔
177
            TDeviceTemplateHardware hw;
632✔
178
            Get(hwItem, "signature", hw.Signature);
316✔
179
            Get(hwItem, "fw", hw.Fw);
316✔
180
            hws.push_back(std::move(hw));
316✔
181
        }
182
        deviceTemplate->SetHardware(hws);
274✔
183
    }
184
    deviceTemplate->SetMqttId(data["device"].get("id", "").asString());
1,782✔
185
    return deviceTemplate;
3,564✔
186
}
187

188
void TTemplateMap::AddTemplatesDir(const std::string& templatesDir,
44✔
189
                                   bool passInvalidTemplates,
190
                                   const Json::Value& settings)
191
{
192
    std::unique_lock m(Mutex);
88✔
193
    PreferredTemplatesDir = templatesDir;
44✔
194
    IterateDirByPattern(
44✔
195
        templatesDir,
196
        ".json",
197
        [&](const std::string& filepath) {
2,054✔
198
            if (!EndsWith(filepath, ".json")) {
2,054✔
199
                return false;
272✔
200
            }
201
            try {
202
                auto deviceTemplate =
203
                    MakeTemplateFromJson(WBMQTT::JSON::ParseWithSettings(filepath, settings), filepath);
3,564✔
204
                auto typeData = Templates.try_emplace(deviceTemplate->Type, std::vector<PDeviceTemplate>{});
1,782✔
205
                if (!typeData.second) {
1,782✔
206
                    TemplateUpdatedWarning(typeData.first->second.back(), filepath);
×
207
                }
208
                typeData.first->second.push_back(deviceTemplate);
1,782✔
209
            } catch (const std::exception& e) {
×
210
                if (passInvalidTemplates) {
×
211
                    LOG(Error) << "Failed to parse " << filepath << "\n" << e.what();
×
212
                    return false;
×
213
                }
214
                throw;
×
215
            }
216
            return false;
1,782✔
217
        },
218
        true);
88✔
219
}
44✔
220

221
PDeviceTemplate TTemplateMap::GetTemplate(const std::string& deviceType)
268✔
222
{
223
    try {
224
        std::unique_lock m(Mutex);
536✔
225
        return Templates.at(deviceType).back();
534✔
226
    } catch (const std::out_of_range&) {
4✔
227
        throw std::out_of_range("Can't find template for '" + deviceType + "'");
2✔
228
    }
229
}
230

231
std::vector<PDeviceTemplate> TTemplateMap::GetTemplates()
2✔
232
{
233
    std::unique_lock m(Mutex);
4✔
234
    std::vector<PDeviceTemplate> templates;
2✔
235
    for (const auto& t: Templates) {
476✔
236
        templates.push_back(t.second.back());
474✔
237
    }
238
    return templates;
4✔
239
}
240

241
std::vector<std::string> TTemplateMap::UpdateTemplate(const std::string& path)
×
242
{
243
    std::vector<std::string> res;
×
244
    if (!EndsWith(path, ".json")) {
×
245
        return res;
×
246
    }
247
    std::unique_lock m(Mutex);
×
248
    auto deletedType = DeleteTemplateUnsafe(path);
×
249
    if (!deletedType.empty()) {
×
250
        res.push_back(deletedType);
×
251
    }
252
    auto deviceTemplate = MakeTemplateFromJson(WBMQTT::JSON::Parse(path), path);
×
253
    auto& typeArray = Templates.try_emplace(deviceTemplate->Type, std::vector<PDeviceTemplate>{}).first->second;
×
254
    if (!PreferredTemplatesDir.empty() && WBMQTT::StringStartsWith(path, PreferredTemplatesDir)) {
×
255
        TemplateUpdatedWarning(typeArray.back(), path);
×
256
        typeArray.push_back(deviceTemplate);
×
257
    } else {
258
        typeArray.insert(typeArray.begin(), deviceTemplate);
×
259
    }
260
    if (deviceTemplate->Type != deletedType) {
×
261
        res.push_back(deviceTemplate->Type);
×
262
    }
263
    return res;
×
264
}
265

266
std::string TTemplateMap::DeleteTemplateUnsafe(const std::string& path)
×
267
{
268
    for (auto& deviceTemplates: Templates) {
×
269
        auto item = std::find_if(deviceTemplates.second.begin(), deviceTemplates.second.end(), [&](const auto& t) {
×
270
            return t->GetFilePath() == path;
×
271
        });
×
272
        if (item != deviceTemplates.second.end()) {
×
273
            auto deviceType = deviceTemplates.first;
×
274
            if (deviceTemplates.second.size() > 1) {
×
275
                deviceTemplates.second.erase(item);
×
276
            } else {
277
                Templates.erase(deviceType);
×
278
            }
279
            return deviceType;
×
280
        }
281
    }
282

283
    return std::string();
×
284
}
285

286
std::string TTemplateMap::DeleteTemplate(const std::string& path)
×
287
{
288
    std::unique_lock m(Mutex);
×
289
    return DeleteTemplateUnsafe(path);
×
290
}
291

292
//=============================================================================
293
//                              TDeviceTemplate
294
//=============================================================================
295
TDeviceTemplate::TDeviceTemplate(const std::string& type,
1,782✔
296
                                 const std::string& protocol,
297
                                 std::shared_ptr<WBMQTT::JSON::TValidator> validator,
298
                                 const std::string& filePath)
1,782✔
299
    : Type(type),
300
      Deprecated(false),
301
      Validator(validator),
302
      FilePath(filePath),
303
      Subdevices(false),
304
      Protocol(protocol)
1,782✔
305
{}
1,782✔
306

307
std::string TDeviceTemplate::GetTitle(const std::string& lang) const
60✔
308
{
309
    auto it = Title.find(lang);
60✔
310
    if (it != Title.end()) {
60✔
311
        return it->second;
4✔
312
    }
313
    if (lang != "en") {
56✔
314
        it = Title.find("en");
×
315
        if (it != Title.end()) {
×
316
            return it->second;
×
317
        }
318
    }
319
    return Type;
56✔
320
}
321

322
const std::string& TDeviceTemplate::GetGroup() const
×
323
{
324
    return Group;
×
325
}
326

327
const std::vector<TDeviceTemplateHardware>& TDeviceTemplate::GetHardware() const
56✔
328
{
329
    return Hardware;
56✔
330
}
331

332
bool TDeviceTemplate::IsDeprecated() const
548✔
333
{
334
    return Deprecated;
548✔
335
}
336

337
void TDeviceTemplate::SetDeprecated()
182✔
338
{
339
    Deprecated = true;
182✔
340
}
182✔
341

342
void TDeviceTemplate::SetGroup(const std::string& group)
1,782✔
343
{
344
    if (!group.empty()) {
1,782✔
345
        Group = group;
1,272✔
346
    }
347
}
1,782✔
348

349
void TDeviceTemplate::SetTitle(const std::unordered_map<std::string, std::string>& translations)
1,782✔
350
{
351
    Title = translations;
1,782✔
352
}
1,782✔
353

354
void TDeviceTemplate::SetHardware(const std::vector<TDeviceTemplateHardware>& hardware)
274✔
355
{
356
    Hardware = hardware;
274✔
357
}
274✔
358

359
const std::string& TDeviceTemplate::GetFilePath() const
560✔
360
{
361
    return FilePath;
560✔
362
}
363

364
const Json::Value& TDeviceTemplate::GetTemplate()
1,558✔
365
{
366
    if (Template.isNull()) {
1,558✔
367
        Json::Value root(WBMQTT::JSON::Parse(GetFilePath()));
1,096✔
368
        // Skip deprecated template validation, it may be broken according to latest schema
369
        if (!IsDeprecated()) {
548✔
370
            try {
371
                Validator->Validate(root);
490✔
372
                ValidateConditionsAndAddDependencies(root["device"]);
486✔
373
                // Check that parameters with same ids have same addresses (for parameters declared as array)
374
                ValidateParameterProperties(root["device"]["parameters"]);
480✔
375
            } catch (const std::runtime_error& e) {
24✔
376
                throw std::runtime_error("File: " + GetFilePath() + " error: " + e.what());
12✔
377
            }
378
            // Check that channels refer to valid subdevices and they are not nested too deep
379
            if (WithSubdevices()) {
478✔
380
                TSubDevicesTemplateMap subdevices(Type, root["device"]);
16✔
381
                CheckNesting(root, 0, subdevices);
8✔
382
            } else {
383
                ConvertParametersObjectToArray(root);
470✔
384
            }
385
        }
386
        Template = root["device"];
536✔
387
    }
388
    return Template;
1,546✔
389
}
390

391
void TDeviceTemplate::SetWithSubdevices()
92✔
392
{
393
    Subdevices = true;
92✔
394
}
92✔
395

396
bool TDeviceTemplate::WithSubdevices() const
554✔
397
{
398
    return Subdevices;
554✔
399
}
400

401
const std::string& TDeviceTemplate::GetProtocol() const
×
402
{
403
    return Protocol;
×
404
}
405

406
void TDeviceTemplate::SetMqttId(const std::string& id)
1,782✔
407
{
408
    MqttId = id;
1,782✔
409
}
1,782✔
410

411
const std::string& TDeviceTemplate::GetMqttId() const
×
412
{
413
    return MqttId;
×
414
}
415

416
//=============================================================================
417
//                          TSubDevicesTemplateMap
418
//=============================================================================
419
TSubDevicesTemplateMap::TSubDevicesTemplateMap(const std::string& deviceType, const Json::Value& device)
584✔
420
    : DeviceType(deviceType)
584✔
421
{
422
    if (device.isMember("subdevices")) {
584✔
423
        AddSubdevices(device["subdevices"]);
58✔
424

425
        // Check that channels refer to valid subdevices
426
        for (const auto& subdeviceTemplate: Templates) {
376✔
427
            for (const auto& ch: subdeviceTemplate.second.Schema["channels"]) {
1,004✔
428
                if (ch.isMember("device_type")) {
686✔
429
                    TSubDevicesTemplateMap::GetTemplate(ch["device_type"].asString());
144✔
430
                }
431
                if (ch.isMember("oneOf")) {
686✔
432
                    for (const auto& subdeviceType: ch["oneOf"]) {
732✔
433
                        TSubDevicesTemplateMap::GetTemplate(subdeviceType.asString());
680✔
434
                    }
435
                }
436
            }
437
        }
438
    }
439
}
584✔
440

441
void TSubDevicesTemplateMap::AddSubdevices(const Json::Value& subdevicesArray)
58✔
442
{
443
    for (auto& dev: subdevicesArray) {
376✔
444
        auto deviceType = dev["device_type"].asString();
636✔
445
        if (Templates.count(deviceType)) {
318✔
446
            LOG(Warn) << "Device type '" << DeviceType << "'. Duplicate subdevice type '" << deviceType << "'";
×
447
        } else {
448
            auto deviceTypeTitle = deviceType;
318✔
449
            Get(dev, "title", deviceTypeTitle);
318✔
450
            Templates.insert({deviceType, {deviceType, deviceTypeTitle, dev["device"]}});
318✔
451
        }
452
    }
453
}
58✔
454

455
const TSubDeviceTemplate& TSubDevicesTemplateMap::GetTemplate(const std::string& deviceType)
10,094✔
456
{
457
    try {
458
        return Templates.at(deviceType);
10,094✔
459
    } catch (const std::out_of_range&) {
×
460
        throw std::out_of_range("Device type '" + DeviceType + "'. Can't find template for subdevice '" + deviceType +
×
461
                                "'");
×
462
    }
463
}
464

465
std::vector<std::string> TSubDevicesTemplateMap::GetDeviceTypes() const
×
466
{
467
    std::vector<std::string> res;
×
468
    for (const auto& elem: Templates) {
×
469
        res.push_back(elem.first);
×
470
    }
471
    return res;
×
472
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc