Files
zoneminder/db/zm_update-1.39.32.sql
T
Isaac ConnorandClaude Opus 5 210d10da1d feat: add ZoneMinder::Control::AMLink for the AMLINK AL5M white light
These cameras have a white light that ONVIF does not expose at all: imaging
offers only Brightness/ColorSaturation/Contrast/Sharpness, GetRelayOutputs is
empty with RelayOutputs="0", and there is no PTZ service and so no auxiliary
commands. The light lives behind a vendor JSON-RPC tunnel.

The vocabulary turns out to be Dahua's -- CoaxialControlIO for the light,
magicBox for reboot -- so this subclasses Dahua_RPC and replaces only open,
rpc_call and login. lightOn, lightOff, lightStatus and reboot are inherited
unchanged, and Dahua_RPC's lightStatus already parses the
params.status.WhiteLight these cameras return.

The transport is the whole of the work, and none of it is discoverable by
probing:

  * The endpoint is /Onvif/device_service with a capital O. The lowercase
    /onvif/device_service is the real ONVIF SOAP service; the capitalised
    spelling is a separate vendor tunnel sharing the path. /RPC2, /RPC3,
    /OutsideCmd and every /cgi-bin/* return 404 on this model, which is why
    the API looked absent.
  * Requests are <body><cmdType>T</cmdType><cmd>P</cmd></body>. Without
    cmdType the camera drops the connection rather than returning an error.
  * P is base64 of the JSON-RPC object, XOR-masked with a session key once one
    exists. Masking is mandatory: an unmasked Request is refused even on a
    freshly authenticated session, so the key exchange is not optional.
  * That key needs a three step bootstrap -- log in, fetch the device RSA
    public key over the unauthenticated OutsideCmd channel, then send an AES
    key encrypted under it and decrypt the mask key from the reply. The salt
    doubles as the AES key and must be 32 decimal digits; 16 is accepted by
    the AES step and then rejected by getGeneralKey.

IO polarity matches Dahua_RPC: IO 1 is on, IO 2 is off. Worth stating because
the camera's own web UI is a toggle that does not reveal which is which, and
because a floodlight cannot be verified optically in daylight -- an RTSP
brightness comparison appeared to say the opposite and was exposure drift.
CoaxialControlIO.getStatus is the reliable witness.

Tested: 26 assertions over the pure transport helpers -- mask symmetry and
cycling, NUL bytes surviving the mask, the envelope, stripping the CRLF the
device pads its replies with, and the channel routing, which fails silently on
the wire if wrong. Verified live against both units: open negotiates a 32 byte
mask key and lightStatus reports Off -> On -> Off around lightOn/lightOff. Perl
suite 15 files / 249 assertions pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvTCzCbvGt8xKQRNCSA7o8
2026-09-11 20:13:02 -05:00

27 lines
1.1 KiB
SQL

--
-- Add a Controls entry for the AMLINK AL5M white light.
--
-- These cameras carry a white light that is reachable only over a vendor
-- JSON-RPC tunnel at /Onvif/device_service -- note the capital O, which is a
-- different endpoint from the ONVIF SOAP service at /onvif/device_service.
-- ONVIF itself exposes no light on this hardware: imaging offers only
-- Brightness/ColorSaturation/Contrast/Sharpness, RelayOutputs is 0, and there
-- is no PTZ service and so no auxiliary commands.
--
-- The camera speaks Dahua's RPC vocabulary, so ZoneMinder::Control::AMLink
-- subclasses Dahua_RPC and replaces only the transport. CanReboot comes along
-- with that inheritance. Nothing else is set: there is no PTZ, and the ONVIF
-- imaging controls stay with the ONVIF protocol module.
--
SELECT 'Checking for the AMLINK AL5M Controls entry';
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM `Controls` WHERE `Protocol` = 'AMLink') > 0,
"SELECT 'AMLINK AL5M control already exists'",
"INSERT INTO `Controls` (`Name`,`Type`,`Protocol`,`CanReboot`,`CanLight`) VALUES ('AMLINK AL5M (light)','Ffmpeg','AMLink',1,1)"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;