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

bcpearce / hass-motion-detection-addon / 21384596426

27 Jan 2026 04:29AM UTC coverage: 69.446% (+6.7%) from 62.713%
21384596426

Pull #28

github

web-flow
Merge 5a36a882a into d19867498
Pull Request #28: Added End to End test

1117 of 1810 branches covered (61.71%)

Branch coverage included in aggregate %.

11 of 13 new or added lines in 3 files covered. (84.62%)

2 existing lines in 1 file now uncovered.

1315 of 1692 relevant lines covered (77.72%)

98.49 hits per line

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

83.6
/src/Util/ProgramOptions.cxx
1
#include "Logger.h"
2
#include "WindowsWrapper.h"
3

4
#include "Util/ProgramOptions.h"
5

6
#include <cstdlib>
7
#include <fstream>
8
#include <iostream>
9
#include <sstream>
10
#include <string_view>
11

12
#include <boost/program_options.hpp>
13
#define JSON_USE_IMPLICIT_CONVERSIONS 0
14
#include <nlohmann/json.hpp>
15

16
using namespace std::string_literals;
17
using namespace std::string_view_literals;
18
namespace po = boost::program_options;
19

20
namespace util {
21

22
std::variant<ProgramOptions, std::string>
23
ProgramOptions::ParseOptions(int argc, const char **argv) {
12✔
24
  /*
25
   * Meta Options
26
   */
27
  po::options_description allOptions("Allowed options");
24✔
28
  allOptions.add_options()
12✔
29
      /**/
30
      ("help,h", "show help message")
12✔
31
      /**/
32
      ("version,v", "show version");
12✔
33

34
  /*
35
   * Source RTSP Feed Options
36
   */
37
  po::options_description sourceFeedOptions("Sources Config File");
24✔
38
  sourceFeedOptions.add_options()
12✔
39
      /**/
40
      ("source-config,c", po::value<std::filesystem::path>(),
12✔
41
       "Path to source Config File");
42

43
  /*
44
   * Source RTSP Feed Options
45
   */
46
  sourceFeedOptions.add_options()
12✔
47
      /**/
48
      ("source-config-raw", po::value<std::string>(), "Raw config JSON string");
12✔
49
  allOptions.add(sourceFeedOptions);
12✔
50

51
  /*
52
   * Home Assistant Handler Options
53
   */
54
  po::options_description homeAssistantOptions("Home Assistant Options");
24✔
55
  homeAssistantOptions.add_options()
12✔
56
      /**/
57
      ("hass-url,u", po::value<std::string>()->default_value(""),
24✔
58
       "Home Assistant URL to send detector updates")
59
      /**/
60
      ("hass-token,t", po::value<std::string>()->default_value(""),
36✔
61
       "Home Assistant long-lived access token for API auth");
62
  allOptions.add(homeAssistantOptions);
12✔
63

64
  /*
65
   * Web User-Interface Options
66
   */
67
  po::options_description webUiOptions("Web User-Interface Options");
24✔
68
  webUiOptions.add_options()
12✔
69
      /**/
70
      ("web-ui-host,s", po::value<std::string>()->default_value("0.0.0.0"),
24✔
71
       "host to bind the web UI to, defaults to localhost, set to '' to "
72
       "disable the Web GUI")
73
      /**/
74
      ("web-ui-port,p", po::value<int>()->default_value(32834),
12✔
75
       "port to bind the web UI to, defaults to 32834, set to <= 0 to disable "
76
       "the Web GUI");
77
  allOptions.add(webUiOptions);
12✔
78

79
  /*
80
   * Detection Options
81
   */
82
  po::options_description detectionOptions("Detection Options");
24✔
83
  detectionOptions.add_options()
12✔
84
      /**/
85
      ("save-destination", po::value<std::string>()->default_value(""),
24✔
86
       "destination to save the motion detection video files to, if empty, "
87
       "video saving is disabled");
88
  allOptions.add(detectionOptions);
12✔
89

90
  po::variables_map vm;
12✔
91

92
  po::store(po::command_line_parser(argc, argv).options(allOptions).run(), vm);
12✔
93

94
  const auto parsedEnv =
95
      po::parse_environment(allOptions, [](std::string_view envVar) {
662✔
96
        static const std::map<std::string, std::string, std::less<>>
97
            envVarToProgOpts{{"MODET_HASS_URL"s, "hass-url"s},
11✔
98
                             {"MODET_HASS_TOKEN"s, "hass-token"},
11✔
99
                             {"MODET_WEB_UI_HOST"s, "web-ui-host"s},
11✔
100
                             {"MODET_WEB_UI_PORT"s, "web-ui-port"s},
11✔
101
                             {"MODET_SAVE_DESTINATION"s, "save-destination"s}};
794!
102
        const auto it = envVarToProgOpts.find(envVar);
662✔
103
        return it != envVarToProgOpts.end() ? it->second : ""s;
662✔
104
      });
67!
105
  po::store(parsedEnv, vm);
12✔
106

107
  if (vm.count("help")) {
24✔
108
    std::ostringstream oss;
3✔
109
    oss << allOptions;
3✔
110
    return oss.str();
3✔
111
  }
3✔
112

113
  if (vm.count("version")) {
18✔
114
    return std::string(MOTION_DETECTION_SEMVER);
6✔
115
  }
116

117
  ProgramOptions options;
6✔
118
  try {
119
    po::notify(vm);
6✔
120

121
    if (!vm["source-config"].empty() && !vm["source-config-raw"].empty()) {
28!
122
      throw std::invalid_argument(
NEW
123
          "Must specify only one of 'source-config' or 'source-config-raw'");
×
124
    } else if (!vm["source-config-raw"].empty()) {
12✔
125
      const auto sourceConfigRaw = vm["source-config-raw"].as<std::string>();
1✔
126
      options.feeds = FeedOptions::ParseJson(std::string_view(sourceConfigRaw));
1✔
127
    } else {
1✔
128
      options.feeds = FeedOptions::ParseJson(
10✔
129
          vm["source-config"].as<std::filesystem::path>());
20✔
130
    }
131

132
    options.hassUrl = boost::url(vm["hass-url"].as<std::string>());
12✔
133
    options.hassToken = vm["hass-token"].as<std::string>();
12✔
134

135
    options.webUiHost = vm["web-ui-host"].as<std::string>();
12✔
136
    options.webUiPort = vm["web-ui-port"].as<int>();
12✔
137

138
    options.saveDestination = vm["save-destination"].as<std::string>();
6✔
139

140
  } catch (const std::exception &e) {
×
141
    std::ostringstream oss;
×
142
    oss << e.what() << "\n" << allOptions;
×
143
    return oss.str();
×
144
  }
×
145
  return options;
6✔
146
}
12✔
147

148
auto _ParseJson(const nlohmann::json &json)
6✔
149
    -> std::unordered_map<std::string, ProgramOptions::FeedOptions> {
150

151
  std::unordered_map<std::string, ProgramOptions::FeedOptions> res;
6✔
152

153
  for (const auto &[key, value] : json.items()) {
14✔
154
    if (!value.is_object()) {
8!
155
      LOGGER->error(
×
156
          "Invalid feed options for key '{}': expected an object, got {}", key,
157
          value.type_name());
×
158
      continue;
×
159
    }
160
    ProgramOptions::FeedOptions feedOpts;
8✔
161
    if (value.contains("sourceUrl")) {
8!
162
      feedOpts.sourceUrl =
163
          boost::url(value["sourceUrl"].template get<std::string>());
8✔
164
    }
165
    if (value.contains("sourceToken")) {
8!
166
      feedOpts.sourceToken = value["sourceToken"].template get<std::string>();
×
167
    }
168
    if (value.contains("sourceUsername")) {
8✔
169
      feedOpts.sourceUsername =
170
          value["sourceUsername"].template get<std::string>();
1✔
171
    }
172
    if (value.contains("sourcePassword")) {
8✔
173
      feedOpts.sourcePassword =
174
          value["sourcePassword"].template get<std::string>();
1✔
175
    }
176
    if (value.contains("hassEntityId")) {
8✔
177
      feedOpts.hassEntityId = value["hassEntityId"].template get<std::string>();
3✔
178
    }
179
    if (value.contains("hassFriendlyName")) {
8✔
180
      feedOpts.hassFriendlyName =
181
          value["hassFriendlyName"].template get<std::string>();
1✔
182
    }
183
    if (value.contains("detectionSize")) {
8✔
184
      if (value["detectionSize"].is_string()) {
5✔
185
        const auto rawSize = value["detectionSize"].template get<std::string>();
3✔
186
        if (rawSize.back() == '%') {
3!
187
          feedOpts.detectionSize =
188
              std::stod(rawSize.substr(0, rawSize.size() - 1)) / 100.0;
3✔
189
        } else {
190
          feedOpts.detectionSize = std::stoi(rawSize);
×
191
        }
192
      } else if (value["detectionSize"].is_number_integer()) {
5!
193
        feedOpts.detectionSize = value["detectionSize"].template get<int>();
2✔
194
      }
195
    }
196
    if (value.contains("detectionDebounce")) {
8✔
197
      feedOpts.detectionDebounce =
2✔
198
          std::chrono::seconds{value["detectionDebounce"].template get<int>()};
2✔
199
    }
200
    if (value.contains("saveSourceUrl")) {
8!
201
      feedOpts.saveSourceUrl =
202
          boost::url(value["saveSourceUrl"].template get<std::string>());
×
203
    }
204
    if (value.contains("saveImageLimit")) {
8✔
205
      feedOpts.saveImageLimit = value["saveImageLimit"].template get<size_t>();
1✔
206
    }
207
    res[key] = std::move(feedOpts);
8✔
208
  }
14✔
209

210
  return res;
6✔
211
}
×
212

213
auto ProgramOptions::FeedOptions::ParseJson(const std::filesystem::path &json)
5✔
214
    -> std::unordered_map<std::string, FeedOptions> {
215
  std::ifstream file(json);
5✔
216
  const nlohmann::json data = nlohmann::json::parse(file);
5✔
217
  return _ParseJson(data);
10✔
218
}
5✔
219

220
auto ProgramOptions::FeedOptions::ParseJson(std::string_view jsonSv)
1✔
221
    -> std::unordered_map<std::string, FeedOptions> {
222
  const nlohmann::json data = nlohmann::json::parse(jsonSv);
1✔
223
  return _ParseJson(data);
2✔
224
}
1✔
225

226
bool ProgramOptions::CanSetupHass(const FeedOptions &feedOpts) const {
5✔
227
  return !hassUrl.empty() && !feedOpts.hassEntityId.empty() &&
7!
228
         !hassToken.empty();
7!
229
}
230

231
bool ProgramOptions::CanSetupFileSave(const FeedOptions &feedOpts) const {
2✔
232
  return !saveDestination.empty() && !feedOpts.saveSourceUrl.empty();
2!
233
}
234

235
} // namespace util
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