mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-09-13 13:49:22 -04: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)
116 lines
3.6 KiB
PHP
116 lines
3.6 KiB
PHP
<?php
|
|
namespace ZM;
|
|
|
|
require_once('database.php');
|
|
require_once('Object.php');
|
|
require_once('Control.php');
|
|
require_once('Monitor.php');
|
|
|
|
class MonitorAction extends ZM_Object {
|
|
protected static $table = 'MonitorActions';
|
|
|
|
protected $defaults = array(
|
|
'Id' => null,
|
|
'MonitorId' => 0,
|
|
'TriggerOn' => 'EventStart',
|
|
'ActionType' => 'AudioPlay',
|
|
'TargetMonitorId' => 0,
|
|
'AudioFile' => null,
|
|
'Label' => '',
|
|
'Enabled' => 1,
|
|
'Sequence' => 0,
|
|
);
|
|
|
|
public static function find($parameters = array(), $options = array()) {
|
|
return ZM_Object::_find(self::class, $parameters, $options);
|
|
}
|
|
|
|
public static function find_one($parameters = array(), $options = array()) {
|
|
return ZM_Object::_find_one(self::class, $parameters, $options);
|
|
}
|
|
|
|
// The zmcontrol command an action type maps onto. Mirrors
|
|
// Monitor::ActionCommandName in src/zm_monitor.cpp; keep the two in step.
|
|
public static function commandFor($action_type) {
|
|
$commands = array(
|
|
'LightOn' => 'lightOn',
|
|
'LightOff' => 'lightOff',
|
|
'IndicatorLightOn' => 'indicatorLightOn',
|
|
'IndicatorLightOff' => 'indicatorLightOff',
|
|
'AudioPlay' => 'audioPlay',
|
|
'AudioStop' => 'audioStop',
|
|
);
|
|
return isset($commands[$action_type]) ? $commands[$action_type] : null;
|
|
}
|
|
|
|
// Which action types a given monitor can actually perform, decided by its
|
|
// Controls row rather than assumed. A monitor with no control, or one whose
|
|
// capabilities were measured as absent, offers nothing.
|
|
public static function typesForMonitor($monitor) {
|
|
$types = array();
|
|
if (!$monitor or !$monitor->Controllable() or !$monitor->ControlId())
|
|
return $types;
|
|
|
|
$control = $monitor->Control();
|
|
if (!$control or !$control->Id())
|
|
return $types;
|
|
|
|
if ($control->CanLight()) {
|
|
$types[] = 'LightOn';
|
|
$types[] = 'LightOff';
|
|
}
|
|
if ($control->CanIndicatorLight()) {
|
|
$types[] = 'IndicatorLightOn';
|
|
$types[] = 'IndicatorLightOff';
|
|
}
|
|
if ($control->CanAudioPlay()) {
|
|
$types[] = 'AudioPlay';
|
|
$types[] = 'AudioStop';
|
|
}
|
|
return $types;
|
|
}
|
|
|
|
// Monitors that can be the target of an action, with the capabilities the
|
|
// editor needs to constrain its inputs.
|
|
public static function targetCandidates() {
|
|
$candidates = array();
|
|
foreach (Monitor::find(array('Deleted' => 0)) as $monitor) {
|
|
$types = self::typesForMonitor($monitor);
|
|
if (!count($types))
|
|
continue;
|
|
$control = $monitor->Control();
|
|
$candidates[] = array(
|
|
'Id' => $monitor->Id(),
|
|
'Name' => $monitor->Name(),
|
|
'DeviceClass' => $monitor->DeviceClass(),
|
|
'Types' => $types,
|
|
'MinAudioFile' => $control->MinAudioFile(),
|
|
'MaxAudioFile' => $control->MaxAudioFile(),
|
|
);
|
|
}
|
|
return $candidates;
|
|
}
|
|
|
|
public function Monitor() {
|
|
return Monitor::find_one(array('Id' => $this->{'MonitorId'}));
|
|
}
|
|
|
|
public function TargetMonitor() {
|
|
return Monitor::find_one(array('Id' => $this->{'TargetMonitorId'}));
|
|
}
|
|
|
|
// Build the zmcontrol option string this action sends, in the same form
|
|
// buildControlCommand produces for the control panel.
|
|
public function controlCommand() {
|
|
$command = self::commandFor($this->{'ActionType'});
|
|
if (!$command)
|
|
return null;
|
|
|
|
$options = '';
|
|
if ($this->{'ActionType'} == 'AudioPlay' and $this->{'AudioFile'} !== null)
|
|
$options .= ' --file='.validInt($this->{'AudioFile'});
|
|
return $options.' --command='.$command;
|
|
}
|
|
}
|
|
?>
|