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

bcpearce / hass-motion-detection-addon / 21126887229

19 Jan 2026 05:54AM UTC coverage: 17.99% (-32.0%) from 49.971%
21126887229

Pull #26

github

web-flow
Merge c98d1d60e into 0206cb867
Pull Request #26: Additional test coverage

292 of 1865 branches covered (15.66%)

Branch coverage included in aggregate %.

8 of 22 new or added lines in 3 files covered. (36.36%)

827 existing lines in 29 files now uncovered.

347 of 1687 relevant lines covered (20.57%)

28.54 hits per line

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

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

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

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

57
  /*
58
   * Web User-Interface Options
59
   */
60
  po::options_description webUiOptions("Web User-Interface Options");
4✔
61
  webUiOptions.add_options()
2✔
62
      /**/
63
      ("web-ui-host,s", po::value<std::string>()->default_value("0.0.0.0"),
4✔
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),
2✔
68
       "port to bind the web UI to, defaults to 32834, set to <= 0 to disable "
69
       "the Web GUI");
70
  allOptions.add(webUiOptions);
2✔
71

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

83
  po::variables_map vm;
2✔
84

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

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

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

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

UNCOV
110
  ProgramOptions options;
×
111
  try {
UNCOV
112
    po::notify(vm);
×
113

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

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

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

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

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

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

UNCOV
136
  std::unordered_map<std::string, ProgramOptions::FeedOptions> res;
×
137

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

UNCOV
195
  return res;
×
196
}
×
197

UNCOV
198
auto ProgramOptions::FeedOptions::ParseJson(const std::filesystem::path &json)
×
199
    -> std::unordered_map<std::string, FeedOptions> {
UNCOV
200
  std::ifstream file(json);
×
UNCOV
201
  const nlohmann::json data = nlohmann::json::parse(file);
×
UNCOV
202
  return _ParseJson(data);
×
UNCOV
203
}
×
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

UNCOV
211
bool ProgramOptions::CanSetupHass(const FeedOptions &feedOpts) const {
×
UNCOV
212
  return !hassUrl.empty() && !feedOpts.hassEntityId.empty() &&
×
UNCOV
213
         !hassToken.empty();
×
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