Next gate
Gates are operated by District IT. Current role can review the package but not advance it.
- CHANGELOG.md321 B
- README.md3,041 B
- config/config.example.json425 B
- config/config.schema.json1,771 B
- config/health-probes.json901 B
- fixtures/events.jsonl1,193 B
- fixtures/expected.json173 B
- manifest.json2,657 B
- mapping/event-map.json1,648 B
- src/Connector.php6,149 B
- src/Transport.php2,855 B
- tests/SandboxTest.php2,037 B
- SHA256SUMS
src/Connector.php
sha256 495487afd68ab9295af0…1<?php 2declare(strict_types=1); 3 4namespace Msad17\Connectors\OnvifProfileS\B2610051533154; 5 6/** 7 * ONVIF Profile S/T — cameras & encoders 8 * Connector build PK-261005-004 · v0.1.4 · generated 2026-10-05 15:33:15 EDT 9 * 10 * Generated by Integration Bot for the MSAD 17 Security Platform. 11 * Owner after promotion: District IT. Developer support after promotion: none. 12 * 13 * MODE: READ-ONLY. No write path (unlock, schedule, credential, PTZ) is compiled into 14 * this artifact. Write capability requires a separate, dual-approved package. 15 */ 16final class Connector 17{ 18 public const STACK = 'onvif_profile_s'; 19 public const VERSION = '0.1.4'; 20 public const MODE = 'read_only'; 21 public const SCHEMA = 'msad17.event/1'; 22 23 /** Vendor event key => [normalized type, priority]. Source of truth: mapping/event-map.json */ 24 public const EVENT_MAP = [ 25 'tns1:VideoSource/SignalLoss' => ['video.loss', 'p2'], 26 'tns1:VideoSource/GlobalSceneChange/ImagingService' => ['video.tamper', 'p2'], 27 'tns1:RuleEngine/CellMotionDetector/Motion' => ['video.motion', 'info'], 28 'tns1:Device/Trigger/DigitalInput' => ['io.duress', 'p1'], 29 'tns1:VideoSource/MotionAlarm' => ['video.motion', 'info'], 30 'tns1:Device/HardwareFailure/StorageFailure' => ['recorder.retention', 'p3'], 31 'tns1:RuleEngine/TamperDetector/Tamper' => ['video.tamper', 'p2'], 32 ]; 33 34 /** Dot-paths into the vendor payload. */ 35 public const PATHS = [ 36 'type' => 'Topic', 37 'time' => 'UtcTime', 38 'device' => 'Source.Device', 39 'state' => 'Data.State', 40 ]; 41 42 /** Endpoints this connector is allowed to call (enforced by sandbox + egress policy). */ 43 public const ENDPOINTS = [ 44 ['POST', '/onvif/device_service'], 45 ['POST', '/onvif/media_service'], 46 ['POST', '/onvif/event_service'], 47 ]; 48 49 /** @param array<string,mixed> $config validated against config/config.schema.json */ 50 public function __construct(private readonly Transport $transport, private readonly array $config) 51 { 52 foreach (['base_url', 'site_id'] as $k) { 53 if (empty($config[$k])) { 54 throw new \InvalidArgumentException("config.{$k} is required"); 55 } 56 } 57 } 58 59 /** Liveness + latency probe. Called by the platform health monitor every poll interval. */ 60 public function probe(): array 61 { 62 $t0 = microtime(true); 63 $res = $this->transport->request('POST', '/onvif/device_service', ['op' => 'GetSystemDateAndTime']); 64 return [ 65 'ok' => $res['status'] >= 200 && $res['status'] < 300, 66 'status' => $res['status'], 67 'latency_ms' => (int)round((microtime(true) - $t0) * 1000), 68 'stack' => self::STACK, 69 'mode' => self::MODE, 70 ]; 71 } 72 73 /** 74 * Pull and normalize pending vendor events. 75 * @return array{events: list<array<string,mixed>>, unmapped: int, unmapped_keys: list<string>} 76 */ 77 public function pullEvents(): array 78 { 79 $res = $this->transport->request('POST', '/onvif/event_service', ['op' => 'PullMessages', 'Timeout' => 'PT5S', 'MessageLimit' => 100]); 80 $out = []; 81 $unmapped = []; 82 foreach (($res['events'] ?? []) as $raw) { 83 $n = is_array($raw) ? $this->normalize($raw) : null; 84 if ($n === null) { 85 $unmapped[] = is_array($raw) ? (string)(self::get($raw, self::PATHS['type']) ?? '?') : '?'; 86 continue; 87 } 88 $out[] = $n; 89 } 90 return ['events' => $out, 'unmapped' => count($unmapped), 'unmapped_keys' => array_values(array_unique($unmapped))]; 91 } 92 93 /** Map one vendor payload to the platform event schema; null = not mapped (logged, never dropped silently). */ 94 public function normalize(array $raw): ?array 95 { 96 $key = self::get($raw, self::PATHS['type']); 97 if (!is_string($key) || !isset(self::EVENT_MAP[$key])) { 98 return null; 99 } 100 [$type, $priority] = self::EVENT_MAP[$key]; 101 $state = self::PATHS['state'] !== null ? self::get($raw, self::PATHS['state']) : null; 102 $rtn = is_scalar($state) && in_array(strtolower((string)$state), ['false', 'inactive', '0', 'normal'], true); 103 $vendorDevice = (string)(self::get($raw, self::PATHS['device']) ?? 'unknown'); 104 105 return [ 106 'schema' => self::SCHEMA, 107 'type' => $type, 108 'priority' => $priority, 109 'rtn' => $rtn, 110 'site_id' => (string)$this->config['site_id'], 111 'device_id' => (string)($this->config['device_map'][$vendorDevice] ?? $vendorDevice), 112 'vendor_device' => $vendorDevice, 113 'vendor_key' => $key, 114 'occurred_at' => self::utc(self::get($raw, self::PATHS['time'])), 115 'source' => self::STACK . '@' . (string)parse_url((string)$this->config['base_url'], PHP_URL_HOST), 116 'raw_sha256' => hash('sha256', (string)json_encode($raw)), 117 ]; 118 } 119 120 /** Credential-free live stream locator; the platform injects auth at session time. */ 121 public function liveUri(string $deviceId): ?string 122 { 123 $tpl = 'rtsp://{host}:554/onvif-media/media.amp?profile={device}'; 124 return $tpl === null ? null : str_replace('{device}', rawurlencode($deviceId), $tpl); 125 } 126 127 private static function get(mixed $a, ?string $path): mixed 128 { 129 if ($path === null || $path === '') { 130 return null; 131 } 132 foreach (explode('.', $path) as $seg) { 133 if (!is_array($a) || !array_key_exists($seg, $a)) { 134 return null; 135 } 136 $a = $a[$seg]; 137 } 138 return $a; 139 } 140 141 private static function utc(mixed $v): string 142 { 143 if (is_int($v) || is_float($v) || (is_string($v) && ctype_digit($v))) { 144 $n = (int)$v; 145 return gmdate('Y-m-d\TH:i:s\Z', $n > 20000000000 ? intdiv($n, 1000) : $n); 146 } 147 if (is_string($v) && ($t = strtotime($v)) !== false) { 148 return gmdate('Y-m-d\TH:i:s\Z', $t); 149 } 150 return gmdate('Y-m-d\TH:i:s\Z'); 151 } 152}