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

bcpearce / hass-motion-detection-addon / 21057544703

16 Jan 2026 06:13AM UTC coverage: 63.085% (+13.1%) from 49.971%
21057544703

Pull #26

github

web-flow
Merge ea254e9b8 into 90e7e9562
Pull Request #26: Additional test coverage

995 of 1826 branches covered (54.49%)

Branch coverage included in aggregate %.

26 of 26 new or added lines in 5 files covered. (100.0%)

15 existing lines in 5 files now uncovered.

1242 of 1720 relevant lines covered (72.21%)

84.24 hits per line

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

82.69
/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) {
11✔
24
  /*
25
   * Meta Options
26
   */
27
  po::options_description allOptions("Allowed options");
33✔
28
  allOptions.add_options()
22✔
29
      /**/
30
      ("help,h", "show help message")
11✔
31
      /**/
32
      ("version,v", "show version");
22✔
33

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

44
  /*
45
   * Home Assistant Handler Options
46
   */
47
  po::options_description homeAssistantOptions("Home Assistant Options");
33✔
48
  homeAssistantOptions.add_options()
22✔
49
      /**/
50
      ("hass-url,u", po::value<std::string>()->default_value(""),
55✔
51
       "Home Assistant URL to send detector updates")
52
      /**/
53
      ("hass-token,t", po::value<std::string>()->default_value(""),
55✔
54
       "Home Assistant long-lived access token for API auth");
55
  allOptions.add(homeAssistantOptions);
11✔
56

57
  /*
58
   * Web User-Interface Options
59
   */
60
  po::options_description webUiOptions("Web User-Interface Options");
33✔
61
  webUiOptions.add_options()
22✔
62
      /**/
63
      ("web-ui-host,s", po::value<std::string>()->default_value("0.0.0.0"),
55✔
64
       "host to bind the web UI to, defaults to localhost, set to '' to "
65
       "disable the Web GUI")
66
      /**/
67
      ("web-ui-port,p", po::value<int>()->default_value(32834),
33✔
68
       "port to bind the web UI to, defaults to 32834, set to <= 0 to disable "
69
       "the Web GUI");
70
  allOptions.add(webUiOptions);
11✔
71

72
  /*
73
   * Detection Options
74
   */
75
  po::options_description detectionOptions("Detection Options");
33✔
76
  detectionOptions.add_options()
22✔
77
      /**/
78
      ("save-destination", po::value<std::string>()->default_value(""),
44✔
79
       "destination to save the motion detection video files to, if empty, "
80
       "video saving is disabled");
81
  allOptions.add(detectionOptions);
11✔
82

83
  po::variables_map vm;
11✔
84

85
  po::store(po::command_line_parser(argc, argv).options(allOptions).run(), vm);
11✔
86

87
  const auto parsedEnv =
11✔
88
      po::parse_environment(allOptions, [](std::string_view envVar) {
596✔
89
        static const std::map<std::string, std::string, std::less<>>
90
            envVarToProgOpts{{"MODET_HASS_URL"s, "hass-url"s},
10✔
91
                             {"MODET_HASS_TOKEN"s, "hass-token"},
10✔
92
                             {"MODET_WEB_UI_HOST"s, "web-ui-host"s},
10✔
93
                             {"MODET_WEB_UI_PORT"s, "web-ui-port"s},
10✔
94
                             {"MODET_SAVE_DESTINATION"s, "save-destination"s}};
776!
95
        const auto it = envVarToProgOpts.find(envVar);
596✔
96
        return it != envVarToProgOpts.end() ? it->second : ""s;
1,788✔
97
      });
111!
98
  po::store(parsedEnv, vm);
11✔
99

100
  if (vm.count("help")) {
33✔
101
    std::ostringstream oss;
3✔
102
    oss << allOptions;
3✔
103
    return oss.str();
3✔
104
  }
3✔
105

106
  if (vm.count("version")) {
24✔
107
    return std::string(MOTION_DETECTION_SEMVER);
9✔
108
  }
109

110
  ProgramOptions options;
5✔
111
  try {
112
    po::notify(vm);
5✔
113

114
    options.feeds =
5✔
115
        FeedOptions::ParseJson(vm["source-config"].as<std::filesystem::path>());
15✔
116

117
    options.hassUrl = boost::url(vm["hass-url"].as<std::string>());
15✔
118
    options.hassToken = vm["hass-token"].as<std::string>();
15✔
119

120
    options.webUiHost = vm["web-ui-host"].as<std::string>();
15✔
121
    options.webUiPort = vm["web-ui-port"].as<int>();
15✔
122

123
    options.saveDestination = vm["save-destination"].as<std::string>();
15✔
124

125
  } catch (const std::exception &e) {
×
126
    std::ostringstream oss;
×
127
    oss << e.what() << "\n" << allOptions;
×
128
    return oss.str();
×
129
  }
×
130
  return options;
5✔
131
}
11✔
132

133
auto _ParseJson(const nlohmann::json &json)
5✔
134
    -> std::unordered_map<std::string, ProgramOptions::FeedOptions> {
135

136
  std::unordered_map<std::string, ProgramOptions::FeedOptions> res;
5✔
137

138
  for (const auto &[key, value] : json.items()) {
11✔
139
    if (!value.is_object()) {
6!
140
      LOGGER->error(
×
141
          "Invalid feed options for key '{}': expected an object, got {}", key,
142
          value.type_name());
×
143
      continue;
×
144
    }
145
    ProgramOptions::FeedOptions feedOpts;
6✔
146
    if (value.contains("sourceUrl")) {
6!
147
      feedOpts.sourceUrl =
6✔
148
          boost::url(value["sourceUrl"].template get<std::string>());
6✔
149
    }
150
    if (value.contains("sourceToken")) {
6!
151
      feedOpts.sourceToken = value["sourceToken"].template get<std::string>();
×
152
    }
153
    if (value.contains("sourceUsername")) {
6✔
154
      feedOpts.sourceUsername =
1✔
155
          value["sourceUsername"].template get<std::string>();
1✔
156
    }
157
    if (value.contains("sourcePassword")) {
6✔
158
      feedOpts.sourcePassword =
1✔
159
          value["sourcePassword"].template get<std::string>();
1✔
160
    }
161
    if (value.contains("hassEntityId")) {
6✔
162
      feedOpts.hassEntityId = value["hassEntityId"].template get<std::string>();
3✔
163
    }
164
    if (value.contains("hassFriendlyName")) {
6✔
165
      feedOpts.hassFriendlyName =
1✔
166
          value["hassFriendlyName"].template get<std::string>();
1✔
167
    }
168
    if (value.contains("detectionSize")) {
6✔
169
      if (value["detectionSize"].is_string()) {
4✔
170
        const auto rawSize = value["detectionSize"].template get<std::string>();
3✔
171
        if (rawSize.back() == '%') {
3!
172
          feedOpts.detectionSize =
3✔
173
              std::stod(rawSize.substr(0, rawSize.size() - 1)) / 100.0;
3✔
174
        } else {
175
          feedOpts.detectionSize = std::stoi(rawSize);
×
176
        }
177
      } else if (value["detectionSize"].is_number_integer()) {
4!
178
        feedOpts.detectionSize = value["detectionSize"].template get<int>();
1✔
179
      }
180
    }
181
    if (value.contains("detectionDebounce")) {
6✔
182
      feedOpts.detectionDebounce =
1✔
183
          std::chrono::seconds{value["detectionDebounce"].template get<int>()};
1✔
184
    }
185
    if (value.contains("saveSourceUrl")) {
6!
UNCOV
186
      feedOpts.saveSourceUrl =
×
187
          boost::url(value["saveSourceUrl"].template get<std::string>());
×
188
    }
189
    if (value.contains("saveImageLimit")) {
6✔
190
      feedOpts.saveImageLimit = value["saveImageLimit"].template get<size_t>();
1✔
191
    }
192
    res[key] = std::move(feedOpts);
6✔
193
  }
11✔
194

195
  return res;
5✔
196
}
×
197

198
auto ProgramOptions::FeedOptions::ParseJson(const std::filesystem::path &json)
5✔
199
    -> std::unordered_map<std::string, FeedOptions> {
200
  std::ifstream file(json);
5✔
201
  const nlohmann::json data = nlohmann::json::parse(file);
5✔
202
  return _ParseJson(data);
10✔
203
}
5✔
204

205
auto ProgramOptions::FeedOptions::ParseJson(std::string_view jsonSv)
×
206
    -> std::unordered_map<std::string, FeedOptions> {
207
  const nlohmann::json data = nlohmann::json::parse(jsonSv);
×
208
  return _ParseJson(data);
×
209
}
×
210

211
bool ProgramOptions::CanSetupHass(const FeedOptions &feedOpts) const {
3✔
212
  return !hassUrl.empty() && !feedOpts.hassEntityId.empty() &&
5!
213
         !hassToken.empty();
5!
214
}
215

216
bool ProgramOptions::CanSetupFileSave(const FeedOptions &feedOpts) const {
×
217
  return !saveDestination.empty() && !feedOpts.saveSourceUrl.empty();
×
218
}
219

220
} // 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