Adds Amcrest On-camera Motion Detection

This commit is contained in:
Jonathan Bennett 2022-01-25 22:27:11 -06:00
parent f10d0fd3f5
commit ec9403fb6f
9 changed files with 214 additions and 60 deletions

View File

@ -318,7 +318,7 @@ endif()
# Do not check for cURL if ZM_NO_CURL is on # Do not check for cURL if ZM_NO_CURL is on
if(NOT ZM_NO_CURL) if(NOT ZM_NO_CURL)
# cURL # cURL
find_package(CURL) find_package(CURL REQUIRED)
if(CURL_FOUND) if(CURL_FOUND)
set(HAVE_LIBCURL 1) set(HAVE_LIBCURL 1)
list(APPEND ZM_BIN_LIBS ${CURL_LIBRARIES}) list(APPEND ZM_BIN_LIBS ${CURL_LIBRARIES})

View File

@ -467,6 +467,7 @@ CREATE TABLE `Monitors` (
`ONVIF_Password` VARCHAR(64) NOT NULL DEFAULT '', `ONVIF_Password` VARCHAR(64) NOT NULL DEFAULT '',
`ONVIF_Options` VARCHAR(64) NOT NULL DEFAULT '', `ONVIF_Options` VARCHAR(64) NOT NULL DEFAULT '',
`ONVIF_Event_Listener` BOOLEAN NOT NULL DEFAULT FALSE, `ONVIF_Event_Listener` BOOLEAN NOT NULL DEFAULT FALSE,
`use_Amcrest_API` BOOLEAN NOT NULL DEFAULT FALSE,
`Device` tinytext NOT NULL default '', `Device` tinytext NOT NULL default '',
`Channel` tinyint(3) unsigned NOT NULL default '0', `Channel` tinyint(3) unsigned NOT NULL default '0',
`Format` int(10) unsigned NOT NULL default '0', `Format` int(10) unsigned NOT NULL default '0',

18
db/zm_update-1.37.11.sql Normal file
View File

@ -0,0 +1,18 @@
--
-- Update Monitors table to have use_Amcrest_API
--
SELECT 'Checking for use_Amcrest_API in Monitors';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Monitors'
AND table_schema = DATABASE()
AND column_name = 'use_Amcrest_API'
) > 0,
"SELECT 'Column use_Amcrest_API already exists in Monitors'",
"ALTER TABLE `Monitors` ADD COLUMN `use_Amcrest_API` BOOLEAN NOT NULL default false AFTER `ONVIF_Event_Listener`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;

View File

@ -63,6 +63,7 @@ $serial = $primary_key = 'Id';
ONVIF_Password ONVIF_Password
ONVIF_Options ONVIF_Options
ONVIF_Event_Listener ONVIF_Event_Listener
use_Amcrest_API
Device Device
Channel Channel
Format Format

View File

@ -92,7 +92,7 @@ std::string load_monitor_sql =
"`SectionLength`, `MinSectionLength`, `FrameSkip`, `MotionFrameSkip`, " "`SectionLength`, `MinSectionLength`, `FrameSkip`, `MotionFrameSkip`, "
"`FPSReportInterval`, `RefBlendPerc`, `AlarmRefBlendPerc`, `TrackMotion`, `Exif`," "`FPSReportInterval`, `RefBlendPerc`, `AlarmRefBlendPerc`, `TrackMotion`, `Exif`,"
"`RTSPServer`, `RTSPStreamName`," "`RTSPServer`, `RTSPStreamName`,"
"`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, " "`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, `use_Amcrest_API`, "
"`SignalCheckPoints`, `SignalCheckColour`, `Importance`-1 FROM `Monitors`"; "`SignalCheckPoints`, `SignalCheckColour`, `Importance`-1 FROM `Monitors`";
std::string CameraType_Strings[] = { std::string CameraType_Strings[] = {
@ -461,7 +461,7 @@ Monitor::Monitor()
"SectionLength, MinSectionLength, FrameSkip, MotionFrameSkip, " "SectionLength, MinSectionLength, FrameSkip, MotionFrameSkip, "
"FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, Exif," "FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, Exif,"
"`RTSPServer`,`RTSPStreamName`, "`RTSPServer`,`RTSPStreamName`,
"`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, " "`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, `use_Amcrest_API`, "
"SignalCheckPoints, SignalCheckColour, Importance-1 FROM Monitors"; "SignalCheckPoints, SignalCheckColour, Importance-1 FROM Monitors";
*/ */
@ -639,6 +639,7 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) {
onvif_password = std::string(dbrow[col] ? dbrow[col] : ""); col++; onvif_password = std::string(dbrow[col] ? dbrow[col] : ""); col++;
onvif_options = std::string(dbrow[col] ? dbrow[col] : ""); col++; onvif_options = std::string(dbrow[col] ? dbrow[col] : ""); col++;
onvif_event_listener = (*dbrow[col] != '0'); col++; onvif_event_listener = (*dbrow[col] != '0'); col++;
use_Amcrest_API = (*dbrow[col] != '0'); col++;
/*"SignalCheckPoints, SignalCheckColour, Importance-1 FROM Monitors"; */ /*"SignalCheckPoints, SignalCheckColour, Importance-1 FROM Monitors"; */
signal_check_points = atoi(dbrow[col]); col++; signal_check_points = atoi(dbrow[col]); col++;
@ -1040,6 +1041,7 @@ bool Monitor::connect() {
Debug(3, "Allocated %zu %zu image buffers", image_buffer.capacity(), image_buffer.size()); Debug(3, "Allocated %zu %zu image buffers", image_buffer.capacity(), image_buffer.size());
if (purpose == CAPTURE) { if (purpose == CAPTURE) {
curl_global_init(CURL_GLOBAL_DEFAULT); //May not be the appropriate place. Need to do this before any other curl calls, and any other threads start.
memset(mem_ptr, 0, mem_size); memset(mem_ptr, 0, mem_size);
shared_data->size = sizeof(SharedData); shared_data->size = sizeof(SharedData);
shared_data->active = enabled; shared_data->active = enabled;
@ -1079,47 +1081,59 @@ bool Monitor::connect() {
usedsubpixorder = camera->SubpixelOrder(); // Used in CheckSignal usedsubpixorder = camera->SubpixelOrder(); // Used in CheckSignal
shared_data->valid = true; shared_data->valid = true;
#ifdef WITH_GSOAP
//ONVIF Setup //ONVIF and Amcrest Setup
//since they serve the same function, handling them as two options of the same feature.
ONVIF_Trigger_State = FALSE; ONVIF_Trigger_State = FALSE;
if (onvif_event_listener) { //Temporarily using this option to enable the feature if (onvif_event_listener) { //
Debug(1, "Starting ONVIF"); Debug(1, "Starting ONVIF");
ONVIF_Healthy = FALSE; ONVIF_Healthy = FALSE;
if (onvif_options.find("closes_event") != std::string::npos) { //Option to indicate that ONVIF will send a close event message if (onvif_options.find("closes_event") != std::string::npos) { //Option to indicate that ONVIF will send a close event message
ONVIF_Closes_Event = TRUE; ONVIF_Closes_Event = TRUE;
} }
tev__PullMessages.Timeout = "PT600S"; if (use_Amcrest_API) {
tev__PullMessages.MessageLimit = 100; curl_multi = curl_multi_init();
soap = soap_new(); start_Amcrest();
soap->connect_timeout = 5; //spin up curl_multi
soap->recv_timeout = 5; //use the onvif_user and onvif_pass and onvif_url here.
soap->send_timeout = 5; //going to use the non-blocking curl api, and in the polling thread, block for 5 seconds waiting for input, just like onvif
soap_register_plugin(soap, soap_wsse); //note that it's not possible for a single camera to use both.
proxyEvent = PullPointSubscriptionBindingProxy(soap); } else { //using GSOAP
std::string full_url = onvif_url + "/Events"; #ifdef WITH_GSOAP
proxyEvent.soap_endpoint = full_url.c_str(); tev__PullMessages.Timeout = "PT600S";
tev__PullMessages.MessageLimit = 100;
soap = soap_new();
soap->connect_timeout = 5;
soap->recv_timeout = 5;
soap->send_timeout = 5;
soap_register_plugin(soap, soap_wsse);
proxyEvent = PullPointSubscriptionBindingProxy(soap);
std::string full_url = onvif_url + "/Events";
proxyEvent.soap_endpoint = full_url.c_str();
set_credentials(soap);
Debug(1, "ONVIF Endpoint: %s", proxyEvent.soap_endpoint);
if (proxyEvent.CreatePullPointSubscription(&request, response) != SOAP_OK) {
Error("Couldn't create subscription! %s, %s", soap_fault_string(soap), soap_fault_detail(soap));
} else {
//Empty the stored messages
set_credentials(soap); set_credentials(soap);
if ((proxyEvent.PullMessages(response.SubscriptionReference.Address, NULL, &tev__PullMessages, tev__PullMessagesResponse) != SOAP_OK) && Debug(1, "ONVIF Endpoint: %s", proxyEvent.soap_endpoint);
( soap->error != SOAP_EOF)) { //SOAP_EOF could indicate no messages to pull. if (proxyEvent.CreatePullPointSubscription(&request, response) != SOAP_OK) {
Error("Couldn't do initial event pull! Error %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); Error("Couldn't create subscription! %s, %s", soap_fault_string(soap), soap_fault_detail(soap));
} else { } else {
Debug(1, "Good Initial ONVIF Pull"); //Empty the stored messages
ONVIF_Healthy = TRUE; set_credentials(soap);
if ((proxyEvent.PullMessages(response.SubscriptionReference.Address, NULL, &tev__PullMessages, tev__PullMessagesResponse) != SOAP_OK) &&
( soap->error != SOAP_EOF)) { //SOAP_EOF could indicate no messages to pull.
Error("Couldn't do initial event pull! Error %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap));
} else {
Debug(1, "Good Initial ONVIF Pull");
ONVIF_Healthy = TRUE;
}
} }
#else
Error("zmc not compiled with GSOAP. ONVIF support not built in!");
#endif
} }
} else { } else {
Debug(1, "Not Starting ONVIF"); Debug(1, "Not Starting ONVIF");
} }
//End ONVIF Setup //End ONVIF Setup
#endif
#if HAVE_LIBCURL //janus setup. Depends on libcurl. #if HAVE_LIBCURL //janus setup. Depends on libcurl.
if (janus_enabled && (path.find("rtsp://") != std::string::npos)) { if (janus_enabled && (path.find("rtsp://") != std::string::npos)) {
@ -1228,6 +1242,12 @@ Monitor::~Monitor() {
sws_freeContext(convert_context); sws_freeContext(convert_context);
convert_context = nullptr; convert_context = nullptr;
} }
if (Amcrest_handle != nullptr) {
curl_multi_remove_handle(curl_multi, Amcrest_handle);
curl_easy_cleanup(Amcrest_handle);
}
if (curl_multi != nullptr) curl_multi_cleanup(curl_multi);
curl_global_cleanup();
} // end Monitor::~Monitor() } // end Monitor::~Monitor()
void Monitor::AddPrivacyBitmask() { void Monitor::AddPrivacyBitmask() {
@ -1800,36 +1820,25 @@ bool Monitor::Poll() {
//We want to trigger every 5 seconds or so. so grab the time at the beginning of the loop, and sleep at the end. //We want to trigger every 5 seconds or so. so grab the time at the beginning of the loop, and sleep at the end.
std::chrono::system_clock::time_point loop_start_time = std::chrono::system_clock::now(); std::chrono::system_clock::time_point loop_start_time = std::chrono::system_clock::now();
#ifdef WITH_GSOAP
if (ONVIF_Healthy) { if (ONVIF_Healthy) {
set_credentials(soap); if(use_Amcrest_API) {
int result = proxyEvent.PullMessages(response.SubscriptionReference.Address, NULL, &tev__PullMessages, tev__PullMessagesResponse); int open_handles;
if (result != SOAP_OK) { int transfers;
if (result != SOAP_EOF) { //Ignore the timeout error curl_multi_perform(curl_multi, &open_handles);
Error("Failed to get ONVIF messages! %s", soap_fault_string(soap)); if (open_handles == 0) {
ONVIF_Healthy = FALSE; start_Amcrest(); //http transfer ended, need to restart.
} } else {
} else { curl_multi_wait(curl_multi, NULL, 0, 5000, &transfers); //wait for max 5 seconds for event.
Debug(1, "Got Good Response! %i", result); if (transfers > 0) { //have data to deal with
for (auto msg : tev__PullMessagesResponse.wsnt__NotificationMessage) { curl_multi_perform(curl_multi, &open_handles); //actually grabs the data, populates amcrest_response
if (msg->Topic->__any.text != NULL && if (amcrest_response.find("action=Start") != std::string::npos) {
std::strstr(msg->Topic->__any.text, "MotionAlarm") &&
msg->Message.__any.elts != NULL &&
msg->Message.__any.elts->next != NULL &&
msg->Message.__any.elts->next->elts != NULL &&
msg->Message.__any.elts->next->elts->atts != NULL &&
msg->Message.__any.elts->next->elts->atts->next != NULL &&
msg->Message.__any.elts->next->elts->atts->next->text != NULL) {
Debug(1,"Got Motion Alarm!");
if (strcmp(msg->Message.__any.elts->next->elts->atts->next->text, "true") == 0) {
//Event Start //Event Start
Debug(1,"Triggered on ONVIF"); Debug(1,"Triggered on ONVIF");
if (!ONVIF_Trigger_State) { if (!ONVIF_Trigger_State) {
Debug(1,"Triggered Event"); Debug(1,"Triggered Event");
ONVIF_Trigger_State = TRUE; ONVIF_Trigger_State = TRUE;
std::this_thread::sleep_for (std::chrono::seconds(1)); //thread sleep
} }
} else { } else if (amcrest_response.find("action=Stop") != std::string::npos){
Debug(1, "Triggered off ONVIF"); Debug(1, "Triggered off ONVIF");
ONVIF_Trigger_State = FALSE; ONVIF_Trigger_State = FALSE;
if (!ONVIF_Closes_Event) { //If we get a close event, then we know to expect them. if (!ONVIF_Closes_Event) { //If we get a close event, then we know to expect them.
@ -1837,11 +1846,53 @@ bool Monitor::Poll() {
Debug(1,"Setting ClosesEvent"); Debug(1,"Setting ClosesEvent");
} }
} }
amcrest_response.clear(); //We've dealt with the message, need to clear the queue
} }
} }
} else {
#ifdef WITH_GSOAP
set_credentials(soap);
int result = proxyEvent.PullMessages(response.SubscriptionReference.Address, NULL, &tev__PullMessages, tev__PullMessagesResponse);
if (result != SOAP_OK) {
if (result != SOAP_EOF) { //Ignore the timeout error
Error("Failed to get ONVIF messages! %s", soap_fault_string(soap));
ONVIF_Healthy = FALSE;
}
} else {
Debug(1, "Got Good Response! %i", result);
for (auto msg : tev__PullMessagesResponse.wsnt__NotificationMessage) {
if (msg->Topic->__any.text != NULL &&
std::strstr(msg->Topic->__any.text, "MotionAlarm") &&
msg->Message.__any.elts != NULL &&
msg->Message.__any.elts->next != NULL &&
msg->Message.__any.elts->next->elts != NULL &&
msg->Message.__any.elts->next->elts->atts != NULL &&
msg->Message.__any.elts->next->elts->atts->next != NULL &&
msg->Message.__any.elts->next->elts->atts->next->text != NULL) {
Debug(1,"Got Motion Alarm!");
if (strcmp(msg->Message.__any.elts->next->elts->atts->next->text, "true") == 0) {
//Event Start
Debug(1,"Triggered on ONVIF");
if (!ONVIF_Trigger_State) {
Debug(1,"Triggered Event");
ONVIF_Trigger_State = TRUE;
std::this_thread::sleep_for (std::chrono::seconds(1)); //thread sleep
}
} else {
Debug(1, "Triggered off ONVIF");
ONVIF_Trigger_State = FALSE;
if (!ONVIF_Closes_Event) { //If we get a close event, then we know to expect them.
ONVIF_Closes_Event = TRUE;
Debug(1,"Setting ClosesEvent");
}
}
}
}
}
#endif
} }
} }
#endif
if (janus_enabled) { if (janus_enabled) {
if (janus_session.empty()) { if (janus_session.empty()) {
get_janus_session(); get_janus_session();
@ -3223,11 +3274,15 @@ int Monitor::Close() {
analysis_thread->Stop(); analysis_thread->Stop();
} }
#ifdef WITH_GSOAP
//ONVIF Teardown //ONVIF Teardown
if (Poller) { if (Poller) {
Poller->Stop(); Poller->Stop();
} }
if (curl_multi != nullptr) {
curl_multi_cleanup(curl_multi);
curl_multi = nullptr;
}
#ifdef WITH_GSOAP
if (onvif_event_listener && (soap != nullptr)) { if (onvif_event_listener && (soap != nullptr)) {
Debug(1, "Tearing Down Onvif"); Debug(1, "Tearing Down Onvif");
_wsnt__Unsubscribe wsnt__Unsubscribe; _wsnt__Unsubscribe wsnt__Unsubscribe;
@ -3240,9 +3295,9 @@ int Monitor::Close() {
} //End ONVIF } //End ONVIF
#endif #endif
#if HAVE_LIBCURL //Janus Teardown #if HAVE_LIBCURL //Janus Teardown
if (janus_enabled && (purpose == CAPTURE)) { if (janus_enabled && (purpose == CAPTURE)) {
remove_from_janus(); remove_from_janus();
} }
#endif #endif
packetqueue.clear(); packetqueue.clear();
@ -3614,3 +3669,55 @@ int Monitor::get_janus_session() {
curl_easy_cleanup(curl); curl_easy_cleanup(curl);
return 1; return 1;
} //get_janus_session } //get_janus_session
int Monitor::start_Amcrest() {
//init the transfer and start it in multi-handle
int running_handles;
long response_code;
struct CURLMsg *m;
CURLMcode curl_error;
if (Amcrest_handle != nullptr) { //potentially clean up the old handle
curl_multi_remove_handle(curl_multi, Amcrest_handle);
curl_easy_cleanup(Amcrest_handle);
}
std::string full_url = onvif_url;
if (full_url.back() != '/') full_url += '/';
full_url += "eventManager.cgi?action=attach&codes=[VideoMotion]";
Amcrest_handle = curl_easy_init();
if (!Amcrest_handle){
Warning("Handle is null!");
return -1;
}
curl_easy_setopt(Amcrest_handle, CURLOPT_URL, full_url.c_str());
curl_easy_setopt(Amcrest_handle, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(Amcrest_handle, CURLOPT_WRITEDATA, &amcrest_response);
curl_easy_setopt(Amcrest_handle, CURLOPT_USERNAME, onvif_username.c_str());
curl_easy_setopt(Amcrest_handle, CURLOPT_PASSWORD, onvif_password.c_str());
curl_easy_setopt(Amcrest_handle, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
curl_error = curl_multi_add_handle(curl_multi, Amcrest_handle);
Warning("error of %i", curl_error);
curl_error = curl_multi_perform(curl_multi, &running_handles);
if (curl_error == CURLM_OK) {
curl_easy_getinfo(Amcrest_handle, CURLINFO_RESPONSE_CODE, &response_code);
int msgq = 0;
m = curl_multi_info_read(curl_multi, &msgq);
if (m && (m->msg == CURLMSG_DONE)) {
Warning("Libcurl exited Early: %i", m->data.result);
}
curl_multi_wait(curl_multi, NULL, 0, 300, NULL);
curl_error = curl_multi_perform(curl_multi, &running_handles);
}
if ((curl_error == CURLM_OK) && (running_handles > 0)) {
ONVIF_Healthy = TRUE;
} else {
Warning("Response: %s", amcrest_response.c_str());
Warning("Seeing %i streams, and error of %i, url: %s", running_handles, curl_error, full_url.c_str());
curl_easy_getinfo(Amcrest_handle, CURLINFO_OS_ERRNO, &response_code);
Warning("Response code: %lu", response_code);
}
return 0;
}

View File

@ -285,7 +285,9 @@ protected:
std::string onvif_username; std::string onvif_username;
std::string onvif_password; std::string onvif_password;
std::string onvif_options; std::string onvif_options;
std::string amcrest_response;
bool onvif_event_listener; bool onvif_event_listener;
bool use_Amcrest_API;
std::string device; std::string device;
int palette; int palette;
@ -437,7 +439,7 @@ protected:
//ONVIF //ONVIF
#ifdef WITH_GSOAP #ifdef WITH_GSOAP
struct soap *soap; struct soap *soap = nullptr;
bool ONVIF_Trigger_State; bool ONVIF_Trigger_State;
bool ONVIF_Healthy; bool ONVIF_Healthy;
bool ONVIF_Closes_Event; bool ONVIF_Closes_Event;
@ -449,8 +451,11 @@ protected:
void set_credentials(struct soap *soap); void set_credentials(struct soap *soap);
#endif #endif
//curl stuff for Janus //curl stuff
CURL *curl; CURL *curl = nullptr;
CURLM *curl_multi = nullptr;
CURL *Amcrest_handle = nullptr;
int start_Amcrest();
//helper class for CURL //helper class for CURL
static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp); static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp);
int add_to_janus(); int add_to_janus();

View File

@ -66,6 +66,7 @@ class Monitor extends ZM_Object {
'ONVIF_Password' => '', 'ONVIF_Password' => '',
'ONVIF_Options' => '', 'ONVIF_Options' => '',
'ONVIF_Event_Listener' => '0', 'ONVIF_Event_Listener' => '0',
'use_Amcrest_API' => '0',
'Device' => '', 'Device' => '',
'Channel' => 0, 'Channel' => 0,
'Format' => '0', 'Format' => '0',

View File

@ -275,6 +275,23 @@ function initPage() {
} }
}); });
// Amcrest API controller
if (document.getElementsByName("newMonitor[ONVIF_Event_Listener]")[0].checked) {
document.getElementById("function_use_Amcrest_API").hidden = false;
} else {
document.getElementById("function_use_Amcrest_API").hidden = true;
}
document.getElementsByName("newMonitor[ONVIF_Event_Listener]")[0].addEventListener('change', function() {
if (this.checked) {
document.getElementById("function_use_Amcrest_API").hidden = false;
}
});
document.getElementsByName("newMonitor[ONVIF_Event_Listener]")[1].addEventListener('change', function() {
if (this.checked) {
document.getElementById("function_use_Amcrest_API").hidden = true;
}
});
if ( ZM_OPT_USE_GEOLOCATION ) { if ( ZM_OPT_USE_GEOLOCATION ) {
if ( window.L ) { if ( window.L ) {
var form = document.getElementById('contentForm'); var form = document.getElementById('contentForm');

View File

@ -730,6 +730,10 @@ if (count($available_monitor_ids)) {
<td class="text-right pr-3"><?php echo translate('ONVIF_Event_Listener') ?></td> <td class="text-right pr-3"><?php echo translate('ONVIF_Event_Listener') ?></td>
<td><?php echo html_radio('newMonitor[ONVIF_Event_Listener]', array('1'=>translate('Enabled'), '0'=>'Disabled'), $monitor->ONVIF_Event_Listener()); ?></td> <td><?php echo html_radio('newMonitor[ONVIF_Event_Listener]', array('1'=>translate('Enabled'), '0'=>'Disabled'), $monitor->ONVIF_Event_Listener()); ?></td>
</tr> </tr>
<tr id="function_use_Amcrest_API">
<td class="text-right pr-3"><?php echo translate('use_Amcrest_API') ?></td>
<td><?php echo html_radio('newMonitor[use_Amcrest_API]', array('1'=>translate('Enabled'), '0'=>'Disabled'), $monitor->use_Amcrest_API()); ?></td>
</tr>
<?php <?php
break; break;
} }