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

wirenboard / wb-mqtt-iec104 / 35

03 Mar 2026 05:57AM UTC coverage: 52.705% (-0.3%) from 52.999%
35

push

github

web-flow
Add option to prevent groups update (#28)

225 of 385 branches covered (58.44%)

0 of 13 new or added lines in 2 files covered. (0.0%)

380 of 721 relevant lines covered (52.7%)

3.53 hits per line

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

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

3
#include <fstream>
4
#include <iostream>
5
#include <queue>
6
#include <set>
7
#include <unordered_set>
8

9
#include <sys/stat.h>
10

11
#include <wblib/json_utils.h>
12
#include <wblib/wbmqtt.h>
13

14
#include "iec104_exception.h"
15
#include "log.h"
16
#include "murmurhash.h"
17

18
using namespace std;
19
using namespace WBMQTT;
20
using namespace WBMQTT::JSON;
21

22
#define LOG(logger) ::logger.Log() << "[config] "
23

24
namespace
25
{
26
    const std::string SINGLE_POINT_CONFIG_VALUE("single");
27
    const std::string MEASURED_VALUE_SHORT_CONFIG_VALUE("short");
28
    const std::string MEASURED_VALUE_SCALED_CONFIG_VALUE("scaled");
29
    const std::string SINGLE_POINT_WITH_TIMESTAMP_CONFIG_VALUE("single_time");
30
    const std::string MEASURED_VALUE_SHORT_WITH_TIMESTAMP_CONFIG_VALUE("short_time");
31
    const std::string MEASURED_VALUE_SCALED_WITH_TIMESTAMP_CONFIG_VALUE("scaled_time");
32

33
    const std::unordered_map<std::string, TIecInformationObjectType> Types = {
34
        {SINGLE_POINT_CONFIG_VALUE, SinglePoint},
35
        {MEASURED_VALUE_SHORT_CONFIG_VALUE, MeasuredValueShort},
36
        {MEASURED_VALUE_SCALED_CONFIG_VALUE, MeasuredValueScaled},
37
        {SINGLE_POINT_WITH_TIMESTAMP_CONFIG_VALUE, SinglePointWithTimestamp},
38
        {MEASURED_VALUE_SHORT_WITH_TIMESTAMP_CONFIG_VALUE, MeasuredValueShortWithTimestamp},
39
        {MEASURED_VALUE_SCALED_WITH_TIMESTAMP_CONFIG_VALUE, MeasuredValueScaledWithTimestamp}};
40

41
    TIecInformationObjectType GetIoType(const std::string& t)
25✔
42
    {
43
        auto it = Types.find(t);
25✔
44
        if (it == Types.end()) {
25✔
45
            throw std::runtime_error("Unknown information object type:" + t);
×
46
        }
47
        return it->second;
25✔
48
    }
49

50
    std::string GetDeviceName(const std::string& topic)
28✔
51
    {
52
        return StringSplit(topic, '/')[0];
56✔
53
    }
54

55
    std::string GetControlName(const std::string& topic)
28✔
56
    {
57
        return StringSplit(topic, '/')[1];
56✔
58
    }
59

60
    bool IsValidTopic(const std::string& topic)
29✔
61
    {
62
        auto l = StringSplit(topic, '/');
29✔
63
        return (l.size() == 2);
58✔
64
    }
65

66
    void LoadControls(TDeviceConfig& config, const Json::Value& controls, std::set<uint32_t>& UsedAddresses)
5✔
67
    {
68
        for (const auto& control: controls) {
32✔
69
            bool enabled = false;
27✔
70
            Get(control, "enabled", enabled);
27✔
71
            if (enabled) {
27✔
72
                auto topic = control["topic"].asString();
52✔
73
                if (IsValidTopic(topic)) {
26✔
74
                    uint32_t ioa = control["address"].asUInt();
25✔
75
                    if (UsedAddresses.count(ioa)) {
25✔
76
                        LOG(Warn) << "Control '" << topic << "' has duplicate address " << ioa;
×
77
                    } else {
78
                        UsedAddresses.insert(ioa);
25✔
79
                        config[GetDeviceName(topic)].insert(
50✔
80
                            {GetControlName(topic), {ioa, GetIoType(control["iec_type"].asString())}});
25✔
81
                    }
82
                } else {
83
                    LOG(Warn) << "Control '" << topic << "' has invalid topic name";
1✔
84
                }
85
            }
86
        }
87
    }
5✔
88

89
    TDeviceConfig LoadGroups(const Json::Value& config, std::set<uint32_t>& UsedAddresses)
5✔
90
    {
91
        TDeviceConfig res;
5✔
92
        bool anyEnabled = false;
5✔
93
        for (const auto& group: config["groups"]) {
11✔
94
            bool enabled = false;
6✔
95
            Get(group, "enabled", enabled);
6✔
96
            if (enabled) {
6✔
97
                anyEnabled = true;
5✔
98
                LoadControls(res, group["controls"], UsedAddresses);
5✔
99
            }
100
        }
101
        if (!anyEnabled) {
5✔
102
            throw TEmptyConfigException();
×
103
        }
104
        return res;
5✔
105
    }
106

107
    TMosquittoMqttConfig LoadMqttConfig(const Json::Value& configRoot)
5✔
108
    {
109
        TMosquittoMqttConfig cfg;
5✔
110
        if (configRoot.isMember("mqtt")) {
5✔
111
            Get(configRoot["mqtt"], "host", cfg.Host);
×
112
            Get(configRoot["mqtt"], "port", cfg.Port);
×
113
            Get(configRoot["mqtt"], "keepalive", cfg.Keepalive);
×
114
            bool auth = false;
×
115
            Get(configRoot["mqtt"], "auth", auth);
×
116
            if (auth) {
×
117
                Get(configRoot["mqtt"], "username", cfg.User);
×
118
                Get(configRoot["mqtt"], "password", cfg.Password);
×
119
            }
120
        }
121
        return cfg;
5✔
122
    }
123

124
    class AddressAssigner
125
    {
126
        std::set<uint32_t> UsedAddresses;
127

128
    public:
129
        AddressAssigner(const Json::Value& config)
1✔
130
        {
1✔
131
            for (const auto& device: config["devices"]) {
1✔
132
                for (const auto& control: device["controls"]) {
×
133
                    UsedAddresses.insert(control["address"].asUInt());
×
134
                }
135
            }
136
        }
1✔
137

138
        uint32_t GetAddress(const std::string& topicName)
1✔
139
        {
140
            uint32_t newAddr = MurmurHash2A((const uint8_t*)topicName.data(), topicName.size(), 0xA30AA568) & 0xFFFFFF;
1✔
141
            const uint32_t ADDR_SALT = 7079;
1✔
142
            while (UsedAddresses.count(newAddr)) {
1✔
143
                newAddr = (newAddr + ADDR_SALT) & 0xFFFFFF;
×
144
            }
145
            UsedAddresses.insert(newAddr);
1✔
146
            return newAddr;
1✔
147
        }
148
    };
149

150
    bool IsConvertibleControl(PControl control)
2✔
151
    {
152
        return (control->GetType() != "text" && control->GetType() != "rgb");
2✔
153
    }
154

155
    Json::Value MakeControlConfig(const std::string& topic,
1✔
156
                                  const std::string& info,
157
                                  uint32_t addr,
158
                                  const std::string& iecType)
159
    {
160
        Json::Value cnt(Json::objectValue);
1✔
161
        cnt["topic"] = topic;
1✔
162
        cnt["info"] = info;
1✔
163
        cnt["enabled"] = false;
1✔
164
        cnt["address"] = addr;
1✔
165
        cnt["iec_type"] = iecType;
1✔
166
        return cnt;
1✔
167
    }
168

169
    void AppendControl(Json::Value& root, PControl c, AddressAssigner& aa)
2✔
170
    {
171
        if (!IsConvertibleControl(c)) {
2✔
172
            ::Warn.Log() << "'" << c->GetId() << "' of type '" << c->GetType() << "' from device '"
1✔
173
                         << c->GetDevice()->GetId() << "' is not convertible to IEC 608760-5-104 information object";
1✔
174
            return;
1✔
175
        }
176

177
        std::string info(c->GetType());
1✔
178
        info += (c->IsReadonly() ? " (read only)" : " (setup is allowed)");
1✔
179

180
        std::string controlName(c->GetDevice()->GetId() + "/" + c->GetId());
2✔
181

182
        if (c->GetType() == "switch" || c->GetType() == "pushbutton") {
1✔
183
            root.append(MakeControlConfig(controlName, info, aa.GetAddress(controlName), SINGLE_POINT_CONFIG_VALUE));
×
184
            return;
×
185
        }
186
        root.append(
187
            MakeControlConfig(controlName, info, aa.GetAddress(controlName), MEASURED_VALUE_SHORT_CONFIG_VALUE));
1✔
188
    }
189

190
    Json::Value MakeControlsConfig(std::map<std::string, PControl>& controls, AddressAssigner& addressAssigner)
2✔
191
    {
192
        Json::Value res(Json::arrayValue);
2✔
193
        for (auto control: controls) {
4✔
194
            AppendControl(res, control.second, addressAssigner);
2✔
195
        }
196
        return res;
2✔
197
    }
198

199
    Json::Value MakeGroupConfig(const std::string& name)
1✔
200
    {
201
        Json::Value dev(Json::objectValue);
1✔
202
        dev["name"] = name;
1✔
203
        dev["enabled"] = false;
1✔
204
        dev["controls"] = Json::Value(Json::arrayValue);
1✔
205
        return dev;
1✔
206
    }
207

208
    Json::Value& GetGroup(Json::Value& config, const std::string& name)
1✔
209
    {
210
        for (auto& group: config["groups"]) {
2✔
211
            if (group["name"].asString() == name) {
1✔
212
                return group;
×
213
            }
214
        }
215
        return config["groups"].append(MakeGroupConfig(name));
1✔
216
    }
217
}
218

219
TConfig LoadConfig(const std::string& configFileName, const std::string& configSchemaFileName)
14✔
220
{
221
    try {
222
        auto config = JSON::Parse(configFileName);
27✔
223
        JSON::Validate(config, JSON::Parse(configSchemaFileName));
20✔
224
        std::set<uint32_t> usedAddresses;
10✔
225

226
        TConfig cfg;
10✔
227
        cfg.Iec.BindIp = config["iec104"]["host"].asString();
5✔
228
        cfg.Iec.BindPort = config["iec104"]["port"].asUInt();
5✔
229
        cfg.Iec.CommonAddress = config["iec104"]["address"].asUInt();
5✔
230
        cfg.Mqtt = LoadMqttConfig(config);
5✔
231
        cfg.Devices = LoadGroups(config, usedAddresses);
5✔
232
        Get(config, "debug", cfg.Debug);
5✔
233
        return cfg;
10✔
234
    } catch (const TEmptyConfigException& e) {
9✔
235
        throw;
×
236
    } catch (const std::exception& e) {
18✔
237
        throw TConfigException(e.what());
9✔
238
    }
239
}
240

241
void UpdateConfig(const string& configFileName, const string& configSchemaFileName)
×
242
{
243
    const auto id = "wb-mqtt-iec104-config_generator";
×
244
    auto config = JSON::Parse(configFileName);
×
245
    JSON::Validate(config, JSON::Parse(configSchemaFileName));
×
246

NEW
247
    bool update_groups = false;
×
NEW
248
    Get(config, "update_groups", update_groups);
×
NEW
249
    if (update_groups) {
×
NEW
250
        WBMQTT::TMosquittoMqttConfig mqttConfig(LoadMqttConfig(config));
×
NEW
251
        mqttConfig.Id = id;
×
NEW
252
        auto mqtt = NewMosquittoMqttClient(mqttConfig);
×
NEW
253
        auto backend = NewDriverBackend(mqtt);
×
NEW
254
        auto driver = NewDriver(TDriverArgs{}.SetId(id).SetBackend(backend));
×
NEW
255
        driver->StartLoop();
×
NEW
256
        UpdateConfig(driver, config);
×
NEW
257
        driver->StopLoop();
×
258
    }
259

NEW
260
    config["update_groups"] = false;
×
261

262
    Json::StreamWriterBuilder builder;
×
263
    builder["indentation"] = "    ";
×
264
    std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
×
265
    std::ofstream file(configFileName);
×
266
    writer->write(config, &file);
×
267
    file << std::endl;
×
268
}
269

270
void UpdateConfig(PDeviceDriver driver, Json::Value& oldConfig)
1✔
271
{
272
    driver->WaitForReady();
1✔
273
    driver->SetFilter(GetAllDevicesFilter());
1✔
274
    driver->WaitForReady();
1✔
275

276
    AddressAssigner addressAssigner(oldConfig);
2✔
277

278
    std::map<std::string, std::map<std::string, PControl>> mqttDevices;
2✔
279
    auto tx = driver->BeginTx();
2✔
280
    for (auto& device: tx->GetDevicesList()) {
3✔
281
        if (!WBMQTT::StringStartsWith(device->GetId(), "system__")) {
2✔
282
            std::map<std::string, PControl> controls;
4✔
283
            for (auto& control: device->ControlsList()) {
5✔
284
                controls.insert({control->GetId(), control});
3✔
285
            }
286
            if (controls.size()) {
2✔
287
                mqttDevices.insert({device->GetId(), controls});
2✔
288
            }
289
        }
290
    }
291

292
    for (auto& group: oldConfig["groups"]) {
2✔
293
        for (auto& control: group["controls"]) {
4✔
294
            auto topic = control["topic"].asString();
6✔
295
            if (IsValidTopic(topic)) {
3✔
296
                auto mqttDevice = mqttDevices.find(GetDeviceName(topic));
3✔
297
                if (mqttDevice != mqttDevices.end()) {
3✔
298
                    auto mqttControl = mqttDevice->second.find(GetControlName(topic));
3✔
299
                    if (mqttControl != mqttDevice->second.end()) {
3✔
300
                        mqttDevice->second.erase(mqttControl);
1✔
301
                    }
302
                }
303
                if (!mqttDevice->second.size()) {
3✔
304
                    mqttDevices.erase(mqttDevice);
×
305
                }
306
            }
307
        }
308
    }
309

310
    for (auto& mqttDevice: mqttDevices) {
3✔
311
        auto controls(MakeControlsConfig(mqttDevice.second, addressAssigner));
4✔
312
        if (controls.size()) {
2✔
313
            auto& configGroup = GetGroup(oldConfig, mqttDevice.first);
1✔
314
            for (auto& v: controls) {
2✔
315
                configGroup["controls"].append(v);
1✔
316
            }
317
        }
318
    }
319
}
1✔
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