mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-11-07 21:23:02 -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)
61 lines
2.2 KiB
SQL
61 lines
2.2 KiB
SQL
--
|
|
-- Add Monitors.DeviceClass and the MonitorActions table.
|
|
--
|
|
-- A speaker is a monitor: it has an address, credentials, a stream and a
|
|
-- Controls entry, and everything ZoneMinder already knows how to do with a
|
|
-- monitor applies to it. What it is not is a camera, and the console, the
|
|
-- montage and the action editor all need to tell the difference. DeviceClass
|
|
-- carries that distinction without disturbing Type, which selects the capture
|
|
-- backend: an IP speaker still captures over Ffmpeg like any other RTSP
|
|
-- device.
|
|
--
|
|
-- MonitorActions records what a monitor does when it alarms. The device
|
|
-- acted on is identified by TargetMonitorId and is frequently NOT the monitor
|
|
-- that triggered - the point of the feature is that a camera can sound a
|
|
-- speaker somewhere else.
|
|
--
|
|
|
|
SELECT 'Checking for DeviceClass in Monitors';
|
|
SET @s = (SELECT IF(
|
|
(SELECT COUNT(*)
|
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
WHERE table_name = 'Monitors'
|
|
AND table_schema = DATABASE()
|
|
AND column_name = 'DeviceClass'
|
|
) > 0,
|
|
"SELECT 'Column DeviceClass already exists in Monitors'",
|
|
"ALTER TABLE `Monitors` ADD COLUMN `DeviceClass` enum('Camera','Speaker') NOT NULL default 'Camera' AFTER `Type`"
|
|
));
|
|
|
|
PREPARE stmt FROM @s;
|
|
EXECUTE stmt;
|
|
DEALLOCATE PREPARE stmt;
|
|
|
|
SELECT 'Checking for the MonitorActions table';
|
|
SET @s = (SELECT IF(
|
|
(SELECT COUNT(*)
|
|
FROM INFORMATION_SCHEMA.TABLES
|
|
WHERE table_name = 'MonitorActions'
|
|
AND table_schema = DATABASE()
|
|
) > 0,
|
|
"SELECT 'Table MonitorActions already exists'",
|
|
"CREATE TABLE `MonitorActions` (
|
|
`Id` int(10) unsigned NOT NULL auto_increment,
|
|
`MonitorId` int(10) unsigned NOT NULL,
|
|
`TriggerOn` enum('EventStart','EventEnd','Alarm','Manual') NOT NULL default 'EventStart',
|
|
`ActionType` enum('LightOn','LightOff','IndicatorLightOn','IndicatorLightOff','AudioPlay','AudioStop') NOT NULL default 'AudioPlay',
|
|
`TargetMonitorId` int(10) unsigned NOT NULL,
|
|
`AudioFile` int(10) unsigned default NULL,
|
|
`Label` varchar(64) NOT NULL default '',
|
|
`Enabled` tinyint(1) unsigned NOT NULL default '1',
|
|
`Sequence` smallint(5) unsigned NOT NULL default '0',
|
|
PRIMARY KEY (`Id`),
|
|
KEY `MonitorId_TriggerOn` (`MonitorId`,`TriggerOn`),
|
|
KEY `TargetMonitorId` (`TargetMonitorId`)
|
|
)"
|
|
));
|
|
|
|
PREPARE stmt FROM @s;
|
|
EXECUTE stmt;
|
|
DEALLOCATE PREPARE stmt;
|