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

wirenboard / wb-mqtt-serial / 671

14 Jul 2025 12:05PM UTC coverage: 73.833% (-0.02%) from 73.854%
671

push

github

web-flow
Add warning logs if device template data updated

6445 of 9062 branches covered (71.12%)

3 of 8 new or added lines in 1 file covered. (37.5%)

1 existing line in 1 file now uncovered.

12342 of 16716 relevant lines covered (73.83%)

377.28 hits per line

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

73.88
/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)
987✔
19
    {
20
        return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
987✔
21
    }
22

23
    void CheckNesting(const Json::Value& root, size_t nestingLevel, TSubDevicesTemplateMap& templates)
575✔
24
    {
25
        if (nestingLevel > 5) {
575✔
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"]) {
714✔
30
            if (ch.isMember("device_type")) {
139✔
31
                CheckNesting(templates.GetTemplate(ch["device_type"].asString()).Schema, nestingLevel + 1, templates);
23✔
32
            }
33
            if (ch.isMember("oneOf")) {
139✔
34
                for (const auto& subdeviceType: ch["oneOf"]) {
561✔
35
                    CheckNesting(templates.GetTemplate(subdeviceType.asString()).Schema, nestingLevel + 1, templates);
542✔
36
                }
37
            }
38
        }
39
    }
575✔
40

41
    void ValidateCondition(const Json::Value& node, std::unordered_set<std::string>& validConditions)
22,509✔
42
    {
43
        if (node.isMember("condition")) {
22,509✔
44
            auto condition = node["condition"].asString();
18,860✔
45
            if (validConditions.find(condition) == validConditions.end()) {
9,430✔
46
                Expressions::TParser parser;
3,925✔
47
                parser.Parse(condition);
3,925✔
48
                validConditions.insert(condition);
3,922✔
49
            }
50
        }
51
    }
22,506✔
52

53
    std::string GetNodeName(const Json::Value& node, const std::string& name)
3✔
54
    {
55
        const std::vector<std::string> keys = {"name", "title"};
18✔
56
        for (const auto& key: keys) {
5✔
57
            if (node.isMember(key)) {
5✔
58
                return node[key].asString();
3✔
59
            }
60
        }
61
        return name;
×
62
    }
63

64
    void ValidateConditions(Json::Value& deviceTemplate)
238✔
65
    {
66
        std::unordered_set<std::string> validConditions;
476✔
67
        std::vector<std::string> sections = {"channels", "setup", "parameters"};
1,666✔
68
        for (const auto& section: sections) {
946✔
69
            if (deviceTemplate.isMember(section)) {
711✔
70
                Json::Value& sectionNodes = deviceTemplate[section];
400✔
71
                for (auto it = sectionNodes.begin(); it != sectionNodes.end(); ++it) {
22,906✔
72
                    try {
73
                        ValidateCondition(*it, validConditions);
22,509✔
74
                    } catch (const runtime_error& e) {
6✔
75
                        throw runtime_error("Failed to parse condition in " + section + "[" +
9✔
76
                                            GetNodeName(*it, it.name()) + "]: " + e.what());
12✔
77
                    }
78
                }
79
            }
80
        }
81
    }
235✔
82

83
    bool CheckParameterProperty(std::unordered_map<std::string, Json::Value>& map,
7,958✔
84
                                const Json::Value& parameter,
85
                                const std::string& propertyName,
86
                                std::string& error)
87
    {
88
        std::string id = parameter["id"].asString();
15,916✔
89
        Json::Value value = parameter[propertyName];
15,916✔
90
        auto it = map.find(id);
7,958✔
91
        if (it != map.end() && it->second != value) {
7,958✔
92
            error = "Parameter \"" + id + "\" has several declarations with different \"" + propertyName +
2✔
93
                    "\" values (" + (it->second.isNull() ? "[null]" : it->second.asString()) + " and " +
2✔
94
                    (value.isNull() ? "[null]" : value.asString()) + "). ";
1✔
95
            return false;
1✔
96
        }
97
        map[id] = value;
7,957✔
98
        return true;
7,957✔
99
    }
100

101
    void ValidateParameterProperties(const Json::Value& parameters)
235✔
102
    {
103
        if (!parameters.isArray()) {
235✔
104
            return;
188✔
105
        }
106
        std::unordered_map<std::string, Json::Value> writeAddressMap;
94✔
107
        std::unordered_map<std::string, Json::Value> addressMap;
94✔
108
        std::unordered_map<std::string, Json::Value> fwVersionMap;
94✔
109
        std::string error;
94✔
110
        for (const auto& parameter: parameters) {
2,699✔
111
            if (!CheckParameterProperty(writeAddressMap, parameter, SerialConfig::WRITE_ADDRESS_PROPERTY_NAME, error) ||
7,959✔
112
                !CheckParameterProperty(addressMap, parameter, SerialConfig::ADDRESS_PROPERTY_NAME, error) ||
10,611✔
113
                !CheckParameterProperty(fwVersionMap, parameter, SerialConfig::FW_VERSION_PROPERTY_NAME, error))
5,305✔
114
            {
115
                break;
1✔
116
            }
117
        }
118
        if (!error.empty()) {
47✔
119
            throw std::runtime_error(
1✔
120
                error + "All parameter declarations with the same id must have the same addresses and FW versions.");
2✔
121
        }
122
    }
123

NEW
124
    void TemplateUpdatedWarning(PDeviceTemplate deviceTemplate, const std::string& path)
×
125
    {
NEW
126
        LOG(Warn) << "Existing template data for device type '" << deviceTemplate->Type << "' (from file "
×
NEW
127
                  << deviceTemplate->GetFilePath() << ") replaced with contents of file " << path;
×
128
    }
129
}
130

131
//=============================================================================
132
//                                TTemplateMap
133
//=============================================================================
134
TTemplateMap::TTemplateMap(const Json::Value& templateSchema): Validator(new WBMQTT::JSON::TValidator(templateSchema))
21✔
135
{}
21✔
136

137
PDeviceTemplate TTemplateMap::MakeTemplateFromJson(const Json::Value& data, const std::string& filePath)
863✔
138
{
139
    std::string deviceType = data["device_type"].asString();
1,726✔
140
    auto deviceTemplate = std::make_shared<TDeviceTemplate>(deviceType,
141
                                                            data["device"].get("protocol", "modbus").asString(),
1,726✔
142
                                                            Validator,
863✔
143
                                                            filePath);
863✔
144
    deviceTemplate->SetTitle(GetTranslations(data.get("title", "").asString(), data["device"]));
863✔
145
    deviceTemplate->SetGroup(data.get("group", "").asString());
863✔
146
    if (data.get("deprecated", false).asBool()) {
863✔
147
        deviceTemplate->SetDeprecated();
70✔
148
    }
149
    if (data["device"].isMember("subdevices")) {
863✔
150
        deviceTemplate->SetWithSubdevices();
46✔
151
    }
152
    if (data.isMember("hw")) {
863✔
153
        std::vector<TDeviceTemplateHardware> hws;
268✔
154
        for (const auto& hwItem: data["hw"]) {
282✔
155
            TDeviceTemplateHardware hw;
296✔
156
            Get(hwItem, "signature", hw.Signature);
148✔
157
            Get(hwItem, "fw", hw.Fw);
148✔
158
            hws.push_back(std::move(hw));
148✔
159
        }
160
        deviceTemplate->SetHardware(hws);
134✔
161
    }
162
    deviceTemplate->SetMqttId(data["device"].get("id", "").asString());
863✔
163
    return deviceTemplate;
1,726✔
164
}
165

166
void TTemplateMap::AddTemplatesDir(const std::string& templatesDir,
22✔
167
                                   bool passInvalidTemplates,
168
                                   const Json::Value& settings)
169
{
170
    std::unique_lock m(Mutex);
44✔
171
    PreferredTemplatesDir = templatesDir;
22✔
172
    IterateDirByPattern(
22✔
173
        templatesDir,
174
        ".json",
175
        [&](const std::string& filepath) {
987✔
176
            if (!EndsWith(filepath, ".json")) {
987✔
177
                return false;
124✔
178
            }
179
            try {
180
                auto deviceTemplate =
181
                    MakeTemplateFromJson(WBMQTT::JSON::ParseWithSettings(filepath, settings), filepath);
1,726✔
182
                auto typeData = Templates.try_emplace(deviceTemplate->Type, std::vector<PDeviceTemplate>{});
863✔
183
                if (!typeData.second) {
863✔
NEW
184
                    TemplateUpdatedWarning(typeData.first->second.back(), filepath);
×
185
                }
186
                typeData.first->second.push_back(deviceTemplate);
863✔
187
            } catch (const std::exception& e) {
×
188
                if (passInvalidTemplates) {
×
189
                    LOG(Error) << "Failed to parse " << filepath << "\n" << e.what();
×
190
                    return false;
×
191
                }
192
                throw;
×
193
            }
194
            return false;
863✔
195
        },
196
        true);
44✔
197
}
22✔
198

199
PDeviceTemplate TTemplateMap::GetTemplate(const std::string& deviceType)
95✔
200
{
201
    try {
202
        std::unique_lock m(Mutex);
190✔
203
        return Templates.at(deviceType).back();
189✔
204
    } catch (const std::out_of_range&) {
2✔
205
        throw std::out_of_range("Can't find template for '" + deviceType + "'");
1✔
206
    }
207
}
208

209
std::vector<PDeviceTemplate> TTemplateMap::GetTemplates()
1✔
210
{
211
    std::unique_lock m(Mutex);
2✔
212
    std::vector<PDeviceTemplate> templates;
1✔
213
    for (const auto& t: Templates) {
226✔
214
        templates.push_back(t.second.back());
225✔
215
    }
216
    return templates;
2✔
217
}
218

219
std::vector<std::string> TTemplateMap::UpdateTemplate(const std::string& path)
×
220
{
221
    std::vector<std::string> res;
×
222
    if (!EndsWith(path, ".json")) {
×
223
        return res;
×
224
    }
225
    std::unique_lock m(Mutex);
×
226
    auto deletedType = DeleteTemplateUnsafe(path);
×
227
    if (!deletedType.empty()) {
×
228
        res.push_back(deletedType);
×
229
    }
230
    auto deviceTemplate = MakeTemplateFromJson(WBMQTT::JSON::Parse(path), path);
×
231
    auto& typeArray = Templates.try_emplace(deviceTemplate->Type, std::vector<PDeviceTemplate>{}).first->second;
×
232
    if (!PreferredTemplatesDir.empty() && WBMQTT::StringStartsWith(path, PreferredTemplatesDir)) {
×
NEW
233
        TemplateUpdatedWarning(typeArray.back(), path);
×
UNCOV
234
        typeArray.push_back(deviceTemplate);
×
235
    } else {
236
        typeArray.insert(typeArray.begin(), deviceTemplate);
×
237
    }
238
    if (deviceTemplate->Type != deletedType) {
×
239
        res.push_back(deviceTemplate->Type);
×
240
    }
241
    return res;
×
242
}
243

244
std::string TTemplateMap::DeleteTemplateUnsafe(const std::string& path)
×
245
{
246
    for (auto& deviceTemplates: Templates) {
×
247
        auto item = std::find_if(deviceTemplates.second.begin(), deviceTemplates.second.end(), [&](const auto& t) {
×
248
            return t->GetFilePath() == path;
×
249
        });
×
250
        if (item != deviceTemplates.second.end()) {
×
251
            auto deviceType = deviceTemplates.first;
×
252
            if (deviceTemplates.second.size() > 1) {
×
253
                deviceTemplates.second.erase(item);
×
254
            } else {
255
                Templates.erase(deviceType);
×
256
            }
257
            return deviceType;
×
258
        }
259
    }
260

261
    return std::string();
×
262
}
263

264
std::string TTemplateMap::DeleteTemplate(const std::string& path)
×
265
{
266
    std::unique_lock m(Mutex);
×
267
    return DeleteTemplateUnsafe(path);
×
268
}
269

270
//=============================================================================
271
//                              TDeviceTemplate
272
//=============================================================================
273
TDeviceTemplate::TDeviceTemplate(const std::string& type,
863✔
274
                                 const std::string& protocol,
275
                                 std::shared_ptr<WBMQTT::JSON::TValidator> validator,
276
                                 const std::string& filePath)
863✔
277
    : Type(type),
278
      Deprecated(false),
279
      Validator(validator),
280
      FilePath(filePath),
281
      Subdevices(false),
282
      Protocol(protocol)
863✔
283
{}
863✔
284

285
std::string TDeviceTemplate::GetTitle(const std::string& lang) const
30✔
286
{
287
    auto it = Title.find(lang);
30✔
288
    if (it != Title.end()) {
30✔
289
        return it->second;
2✔
290
    }
291
    if (lang != "en") {
28✔
292
        it = Title.find("en");
×
293
        if (it != Title.end()) {
×
294
            return it->second;
×
295
        }
296
    }
297
    return Type;
28✔
298
}
299

300
const std::string& TDeviceTemplate::GetGroup() const
×
301
{
302
    return Group;
×
303
}
304

305
const std::vector<TDeviceTemplateHardware>& TDeviceTemplate::GetHardware() const
×
306
{
307
    return Hardware;
×
308
}
309

310
bool TDeviceTemplate::IsDeprecated() const
262✔
311
{
312
    return Deprecated;
262✔
313
}
314

315
void TDeviceTemplate::SetDeprecated()
70✔
316
{
317
    Deprecated = true;
70✔
318
}
70✔
319

320
void TDeviceTemplate::SetGroup(const std::string& group)
863✔
321
{
322
    if (!group.empty()) {
863✔
323
        Group = group;
608✔
324
    }
325
}
863✔
326

327
void TDeviceTemplate::SetTitle(const std::unordered_map<std::string, std::string>& translations)
863✔
328
{
329
    Title = translations;
863✔
330
}
863✔
331

332
void TDeviceTemplate::SetHardware(const std::vector<TDeviceTemplateHardware>& hardware)
134✔
333
{
334
    Hardware = hardware;
134✔
335
}
134✔
336

337
const std::string& TDeviceTemplate::GetFilePath() const
268✔
338
{
339
    return FilePath;
268✔
340
}
341

342
const Json::Value& TDeviceTemplate::GetTemplate()
767✔
343
{
344
    if (Template.isNull()) {
767✔
345
        Json::Value root(WBMQTT::JSON::Parse(GetFilePath()));
524✔
346
        // Skip deprecated template validation, it may be broken according to latest schema
347
        if (!IsDeprecated()) {
262✔
348
            try {
349
                Validator->Validate(root);
240✔
350
                ValidateConditions(root["device"]);
238✔
351
                // Check that parameters with same ids have same addresses (for parameters declared as array)
352
                ValidateParameterProperties(root["device"]["parameters"]);
235✔
353
            } catch (const std::runtime_error& e) {
12✔
354
                throw std::runtime_error("File: " + GetFilePath() + " error: " + e.what());
6✔
355
            }
356
            // Check that channels refer to valid subdevices and they are not nested too deep
357
            if (WithSubdevices()) {
234✔
358
                TSubDevicesTemplateMap subdevices(Type, root["device"]);
20✔
359
                CheckNesting(root, 0, subdevices);
10✔
360
            }
361
        }
362
        Template = root["device"];
256✔
363
    }
364
    return Template;
761✔
365
}
366

367
void TDeviceTemplate::SetWithSubdevices()
46✔
368
{
369
    Subdevices = true;
46✔
370
}
46✔
371

372
bool TDeviceTemplate::WithSubdevices() const
271✔
373
{
374
    return Subdevices;
271✔
375
}
376

377
const std::string& TDeviceTemplate::GetProtocol() const
×
378
{
379
    return Protocol;
×
380
}
381

382
void TDeviceTemplate::SetMqttId(const std::string& id)
863✔
383
{
384
    MqttId = id;
863✔
385
}
863✔
386

387
const std::string& TDeviceTemplate::GetMqttId() const
×
388
{
389
    return MqttId;
×
390
}
391

392
//=============================================================================
393
//                          TSubDevicesTemplateMap
394
//=============================================================================
395
TSubDevicesTemplateMap::TSubDevicesTemplateMap(const std::string& deviceType, const Json::Value& device)
315✔
396
    : DeviceType(deviceType)
315✔
397
{
398
    if (device.isMember("subdevices")) {
315✔
399
        AddSubdevices(device["subdevices"]);
35✔
400

401
        // Check that channels refer to valid subdevices
402
        for (const auto& subdeviceTemplate: Templates) {
286✔
403
            for (const auto& ch: subdeviceTemplate.second.Schema["channels"]) {
772✔
404
                if (ch.isMember("device_type")) {
521✔
405
                    TSubDevicesTemplateMap::GetTemplate(ch["device_type"].asString());
124✔
406
                }
407
                if (ch.isMember("oneOf")) {
521✔
408
                    for (const auto& subdeviceType: ch["oneOf"]) {
732✔
409
                        TSubDevicesTemplateMap::GetTemplate(subdeviceType.asString());
680✔
410
                    }
411
                }
412
            }
413
        }
414
    }
415
}
315✔
416

417
void TSubDevicesTemplateMap::AddSubdevices(const Json::Value& subdevicesArray)
35✔
418
{
419
    for (auto& dev: subdevicesArray) {
286✔
420
        auto deviceType = dev["device_type"].asString();
502✔
421
        if (Templates.count(deviceType)) {
251✔
422
            LOG(Warn) << "Device type '" << DeviceType << "'. Duplicate subdevice type '" << deviceType << "'";
×
423
        } else {
424
            auto deviceTypeTitle = deviceType;
251✔
425
            Get(dev, "title", deviceTypeTitle);
251✔
426
            Templates.insert({deviceType, {deviceType, deviceTypeTitle, dev["device"]}});
251✔
427
        }
428
    }
429
}
35✔
430

431
const TSubDeviceTemplate& TSubDevicesTemplateMap::GetTemplate(const std::string& deviceType)
5,993✔
432
{
433
    try {
434
        return Templates.at(deviceType);
5,993✔
435
    } catch (const std::out_of_range&) {
×
436
        throw std::out_of_range("Device type '" + DeviceType + "'. Can't find template for subdevice '" + deviceType +
×
437
                                "'");
×
438
    }
439
}
440

441
std::vector<std::string> TSubDevicesTemplateMap::GetDeviceTypes() const
×
442
{
443
    std::vector<std::string> res;
×
444
    for (const auto& elem: Templates) {
×
445
        res.push_back(elem.first);
×
446
    }
447
    return res;
×
448
}
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