mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-11-07 13:13:05 -05:00
A camera that detects motion should be able to sound a speaker, and the
speaker is rarely the camera: it is a separate device with its own address,
credentials, stream and Controls entry. Model it as a monitor and let a
monitor's alarm drive actions on other monitors.
Add Monitors.DeviceClass enum('Camera','Speaker'). This is what the device
is, as distinct from Type, which selects the capture backend - an IP speaker
still captures over Ffmpeg like any other RTSP device, so Type could not
carry the distinction.
Add the MonitorActions table: MonitorId is the monitor that triggers,
TargetMonitorId the device acted on, and the two are frequently different.
TriggerOn covers EventStart, EventEnd, Alarm and Manual.
Which actions a device is offered is decided by its Controls row - CanLight,
CanIndicatorLight, CanAudioPlay - so a device can only be asked to do what it
has been measured to do. The editor filters on this and the save path
re-checks it, because the request is not to be trusted.
Execution goes straight to the target's zmcontrol socket rather than forking
zmcontrol.pl per action: the daemon already accepts a line of JSON there, and
it is the same path the control panel uses. ActionCommandName maps the DB
enum onto a method name as a whitelist, so nothing out of the database
reaches the control daemon uninspected. Actions are fire-and-forget - a
speaker that is offline is logged and skipped, never allowed to hold up event
handling.
Alarm actions fire only on the genuine entry into alarm, not on the
ALERT->ALARM re-entry, which would re-sound a speaker within one incident.
EventEnd runs on the calling thread before the event is handed to the closing
thread, which does not capture `this`.
Manual actions appear as buttons on the watch page, and are the reason the
control panel is now shown for a monitor that has actions but no control of
its own. Firing one sends only the action id; the command and target are
rebuilt server side, and Control rights are required on the target device and
not merely on the monitor the action hangs off.
Also add --file to zmcontrol.pl, without which audioPlay could not be driven
from the command line. The web path was unaffected as it bypasses GetOptions.
Tests: tests/zm_monitor_action.cpp covers the command whitelist, the message
format (including that file id 0 is a real id and that a stale AudioFile is
never passed to a command that takes none), trigger names, and that every
value of the ActionType enum maps to a command.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvTCzCbvGt8xKQRNCSA7o8
(cherry picked from commit b561341e988af69c3a46db854da410309f7cfa82)
380 lines
15 KiB
PHP
380 lines
15 KiB
PHP
<?php
|
|
//
|
|
// ZoneMinder web control function library, $Date$, $Revision$
|
|
// Copyright (C) 2001-2008 Philip Coombes
|
|
//
|
|
// This program is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU General Public License
|
|
// as published by the Free Software Foundation; either version 2
|
|
// of the License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program; if not, write to the Free Software
|
|
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
//
|
|
|
|
|
|
function controlFocus($monitor, $cmds) {
|
|
$control = $monitor->Control();
|
|
$cmd = $control->CanFocusCon() ? 'data-on-mousedown="controlCmd" data-on-mouseup="controlCmd"' : 'data-on-click="controlCmd"';
|
|
ob_start();
|
|
?>
|
|
<div class="arrowControl focusControls">
|
|
<div class="arrowLabel"><?php echo translate('Near') ?></div>
|
|
<button type="button" class="longArrowBtn upBtn" value="<?php echo $cmds['FocusNear'] ?>" <?php echo $cmd ?> data-xtell="0" data-ytell="-1"></button>
|
|
<button type="button" class="arrowCenter"<?php if ( $control->CanFocusCon() ) { ?> data-on-click="controlCmd" value="<?php echo $cmds['FocusStop'] ?>"<?php } ?>><?php echo translate('Focus') ?></button>
|
|
<button type="button" class="longArrowBtn downBtn" value="<?php echo $cmds['FocusFar'] ?>" <?php echo $cmd ?> data-xtell="0" data-ytell="1"></button>
|
|
<div class="arrowLabel"><?php echo translate('Far') ?></div>
|
|
<?php
|
|
if ( $control->CanAutoFocus() ) {
|
|
?>
|
|
<button type="button" class="ptzTextBtn" value="<?php echo $cmds['FocusAuto'] ?>" data-on-click="controlCmd"><?php echo translate('Auto') ?></button>
|
|
<button type="button" class="ptzTextBtn" value="<?php echo $cmds['FocusMan'] ?>" data-on-click="controlCmd"><?php echo translate('Man') ?></button>
|
|
<?php
|
|
}
|
|
?>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
function controlZoom($monitor, $cmds) {
|
|
$control = $monitor->Control();
|
|
$cmd = $control->CanZoomCon() ? 'data-on-mousedown="controlCmd" data-on-mouseup="controlCmd"' : 'data-on-click="controlCmd"';
|
|
ob_start();
|
|
?>
|
|
<div class="arrowControl zoomControls">
|
|
<div class="arrowLabel"><?php echo translate('Tele') ?></div>
|
|
<button type="button" class="longArrowBtn upBtn" <?php echo $cmd ?> value="<?php echo $cmds['ZoomTele'] ?>" data-xtell="0" data-ytell="-1"></button>
|
|
<button type="button" class="arrowCenter"<?php if ( $control->CanZoomCon() ) { ?> data-on-click="controlCmd" value="<?php echo $cmds['ZoomStop'] ?>"<?php } ?>><?php echo translate('Zoom') ?></button>
|
|
<button type="button" class="longArrowBtn downBtn" <?php echo $cmd ?> value="<?php echo $cmds['ZoomWide'] ?>" data-xtell="0" data-ytell="1"></button>
|
|
<div class="arrowLabel"><?php echo translate('Wide') ?></div>
|
|
<?php
|
|
if ( $control->CanAutoZoom() ) {
|
|
?>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['ZoomAuto'] ?>"><?php echo translate('Auto') ?></button>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['ZoomMan'] ?>"><?php echo translate('Man') ?></button>
|
|
<?php
|
|
}
|
|
?>
|
|
</div><?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
function controlIris($monitor, $cmds) {
|
|
$control = $monitor->Control();
|
|
$cmd = $control->CanIrisCon() ? 'data-on-mousedown="controlCmd" data-on-mouseup="controlCmd"' : 'data-on-click="controlCmd"';
|
|
ob_start();
|
|
?>
|
|
<div class="arrowControl irisControls">
|
|
<div class="arrowLabel"><?php echo translate('Open') ?></div>
|
|
<button type="button" class="longArrowBtn upBtn" value="<?php echo $cmds['IrisOpen'] ?>" <?php echo $cmd ?> data-xtell="0" data-ytell="-1"></button>
|
|
<button type="button" class="arrowCenter"<?php if ( $control->CanIrisCon() ) { ?> data-on-click="controlCmd" value="<?php echo $cmds['IrisStop'] ?>"<?php } ?>><?php echo translate('Iris') ?></button>
|
|
<button type="button" class="longArrowBtn downBtn" value="<?php echo $cmds['IrisClose'] ?>" <?php echo $cmd ?> data-xtell="0" data-ytell="1"></button>
|
|
<div class="arrowLabel"><?php echo translate('Close') ?></div>
|
|
<?php
|
|
if ( $control->CanAutoIris() ) {
|
|
?>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['IrisAuto'] ?>"><?php echo translate('Auto') ?></button>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['IrisMan'] ?>"><?php echo translate('Man') ?></button>
|
|
<?php
|
|
}
|
|
?>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
function controlWhite($monitor, $cmds) {
|
|
$control = $monitor->Control();
|
|
$cmd = $control->CanWhiteCon() ? 'data-on-mousedown="controlCmd" data-on-mouseup="controlCmd"' : 'data-on-click="controlCmd"';
|
|
ob_start();
|
|
?>
|
|
<div class="arrowControl whiteControls">
|
|
<div class="arrowLabel"><?php echo translate('In') ?></div>
|
|
<button type="button" class="longArrowBtn upBtn" value="<?php echo $cmds['WhiteIn'] ?>" <?php echo $cmd ?> data-xtell="0" data-ytell="-1"></button>
|
|
<button type="button" class="arrowCenter"<?php if ( $control->CanWhiteCon() ) { ?> data-on-click="controlCmd" value="<?php echo $cmds['WhiteStop'] ?>"<?php } ?>><?php echo translate('White') ?></button>
|
|
<button type="button" class="longArrowBtn downBtn" value="<?php echo $cmds['WhiteOut'] ?>" <?php echo $cmd ?> data-xtell="0" data-ytell="1)"></button>
|
|
<div class="arrowLabel"><?php echo translate('Out') ?></div>
|
|
<?php
|
|
if ( $control->CanAutoWhite() ) {
|
|
?>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['WhiteAuto'] ?>"><?php echo translate('Auto') ?></button>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['WhiteMan'] ?>"><?php echo translate('Man') ?></button>
|
|
<?php
|
|
}
|
|
?>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
function controlLight($monitor, $cmds) {
|
|
ob_start();
|
|
?>
|
|
<div class="lightControls">
|
|
<button type="button" class="ptzTextBtn lightToggleBtn" data-on-click="controlCmd" data-on-cmd="<?php echo $cmds['LightOn'] ?>" data-off-cmd="<?php echo $cmds['LightOff'] ?>" value="<?php echo $cmds['LightOn'] ?>"><?php echo translate('Light') ?></button>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
function controlIndicatorLight($monitor, $cmds) {
|
|
ob_start();
|
|
?>
|
|
<div class="lightControls">
|
|
<button type="button" class="ptzTextBtn indicatorLightToggleBtn" data-on-click="controlCmd" data-on-cmd="<?php echo $cmds['IndicatorLightOn'] ?>" data-off-cmd="<?php echo $cmds['IndicatorLightOff'] ?>" value="<?php echo $cmds['IndicatorLightOn'] ?>"><?php echo translate('Indicator') ?></button>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
function controlAudio($monitor, $cmds) {
|
|
$control = $monitor->Control();
|
|
|
|
// One button per sound the device holds, numbered by the file id the
|
|
// firmware expects, the same way presets are addressed. The id is carried
|
|
// in the command name (audioPlay12) because the control request has no
|
|
// general way to attach a parameter to a button.
|
|
$min = $control->MinAudioFile();
|
|
$max = $control->MaxAudioFile();
|
|
|
|
ob_start();
|
|
?>
|
|
<div class="audioControls">
|
|
<?php
|
|
if ( $control->CanAudioPlay() ) {
|
|
if ( is_numeric($min) && is_numeric($max) && $min <= $max ) {
|
|
?>
|
|
<div>
|
|
<?php
|
|
for ( $i = $min; $i <= $max; $i++ ) {
|
|
?>
|
|
<button type="button" class="ptzNumBtn" data-on-click="controlCmd" value="<?php echo $cmds['AudioPlay'].$i ?>"><?php echo $i ?></button>
|
|
<?php
|
|
} // end foreach sound
|
|
?>
|
|
</div>
|
|
<?php
|
|
} // end if a file range is configured
|
|
?>
|
|
<div>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['AudioStop'] ?>"><?php echo translate('AudioStop') ?></button>
|
|
</div>
|
|
<?php
|
|
} // end if CanAudioPlay
|
|
|
|
if ( $control->CanAudioVolume() ) {
|
|
?>
|
|
<div>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['AudioVolumeDown'] ?>">-</button>
|
|
<span class="audioVolumeLabel"><?php echo translate('AudioVolume') ?></span>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['AudioVolumeUp'] ?>">+</button>
|
|
</div>
|
|
<?php
|
|
} // end if CanAudioVolume
|
|
?>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
function controlPanTilt($monitor, $cmds) {
|
|
$control = $monitor->Control();
|
|
ob_start();
|
|
?>
|
|
<div class="pantiltControls">
|
|
<div class="pantiltLabel"><?php echo translate('PanTilt') ?></div>
|
|
<div class="pantiltButtons">
|
|
<?php
|
|
$hasPan = $control->CanPan();
|
|
$hasTilt = $control->CanTilt();
|
|
$hasDiag = $hasPan && $hasTilt && $control->CanMoveDiag();
|
|
$cmd = $control->CanMoveCon() ? 'data-on-mousedown="controlCmd" data-on-mouseup="controlCmd" ' : 'data-on-click="controlCmd" ';
|
|
?>
|
|
<button type="button" class="arrowBtn upLeftBtn<?php echo $hasDiag?'':' invisible' ?>" <?php echo $cmd ?> value="<?php echo $cmds['MoveUpLeft'] ?>" data-xtell="-1" data-ytell="-1"></button>
|
|
<button type="button" class="arrowBtn upBtn<?php echo $hasTilt?'':' invisible' ?>" <?php echo $cmd ?> value="<?php echo $cmds['MoveUp'] ?>" data-xtell="0" data-ytell="-1"></button>
|
|
<button type="button" class="arrowBtn upRightBtn<?php echo $hasDiag?'':' invisible' ?>" <?php echo $cmd ?> value="<?php echo $cmds['MoveUpRight'] ?>" data-xtell="1" data-ytell="-1"></button>
|
|
<button type="button" class="arrowBtn leftBtn<?php echo $hasPan?'':' invisible' ?>" <?php echo $cmd ?> value="<?php echo $cmds['MoveLeft'] ?>" data-xtell="-1" data-ytell="0"></button>
|
|
<?php if ( isset($cmds['Center']) ) { ?>
|
|
<button type="button" class="arrowBtn centerBtn" <?php echo $cmd ?> value="<?php echo $cmds['Center'] ?>"></button>
|
|
<?php } else { ?>
|
|
<button type="button" class="arrowBtn NocenterBtn"></button>
|
|
<?php } ?>
|
|
<button type="button" class="arrowBtn rightBtn<?php echo $hasPan?'':' invisible' ?>" <?php echo $cmd ?> value="<?php echo $cmds['MoveRight'] ?>" data-xtell="1" data-ytell="0"></button>
|
|
<button type="button" class="arrowBtn downLeftBtn<?php echo $hasDiag?'':' invisible' ?>" <?php echo $cmd ?> value="<?php echo $cmds['MoveDownLeft'] ?>" data-xtell="-1" data-ytell="1"></button>
|
|
<button type="button" class="arrowBtn downBtn<?php echo $hasTilt?'':' invisible' ?>" <?php echo $cmd ?> value="<?php echo $cmds['MoveDown'] ?>" data-xtell="0" data-ytell="1"></button>
|
|
<button type="button" class="arrowBtn downRightBtn<?php echo $hasDiag?'':' invisible' ?>" <?php echo $cmd ?> value="<?php echo $cmds['MoveDownRight'] ?>" data-xtell="1" data-ytell="1"></button>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
function controlPresets($monitor, $cmds) {
|
|
$control = $monitor->Control();
|
|
// MAX_PRESETS IS PER LINE
|
|
$max_presets = 12;
|
|
|
|
$sql = 'SELECT * FROM ControlPresets WHERE MonitorId = ?';
|
|
$labels = array();
|
|
foreach ( dbFetchAll($sql, NULL, array($monitor->Id())) as $row ) {
|
|
$labels[$row['Preset']] = $row['Label'];
|
|
}
|
|
|
|
$presetBreak = (int)(($control->NumPresets()+1)/((int)(($control->NumPresets()-1)/$max_presets)+1));
|
|
|
|
ob_start();
|
|
?>
|
|
<div class="presetControls">
|
|
<!--<div><?php echo translate('Presets') ?></div>-->
|
|
<div>
|
|
<?php
|
|
for ( $i = 1; $i <= $control->NumPresets(); $i++ ) {
|
|
?>
|
|
<button type="button" class="ptzNumBtn" title="<?php echo isset($labels[$i])?htmlentities($labels[$i]):'' ?>" data-on-click="controlCmd" value="<?php echo $cmds['PresetGoto'].$i ?>"/><?php echo $i ?></button>
|
|
<?php
|
|
} // end foreach preset
|
|
?>
|
|
</div>
|
|
<div>
|
|
<?php
|
|
if ( $control->HasHomePreset() ) {
|
|
?>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['PresetHome'] ?>"><?php echo translate('Home') ?></button>
|
|
<?php
|
|
}
|
|
if ( canEdit('Monitors') && $control->CanSetPresets() ) {
|
|
?>
|
|
<button type="button" class="ptzTextBtn" value="Set" data-on-click="controlSetClicked"><?php echo translate('Set') ?></button>
|
|
<?php
|
|
}
|
|
?>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
function controlPower($monitor, $cmds) {
|
|
$control = $monitor->Control();
|
|
ob_start();
|
|
?>
|
|
<div class="powerControls">
|
|
<div class="powerLabel"><?php echo translate('Control') ?></div>
|
|
<div>
|
|
<?php
|
|
if ( $control->CanWake() ) {
|
|
?>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['Wake'] ?>"><?php echo translate('Wake') ?></button>
|
|
<?php
|
|
}
|
|
if ( $control->CanSleep() ) {
|
|
?>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['Sleep'] ?>"><?php echo translate('Sleep') ?></button>
|
|
<?php
|
|
}
|
|
if ( $control->CanReset() ) {
|
|
?>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['Reset'] ?>"><?php echo translate('Reset') ?></button>
|
|
<?php
|
|
}
|
|
if ( $control->CanReboot() ) {
|
|
?>
|
|
<button type="button" class="ptzTextBtn" data-on-click="controlCmd" value="<?php echo $cmds['Reboot'] ?>"><?php echo translate('Reboot') ?></button>
|
|
<?php
|
|
}
|
|
?>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
// Buttons for this monitor's manually-triggered actions. These drive other
|
|
// monitors - usually a speaker - so they are shown even when this monitor has
|
|
// no control of its own.
|
|
function monitorActionControls($monitor) {
|
|
require_once('includes/MonitorAction.php');
|
|
|
|
$actions = ZM\MonitorAction::find(
|
|
array('MonitorId'=>$monitor->Id(), 'TriggerOn'=>'Manual', 'Enabled'=>1),
|
|
array('order'=>'`Sequence`,`Id`'));
|
|
if ( !count($actions) )
|
|
return '';
|
|
|
|
ob_start();
|
|
?>
|
|
<div class="monitorActionControls">
|
|
<?php
|
|
foreach ( $actions as $action ) {
|
|
$target = $action->TargetMonitor();
|
|
if ( !$target or !canView('Control', $target->Id()) )
|
|
continue;
|
|
$label = $action->Label() ? $action->Label() :
|
|
translate('Action'.$action->ActionType()).' - '.$target->Name();
|
|
?>
|
|
<button type="button" class="ptzTextBtn monitorActionBtn" data-on-click-this="fireMonitorAction" data-action-id="<?php echo $action->Id() ?>" title="<?php echo validHtmlStr($label) ?>"><?php echo validHtmlStr($label) ?></button>
|
|
<?php
|
|
} // end foreach action
|
|
?>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
function ptzControls($monitor) {
|
|
$control = $monitor->Control();
|
|
$actionControls = monitorActionControls($monitor);
|
|
|
|
// A monitor with manual actions but no control of its own still gets a
|
|
// panel; there is simply nothing but the action buttons in it.
|
|
if ( !$control or !$control->Id() ) {
|
|
if ( $actionControls === '' )
|
|
return '';
|
|
return '<div class="controlsPanel">'.$actionControls.'</div>';
|
|
}
|
|
|
|
$cmds = $control->commands();
|
|
ob_start();
|
|
?>
|
|
<div class="controlsPanel">
|
|
<?php
|
|
echo $actionControls;
|
|
if ( $control->CanFocus() )
|
|
echo controlFocus($monitor, $cmds);
|
|
if ( $control->CanZoom() )
|
|
echo controlZoom($monitor, $cmds);
|
|
if ( $control->CanIris() )
|
|
echo controlIris($monitor, $cmds);
|
|
if ( $control->CanWhite() )
|
|
echo controlWhite($monitor, $cmds);
|
|
if ( $control->CanLight() )
|
|
echo controlLight($monitor, $cmds);
|
|
if ( $control->CanIndicatorLight() )
|
|
echo controlIndicatorLight($monitor, $cmds);
|
|
if ( $control->CanAudioPlay() || $control->CanAudioVolume() )
|
|
echo controlAudio($monitor, $cmds);
|
|
if ( $control->CanMove() ) {
|
|
?>
|
|
<div class="pantiltPanel">
|
|
<?php echo controlPanTilt($monitor, $cmds); ?>
|
|
</div>
|
|
<?php
|
|
}
|
|
if ( $control->CanWake() || $control->CanSleep() || $control->CanReset() || $control->CanReboot() )
|
|
echo controlPower($monitor, $cmds);
|
|
if ( $control->HasPresets() )
|
|
echo controlPresets($monitor, $cmds);
|
|
?>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
?>
|