• 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

0.0
/src/Gui/WebHandler.cxx
1
#include "WindowsWrapper.h"
2

3
#include "Logger.h"
4

5
#include "Gui/WebHandler.h"
6

7
#include <barrier>
8
#include <iostream>
9

10
#define JSON_USE_IMPLICIT_CONVERSIONS 0
11
#include <nlohmann/json.hpp>
12

13
#include <opencv2/imgcodecs.hpp>
14
#include <opencv2/imgproc.hpp>
15

16
using namespace std::string_view_literals;
17

18
using json = nlohmann::json;
19

20
namespace {
21

22
static std::atomic_bool broadcastLogs{false};
23
static struct mg_mgr *pMgr{nullptr};
24
static unsigned long parentConnId{0};
25

26
static std::shared_mutex feedMappingMtx;
27
static std::unordered_map<std::string_view, std::filesystem::path>
28
    savedFilesPath;
29
static std::unordered_map<std::string_view, char> feedIds;
30
static std::atomic_char feedMarker{1};
31

32
static constexpr const char *mjpegHeaders =
33
    "HTTP/1.0 200 OK\r\n"
34
    "Cache-Control: no-cache\r\n"
35
    "Pragma: no-cache\r\n"
36
    "Content-Type: multipart/x-mixed-replace; boundary=--boundary\r\n\r\n";
37

38
char SafeGetFeedId(mg_str &cap) {
×
39
  std::shared_lock lk(feedMappingMtx);
×
40
  const auto it = feedIds.find({cap.buf, cap.len});
×
41
  if (it != feedIds.end()) {
×
42
    return it->second;
×
43
  }
44
  return 0;
×
45
}
×
46

47
} // namespace
48

49
namespace gui {
50

UNCOV
51
void WebHandler::BroadcastMjpegFrame(
×
52
    gui::WebHandler::BroadcastMap *broadcastMap) {
53

UNCOV
54
  for (const auto &imageData : *broadcastMap | std::views::values) {
×
UNCOV
55
    std::array<BroadcastImageData *, 2> ds{&imageData->imageBroadcastData_,
×
UNCOV
56
                                           &imageData->modelBroadcastData_};
×
UNCOV
57
    for (const auto &broadcastData : ds) {
×
58

UNCOV
59
      mg_mgr *mgr = broadcastData->mgr;
×
UNCOV
60
      std::vector<uint8_t> &jpgBuf = broadcastData->jpgBuf;
×
UNCOV
61
      const auto marker = broadcastData->marker;
×
UNCOV
62
      std::shared_mutex *mtx = broadcastData->mtx;
×
63

UNCOV
64
      for (mg_connection *c = mgr->conns; c != nullptr; c = c->next) {
×
UNCOV
65
        if (std::ranges::equal(std::span(c->data, 2), marker) &&
×
66
            !jpgBuf.empty()) {
×
67
          std::shared_lock lk(*mtx);
×
68
          mg_printf(c,
×
69
                    "--boundary\r\nContent-Type: image/jpeg\r\n"
70
                    "Content-Length: %lu\r\n\r\n",
71
                    jpgBuf.size());
72
          mg_send(c, jpgBuf.data(), jpgBuf.size());
×
73
          mg_send(c, "\r\n", 2);
×
74
        }
×
75
      }
76
    }
77
  }
UNCOV
78
}
×
79

UNCOV
80
void WebHandler::BroadcastImage_TimerCallback(void *arg) {
×
UNCOV
81
  if (arg) {
×
UNCOV
82
    BroadcastMjpegFrame(static_cast<gui::WebHandler::BroadcastMap *>(arg));
×
83
  }
UNCOV
84
}
×
85

86
// HTTP server event handler function
UNCOV
87
void WebHandler::EventHandler(mg_connection *c, int ev, void *ev_data) {
×
UNCOV
88
  switch (ev) {
×
UNCOV
89
  case MG_EV_OPEN:
×
UNCOV
90
    if (c->is_listening) {
×
UNCOV
91
      pMgr = c->mgr;
×
UNCOV
92
      parentConnId = c->id;
×
UNCOV
93
      broadcastLogs = true;
×
94
    }
UNCOV
95
    break;
×
UNCOV
96
  case MG_EV_HTTP_MSG: {
×
UNCOV
97
    struct mg_http_message *hm = static_cast<mg_http_message *>(ev_data);
×
UNCOV
98
    struct mg_str cap[2] = {mg_str(""), mg_str("")};
×
99

UNCOV
100
    if (mg_match(hm->uri, mg_str("/media/feeds"), nullptr)) {
×
UNCOV
101
      json feeds = json::array();
×
UNCOV
102
      std::ranges::copy(feedIds | std::views::keys, std::back_inserter(feeds));
×
UNCOV
103
      mg_http_reply(c, 200, "Content-Type: application/json\r\n",
×
UNCOV
104
                    feeds.dump().c_str());
×
UNCOV
105
    } else if (mg_match(hm->uri, mg_str("/media/live/*"), cap)) {
×
106
      c->data[0] = 'L';
×
107
      c->data[1] = SafeGetFeedId(cap[0]);
×
108
      mg_printf(c, "%s", mjpegHeaders);
×
UNCOV
109
    } else if (mg_match(hm->uri, mg_str("/media/model/*"), cap)) {
×
110
      c->data[0] = 'M';
×
111
      c->data[1] = SafeGetFeedId(cap[0]);
×
112
      mg_printf(c, "%s", mjpegHeaders);
×
UNCOV
113
    } else if (mg_match(hm->uri, mg_str("/websocket"), nullptr)) {
×
114
      mg_ws_upgrade(c, hm, nullptr);
×
115
      c->data[0] = 'W';
×
UNCOV
116
    } else if (mg_match(hm->uri, mg_str("/media/saved/*/*"), cap)) {
×
117
      const auto &cSavedFilesPath = savedFilesPath;
×
118
      const std::string_view savedFilesSlug(cap[0].buf, cap[0].len);
×
119
      if (savedFilesSlug.empty() || !cSavedFilesPath.contains(savedFilesSlug)) {
×
120
        mg_http_reply(c, 404, "", "Saved Media Slug Not Found");
×
121
        return;
×
122
      }
123
      struct mg_http_serve_opts opts;
124
      memset(&opts, 0, sizeof(opts));
×
125
      thread_local std::string pathStr;
×
126
      const std::string_view savedFilesPath(cap[1].buf, cap[1].len);
×
127
      if (savedFilesPath.empty() || savedFilesSlug == "null"sv) {
×
128
        pathStr = std::format(".,/media/saved/{}/={}", savedFilesSlug,
×
129
                              cSavedFilesPath.at(savedFilesSlug).string());
×
130
        opts.root_dir = pathStr.c_str();
×
131
        mg_http_serve_dir(c, hm, &opts);
×
132
      } else {
133
        pathStr =
134
            (cSavedFilesPath.at(savedFilesSlug) / savedFilesPath).string();
×
135
        opts.mime_types = "jpg=image/jpg";
×
136
        mg_http_serve_file(c, hm, pathStr.c_str(), &opts);
×
137
      }
138
    } else {
139
      struct mg_http_serve_opts opts;
UNCOV
140
      memset(&opts, 0, sizeof(opts));
×
141
#ifdef SERVE_UNPACKED
142
      // Use for testing, enables "hot reload" for resources in public folder
143
      static const auto rootDir =
144
          std::filesystem::path(__FILE__).parent_path() / "public";
145
      thread_local std::string pathStr;
146
      pathStr = rootDir.string();
147
      opts.root_dir = pathStr.c_str();
148
#else
UNCOV
149
      opts.root_dir = "/public";
×
UNCOV
150
      opts.fs = &mg_fs_packed;
×
151
#endif
UNCOV
152
      mg_http_serve_dir(c, hm, &opts);
×
153
    }
UNCOV
154
  } break;
×
UNCOV
155
  case MG_EV_WAKEUP: {
×
UNCOV
156
    const std::string_view msg = *((std::string_view *)ev_data);
×
UNCOV
157
    for (mg_connection *wc = c->mgr->conns; wc != nullptr; wc = wc->next) {
×
UNCOV
158
      if (wc->data[0] == 'W') {
×
159
        mg_ws_send(wc, msg.data(), msg.size(), WEBSOCKET_OP_TEXT);
×
160
      }
161
    }
UNCOV
162
  } break;
×
163
  }
164
}
165

UNCOV
166
void WebHandler::SetSavedFilesServePath(
×
167
    std::string_view slug, const std::filesystem::path &_savedFilesPath) {
168
  savedFilesPath[slug] = _savedFilesPath;
×
169
}
×
170

UNCOV
171
WebHandler::WebHandler(int port, std::string_view host) {
×
UNCOV
172
  url_.set_scheme("http");
×
UNCOV
173
  url_.set_host(host);
×
UNCOV
174
  url_.set_port_number(port);
×
UNCOV
175
}
×
176

UNCOV
177
void WebHandler::Start() {
×
UNCOV
178
  std::barrier sync(2);
×
UNCOV
179
  listenerThread_ = std::jthread([this, &sync](std::stop_token stopToken) {
×
180
#ifdef _WIN32
181
    SetThreadDescription(GetCurrentThread(), L"WebHandler Thread");
182
#endif
183
#ifdef _DEBUG
UNCOV
184
    mg_log_set(MG_LL_DEBUG);
×
185
#endif
UNCOV
186
    mg_mgr_init(&mgr_);
×
UNCOV
187
    mg_timer_add(&mgr_, 33, MG_TIMER_REPEAT, BroadcastImage_TimerCallback,
×
UNCOV
188
                 &feedImageDataMap_);
×
UNCOV
189
    mg_wakeup_init(&mgr_);
×
UNCOV
190
    sync.arrive_and_drop();
×
UNCOV
191
    mg_http_listen(&mgr_, url_.c_str(), EventHandler, nullptr);
×
UNCOV
192
    while (!stopToken.stop_requested()) {
×
UNCOV
193
      mg_mgr_poll(&mgr_, 100);
×
194
    }
UNCOV
195
    mg_mgr_free(&mgr_);
×
UNCOV
196
  });
×
197

UNCOV
198
  sync.arrive_and_wait();
×
199
  auto pWebsocketSink = std::make_shared<spdlog::sinks::callback_sink_mt>(
UNCOV
200
      [this](const spdlog::details::log_msg &msg) {
×
201
        const auto levelSv =
UNCOV
202
            std::string_view(spdlog::level::to_string_view(msg.level));
×
UNCOV
203
        const auto msgSv = std::string_view(msg.payload);
×
UNCOV
204
        thread_local json wsMsg = {};
×
UNCOV
205
        thread_local std::string dumped;
×
206

UNCOV
207
        wsMsg["log"] = {};
×
208

UNCOV
209
        wsMsg["log"]["level"] =
×
UNCOV
210
            std::string_view(spdlog::level::to_string_view(msg.level));
×
UNCOV
211
        wsMsg["log"]["payload"] = std::string_view(msg.payload);
×
UNCOV
212
        wsMsg["log"]["timestamp"] = std::format("{}", msg.time);
×
UNCOV
213
        dumped = wsMsg.dump();
×
214

UNCOV
215
        if (broadcastLogs) {
×
UNCOV
216
          mg_wakeup(pMgr, parentConnId, dumped.data(), dumped.size());
×
217
        }
UNCOV
218
      });
×
219

UNCOV
220
  LOGGER->debug("Initializing WebSocket sink");
×
UNCOV
221
  LOGGER->sinks().push_back(pWebsocketSink);
×
UNCOV
222
}
×
223

UNCOV
224
void WebHandler::Stop() { listenerThread_ = {}; }
×
225

226
const boost::url &WebHandler::GetUrl() const noexcept { return url_; }
×
227

UNCOV
228
void WebHandler::operator()(Payload data) {
×
229

UNCOV
230
  if (!feedIds.contains(data.feedId) ||
×
231
      !feedImageDataMap_.contains(data.feedId)) {
×
UNCOV
232
    std::scoped_lock lk(feedMappingMtx);
×
UNCOV
233
    if (feedMarker < 0) {
×
234
      // maximum feeds of 128, log an error and early exit
235
      static bool warnOnce{false};
236
      if (!warnOnce) {
×
237
        LOGGER->warn("Maximum feed count reached, cannot add {}", data.feedId);
×
238
      }
239
      return;
×
240
    }
UNCOV
241
    const char thisFeedMarker = feedMarker;
×
242

UNCOV
243
    const auto [feedIdIt, didInsert_feedId] =
×
UNCOV
244
        feedIds.insert({data.feedId, thisFeedMarker});
×
UNCOV
245
    if (didInsert_feedId) {
×
UNCOV
246
      ++feedMarker;
×
247
    }
UNCOV
248
    auto [feedDataIt, didInsert_feedData] = feedImageDataMap_.insert(
×
UNCOV
249
        {data.feedId, std::make_unique<FeedImageData>(&mgr_)});
×
UNCOV
250
    if (!didInsert_feedData) {
×
251
      throw std::runtime_error(
252
          std::format("Failed to add feed data with ID {}", thisFeedMarker));
×
253
    } else {
UNCOV
254
      feedDataIt->second->imageBroadcastData_.marker[1] = thisFeedMarker;
×
UNCOV
255
      feedDataIt->second->modelBroadcastData_.marker[1] = thisFeedMarker;
×
256
    }
257

UNCOV
258
    LOGGER->info("Feed {} available at Web GUI", data.feedId);
×
UNCOV
259
  }
×
260

UNCOV
261
  auto &fi = feedImageDataMap_.at(data.feedId);
×
UNCOV
262
  if (!data.frame.img.empty()) {
×
UNCOV
263
    switch (data.frame.img.channels()) {
×
UNCOV
264
    case 1:
×
UNCOV
265
      cv::cvtColor(data.frame.img, fi->imageBgr_, cv::COLOR_GRAY2BGR);
×
UNCOV
266
      cv::cvtColor(data.detail, fi->modelBgr_, cv::COLOR_GRAY2BGR);
×
UNCOV
267
      break;
×
UNCOV
268
    case 3:
×
UNCOV
269
      data.frame.img.copyTo(fi->imageBgr_);
×
UNCOV
270
      data.detail.copyTo(fi->modelBgr_);
×
UNCOV
271
      break;
×
UNCOV
272
    case 4:
×
UNCOV
273
      cv::cvtColor(data.frame.img, fi->imageBgr_, cv::COLOR_BGRA2BGR);
×
UNCOV
274
      cv::cvtColor(data.detail, fi->modelBgr_, cv::COLOR_BGRA2BGR);
×
UNCOV
275
      break;
×
276
    default:
×
277
      throw std::invalid_argument("Frame must be BGR, BGRA, or Monochrome");
×
278
    }
279

UNCOV
280
    for (const auto &bbox : data.rois) {
×
281
      cv::rectangle(fi->imageBgr_, bbox, cv::Scalar(0x00, 0xFF, 0x00), 1);
×
282
    }
283

UNCOV
284
    thread_local std::string txt;
×
UNCOV
285
    txt = std::format(
×
UNCOV
286
        "Frame: {} | Objects: {}{}", data.frame.id, data.rois.size(),
×
UNCOV
287
        std::isnormal(data.fps) ? std::format(" | FPS: {:.1f}", data.fps) : "");
×
UNCOV
288
    cv::Point2i anchor{int(fi->imageBgr_.cols * 0.05),
×
UNCOV
289
                       int(fi->imageBgr_.rows * 0.05)};
×
UNCOV
290
    cv::putText(fi->modelBgr_, txt, anchor,
×
UNCOV
291
                cv::HersheyFonts::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0x00),
×
292
                3);
UNCOV
293
    cv::putText(fi->modelBgr_, txt, anchor,
×
294
                cv::HersheyFonts::FONT_HERSHEY_SIMPLEX, 0.5,
UNCOV
295
                cv::Scalar(0x00, 0xFF, 0xFF), 1);
×
296

UNCOV
297
    if (auto lk = std::unique_lock(fi->imageMtx_)) {
×
UNCOV
298
      cv::imencode(".jpg", fi->imageBgr_, fi->imageJpeg_);
×
UNCOV
299
      std::swap(fi->imageJpeg_, fi->imageBroadcastData_.jpgBuf);
×
UNCOV
300
    }
×
UNCOV
301
    if (auto lk = std::unique_lock(fi->modelMtx_)) {
×
UNCOV
302
      cv::imencode(".jpg", fi->modelBgr_, fi->modelJpeg_);
×
UNCOV
303
      std::swap(fi->modelJpeg_, fi->modelBroadcastData_.jpgBuf);
×
UNCOV
304
    }
×
305
  }
306
}
307

308
} // namespace gui
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